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
21 ver4 : variable sized board
\r
22 ver5 : bugfixes, added singular extension to search
\r
32 SQUARE g_sComputerPlays = O_MARK; // what side comp plays
\r
33 unsigned int g_uPly = 0;
\r
34 MOVE g_mvBest = { 0 };
\r
35 unsigned int g_uNodes = 0;
\r
36 unsigned int g_uExtensions = 0;
\r
37 COORD g_uBoardSize = 3;
\r
39 //+----------------------------------------------------------------------------
\r
41 // Function: SquareContentsToChar
\r
43 // Synopsis: Helper function for DrawBoard
\r
45 // Arguments: IN SQUARE s - a square to return a char to represent
\r
47 // Returns: char - character representing square
\r
49 //+----------------------------------------------------------------------------
\r
50 char SquareContentsToChar(IN SQUARE s)
\r
72 //+----------------------------------------------------------------------------
\r
74 // Function: DrawBoard
\r
76 // Synopsis: Draw the board
\r
78 // Arguments: IN POSITION *p - pointer to a position whose board to draw
\r
82 //+----------------------------------------------------------------------------
\r
83 void DrawBoard(IN POSITION *p)
\r
87 for (y = 0; y < g_uBoardSize; y++)
\r
89 for (x = 0; x < g_uBoardSize; x++)
\r
91 printf("%c ", SquareContentsToChar(p->sBoard[y][x]));
\r
94 printf(" = %d\n", p->iVSums[y]);
\r
101 for (x = 0; x < g_uBoardSize; x++)
\r
106 for (x = 0; x < g_uBoardSize; x++)
\r
108 printf("%d ", p->iHSums[x]);
\r
110 printf("\t%d %d\n", p->iDSums[0], p->iDSums[1]);
\r
114 ASSERT(X_OR_O(p->sWhoseTurn));
\r
115 printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn));
\r
118 //+----------------------------------------------------------------------------
\r
120 // Function: ClearBoard
\r
122 // Synopsis: Clear the board
\r
124 // Arguments: IN OUT POSITION *p - pointer to position whose board to clear
\r
128 //+----------------------------------------------------------------------------
\r
129 void ClearBoard(IN OUT POSITION *p)
\r
133 for (h = 0; h < g_uBoardSize; h++)
\r
135 memset(p->sBoard[h], 0, sizeof(int) * g_uBoardSize);
\r
137 memset(p->iHSums, 0, sizeof(int) * g_uBoardSize);
\r
138 memset(p->iVSums, 0, sizeof(int) * g_uBoardSize);
\r
139 p->iDSums[0] = p->iDSums[1] = 0;
\r
140 p->sWhoseTurn = X_MARK; // x's go first
\r
141 p->uNumEmpty = (g_uBoardSize * g_uBoardSize);
\r
144 //+----------------------------------------------------------------------------
\r
146 // Function: IsLegalMove
\r
148 // Synopsis: Determine if a given move is legal on a given board
\r
150 // Arguments: IN POSITION *p - the board to play the move on
\r
151 // IN MOVE *m - the move in question
\r
153 // Returns: BOOL - TRUE if it's legal, FALSE otherwise
\r
155 //+----------------------------------------------------------------------------
\r
156 BOOL IsLegalMove(IN POSITION *p, IN MOVE *m)
\r
158 if ((m->cVpos < g_uBoardSize) && (m->cHpos < g_uBoardSize))
\r
160 if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos]))
\r
168 //+----------------------------------------------------------------------------
\r
170 // Function: GetHumanMove
\r
172 // Synopsis: Ask the human for a move
\r
174 // Arguments: IN POSITION *p - the current board
\r
175 // OUT MOVE *m - the move the human made; this struct is populated
\r
176 // as a side-effect of this function.
\r
178 // Returns: void* (populates the move struct)
\r
180 //+----------------------------------------------------------------------------
\r
181 void GetHumanMove(IN POSITION *p, OUT MOVE *m)
\r
187 printf("Enter your move number: ");
\r
190 m->cHpos = NUM_TO_HPOS(x);
\r
191 m->cVpos = NUM_TO_VPOS(x);
\r
192 m->sMark = OPPOSITE_MARK(g_sComputerPlays);
\r
194 while(FALSE == IsLegalMove(p, m));
\r
198 //+----------------------------------------------------------------------------
\r
200 // Function: GameOver
\r
202 // Synopsis: Is the game over?
\r
204 // Arguments: IN POSITION *p - the board
\r
205 // OUT SQUARE *psWhoWon - who won the game (if it's over)
\r
207 // Returns: TRUE if the game is over. Also sets psWhoWon telling
\r
208 // which side one if the game is over. This also serves
\r
209 // as a very simple evaluation routine for the search.
\r
211 // FALSE if the game is not over.
\r
213 //+----------------------------------------------------------------------------
\r
214 BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon)
\r
218 unsigned int uFull = (g_uBoardSize * g_uBoardSize) - p->uNumEmpty;
\r
221 // The game can't be over if less than g_uBoardSize * 2 - 1 marks on it
\r
223 if (uFull < (g_uBoardSize * 2 - 1))
\r
229 for (x = 0; x < g_uBoardSize; x++)
\r
231 iSum = p->iHSums[x];
\r
232 if (abs(iSum) == g_uBoardSize) goto winner;
\r
234 iSum = p->iVSums[x];
\r
235 if (abs(iSum) == g_uBoardSize) goto winner;
\r
238 iSum = p->iDSums[0];
\r
239 if (abs(iSum) == g_uBoardSize) goto winner;
\r
241 iSum = p->iDSums[1];
\r
242 if (abs(iSum) == g_uBoardSize) goto winner;
\r
245 // No one won yet, either game ongoing or draw.
\r
248 if (p->uNumEmpty == 0)
\r
261 *psWhoWon = (iSum / (int)g_uBoardSize);
\r
262 ASSERT(X_OR_O(*psWhoWon));
\r
267 //+----------------------------------------------------------------------------
\r
269 // Function: MakeMove
\r
271 // Synopsis: Make a move on a board
\r
273 // Arguments: IN OUT POSITION *p - the board
\r
274 // IN MOVE *m - the move
\r
278 //+----------------------------------------------------------------------------
\r
279 void MakeMove(IN OUT POSITION *p, IN MOVE *m)
\r
281 if (TRUE == IsLegalMove(p, m))
\r
284 // Make the new make on the board
\r
286 ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY);
\r
287 p->sBoard[m->cVpos][m->cHpos] = m->sMark;
\r
290 // One less empty square
\r
293 ASSERT(p->uNumEmpty < (g_uBoardSize * g_uBoardSize));
\r
296 // Update sums as appropriate
\r
298 p->iHSums[m->cHpos] += m->sMark;
\r
299 ASSERT(VALID_SUM(p->iHSums[m->cHpos]));
\r
300 p->iVSums[m->cVpos] += m->sMark;
\r
301 ASSERT(VALID_SUM(p->iVSums[m->cVpos]));
\r
302 if (ON_DIAGONAL_1(m->cHpos, m->cVpos))
\r
304 p->iDSums[0] += m->sMark;
\r
305 ASSERT(VALID_SUM(p->iDSums[0]));
\r
307 if (ON_DIAGONAL_2(m->cHpos, m->cVpos))
\r
309 p->iDSums[1] += m->sMark;
\r
310 ASSERT(VALID_SUM(p->iDSums[1]));
\r
314 // Other guy's turn
\r
316 p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn);
\r
317 ASSERT(X_OR_O(p->sWhoseTurn));
\r
323 ASSERT(g_uPly > 0);
\r
327 //+----------------------------------------------------------------------------
\r
329 // Function: UnmakeMove
\r
331 // Synopsis: The opposite of MakeMove
\r
333 // Arguments: IN OUT POSITION *p - the board
\r
334 // IN MOVE *m - the move to undo
\r
338 //+----------------------------------------------------------------------------
\r
339 void UnmakeMove(IN OUT POSITION *p, IN MOVE *m)
\r
341 if (p->sBoard[m->cVpos][m->cHpos] == m->sMark)
\r
343 p->sBoard[m->cVpos][m->cHpos] = EMPTY;
\r
346 // One more empty square
\r
349 ASSERT(p->uNumEmpty > 0);
\r
350 ASSERT(p->uNumEmpty <= (g_uBoardSize * g_uBoardSize));
\r
353 // Update sums as appropriate
\r
355 p->iHSums[m->cHpos] -= m->sMark;
\r
356 ASSERT(VALID_SUM(p->iHSums[m->cHpos]));
\r
357 p->iVSums[m->cVpos] -= m->sMark;
\r
358 ASSERT(VALID_SUM(p->iVSums[m->cVpos]));
\r
359 if (ON_DIAGONAL_1(m->cHpos, m->cVpos))
\r
361 p->iDSums[0] -= m->sMark;
\r
362 ASSERT(VALID_SUM(p->iDSums[0]));
\r
364 if (ON_DIAGONAL_2(m->cHpos, m->cVpos))
\r
366 p->iDSums[1] -= m->sMark;
\r
367 ASSERT(VALID_SUM(p->iDSums[1]));
\r
371 // Other guy's turn
\r
373 p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn);
\r
374 ASSERT(X_OR_O(p->sWhoseTurn));
\r
379 ASSERT(g_uPly > 0);
\r
385 //+----------------------------------------------------------------------------
\r
387 // Function: IsMoveSingular
\r
389 // Synopsis: Determine if a move is singular (i.e. only good move) or not
\r
391 // Arguments: IN POSITION *p - the board
\r
392 // IN MOVE *m - the move to undo
\r
394 // Returns: BOOL : TRUE if *m is singular
\r
396 //+----------------------------------------------------------------------------
\r
398 IsMoveSingular(IN POSITION *p, IN MOVE *m)
\r
400 if ((abs(p->iVSums[m->cVpos]) >= (g_uBoardSize - 2)) ||
\r
401 (abs(p->iHSums[m->cHpos]) >= (g_uBoardSize - 2)))
\r
405 if ((m->cHpos == m->cVpos) &&
\r
406 (abs(p->iDSums[0]) == (g_uBoardSize - 1)))
\r
410 if ((m->cVpos == ((g_uBoardSize - m->cHpos) - 1)) &&
\r
411 (abs(p->iDSums[1] == (g_uBoardSize - 1))))
\r
420 IsMoveWorthSearching(POSITION *p, MOVE *m)
\r
424 unsigned int uSum = 0;
\r
426 for (h = m->cHpos - 1;
\r
427 h < (signed int)m->cHpos + 2;
\r
430 for (v = m->cVpos - 1;
\r
431 v < (signed int)m->cVpos + 2;
\r
434 if (GOOD_COORD((COORD)v) && GOOD_COORD((COORD)h))
\r
436 uSum += abs(p->sBoard[v][h]);
\r
452 int iSum = p->iDSums[0];
\r
459 iSum += p->iHSums[x];
\r
460 iSum += p->iVSums[x];
\r
462 iSum += p->iDSums[1];
\r
464 return(iSum * p->sWhoseTurn);
\r
468 //+----------------------------------------------------------------------------
\r
470 // Function: AlphaBeta
\r
472 // Synopsis: An AlphaBeta Search
\r
474 // Arguments: IN OUT POSITION *p - the board
\r
475 // IN int iAlpha - the lower bound of the score window
\r
476 // IN int iBeta - the upper bound of the score window
\r
477 // IN int uDepth - search depth horizon
\r
481 //+----------------------------------------------------------------------------
\r
483 AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta, IN unsigned int uDepth)
\r
489 int iBestScore = -INFINITY - 2;
\r
490 BOOL fMoveIsSingular;
\r
491 unsigned int uNextDepth;
\r
492 unsigned int uMoveNum = 1;
\r
497 // Evaluate this position
\r
499 if (TRUE == GameOver(p, &sWhoWon))
\r
501 if (sWhoWon == p->sWhoseTurn)
\r
503 return(+INFINITY - g_uPly);
\r
505 else if (sWhoWon == (p->sWhoseTurn * -1))
\r
507 return(-INFINITY + g_uPly);
\r
511 else if (uDepth == 0)
\r
517 // No one won, game is still going. Evaluate some moves from here.
\r
519 ASSERT(p->uNumEmpty > 0);
\r
520 for (s = 0; s < (g_uBoardSize * g_uBoardSize); s++)
\r
522 mv.cHpos = NUM_TO_HPOS(s);
\r
523 mv.cVpos = NUM_TO_VPOS(s);
\r
524 mv.sMark = p->sWhoseTurn;
\r
526 if (IsLegalMove(p, &mv))
\r
529 // Determine if move is singular
\r
531 fMoveIsSingular = IsMoveSingular(p, &mv);
\r
533 if ((FALSE == fMoveIsSingular) &&
\r
537 // Determine if we should bother with this subtree...
\r
539 if (FALSE == IsMoveWorthSearching(p, &mv))
\r
551 uNextDepth = uDepth - 1;
\r
552 if (TRUE == fMoveIsSingular)
\r
554 uNextDepth = uDepth;
\r
557 iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha, uNextDepth);
\r
558 ASSERT(-INFINITY <= iScore);
\r
559 ASSERT(iScore <= +INFINITY);
\r
561 UnmakeMove(p, &mv);
\r
566 if (iScore >= iBeta)
\r
571 if (iScore > iBestScore)
\r
573 iBestScore = iScore;
\r
575 if (iScore > iAlpha)
\r
583 // If this is the ply 0 move, remember it.
\r
593 return(iBestScore);
\r
596 //+----------------------------------------------------------------------------
\r
598 // Function: SearchForComputerMove
\r
600 // Synopsis: Use our sophisticated search algorithm to find a computer
\r
603 // Arguments: IN POSITION *p - the current board
\r
604 // OUT MOVE *m - the move the computer chooses; this move struct
\r
605 // is populated as a side-effect of this function.
\r
607 // Returns: void* (populates move struct)
\r
609 //+----------------------------------------------------------------------------
\r
610 void SearchForComputerMove(IN POSITION *p, OUT MOVE *m)
\r
612 #if defined(PLAY_RANDOMLY)
\r
617 x = rand() % (g_uBoardSize * g_uBoardSize);
\r
618 m->cHpos = NUM_TO_HPOS(x);
\r
619 m->cVpos = NUM_TO_VPOS(x);
\r
620 m->sMark = g_sComputerPlays;
\r
622 while(FALSE == IsLegalMove(p, m));
\r
624 #elif defined(ALPHA_BETA_SEARCH)
\r
627 g_uPly = g_uNodes = g_uExtensions = 0;
\r
629 AlphaBeta(p, -INFINITY-1, +INFINITY+1, 3);
\r
631 printf("Searched %u node(s), %u extension(s)\n",
\r
636 #error "No Search Strategy Defined"
\r
641 //+----------------------------------------------------------------------------
\r
645 // Synopsis: The program entry point and main game loop.
\r
651 //+----------------------------------------------------------------------------
\r
661 // Randomize: the random numbers returned by rand() will be based on
\r
662 // the system clock when the program starts up.
\r
671 printf("How big do you want the board (2..20)? ");
\r
672 scanf("%u", &g_uBoardSize);
\r
674 while((g_uBoardSize < 2) || (g_uBoardSize > 20));
\r
677 // Allocate space for 2d int array ptr
\r
679 p.sBoard = (SQUARE **)malloc(g_uBoardSize * sizeof(SQUARE *));
\r
680 if (NULL == p.sBoard)
\r
682 fprintf(stderr, "Out of memory\n");
\r
687 // Allocate each row of the array
\r
689 for (u = 0; u < g_uBoardSize; u++)
\r
691 p.sBoard[u] = (SQUARE *)
\r
692 malloc(g_uBoardSize * sizeof(SQUARE));
\r
693 if (NULL == p.sBoard[u])
\r
695 fprintf(stderr, "Out of memory!\n");
\r
701 // Allocate space for sums
\r
703 p.iHSums = (int *)malloc(g_uBoardSize * sizeof(int));
\r
704 p.iVSums = (int *)malloc(g_uBoardSize * sizeof(int));
\r
705 if ((NULL == p.iHSums) ||
\r
706 (NULL == p.iVSums))
\r
708 fprintf(stderr, "Out of memory!\n");
\r
713 // Setup the board and draw it once.
\r
724 // See whose turn it is -- the human's or the computers -- and
\r
725 // get a move from whoever's turn it is.
\r
727 if (p.sWhoseTurn == g_sComputerPlays)
\r
729 SearchForComputerMove(&p, &mv);
\r
733 //SearchForComputerMove(&p, &mv);
\r
734 GetHumanMove(&p, &mv);
\r
738 // Make the move on the board and draw the board again.
\r
743 while(FALSE == GameOver(&p, &sResult));
\r
746 // If we get here the game is over... see what happened.
\r
751 printf("\nX's win.\n");
\r
754 printf("\nO's win.\n");
\r
757 printf("Tie (what a surprise)\n");
\r
762 // TODO: cleanup heap
\r