// 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. // // Phase 4.1 migration: menu mini-format strings, AlertTemplate, // NewWindowParm boilerplate folded into iigs/uiBuilder.h. #include "iigs/toolbox.h" #include "iigs/desktop.h" #include "iigs/uiBuilder.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 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 { short num; unsigned char moves[60]; } MoveList; // --- alphabetised forward decls ----------------------------------- static void checkForDone(void); static void doContent(void); static short endScore(const unsigned char *board); static void findMove(short col); static void getMoves(const unsigned char *board, short color, MoveList *out); static short legalMove(short idx, short color); static void makeAMove(short idx, short col); static void menuAbout(void); static void menuColor(void); static void menuPass(void); static void menuSelfPlay(void); static void menuSetPly(short menuNum); static void newGame(void); static void onAbout(uint16_t cmdId); static void onMenuPick(uint16_t menuId, uint16_t itemId); static void onNewGame(uint16_t cmdId); static void onPass(uint16_t cmdId); static void onPlyN(uint16_t cmdId); static void onQuit(uint16_t cmdId); static void onSelfPlay(uint16_t cmdId); static void onTogglePlayer(uint16_t cmdId); static void scoreString(unsigned short bcnt, unsigned short wcnt); static short score(const unsigned char *board); static short scoreMove(unsigned char *board, short idx, short col, short level); static void drawBoard(void); static void drawMovesList(void); static void drawScore(void); static void drawSquare(short sq, short col); static void initWindows(void); static void moveNotation(short idx); static void plot(short h, short v); static void tryMove(void); static void update(void); static void handleMenuLegacy(unsigned short menuNum); 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. 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 }; // --- menu spec via uiBuilder -------------------------------------- static const UiMenuItemT gEditItems[] = { { edit_UndoLastMove, "Undo Last Move", 'Z', MI_DISABLED }, { 512, (const char *)0, 0, MI_DIVIDER | MI_DISABLED }, { 271, "Cut", 'X', MI_DISABLED }, { 272, "Copy", 'C', MI_DISABLED }, { 273, "Paste", 'V', MI_DISABLED }, { 274, "Clear", 0, MI_DISABLED }, }; static const UiMenuItemT gLevelItems[] = { { level_1Ply, "1 Ply", 0, 0 }, { level_2Ply, "2 Ply", 0, 0 }, { level_3Ply, "3 Ply", 0, 0 }, { level_4Ply, "4 Ply", 0, 0 }, { level_5Ply, "5 Ply", 0, 0 }, { level_6Ply, "6 Ply", 0, 0 }, { level_7Ply, "7 Ply", 0, 0 }, { level_8Ply, "8 Ply", 0, 0 }, }; static const UiMenuItemT gOptionsItems[] = { { options_SelfPlay, "Self Play", 0, 0 }, { options_ComputerPlaysWhite, "Computer Plays Black", 0, 0 }, { 514, (const char *)0, 0, MI_DIVIDER | MI_DISABLED }, { options_Pass, "Pass", 0, 0 }, { options_ShowScoreWindow, "Show Score Window", 0, 0 }, { options_ShowMovesWindow, "Show Moves Window", 0, 0 }, }; static const UiMenuItemT gFileItems[] = { { file_NewGame, "New Game", 'N', 0 }, { 513, (const char *)0, 0, MI_DIVIDER | MI_DISABLED }, { file_Quit, "Quit", 'Q', 0 }, }; static const UiMenuItemT gAppleItems[] = { { apple_AboutReversi, "About Reversi", 0, 0 }, }; static const UiMenuT gMenus[] = { { 1, "Apple", MN_APPLE, 1, gAppleItems }, { 2, " File", 0, 3, gFileItems }, { 3, " Edit", 0, 6, gEditItems }, { 4, " Level", 0, 8, gLevelItems }, { 5, " Options", 0, 6, gOptionsItems }, }; static const UiCmdHandlerT gCmdTable[] = { { apple_AboutReversi, onAbout }, { file_NewGame, onNewGame }, { file_Quit, onQuit }, { level_1Ply, onPlyN }, { level_2Ply, onPlyN }, { level_3Ply, onPlyN }, { level_4Ply, onPlyN }, { level_5Ply, onPlyN }, { level_6Ply, onPlyN }, { level_7Ply, onPlyN }, { level_8Ply, onPlyN }, { options_SelfPlay, onSelfPlay }, { options_ComputerPlaysWhite, onTogglePlayer }, { options_Pass, onPass }, }; static void *gBoardWin, *gScoreWin, *gMovesWin; static WmTaskRec gEvent; static volatile unsigned short gDone; // --- 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; } 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; } static void makeAMove(short idx, short col) { gMoves[++gMovesMade] = idx; 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); 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) { uiBuilderAlert(UA_NOTE, "I cannot move, so I\rmust pass.\r"); 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); PaintRect(&r); 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]); } } } static unsigned char gScoreBuf[21]; static void scoreString(unsigned short bcnt, unsigned short wcnt) { 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; unsigned short 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); } 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); 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; unsigned short wcnt = 0; for (short i = 11; i < 90; i++) { if (gBoard[i] == blackPiece) { bcnt++; } else if (gBoard[i] == whitePiece) { wcnt++; } } if (wcnt == bcnt) { uiBuilderAlert(UA_NOTE, "The game is over. It\ris a draw."); } else if (wcnt > bcnt) { uiBuilderAlert(UA_NOTE, "White wins the game."); } else { uiBuilderAlert(UA_NOTE, "Black wins the game."); } 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 { uiBuilderAlert(UA_STOP, "Illegal move -\rtry again."); } 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 { uiBuilderAlert(UA_STOP, "You have legal moves\rso you cannot pass.\r"); } } 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) { uiBuilderAlert(UA_NOTE, "Reversi 1.0\r" "Copyright 1989\r" "Byte Works, Inc.\r\r" "By Mike Westerfield"); } static void onAbout(uint16_t cmdId) { (void)cmdId; menuAbout(); } static void onNewGame(uint16_t cmdId) { (void)cmdId; newGame(); } static void onQuit(uint16_t cmdId) { (void)cmdId; gDone = 1; } static void onPlyN(uint16_t cmdId) { menuSetPly((short)cmdId); } static void onSelfPlay(uint16_t cmdId) { (void)cmdId; menuSelfPlay(); } static void onTogglePlayer(uint16_t cmdId) { (void)cmdId; menuColor(); } static void onPass(uint16_t cmdId) { (void)cmdId; menuPass(); } static void onMenuPick(uint16_t menuId, uint16_t itemId) { (void)menuId; uiBuilderDispatch(itemId, gCmdTable, (uint16_t)(sizeof gCmdTable / sizeof gCmdTable[0])); HiliteMenu(0, (unsigned short)(gEvent.wmTaskData >> 16)); } static void handleMenuLegacy(unsigned short menuNum) { onMenuPick(0, (uint16_t)menuNum); } // --- init ---------------------------------------------------------- static void initWindows(void) { UiWindowT spec; // Board window: 0x80E4 = fTitle | fVis | fMove | fInfo + fPage spec.title = "Reversi"; spec.frameBits = 0x80E4; spec.position.v1 = 32; spec.position.h1 = 32; spec.position.v2 = (int16_t)(32 + squareHeight * 8); spec.position.h2 = (int16_t)(32 + squareWidth * 8); spec.maxHeight = (int16_t)(squareHeight * 8); spec.maxWidth = (int16_t)(squareWidth * 8); spec.refCon = 0; spec.contentDefProc = (void *)0; gBoardWin = uiBuilderOpenWindow(&spec); // Score window: 0xC0C4 = fTitle | fClose | fVis | fMove | fInfo spec.title = "Scores"; spec.frameBits = 0xC0C4; spec.position.v1 = 32; spec.position.h1 = (int16_t)(640 - 32 - 200); spec.position.v2 = 61; spec.position.h2 = (int16_t)(640 - 32); spec.maxHeight = 29; spec.maxWidth = 200; gScoreWin = uiBuilderOpenWindow(&spec); // Moves window. spec.title = "Moves"; spec.frameBits = 0xC0C4; spec.position.v1 = 80; spec.position.h1 = (int16_t)(640 - 32 - 100); spec.position.v2 = 192; spec.position.h2 = (int16_t)(640 - 32); spec.maxHeight = 112; spec.maxWidth = 100; gMovesWin = uiBuilderOpenWindow(&spec); SelectWindow(gBoardWin); } int main(void) { unsigned short userId = startdesk(640); (void)userId; paintDesktopBackdrop(); uiBuilderInstallMenuBar(gMenus, (uint16_t)(sizeof gMenus / sizeof gMenus[0])); CheckMItem(1, level_1Ply); initWindows(); newGame(); gEvent.wmTaskMask = 0x13FFL; ShowCursor(); *(volatile unsigned char *)0x70 = 0x99; gDone = 0; unsigned short watchdog = 0; do { unsigned short event = TaskMaster(0x074E, (void *)&gEvent); switch (event) { case wInSpecial: case wInMenuBar: handleMenuLegacy((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; }