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;
        }
    }
}

Comments

Popular posts from this blog

Advent of Code - Day 6, 2024: BFS and FSM

Golang vs. C#: performance characteristics (simple case study)

Advent of Code - Day 7, 2024: Backtracking and Eval