From 6d38eb746e86589e3b8fa10ba37d206af5eb7ebf Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Wed, 1 Jun 2016 19:41:17 -0700 Subject: [PATCH 1/1] Initial checking of tic tac toe minimax tutorial code. --- ver0/ttt.c | 329 ++++++++++++++++++++ ver0/ttt.h | 48 +++ ver1/ttt.c | 434 ++++++++++++++++++++++++++ ver1/ttt.h | 74 +++++ ver2/ttt.c | 535 ++++++++++++++++++++++++++++++++ ver2/ttt.h | 74 +++++ ver3/ttt.c | 775 ++++++++++++++++++++++++++++++++++++++++++++++ ver3/ttt.h | 79 +++++ ver4/ttt.c | 846 +++++++++++++++++++++++++++++++++++++++++++++++++++ ver4/ttt.h | 93 ++++++ ver5/ttt.c | 766 ++++++++++++++++++++++++++++++++++++++++++++++ ver5/ttt.exe | Bin 0 -> 31744 bytes ver5/ttt.h | 96 ++++++ ver5/ttt.pdb | Bin 0 -> 232448 bytes 14 files changed, 4149 insertions(+) create mode 100644 ver0/ttt.c create mode 100644 ver0/ttt.h create mode 100644 ver1/ttt.c create mode 100644 ver1/ttt.h create mode 100644 ver2/ttt.c create mode 100644 ver2/ttt.h create mode 100644 ver3/ttt.c create mode 100644 ver3/ttt.h create mode 100644 ver4/ttt.c create mode 100644 ver4/ttt.h create mode 100644 ver5/ttt.c create mode 100644 ver5/ttt.exe create mode 100644 ver5/ttt.h create mode 100644 ver5/ttt.pdb diff --git a/ver0/ttt.c b/ver0/ttt.c new file mode 100644 index 0000000..779cbe5 --- /dev/null +++ b/ver0/ttt.c @@ -0,0 +1,329 @@ +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + default: + c = '_'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < BOARD_SIZE; y++) + { + for (x = 0; x < BOARD_SIZE; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } + printf("\n"); + } + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + memset(p->sBoard, 0, sizeof(p->sBoard)); + p->sWhoseTurn = X_MARK; + p->uNumEmpty = (BOARD_SIZE * BOARD_SIZE); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < BOARD_SIZE) && (m->cHpos < BOARD_SIZE)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = (x % BOARD_SIZE); + m->cVpos = (x / BOARD_SIZE); + m->sMark = g_sComputerPlays * -1; + } + while(FALSE == IsLegalMove(p, m)); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + x = rand() % (BOARD_SIZE * BOARD_SIZE); + m->cHpos = (x % BOARD_SIZE); + m->cVpos = (x / BOARD_SIZE); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); +} + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + p->uNumEmpty--; + p->sWhoseTurn *= -1; + } +} + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x, y; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum = 0; + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum = 0; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][x]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][(BOARD_SIZE - 1 - x)]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + *psWhoWon = (iSum / BOARD_SIZE); + return(TRUE); +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + exit(0); +} + diff --git a/ver0/ttt.h b/ver0/ttt.h new file mode 100644 index 0000000..c20a3d1 --- /dev/null +++ b/ver0/ttt.h @@ -0,0 +1,48 @@ +#ifndef _TTT_H_ +#define _TTT_H_ + +#define TRUE (1) +#define FALSE (0) + +#define IN +#define OUT + +typedef unsigned int BOOL; + +// +// Constants for each state a board square can be in and a programmer +// defined type for these states. +// +#define X_MARK (-1) +#define EMPTY (0) +#define O_MARK (+1) +#define TIE (+2) + +typedef signed char SQUARE; +#define IS_SQUARE_EMPTY(x) (x == EMPTY) + +// +// A (simple) representation of a tic tac toe position +// +#define BOARD_SIZE (3) + +typedef struct _POSITION +{ + SQUARE sWhoseTurn; + SQUARE sBoard[BOARD_SIZE][BOARD_SIZE]; + unsigned int uNumEmpty; +} POSITION; + +// +// A representation of a move in a tic tac toe game +// +typedef unsigned int COORD; + +typedef struct _MOVE +{ + COORD cHpos; + COORD cVpos; + SQUARE sMark; +} MOVE; + +#endif /* _TTT_H_ */ diff --git a/ver1/ttt.c b/ver1/ttt.c new file mode 100644 index 0000000..80efd08 --- /dev/null +++ b/ver1/ttt.c @@ -0,0 +1,434 @@ +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays +unsigned int g_uPly = 0; +MOVE g_mvBest = { 0 }; + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + case EMPTY: + c = '_'; + break; + default: + ASSERT(FALSE); + c = '?'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < BOARD_SIZE; y++) + { + for (x = 0; x < BOARD_SIZE; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } + printf("\n"); + } + ASSERT(X_OR_O(p->sWhoseTurn)); + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + memset(p->sBoard, 0, sizeof(p->sBoard)); + p->sWhoseTurn = X_MARK; // x's go first + p->uNumEmpty = (BOARD_SIZE * BOARD_SIZE); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < BOARD_SIZE) && (m->cHpos < BOARD_SIZE)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = OPPOSITE_MARK(g_sComputerPlays); + } + while(FALSE == IsLegalMove(p, m)); +} + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. This also serves +// as a very simple evaluation routine for the search. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x, y; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum = 0; + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum = 0; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][x]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][(BOARD_SIZE - 1 - x)]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + *psWhoWon = (iSum / BOARD_SIZE); + ASSERT(X_OR_O(*psWhoWon)); + return(TRUE); +} + + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY); + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + p->uNumEmpty--; + ASSERT(p->uNumEmpty < (BOARD_SIZE * BOARD_SIZE)); + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + g_uPly++; + ASSERT(g_uPly > 0); + } +} + +//+---------------------------------------------------------------------------- +// +// Function: UnmakeMove +// +// Synopsis: The opposite of MakeMove +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void UnmakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (p->sBoard[m->cVpos][m->cHpos] == m->sMark) + { + p->sBoard[m->cVpos][m->cHpos] = EMPTY; + p->uNumEmpty++; + ASSERT(p->uNumEmpty > 0); + ASSERT(p->uNumEmpty <= (BOARD_SIZE * BOARD_SIZE)); + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(g_uPly > 0); + g_uPly--; + } +} + + +int +SimpleSearch(IN POSITION *p) +{ + SQUARE sWhoWon; + SQUARE s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY); + } + return(DRAWSCORE); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * SimpleSearch(p); + if (iScore > iBestScore) + { + iBestScore = iScore; + if (g_uPly == 1) + { + g_mvBest = mv; + } + } + + UnmakeMove(p, &mv); + } + } + return(iBestScore); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + +#if defined(PLAY_RANDOMLY) + do + { + x = rand() % (BOARD_SIZE * BOARD_SIZE); + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); +#elif defined(SIMPLE_SEARCH) + g_uPly = 0; + SimpleSearch(p); + *m = g_mvBest; +#else + #error "No Search Strategy Defined" +#endif +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + exit(0); +} + diff --git a/ver1/ttt.h b/ver1/ttt.h new file mode 100644 index 0000000..04bbebf --- /dev/null +++ b/ver1/ttt.h @@ -0,0 +1,74 @@ +#ifndef TTT_H_ +#define TTT_H_ + +#define SIMPLE_SEARCH + +#define TRUE (1) +#define FALSE (0) + +#define IN +#define OUT + +typedef unsigned int BOOL; + +// +// Constants for each state a board square can be in and a programmer +// defined type for these states. +// +#define X_MARK (-1) +#define EMPTY (0) +#define O_MARK (+1) +#define TIE (+2) +#define OPPOSITE_MARK(m) ((m) * -1) +#define X_OR_O(m) (((m) == X_MARK) || \ + ((m) == O_MARK)) + +typedef signed char SQUARE; +#define IS_SQUARE_EMPTY(x) (x == EMPTY) + +// +// A (simple) representation of a tic tac toe position +// +#define BOARD_SIZE (3) + +typedef struct _POSITION +{ + SQUARE sWhoseTurn; + SQUARE sBoard[BOARD_SIZE][BOARD_SIZE]; + unsigned int uNumEmpty; +} POSITION; + +#define NUM_TO_HPOS(x) ((x) % BOARD_SIZE) +#define NUM_TO_VPOS(x) ((x) / BOARD_SIZE) + +// +// A representation of a move in a tic tac toe game +// +typedef unsigned int COORD; + +typedef struct _MOVE +{ + COORD cHpos; + COORD cVpos; + SQUARE sMark; +} MOVE; + +// +// Score values +// +#define INFINITY (+100) +#define DRAWSCORE (0) + +// +// An assert mechanism +// +#ifdef DEBUG +#define ASSERT(x) if (x) \ + { ; } \ + else \ + { (void) _assert(__FILE__, __LINE__); } +#else +#define ASSERT(x) ; +#endif /* DEBUG */ + +#endif /* TTT_H_ */ diff --git a/ver2/ttt.c b/ver2/ttt.c new file mode 100644 index 0000000..d39bc7e --- /dev/null +++ b/ver2/ttt.c @@ -0,0 +1,535 @@ +/*++ + +Module Name: + + ttt.c + +Abstract: + + tic tac toe program to illustrate simple minimax searching + +Author: + + Scott Gasch (SGasch) 18 Mar 2004 + +Revision History: + + ver0 : random play + ver1 : simple search + ver2 : alpha beta search + +--*/ + +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays +unsigned int g_uPly = 0; +MOVE g_mvBest = { 0 }; +unsigned int g_uNodes = 0; + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + case EMPTY: + c = '_'; + break; + default: + ASSERT(FALSE); + c = '?'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < BOARD_SIZE; y++) + { + for (x = 0; x < BOARD_SIZE; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } + printf("\n"); + } + ASSERT(X_OR_O(p->sWhoseTurn)); + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + memset(p->sBoard, 0, sizeof(p->sBoard)); + p->sWhoseTurn = X_MARK; // x's go first + p->uNumEmpty = (BOARD_SIZE * BOARD_SIZE); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < BOARD_SIZE) && (m->cHpos < BOARD_SIZE)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = OPPOSITE_MARK(g_sComputerPlays); + } + while(FALSE == IsLegalMove(p, m)); +} + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. This also serves +// as a very simple evaluation routine for the search. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x, y; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum = 0; + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + for (y = 0; y < BOARD_SIZE; y++) + { + iSum = 0; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][y]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][x]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + iSum = 0; + for (x = 0; x < BOARD_SIZE; x++) + { + iSum += p->sBoard[x][(BOARD_SIZE - 1 - x)]; + } + if (abs(iSum) == BOARD_SIZE) goto winner; + + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + *psWhoWon = (iSum / BOARD_SIZE); + ASSERT(X_OR_O(*psWhoWon)); + return(TRUE); +} + + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY); + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + p->uNumEmpty--; + ASSERT(p->uNumEmpty < (BOARD_SIZE * BOARD_SIZE)); + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + g_uPly++; + ASSERT(g_uPly > 0); + } +} + +//+---------------------------------------------------------------------------- +// +// Function: UnmakeMove +// +// Synopsis: The opposite of MakeMove +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void UnmakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (p->sBoard[m->cVpos][m->cHpos] == m->sMark) + { + p->sBoard[m->cVpos][m->cHpos] = EMPTY; + p->uNumEmpty++; + ASSERT(p->uNumEmpty > 0); + ASSERT(p->uNumEmpty <= (BOARD_SIZE * BOARD_SIZE)); + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(g_uPly > 0); + g_uPly--; + } +} + + +int +AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta) +{ + SQUARE sWhoWon; + SQUARE s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha); + + UnmakeMove(p, &mv); + + if (iScore >= iBeta) + { + return(iScore); + } + + if (iScore > iBestScore) + { + iBestScore = iScore; + + if (iScore > iAlpha) + { + iAlpha = iScore; + if (g_uPly == 0) + { + g_mvBest = mv; + } + } + } + } + } + return(iBestScore); +} + + +int +SimpleSearch(IN POSITION *p) +{ + SQUARE sWhoWon; + SQUARE s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * SimpleSearch(p); + if (iScore > iBestScore) + { + iBestScore = iScore; + if (g_uPly == 1) + { + g_mvBest = mv; + } + } + + UnmakeMove(p, &mv); + } + } + return(iBestScore); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + +#if defined(PLAY_RANDOMLY) + do + { + x = rand() % (BOARD_SIZE * BOARD_SIZE); + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); +#elif defined(SIMPLE_SEARCH) + g_uPly = g_uNodes = 0; + SimpleSearch(p); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); +#elif defined(ALPHA_BETA_SEARCH) + g_uPly = g_uNodes = 0; + AlphaBeta(p, -INFINITY-1, +INFINITY+1); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); +#else + #error "No Search Strategy Defined" +#endif +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + exit(0); +} + diff --git a/ver2/ttt.h b/ver2/ttt.h new file mode 100644 index 0000000..2cee8f3 --- /dev/null +++ b/ver2/ttt.h @@ -0,0 +1,74 @@ +#ifndef TTT_H_ +#define TTT_H_ + +#define ALPHA_BETA_SEARCH 1 + +#define TRUE (1) +#define FALSE (0) + +#define IN +#define OUT + +typedef unsigned int BOOL; + +// +// Constants for each state a board square can be in and a programmer +// defined type for these states. +// +#define X_MARK (-1) +#define EMPTY (0) +#define O_MARK (+1) +#define TIE (+2) +#define OPPOSITE_MARK(m) ((m) * -1) +#define X_OR_O(m) (((m) == X_MARK) || \ + ((m) == O_MARK)) + +typedef signed char SQUARE; +#define IS_SQUARE_EMPTY(x) (x == EMPTY) + +// +// A (simple) representation of a tic tac toe position +// +#define BOARD_SIZE (3) + +typedef struct _POSITION +{ + SQUARE sWhoseTurn; + SQUARE sBoard[BOARD_SIZE][BOARD_SIZE]; + unsigned int uNumEmpty; +} POSITION; + +#define NUM_TO_HPOS(x) ((x) % BOARD_SIZE) +#define NUM_TO_VPOS(x) ((x) / BOARD_SIZE) + +// +// A representation of a move in a tic tac toe game +// +typedef unsigned int COORD; + +typedef struct _MOVE +{ + COORD cHpos; + COORD cVpos; + SQUARE sMark; +} MOVE; + +// +// Score values +// +#define INFINITY (+100) +#define DRAWSCORE (0) + +// +// An assert mechanism +// +#ifdef DEBUG +#define ASSERT(x) if (x) \ + { ; } \ + else \ + { (void) _assert(__FILE__, __LINE__); } +#else +#define ASSERT(x) ; +#endif /* DEBUG */ + +#endif /* TTT_H_ */ diff --git a/ver3/ttt.c b/ver3/ttt.c new file mode 100644 index 0000000..426fc73 --- /dev/null +++ b/ver3/ttt.c @@ -0,0 +1,775 @@ +/*++ + +Module Name: + + ttt.c + +Abstract: + + tic tac toe program to illustrate simple minimax searching + +Author: + + Scott Gasch (SGasch) 18 Mar 2004 + +Revision History: + + ver0 : random play + ver1 : simple search + ver2 : alpha beta search + ver3 : added eval and depth on ab search, more efficient gaveover + +--*/ + +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays +unsigned int g_uPly = 0; +MOVE g_mvBest = { 0 }; +unsigned int g_uNodes = 0; + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + case EMPTY: + c = '_'; + break; + default: + ASSERT(FALSE); + c = '?'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < BOARD_SIZE; y++) + { + for (x = 0; x < BOARD_SIZE; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } +#ifdef DEBUG + printf(" = %d\n", p->iHSums[y]); +#else + printf("\n"); +#endif + } + +#ifdef DEBUG + for (x = 0; x < BOARD_SIZE; x++) + { + printf("| "); + } + printf("\n"); + for (x = 0; x < BOARD_SIZE; x++) + { + printf("%d ", p->iVSums[x]); + } + printf("\n"); +#endif + + ASSERT(X_OR_O(p->sWhoseTurn)); + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + memset(p->sBoard, 0, sizeof(p->sBoard)); + memset(p->iHSums, 0, sizeof(p->iHSums)); + memset(p->iVSums, 0, sizeof(p->iVSums)); + p->iDSums[0] = p->iDSums[1] = 0; + p->sWhoseTurn = X_MARK; // x's go first + p->uNumEmpty = (BOARD_SIZE * BOARD_SIZE); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < BOARD_SIZE) && (m->cHpos < BOARD_SIZE)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = OPPOSITE_MARK(g_sComputerPlays); + } + while(FALSE == IsLegalMove(p, m)); +} + + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. This also serves +// as a very simple evaluation routine for the search. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x; + + for (x = 0; x < BOARD_SIZE; x++) + { + iSum = p->iHSums[x]; + if (abs(iSum) == BOARD_SIZE) goto winner; + + iSum = p->iVSums[x]; + if (abs(iSum) == BOARD_SIZE) goto winner; + } + + iSum = p->iDSums[0]; + if (abs(iSum) == BOARD_SIZE) goto winner; + + iSum = p->iDSums[1]; + if (abs(iSum) == BOARD_SIZE) goto winner; + + // + // No one won yet, either game ongoing or draw. + // + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + // + // Some side won + // + *psWhoWon = (iSum / BOARD_SIZE); + ASSERT(X_OR_O(*psWhoWon)); + return(TRUE); +} + + +//+---------------------------------------------------------------------------- +// +// Function: CountAdjacents +// +// Synopsis: Return the number of marks in adjacent squares to square x +// that are of the same type (x or o) as the mark in square x. +// +// FIXME: does not consider diagonals +// +// Arguments: IN POSITION *p - the board +// IN SQUARE x - the square to test +// +// Returns: A count +// +//+---------------------------------------------------------------------------- +unsigned int CountAdjacents(IN POSITION *p, IN SQUARE x) +{ + COORD v = NUM_TO_VPOS(x); + COORD h = NUM_TO_HPOS(x); + SQUARE sSide = p->sBoard[h][v]; + unsigned int uCount = 0; + + // + // If nothing at square x, nothing to count + // + if (sSide == EMPTY) goto end; + + // + // Look above, below, left and right + // + if ((v > 0) && (p->sBoard[h][v-1] == sSide)) + { + uCount++; + } + + if ((v < (BOARD_SIZE - 1)) && (p->sBoard[h][v+1] == sSide)) + { + uCount++; + } + + if ((h > 0) && (p->sBoard[h-1][v] == sSide)) + { + uCount++; + } + + if ((h < (BOARD_SIZE - 1)) && (p->sBoard[h+1][v] == sSide)) + { + uCount++; + } + + end: + ASSERT(0 <= uCount); + ASSERT(uCount <= 4); + return(uCount); +} + + +//+---------------------------------------------------------------------------- +// +// Function: Eval +// +// Synopsis: Evaluate a position +// +// Arguments: IN POSITION *p - the board +// +// Returns: A score +// +//+---------------------------------------------------------------------------- +int Eval(IN POSITION *p) +{ + SQUARE sWinner; + COORD x; + SQUARE sMark; + int iScore = 0; + + // + // See if the game is already over. + // + if (TRUE == GameOver(p, &sWinner)) + { + if (sWinner == p->sWhoseTurn) + { + iScore = +INFINITY - g_uPly; + goto end; + } + else if (sWinner == OPPOSITE_MARK(p->sWhoseTurn)) + { + iScore = -INFINITY + g_uPly; + goto end; + } + iScore = DRAWSCORE; + goto end; + } + + // + // No one won but instead of returning score=0 see if we can + // find some "good characteristics" or "bad characteristics" + // of the position and give bonuses / penalties. + // + for (x = BOARD_SIZE + 1; + x < ((BOARD_SIZE * BOARD_SIZE) - BOARD_SIZE - 1); + x++) + { + sMark = p->sBoard[NUM_TO_HPOS(x)][NUM_TO_VPOS(x)]; + if (sMark == p->sWhoseTurn) + { + iScore += CountAdjacents(p, x); + } + else if (sMark == OPPOSITE_MARK(p->sWhoseTurn)) + { + iScore -= CountAdjacents(p, x); + } + } + + end: + ASSERT(-INFINITY <= iScore); + ASSERT(iScore <= +INFINITY); + return(iScore); +} + + + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + // + // Make the new make on the board + // + ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY); + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + + // + // One less empty square + // + p->uNumEmpty--; + ASSERT(p->uNumEmpty < (BOARD_SIZE * BOARD_SIZE)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] += m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] += m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (m->cHpos == m->cVpos) + { + p->iDSums[0] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + else if (m->cHpos == (BOARD_SIZE - m->cVpos)) + { + p->iDSums[1] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + g_uPly++; + ASSERT(g_uPly > 0); + } +} + +//+---------------------------------------------------------------------------- +// +// Function: UnmakeMove +// +// Synopsis: The opposite of MakeMove +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void UnmakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (p->sBoard[m->cVpos][m->cHpos] == m->sMark) + { + p->sBoard[m->cVpos][m->cHpos] = EMPTY; + + // + // One more empty square + // + p->uNumEmpty++; + ASSERT(p->uNumEmpty > 0); + ASSERT(p->uNumEmpty <= (BOARD_SIZE * BOARD_SIZE)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] -= m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] -= m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (m->cHpos == m->cVpos) + { + p->iDSums[0] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + else if (m->cHpos == (BOARD_SIZE - m->cVpos)) + { + p->iDSums[1] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + ASSERT(g_uPly > 0); + g_uPly--; + } +} + + +//+---------------------------------------------------------------------------- +// +// Function: AlphaBeta +// +// Synopsis: An AlphaBeta Search +// +// Arguments: IN OUT POSITION *p - the board +// IN int iAlpha - the lower bound of the score window +// IN int iBeta - the upper bound of the score window +// IN int uDepth - search depth horizon +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta, IN unsigned int uDepth) +{ + SQUARE sWhoWon; + SQUARE s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + else + { + if (uDepth == 0) + { + return(Eval(p)); + } + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha, uDepth - 1); + ASSERT(-INFINITY <= iScore); + ASSERT(iScore <= +INFINITY); + + UnmakeMove(p, &mv); + + if (iScore >= iBeta) + { + return(iScore); + } + + if (iScore > iBestScore) + { + iBestScore = iScore; + + if (iScore > iAlpha) + { + iAlpha = iScore; + + // + // If this is the ply 0 move, remember it. + // + if (g_uPly == 0) + { + g_mvBest = mv; + } + } + } + } + } + return(iBestScore); +} + + +//+---------------------------------------------------------------------------- +// +// Function: SimpleSearch +// +// Synopsis: A Simple Search +// +// Arguments: IN OUT POSITION *p - the board +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +SimpleSearch(IN POSITION *p) +{ + SQUARE sWhoWon; + SQUARE s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (BOARD_SIZE * BOARD_SIZE); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * SimpleSearch(p); + if (iScore > iBestScore) + { + iBestScore = iScore; + if (g_uPly == 1) + { + g_mvBest = mv; + } + } + UnmakeMove(p, &mv); + } + } + return(iBestScore); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + +#if defined(PLAY_RANDOMLY) + + do + { + x = rand() % (BOARD_SIZE * BOARD_SIZE); + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); + +#elif defined(SIMPLE_SEARCH) + + g_uPly = g_uNodes = 0; + SimpleSearch(p); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); + +#elif defined(ALPHA_BETA_SEARCH) + + g_uPly = g_uNodes = 0; + AlphaBeta(p, -INFINITY-1, +INFINITY+1, BOARD_SIZE); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); + +#else + + #error "No Search Strategy Defined" + +#endif +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + exit(0); +} + diff --git a/ver3/ttt.h b/ver3/ttt.h new file mode 100644 index 0000000..115e5b9 --- /dev/null +++ b/ver3/ttt.h @@ -0,0 +1,79 @@ +#ifndef TTT_H_ +#define TTT_H_ + +#define ALPHA_BETA_SEARCH 1 + +#define TRUE (1) +#define FALSE (0) + +#define IN +#define OUT + +typedef unsigned int BOOL; + +// +// Constants for each state a board square can be in and a programmer +// defined type for these states. +// +#define X_MARK (-1) +#define EMPTY (0) +#define O_MARK (+1) +#define TIE (+2) +#define OPPOSITE_MARK(m) ((m) * -1) +#define X_OR_O(m) (((m) == X_MARK) || \ + ((m) == O_MARK)) + +typedef signed char SQUARE; +#define IS_SQUARE_EMPTY(x) (x == EMPTY) + +// +// A (simple) representation of a tic tac toe position +// +#define BOARD_SIZE (5) + +typedef struct _POSITION +{ + SQUARE sWhoseTurn; + SQUARE sBoard[BOARD_SIZE][BOARD_SIZE]; + int iVSums[BOARD_SIZE]; + int iHSums[BOARD_SIZE]; + int iDSums[2]; + unsigned int uNumEmpty; +} POSITION; + +#define NUM_TO_HPOS(x) ((x) % BOARD_SIZE) +#define NUM_TO_VPOS(x) ((x) / BOARD_SIZE) +#define VALID_SUM(x) (((x) <= BOARD_SIZE) && \ + ((x) >= -BOARD_SIZE)) + +// +// A representation of a move in a tic tac toe game +// +typedef unsigned int COORD; + +typedef struct _MOVE +{ + COORD cHpos; + COORD cVpos; + SQUARE sMark; +} MOVE; + +// +// Score values +// +#define INFINITY (+100) +#define DRAWSCORE (0) + +// +// An assert mechanism +// +#ifdef DEBUG +#define ASSERT(x) if (x) \ + { ; } \ + else \ + { (void) _assert(__FILE__, __LINE__); } +#else +#define ASSERT(x) ; +#endif /* DEBUG */ + +#endif /* TTT_H_ */ diff --git a/ver4/ttt.c b/ver4/ttt.c new file mode 100644 index 0000000..4dd54a6 --- /dev/null +++ b/ver4/ttt.c @@ -0,0 +1,846 @@ +/*++ + +Module Name: + + ttt.c + +Abstract: + + tic tac toe program to illustrate simple minimax searching + +Author: + + Scott Gasch (SGasch) 18 Mar 2004 + +Revision History: + + ver0 : random play + ver1 : simple search + ver2 : alpha beta search + ver3 : added eval and depth on ab search, more efficient gaveover + ver4 : variable sized board + +--*/ + +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays +unsigned int g_uPly = 0; +MOVE g_mvBest = { 0 }; +unsigned int g_uNodes = 0; +COORD g_uBoardSize = 3; + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + case EMPTY: + c = '_'; + break; + default: + ASSERT(FALSE); + c = '?'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < g_uBoardSize; y++) + { + for (x = 0; x < g_uBoardSize; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } +#ifdef DEBUG + printf(" = %d\n", p->iVSums[y]); +#else + printf("\n"); +#endif + } + +#ifdef DEBUG + for (x = 0; x < g_uBoardSize; x++) + { + printf("| "); + } + printf("\n"); + for (x = 0; x < g_uBoardSize; x++) + { + printf("%d ", p->iHSums[x]); + } + printf("\t%d %d\n", p->iDSums[0], p->iDSums[1]); + printf("\n"); +#endif + + ASSERT(X_OR_O(p->sWhoseTurn)); + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + COORD h; + + for (h = 0; h < g_uBoardSize; h++) + { + memset(p->sBoard[h], 0, sizeof(int) * g_uBoardSize); + } + memset(p->iHSums, 0, sizeof(int) * g_uBoardSize); + memset(p->iVSums, 0, sizeof(int) * g_uBoardSize); + p->iDSums[0] = p->iDSums[1] = 0; + p->sWhoseTurn = X_MARK; // x's go first + p->uNumEmpty = (g_uBoardSize * g_uBoardSize); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < g_uBoardSize) && (m->cHpos < g_uBoardSize)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = OPPOSITE_MARK(g_sComputerPlays); + } + while(FALSE == IsLegalMove(p, m)); +} + + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. This also serves +// as a very simple evaluation routine for the search. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x; + unsigned int uFull = (g_uBoardSize * g_uBoardSize) - p->uNumEmpty; + + // + // The game can't be over if less than g_uBoardSize * 2 - 1 marks on it + // + if (uFull < (g_uBoardSize * 2 - 1)) + { + *psWhoWon = EMPTY; + return(FALSE); + } + + for (x = 0; x < g_uBoardSize; x++) + { + iSum = p->iHSums[x]; + if (abs(iSum) == g_uBoardSize) goto winner; + + iSum = p->iVSums[x]; + if (abs(iSum) == g_uBoardSize) goto winner; + } + + iSum = p->iDSums[0]; + if (abs(iSum) == g_uBoardSize) goto winner; + + iSum = p->iDSums[1]; + if (abs(iSum) == g_uBoardSize) goto winner; + + // + // No one won yet, either game ongoing or draw. + // + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + // + // Some side won + // + *psWhoWon = (iSum / (int)g_uBoardSize); + ASSERT(X_OR_O(*psWhoWon)); + return(TRUE); +} + + +//+---------------------------------------------------------------------------- +// +// Function: CountAdjacents +// +// Synopsis: Return the number of marks in adjacent squares to square x +// that are of the same type (x or o) as the mark in square x. +// +// FIXME: does not consider diagonals +// +// Arguments: IN POSITION *p - the board +// IN SQUARE x - the square to test +// +// Returns: A count +// +//+---------------------------------------------------------------------------- +unsigned int CountAdjacents(IN POSITION *p, IN COORD x) +{ + COORD v = NUM_TO_VPOS(x); + COORD h = NUM_TO_HPOS(x); + SQUARE sSide = p->sBoard[h][v]; + unsigned int uCount = 0; + + // + // If nothing at square x, nothing to count + // + if (sSide == EMPTY) goto end; + + // + // Look above, below, left and right + // + if ((v > 0) && (p->sBoard[h][v-1] == sSide)) + { + uCount++; + } + + if ((v < (g_uBoardSize - 1)) && (p->sBoard[h][v+1] == sSide)) + { + uCount++; + } + + if ((h > 0) && (p->sBoard[h-1][v] == sSide)) + { + uCount++; + } + + if ((h < (g_uBoardSize - 1)) && (p->sBoard[h+1][v] == sSide)) + { + uCount++; + } + + end: + ASSERT(0 <= uCount); + ASSERT(uCount <= 4); + return(uCount); +} + + +//+---------------------------------------------------------------------------- +// +// Function: Eval +// +// Synopsis: Evaluate a position +// +// Arguments: IN POSITION *p - the board +// +// Returns: A score +// +//+---------------------------------------------------------------------------- +int Eval(IN POSITION *p) +{ + SQUARE sWinner; + COORD x; + SQUARE sMark; + int iScore = 0; + + // + // See if the game is already over. + // + if (TRUE == GameOver(p, &sWinner)) + { + if (sWinner == p->sWhoseTurn) + { + iScore = +INFINITY - g_uPly; + goto end; + } + else if (sWinner == OPPOSITE_MARK(p->sWhoseTurn)) + { + iScore = -INFINITY + g_uPly; + goto end; + } + iScore = DRAWSCORE; + goto end; + } + + // + // No one won but instead of returning score=0 see if we can + // find some "good characteristics" or "bad characteristics" + // of the position and give bonuses / penalties. + // + for (x = g_uBoardSize + 1; + x < ((g_uBoardSize * g_uBoardSize) - g_uBoardSize - 1); + x++) + { + sMark = p->sBoard[NUM_TO_HPOS(x)][NUM_TO_VPOS(x)]; + if (sMark == p->sWhoseTurn) + { + iScore += CountAdjacents(p, x); + } + else if (sMark == OPPOSITE_MARK(p->sWhoseTurn)) + { + iScore -= CountAdjacents(p, x); + } + } + + end: + ASSERT(-INFINITY <= iScore); + ASSERT(iScore <= +INFINITY); + return(iScore); +} + + + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + // + // Make the new make on the board + // + ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY); + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + + // + // One less empty square + // + p->uNumEmpty--; + ASSERT(p->uNumEmpty < (g_uBoardSize * g_uBoardSize)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] += m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] += m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (m->cHpos == m->cVpos) + { + p->iDSums[0] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + if (m->cVpos == ((g_uBoardSize - m->cHpos) - 1)) + { + p->iDSums[1] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + g_uPly++; + ASSERT(g_uPly > 0); + } +} + +//+---------------------------------------------------------------------------- +// +// Function: UnmakeMove +// +// Synopsis: The opposite of MakeMove +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void UnmakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (p->sBoard[m->cVpos][m->cHpos] == m->sMark) + { + p->sBoard[m->cVpos][m->cHpos] = EMPTY; + + // + // One more empty square + // + p->uNumEmpty++; + ASSERT(p->uNumEmpty > 0); + ASSERT(p->uNumEmpty <= (g_uBoardSize * g_uBoardSize)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] -= m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] -= m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (m->cHpos == m->cVpos) + { + p->iDSums[0] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + if (m->cVpos == ((g_uBoardSize - m->cHpos) - 1)) + { + p->iDSums[1] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + ASSERT(g_uPly > 0); + g_uPly--; + } +} + + +//+---------------------------------------------------------------------------- +// +// Function: AlphaBeta +// +// Synopsis: An AlphaBeta Search +// +// Arguments: IN OUT POSITION *p - the board +// IN int iAlpha - the lower bound of the score window +// IN int iBeta - the upper bound of the score window +// IN int uDepth - search depth horizon +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta, IN unsigned int uDepth) +{ + SQUARE sWhoWon; + COORD s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY - 2; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + else if (uDepth == 0) + { + return(0);//Eval(p)); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (g_uBoardSize * g_uBoardSize); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha, uDepth - 1); + ASSERT(-INFINITY <= iScore); + ASSERT(iScore <= +INFINITY); + + UnmakeMove(p, &mv); + + // + // Fail high + // + if (iScore >= iBeta) + { + return(iScore); + } + + if (iScore > iBestScore) + { + iBestScore = iScore; + + if (iScore > iAlpha) + { + // + // PV node + // + iAlpha = iScore; + + // + // If this is the ply 0 move, remember it. + // + if (g_uPly == 0) + { + g_mvBest = mv; + } + } + } + } + } + return(iBestScore); +} + + +//+---------------------------------------------------------------------------- +// +// Function: SimpleSearch +// +// Synopsis: A Simple Search +// +// Arguments: IN OUT POSITION *p - the board +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +SimpleSearch(IN POSITION *p) +{ + SQUARE sWhoWon; + COORD s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + + // + // No one won, game is still going. Evaluate every + // possible move from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (g_uBoardSize * g_uBoardSize); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + MakeMove(p, &mv); + + iScore = -1 * SimpleSearch(p); + if (iScore > iBestScore) + { + iBestScore = iScore; + if (g_uPly == 1) + { + g_mvBest = mv; + } + } + UnmakeMove(p, &mv); + } + } + return(iBestScore); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ +#if defined(PLAY_RANDOMLY) + unsigned int x; + + do + { + x = rand() % (g_uBoardSize * g_uBoardSize); + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); + +#elif defined(SIMPLE_SEARCH) + + g_uPly = g_uNodes = 0; + SimpleSearch(p); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); + +#elif defined(ALPHA_BETA_SEARCH) + + g_uPly = g_uNodes = 0; + AlphaBeta(p, -INFINITY-1, +INFINITY+1, g_uBoardSize * 2); + *m = g_mvBest; + printf("Searched %u node(s).\n", g_uNodes); + +#else + + #error "No Search Strategy Defined" + +#endif +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + unsigned int u; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Make the board + // + do + { + printf("How big do you want the board (2..20)? "); + scanf("%u", &g_uBoardSize); + } + while((g_uBoardSize < 2) || (g_uBoardSize > 20)); + + // + // Allocate space for 2d int array ptr + // + p.sBoard = (int **)malloc(g_uBoardSize * sizeof(int *)); + if (NULL == p.sBoard) + { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + + // + // Allocate each row of the array + // + for (u = 0; u < g_uBoardSize; u++) + { + p.sBoard[u] = (int *) + malloc(g_uBoardSize * sizeof(int)); + if (NULL == p.sBoard[u]) + { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + } + + // + // Allocate space for sums + // + p.iHSums = (int *)malloc(g_uBoardSize * sizeof(int)); + p.iVSums = (int *)malloc(g_uBoardSize * sizeof(int)); + if ((NULL == p.iHSums) || + (NULL == p.iVSums)) + { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + // + // TODO: cleanup heap + // + + exit(0); +} diff --git a/ver4/ttt.h b/ver4/ttt.h new file mode 100644 index 0000000..6ddee35 --- /dev/null +++ b/ver4/ttt.h @@ -0,0 +1,93 @@ +#ifndef TTT_H_ +#define TTT_H_ + +#define ALPHA_BETA_SEARCH 1 + +#define TRUE (1) +#define FALSE (0) + +#define IN +#define OUT + +typedef unsigned int BOOL; + +// +// Constants for each state a board square can be in and a programmer +// defined type for these states. +// +#define X_MARK (-1) +#define EMPTY (0) +#define O_MARK (+1) +#define TIE (+2) +#define OPPOSITE_MARK(m) ((m) * -1) +#define X_OR_O(m) (((m) == X_MARK) || \ + ((m) == O_MARK)) + +typedef signed char SQUARE; +#define IS_SQUARE_EMPTY(x) (x == EMPTY) + +// +// A (simple) representation of a tic tac toe position +// +typedef struct _POSITION +{ + SQUARE sWhoseTurn; + int **sBoard; + //SQUARE sBoard[BOARD_SIZE][BOARD_SIZE]; + int *iVSums; + //int iVSums[BOARD_SIZE]; + int *iHSums; + //int iHSums[BOARD_SIZE]; + int iDSums[2]; + unsigned int uNumEmpty; +} POSITION; + +#define NUM_TO_HPOS(x) ((x) % g_uBoardSize) +#define NUM_TO_VPOS(x) ((x) / g_uBoardSize) +#define VALID_SUM(x) (abs(x) <= g_uBoardSize) + +// +// A representation of a move in a tic tac toe game +// +typedef unsigned int COORD; + +typedef struct _MOVE +{ + COORD cHpos; + COORD cVpos; + SQUARE sMark; +} MOVE; + +// +// Score values +// +#define INFINITY (+100) +#define DRAWSCORE (0) + +// +// An assert mechanism +// +#ifdef DEBUG +#define ASSERT(x) if (x) \ + { ; } \ + else \ + { (void) _assert(__FILE__, __LINE__); } +#else +#define ASSERT(x) ; +#endif /* DEBUG */ + + +void +_assert(char *sz, unsigned int i) +{ + fprintf(stderr, "Assertion failed in %s at line %u.\n", sz, i); +#if defined(WIN32) + __asm int 3; +#elif defined(__unix__) + asm("int3\n"); +#else + #error foo +#endif +} + +#endif /* TTT_H_ */ diff --git a/ver5/ttt.c b/ver5/ttt.c new file mode 100644 index 0000000..58f69e9 --- /dev/null +++ b/ver5/ttt.c @@ -0,0 +1,766 @@ +/*++ + +Module Name: + + ttt.c + +Abstract: + + tic tac toe program to illustrate simple minimax searching + +Author: + + Scott Gasch (SGasch) 18 Mar 2004 + +Revision History: + + ver0 : random play + ver1 : simple search + ver2 : alpha beta search + ver3 : added eval and depth on ab search, more efficient gaveover + ver4 : variable sized board + ver5 : bugfixes, added singular extension to search + +--*/ + +#include +#include +#include +#include +#include "ttt.h" + +SQUARE g_sComputerPlays = O_MARK; // what side comp plays +unsigned int g_uPly = 0; +MOVE g_mvBest = { 0 }; +unsigned int g_uNodes = 0; +unsigned int g_uExtensions = 0; +COORD g_uBoardSize = 3; + +//+---------------------------------------------------------------------------- +// +// Function: SquareContentsToChar +// +// Synopsis: Helper function for DrawBoard +// +// Arguments: IN SQUARE s - a square to return a char to represent +// +// Returns: char - character representing square +// +//+---------------------------------------------------------------------------- +char SquareContentsToChar(IN SQUARE s) +{ + static char c; + switch(s) + { + case X_MARK: + c = 'X'; + break; + case O_MARK: + c = 'O'; + break; + case EMPTY: + c = '_'; + break; + default: + ASSERT(FALSE); + c = '?'; + break; + } + return(c); +} + +//+---------------------------------------------------------------------------- +// +// Function: DrawBoard +// +// Synopsis: Draw the board +// +// Arguments: IN POSITION *p - pointer to a position whose board to draw +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void DrawBoard(IN POSITION *p) +{ + COORD x, y; + + for (y = 0; y < g_uBoardSize; y++) + { + for (x = 0; x < g_uBoardSize; x++) + { + printf("%c ", SquareContentsToChar(p->sBoard[y][x])); + } +#ifdef DEBUG + printf(" = %d\n", p->iVSums[y]); +#else + printf("\n"); +#endif + } + +#ifdef DEBUG + for (x = 0; x < g_uBoardSize; x++) + { + printf("| "); + } + printf("\n"); + for (x = 0; x < g_uBoardSize; x++) + { + printf("%d ", p->iHSums[x]); + } + printf("\t%d %d\n", p->iDSums[0], p->iDSums[1]); + printf("\n"); +#endif + + ASSERT(X_OR_O(p->sWhoseTurn)); + printf("\n%c to move.\n", SquareContentsToChar(p->sWhoseTurn)); +} + +//+---------------------------------------------------------------------------- +// +// Function: ClearBoard +// +// Synopsis: Clear the board +// +// Arguments: IN OUT POSITION *p - pointer to position whose board to clear +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void ClearBoard(IN OUT POSITION *p) +{ + COORD h; + + for (h = 0; h < g_uBoardSize; h++) + { + memset(p->sBoard[h], 0, sizeof(int) * g_uBoardSize); + } + memset(p->iHSums, 0, sizeof(int) * g_uBoardSize); + memset(p->iVSums, 0, sizeof(int) * g_uBoardSize); + p->iDSums[0] = p->iDSums[1] = 0; + p->sWhoseTurn = X_MARK; // x's go first + p->uNumEmpty = (g_uBoardSize * g_uBoardSize); +} + +//+---------------------------------------------------------------------------- +// +// Function: IsLegalMove +// +// Synopsis: Determine if a given move is legal on a given board +// +// Arguments: IN POSITION *p - the board to play the move on +// IN MOVE *m - the move in question +// +// Returns: BOOL - TRUE if it's legal, FALSE otherwise +// +//+---------------------------------------------------------------------------- +BOOL IsLegalMove(IN POSITION *p, IN MOVE *m) +{ + if ((m->cVpos < g_uBoardSize) && (m->cHpos < g_uBoardSize)) + { + if (IS_SQUARE_EMPTY(p->sBoard[m->cVpos][m->cHpos])) + { + return(TRUE); + } + } + return(FALSE); +} + +//+---------------------------------------------------------------------------- +// +// Function: GetHumanMove +// +// Synopsis: Ask the human for a move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the human made; this struct is populated +// as a side-effect of this function. +// +// Returns: void* (populates the move struct) +// +//+---------------------------------------------------------------------------- +void GetHumanMove(IN POSITION *p, OUT MOVE *m) +{ + unsigned int x; + + do + { + printf("Enter your move number: "); + scanf("%u", &x); + + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = OPPOSITE_MARK(g_sComputerPlays); + } + while(FALSE == IsLegalMove(p, m)); +} + + +//+---------------------------------------------------------------------------- +// +// Function: GameOver +// +// Synopsis: Is the game over? +// +// Arguments: IN POSITION *p - the board +// OUT SQUARE *psWhoWon - who won the game (if it's over) +// +// Returns: TRUE if the game is over. Also sets psWhoWon telling +// which side one if the game is over. This also serves +// as a very simple evaluation routine for the search. +// +// FALSE if the game is not over. +// +//+---------------------------------------------------------------------------- +BOOL GameOver(IN POSITION *p, OUT SQUARE *psWhoWon) +{ + int iSum; + COORD x; + unsigned int uFull = (g_uBoardSize * g_uBoardSize) - p->uNumEmpty; + + // + // The game can't be over if less than g_uBoardSize * 2 - 1 marks on it + // + if (uFull < (g_uBoardSize * 2 - 1)) + { + *psWhoWon = EMPTY; + return(FALSE); + } + + for (x = 0; x < g_uBoardSize; x++) + { + iSum = p->iHSums[x]; + if (abs(iSum) == g_uBoardSize) goto winner; + + iSum = p->iVSums[x]; + if (abs(iSum) == g_uBoardSize) goto winner; + } + + iSum = p->iDSums[0]; + if (abs(iSum) == g_uBoardSize) goto winner; + + iSum = p->iDSums[1]; + if (abs(iSum) == g_uBoardSize) goto winner; + + // + // No one won yet, either game ongoing or draw. + // + *psWhoWon = EMPTY; + if (p->uNumEmpty == 0) + { + return(TRUE); + } + else + { + return(FALSE); + } + + winner: + // + // Some side won + // + *psWhoWon = (iSum / (int)g_uBoardSize); + ASSERT(X_OR_O(*psWhoWon)); + return(TRUE); +} + + +//+---------------------------------------------------------------------------- +// +// Function: MakeMove +// +// Synopsis: Make a move on a board +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void MakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (TRUE == IsLegalMove(p, m)) + { + // + // Make the new make on the board + // + ASSERT(p->sBoard[m->cVpos][m->cHpos] == EMPTY); + p->sBoard[m->cVpos][m->cHpos] = m->sMark; + + // + // One less empty square + // + p->uNumEmpty--; + ASSERT(p->uNumEmpty < (g_uBoardSize * g_uBoardSize)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] += m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] += m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (ON_DIAGONAL_1(m->cHpos, m->cVpos)) + { + p->iDSums[0] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + if (ON_DIAGONAL_2(m->cHpos, m->cVpos)) + { + p->iDSums[1] += m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + g_uPly++; + ASSERT(g_uPly > 0); + } +} + +//+---------------------------------------------------------------------------- +// +// Function: UnmakeMove +// +// Synopsis: The opposite of MakeMove +// +// Arguments: IN OUT POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: void +// +//+---------------------------------------------------------------------------- +void UnmakeMove(IN OUT POSITION *p, IN MOVE *m) +{ + if (p->sBoard[m->cVpos][m->cHpos] == m->sMark) + { + p->sBoard[m->cVpos][m->cHpos] = EMPTY; + + // + // One more empty square + // + p->uNumEmpty++; + ASSERT(p->uNumEmpty > 0); + ASSERT(p->uNumEmpty <= (g_uBoardSize * g_uBoardSize)); + + // + // Update sums as appropriate + // + p->iHSums[m->cHpos] -= m->sMark; + ASSERT(VALID_SUM(p->iHSums[m->cHpos])); + p->iVSums[m->cVpos] -= m->sMark; + ASSERT(VALID_SUM(p->iVSums[m->cVpos])); + if (ON_DIAGONAL_1(m->cHpos, m->cVpos)) + { + p->iDSums[0] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[0])); + } + if (ON_DIAGONAL_2(m->cHpos, m->cVpos)) + { + p->iDSums[1] -= m->sMark; + ASSERT(VALID_SUM(p->iDSums[1])); + } + + // + // Other guy's turn + // + p->sWhoseTurn = OPPOSITE_MARK(p->sWhoseTurn); + ASSERT(X_OR_O(p->sWhoseTurn)); + + // + // One ply deeper + // + ASSERT(g_uPly > 0); + g_uPly--; + } +} + + +//+---------------------------------------------------------------------------- +// +// Function: IsMoveSingular +// +// Synopsis: Determine if a move is singular (i.e. only good move) or not +// +// Arguments: IN POSITION *p - the board +// IN MOVE *m - the move to undo +// +// Returns: BOOL : TRUE if *m is singular +// +//+---------------------------------------------------------------------------- +BOOL +IsMoveSingular(IN POSITION *p, IN MOVE *m) +{ + if ((abs(p->iVSums[m->cVpos]) >= (g_uBoardSize - 2)) || + (abs(p->iHSums[m->cHpos]) >= (g_uBoardSize - 2))) + { + return(TRUE); + } + if ((m->cHpos == m->cVpos) && + (abs(p->iDSums[0]) == (g_uBoardSize - 1))) + { + return(TRUE); + } + if ((m->cVpos == ((g_uBoardSize - m->cHpos) - 1)) && + (abs(p->iDSums[1] == (g_uBoardSize - 1)))) + { + return(TRUE); + } + return(FALSE); +} + + +BOOL +IsMoveWorthSearching(POSITION *p, MOVE *m) +{ + signed int h; + signed int v; + unsigned int uSum = 0; + + for (h = m->cHpos - 1; + h < (signed int)m->cHpos + 2; + h++) + { + for (v = m->cVpos - 1; + v < (signed int)m->cVpos + 2; + v++) + { + if (GOOD_COORD((COORD)v) && GOOD_COORD((COORD)h)) + { + uSum += abs(p->sBoard[v][h]); + } + } + } + + if (uSum == 0) + { + return(FALSE); + } + return(TRUE); +} + + +int +Eval(POSITION *p) +{ + int iSum = p->iDSums[0]; + COORD x; + + for (x = 0; + x < g_uBoardSize; + x++) + { + iSum += p->iHSums[x]; + iSum += p->iVSums[x]; + } + iSum += p->iDSums[1]; + + return(iSum * p->sWhoseTurn); +} + + +//+---------------------------------------------------------------------------- +// +// Function: AlphaBeta +// +// Synopsis: An AlphaBeta Search +// +// Arguments: IN OUT POSITION *p - the board +// IN int iAlpha - the lower bound of the score window +// IN int iBeta - the upper bound of the score window +// IN int uDepth - search depth horizon +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +AlphaBeta(IN POSITION *p, IN int iAlpha, IN int iBeta, IN unsigned int uDepth) +{ + SQUARE sWhoWon; + COORD s; + MOVE mv; + int iScore; + int iBestScore = -INFINITY - 2; + BOOL fMoveIsSingular; + unsigned int uNextDepth; + unsigned int uMoveNum = 1; + + g_uNodes++; + + // + // Evaluate this position + // + if (TRUE == GameOver(p, &sWhoWon)) + { + if (sWhoWon == p->sWhoseTurn) + { + return(+INFINITY - g_uPly); + } + else if (sWhoWon == (p->sWhoseTurn * -1)) + { + return(-INFINITY + g_uPly); + } + return(DRAWSCORE); + } + else if (uDepth == 0) + { + return(Eval(p)); + } + + // + // No one won, game is still going. Evaluate some moves from here. + // + ASSERT(p->uNumEmpty > 0); + for (s = 0; s < (g_uBoardSize * g_uBoardSize); s++) + { + mv.cHpos = NUM_TO_HPOS(s); + mv.cVpos = NUM_TO_VPOS(s); + mv.sMark = p->sWhoseTurn; + + if (IsLegalMove(p, &mv)) + { + // + // Determine if move is singular + // + fMoveIsSingular = IsMoveSingular(p, &mv); + + if ((FALSE == fMoveIsSingular) && + (uMoveNum > 1)) + { + // + // Determine if we should bother with this subtree... + // + if (FALSE == IsMoveWorthSearching(p, &mv)) + { + continue; + } + } + + // + // Do it + // + MakeMove(p, &mv); + uMoveNum++; + + uNextDepth = uDepth - 1; + if (TRUE == fMoveIsSingular) + { + uNextDepth = uDepth; + g_uExtensions++; + } + iScore = -1 * AlphaBeta(p, -iBeta, -iAlpha, uNextDepth); + ASSERT(-INFINITY <= iScore); + ASSERT(iScore <= +INFINITY); + + UnmakeMove(p, &mv); + + // + // Fail high + // + if (iScore >= iBeta) + { + return(iScore); + } + + if (iScore > iBestScore) + { + iBestScore = iScore; + + if (iScore > iAlpha) + { + // + // PV node + // + iAlpha = iScore; + + // + // If this is the ply 0 move, remember it. + // + if (g_uPly == 0) + { + g_mvBest = mv; + } + } + } + } + } + return(iBestScore); +} + +//+---------------------------------------------------------------------------- +// +// Function: SearchForComputerMove +// +// Synopsis: Use our sophisticated search algorithm to find a computer +// move +// +// Arguments: IN POSITION *p - the current board +// OUT MOVE *m - the move the computer chooses; this move struct +// is populated as a side-effect of this function. +// +// Returns: void* (populates move struct) +// +//+---------------------------------------------------------------------------- +void SearchForComputerMove(IN POSITION *p, OUT MOVE *m) +{ +#if defined(PLAY_RANDOMLY) + unsigned int x; + + do + { + x = rand() % (g_uBoardSize * g_uBoardSize); + m->cHpos = NUM_TO_HPOS(x); + m->cVpos = NUM_TO_VPOS(x); + m->sMark = g_sComputerPlays; + } + while(FALSE == IsLegalMove(p, m)); + +#elif defined(ALPHA_BETA_SEARCH) + double dTime; + + g_uPly = g_uNodes = g_uExtensions = 0; + + AlphaBeta(p, -INFINITY-1, +INFINITY+1, 3); + *m = g_mvBest; + printf("Searched %u node(s), %u extension(s)\n", + g_uNodes, + g_uExtensions); +#else + + #error "No Search Strategy Defined" + +#endif +} + +//+---------------------------------------------------------------------------- +// +// Function: main +// +// Synopsis: The program entry point and main game loop. +// +// Arguments: void +// +// Returns: int +// +//+---------------------------------------------------------------------------- +int +main(void) +{ + POSITION p; + MOVE mv; + SQUARE sResult; + unsigned int u; + + // + // Randomize: the random numbers returned by rand() will be based on + // the system clock when the program starts up. + // + srand(time(0)); + + // + // Make the board + // + do + { + printf("How big do you want the board (2..20)? "); + scanf("%u", &g_uBoardSize); + } + while((g_uBoardSize < 2) || (g_uBoardSize > 20)); + + // + // Allocate space for 2d int array ptr + // + p.sBoard = (SQUARE **)malloc(g_uBoardSize * sizeof(SQUARE *)); + if (NULL == p.sBoard) + { + fprintf(stderr, "Out of memory\n"); + exit(1); + } + + // + // Allocate each row of the array + // + for (u = 0; u < g_uBoardSize; u++) + { + p.sBoard[u] = (SQUARE *) + malloc(g_uBoardSize * sizeof(SQUARE)); + if (NULL == p.sBoard[u]) + { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + } + + // + // Allocate space for sums + // + p.iHSums = (int *)malloc(g_uBoardSize * sizeof(int)); + p.iVSums = (int *)malloc(g_uBoardSize * sizeof(int)); + if ((NULL == p.iHSums) || + (NULL == p.iVSums)) + { + fprintf(stderr, "Out of memory!\n"); + exit(1); + } + + // + // Setup the board and draw it once. + // + ClearBoard(&p); + DrawBoard(&p); + + // + // Main game loop + // + do + { + // + // See whose turn it is -- the human's or the computers -- and + // get a move from whoever's turn it is. + // + if (p.sWhoseTurn == g_sComputerPlays) + { + SearchForComputerMove(&p, &mv); + } + else + { + //SearchForComputerMove(&p, &mv); + GetHumanMove(&p, &mv); + } + + // + // Make the move on the board and draw the board again. + // + MakeMove(&p, &mv); + DrawBoard(&p); + } + while(FALSE == GameOver(&p, &sResult)); + + // + // If we get here the game is over... see what happened. + // + switch(sResult) + { + case X_MARK: + printf("\nX's win.\n"); + break; + case O_MARK: + printf("\nO's win.\n"); + break; + default: + printf("Tie (what a surprise)\n"); + break; + } + + // + // TODO: cleanup heap + // + + exit(0); +} diff --git a/ver5/ttt.exe b/ver5/ttt.exe new file mode 100644 index 0000000000000000000000000000000000000000..49cde61d516cc66014b95858f533745e453096a4 GIT binary patch literal 31744 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%0A2mPpZnXTnah&IuSbT4{ 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

    xI2Yo?zM=T6G9d-goO2 zj`!A@@>z9AI6Xn_!ILjIFP;G(EYkPMFk9axJ zMf_5nf3aWU@?mW6P}Bw$^U^_`zM^4_%)f7Z>Mv66Sbt(1Eo>+#(% zKYz~sjQ-5rOBDO^W-fg5_vPFO=@S6<;dm6nc`&ZTOeNin|6 z)n|Qpa_%SV!v1l z{JoibJ^J#)yEpps!}~G({pdR^{JoeHU;ciy_T4)PJToh%efR!=51&^vr}<^pYNuV6 zgZZBEZS7cxCf1yv@57TmO`~unsGBBO#0pU1AhtWn*^M5s5I1%;Je0~eb@|dr%&*_31%;L;@qm> z0Wc3nMPW-eZ{xE)7hz*8u>djA>vkD@Eey+qE31dq( zpO|d!0W&WAYr-=u`A-Etg^n$h=<|M!6}a^#C;>*oRBXA0ohwtk-m zZqKXIi@{B929Wuiop5&kGA^V{;Gao?OKw~VJX!oD{1)Jx(xQ>{+4XlvAK`E9BmBL< zM;P|*U#5>`pYpzEg%=O6v+z5yc@VhNd%^AS4*}1X*6 z{k#pF^FuT?E>HgrJYwJ&uH*0I9}53@N%1W0+5~)PB93Qif4YHlV=cMa@}+&a6L=kz zDe)yMJN`chz90!M++GGgDFL5E22TCUK;e86Pd0ypoA?hq*~E__#F+AH!Yl;Ndmd@* z@_PiH$+HUh>I9tSZ?KcvbAYc(f)8@ItpL6x2`=r!J$dG=16aDoEg^gPWrb4KQ{>;aJc;) zi+?*T-uS8WU&>1Wjq|k$IP*{1uORTRCc&lsO$UB;5?s=EBXHiMXi^K0Ff2Y?k7xS6 z1w1DKzZo7(?+4#X2hKa7?eN4odrk8l;KP#O65;{8*m&$aG~_S!vm6U;S87+2Ucu>?bPn*#lHlU|bHL^O;qtfIkIleK z62oiUXzY35MGkJ4FUkLxfX_{Wi~QrzxSZS<5 z)d3g%y113Ti-8xI;Boas_+JKG;%BE%_l(P-ccn+- zX^yr!fph)eHpEk%)ZPl5<%6=snEGMdz6tyc0C70NXE-k30?xhXi8#kEGgHbv2Fe@0UzPGyY(N>X&`cV6L<(Vjjcby-vuu1lZ}h-kwxa* z*lxtxZa&Ii6eDQ={_Tw&HIz2bjzWo-OlB%1Og z={pZN*W9zgz}63SM*kN84<^B-{b>hIWhRCfZr1^S5(U!6?}V;ITNhTFb8vJK!~7eH z2g+Xu{B{8xo4B+;7X!aB2`=(as4-{hm^M58lD?CG9|UIO?EeHE-&26Un*^8sRUYsI zNpMNu_*!#bPx?FXs@tZ?uo<6H;P*qGjeiaLOn2n-i@HBbf=m6Y1pdP$xX2%d#tlw!O%1efv6t-ybr1Q-1_VQ?w^ zAI6%-*6$7HWBv#V$%VTeKdBG506#GSAA$k3yx(pGJ~jz1`gscYn~0yypW!8b53e%! zz0$>&FXi)X;Ozg?7>%3yBk$jXt57x*@Grry#P6@bIX<9~@Wkb*vkv<^^|QE``)?(F zgIAmTC}Save01q_d`AG6@w@PcIvf=3NWJ-f9bIgkDqwmC0sj<17#o-TKLU8vz;U?v z-V9v&H@5zUAPk?c1db+38ylDQ^D5wnj4!lr6PuTSZ-f4Lw~gICjD{cbdmH$r3HSr3 zAJTt#2l%Ix;L<+43;dEKxJcU&IkbO8{iZqcp8|e~^V`-R<3{=maJF6iZR5rTXJ8x6 zo*uy34{h?7@JoRIhK?|Fqp|aMGw?f&a{_Lpor2C$yMx>EC4Wu>UXg&ad`kZ017DB? zm(<)0e2RnH@e}<#0sLVHxARZx&r`rpOu+5>^EB|WNpO*WTr1iq2rGi0kmHZ~k@5AY zHj`I2ZjY~D?gI~X_=o>J@SmdmPzP9Q(Z-hlK&J_B*bPN5l9_{(TZS`;xY?+wV=l_c)*(UgEzQ z_zx0r#(#)YTE7DP?j*Rx|0>{#<(uIp{zYA}_qQ#dzey#)<^65rQk%MfpF?&qHZJMk z3w#GK)*aSk8tO;t^i#mM;5iYO_Wx<%Urd5a`Hgg&_5)txDZqJ`P$Dk*HyikqNpO*t zwa$#cs6RXY(mtFCoae!dfU}Q6W7n@E*T?Fo;CB5Qya9X>zSabF!?XWFc_V=TH*6ap z4M656&fRF%?-TVP;O*(XXp%AEuA4g z&kCIsI$t}$oet)q%RI~h3eRO4nO48LrfzMdp*qsk6`D%-D(y~Bz^dw^b!}Y?aL6Xy zWZQB-uE;RO>z$-dW$L^(tR1o;IJb5N0z(r>TvZ&4uKmTk`;0$+?aY{iJf(U*e$mrs{PRT5{-=qaIsKm}d&aDPTC(RzIrG>z zs;l8gvz7kiFWDa^tsliE{*R{V>R(fe2D7!J-g^V(zxrW;kH0jYa+H-ZC)B-~R>Cda}b$svi4x)E_-`<|KY9Q>{IcS3aYb$Qa<4N_v12sOXE1#ZM6?}x zFy`Y|-t?Jy(vkcp2+uo3hG!ntX5dJWbEMcPV&F)S=Zjj74*j!_1YLp2uq!wx?U*9W zXSZ@rTb?uI*P-@?bfsRz{5!YVnWgrapKF}8kipmzQ-a^~A>-R1ZmHq6(6>g!{K_wG z=LlJ{#r+Jf!NJ@QULo*00rEI?&E@Iq5eF`FECRpHn7+OS_C=Tz$4~W#p^ynbZ-P?c zwl#tGot#UD`JT*Io5#4n)4@Xwmw%7PdcbAiN3$2$8rbiF&uGMl%RSt(cM;qPcjKMKFcLw*Nj zh~XzWmdV3_J)nXAOByJsEl{bW(@>ELE33mximPi%D=Wg~1!wbVhC1MZ=`gq{#12zh zQCY3sGqrnBaanO~aZ$LUpu9L-R$Q^LwnV+D`5AnP?uaccsEYGJc`2%*ptf{LakzGQ zRWahSEL=EWy=B6Fpm~>;l^55RR2GG6iVJJQHRT0mW!hg6l2cQJ=;Qw01LAxQRo@hx~I?QKYb=t*xbdK{OJXkGaS- zwau7VZtiGpu=$N~{nd7KBRcS$)L%$p5ok?S6k1zfTerG3f^sHt%0=4Ox3u-N6>!e8 zjf2(!HU)u4|1W zpE{nU3Tui)^Nz0QJ{vjT6^UZeZn~?NIjXp!rMs*Jdjs6~&r{H457fG$u%ly5OT^Ys zzN#rMscUa&jYLhdt-L6TVTBz%?K+=ksNm>PkbQ#nA}(>tc3EXX5h`rOg3^WIqS9(q z@5<`sGiRWTq>M~K7;Fc?dAQ7MmRojb#)8LSm6|m`72j+XUbwil2zwKQqm}9fYMbu5Cgf2= zB&y>!(TUqQFTcg@-O&xj?G4B;rsWe>S~|g&X~{=gm^P-L&%Jo|l)W(8(bH-CPX#aS zKVKc#=;*@euBE*}`&-;4<@6-@4XFC|Zmi$h<#P($i`43t?zK8VNsCji=)O1RUe@8b zPow{orf4U1po`I)IbPvSb-7-PalL*3C$aF!8fZV}vK{&B_v&(cQGzuiVHv@n(hV&up6s}xxB zvQ<)AQBzw`QCJ)fLkDaZ7HK}ngA}_>YgktqX)9&!U07F3Mp%&6}_zTDzfB z+As1RL*DKyb|J3c>1nOwYs=D4$kI<)WNoC?!p2!xb!2Ue?x{c*%p;Bs%aI3Sa#gry zc};C`dAPJ0_?Bd6R|rwA(0K@7HXF zK2~?c^*4(B039;c_sCINNobFPqd9L;X{BL|Lbtda@7S>J;p&uBm!tb#S%Gs&cKg-S z-qVGzxj4FDy*kI@6HxKx)h^?FN89y*WnSZ2ky6)8od`JZbg6r6H%P)SfRejZ!s|L@ zo|Cs>;RU4_gp{JgJK0IgiCSi3cNn+3pT@xu|rZ zi~-a6Y^5(lhv%@=#24e=5{d6Ih%e`o7~gipx3Hw3S|0kA9>a#rq4W4Yogc_VV{h_!F6w$MV(b$YdDq&ixeJnBGo2>H3Gh)kvp4 zUIGkXr`pK4AA@?!wDr<0bzV<*R z#I^|6V8+E8^NhMobi$w3gUNS2E{d}b@(lL5LY&{O+7u<^2@fDetS`F={JPm680GAiG5q@G#&nEvo6Kgr=Z#a%$9>Y zs6FBw?T$pFZCy=W+W#$%>}}AgyaSC6wZC>5vzfA~-MTSSa^G9GA-tGOW%I`SS_7sJ zYsU@eadkSs?#M=R^s}^Ae5O9f;)mqD_yx;2zkH@&hBbbT+zh9mKx~(bnRnaWcqYZl zP^w;Jkuu5j3&s4Tse5#ow9#xwW92aRJPL7XclzG+gIW}Bt7~ehN4u`u4!ESkt$uZD zr<6nb3(yVYaq*MR=V)ua?I%S8PmD(no?9F17+aU`F>vcxi=yMywR@qrF>u3hK=*|h z_gwmkw6)mdk_nC*s~mOlJ%pQN!p#s~?ElPz8)UXe)~N#%P;a?4eK-7y$yZ@Bv0GGG z6D}z(z$7Vjm72X1epp5r2DMGRJGK+1qSgsgqr?2r3B$gzXPC+=_J<3~L>SUdc|UT( zPWB7SDe2fumadaOK{(wWpgc9EnfH(>PvyEaQ8yvPn(q+ImzK4(uZg&`rjE{urds_qobQvU; zs}`WOj{#D*CK;Vo;<`+XQrnwqHgt7I+GOH0R!=7z-Sz2jis7dtCtO6%ejyYPL-)36G)J5{jSB1@y z`ZYDE`EDJ?ysSjct+NYMQEO{qGfrvL;~j!&sg`;@_b>TT;_%0avwn>;dm#0ea>{Jn zndXtORk65XVn@$7yZKyeVNNhNpO;}yofD~Mx;s|>Rw|hxs9sxF(B8n5%Ur}B)UQ=~ z?zX5Uia|8y({Tq~a5V1`_$e?kFG@R-q<;U*>2FBA#4Jj?%rnp})s^p*QEPr8+4=|d zlc{tq!7O=4%?4a&+-hda-Lz)KcxqVELwOkI>%6V(reYPdzN`8KLEb+{_aE^nZ{nUX*W5#<1@#~l{HQ=E=9}FWLRaE zW9GP3A*)TuBxbJ+Sy&dZ`8_SI4W<~lWr=f3Rjqa1jU7==ifR2khD4cZvAg-hnYuy?J#j^YjrzqwZp`g zS#7P|4ij5tVTtWb6>j+t+J%;JwRwbNgW?~D2N+Y;Kxv*Y4XgZ@aJo>FHyc^t9=x}H zEjBRrPt%}-C#*OglsH=bQ{R4ULf`in;KjB$J#KBFi>EQeIMC^~j{zla>s_}@acgLC z-Lk~3E9RE%y2bdRUDYz85vapi(`UP%=MbH64F5`7;#hdS>#Qt=a_P+p!TO-a3UdHAr>X(*$tQl97Ei{V- zGVjZ{$Ht&3>RI@|D=u!4_4N_`(hs-mHIY~!7~@J8Yvdk@xOm1e^np5}&byHEFn)By z)j6v;(%xk&BPHC)!@3dl17rP2j1}E*oHvQK8SR*Ql%k%u(psg$#mfqd^}s2NsoTnG zEbCX-q8}DpyF$6E@92zl>#Y)2{$t%$N(JWKIW9|4FIc>xN1OM;(wf?8y<&ll1Lfsd zxHwSPRAI@%IK7=0>|;0(Hia$Lj|?cx8X=W-KNj)Av{6fAOMTe+{Er!jG9JIN;*o{* zpTr&I6XN7{a&_75-vyMgdsDB)kl*hterTtXrwjR-e2p-?tgxy!Tu`W&YxKOyOW?;m zUXOm*Fl(-<3eV5P#jOy_TsiJs~_{|RXuqZYJw)OGXv0k^zeiTQ)3g|YT)3!fLn zp11P3IQG1a&$tjq^6+}@gR*39SH*Z6#>)9Es(AH!>v<>q+s}8X;(GgemnyEapYK8V zPAmLwRg9^)82|eipTbyt_Nd~xvc&SZ7jd$~Jj5_HCd}*5%VQ`P-I4YNeJKN8#B9$w z*L^g?{+CrI9!uq}tJ>mawes%jg%8`raL#?bfv~I_vM$PUtzu;`u3Ya2hBY%&E*wPq zX$P!0#I=7_m8FPIbq(|(Yb{JKda(7K-5B&adg44T`+iMya_ZO*z%x_(<1PcdzTqxw zEpJ)!(0<3YpB{A});3siq7L2xFVjAld1uD&UAXam&UkT_;4gTNU%O~5ZEwcxiZhpG z;@{|W#T(qR$9{&%Y}3B6zxday_$?_A#_$(p{=6ZZUK(M;At*bdTN)`Q03 zS#+9#n=RAem%IboHW(M(xVORV!%59fcQEN^5hoAh54Q_xJT-fe<4@K!i6@IA?=u_X zgvoNkkl*`Ai!FDUi^bM?QWM_A*hg|e_N`G5*i}-up3^th+(!g^z?2nG!Y@E>j(&Wy?M( zldr^?R`#35BHnh~xCYMWF1YKsI%`n!9B|g&q)g?*zejyHV@2*P=T>-YePJBp#QL7R zj)gIDt1X?6d}7(mb^KPu_Z6fcYPKCY?G@vs$B$^87jRf(xo0Y8XRQWeW5)^LFD6+( zRN7^eZ&l43x>~T|MY$r!;?D?ZR_AYT1f%BsRxqY7;3ya7OYHQAQObo|* zNW=_cthKOEY@WgjZuG$U)p0Iv2ao9~*JFN_(i7sx1Z@0Fq3yD!*8al;kfLOz1&%< zHoC!-+yhJxn~6Nl+oXx(GQaegv0Sh5?EdvgzcXlLU0|AKIBB}qNz+U%*G+@oWz2q? zNt>Sea@UgRhBR^2qO1kG{q6dNwXUAkW|642Wwp@({byr72OALM%4fEc6(CbC=x>xN zh+-FPv@W^<8>yr2_@8m%A+#Ly$66arJIyw5mZdN3y|da3vk#Sd6>A4m)NDMh}MI`Ips*Sxef)F<(O;om=o*7nyPkVUpf_T>FF%gRWt$?oJ_Q%@4Ai<*9Ww%^mBo$lTDe&hbn48LED@q+iD4A*6Mdr5AB$S(>Ao zYq}ds+dG|eRdk!D-70&!>FDT??)lnX&l#a?21hfGmgus8A23CjPs1+D+Qn67^(}cn z3%i@J697xPy6!GRcJSeu-xRw9g)JNA9qpG~Y&=fjV?-wxrWT+-&aaFxbm^%QlTbzbk~BR|SBWjICE z!|#9@(7;Ebfn!qcO}{<3PCXsG1MjVqRCMqSS?kj`1{+c<0u$4x22UDP9hjLO2v!F> zQ`3SQgKN?s4vtSR3BHik66_9?WGqVgRC+X+AGkz)BmE8BvR)iml)fm?q;67g58AJ; z81j{2m4UXwRe`-j?;5@{y*~BA;Y)`1V;wfW&OvH3912aym@NJ@DYJyGT%Z?Pp9Wq|sn|{Nf5y6eAWAQD9j`a6}DZzQE*9W%`e=M*wbw_YR%9-gm z1q;Z1O zl#U3D3qBb*IsKQx#`HIX7pv9K%bK)D0y~1w2d>QeUEs5WE>M3Be0A`M^h<|kr+hAO zyLx!&4ArO#vsMH)2QL~F&DfrK!SJgBuMag4pdg0E%XhcA*;rOr*eI;}gk zE9HsI*AeT#t9gU|G-y+x1=mPC5- z1tM7)sUa0g|14U$_S8EAvs3efC!~xIwy6ne<5Fh@p3TS)KA5&3n4fxe+Sbe?Glyoq zH|VX*iePP4S@8C>_F?B_R0Yq=ye55OMkH{}u*ZiTm;O}7i7Io@?v#ycZOXco3)Q=+ zFAQ6odh_r=aBaqhz=S|v>c+tQ!MT~+Gxwy7O`jMDrRN6zoKldseaNCA5p`N1bJ(=h zvcS~T4(PTjkgMv`*9K60wfQ;i5PME+XN}F@G%wPtF1Odo~MI77Nbo;&f3F+zD=bd5eo ztLJa59KhT~dF7H~uQ`>Lj@3Pl(yy0&)L?nwi_SQZ?`Pbi-P!G&4L9%kFb-Rqv7G6< zg;&=(`wEFeW+S>ox^K&8j79X;arSlj%=t5Px+q!q>FJiE!g%g#+356@$QR?c?zYZG zo-)w!=lCvQr7fhwZu~ihWf=D1*&ij(aUkw>zgW_OXPiY?+T76vrF!9F z8&fu@yKIMt^r^^Wgu{pF$v|Bn={y_0@@)EX^iTa$uJ|AA_-9^7JY*i2{^?Gle}*&u z0xAbsI{IpGcHQ}#bI-ToUIdH^$aM*h4*BM!$JcxA;xUhYs+y08Ll%vqyAFI&I_zgR9W zTvSv%fAPYit<$EbY<#jUpDpAk$Nuf^XJ+1Ba`#Ewj;q*PKAhdolexQ?D~jr5mh&#% zq(YBiN+lS>%$19Chz*zl4Gd`D|AYosAeMZe<9wmoiFg0Zu!;6ASj}kRif~Cx(JX~M zg}4aoID88jjlv?d)I0HfA7~cZfht^##cz;pLK|=p{e!3Bz8RE@-*Z63N?<<+y8(0u z+Kr3gcNcyafmVWA!J``&YhfI(Zb1XyOE*w2!Z8dRsCoF!=X>z`ebB3DYwp7{L?XaH)uRZWr1uDbB6~kA-FXO%y^bqJu5b;i6_aOZH;FE)J7lEqa z_A30p0-6Kb47aVY8TTsu-Uoaies2do4Pv}#-vOeW67bGC2kXM{2NCLd&~qTleHioz z=uyyPpnnHF4tfH#5A+Mrlc1+S)afrl&wy?M-3+<~bSvmK(Cwg|pl^V_3Hlc34$z&T zyFhn?sHcAeeH-*0&^@5tp8;J8+64M6=rYh3K$}6I2VD-j0z}<) zfx1C_Pp$>wlPlbRq&9%?c@zD8vWsBzqBwqQh~K;F1w}zW0R0fe{P+>*$Dp5p_JDp0 zLRR42Y`!7H*9`89Q$GXU584ZQ0Q7UvgP<%BiV@z)=KHtVuu)v~{3YiMM}bgu_4hW9 zf}I088gvZkSkM>{ia7c-=9_~d*keKCKqrE7LE}LaKodceK%W3j2Au?&0-6e%209s( z2bvC=0h$S#1v&*Z8+0mY4(K${T+r#Dd7v{uXM)ZG<%0@9^Ff86B2Y1C0catp1XKz- z8?*>i1}XfB(0QQqL2L$Af$Bi3LG_>p zPz2NnY63NbT0j?o)___;ZJ>5g2dEQtA?S0UFM_@V+5-AA=qsSBKwCjqgRTL66|@a> zE$C~Y>p<6oZUB89bR%dxXb0$7(101xz<>t+F%A3*!C&~tSPX<7(7=EO1~f3BfdLH+ zXkb7C0~#36z<>q@G%%on0SydjU_b){8W_;PfCdIMFra|}4Gd^tKm!9B7|_6g2L6|5 G;Qs-`%5cO0 literal 0 HcmV?d00001 -- 2.46.1