Skip to main content

Part 1 - Computer Tic Tac Toe Basics

Part 1 - Computer Tic Tac Toe Basics

Basic Tic Tac Toe support classes and game logic

In the first part of this series, we will introduce the basic framework and helper classes that we will use throughout. We will also create the first computer player.


Let's get started:

We will use the following classes which are defined in Board.py:

  • Board: Contains all the Tic Tac Toe board state management plus some utility methods
  • GameResult: Enum of all the possible game states. A game can be either NOT_FINISHED, DRAW, CROSS_WIN, or NAUGT_WIN
  • CROSS, NAUGHT: Will tell our players what side they play, as well as inidcate what pieces are on each square of the board - which can also be EMPTY.

We also define a utility method print_board that prints a board state pretty in HTML:

In [1]:
from IPython.display import HTML, display
from tic_tac_toe.Board import Board, GameResult, CROSS, NAUGHT


def print_board(board):
    display(HTML("""
    <style>
    .rendered_html table, .rendered_html th, .rendered_html tr, .rendered_html td {
      border: 1px  black solid !important;
      color: black !important;
    }
    </style>
    """+board.html_str()))

We can now create a new board and print it in all its empty glory:

In [2]:
board = Board()
print_board(board)

Time to make a move. We use the methods random_empty_spot and move to find a random empty spot on the board and put a CROSS there. We then print the board to confirm:

In [3]:
board.move(board.random_empty_spot(), CROSS)
print_board(board)
x

Let's extend that to play a whole game.

We reset the board state and play alternating CROSS and NAUGHT until the game is either won by one side or a draw. We print the board after each move. After the game has finished print out who won.

In [4]:
board.reset()
finished = False
while not finished:
   _, result, finished = board.move(board.random_empty_spot(), CROSS)
   print_board(board)
   if finished:
       if result == GameResult.DRAW:
           print("Game is a draw")
       else:
           print("Cross won!")
   else:
       _, result, finished = board.move(board.random_empty_spot(), NAUGHT)
       print_board(board)
       if finished:
            if result == GameResult.DRAW:
               print("Game is a draw")
            else:
               print("Naught won!")
x
ox
ox
x
ox
xo
ox
xox
oox
xox
oox
xox
x
oox
xox
xo
Naught won!

let's wrap this code in a utility function called play_game. It takes a board and 2 players and plays a complete game. It returns the result of the game at the end. We will use this going forward to evaluate our computer players.

Let's test the new function by playing a game:

In [5]:
from tic_tac_toe.Player import Player


def play_game(board: Board, player1: Player, player2: Player):
    player1.new_game(CROSS)
    player2.new_game(NAUGHT)
    board.reset()
    
    finished = False
    while not finished:
        result, finished = player1.move(board)
        if finished:
            if result == GameResult.DRAW:
                final_result = GameResult.DRAW
            else:
                final_result =  GameResult.CROSS_WIN
        else:
            result, finished = player2.move(board)
            if finished:
                if result == GameResult.DRAW:
                    final_result =  GameResult.DRAW
                else:
                    final_result =  GameResult.NAUGHT_WIN
        
    player1.final_result(final_result)
    player2.final_result(final_result)
    return final_result

The RandomPlayer

Time to introduce our first contender, the RandomPlayer. It looks for a random empty spot on the board and puts its piece there - pretty much the same way as we just did above; just wrapped into a class.

Let's import it, create 2 instances and let them play a game:

In [6]:
from tic_tac_toe.RandomPlayer import RandomPlayer

player1 = RandomPlayer()
player2 = RandomPlayer()
result = play_game(board, player1, player2)
print_board(board)
if result == GameResult.CROSS_WIN:
    print("Cross won")
elif result == GameResult.NAUGHT_WIN:
    print("Naught won")
else:
    print("Draw")
ox
ox
x
Cross won

Establishing some ground truth.

Using the code we introduced above we can now establish some ground truth: If we let 2 random players play against each other a large enough number of times, how many games do we expect to be won by NAUGHT, how many by CROSS, and how many do we expect to end in a draw?

Going forward, building more intelligent players, we can then measure how much better they play compared to a random player.

In [7]:
num_games = 100000

draw_count = 0
cross_count = 0
naught_count = 0

p1 = RandomPlayer()
p2 = RandomPlayer()

for _ in range(num_games):
    result = play_game(board, p1, p2)
    if result == GameResult.CROSS_WIN:
        cross_count += 1
    elif result == GameResult.NAUGHT_WIN:
        naught_count += 1
    else:
        draw_count += 1
        
print("After {} game we have draws: {}, cross wins: {}, and naught wins: {}.".format(num_games, draw_count, 
                                                                        cross_count, naught_count))

print("Which gives percentages of draws : cross : naught of about {:.2%} : {:.2%} : {:.2%}".format(
    draw_count / num_games, cross_count / num_games, naught_count / num_games))
After 100000 game we have draws: 12827, cross wins: 58313, and naught wins: 28860.
Which gives percentages of draws : cross : naught of about 12.83% : 58.31% : 28.86%

Established Baseline

In addition to the statistics computed above, we also know that if both players always make the best possible move, a game of Tic Tac Toe will always end in a draw. The gives us the following baseline:

Player P1 Win P2 Win Draw
Perfect 0% 0% 100%
Random 58% 13% 29%

In the following parts we will aim to create players that play perfectly. Where we don't quite achieve that, we will still be able to at least measure how better than the RandomPlayer our player is.

Comments