Don't lose on Tic-Tac-Toe 99% of the time with this simple strategy!
The game of Tic-Tac-Toe has been played billions of times and has been around for thousands of years. There is a well-defined strategy to win the game 100% of the time - it is there in the Wiki. But there is a simpler strategy that can guarantee you (or a computer) victory in 99% of the cases (well, victory + draws in 99% of the time). Without further due, here is the proposed algorithm:
1) If the computer can win in the next move, then: win!
2) If the human can win in the next move, then: computer blocks it!
3) If the center hasn't been played yet: computer plays the center!
4) If the diagonals are open: computer plays them randomly!
5) Computer keeps track of its last move: plays as far away as possible!
As you can see, 1-2-3 are straightforward moves and super easy to implement. 4 is not as simple as there is a variant (see code for more info). The case number 5 is actually simple to implement: keep track of the last move played, and then try to play a move from the far out positions inward, checking for a possible move. The idea around 4 is that most of the traps occur by leveraging positions in the diagonals, hence utilize that for yours (or the computer's) benefit. The idea behind 5 is controversial, but in essence we're trying to create a "pin" position for the opponent through deceptive moves.
The strategy not always works, but in a test where it was run 50,000 times against a semi-random opponent, it either beat or draw with the opponent 99.416% of the times (the percentage of victories was 85.972%).
Computer won 42986 times in 50000 games played!
Human won 292 times in 50000 games played!
Draws: 6722 times
And the stats above assume that the opponent starts the game. When the computer starts it, then it *is* truly unbeatable:
Computer won 48306 times in 50000 games played!
Human won 0 times in 50000 games played!
Draws: 1694 times
Code is below. In less that 500 lines of C# code, become virtually unbeatable! Cheers, Marcelo.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TicTacToePlay
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 2 && args.Length != 3)
{
Console.WriteLine("Usage: TicTacToePlay [computer | human] [number of rows for the board (odd number)] <N (number), which is the number of simulations to run>");
return;
}
int boardSize = 0;
if (!Int32.TryParse(args[1], out boardSize) || boardSize < 0 || boardSize % 2 == 0)
{
Console.WriteLine("Usage: TicTacToePlay [computer | human] [number of rows for the board (odd number)] <N (number), which is the number of simulations to run>");
return;
}
int NGAMES = 1;
bool simulationMode = false;
if (args.Length == 3)
{
NGAMES = Int32.Parse(args[2]);
simulationMode = true;
}
int computerWins = 0;
int humanWins = 0;
for (int i = 0; i < NGAMES; i++)
{
TicTacToe ticTacToe = new TicTacToe(boardSize);
char[] playOrder = null;
if (args[0].Equals("computer"))
{
playOrder = new char[] { 'o', 'x' };
}
else
{
playOrder = new char[] { 'x', 'o' };
}
int index = 0;
while (!ticTacToe.GameOver())
{
if (playOrder[index] == 'x')
{
int r = 0;
int c = 0;
if (!simulationMode)
{
Console.WriteLine();
ticTacToe.Print();
Console.WriteLine();
Console.WriteLine("Enter your move (r c):");
string[] parts = Console.ReadLine().Split(' ');
r = Int32.Parse(parts[0]);
c = Int32.Parse(parts[1]);
}
else
{
ticTacToe.RandomPlay(playOrder[index], out r, out c);
}
if (ticTacToe.Play(playOrder[index], r, c))
{
computerWins = (ticTacToe.computerWins) ? computerWins + 1 : computerWins;
humanWins = (ticTacToe.humanWins) ? humanWins + 1 : humanWins;
break;
}
}
else
{
if (ticTacToe.Play(playOrder[index], -1, -1))
{
computerWins = (ticTacToe.computerWins) ? computerWins + 1 : computerWins;
humanWins = (ticTacToe.humanWins) ? humanWins + 1 : humanWins;
break;
}
}
index = (index + 1) % 2;
}
Console.WriteLine();
ticTacToe.Print();
}
Console.WriteLine();
Console.WriteLine("Computer won {0} times in {1} games played!", computerWins, NGAMES);
Console.WriteLine("Human won {0} times in {1} games played!", humanWins, NGAMES);
Console.WriteLine("Draws: {0} times", NGAMES - computerWins - humanWins);
}
}
class TicTacToe
{
private char[,] board = null;
private int[] rowCount = null;
private int[] colCount = null;
private int[] diagCount = null;
private int[] diagCountAnyPlay = null;
private int nRows = 0;
private int nCols = 0;
private int myLatestMoveRow = 0;
private int myLatestMoveCol = 0;
public bool computerWins = false;
public bool humanWins = false;
private TicTacToe() { }
public TicTacToe(int boardSize)
{
this.nRows = boardSize;
this.nCols = boardSize;
board = new char[nRows, nCols];
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
board[r, c] = '.';
}
}
rowCount = new int[nRows];
colCount = new int[nCols];
diagCount = new int[2];
diagCountAnyPlay = new int[2];
}
public void RandomPlay(char player, out int r, out int c)
{
r = 0;
c = 0;
if (GameOver()) return;
Random rd = new Random();
bool played = false;
while (!played)
{
r = rd.Next(0, nRows);
c = rd.Next(0, nCols);
if (Play(player, r, c, true) != -1)
{
played = true;
}
}
}
public void Print()
{
for (int r = -1; r < nRows; r++)
{
if (r == -1)
{
Console.Write(" ");
for (int c = 0; c < nCols; c++)
{
Console.Write("{0} ", c);
}
}
else
{
Console.Write("{0} ", r);
for (int c = 0; c < nCols; c++)
{
Console.Write("{0} ", board[r, c].ToString().ToUpper());
}
}
Console.WriteLine();
}
}
public bool GameOver()
{
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (board[r, c] == '.') return false;
}
}
return true;
}
public bool Play(char player,
int row,
int col)
{
bool retVal = false;
if (player == 'x')
{
int move = Play(player, row, col, false);
if (move == -1)
{
//Console.WriteLine("*** Invalid move!");
}
else if (move == 0)
{
//Console.WriteLine("*** Good move! Keep on playing!");
}
else if (move == 1)
{
//Console.WriteLine("*** You win!");
humanWins = true;
retVal = true;
}
}
else
{
//Rule 1: if I can will, I'll damn right win!
bool played = false;
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (Play('o', r, c, true) == 2)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** I win!");
played = true;
retVal = true;
computerWins = true;
break;
}
}
if (played)
{
break;
}
}
//Rule 2: I won't let you win, bitch!
if (!played)
{
played = false;
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (Play('x', r, c, true) == 1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** You won't win, dude!");
played = true;
break;
}
}
if (played)
{
break;
}
}
}
//Rule 3: if the center has not been played, play it!
if (!played)
{
if (Play('o', nRows / 2, nCols / 2, true) != -1)
{
Play('o', nRows / 2, nCols / 2, false);
this.myLatestMoveRow = nRows / 2;
this.myLatestMoveCol = nCols / 2;
//Console.WriteLine("*** Play the center!");
played = true;
}
}
//Rule 4: look for the diagonals (in case a certain diagonal is fully occupied, don't do that, it is a trap)!!!
if (!played && diagCountAnyPlay[0] < nRows && diagCountAnyPlay[1] < nRows)
{
Random rd = new Random();
//Diag 1
for (int r = 0, c = nCols - 1; !played && r < nRows; r++, c--)
{
if (rd.Next(0, 2) == 0)
{
if (Play('o', r, r, true) != -1)
{
Play('o', r, r, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = r;
//Console.WriteLine("*** Play the first diagonal!");
played = true;
}
else if (Play('o', r, c, true) != -1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** Play the second diagonal!");
played = true;
}
}
else
{
if (Play('o', r, c, true) != -1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** Play the second diagonal!");
played = true;
}
else if (Play('o', r, r, true) != -1)
{
Play('o', r, r, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = r;
//Console.WriteLine("*** Play the first diagonal!");
played = true;
}
}
}
}
//Rule 5: play away from my last position
if (!played)
{
for (int i = nRows; i >= 0; i--)
{
for (int j = nCols; j >= 0; j--)
{
foreach (int sign in (new int[] {-1, 1}))
{
int r = this.myLatestMoveRow + sign * i;
int c = this.myLatestMoveCol + sign * j;
if (Play('x', r, c, true) != -1)
{
Play('o', r, c, false);
played = true;
//Console.WriteLine("*** Trying a trap!");
break;
}
}
}
if (played)
{
break;
}
}
}
}
return retVal;
}
/*
* 0: move was valid, and no winner
* 1: move was valid, and 'x' wins
* 2: move was valid, and 'o' wins
* -1: invalid move
* */
private int Play(char player,
int row,
int col,
bool simulationOnly = false)
{
if (row < 0 || row >= nRows || col < 0 || col >= nCols) return -1;
if (board[row, col] != '.') return -1;
if (!simulationOnly)
{
board[row, col] = player;
}
if (player == 'x')
{
//Row
rowCount[row]++;
int temp = rowCount[row];
if (simulationOnly)
{
rowCount[row]--;
}
if (temp == nRows) return 1;
//Col
colCount[col]++;
temp = colCount[col];
if (simulationOnly)
{
colCount[col]--;
}
if (temp == nCols) return 1;
//Diag 1
if (row == col)
{
diagCountAnyPlay[0]++;
diagCount[0]++;
temp = diagCount[0];
if (simulationOnly)
{
diagCount[0]--;
diagCountAnyPlay[0]--;
}
if (temp == nCols) return 1;
}
//Diag 2
if (row + col == nRows - 1)
{
diagCountAnyPlay[1]++;
diagCount[1]++;
temp = diagCount[1];
if (simulationOnly)
{
diagCount[1]--;
diagCountAnyPlay[1]--;
}
if (temp == nCols) return 1;
}
}
else
{
//Row
rowCount[row]--;
int temp = rowCount[row];
if (simulationOnly)
{
rowCount[row]++;
}
if (temp == -nRows) return 2;
//Col
colCount[col]--;
temp = colCount[col];
if (simulationOnly)
{
colCount[col]++;
}
if (temp == -nCols) return 2;
//Diag 1
if (row == col)
{
diagCountAnyPlay[0]++;
diagCount[0]--;
temp = diagCount[0];
if (simulationOnly)
{
diagCount[0]++;
diagCountAnyPlay[0]--;
}
if (temp == -nCols) return 2;
}
//Diag 2
if (row + col == nRows - 1)
{
diagCountAnyPlay[1]++;
diagCount[1]--;
temp = diagCount[1];
if (simulationOnly)
{
diagCount[1]++;
diagCountAnyPlay[1]--;
}
if (temp == -nCols) return 2;
}
}
return 0;
}
}
}
1) If the computer can win in the next move, then: win!
2) If the human can win in the next move, then: computer blocks it!
3) If the center hasn't been played yet: computer plays the center!
4) If the diagonals are open: computer plays them randomly!
5) Computer keeps track of its last move: plays as far away as possible!
As you can see, 1-2-3 are straightforward moves and super easy to implement. 4 is not as simple as there is a variant (see code for more info). The case number 5 is actually simple to implement: keep track of the last move played, and then try to play a move from the far out positions inward, checking for a possible move. The idea around 4 is that most of the traps occur by leveraging positions in the diagonals, hence utilize that for yours (or the computer's) benefit. The idea behind 5 is controversial, but in essence we're trying to create a "pin" position for the opponent through deceptive moves.
The strategy not always works, but in a test where it was run 50,000 times against a semi-random opponent, it either beat or draw with the opponent 99.416% of the times (the percentage of victories was 85.972%).
Computer won 42986 times in 50000 games played!
Human won 292 times in 50000 games played!
Draws: 6722 times
And the stats above assume that the opponent starts the game. When the computer starts it, then it *is* truly unbeatable:
Computer won 48306 times in 50000 games played!
Human won 0 times in 50000 games played!
Draws: 1694 times
Code is below. In less that 500 lines of C# code, become virtually unbeatable! Cheers, Marcelo.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TicTacToePlay
{
class Program
{
static void Main(string[] args)
{
if (args.Length != 2 && args.Length != 3)
{
Console.WriteLine("Usage: TicTacToePlay [computer | human] [number of rows for the board (odd number)] <N (number), which is the number of simulations to run>");
return;
}
int boardSize = 0;
if (!Int32.TryParse(args[1], out boardSize) || boardSize < 0 || boardSize % 2 == 0)
{
Console.WriteLine("Usage: TicTacToePlay [computer | human] [number of rows for the board (odd number)] <N (number), which is the number of simulations to run>");
return;
}
int NGAMES = 1;
bool simulationMode = false;
if (args.Length == 3)
{
NGAMES = Int32.Parse(args[2]);
simulationMode = true;
}
int computerWins = 0;
int humanWins = 0;
for (int i = 0; i < NGAMES; i++)
{
TicTacToe ticTacToe = new TicTacToe(boardSize);
char[] playOrder = null;
if (args[0].Equals("computer"))
{
playOrder = new char[] { 'o', 'x' };
}
else
{
playOrder = new char[] { 'x', 'o' };
}
int index = 0;
while (!ticTacToe.GameOver())
{
if (playOrder[index] == 'x')
{
int r = 0;
int c = 0;
if (!simulationMode)
{
Console.WriteLine();
ticTacToe.Print();
Console.WriteLine();
Console.WriteLine("Enter your move (r c):");
string[] parts = Console.ReadLine().Split(' ');
r = Int32.Parse(parts[0]);
c = Int32.Parse(parts[1]);
}
else
{
ticTacToe.RandomPlay(playOrder[index], out r, out c);
}
if (ticTacToe.Play(playOrder[index], r, c))
{
computerWins = (ticTacToe.computerWins) ? computerWins + 1 : computerWins;
humanWins = (ticTacToe.humanWins) ? humanWins + 1 : humanWins;
break;
}
}
else
{
if (ticTacToe.Play(playOrder[index], -1, -1))
{
computerWins = (ticTacToe.computerWins) ? computerWins + 1 : computerWins;
humanWins = (ticTacToe.humanWins) ? humanWins + 1 : humanWins;
break;
}
}
index = (index + 1) % 2;
}
Console.WriteLine();
ticTacToe.Print();
}
Console.WriteLine();
Console.WriteLine("Computer won {0} times in {1} games played!", computerWins, NGAMES);
Console.WriteLine("Human won {0} times in {1} games played!", humanWins, NGAMES);
Console.WriteLine("Draws: {0} times", NGAMES - computerWins - humanWins);
}
}
class TicTacToe
{
private char[,] board = null;
private int[] rowCount = null;
private int[] colCount = null;
private int[] diagCount = null;
private int[] diagCountAnyPlay = null;
private int nRows = 0;
private int nCols = 0;
private int myLatestMoveRow = 0;
private int myLatestMoveCol = 0;
public bool computerWins = false;
public bool humanWins = false;
private TicTacToe() { }
public TicTacToe(int boardSize)
{
this.nRows = boardSize;
this.nCols = boardSize;
board = new char[nRows, nCols];
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
board[r, c] = '.';
}
}
rowCount = new int[nRows];
colCount = new int[nCols];
diagCount = new int[2];
diagCountAnyPlay = new int[2];
}
public void RandomPlay(char player, out int r, out int c)
{
r = 0;
c = 0;
if (GameOver()) return;
Random rd = new Random();
bool played = false;
while (!played)
{
r = rd.Next(0, nRows);
c = rd.Next(0, nCols);
if (Play(player, r, c, true) != -1)
{
played = true;
}
}
}
public void Print()
{
for (int r = -1; r < nRows; r++)
{
if (r == -1)
{
Console.Write(" ");
for (int c = 0; c < nCols; c++)
{
Console.Write("{0} ", c);
}
}
else
{
Console.Write("{0} ", r);
for (int c = 0; c < nCols; c++)
{
Console.Write("{0} ", board[r, c].ToString().ToUpper());
}
}
Console.WriteLine();
}
}
public bool GameOver()
{
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (board[r, c] == '.') return false;
}
}
return true;
}
public bool Play(char player,
int row,
int col)
{
bool retVal = false;
if (player == 'x')
{
int move = Play(player, row, col, false);
if (move == -1)
{
//Console.WriteLine("*** Invalid move!");
}
else if (move == 0)
{
//Console.WriteLine("*** Good move! Keep on playing!");
}
else if (move == 1)
{
//Console.WriteLine("*** You win!");
humanWins = true;
retVal = true;
}
}
else
{
//Rule 1: if I can will, I'll damn right win!
bool played = false;
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (Play('o', r, c, true) == 2)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** I win!");
played = true;
retVal = true;
computerWins = true;
break;
}
}
if (played)
{
break;
}
}
//Rule 2: I won't let you win, bitch!
if (!played)
{
played = false;
for (int r = 0; r < nRows; r++)
{
for (int c = 0; c < nCols; c++)
{
if (Play('x', r, c, true) == 1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** You won't win, dude!");
played = true;
break;
}
}
if (played)
{
break;
}
}
}
//Rule 3: if the center has not been played, play it!
if (!played)
{
if (Play('o', nRows / 2, nCols / 2, true) != -1)
{
Play('o', nRows / 2, nCols / 2, false);
this.myLatestMoveRow = nRows / 2;
this.myLatestMoveCol = nCols / 2;
//Console.WriteLine("*** Play the center!");
played = true;
}
}
//Rule 4: look for the diagonals (in case a certain diagonal is fully occupied, don't do that, it is a trap)!!!
if (!played && diagCountAnyPlay[0] < nRows && diagCountAnyPlay[1] < nRows)
{
Random rd = new Random();
//Diag 1
for (int r = 0, c = nCols - 1; !played && r < nRows; r++, c--)
{
if (rd.Next(0, 2) == 0)
{
if (Play('o', r, r, true) != -1)
{
Play('o', r, r, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = r;
//Console.WriteLine("*** Play the first diagonal!");
played = true;
}
else if (Play('o', r, c, true) != -1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** Play the second diagonal!");
played = true;
}
}
else
{
if (Play('o', r, c, true) != -1)
{
Play('o', r, c, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = c;
//Console.WriteLine("*** Play the second diagonal!");
played = true;
}
else if (Play('o', r, r, true) != -1)
{
Play('o', r, r, false);
this.myLatestMoveRow = r;
this.myLatestMoveCol = r;
//Console.WriteLine("*** Play the first diagonal!");
played = true;
}
}
}
}
//Rule 5: play away from my last position
if (!played)
{
for (int i = nRows; i >= 0; i--)
{
for (int j = nCols; j >= 0; j--)
{
foreach (int sign in (new int[] {-1, 1}))
{
int r = this.myLatestMoveRow + sign * i;
int c = this.myLatestMoveCol + sign * j;
if (Play('x', r, c, true) != -1)
{
Play('o', r, c, false);
played = true;
//Console.WriteLine("*** Trying a trap!");
break;
}
}
}
if (played)
{
break;
}
}
}
}
return retVal;
}
/*
* 0: move was valid, and no winner
* 1: move was valid, and 'x' wins
* 2: move was valid, and 'o' wins
* -1: invalid move
* */
private int Play(char player,
int row,
int col,
bool simulationOnly = false)
{
if (row < 0 || row >= nRows || col < 0 || col >= nCols) return -1;
if (board[row, col] != '.') return -1;
if (!simulationOnly)
{
board[row, col] = player;
}
if (player == 'x')
{
//Row
rowCount[row]++;
int temp = rowCount[row];
if (simulationOnly)
{
rowCount[row]--;
}
if (temp == nRows) return 1;
//Col
colCount[col]++;
temp = colCount[col];
if (simulationOnly)
{
colCount[col]--;
}
if (temp == nCols) return 1;
//Diag 1
if (row == col)
{
diagCountAnyPlay[0]++;
diagCount[0]++;
temp = diagCount[0];
if (simulationOnly)
{
diagCount[0]--;
diagCountAnyPlay[0]--;
}
if (temp == nCols) return 1;
}
//Diag 2
if (row + col == nRows - 1)
{
diagCountAnyPlay[1]++;
diagCount[1]++;
temp = diagCount[1];
if (simulationOnly)
{
diagCount[1]--;
diagCountAnyPlay[1]--;
}
if (temp == nCols) return 1;
}
}
else
{
//Row
rowCount[row]--;
int temp = rowCount[row];
if (simulationOnly)
{
rowCount[row]++;
}
if (temp == -nRows) return 2;
//Col
colCount[col]--;
temp = colCount[col];
if (simulationOnly)
{
colCount[col]++;
}
if (temp == -nCols) return 2;
//Diag 1
if (row == col)
{
diagCountAnyPlay[0]++;
diagCount[0]--;
temp = diagCount[0];
if (simulationOnly)
{
diagCount[0]++;
diagCountAnyPlay[0]--;
}
if (temp == -nCols) return 2;
}
//Diag 2
if (row + col == nRows - 1)
{
diagCountAnyPlay[1]++;
diagCount[1]--;
temp = diagCount[1];
if (simulationOnly)
{
diagCount[1]++;
diagCountAnyPlay[1]--;
}
if (temp == -nCols) return 2;
}
}
return 0;
}
}
}
Comments
Post a Comment