iBinx Corp.
contact usshisen-sho

Source listing: . Return to the main page

/* -*- C++ -*-
 *******************************************************************
 *
 *
 * Shisen-Sho for Windows
 *
 * frame.cpp - Implementation of the Frame class
 *
 *
 *******************************************************************
 *
 * A japanese game similar to mahjongg
 *
 *******************************************************************
 *
 * Created 2000-12-28 by Jim Mason <jmason@sirius.com>
 *
 * Game engine ported from Mario Weilguni's <mweilguni@sime.com>
 * original game for KDE, KSHISEN.  If you are interested in the
 * KDE version of the game, see http://www.kde.org/kdegames/
 *
 *******************************************************************
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 *******************************************************************
 */

#include "stdafx.h"
#include <math.h>
#include <commctrl.h>
#include "shisen.h"
#include "frame.h"
#include "registry.h"
#include "dialogs.h"

static int size_x[5] = {14, 18, 24, 26, 30};
static int size_y[5] = { 6,  8, 12, 14, 16};
static int DELAY[5] = {125, 250, 500, 750, 1000};


Frame::Frame() {
    m_nTimer = 0;

    // Load the cursors
    m_hWait = LoadCursor(NULL, IDC_WAIT);
    m_hArrow = LoadCursor(NULL, IDC_ARROW);

    // Cache this resource, as it is accessed often
    m_szStatus.LoadString(IDS_STATUS);
}

Frame::~Frame() {
}

BOOL Frame::PreCreateWindow(CREATESTRUCT& cs) {
    // Register the class if it does not exist
    WNDCLASSEX wcex;
    if(!GetClassInfoEx(App::GetHInstance(), cs.lpszClass, &wcex)) {
        wcex.cbSize         = sizeof(WNDCLASSEX); 
        wcex.style          = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc    = NULL;  // Window::RegisterClassEx will set this
        wcex.cbClsExtra     = 0;
        wcex.cbWndExtra     = 0;
        wcex.hInstance      = App::GetHInstance();
        wcex.hIcon          = LoadIcon(App::GetHInstance(), (LPCTSTR)IDR_MAINFRAME);
        wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
        wcex.lpszMenuName   = (LPCSTR)IDR_MAINFRAME;
        wcex.lpszClassName  = cs.lpszClass;
        wcex.hIconSm        = wcex.hIcon;
        return RegisterClassEx(&wcex);
    }
    return TRUE;
}

void Frame::BeginWaitCursor() {
    SetCursor(m_hWait);
}

void Frame::EndWaitCursor() {
    SetCursor(m_hArrow);
}

void Frame::UpdateScore() {
    CStdString s;
    int t = m_board.getTimeForGame();

    s.Format(m_szStatus, t/3600, (t/60)%60, t%60);
    SendMessage(m_hStatus, SB_SETTEXT, 0|SBT_NOBORDERS, (LPARAM)(LPCTSTR)s);
}

void Frame::SetCheatMode(BOOL bCheat) {
    CStdString szCheatMode;
    szCheatMode.LoadString(IDS_INDICATOR_CHEAT);
    SendMessage(m_hStatus, SB_SETTEXT, 1|SBT_NOBORDERS,
                (LPARAM)(LPCTSTR)(bCheat?szCheatMode:_T("")));
}

void Frame::LoadDefaults() {
    // We must load the initial tileset *before* we call
    // OnSizeChange/OnLevelChange, as otherwise, the
    // m_bUniqueSeasons flag may not be correct when the
    // tiles are laid out.
    CStdString szImage;
    szImage = App::GetProfile()->GetProfileString(_T("Tilesets"), _T("Tileset"), _T(""));
    m_board.loadMahjonggTileset(szImage);
    szImage = App::GetProfile()->GetProfileString(_T("Tilesets"), _T("Background"), _T(""));
    m_board.loadBackground(szImage);

    UINT nID = App::GetProfile()->GetProfileInt(_T("Options"), _T("Speed"), ID_OSPEED3);
    OnSpeedChange(nID);

    nID = App::GetProfile()->GetProfileInt(_T("Options"), _T("Size"), ID_OSIZE2);
    OnSizeChange(nID);

    nID = App::GetProfile()->GetProfileInt(_T("Options"), _T("Level"), ID_OLEVEL2);
    OnLevelChange(nID);

    BOOL b = App::GetProfile()->GetProfileInt(_T("Options"), _T("Solvable"), TRUE);
    m_board.setSolvableFlag(b);

    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);
    CheckMenuItem(hOptions, ID_OSOLVABLE, MF_BYCOMMAND|
                            (b?MF_CHECKED:MF_UNCHECKED));

    b = App::GetProfile()->GetProfileInt(_T("Options"), _T("Gravity"), FALSE);
    m_board.setGravityFlag(b);
    CheckMenuItem(hOptions, ID_OGRAVITY, MF_BYCOMMAND|
                            (b?MF_CHECKED:MF_UNCHECKED));

    ReadHighscore();
}

CStdString Frame::GetPlayerName() {
    PlayerName dlgName(this);
    if(dlgName.DoModal() == IDOK)
        return dlgName.m_szName;
    else
        return _T("");
}

int Frame::GetScore(HighScore &hs) {
    double ntiles = hs.x*hs.y;
    double tilespersec = ntiles/(double)hs.seconds;

    double sizebonus = sqrt(ntiles/(double)(14.0 * 6.0));
    double points = tilespersec / 0.14 * 100.0;
    ////return (int)(points * sizebonus * (hs.gravity?2.0:1.0));
    return (int)(points * sizebonus);
}

BOOL Frame::IsBetter(HighScore &hs, HighScore &than) {
    return (GetScore(hs) > GetScore(than));
}

int Frame::InsertHighscore(HighScore &hs) {
    int i;

    if(m_highscore.size() == 0) {
        m_highscore.resize(1);
        m_highscore[0] = hs;
        WriteHighscore();
        return 0;
    } else {
        HighScore last = m_highscore[m_highscore.size() - 1];
        if(IsBetter(hs, last) || (m_highscore.size() < HIGHSCORE_MAX)) {
            if(m_highscore.size() == HIGHSCORE_MAX)
                m_highscore[HIGHSCORE_MAX - 1] = hs;
            else {
                m_highscore.resize(m_highscore.size()+1);
                m_highscore[m_highscore.size() - 1] = hs;
            }

            // sort in new entry
            int bestsofar = m_highscore.size() - 1;
            for(i = m_highscore.size() - 1; i > 0; i--)
                if(IsBetter(m_highscore[i], m_highscore[i-1])) {
                    // swap entries
                    HighScore temp = m_highscore[i-1];
                    m_highscore[i-1] = m_highscore[i];
                    m_highscore[i] = temp;
                    bestsofar = i - 1;
                }

            WriteHighscore();
            return bestsofar;
        }
    }
    return -1;
}

void Frame::ReadHighscore() {
    int i;
    CStdString s, e;

    m_highscore.resize(0);
    i = 0;
    while ((i < (int)HIGHSCORE_MAX)) {
        s.Format(_T("Highscore_%d"), i);
        e = App::GetProfile()->GetProfileString(_T("Hall of Fame"), s, _T(""));
        if(!e.IsEmpty()) {
            m_highscore.resize(i+1);

            HighScore hs;
            hs.gravity = 0;
            memset(hs.name, 0, sizeof(hs.name));
            sscanf((const char *)e, _T("%d %d %d %ld %30c"),
                        &hs.x, &hs.y, &hs.seconds, &hs.date, (char*)&hs.name);
            if(hs.x < 0) {
                hs.x = -hs.x;
                hs.gravity = 1;
            }
            m_highscore[i] = hs;
        } else
            break;
        i++;
    }
}

void Frame::WriteHighscore() {
    int i;
    CStdString s, e;

    for(i = 0; i < m_highscore.size(); i++) {
        s.Format(_T("Highscore_%d"), i);
        HighScore hs = m_highscore[i];
        e.Format(_T("%d %d %d %ld %30s"),
                    hs.gravity?-hs.x:hs.x, hs.y, hs.seconds, hs.date, hs.name);
        App::GetProfile()->WriteProfileString(_T("Hall of Fame"), s, e);
    }
}

void Frame::ShowHighscore(int focusitem)  {
    ShowScores dlg(this);
    dlg.m_pHighScore = &m_highscore;
    dlg.m_iHotItem = focusitem;
    dlg.DoModal();
}


/////////////////////////////////////////////////////////////////////////////
// Frame message handlers

LRESULT Frame::WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch(uMsg) {
    case WM_GETMINMAXINFO:
        OnGetMinMaxInfo((LPMINMAXINFO)lParam);
        break;
    case WM_SIZE:
        OnSize(wParam, LOWORD(lParam), HIWORD(lParam));
        break;
    case WM_TIMER:
        OnTimer(wParam);
        break;
    case WM_INITMENUPOPUP:
        OnInitMenuPopup((HMENU)wParam, LOWORD(lParam), HIWORD(lParam));
        break;
    case WM_END_OF_GAME:
        OnEndOfGame(wParam, lParam);
        break;
    default:
        return Window::WndProc(uMsg, wParam, lParam);
    }
    return 0L;
}

int Frame::OnCreate(LPCREATESTRUCT lpCreateStruct) {
    // Let the superclass do its thing
    Window::OnCreate(lpCreateStruct);

    // Create the board
    RECT rc;
    SetRect(&rc, 0, 0, 0, 0);
    m_board.Create(0, _T("Shisen Board"), _T("Shisen Board"),
                    WS_CHILD|WS_VISIBLE, rc, m_hWnd, IDW_MAIN);

    // Create the status bar
    m_hStatus = CreateStatusWindow(WS_CHILD|WS_VISIBLE, _T(""), m_hWnd, 10);

    // Setup the status bar with two segments
    // (actual geometry will be set in OnSize)
    int iWidths[2] = {-1, -1};
    SendMessage(m_hStatus, SB_SETPARTS, 2, (LPARAM)&iWidths);
    SendMessage(m_hStatus, SB_SIMPLE, 0, 0);

    // Prime the status display
    UpdateScore();
    SetCheatMode(FALSE);

    // Setup the elapsed time refresh timer
    m_nTimer = SetTimer(m_hWnd, 1, 500, 0);

    // Setup the defaults
    LoadDefaults();

    return 0;
}

void Frame::OnDestroy() {
    if(m_nTimer)
        KillTimer(m_hWnd, m_nTimer);
    m_nTimer = 0;
    PostQuitMessage(0);

    // Let the superclass do its thing
    Window::OnDestroy();
}

BOOL Frame::OnCommand(UINT nID, UINT nEvent, HWND hSource) {
    // Parse the menu selections
    if(nID >= ID_OLEVEL1 && nID <= ID_OLEVEL3)
        OnLevelChange(nID);
    else if(nID >= ID_OSIZE1 && nID <= ID_OSIZE5)
        OnSizeChange(nID);
    else if(nID >= ID_OSPEED1 && nID <= ID_OSPEED5)
        OnSpeedChange(nID);
    else switch (nID)
        {
        case ID_FEXIT:
            DestroyWindow(m_hWnd);
            break;
        case ID_GUNDO:
            OnUndo();
            break;
        case ID_GREDO:
            OnRedo();
            break;
        case ID_GHINT:
            OnGetHint();
            break;
        case ID_GNEW:
            OnNewGame();
            break;
        case ID_GRESTART:
            OnRestartGame();
            break;
        case ID_GPAUSE:
            OnPauseGame();
            break;
        case ID_GISSOLVABLE:
            OnIsSolvable();
            break;
        case ID_GHOF:
            OnHallOfFame();
            break;
        case ID_GFINISH:
            OnFinish();
            break;
        case ID_OGRAVITY:
            OnGravity();
            break;
        case ID_OSOLVABLE:
            OnSolvable();
            break;
        case ID_OTILESET:
            OnSelectTileset();
            break;
        case ID_HABOUT:
            OnAppAbout();
            break;
        default:
            return Window::OnCommand(nID, nEvent, hSource);
    }
    return TRUE;
}

void Frame::OnGetMinMaxInfo(LPMINMAXINFO lpMMI) {
    // Calculate window size relative to board dimensions

    // Get board size
    SIZE sizeBoard = m_board.sizeHint();

    // Account for borders and menu
    sizeBoard.cx += GetSystemMetrics(SM_CXEDGE)*4;
    sizeBoard.cy += GetSystemMetrics(SM_CYEDGE)*4;
    sizeBoard.cx += GetSystemMetrics(SM_CXFRAME)*2;
    sizeBoard.cy += GetSystemMetrics(SM_CYFRAME)*2;
    sizeBoard.cy += GetSystemMetrics(SM_CYMENU)+GetSystemMetrics(SM_CYCAPTION);

    // Account for status bar
    if(m_hStatus) {
        RECT rcCtl;
        ::GetWindowRect(m_hStatus, &rcCtl);
        sizeBoard.cy += rcCtl.bottom - rcCtl.top;
    }

    lpMMI->ptMaxTrackSize.x = sizeBoard.cx;
    lpMMI->ptMaxTrackSize.y = sizeBoard.cy;
    lpMMI->ptMinTrackSize.x = sizeBoard.cx;
    lpMMI->ptMinTrackSize.y = sizeBoard.cy;
}

void Frame::OnSize(UINT nType, int cx, int cy) {
    // Reposition status bar
    RECT rcClient, rcCtl;
    GetClientRect(&rcClient);
    ::GetClientRect(m_hStatus, &rcCtl);
    MapWindowPoints(m_hStatus, m_hWnd, (LPPOINT)&rcCtl, 2);
    ::MoveWindow(m_hStatus, rcCtl.left, rcClient.bottom - rcCtl.bottom + rcCtl.top,
                    rcClient.right - rcClient.left,
                    rcCtl.bottom - rcCtl.top, TRUE);

    // Calculate geometry of status bar segments
    RECT rcText;
    int iWidths[2];
    HDC hDC = GetDC();
    CStdString szCheatMode;
    szCheatMode.LoadString(IDS_INDICATOR_CHEAT);
    SetRect(&rcText, 0, 0, 0, 0);
    DrawText(hDC, (LPCTSTR)szCheatMode, -1, &rcText, DT_CALCRECT|DT_LEFT);
    ReleaseDC(hDC);
    ::GetClientRect(m_hStatus, &rcCtl);
    iWidths[0] = rcCtl.right - rcText.right;
    iWidths[1] = -1;

    // Reposition the status bar segments
    SendMessage(m_hStatus, SB_SETPARTS, 2, (LPARAM)&iWidths);

    // Resize board to fit
    m_board.MoveWindow(0, 0,
        rcClient.right-rcClient.left, rcClient.bottom-rcClient.top - rcCtl.bottom + rcCtl.top, FALSE);

    // Invalidate the client area
    InvalidateRect(NULL, TRUE);
}

void Frame::OnTimer(UINT nIDEvent) {
    UpdateScore();
}

void Frame::OnUndo() {
    if(m_board.canUndo()) {
        m_board.undo();
        SetCheatMode(TRUE);
    }
}

void Frame::OnRedo() {
    if(m_board.canRedo()) 
        m_board.redo();
}

void Frame::OnGetHint() {
    m_board.getHint();
    SetCheatMode(TRUE);
}

void Frame::OnNewGame() {
    BeginWaitCursor();
    m_board.newGame();
    EndWaitCursor();
    SetCheatMode(FALSE);
}

void Frame::OnRestartGame() {
    while(m_board.canUndo())
      m_board.undo();
}

void Frame::OnPauseGame() {
    CStdString szPause, szResume;
    szPause.LoadString(IDS_PAUSEGAME);
    szResume.LoadString(IDS_RESUMEGAME);

    BOOL bPaused = m_board.pause();      
    HMENU hGame = GetSubMenu(GetMenu(m_hWnd), 1);
    ModifyMenu(hGame, ID_GPAUSE, MF_BYCOMMAND, ID_GPAUSE,
                        bPaused?szResume:szPause);
}

void Frame::OnIsSolvable() {
    CStdString s;
    s.Format(m_board.solvable()?IDS_SOLVABLE:IDS_NOTSOLVABLE);
    MessageBox(m_hWnd, (LPCTSTR)s, App::GetAppName(), MB_ICONINFORMATION);
}

void Frame::OnHallOfFame() {
   ShowHighscore();
}

void Frame::OnFinish() {
#ifdef _DEBUG
    SetCheatMode(TRUE);
    m_board.finish();
#endif // _DEBUG
}

void Frame::OnSolvable() {
    m_board.setSolvableFlag(!m_board.getSolvableFlag());
    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);
    CheckMenuItem(hOptions, ID_OSOLVABLE, MF_BYCOMMAND|
                            (m_board.getSolvableFlag()?MF_CHECKED:MF_UNCHECKED));

    // Save option to the registry
    App::GetProfile()->WriteProfileInt(_T("Options"), _T("Solvable"), m_board.getSolvableFlag());
}

void Frame::OnGravity() {
    m_board.setGravityFlag(!m_board.gravityFlag());
    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);
    CheckMenuItem(hOptions, ID_OGRAVITY, MF_BYCOMMAND|
                            (m_board.gravityFlag()?MF_CHECKED:MF_UNCHECKED));

    // Save option to the registry
    App::GetProfile()->WriteProfileInt(_T("Options"), _T("Gravity"), m_board.gravityFlag());
}

void Frame::OnLevelChange(UINT nID) {
    // Update the menu items
    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);
    for(UINT i = ID_OLEVEL1; i <= ID_OLEVEL3; i++)
        CheckMenuItem(hOptions, i, MF_BYCOMMAND|((i == nID)?MF_CHECKED:MF_UNCHECKED));

    // Create a new game with the changed level
    m_board.setShuffle((nID - ID_OLEVEL1) * 4 + 1);
    OnNewGame();

    // Save option to the registry
    App::GetProfile()->WriteProfileInt(_T("Options"), _T("Level"), nID);
}

void Frame::OnSizeChange(UINT nID) {
    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);

    // Create a new game with the changed size
    BeginWaitCursor();
    m_board.setSize(size_x[nID-ID_OSIZE1], size_y[nID-ID_OSIZE1]);
    OnNewGame();

    // Update the menu items
    for(UINT i = ID_OSIZE1; i <= ID_OSIZE5; i++)
        CheckMenuItem(hOptions, i, MF_BYCOMMAND|((i == nID)?MF_CHECKED:MF_UNCHECKED));

    // Frame must be resized; call MoveWindow to force WM_GETMINMAXINFO
    RECT rcWnd;
    GetWindowRect(&rcWnd);
    MoveWindow(rcWnd, TRUE);
    EndWaitCursor();

    // Save option to the registry
    App::GetProfile()->WriteProfileInt(_T("Options"), _T("Size"), nID);
}

void Frame::OnSpeedChange(UINT nID) {
    HMENU hOptions = GetSubMenu(GetMenu(m_hWnd), 2);
    m_board.setDelay(DELAY[nID - ID_OSPEED1]);
    for(UINT i = ID_OSPEED1; i <= ID_OSPEED5; i++)
        CheckMenuItem(hOptions, i, MF_BYCOMMAND|((i == nID)?MF_CHECKED:MF_UNCHECKED));

    // Save option to the registry
    App::GetProfile()->WriteProfileInt(_T("Options"), _T("Speed"), nID);
}

void Frame::OnSelectTileset() {
    SelectTileset dlg(this);
    if(dlg.DoModal() == IDOK) {
        // Select new tileset
        m_board.loadMahjonggTileset(dlg.GetTileset());

        // Select new background
        m_board.loadBackground(dlg.GetBackground());

        // Re-layout tiles (necessary, as the m_bUniqueSeasons
        // flag may have changed)
        OnNewGame();

        // Mark board for update
        m_board.InvalidateRect(NULL, TRUE);
    }
}

void Frame::OnInitMenuPopup(HMENU hMenu, UINT nIndex, BOOL bSysMenu) {
    int iSize = GetMenuItemCount(hMenu);
    for(int i=0; i<iSize; i++) {
        int id = GetMenuItemID(hMenu, i);
        if(id >= ID_GFIRST && id <= ID_OLAST)
            OnUpdateMenu(hMenu, (UINT)id);
    }
}

void Frame::OnUpdateMenu(HMENU hMenu, UINT nID) {
    BOOL bEnable;
    switch(nID) {
    case ID_GUNDO:
    case ID_GREDO:
    case ID_GRESTART:
        bEnable = m_board.canUndo() && !m_board.isPaused();
        break;
    case ID_OSOLVABLE:
    case ID_OGRAVITY:
        bEnable = !m_board.canUndo() && !m_board.isPaused();
        break;
    case ID_GPAUSE:
        bEnable = TRUE;
        break;
    default:
        bEnable = !m_board.isPaused();
    }

    EnableMenuItem(hMenu, nID, MF_BYCOMMAND|(bEnable?MF_ENABLED:MF_GRAYED));
}

void Frame::OnAppAbout() {
    About dlg(this);
    dlg.DoModal();
}

LRESULT Frame::OnEndOfGame(WPARAM wParam, LPARAM lParam) {
    if(m_board.tilesLeft() > 0) {
        CStdString s;
        s.LoadString(IDS_NOMOREMOVES);
        MessageBox(m_hWnd, (LPCTSTR)s, App::GetAppName(), MB_ICONEXCLAMATION);
    } else {
        // create highscore entry
        HighScore hs;
        hs.seconds = m_board.getTimeForGame();
        hs.x = m_board.x_tiles();
        hs.y = m_board.y_tiles();
        hs.gravity = m_board.gravityFlag();

        // check if we made it into Top10
        BOOL isHighscore = FALSE;
        if(m_highscore.size() < HIGHSCORE_MAX)
            isHighscore = TRUE;
        else if(IsBetter(hs, m_highscore[HIGHSCORE_MAX-1]))
            isHighscore = TRUE;

        if(isHighscore) {
            CStdString name = GetPlayerName();
            if(!name.IsEmpty()) {
                strncpy(hs.name, (LPCSTR)name, sizeof(hs.name) - 1);
                hs.date = time((time_t*)0);
                hs.x = m_board.x_tiles();
                hs.y = m_board.y_tiles();
                int rank = InsertHighscore(hs);
                ShowHighscore(rank);
            }
        } else {
            CStdString s;
            int t = m_board.getTimeForGame();
            s.Format(IDS_GAMEOVER, t/3600, (t/60)%60, t%60);
            MessageBox(m_hWnd, (LPCTSTR)s, App::GetAppName(), MB_ICONINFORMATION);
        }
    }

    OnNewGame();

    return 0L;
}
<< Back to Shisen-Sho