Minimax for Tic Tac Toe

Started by
20 comments, last by Jeremy Stover 11 years, 10 months ago
Quote:I once tried to implement a hardcoded tic tac toe strategy, but it is much harder than it sounds. Even after exploiting all the symmetry, there are still so many cases that it is nearly unmanageable.


Are you sure about this? I once heard that all cases could fit on a single sheet of paper.

One detail to keep in mind about pure minimax is that it plays towards the best outcome given that both players play optimal, which in tic tac toe is a draw.
It doesn't try to fool the opponent into traps, because it assumes that the opponent won't fall for them.

Edit: fixed typo...
Advertisement
Quote:Original post by Ohforf sakeOne detail to keep in mind about pure minimax is that it plays towards the best outcome given that both players play optimal, which in tic tac toe is a draw.
It doesn't try to fool the opponent into traps, because it assumes that the opponent won't fall for them.


This is the one problem with using minimax for TTT. The computer will play a guaranteed winning move at any point in the game if one exists, but it otherwise doesn't differentiate between moves that it anticipates leading to a draw as it assumes the player will always make the proper move. For example, even though playing a corner to start forces the opponent into a single move (in the middle), the computer will play anywhere because it assumes the player will always play in the middle if it chooses a corner, etc. etc.


At any rate, I have a TTT game written in C++ in which my AI uses minimax. I have it set up with a Board class, but that isn't strictly necessary. You should also be able to implement it similar to the following pseudocode.

You will want a function that returns a "score" from the perspective of the computer player using the function you use to check your game board for the current game state:

int EvaluateBoard(board){        int nGameState = CheckGameState(board);	if(nGameState == computer wins) return large positive int; (LARGE_INT)	if(nGameState == player wins) return large negative int; (-LARGE_INT)	if(nGameState == draw) return 0;        return -1;  //still playing}


A function that generates a container of available moves from a board is also helpful:

container<int> GetValidMoves(board){	container<int> nContainer;	for(each location on the board)	{		if(location is not occupied)		  nContainer.push_back(location);	} 	return nContainer;}


Your Minimax function takes a board and returns a move. It will utilize a Min (and via Min, a Max) function that takes a board and returns a score.

int MiniMax(board){	int nBestScore = -LARGE_INT;  //stores the best score evaluated, initially set to the value associated with a human player win	container<int> nBestMoves; //container to store the moves yielding the nBestScore;	container<int> nValidMoves;	nValidMoves = GetValidMoves(board)  //valid moves for the board	while(!(nValidMoves.empty())) //we're going to loop over all of the valid moves	{		SetSquare(board, nValidMoves.front(), ComputerPiece); //set the square associated with the first valid move to the computer's piece		int nScore = Min(board);  //call the int Min(board) function which ultimately will return a score based on the final board state		if(nScore > nBestScore)		{			nBestScore = nScore;  //the score becomes our new best score			nBestMoves.clear();  //we have a new best score, so our best moves are no longer the best, get rid of them			nBestMove.push_back(nValidMoves.front());  //add the move to the best moves container		}		else if (nScore == nBestScore)		{			nBestMove.push_back(nValidMoves.front());  //add the move to the best moves container		}		SetSquare(board, nValidMoves.front(), EmptyPiece);  //return the square to its original state		nValidMoves.pop_front();  //we're finally done with the move, discard it	}		return (random move from your nBestMoves container);  //choose a random move from your best moves to add some variety}


int Min(board) returns a score based on the final board state, it calls Max (which in turn calls Min), until a GameOver state is reached:

int Min(board){	int nMoveScore = EvaluateBoard(board);	if(!(nMoveScore == -1))  return nScore;  //if we're not still playing, the game is over so return the score;		int nBestScore = LARGE_INT;  //Min is from the player's perspective, from which LARGE_INT represents a loss	container<int> nValidMoves;	nValidMoves = GetValidMoves(board);		while(!(nValidMoves.empty()))	{		SetSquare(board, nValidMoves.front(), PlayerPiece);  //set the square to the player piece		int nScore = Max(board);  //we've made a move, so evaluate the board from the other player's perspective				if(nScore < nBestScore) //if we found a better move for the human player		{			nBestScore = nScore;		}		SetSquare(board, nValidMoves.front(), EmptyPiece);  //reset the square;		nValidMoves.pop_front();	}		return nBestScore;}


int Max(board) is the same as Min except from the computer player's perspective, so swap -LARGE_INT for LARGE_INT, (nScore > nBestScore) for (nScore < nBestScore), ComputerPiece for PlayerPiece, and Min(board) for Max(board).

I'm sure this can be streamlined, but for a Tic-Tac-Toe game where at most you are evaluating 9 moves, it's not really necessary IMO.

[Edited by - Crowseye on February 15, 2010 12:52:08 PM]
From wikipedia:
Quote:
A player can play perfect tic-tac-toe if they choose the move with the highest priority in the following table[3].

1. Win: If you have two in a row, play the third to get three in a row.
2. Block: If the opponent has two in a row, play the third to block them.
3. Fork: Create an opportunity where you can win in two ways.
4. Block Opponent's Fork:

Option 1: Create two in a row to force the opponent into defending, as long as it doesn't result in them creating a fork or winning. For example, if "X" has a corner, "O" has the center, and "X" has the opposite corner as well, "O" must not play a corner in order to win. (Playing a corner in this scenario creates a fork for "X" to win.)
Option 2: If there is a configuration where the opponent can fork, block that fork.

5. Center: Play the center.
6. Opposite Corner: If the opponent is in the corner, play the opposite corner.
7. Empty Corner: Play in a corner square.
8. Empty Side: Play in a middle square on any of the 4 sides.
Hi guyz,

I am new to this forum so Hello to everyone.

I am working on my tic tac toe and i did not manage to get an ideea how to do AI player. I want to use min max algorithm but i cant get it ... i was searching on the internet and i got some solution but i didn't get them..
how can i do my AI ? how should i use min max? i am using c++. Does anyone has a TTT that is not so complicated to understand how min max is implemented in c++ ?
thank you

It's probably best if you show us your own attempt. Explain what part you are having problems with and we'll help you.

If you don't know where to start, write a Board class that contains the game position and which can answer questions like "what moves are available" and "is the game over".
Until now i have made the game for 2 players.
Now i must move the second player to AI.
I only have the code for 2player game. I replaced the 2nd player with a random move but i have no ideea how to get it on to be truly inteligent. I have pseudocode for minimax but i dont know how to implement it and build the tree.
My board consists of a vector with 9 positions.I have a function win that checks is there are 3 positions for winning.

should i post the code for 2 players?
Thanks.
See if this helps you. I didn't comment it, but I wrote it in a straight-forward manner so it will be easy to follow. I wrote a function pick_move that analyzes the root and a separate function negamax that analyzes any other node. This might not be very important for this very simple case, but it's a good idea if you are going to use it for a more interesting game: pick_move is where you would do things like time control and iterative deepening.

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <limits>

int opponent(int player) {
return 3-player;
}

class Board {
int a[9];
int length;

public:
Board() : length(0) {
for (int i=0; i<9; ++i)
a = 0;
}

int to_move() const {
return 1 + (length % 2);
}

int game_length() const {
return length;
}

bool is_valid_move(int square) const {
return square >= 0 && square < 9 && a[square] == 0;
}

int generate_moves(int *moves) const {
int n_moves = 0;

for (int i=0; i<9; ++i) {
if (a == 0)
moves[n_moves++] = i;
}

return n_moves;
}

void play_move(int square) {
a[square] = to_move();
++length;
}

void undo_move(int square) {
a[square] = 0;
--length;
}

bool is_game_over(int *result) const {
if (a[4] == 1) {
if (a[0] == 1 && a[8] == 1) {*result = 1; return true;}
if (a[1] == 1 && a[7] == 1) {*result = 1; return true;}
if (a[2] == 1 && a[6] == 1) {*result = 1; return true;}
if (a[3] == 1 && a[5] == 1) {*result = 1; return true;}
}
else if (a[4] == 2) {
if (a[0] == 2 && a[8] == 2) {*result = 2; return true;}
if (a[1] == 2 && a[7] == 2) {*result = 2; return true;}
if (a[2] == 2 && a[6] == 2) {*result = 2; return true;}
if (a[3] == 2 && a[5] == 2) {*result = 2; return true;}
}

if (a[0] == 1) {
if (a[1] == 1 && a[2] == 1) {*result = 1; return true;}
if (a[3] == 1 && a[6] == 1) {*result = 1; return true;}
}
else if (a[0] == 2) {
if (a[1] == 2 && a[2] == 2) {*result = 2; return true;}
if (a[3] == 2 && a[6] == 2) {*result = 2; return true;}
}

if (a[8] == 1) {
if (a[7] == 1 && a[6] == 1) {*result = 1; return true;}
if (a[5] == 1 && a[2] == 1) {*result = 1; return true;}
}
else if (a[8] == 2) {
if (a[7] == 2 && a[6] == 2) {*result = 2; return true;}
if (a[5] == 2 && a[2] == 2) {*result = 2; return true;}
}

for (int i=0; i<9; ++i) {
if (a == 0)
return false;
}

*result = 0;
return true;
}

void print() const {
for (int i=0; i<3; ++i) {
for (int j=0; j<3; ++j)
std::cout << "_xo"[a[6-3*i+j]] << ' ';
std::cout << '\n';
}
}
};

int negamax(Board &b) {
int result;
if (b.is_game_over(&result))
return result == 0 ? -100+(std::rand()%201) : -1000 + b.game_length();

int moves[9];
int n_moves = b.generate_moves(moves);

int best_score = -1000;
for (int i=0; i<n_moves; ++i) {
b.play_move(moves);
int score = -negamax(b);
b.undo_move(moves);
if (score > best_score)
best_score = score;
}

return best_score;
}

int pick_move(Board &b) {
int moves[9];
int n_moves = b.generate_moves(moves);

int best_score = -1000;
int best_move = moves[0];
for (int i=0; i<n_moves; ++i) {
b.play_move(moves);
int score = -negamax(b);
b.undo_move(moves);
if (score > best_score) {
best_score = score;
best_move = moves;
}
}

return best_move;
}

int main() {
std::srand(std::time(0));

Board b;
int result;
while (b.print(),!b.is_game_over(&result)) {
int move;
if (b.to_move() == 2) {
while (!(std::cin >> move) || !b.is_valid_move(--move)) {
std::cout << "Invalid move\n";
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
}
else
move = pick_move(b);
b.play_move(move);
std::cout << '\n';

}
char const *result_names[3] = {"draw", "x wins", "o wins"};
std::cout << "Result: " << result_names[result] << '\n';
}



Let me know if there's anything in there you don't understand. Notice how you don't actually need to build the tree at all: You simply explore branches of a virtual tree using depth-first search, but you never have more than one branch of the tree in memory at any given time.

EDIT: Oh, at the very least I should tell you that the players are 1 and 2, 0 means empty and you enter board positions using the numeric keyboard, although internally the program uses 0, 1, 2, 3, 4, 5, 6, 7, 8 instead of 1, 2, 3, 4, 5, 6, 7, 8, 9.
I was lookign through my code, and I founf my problem. It wasn't returning the game over right, and it was throwing me into an infinite loop lol. Sorry for being an ahole :P
i dont see the error in your code here yet. What happens to the value of nValidMoves.Count in the endless loop? Does it decrease and increase? Or does it stay the same? Also, what does the SetSquare function do exactly. Do you have the code for that?

My personal blog on game development!

Black Wolf Game Development

Ok, I have been looking through this relentlessly, Still throwing me into an infinite loop. Could you guys look through my new code when you have a chance and see what my problem is. Thanks a bunch for all your help.

[source lang="csharp"] public Vector2 updateMINIMAX()
{
int bestscore = -1000;
List<Vector2> bestMoves = new List<Vector2>();

List<Vector2> nValidMoves;
nValidMoves = allNodes[0, 0].validMoves();

while (nValidMoves.Count != 0)
{
allNodes[0, 0].setNewMove(nValidMoves.First<Vector2>(), 2);

int nscore = Min(allNodes[0,0]);

if (nscore > bestscore)
{
bestscore = nscore;
bestMoves.Clear();
bestMoves.Add(nValidMoves.First<Vector2>());
}
else if (nscore == bestscore)
{
bestMoves.Add(nValidMoves.First<Vector2>());
}
allNodes[0, 0].setNewMove(nValidMoves.First<Vector2>(), 0);

nValidMoves.Remove(nValidMoves.First<Vector2>());

}

return bestMoves.First<Vector2>();

}




private int Min(Board tempBoard)
{
int moveScore = tempBoard.winCheck();
if (moveScore != 0)
return moveScore;

int nBestScore = 1000;
List<Vector2> nValidMoves;
nValidMoves = tempBoard.validMoves();

while (nValidMoves.Count != 0)
{
tempBoard.setNewMove(nValidMoves.First<Vector2>(), 1);

int nScore = Max(tempBoard);

if (nScore < nBestScore)
{
nBestScore = nScore;
}
allNodes[0, 0].setNewMove(nValidMoves.First<Vector2>(), 0);
nValidMoves.Remove(nValidMoves.First<Vector2>());
}

return nBestScore;
}



private int Max(Board tempBoard)
{
int moveScore = tempBoard.winCheck();
if (moveScore != 0)
return moveScore;

int nBestScore = -1000;
List<Vector2> nValidMoves;
nValidMoves = tempBoard.validMoves();

while (nValidMoves.Count != 0)
{
tempBoard.setNewMove(nValidMoves.First<Vector2>(), 2);

int nScore = Min(tempBoard);

if (nScore > nBestScore)
{
nBestScore = nScore;
}
allNodes[0, 0].setNewMove(nValidMoves.First<Vector2>(), 0);
nValidMoves.Remove(nValidMoves.First<Vector2>());
}

return nBestScore;
}
[/source]

This topic is closed to new replies.

Advertisement