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 methodsGameResult
: Enum of all the possible game states. A game can be eitherNOT_FINISHED
,DRAW
,CROSS_WIN
, orNAUGT_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 beEMPTY
.
We also define a utility method print_board
that prints a board state pretty in HTML:
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:
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:
board.move(board.random_empty_spot(), CROSS)
print_board(board)
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.
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!")
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:
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:
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")
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.
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))
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
Post a Comment