C++ Tutorials‎ > ‎

QuickDraw: TicTacToe Demo

By this point, we have nearly covered enough ground to actually do something using the Mac toolkit. So in this part of the tutorial we are going to make a simple TicTacToe game. When it's done it'll look something like this:


The code for this section is fairly long, so I've listed it at the bottom. I'll highlight the relevant parts here.

class Board {
// lots of functions

First we declare a Board class which contains the TicTacToe board and handles all the game logic. None of it is really Mac-related code so I won't go over it here.

if (menu == menuFile) {
    if (item == menuitemNew) {
        WindowPtr window = GetNewWindow(defaultWindow, nil, (WindowPtr) -1);
        Str255 name = "\pTicTacToe";
        SetWTitle(window, name);
        Board* game = new Board();
        SetWRefCon(window, (long) game);

In our doMenus() function, we're going to change the behavior that occurs when the user creates a new window. First we'll call the SetWTitle() function so that our windows don't just say "New Window" at the top. The next thing we'll do is create a new Board object and put a pointer to it in the Window object. Windows have a RefCon field that is used to keep a pointer to whatever data is stored inside the window (for example, the contents of a text document if you were writing a text editor.) The SetWRefCon() and GetWRefCon() functions allow you to access this field.

} else if (clickedPart == inGoAway) {
    if (TrackGoAway(clickedWindow, event.where, clickedPart)) {
        Board* game = (Board*) GetWRefCon(clickedWindow);
        delete game;
        DisposeWindow(clickedWindow);
    }

One thing that we have to make sure to do is delete the Board object we created when a window is closed. We have to do this before calling DisposeWindow() or we'll leak memory.

Throughout the event loop are calls to a new function we defined called drawWindow(). This function is responsible for redrawing the contents of the window whenever it needs to be updated: when the window is brought to the foreground or when the window gets an updateEvt. Inside the drawWindow function is some interesting code, so let's look at it in detail.

void drawWindow(WindowPtr window) {
    Board* game = (Board*) GetWRefCon(window);

    SetPort(window);

    Rect rect;

    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++) {
            if (game->board[r][c] == 1) {
                ForeColor(redColor);
                SetRect(&rect, r*boxSize, c*boxSize, (r+1)*boxSize, (c+1)*boxSize);
            } else if (game->board[r][c] == 2) {
                ForeColor(blueColor);
                SetRect(&rect, r*boxSize, c*boxSize, (r+1)*boxSize, (c+1)*boxSize);
            } else {
                SetRect(&rect, 0,0,0,0);
            }
            PaintRect(&rect);
        }
    }
}

The drawWindow() function takes a WindowPtr as an argument, so the first thing we have to do is extract the Board object using GetWRefCon(). Then we call SetPort() which tells QuickDraw where we want to draw at. We then loop through the game board and draw a series of rectangles using three calls: First we call ForeColor() to set the color that will be used by QuickDraw. Then we call SetRect to set the dimensions of the rectangle we want to draw. Finally, we call PaintRect to do the actual drawing.

Another new function is the doClick() function. It handles the user clicking inside the window, and all it does is figure out which square they clicked on and then passes that information on to the Board object, which does the work of figuring out if it is a valid move, and so forth. There is one function worth mentioning in doClick(), which is the GlobalToLocal() function. It translates a Point object from world coordinates (the coordinates of the entire screen, with (0,0) being at the top left of the screen) to local coordinates, with (0,0) being at the top left of the current port (which in this case is the window we're clicking in.)

One minor change needs to be made to the resource file before this program will work properly. Go to the WIND resource and change both its width and height parameters to 225.

Here is the complete code listing:

#include <MacWindows.h>
#include <Events.h>
#include <Dialogs.h>
#include <ToolUtils.h>
#include <Sound.h>

// constants
const short defaultMenubar = 128;
const short menuApple = 128;
const short menuFile = 129;
const short menuitemAbout = 1;
const short menuitemNew = 1;
const short menuitemQuit = 2;
const short defaultWindow = 128;

const short boxSize = 75;

// globals
if __MWERKS__ == 0
QDGlobals qd;
#endif

/*******************/
/* TicTacToe Board */
/*******************/

class Board {
public:
    Board();

    short checkWinner();
    void doEnemyMove();
    bool doPlayerMove(short r, short c);

    short board[3][3];
};

Board::Board() {
    // initialize the board to a blank state
    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++) {
            board[r][c] = 0;
        }
    }
}

short Board::checkWinner() {
    for (int i = 0; i < 3; i++) {
        // check rows
        if (board[i][0] != 0 && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
            return board[i][0];
        }
        // check columns
        if (board[0][i] != 0 && board[0][i] == board[1][i] && board[1][i] == board[2][i]) {
            return board[0][i];
        }
    }
    // check diagonals
    if (board[1][1] != 0) {
        if ((board[0][0] == board[1][1] && board[1][1] == board[2][2]) ||
            (board[0][2] == board[1][1] && board[1][1] == board[2][0])) {
            return board[1][1];
        }
    }
    return 0;
}

bool Board::doPlayerMove(short r, short c) {
    if (board[r][c] == 0) {
        board[r][c] = 1;
        short won = checkWinner();
        if (won == 0) {
            doEnemyMove();
            won = checkWinner();
        }
        if (won != 0) {
            // Set all tiles to the winner's color to indicate a win.
            for (int r = 0; r < 3; r++) {
                for (int c = 0; c < 3; c++) {
                    board[r][c] = won;
                }
            }
        }
        return true;
    }
    return false;
}

void Board::doEnemyMove() {
    // super dumb behavior, just find the first empty space and fill it.
    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++) {
            if (board[r][c] == 0) {
                board[r][c] = 2;
                return;
            }
        }
    }
}

// function prototypes
void Initialize();
void MainLoop();
void Terminate();
void doMenuBar(long menuAction);

void main() {
    Initialize();

    MainLoop();

    Terminate();
}

// Initialize
void Initialize() {
    InitGraf(&qd.thePort);
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(nil);
    InitCursor();

    Handle menubar = GetNewMBar(defaultMenubar);
    SetMenuBar(menubar);
    DrawMenuBar();
}

void MainLoop() {

    EventRecord event;

    while (true) {
        if (WaitNextEvent(everyEvent, &event, 10L, nil)) {
            if (event.what == mouseDown) {
                WindowPtr clickedWindow;
                short clickedPart = FindWindow(event.where, &clickedWindow);
                if (clickedPart == inMenuBar) {
                    doMenuBar(MenuSelect(event.where));
                } else if (clickedPart == inDrag) {
                    DragWindow(clickedWindow, event.where, &qd.screenBits.bounds);
                } else if (clickedPart == inGoAway) {
                    if (TrackGoAway(clickedWindow, event.where, clickedPart)) {
                        Board* game = (Board*) GetWRefCon(clickedWindow);
                        delete game;
                        DisposeWindow(clickedWindow);
                    }
                } else if (clickedPart == inContent) {
                    if (clickedWindow != FrontWindow()) {
                        SelectWindow(clickedWindow);
                        drawWindow(clickedWindow);
                    } else {
                        doClick(clickedWindow, event.where);
                    }
                }
            } else if (event.what == updateEvt) {
                BeginUpdate((WindowPtr) event.message);
                drawWindow((WindowPtr) event.message);
                EndUpdate((WindowPtr) event.message);
            }
        }
    }
}

void Terminate() {
    ExitToShell();
}

void drawWindow(WindowPtr window) {
    Board* game = (Board*) GetWRefCon(window);

    SetPort(window);

    Rect rect;

    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++) {
            if (game->board[r][c] == 1) {
                ForeColor(redColor);
                SetRect(&rect, r*boxSize, c*boxSize, (r+1)*boxSize, (c+1)*boxSize);
            } else if (game->board[r][c] == 2) {
                ForeColor(blueColor);
                SetRect(&rect, r*boxSize, c*boxSize, (r+1)*boxSize, (c+1)*boxSize);
            } else {
                SetRect(&rect, 0,0,0,0);
            }
            PaintRect(&rect);
        }
    }
}

void doClick(WindowPtr window, Point where) {
    SetPort(window);
    GlobalToLocal(&where);

    short c = where.v / boxSize;
    short r = where.h / boxSize;
    if (c >= 3) {c = 2;}
    if (c < 0) {c = 0;}
    if (r >= 3) {r = 2;}
    if (r < 0) {r = 0;}

    Board* game = (Board*) GetWRefCon(window);
    if (game->doPlayerMove(r, c)) {
        drawWindow(window);
    }
}

void doMenuBar(long menuAction) {
    if (menuAction <= 0) {
        return;
    }

    short menu = HiWord(menuAction);
    short item = LoWord(menuAction);
    if (menu == menuApple) {
        if (item == menuitemAbout) {
            SysBeep(1);
        }
    } else if (menu == menuFile) {
        if (item == menuitemNew) {
            WindowPtr window = GetNewWindow(defaultWindow, nil, (WindowPtr) -1);
            Str255 name = "\pTicTacToe";
            SetWTitle(window, name);
            Board* game = new Board();
            SetWRefCon(window, (long) game);
        } else if (item == menuitemQuit) {
            Terminate();
        }
    }
    HiliteMenu(0);
}

Once you get the code compiled, you should be able to run it and start a new game by clicking File->New. Click inside the window to make a move; the computer opponent will move after you do. (Your tiles are red, theirs are blue.) If one player beats the other, the window will change to the color of the winning player. You can then start a new game.

Obviously there is a lot that can be done to improve this simple game. I used colored rectangles instead of X's and O's because it simplified the code, but you could easily add better graphics. See Inside Macintosh: Imaging with QuickDraw for more information on how to do this. The AI of the computer opponent is also pretty terrible; almost anything would be an improvement. You could also add a two-player mode, or even expand it into a more complicated and interesting game, like checkers. At this point you've learned enough to start writing useful programs, so get out there and create!

Next: QuickDraw: Text Demo




Comments