9 tic tac toe program to illustrate simple minimax searching
\r
13 Scott Gasch (SGasch) 18 Mar 2004
\r
18 ver1 : simple search
\r
19 ver2 : alpha beta search
\r
20 ver3 : added eval and depth on ab search, more efficient gaveover
\r
30 SQUARE g_sComputerPlays = O_MARK; // what side comp plays
\r
31 unsigned int g_uPly = 0;
\r
32 MOVE g_mvBest = { 0 };
\r
33 unsigned int g_uNodes = 0;
\r
35 //+----------------------------------------------------------------------------
\r
37 // Function: SquareContentsToChar
\r
39 // Synopsis: Helper function for DrawBoard
\r
41 // Arguments: IN SQUARE s - a square to return a char to represent
\r
43 // Returns: char - character representing square
\r
45 //+----------------------------------------------------------------------------
\r
46 char SquareContentsToChar(IN SQUARE s)
\r
68 //+----------------------------------------------------------------------------
\r
70 // Function: DrawBoard
\r
72 // Synopsis: Draw the board
\r
74 // Arguments: IN POSITION *p - pointer to a position whose board to draw
\r
78 //+----------------------------------------------------------------------------
\r
79 void DrawBoard(IN POSITION *p)
\r
83 for (y = 0; y < BOARD_SIZE; y++)
\r
85 for (x = 0; x < BOARD_SIZE; x++)
\r
87 printf("%c ", SquareContentsToChar(p->sBoard[y][x]));
\r
90 printf(" = %d\n", p->iHSums[y]);
\r
97 for (x = 0; x < BOARD_SIZE; x++)
\r
102 for (x = 0; x < BOARD_SIZE; x++)
\r
104 printf("%d ", p->iVSums[x]);
\r
109 ASSERT(X_OR_O(p->sWhoseTurn));
\r
110 printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn));
\r
113 //+----------------------------------------------------------------------------
\r
115 // Function: ClearBoard
\r
117 // Synopsis: Clear the board
\r
119 // Arguments: IN OUT POSITION *p - pointer to position whose board to clear
\r
123 //+----------------------------------------------------------------------------
\r
124 void ClearBoard(IN OUT POSITION *p)
\r
126 memset(p->sBoard, 0, sizeof(p->sBoard));
\r
127 memset(p->iHSums, 0, sizeof(p->iHSums));
\r
128 memset(p->iVSums, 0, sizeof(p->iVSums));
\r
129 p->iDSums[0] = p->iDSums[1] = 0;
\r
130 p->sWhoseTurn = X_MARK; // x's go first
\r
131 p->uNumEmpty = (BOARD_SIZE * BOARD_SIZE);
\r
134 //+----------------------------------------------------------------------------
\r
136 // Function: IsLegalMove
\r
138 // Synopsis: Determine if a given move is legal on a given board
\r
140 // Arguments: IN POSITION *p - the board to play the move on
\r
141 // IN MOVE *m - the move in question
\r
143 // Returns: BOOL - TRUE if it's legal, FALSE otherwise
\r
145 //+----------------------------------------------------------------------------
\r
146 BOOL IsLegalMove(IN POSITION *p, IN MOVE *m)
\r
148 if ((m->cVpos < BOARD_SIZE) && (m->cHpos < BOARD_SIZE))
\r
150 if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos]))
\r
158 //+----------------------------------------------------------------------------
\r
160 // Function: GetHumanMove
\r
162 // Synopsis: Ask the human for a move
\r
164 // Arguments: IN POSITION *p - the current board
\r
165 // OUT MOVE *m - the move the human made; this struct is populated
\r
166 // as a side-effect of this function.
\r
168 // Returns: void* (populates the move struct)
\r
170 //+----------------------------------------------------------------------------
\r
171 void GetHumanMove(IN POSITION *p, OUT MOVE *m)
\r
177 printf("Enter your move number: ");
\r
180 m->cHpos = NUM_TO_HPOS(x);
\r
181 m->cVpos = NUM_TO_VPOS(x);
\r
182 m->sMark = OPPOSITE_MARK(g_sComputerPlays);
\r
184 while(FALSE == IsLegalMove(p, m));
\r
188 //+----------------------------------------------------------------------------
\r
190 // Function: GameOver
\r
192 // Synopsis: Is the game over?
\r
194 // Arguments: IN POSITION *p - the board
\r
195 // OUT SQUARE *psWhoWon - who won the game (if it's over)
\r
197 // Returns: TRUE if the game is over. Also sets psWhoWon telling
\r
198 // which side one if the game is over. This also serves
\r
199 // as a very simple evaluation routine for the search.
\r
201 // FALSE if the game is not over.
\r
203 //+----------------------------------------------------------------------------
\r
204 BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon)
\r
209 for (x = 0; x < BOARD_SIZE; x++)
\r
211 iSum = p->iHSums[x];
\r
212 if (abs(iSum) == BOARD_SIZE) goto winner;
\r
214 iSum = p->iVSums[x];
\r
215 if (abs(iSum) == BOARD_SIZE) goto winner;
\r
218 iSum = p->iDSums[0];
\r
219 if (abs(iSum) == BOARD_SIZE) goto winner;
\r
221 iSum = p->iDSums[1];
\r
222 if (abs(iSum) == BOARD_SIZE) goto winner;
\r
225 // No one won yet, either game ongoing or draw.
\r
228 if (p->uNumEmpty == 0)
\r
241 *psWhoWon = (iSum / BOARD_SIZE);
\r
242 ASSERT(X_OR_O(*psWhoWon));
\r
247 //+----------------------------------------------------------------------------
\r
249 // Function: CountAdjacents
\r
251 // Synopsis: Return the number of marks in adjacent squares to square x
\r
252 // that are of the same type (x or o) as the mark in square x.
\r
254 // FIXME: does not consider diagonals
\r
256 // Arguments: IN POSITION *p - the board
\r
257 // IN SQUARE x - the square to test
\r
259 // Returns: A count
\r
261 //+----------------------------------------------------------------------------
\r
262 unsigned int CountAdjacents(IN POSITION *p, IN SQUARE x)
\r
264 COORD v = NUM_TO_VPOS(x);
\r
265 COORD h = NUM_TO_HPOS(x);
\r
266 SQUARE sSide = p->sBoard[h][v];
\r
267 unsigned int uCount = 0;
\r
270 // If nothing at square x, nothing to count
\r
272 if (sSide == EMPTY) goto end;
\r
275 // Look above, below, left and right
\r
277 if ((v > 0) && (p->sBoard[h][v-1] == sSide))
\r
282 if ((v < (BOARD_SIZE - 1)) && (p->sBoard[h][v+1] == sSide))
\r
287 if ((h > 0) && (p->sBoard[h-1][v] == sSide))
\r
292 if ((h < (BOARD_SIZE - 1)) && (p->sBoard[h+1][v] == sSide))
\r
298 ASSERT(0 <= uCount);
\r
299 ASSERT(uCount <= 4);
\r
304 //+----------------------------------------------------------------------------
\r
308 // Synopsis: Evaluate a position
\r
310 // Arguments: IN POSITION *p - the board
\r
312 // Returns: A score
\r
314 //+----------------------------------------------------------------------------
\r
315 int Eval(IN POSITION *p)
\r
323 // See if the game is already over.
\r
325 if (TRUE == GameOver(p, &sWinner))
\r
327 if (sWinner == p->sWhoseTurn)
\r
329 iScore = +INFINITY - g_uPly;
\r
332 else if (sWinner == OPPOSITE_MARK(p->sWhoseTurn))
\r
334 iScore = -INFINITY + g_uPly;
\r
337 iScore = DRAWSCORE;
\r
342 // No one won but instead of returning score=0 see if we can
\r
343 // find some "good characteristics" or "bad characteristics"
\r
344 // of the position and give bonuses / penalties.
\r
346 for (x = BOARD_SIZE + 1;
\r
347 x < ((BOARD_SIZE * BOARD_SIZE) - BOARD_SIZE - 1);
\r
350 sMark = p->sBoard[NUM_TO_HPOS(x)][NUM_TO_VPOS(x)];
\r
351 if (sMark == p->sWhoseTurn)
\r
353 iScore += CountAdjacents(p, x);
\r
355 else if (sMark == OPPOSITE_MARK(p->sWhoseTurn))
\r
357 iScore -= CountAdjacents(p, x);
\r
362 ASSERT(-INFINITY <= iScore);
\r
363 ASSERT(iScore <= +INFINITY);
\r
369 //+----------------------------------------------------------------------------
\r
371 // Function: MakeMove
\r
373 // Synopsis: Make a move on a board
\r
375 // Arguments: IN OUT POSITION *p - the board
\r
376 // IN MOVE *m - the move
\r
380 //+----------------------------------------------------------------------------
\r
381 void MakeMove(IN OUT POSITION *p, IN MOVE *m)
\r
383 if (TRUE == IsLegalMove(p, m))
\r
386 // Make the new make on the board
\r
388 ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY);
\r
389 p->sBoard[m->cVpos][m->cHpos] = m->sMark;
\r
392 // One less empty square
\r
395 ASSERT(p->uNumEmpty < (BOARD_SIZE * BOARD_SIZE));
\r
398 // Update sums as appropriate
\r
400 p->iHSums[m->cHpos] += m->sMark;
\r
401 ASSERT(VALID_SUM(p->iHSums[m->cHpos]));
\r
402 p->iVSums[m->cVpos] += m->sMark;
\r
403 ASSERT(VALID_SUM(p->iVSums[m->cVpos]));
\r
404 if (m->cHpos == m->cVpos)
\r
406 p->iDSums[0] += m->sMark;
\r
407 ASSERT(VALID_SUM(p->iDSums[0]));
\r
409 else if (m->cHpos == (BOARD_SIZE - m->cVpos))
\r
411 p->iDSums[1] += m->sMark;
\r
412 ASSERT(VALID_SUM(p->iDSums[1]));
\r
416 // Other guy's turn
\r
418 p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn);
\r
419 ASSERT(X_OR_O(p->sWhoseTurn));
\r
425 ASSERT(g_uPly > 0);
\r
429 //+----------------------------------------------------------------------------
\r
431 // Function: UnmakeMove
\r
433 // Synopsis: The opposite of MakeMove
\r
435 // Arguments: IN OUT POSITION *p - the board
\r
436 // IN MOVE *m - the move to undo
\r
440 //+----------------------------------------------------------------------------
\r
441 void UnmakeMove(IN OUT POSITION *p, IN MOVE *m)
\r
443 if (p->sBoard[m->cVpos][m->cHpos] == m->sMark)
\r
445 p->sBoard[m->cVpos][m->cHpos] = EMPTY;
\r
448 // One more empty square
\r
451 ASSERT(p->uNumEmpty > 0);
\r
452 ASSERT(p->uNumEmpty <= (BOARD_SIZE * BOARD_SIZE));
\r
455 // Update sums as appropriate
\r
457 p->iHSums[m->cHpos] -= m->sMark;
\r
458 ASSERT(VALID_SUM(p->iHSums[m->cHpos]));
\r
459 p->iVSums[m->cVpos] -= m->sMark;
\r
460 ASSERT(VALID_SUM(p->iVSums[m->cVpos]));
\r
461 if (m->cHpos == m->cVpos)
\r
463 p->iDSums[0] -= m->sMark;
\r
464 ASSERT(VALID_SUM(p->iDSums[0]));
\r
466 else if (m->cHpos == (BOARD_SIZE - m->cVpos))
\r
468 p->iDSums[1] -= m->sMark;
\r
469 ASSERT(VALID_SUM(p->iDSums[1]));
\r
473 // Other guy's turn
\r
475 p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn);
\r
476 ASSERT(X_OR_O(p->sWhoseTurn));
\r
481 ASSERT(g_uPly > 0);
\r
487 //+----------------------------------------------------------------------------
\r
489 // Function: AlphaBeta
\r
491 // Synopsis: An AlphaBeta Search
\r
493 // Arguments: IN OUT POSITION *p - the board
\r
494 // IN int iAlpha - the lower bound of the score window
\r
495 // IN int iBeta - the upper bound of the score window
\r
496 // IN int uDepth - search depth horizon
\r
500 //+----------------------------------------------------------------------------
\r
502 AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta, IN unsigned int uDepth)
\r
508 int iBestScore = -INFINITY;
\r
513 // Evaluate this position
\r
515 if (TRUE == GameOver(p, &sWhoWon))
\r
517 if (sWhoWon == p->sWhoseTurn)
\r
519 return(+INFINITY - g_uPly);
\r
521 else if (sWhoWon == (p->sWhoseTurn * -1))
\r
523 return(-INFINITY + g_uPly);
\r
536 // No one won, game is still going. Evaluate every
\r
537 // possible move from here.
\r
539 ASSERT(p->uNumEmpty > 0);
\r
540 for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++)
\r
542 mv.cHpos = NUM_TO_HPOS(s);
\r
543 mv.cVpos = NUM_TO_VPOS(s);
\r
544 mv.sMark = p->sWhoseTurn;
\r
546 if (IsLegalMove(p, &mv))
\r
550 iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha, uDepth - 1);
\r
551 ASSERT(-INFINITY <= iScore);
\r
552 ASSERT(iScore <= +INFINITY);
\r
554 UnmakeMove(p, &mv);
\r
556 if (iScore >= iBeta)
\r
561 if (iScore > iBestScore)
\r
563 iBestScore = iScore;
\r
565 if (iScore > iAlpha)
\r
570 // If this is the ply 0 move, remember it.
\r
580 return(iBestScore);
\r
584 //+----------------------------------------------------------------------------
\r
586 // Function: SimpleSearch
\r
588 // Synopsis: A Simple Search
\r
590 // Arguments: IN OUT POSITION *p - the board
\r
594 //+----------------------------------------------------------------------------
\r
596 SimpleSearch(IN POSITION *p)
\r
602 int iBestScore = -INFINITY;
\r
607 // Evaluate this position
\r
609 if (TRUE == GameOver(p, &sWhoWon))
\r
611 if (sWhoWon == p->sWhoseTurn)
\r
613 return(+INFINITY - g_uPly);
\r
615 else if (sWhoWon == (p->sWhoseTurn * -1))
\r
617 return(-INFINITY + g_uPly);
\r
623 // No one won, game is still going. Evaluate every
\r
624 // possible move from here.
\r
626 ASSERT(p->uNumEmpty > 0);
\r
627 for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++)
\r
629 mv.cHpos = NUM_TO_HPOS(s);
\r
630 mv.cVpos = NUM_TO_VPOS(s);
\r
631 mv.sMark = p->sWhoseTurn;
\r
633 if (IsLegalMove(p, &mv))
\r
637 iScore = -1 * SimpleSearch(p);
\r
638 if (iScore > iBestScore)
\r
640 iBestScore = iScore;
\r
646 UnmakeMove(p, &mv);
\r
649 return(iBestScore);
\r
652 //+----------------------------------------------------------------------------
\r
654 // Function: SearchForComputerMove
\r
656 // Synopsis: Use our sophisticated search algorithm to find a computer
\r
659 // Arguments: IN POSITION *p - the current board
\r
660 // OUT MOVE *m - the move the computer chooses; this move struct
\r
661 // is populated as a side-effect of this function.
\r
663 // Returns: void* (populates move struct)
\r
665 //+----------------------------------------------------------------------------
\r
666 void SearchForComputerMove(IN POSITION *p, OUT MOVE *m)
\r
670 #if defined(PLAY_RANDOMLY)
\r
674 x = rand() % (BOARD_SIZE * BOARD_SIZE);
\r
675 m->cHpos = NUM_TO_HPOS(x);
\r
676 m->cVpos = NUM_TO_VPOS(x);
\r
677 m->sMark = g_sComputerPlays;
\r
679 while(FALSE == IsLegalMove(p, m));
\r
681 #elif defined(SIMPLE_SEARCH)
\r
683 g_uPly = g_uNodes = 0;
\r
686 printf("Searched %u node(s).\n", g_uNodes);
\r
688 #elif defined(ALPHA_BETA_SEARCH)
\r
690 g_uPly = g_uNodes = 0;
\r
691 AlphaBeta(p, -INFINITY-1, +INFINITY+1, BOARD_SIZE);
\r
693 printf("Searched %u node(s).\n", g_uNodes);
\r
697 #error "No Search Strategy Defined"
\r
702 //+----------------------------------------------------------------------------
\r
706 // Synopsis: The program entry point and main game loop.
\r
712 //+----------------------------------------------------------------------------
\r
720 // Randomize: the random numbers returned by rand() will be based on
\r
721 // the system clock when the program starts up.
\r
726 // Setup the board and draw it once.
\r
737 // See whose turn it is -- the human's or the computers -- and
\r
738 // get a move from whoever's turn it is.
\r
740 if (p.sWhoseTurn == g_sComputerPlays)
\r
742 SearchForComputerMove(&p, &mv);
\r
746 GetHumanMove(&p, &mv);
\r
750 // Make the move on the board and draw the board again.
\r
755 while(FALSE == GameOver(&p, &sResult));
\r
758 // If we get here the game is over... see what happened.
\r
763 printf("\nX's win.\n");
\r
766 printf("\nO's win.\n");
\r
769 printf("Tie (what a surprise)\n");
\r