Game Tree Search
We start with defining the abstract class Game
, for turn-taking n-player games. We rely on, but do not define yet, the concept of a state
of the game; we'll see later how individual games define states. For now, all we require is that a state has a state.to_move
attribute, which gives the name of the player whose turn it is. ("Name" will be something like 'X'
or 'O'
for tic-tac-toe.)
We also define play_game
, which takes a game and a dictionary of {player_name: strategy_function}
pairs, and plays out the game, on each turn checking state.to_move
to see whose turn it is, and then getting the strategy function for that player and applying it to the game and the state to get a move.
Minimax-Based Game Search Algorithms
We will define several game search algorithms. Each takes two inputs, the game we are playing and the current state of the game, and returns a a (value, move)
pair, where value
is the utility that the algorithm computes for the player whose turn it is to move, and move
is the move itself.
First we define minimax_search
, which exhaustively searches the game tree to find an optimal move (assuming both players play optimally), and alphabeta_search
, which does the same computation, but prunes parts of the tree that could not possibly have an affect on the optimnal move.
A Simple Game: Tic-Tac-Toe
We have the notion of an abstract game, we have some search functions; now it is time to define a real game; a simple one, tic-tac-toe. Moves are (x, y)
pairs denoting squares, where (0, 0)
is the top left, and (2, 2)
is the bottom right (on a board of size height=width=3
).
States in tic-tac-toe (and other games) will be represented as a Board
, which is a subclass of defaultdict
that in general will consist of {(x, y): contents}
pairs, for example {(0, 0): 'X', (1, 1): 'O'}
might be the state of the board after two moves. Besides the contents of squares, a board also has some attributes:
.to_move
to name the player whose move it is;.width
and.height
to give the size of the board (both 3 in tic-tac-toe, but other numbers in related games);possibly other attributes, as specified by keywords.
As a defaultdict
, the Board
class has a __missing__
method, which returns empty
for squares that have no been assigned but are within the width
× height
boundaries, or off
otherwise. The class has a __hash__
method, so instances can be stored in hash tables.
Players
We need an interface for players. I'll represent a player as a callable
that will be passed two arguments: (game, state)
and will return a move
. The function player
creates a player out of a search algorithm, but you can create your own players as functions, as is done with random_player
below:
Playing a Game
We're ready to play a game. I'll set up a match between a random_player
(who chooses randomly from the legal moves) and a player(alphabeta_search)
(who makes the optimal alpha-beta move; practical for tic-tac-toe, but not for large games). The player(alphabeta_search)
will never lose, but if random_player
is lucky, it will be a tie.
The alpha-beta player will never lose, but sometimes the random player can stumble into a draw. When two optimal (alpha-beta or minimax) players compete, it will always be a draw:
Connect Four
Connect Four is a variant of tic-tac-toe, played on a larger (7 x 6) board, and with the restriction that in any column you can only play in the lowest empty square in the column.
Transposition Tables
By treating the game tree as a tree, we can arrive at the same state through different paths, and end up duplicating effort. In state-space search, we kept a table of reached
states to prevent this. For game-tree search, we can achieve the same effect by applying the @cache
decorator to the min_value
and max_value
functions. We'll use the suffix _tt
to indicate a function that uses these transisiton tables.
For alpha-beta search, we can still use a cache, but it should be based just on the state, not on whatever values alpha and beta have.