iBinx Corp.
contact usshisen-sho

Source listing: . Return to the main page

/* -*- C++ -*-
 *******************************************************************
 *
 *
 * Shisen-Sho for Windows
 *
 * board.cpp - Implementation of the Board 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 "shisen.h"
#include "board.h"
#include "bmputil.h"

#define EMPTY          0
#define XBORDER        20
#define YBORDER        20
#define DEFAULTDELAY   500
#define DEFAULTSHUFFLE 4

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

/////////////////////////////////////////////////////////////////////////////
// Board

Board::Board() {
    m_bNoEvents = FALSE;
    m_hOldTiles = NULL;

    m_hBmpPaused = NULL;
    paused = FALSE;
    trying = FALSE;
    _solvable_flag = TRUE;

    grav_col_1 = -1;
    grav_col_2 = -1;
    setGravityFlag(FALSE);

    // randomize
    srand((unsigned)time(NULL));

    starttime = time((time_t *)0);

    setDelay(DEFAULTDELAY);

    field = 0;
    highlighted_tile = -1;
    m_nTimer = 0;
}

BOOL Board::PreCreateWindow(CREATESTRUCT& cs) {
    cs.dwExStyle |= WS_EX_CLIENTEDGE;
    cs.style &= ~WS_BORDER;

    // 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          = NULL;
        wcex.hCursor        = LoadCursor(NULL, IDC_ARROW);
        wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
        wcex.lpszMenuName   = NULL;
        wcex.lpszClassName  = cs.lpszClass;
        wcex.hIconSm        = NULL;
        return RegisterClassEx(&wcex);
    }
    return TRUE;
}

int Board::x_tiles() {
    return _x_tiles;
}

int Board::y_tiles() {
    return _y_tiles;
}

void Board::setField(int x, int y, int value) {
    if(x < x_tiles() && y < y_tiles())
        field[y * x_tiles() + x] = value;
    else {
        CStdString s;
        s.Format(IDS_INVALID_FIELD, x, y);
        fprintf(stderr, (LPCSTR)s);
        exit(1);
    }
}

int Board::getField(int x, int y) {
    if(x == x_tiles() || x == -1)
        return EMPTY;
    else if(y == y_tiles() || y == -1)
        return EMPTY;
    else if(x < -1 || x > x_tiles() || y < -1 || y > y_tiles()) {
        return EMPTY;
    } else
    return field[y * x_tiles() + x];
}

void Board::setSize(int x, int y) {
    if(x == x_tiles() && y == y_tiles())
        return;

    if(field != 0)
        free(field);

    field = (int*)malloc(sizeof(int) * x * y);
    _x_tiles = x;
    _y_tiles = y;
    for(int i = 0; i < x; i++)
        for(int j = 0; j < y; j++)
            setField(i, j, EMPTY);

    newGame();
}

void Board::newGame() {
    int i, j, x, y, k;

    mark_x = -1;
    mark_y = -1;

    while(_undo.size())
        _undo.pop_front();
    while(_redo.size())
        _redo.pop_front();

    clearHistory();

    for(i = 0; i < x_tiles(); i++)
    for(j = 0; j < y_tiles(); j++)
        setField(i, j, EMPTY);

    // distribute all tiles on board
    int cur_tile = 0;
    for(i = 0; i < x_tiles() * y_tiles() * 12; i++) {
        // map the tileindex to a tile
        // not all tiles from the pixmap are really used, only
        // 36 out of 45 are used. This maps and index to
        // the "real" index.
        int tile;
        if(m_bUniqueSeasons) {
            // Include two seasons and all flowers
            if(cur_tile == 28)
                tile = 30;
            else if(cur_tile >= 29 && cur_tile <= 35)
                tile = cur_tile + 7;
            else
                tile = cur_tile;

            cur_tile++;
            if(cur_tile == 36)
                cur_tile = 0;
        } else {
            // Include only one season and one flower
            if(cur_tile >= 28 && cur_tile <= 31)
                tile = cur_tile + 8;
            else
                tile = cur_tile;

            cur_tile++;
            if(cur_tile == 32)
                cur_tile = 0;
        }

        x = i % x_tiles();
        y = i / x_tiles() * 4;

        tile++;
        for(k = 0; k < 4 && k + y < y_tiles(); k++)
            setField(x, y+k, tile);
    }

    if(getShuffle() == 0) {
        if(!trying) {
            if(m_hWnd)
                InvalidateRect(NULL, FALSE);
            starttime = time((time_t *)0);
        }    
        return;
    }

    // shuffle the field
    int tx = x_tiles();
    int ty = y_tiles();
    for(i = 0; i < x_tiles() * y_tiles() * getShuffle(); i++) {
        int x1 = random(tx);
        int y1 = random(ty);
        int x2 = random(tx);
        int y2 = random(ty);
        int t  = getField(x1, y1);
        setField(x1, y1, getField(x2, y2));
        setField(x2, y2, t);
    }

    // do not make solvable if _solvable_flag is FALSE
    if(!_solvable_flag) {
        if(!trying) {
            if(m_hWnd)
                InvalidateRect(NULL, FALSE);
            starttime = time((time_t *)0);
        }    
        return;
    }

    // rearrange tiles so game is solvable
    int fsize = x_tiles() * y_tiles() * sizeof(int);
    int *oldfield = new int[x_tiles() * y_tiles()];
    memcpy(oldfield, field, fsize);            // save field
    int *tiles = new int[x_tiles() * y_tiles()];
    int *pos = new int[x_tiles() * y_tiles()];

    while(!solvable(TRUE)) {
        // generate a list of free tiles and positions
        int num_tiles = 0;
        for(i = 0; i < x_tiles() * y_tiles(); i++)
            if(field[i] != EMPTY) {
                pos[num_tiles] = i;
                tiles[num_tiles] = field[i];
                num_tiles++;
            }

        // restore field
        memcpy(field, oldfield, fsize);

        // redistribute unsolved tiles
        while(num_tiles > 0) {
            // get a random tile
            int r1 = random(num_tiles);
            int r2 = random(num_tiles);
            int tile = tiles[r1];
            int apos = pos[r2];

            // truncate list
            tiles[r1] = tiles[num_tiles-1];
            pos[r2] = pos[num_tiles-1];
            num_tiles--;

            // put this tile on the new position
            field[apos] = tile;
        }

        // remember field
        memcpy(oldfield, field, fsize);
    }

    // restore field
    memcpy(field, oldfield, fsize);  
    delete tiles;
    delete pos;
    delete oldfield;

    if(!trying) {
        if(m_hWnd)
            InvalidateRect(NULL, FALSE);
        starttime = time((time_t *)0);
    }
}

void Board::updateField(int x, int y, BOOL bErase) {
    if(trying)
        return;

    // Add the specified tile to the update region
    // This allows invalidated tiles to accumulate
    // and be repainted together on the next WM_PAINT.
    // Note that if an immediate repaint is required,
    // you must call UpdateWindow() immediately after
    // updateField().
    RECT r;
    int x0 = XBORDER + x * m_tileWidth;
    int y0 = YBORDER + y * m_tileHeight;
    SetRect(&r, x0, y0,
              x0 + m_tileWidth,
              y0 + m_tileHeight);
    InvalidateRect(&r, bErase);
}

int MIN(int a, int b) {
    if(a < b)
        return a;
    else
        return b;
}

int MAX(int a, int b) {
    if(a > b)
        return a;
    else
        return b;
}

void Board::loadMahjonggTileset(LPCSTR szTileset) {
    // Release the current tileset
    releaseTileset();

    HBITMAP bmpTileSrc;
    if(!strlen(szTileset) || !CBmpUtil::LoadBmpImage(szTileset, &bmpTileSrc)) {
        loadDefaultTileset();
        return;
    }

    // Initialize new tileset bitmaps.  We create two bitmaps: one for
    // the unselected tiles, and one for the tiles in the selected state.
    BITMAP bm;
    HDC dcTemp;
    HBITMAP hBmpTemp;
    HDC hDC = GetDC();
    GetObject(bmpTileSrc, sizeof(BITMAP), &bm);
    m_bmpTiles = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight);
    m_bmpTilesSel = CreateCompatibleBitmap(hDC, bm.bmWidth, bm.bmHeight);
    dcTemp = CreateCompatibleDC(hDC);
    hBmpTemp = (HBITMAP)SelectObject(dcTemp, m_bmpTilesSel);
    ReleaseDC(hDC);
    m_tileWidth = bm.bmWidth / 9;
    m_tileHeight = bm.bmHeight / 5;

    initCacheDC();
    m_bUniqueSeasons = FALSE;

    // Erase background
    RECT rcTiles;
    SetRect(&rcTiles, 0, 0, bm.bmWidth, bm.bmHeight);
    SetBkColor(m_dcTiles, RGB(0,0,0));
    ExtTextOut(m_dcTiles, 0, 0, ETO_OPAQUE, &rcTiles, NULL, 0, NULL);
    BitBlt(dcTemp, 0, 0, bm.bmWidth, bm.bmHeight, m_dcTiles, 0, 0, SRCCOPY);

    // Render first tile background
    // Unselected background is at position 43; selected background is at 44
    CBmpUtil::DrawBitmap(m_dcTiles, bmpTileSrc, 0, 0,
                            m_tileWidth, m_tileHeight,
                            (43%9)*m_tileWidth, (43/9)*m_tileHeight);
    CBmpUtil::DrawBitmap(dcTemp, bmpTileSrc, 0, 0,
                            m_tileWidth, m_tileHeight,
                            (44%9)*m_tileWidth, (44/9)*m_tileHeight);

    // Replicate background to all tile positions
    for(int i = 0; i < 9; i++)
        for(int j = 0; j < 5; j++) {
            if((i == 0) && (j == 0))
                continue;
            BitBlt(m_dcTiles, i*m_tileWidth, j*m_tileHeight,
                                m_tileWidth, m_tileHeight,
                                m_dcTiles,
                                0, 0, SRCCOPY);
            BitBlt(dcTemp, i*m_tileWidth, j*m_tileHeight,
                                m_tileWidth, m_tileHeight,
                                dcTemp,
                                0, 0, SRCCOPY);
        }

    // Render the tile faces
    for(int ii = 0; ii < 9; ii++)
        for(int jj = 0; jj < 5; jj++) {
            CBmpUtil::DrawBitmap(m_dcTiles, bmpTileSrc,
                                ii*m_tileWidth+5, jj*m_tileHeight+1,
                                m_tileWidth-6, m_tileHeight-6,
                                ii*m_tileWidth+5, jj*m_tileHeight+1);
            CBmpUtil::DrawBitmap(dcTemp, bmpTileSrc,
                                ii*m_tileWidth+5, jj*m_tileHeight+1,
                                m_tileWidth-6, m_tileHeight-6,
                                ii*m_tileWidth+5, jj*m_tileHeight+1);
        }

    // Restore the temp DC
    SelectObject(dcTemp, hBmpTemp);
    DeleteDC(dcTemp);
    DeleteObject(bmpTileSrc);
}

void Board::loadDefaultTileset() {
    // Setup the default tileset
    BITMAP bm;
    m_bmpTiles = LoadBitmap(App::GetHInstance(), MAKEINTRESOURCE(IDB_SHISEN));
    GetObject(m_bmpTiles, sizeof(BITMAP), &bm);
    m_tileWidth = bm.bmWidth / 9;
    m_tileHeight = bm.bmHeight / 5;
    initCacheDC();

    // Create the default "selected" tileset
    //
    // The default selected tileset consists of the default
    // tileset or'ed with a highlight brush.
    HDC dcTemp;
    HBITMAP hOldBmp;
    dcTemp = CreateCompatibleDC(m_dcTiles);
    m_bmpTilesSel = CreateCompatibleBitmap(m_dcTiles, bm.bmWidth, bm.bmHeight);
    hOldBmp = (HBITMAP)SelectObject(dcTemp, m_bmpTilesSel);

    // Create the highlight brush
    HBRUSH brush;
    brush = CreateSolidBrush(RGB(48, 48, 48));
    HBRUSH hOldBrush = (HBRUSH)SelectObject(dcTemp, brush);

    // Blit the tiles into the highlight DC
    BitBlt(dcTemp, 0, 0, bm.bmWidth, bm.bmHeight, m_dcTiles,
                0, 0,
                /*PSo*/0x00FC008A);

    // Restore the temp DC
    SelectObject(dcTemp, hOldBrush);
    DeleteObject(brush);
    SelectObject(dcTemp, hOldBmp);
    DeleteDC(dcTemp);

    m_bUniqueSeasons = TRUE;
}

void Board::initCacheDC() {
    // Setup the cached tile DC for fast blitting
    HDC hDC = GetDC();
    m_dcTiles = CreateCompatibleDC(hDC);
    m_hOldTiles = (HBITMAP)SelectObject(m_dcTiles, m_bmpTiles);
    ReleaseDC(hDC);
}

void Board::releaseTileset() {
    // Release GDI resources
    if(m_hOldTiles) {
        SelectObject(m_dcTiles, m_hOldTiles);
        DeleteObject(m_bmpTiles);
        DeleteObject(m_bmpTilesSel);
        DeleteDC(m_dcTiles);
        m_hOldTiles = NULL;
    }
}

void Board::loadBackground(LPCSTR szBackground) {
    // Release the current background
    releaseBackground();

    if(strlen(szBackground) && CBmpUtil::LoadBmpImage(szBackground, &m_bmpBkgnd)) {
        BITMAP bm;
        GetObject(m_bmpBkgnd, sizeof(BITMAP), &bm);
        m_sizeBkgnd.cx = bm.bmWidth;
        m_sizeBkgnd.cy = bm.bmHeight;
    } else
        loadDefaultBackground();

}

void Board::loadDefaultBackground() {
    BITMAP bm;
    m_bmpBkgnd = LoadBitmap(App::GetHInstance(), MAKEINTRESOURCE(IDB_BGND));
    GetObject(m_bmpBkgnd, sizeof(BITMAP), &bm);
    m_sizeBkgnd.cx = bm.bmWidth;
    m_sizeBkgnd.cy = bm.bmHeight;
}

void Board::releaseBackground() {
    DeleteObject(m_bmpBkgnd);
}

void Board::eraseBackground(HDC hDC, RECT& rcUpdate) {
    HDC memdc;
    HBITMAP hBmpOld;

    // Prepare the background bitmap
    memdc = CreateCompatibleDC(hDC);
    hBmpOld = (HBITMAP)SelectObject(memdc, m_bmpBkgnd);

    // Tile the background bitmap into the DC
    RECT rc;
    GetClientRect(&rc);
    // Lay it out from the origin so it is always in the same
    // position regardless of where the update region is located.
    for(int y=0; y < (rc.bottom - rc.top); y += m_sizeBkgnd.cy)
        for(int x=0; x < (rc.right - rc.left); x += m_sizeBkgnd.cx) {
            RECT ri, r;
            SetRect(&r, x, y, x + m_sizeBkgnd.cx, y + m_sizeBkgnd.cy);

            // Only bother to draw background if it is in the update region
            if(IntersectRect(&ri, &rcUpdate, &r))
                BitBlt(hDC, x-rcUpdate.left, y-rcUpdate.top,
                            m_sizeBkgnd.cx, m_sizeBkgnd.cy, memdc, 0, 0, SRCCOPY);
        }

    // Restore the memory DC
   SelectObject(memdc, hBmpOld);
   DeleteDC(memdc);
}

void Board::drawTile(HDC hDC, int iTile, int xpos, int ypos, BOOL bLighten) {
    if(bLighten)
        SelectObject(m_dcTiles, m_bmpTilesSel);

    // Calculate location of the tile within the source
    int x = iTile % 9;
    int y = iTile / 9;

    // Blit the bitmap into the DC
    BitBlt(hDC, xpos, ypos, m_tileWidth, m_tileHeight, m_dcTiles,
                x*m_tileWidth, y*m_tileHeight,
                SRCCOPY);

    if(bLighten)
        SelectObject(m_dcTiles, m_bmpTiles);
}

// returns a random number < max
int Board::random(int max) {
    return ::rand() % max; // don't depend on RAND_MAX...
}

void Board::marked(int x, int y) {
    // make sure that the last arrow is correctly undrawn
    undrawArrow();

    if(getField(x, y) == EMPTY)
        return;

    if(x == mark_x && y == mark_y) {
        // unmark the piece
        mark_x = -1;
        mark_y = -1;
        updateField(x, y);
        return;
    }

    if(mark_x == -1) {
        mark_x = x;
        mark_y = y;
        updateField(x, y);
        return;
    } else {
        int fld1 = getField(mark_x, mark_y);
        int fld2 = getField(x, y);

        // both field same?
        if(fld1 != fld2)
            return;

        // trace    
        if(findPath(mark_x, mark_y, x, y)) {
            madeMove(mark_x, mark_y, x, y);
            drawArrow(mark_x, mark_y, x, y);
            setField(mark_x, mark_y, EMPTY);
            setField(x, y, EMPTY);
            grav_col_1 = x;
            grav_col_2 = mark_x;
            mark_x = -1;
            mark_y = -1;

            int dummyx;
            History dummyh[4];

            // game is over?      
            if(!getHint_I(dummyx,dummyx,dummyx,dummyx,dummyh)) {
                time_for_game = (int)time((time_t*)0) - starttime;
                PostMessage(GetParent(m_hWnd), WM_END_OF_GAME, 0, 0);
            }

        } else
            clearHistory();
    }
}

BOOL Board::canMakePath(int x1, int y1, int x2, int y2) {
    int i;

    if(x1 == x2) {
        for(i = MIN(y1, y2)+1; i < MAX(y1, y2); i++) 
            if(getField(x1, i) != EMPTY)
                return FALSE;

        return TRUE;
    } 

    if(y1 == y2) {
        for(i = MIN(x1, x2)+1; i < MAX(x1, x2); i++)
            if(getField(i, y1) != EMPTY)
                return FALSE;

        return TRUE;
    }

    return FALSE;
}

BOOL Board::findPath(int x1, int y1, int x2, int y2) {
    clearHistory();

    if(findSimplePath(x1, y1, x2, y2))
        return TRUE;
    else {
        // find 3-way path
        int dx[4] = {1, 0, -1, 0};
        int dy[4] = {0, 1, 0, -1};
        int i;

        for(i = 0; i < 4; i++) {
        int newx = x1 + dx[i], newy = y1 + dy[i];
        while(getField(newx, newy) == EMPTY && 
                newx >= -1 && newx <= x_tiles() &&
                newy >= -1 && newy <= y_tiles()) {
            if(findSimplePath(newx, newy, x2, y2)) {
                // make place for history point
                for(int j = 3; j > 0; j--)
                    history[j] = history[j-1];

                    // insert history point
                    history[0].x = x1;
                    history[0].y = y1;
                    return TRUE;     
                }

                newx += dx[i];
                newy += dy[i];
            }
        }

        clearHistory();
        return FALSE;
    }

    return FALSE;
}

BOOL Board::findSimplePath(int x1, int y1, int x2, int y2) {
    BOOL r = FALSE;

    // find direct line
    if(canMakePath(x1, y1, x2, y2)) {
        history[0].x = x1;
        history[0].y = y1;
        history[1].x = x2;
        history[1].y = y2;
        r = TRUE;
    } else {
        if(!(x1 == x2 || y1 == y2)) // requires complex path
            if(getField(x2, y1) == EMPTY &&
                    canMakePath(x1, y1, x2, y1) && canMakePath(x2, y1, x2, y2)) {
                history[0].x = x1;
                history[0].y = y1;
                history[1].x = x2;
                history[1].y = y1;
                history[2].x = x2;
                history[2].y = y2;
                r = TRUE;
            } else if(getField(x1, y2) == EMPTY &&
                canMakePath(x1, y1, x1, y2) && canMakePath(x1, y2, x2, y2)) {
                history[0].x = x1;
                history[0].y = y1;
                history[1].x = x1;
                history[1].y = y2;
                history[2].x = x2;
                history[2].y = y2;
                r = TRUE;
            }
    }

    return r;
}

void Board::gravity(int col, BOOL update) {
    if(gravity_flag) {
        int rptr = y_tiles()-1, wptr = y_tiles()-1;
        while(rptr >= 0) {
            if(getField(col, wptr) != EMPTY) {
                rptr--;
                wptr--;
            } else if(getField(col, rptr) != EMPTY) {
                setField(col, wptr, getField(col, rptr));
                setField(col, rptr, EMPTY);
                if(update) {
                    updateField(col, rptr);
                    updateField(col, wptr);
                }
                wptr--;
                rptr--;
            } else
                rptr--;    
        }
    }
}

void Board::drawArrow(int x1, int y1, int x2, int y2) {
    if(trying)
        return;

    // find out number of array
    int num = 0;
    while(num < 4 && history[num].x != -2)
        num++;

    // lighten the fields
    // remember mark_x,mark_y
    int mx = mark_x, my = mark_y;
    mark_x = x1; mark_y = y1;
    updateField(x1, y1);
    UpdateWindow();
    mark_x = x2; mark_y = y2;
    updateField(x2, y2);
    UpdateWindow();

    // restore the mark
    mark_x = mx;
    mark_y = my;

    // draw the arrow
    HDC dc = GetDC();
    HPEN hOldPen, p = CreatePen(PS_SOLID, 6, RGB(255,0,0));
    hOldPen = (HPEN)SelectObject(dc, p);
    num = 0;
    while(num < 3 && history[num+1].x != -2) {
        POINT pt = midCoord(history[num].x, history[num].y);
        MoveToEx(dc, pt.x, pt.y, NULL);
        pt = midCoord(history[num+1].x, history[num+1].y);
        LineTo(dc, pt.x, pt.y);
        num++;    
    }
    SelectObject(dc, hOldPen);
    DeleteObject(p);
    ReleaseDC(dc);

    // set the timer to force undraw later
    m_nTimer = SetTimer(m_hWnd, 1, getDelay(), 0);
}

void Board::undrawArrow() {
    if(trying)
        return;

    if(grav_col_1 != -1 || grav_col_2 != -1) {
        gravity(grav_col_1, TRUE);
        gravity(grav_col_2, TRUE);
        grav_col_1 = -1;
        grav_col_2 = -1;
    }

    // is already undrawn?
    if(history[0].x == -2)
        return;

    // redraw all affected fields
    int num = 0;
    while(num < 3 && history[num+1].x != -2) {
        if(history[num].y == history[num+1].y)
            for(int i = MIN(history[num].x, history[num+1].x); 
                    i <= MAX(history[num].x, history[num+1].x); i++)
                updateField(i, history[num].y, TRUE);
        else 
            for(int i = MIN(history[num].y, history[num+1].y); 
                    i <= MAX(history[num].y, history[num+1].y); i++)
                updateField(history[num].x, i, TRUE);
        num++;
    }

    clearHistory();
}

POINT Board::midCoord(int x, int y) {
    POINT p;
    int w = m_tileWidth;
    int h = m_tileHeight;

    if(x == -1)
        p.x = (XBORDER/2 - w/2);
    else if(x == x_tiles())
        p.x = (XBORDER/2 + w * x_tiles());
    else 
        p.x = (XBORDER + w * x);

    if(y == -1) 
        p.y = (YBORDER/2 - h/2);
    else if(y == y_tiles())
        p.y = (YBORDER/2 + h * y_tiles());
    else 
        p.y = (YBORDER + h * y);

    p.x = (p.x + w/2);
    p.y = (p.y + h/2);
    return p;
} 

void Board::setDelay(int newvalue) {
    _delay = newvalue;
}

int Board::getDelay() {
    return _delay;
}

void Board::madeMove(int x1, int y1, int x2, int y2) {
    Move *m = new Move(x1, y1, x2, y2, getField(x1, y1));
    _undo.push_back(*m);
    while(_redo.size())
        _redo.pop_front();
}

BOOL Board::canUndo() {
    return (_undo.size() > 0);
}

BOOL Board::canRedo() {
    return (_redo.size() > 0);
}

void Board::undo() {
    if(canUndo()) {
        undrawArrow();
        Move m = _undo.back();
        _undo.pop_back();

        if(gravityFlag()) {
            int y;
            int delta = 1;

            if(m.x1 == m.x2) {
                delta++;

                /* damned, I hate this. This undo/redo stuff is really complicated
                 * when used with gravity. This avoids a bug when both tiles reside
                 * in the same row, but not adjascent y positions. In that case, the
                 * order of undo is important
                 */
                if(m.y1>m.y2) {
                    int t = m.x1;
                    m.x1 = m.x2;
                    m.x2 = t;
                    t = m.y1;
                    m.y1 = m.y2;
                    m.y2 = t;
                }
            }

            for(y = 0; y <= m.y1-1; y++) {
                setField(m.x1, y, getField(m.x1, y+delta));
                updateField(m.x1, y);
            }

            if(m.x1 != m.x2) {
                for(y = 0; y < m.y2; y++) {
                    setField(m.x2, y, getField(m.x2, y+1));
                    updateField(m.x2, y);
                }
            }
        }        
        
        setField(m.x1, m.y1, m.tile);
        setField(m.x2, m.y2, m.tile);
        updateField(m.x1, m.y1);
        updateField(m.x2, m.y2);
        _redo.push_front(m);
    }
}

void Board::redo() {
    if(canRedo()) {
        undrawArrow();
        Move m = _redo.front();
        _redo.pop_front();
        setField(m.x1, m.y1, EMPTY);
        setField(m.x2, m.y2, EMPTY);
        updateField(m.x1, m.y1);
        updateField(m.x2, m.y2);
        gravity(m.x1, TRUE);
        gravity(m.x2, TRUE);
        _undo.push_back(m);
    }
}

SIZE Board::sizeHint() {
    SIZE sz;
    sz.cx = x_tiles() * m_tileWidth + 2 * XBORDER;
    sz.cy = y_tiles() * m_tileHeight + 2 * YBORDER;
    return sz;
}

void Board::clearHistory() {
    // init history
    for(int i = 0; i < 4; i++) {
        history[i].x = -2;
        history[i].y = -2;
    }
}

void Board::getHint() {
    int x1, y1, x2, y2;
    History h[4];

    if(getHint_I(x1, y1, x2, y2, h)) {
        undrawArrow();
        for(int i = 0; i < 4; i++)
            history[i] = h[i];

        int old_delay = getDelay();
        setDelay(1000);
        drawArrow(x1, y1, x2, y2);
        setDelay(old_delay);
    }
}

BOOL Board::getHint_I(int &x1, int &y1, int &x2, int &y2, History h[4]) {
    short done[45];
    for( short index = 0; index < 45; index++ )
        done[index] = 0;

    // remember old history
    History old[4];
    for(int i = 0; i < 4; i++)
        old[i] = history[i];

    // initial no hint
    x1 = -1;
    x2 = -1;
    y1 = -1;
    y2 = -1;

    for(int x = 0; x < x_tiles(); x++)
        for(int y = 0; y < y_tiles(); y++)
            if(getField(x, y) != EMPTY && done[getField(x, y)] != 4) {
                int tile = getField(x, y);

                // for all these types of tile search path's
                for(int xx = 0; xx < x_tiles(); xx++)
                    for(int yy = 0; yy < y_tiles(); yy++)
                        if(xx != x || yy != y)
                            if(getField(xx, yy) == tile)
                                if(findPath(x, y, xx, yy)) {
                                    for(int i = 0; i < 4; i++)
                                        h[i] = history[i];

                                    x1 = x;
                                    x2 = xx;
                                    y1 = y;
                                    y2 = yy;
                                    for(int ii = 0; ii < 4; ii++)
                                        history[ii] = old[ii];
                                    return TRUE;
                                }

                clearHistory();
                done[tile]++;
            }

    for(int ii = 0; ii < 4; ii++)
        history[ii] = old[ii];

    return FALSE;
}

void Board::setShuffle(int newvalue) {
    _shuffle = newvalue;
}

int Board::getShuffle() {
    return _shuffle;
}

int Board::tilesLeft() {
    int left = 0;

    for(int i = 0; i < x_tiles(); i++)
        for(int j = 0; j < x_tiles(); j++)
            if(getField(i, j) != EMPTY)
                left++;

    return left;
}

int Board::getCurrentTime() {
    return (int)(time((time_t *)0) - starttime);
}

int Board::getTimeForGame() {
    if(tilesLeft() == 0) 
        return time_for_game;
    else if(paused)
        return (int)(pause_start - starttime);
    else
        return (int)(time((time_t *)0) - starttime);
}

BOOL Board::solvable(BOOL norestore) {
    int x1, y1, x2, y2;
    History h[4];
    int *oldfield = 0;

    if(!norestore) {
        oldfield = (int *)malloc(x_tiles() * y_tiles() * sizeof(int));
        memcpy(oldfield, field, x_tiles() * y_tiles() * sizeof(int));
    }

    while(getHint_I(x1, y1, x2, y2, h)) {
        setField(x1, y1, EMPTY);
        setField(x2, y2, EMPTY);
    }

    int left = tilesLeft();

    if(!norestore) {
        memcpy(field, oldfield, x_tiles() * y_tiles() * sizeof(int));
        free(oldfield);  
    }

    return (left == 0);
}

BOOL Board::getSolvableFlag() {
    return _solvable_flag;
}

void Board::setSolvableFlag(BOOL value) {
    _solvable_flag = value;
}

void Board::setGravityFlag(BOOL b) {
    gravity_flag = b;
}

BOOL Board::gravityFlag() {
    return gravity_flag;
}

BOOL Board::pause() {
    paused = !paused;
    if(paused)
        pause_start = time((time_t *)0);
    else
        starttime += (time_t)(time((time_t *)0) - pause_start);
    InvalidateRect(NULL, TRUE);

    return paused;
}

#ifdef _DEBUG
void Board::finish() {
    MSG msg;
    int x1, y1, x2, y2;
    History h[4];
    BOOL ready = FALSE;

    // Loop as long as there are tiles to remove
    while(!ready && getHint_I(x1, y1, x2, y2, h)) {
        mark_x = -1;
        mark_y = -1;
        if(tilesLeft() == 2)
            ready = TRUE;
        marked(x1, y1);
        marked(x2, y2);
        UpdateWindow();
        Sleep(getDelay());
        // Handle any event(s) in the message queue
        while(PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
            if(!GetMessage(&msg, NULL, 0, 0)) {
                // Close the application
                PostQuitMessage(0);
                ready = TRUE;
                break;
            }

            TranslateMessage(&msg);
            DispatchMessage(&msg);

            switch(msg.message) {
            case WM_END_OF_GAME:
            case WM_COMMAND:
            case WM_LBUTTONDOWN:
            case WM_RBUTTONDOWN:
            case WM_CHAR:
                // Terminate loop if the user selects a
                // menu item, presses a key, or clicks
                // a mouse button, or when game is over.
                ready = TRUE;
                break;
            }
        }
    }
}
#endif // _DEBUG


/////////////////////////////////////////////////////////////////////////////
// Board message handlers

LRESULT Board::WndProc(UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch(uMsg) {
    case WM_PAINT:
        OnPaint();
        return 0;
    case WM_ERASEBKGND:
        return OnEraseBkgnd((HDC)wParam);
    case WM_TIMER:
        OnTimer(wParam);
        return 0;
    case WM_LBUTTONDOWN:
        {
            POINT pt;
            pt.x = LOWORD(lParam);
            pt.y = HIWORD(lParam);
            OnLButtonDown(wParam, pt);
            return 0;
        }
    case WM_RBUTTONDOWN:
        {
            POINT pt;
            pt.x = LOWORD(lParam);
            pt.y = HIWORD(lParam);
            OnRButtonDown(wParam, pt);
            return 0;
        }
    }

    return Window::WndProc(uMsg, wParam, lParam);
}

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

    // Setup the default background and tileset
    loadDefaultBackground();
    loadDefaultTileset();

    return 0;
}

void Board::OnDestroy() {
    // Release tileset and background resources
    releaseTileset();
    releaseBackground();

    // Delete the paused bitmap, if any
    if(m_hBmpPaused)
        DeleteObject(m_hBmpPaused);
    m_hBmpPaused = NULL;

    // Kill the undraw timer, if any
    if(m_nTimer)
        KillTimer(m_hWnd, m_nTimer);
    m_nTimer = 0;

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

BOOL Board::OnEraseBkgnd(HDC hDC) {
    return FALSE;   // OnPaint will take care of the background
}

void Board::OnPaint() {
    RECT rcUpdate;
    GetUpdateRect(&rcUpdate, FALSE);
    int rcUpdateWidth = rcUpdate.right - rcUpdate.left;
    int rcUpdateHeight = rcUpdate.bottom - rcUpdate.top;

    PAINTSTRUCT ps;
    BeginPaint(&ps);

    // Create the backing DC
    HDC dcBacking = CreateCompatibleDC(ps.hdc);
    HBITMAP bmpBacking, hOldBacking;
    bmpBacking = CreateCompatibleBitmap(ps.hdc, rcUpdateWidth, rcUpdateHeight);
    hOldBacking = (HBITMAP)SelectObject(dcBacking, bmpBacking);

    // Prepare DC for painting
    if(ps.fErase)
        eraseBackground(dcBacking, rcUpdate);
    else
        BitBlt(dcBacking, 0, 0, rcUpdateWidth,
                        rcUpdateHeight, ps.hdc, rcUpdate.left, rcUpdate.top, SRCCOPY);
    
    if(paused) {
        // Game is paused; draw the paused bitmap

        // Load paused bitmap, if it is not already loaded
        if(!m_hBmpPaused) {
            m_hBmpPaused = LoadBitmap(App::GetHInstance(), MAKEINTRESOURCE(IDB_PAUSED));
        }

        // Centre bitmap on screen
        BITMAP bm;
        RECT rcClient;
        GetClientRect(&rcClient);
        GetObject(m_hBmpPaused, sizeof(BITMAP), &bm);
        int x = (rcClient.right - rcClient.left - bm.bmWidth)/2;
        int y = (rcClient.bottom - rcClient.top - bm.bmHeight)/2;

        // Draw the bitamp
        CBmpUtil::DrawBitmap(dcBacking, m_hBmpPaused,
                                x-rcUpdate.left, y-rcUpdate.top,
                                bm.bmWidth, bm.bmHeight);
    } else {
        // Draw the tiles
        for(int i = 0; i < x_tiles(); i++)
            for(int j = 0; j < y_tiles(); j++) {
                if(getField(i, j) == EMPTY)
                    continue;

                int xpos = XBORDER + i * m_tileWidth;
                int ypos = YBORDER + j * m_tileHeight;
                RECT ri, r;
                SetRect(&r, xpos, ypos, xpos + m_tileWidth, ypos + m_tileHeight);

                // Only bother to draw a tile if it is in the update region
                if(IntersectRect(&ri, &rcUpdate, &r))
                    drawTile(dcBacking, getField(i, j)-1,
                                xpos-rcUpdate.left, ypos-rcUpdate.top,
                                (i == mark_x && j == mark_y));
            }
    }

    // Blit the backing DC to the window
    BitBlt(ps.hdc, rcUpdate.left, rcUpdate.top, rcUpdateWidth, rcUpdateHeight,
                    dcBacking, 0, 0, SRCCOPY);

    // Cleanup
    EndPaint(&ps);
    SelectObject(dcBacking, hOldBacking);
    DeleteObject(bmpBacking);
    DeleteDC(dcBacking);
}

void Board::OnLButtonDown(UINT nFlags, POINT point) {
    if(!m_bNoEvents && !paused) {
        // calculate position
        int pos_x = (point.x - XBORDER <0)?-1:
                                  (point.x - XBORDER) / m_tileWidth;
        int pos_y = (point.y - YBORDER <0)?-1:
                                  (point.y - YBORDER) / m_tileHeight;

        // Mark tile
        if(highlighted_tile != -1) {
            int oldmarkx = mark_x;
            int oldmarky = mark_y;

            mark_x=-1; mark_y=-1;
            for(int i = 0; i < x_tiles(); i++)
                for(int j = 0; j < y_tiles(); j++) {
                    if( highlighted_tile == getField(i, j))
                        updateField(i, j);          
                }
            mark_x = oldmarkx; 
            mark_y = oldmarky;   // no tile selected
            highlighted_tile = -1;
        }

        if(pos_x >= 0 && pos_x < x_tiles() && pos_y >= 0 && pos_y < y_tiles())
            marked(pos_x, pos_y);  
    }
}

void Board::OnRButtonDown(UINT nFlags, POINT point) {
    if(m_bNoEvents)
        return;

    // calculate position
    int pos_x = (point.x - XBORDER <0)?-1:
                              (point.x - XBORDER) / m_tileWidth;
    int pos_y = (point.y - YBORDER <0)?-1:
                              (point.y - YBORDER) / m_tileHeight;

    int field = getField(pos_x,pos_y);

    if(paused || (field == EMPTY)) {
        // Right clicked on background; display popup menu
        HMENU hGameMenu = GetSubMenu(GetMenu(App::GetMainHWnd()), 1);
        ClientToScreen(m_hWnd, &point);
        TrackPopupMenu(hGameMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON,
                                    point.x, point.y, 0, GetParent(m_hWnd), NULL);
        return;
    }

    // Assist by lighting all tiles of same type
    highlighted_tile = field;

    BOOL bUpdateWindow;
    for(int i = 0; i < x_tiles(); i++)
        for(int j = 0; j < y_tiles(); j++) {
            if( field == getField(i, j)) {
                mark_x=i; mark_y=j;
                bUpdateWindow = TRUE;
            } else {
                bUpdateWindow = FALSE;
                mark_x=-1; mark_y=-1;
            }
            updateField(i, j);
            if(bUpdateWindow)
                UpdateWindow();
        }
    mark_x=-1; mark_y=-1;   // no tile selected
}

void Board::OnTimer(UINT nIDEvent) {
    // Kill the undraw timer and undraw the arrow
    KillTimer(m_hWnd, m_nTimer);
    m_nTimer = 0;
    undrawArrow();
}
<< Back to Shisen-Sho