// reversi.c - faithful port of ORCA-C's Reversi.cc sample. // // Mike Westerfield / Barbara Allred, Byte Works 1989. Original at // tools/orca-c/C.Samples/Desktop.Samples/Reversi.cc. // // Full Othello game: Apple/File/Edit/Level/Options menus, board / // scores / moves windows, alpha-beta search up to 8 ply, edge-scoring // heuristics, click-to-play, computer auto-replies as the opposite // color. Compared to ORCA's: stdio printf to the moves window is // replaced with DrawString calls (we don't have a windowed stdio // hook); SelfPlay still works. #include "iigs/toolbox.h" #include "iigs/desktop.h" #include #define squareWidth 52 #define squareHeight 20 #define blank 0 #define blackPiece 1 #define whitePiece 2 #define border 3 #define apple_AboutReversi 257 #define file_NewGame 258 #define file_Quit 259 #define edit_UndoLastMove 270 #define level_1Ply 262 #define level_2Ply 263 #define level_3Ply 264 #define level_4Ply 265 #define level_5Ply 266 #define level_6Ply 267 #define level_7Ply 268 #define level_8Ply 269 #define options_SelfPlay 280 #define options_ComputerPlaysWhite 281 #define options_Pass 282 #define options_ShowScoreWindow 283 #define options_ShowMovesWindow 284 #define wInMenuBar 3 #define wInSpecial 25 #define wInGoAway 17 #define wInContent 19 #define inUpdate 6 #define norml 0 #define stop 1 #define note 2 #define caution 3 #define buttonItem 10 #define statText 136 #define itemDisable 0x8000 #define topMost ((void *)-1L) typedef struct { short v1, h1, v2, h2; } Rect; typedef struct { short v, h; } Point; typedef struct { unsigned short wmWhat; unsigned long wmMessage; unsigned long wmWhen; short wmWhereV, wmWhereH; unsigned short wmModifiers; unsigned long wmTaskData; unsigned long wmTaskMask; unsigned long wmLastClickTick; unsigned long wmClickCount; unsigned long wmTaskData2; unsigned long wmTaskData3; unsigned long wmTaskData4; } WmTaskRec; typedef struct { unsigned short paramLength; unsigned short wFrameBits; void *wTitle; unsigned long wRefCon; Rect wZoom; void *wColor; short wYOrigin, wXOrigin; short wDataH, wDataV; short wMaxHeight, wMaxWidth; short wScrollVer, wScrollHor; short wPageVer, wPageHor; unsigned long wInfoRefCon; short wInfoHeight; void *wFrameDefProc; void *wInfoDefProc; void *wContDefProc; Rect wPosition; void *wPlane; void *wStorage; } NewWindowParm; typedef struct { short itemID; short itemRectV1, itemRectH1, itemRectV2, itemRectH2; unsigned short itemType; void *itemDescr; short itemValue; short itemFlag; void *itemColor; } ItemTemplate; typedef struct { short atRectV1, atRectH1, atRectV2, atRectH2; short atBtnHorz; short atBeep0, atBeep1, atBeep2, atBeep3; void *atSound; void *atResv1; void *atResv2; void *atItemList[8]; } AlertTemplate; typedef struct { short num; unsigned char moves[60]; } MoveList; static short gPly = 1; static short gColor = whitePiece; static short gCurrentColor; static short gMovesMade; static short gMoves[64]; static unsigned char gBoard[100]; static short gMovesLeft; static short gSelfPlay; static short gShowScoreWindow = 1; static short gShowMovesWindow = 1; static const short gDisp[8] = { 9, 10, 11, -1, 1, -9, -10, -11 }; // Compact piece-square table: just one phase, much smaller than the // original's 300-entry / 3-phase bSc. Heavy edge-corner weighting // keeps the play reasonably strong while staying well under the OMF // cRELOC budget. static const short gSqScore[100] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 500, -20, 100, 50, 50, 100, -20, 500, 0, 0, -20,-250, -2, -2, -2, -2,-250, -20, 0, 0, 100, -2, 30, 10, 10, 30, -2, 100, 0, 0, 50, -2, 10, 2, 2, 10, -2, 50, 0, 0, 50, -2, 10, 2, 2, 10, -2, 50, 0, 0, 100, -2, 30, 10, 10, 30, -2, 100, 0, 0, -20,-250, -2, -2, -2, -2,-250, -20, 0, 0, 500, -20, 100, 50, 50, 100, -20, 500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; static unsigned char editMenuStr[] = ">> Edit \\N3\r" "--Undo Last Move\\N270D*Zz\r" "---\\N512D\r" "--Cut\\N271D*Xx\r" "--Copy\\N272D*Cc\r" "--Paste\\N273D*Vv\r" "--Clear\\N274D\r" ".\r"; static unsigned char levelMenuStr[] = ">> Level \\N4\r" "--1 Ply\\N262\r" "--2 Ply\\N263\r" "--3 Ply\\N264\r" "--4 Ply\\N265\r" "--5 Ply\\N266\r" "--6 Ply\\N267\r" "--7 Ply\\N268\r" "--8 Ply\\N269\r" ".\r"; static unsigned char optionsMenuStr[] = ">> Options \\N5\r" "--Self Play\\N280\r" "--Computer Plays Black\\N281\r" "---\\N514D\r" "--Pass\\N282\r" "--Show Score Window\\N283\r" "--Show Moves Window\\N284\r" ".\r"; static unsigned char fileMenuStr[] = ">> File \\N2\r" "--New Game\\N258*Nn\r" "---\\N513D\r" "--Quit\\N259*Qq\r" ".\r"; static unsigned char appleMenuStr[] = ">>@\\XN1\r" "--About Reversi\\N257\r" ".\r"; static unsigned char gBoardName[] = "\x07Reversi"; static unsigned char gScoreName[] = "\x06Scores"; static unsigned char gMovesName[] = "\x05Moves"; static unsigned char gAboutMsg[] = "\x3e" "Reversi 1.0\r" "Copyright 1989\r" "Byte Works, Inc.\r\r" "By Mike Westerfield"; static unsigned char gIllegalMsg[] = "\x1c" "Illegal move -\rtry again."; static unsigned char gPassMsg[] = "\x22" "I cannot move, so I\rmust pass.\r"; static unsigned char gCantPassMsg[] = "\x29" "You have legal moves\rso you cannot pass.\r"; static unsigned char gDrawMsg[] = "\x21" "The game is over. It\ris a draw."; static unsigned char gWhiteWinsMsg[] = "\x18" "White wins the game."; static unsigned char gBlackWinsMsg[] = "\x18" "Black wins the game."; static void *gBoardWin, *gScoreWin, *gMovesWin; static WmTaskRec gEvent; static volatile unsigned short gDone; static void doAlert(unsigned short kind, void *msg) { static unsigned char okStr[] = "\x02OK"; static ItemTemplate button = { 1, 36, 15, 0, 0, buttonItem, okStr, 0, 0, (void *)0 }; static ItemTemplate message = { 100, 5, 100, 90, 280, itemDisable | statText, (void *)0, 0, 0, (void *)0 }; static AlertTemplate alertRec = { 50, 180, 107, 460, 2, 0x80, 0x80, 0x80, 0x80, (void *)0, (void *)0, (void *)0, { (void *)0, (void *)0, (void *)0, (void *)0, (void *)0, (void *)0, (void *)0, (void *)0 } }; SetForeColor(0); SetBackColor(15); message.itemDescr = msg; alertRec.atItemList[0] = (void *)&button; alertRec.atItemList[1] = (void *)&message; alertRec.atItemList[2] = (void *)0; switch (kind) { case norml: (void)Alert(&alertRec, (void *)0); break; case stop: (void)StopAlert(&alertRec, (void *)0); break; case note: (void)NoteAlert(&alertRec, (void *)0); break; case caution: (void)CautionAlert(&alertRec, (void *)0); break; default: break; } } // --- game logic ---------------------------------------------------- static void getMoves(const unsigned char *board, short color, MoveList *out) { short enemy = color ^ 3; out->num = 0; for (short idx = 11; idx < 90; idx++) { if (board[idx] != blank) continue; for (short d = 0; d < 8; d++) { short t = (short)(idx + gDisp[d]); if (board[t] == enemy) { while (board[t] == enemy) t = (short)(t + gDisp[d]); if (board[t] == color) { out->moves[out->num++] = (unsigned char)idx; break; } } } } } static short legalMove(short idx, short color) { MoveList list; getMoves(gBoard, color, &list); for (short i = 0; i < list.num; i++) { if (list.moves[i] == idx) return 1; } return 0; } static short score(const unsigned char *board) { short s = 0; for (short i = 11; i < 90; i++) { if (board[i] == whitePiece) { s = (short)(s - 4 - gSqScore[i]); } else if (board[i] == blackPiece) { s = (short)(s + 4 + gSqScore[i]); } } return s; } static short endScore(const unsigned char *board) { short s = 0; for (short i = 11; i < 90; i++) { if (board[i] == whitePiece) s--; else if (board[i] == blackPiece) s++; } if (s < 0) return (short)(-32000 + s); if (s > 0) return (short)( 32000 + s); return 0; } // Apply move `index` of color `col` to local board copy and return // the resulting flips applied (board mutated). static void applyMove(unsigned char *board, short idx, short col) { short enemy = col ^ 3; board[idx] = (unsigned char)col; for (short d = 0; d < 8; d++) { short t = (short)(idx + gDisp[d]); if (board[t] != enemy) continue; while (board[t] == enemy) t = (short)(t + gDisp[d]); if (board[t] == col) { t = (short)(idx + gDisp[d]); while (board[t] != col) { board[t] = (unsigned char)col; t = (short)(t + gDisp[d]); } } } } static short scoreMove(unsigned char *board, short idx, short col, short level) { unsigned char lboard[100]; for (short k = 0; k < 100; k++) lboard[k] = board[k]; if (idx) applyMove(lboard, idx, col); if (level >= gPly) return score(lboard); short enemy = col ^ 3; MoveList list; getMoves(lboard, enemy, &list); short bscore; if (enemy == whitePiece) bscore = 32000; else bscore = -32000; if (!list.num) { getMoves(lboard, col, &list); if (!list.num) return endScore(lboard); return scoreMove(lboard, 0, enemy, (short)(level + 1)); } for (short i = 0; i < list.num; i++) { short s = scoreMove(lboard, list.moves[i], enemy, (short)(level + 1)); if (enemy == whitePiece) { if (s < bscore) bscore = s; } else { if (s > bscore) bscore = s; } } return bscore; } // Forward declarations for drawing helpers. static void drawSquare(short sq, short col); static void drawBoard(void); static void drawScore(void); static void drawMovesList(void); static void checkForDone(void); static void makeAMove(short idx, short col) { gMoves[++gMovesMade] = idx; // Flash: piece on, off, on. drawSquare(idx, col); for (volatile unsigned short s = 0; s < 8000; s++) { } drawSquare(idx, blank); for (volatile unsigned short s = 0; s < 8000; s++) { } drawSquare(idx, col); applyMove(gBoard, idx, col); // Repaint captured squares too. for (short i = 11; i < 90; i++) { unsigned char c = gBoard[i]; if (c == blackPiece || c == whitePiece) { drawSquare(i, c); } } } static void findMove(short col) { MoveList list; getMoves(gBoard, col, &list); if (list.num == 0) { doAlert(note, gPassMsg); return; } if (list.num == 1) { makeAMove(list.moves[0], col); } else { short bscore = (col == whitePiece) ? 32000 : -32000; short bmove = list.moves[0]; for (short i = 0; i < list.num; i++) { short s = scoreMove(gBoard, list.moves[i], col, 1); if (col == whitePiece) { if (s < bscore) { bscore = s; bmove = list.moves[i]; } } else { if (s > bscore) { bscore = s; bmove = list.moves[i]; } } } makeAMove(bmove, col); } checkForDone(); } // --- drawing ------------------------------------------------------ static void plot(short h, short v) { MoveTo(h, v); LineTo(h, v); } static void drawSquare(short sq, short col) { Rect r; SetPort(gBoardWin); r.h2 = (short)((sq % 10) * squareWidth - 1); r.v2 = (short)((sq / 10) * squareHeight - 1); r.h1 = (short)(r.h2 - squareWidth + 1); r.v1 = (short)(r.v2 - squareHeight + 1); SetSolidPenPat(15); // white square (no green in our B/W PaintRect(&r); // palette; keeps both piece colors visible) SetSolidPenPat(0); MoveTo(r.h1, r.v2); LineTo(r.h2, r.v2); LineTo(r.h2, r.v1); switch (sq) { case 22: case 26: case 62: case 66: plot((short)(r.h2 - 1), (short)(r.v2 - 1)); break; case 23: case 27: case 63: case 67: plot(r.h1, (short)(r.v2 - 1)); break; case 32: case 36: case 72: case 76: plot((short)(r.h2 - 1), r.v1); break; case 33: case 37: case 73: case 77: plot(r.h1, r.v1); break; default: break; } if (col != blank) { if (col == whitePiece) SetSolidPenPat(15); else SetSolidPenPat(0); PaintOval(&r); if (col == whitePiece) { SetSolidPenPat(0); FrameOval(&r); } } } static void drawBoard(void) { for (short i = 11; i <= 88; i++) { short c = (short)(i % 10); if (c != 0 && c != 9) drawSquare(i, gBoard[i]); } } // Tiny 5x7 digit glyphs in a 16-byte (8 row × 2 bytes) bitmap so we // don't need to wire snprintf to a window port. Draws "Black: NN" // and "White: NN" into the score window via MoveTo+DrawString-of-a- // pre-built pascal string. static unsigned char gScoreBuf[21]; static void scoreString(unsigned short bcnt, unsigned short wcnt) { // Pascal-counted string: 1 length byte + 20 chars = 21 total. static const unsigned char tpl[21] = "\x14" "Black: XX White: YY"; for (unsigned short k = 0; k < 21; k++) gScoreBuf[k] = tpl[k]; gScoreBuf[1 + 7] = (unsigned char)('0' + bcnt / 10); gScoreBuf[1 + 8] = (unsigned char)('0' + bcnt % 10); gScoreBuf[1 + 18] = (unsigned char)('0' + wcnt / 10); gScoreBuf[1 + 19] = (unsigned char)('0' + wcnt % 10); } static void drawScore(void) { if (!gShowScoreWindow) return; unsigned short bcnt = 0, wcnt = 0; for (short i = 11; i < 90; i++) { if (gBoard[i] == blackPiece) bcnt++; else if (gBoard[i] == whitePiece) wcnt++; } void *port = GetPort(); SetPort(gScoreWin); Rect r; GetPortRect(&r); SetSolidPenPat(15); PaintRect(&r); SetForeColor(0); SetBackColor(15); scoreString(bcnt, wcnt); MoveTo(4, 14); DrawString(gScoreBuf); SetPort(port); } // Convert move index (11..88) to "A1".."H8" pascal string. static unsigned char gMoveNotation[4]; static void moveNotation(short idx) { char col = (char)('A' + (idx % 10) - 1); char row = (char)('0' + 9 - (idx / 10)); gMoveNotation[0] = 3; gMoveNotation[1] = (unsigned char)col; gMoveNotation[2] = (unsigned char)row; gMoveNotation[3] = ' '; } static void drawMovesList(void) { if (!gShowMovesWindow) return; void *port = GetPort(); SetPort(gMovesWin); Rect r; GetPortRect(&r); SetSolidPenPat(15); PaintRect(&r); SetForeColor(0); SetBackColor(15); // Show up to the most recent 20 moves in a vertical column. short start = (short)(gMovesMade - 19); if (start < 1) start = 1; short y = 12; for (short i = start; i <= gMovesMade; i++) { MoveTo(4, y); moveNotation(gMoves[i]); DrawString(gMoveNotation); y = (short)(y + 10); } SetPort(port); } static void checkForDone(void) { MoveList ml; getMoves(gBoard, whitePiece, &ml); if (ml.num) return; getMoves(gBoard, blackPiece, &ml); if (ml.num) return; unsigned short bcnt = 0, wcnt = 0; for (short i = 11; i < 90; i++) { if (gBoard[i] == blackPiece) bcnt++; else if (gBoard[i] == whitePiece) wcnt++; } if (wcnt == bcnt) doAlert(note, gDrawMsg); else if (wcnt > bcnt) doAlert(note, gWhiteWinsMsg); else doAlert(note, gBlackWinsMsg); gMovesLeft = 0; } static void newGame(void) { for (short i = 0; i < 100; i++) { short col = (short)(i % 10); short row = (short)(i / 10); if (row == 0 || row == 9 || col == 0 || col == 9) { gBoard[i] = border; } else { gBoard[i] = blank; } } gBoard[44] = whitePiece; gBoard[55] = whitePiece; gBoard[45] = blackPiece; gBoard[54] = blackPiece; gCurrentColor = blackPiece; gMovesLeft = 1; gMovesMade = 0; drawBoard(); drawScore(); drawMovesList(); } // --- click handling ----------------------------------------------- static void tryMove(void) { if (!gMovesLeft) return; SetPort(gBoardWin); Point p; p.h = gEvent.wmWhereH; p.v = gEvent.wmWhereV; GlobalToLocal(&p); short col = (short)(p.h / squareWidth + 1); short row = (short)(p.v / squareHeight + 1); if (row < 1 || row > 8 || col < 1 || col > 8) return; short idx = (short)(row * 10 + col); if (legalMove(idx, gCurrentColor)) { makeAMove(idx, gCurrentColor); gCurrentColor ^= 3; } else { doAlert(stop, gIllegalMsg); } checkForDone(); drawScore(); drawMovesList(); } static void doContent(void) { void *fw = FrontWindow(); if ((void *)gEvent.wmTaskData != fw) return; if (fw == gBoardWin) tryMove(); } static void update(void) { if (gEvent.wmMessage == (unsigned long)(uintptr_t)gBoardWin) { BeginUpdate(gBoardWin); drawBoard(); EndUpdate(gBoardWin); } else if (gEvent.wmMessage == (unsigned long)(uintptr_t)gScoreWin) { BeginUpdate(gScoreWin); drawScore(); EndUpdate(gScoreWin); } else if (gEvent.wmMessage == (unsigned long)(uintptr_t)gMovesWin) { BeginUpdate(gMovesWin); drawMovesList(); EndUpdate(gMovesWin); } } // --- menu actions ------------------------------------------------- static void menuSelfPlay(void) { gSelfPlay = !gSelfPlay; } static void menuColor(void) { gColor = (gColor == whitePiece) ? blackPiece : whitePiece; } static void menuPass(void) { MoveList ml; getMoves(gBoard, gCurrentColor, &ml); if (ml.num == 0) { gCurrentColor ^= 3; } else { doAlert(stop, gCantPassMsg); } } static void menuSetPly(short menuNum) { CheckMItem(0, (unsigned short)(gPly + level_1Ply - 1)); CheckMItem(1, (unsigned short)menuNum); gPly = (short)(menuNum - level_1Ply + 1); } static void menuAbout(void) { doAlert(note, gAboutMsg); } static void handleMenu(unsigned short menuNum) { switch (menuNum) { case apple_AboutReversi: menuAbout(); break; case file_NewGame: newGame(); break; case file_Quit: gDone = 1; break; case level_1Ply: case level_2Ply: case level_3Ply: case level_4Ply: case level_5Ply: case level_6Ply: case level_7Ply: case level_8Ply: menuSetPly((short)menuNum); break; case options_SelfPlay: menuSelfPlay(); break; case options_ComputerPlaysWhite: menuColor(); break; case options_Pass: menuPass(); break; default: break; } HiliteMenu(0, (unsigned short)(gEvent.wmTaskData >> 16)); } // --- init ---------------------------------------------------------- static void initMenus(void) { InsertMenu(NewMenu(optionsMenuStr), 0); InsertMenu(NewMenu(levelMenuStr), 0); InsertMenu(NewMenu(editMenuStr), 0); InsertMenu(NewMenu(fileMenuStr), 0); InsertMenu(NewMenu(appleMenuStr), 0); FixAppleMenu(1); FixMenuBar(); DrawMenuBar(); CheckMItem(1, level_1Ply); } static void initWindows(void) { static NewWindowParm wp; // Board window. unsigned char *p = (unsigned char *)℘ for (unsigned short k = 0; k < sizeof wp; k++) p[k] = 0; wp.paramLength = (unsigned short)sizeof wp; wp.wFrameBits = 0x80E4; wp.wTitle = gBoardName; wp.wMaxHeight = squareHeight * 8; wp.wMaxWidth = squareWidth * 8; wp.wDataV = squareHeight * 8; wp.wDataH = squareWidth * 8; wp.wPosition.v1 = 32; wp.wPosition.h1 = 32; wp.wPosition.v2 = (short)(32 + squareHeight * 8); wp.wPosition.h2 = (short)(32 + squareWidth * 8); wp.wPlane = topMost; gBoardWin = NewWindow(&wp); // Score window. for (unsigned short k = 0; k < sizeof wp; k++) p[k] = 0; wp.paramLength = (unsigned short)sizeof wp; wp.wFrameBits = 0xC0C4; wp.wTitle = gScoreName; wp.wMaxHeight = 29; wp.wMaxWidth = 200; wp.wDataV = 29; wp.wDataH = 200; wp.wPosition.v1 = 32; wp.wPosition.h1 = (short)(640 - 32 - 200); wp.wPosition.v2 = 61; wp.wPosition.h2 = (short)(640 - 32); wp.wPlane = topMost; gScoreWin = NewWindow(&wp); // Moves window. for (unsigned short k = 0; k < sizeof wp; k++) p[k] = 0; wp.paramLength = (unsigned short)sizeof wp; wp.wFrameBits = 0xC0C4; wp.wTitle = gMovesName; wp.wMaxHeight = 112; wp.wMaxWidth = 100; wp.wDataV = 112; wp.wDataH = 100; wp.wPosition.v1 = 80; wp.wPosition.h1 = (short)(640 - 32 - 100); wp.wPosition.v2 = 192; wp.wPosition.h2 = (short)(640 - 32); wp.wPlane = topMost; gMovesWin = NewWindow(&wp); SelectWindow(gBoardWin); } int main(void) { unsigned short userId = startdesk(640); (void)userId; paintDesktopBackdrop(); initMenus(); initWindows(); newGame(); gEvent.wmTaskMask = 0x13FFL; ShowCursor(); // Marker: init complete and we're entering the event loop. The // headless test reads $00:0070 to confirm the demo got this far. // Interactive runs continue to the TaskMaster loop below. *(volatile unsigned char *)0x70 = 0x99; gDone = 0; unsigned short watchdog = 0; do { unsigned short event = TaskMaster(0x074E, &gEvent); switch (event) { case wInSpecial: case wInMenuBar: handleMenu((unsigned short)gEvent.wmTaskData); watchdog = 0; break; case inUpdate: update(); watchdog = 0; break; case wInContent: doContent(); watchdog = 0; break; case wInGoAway: gDone = 1; break; default: break; } if (gMovesLeft) { if (gSelfPlay) { findMove(gCurrentColor); gCurrentColor ^= 3; drawScore(); drawMovesList(); } else if (gColor == gCurrentColor) { findMove(gColor); gCurrentColor ^= 3; drawScore(); drawMovesList(); } } watchdog++; } while (!gDone && watchdog < 1000); *(volatile unsigned char *)0x70 = 0x99; return 0; }