Take a Snapshot of the Player's Move
00:00 Take a Snapshot of the Player’s Move. An object representing the player’s move in tic-tac-toe should primarily answer two questions. Firstly, the player’s mark: what mark did the player place?
00:14 Secondly, the mark’s location: where was it placed? However, in order to have the complete picture, one must also know about the state of the game before making a move.
00:25 After all, it could be a good or bad move depending on the current situation. You may also find it convenient to have the resulting state of the game at hand so you can assign it a score.
00:36 By simulating that move, you’ll be able to compare it with other possible moves. A move object can’t validate itself without knowing some of the game details, such as the starting player’s mark, which aren’t available to it.
00:49 You’ll check whether a given move is valid, along with validating a specific grid cell combination, in a class responsible for managing the game state.
00:59 Based on these thoughts, you can add another immutable data class to your models.
01:14
Ignore the two forward declarations of the GameState
class for the moment, as you’ll define it next. Your new class is strictly a data transfer object whose main purpose is to carry data, as it doesn’t provide any behavior through methods or dynamically computed properties.
01:34
Objects of the Move
class consist of the mark identifying the player who made a move, a numeric zero-based indexing of the string of cells, and the two states before and after making a move.
01:46
The Move
class will be instantiated, populated with values, and manipulated by the missing GameState
class. Without it, you won’t be able to correctly create the Move
objects yourself, so it’s time to fix that.
02:00 Now, a tic-tac-toe game can be in one of several states, including three possible outcomes: the game hasn’t started yet, the game is still going on, the game is finished in a tie, the game is finished with player X winning, and the game is finished with player O winning. You can determine the current state of a game of tic-tac-toe based on two parameters: the combination of the cells in the grid and the mark of the starting player.
02:31
Without knowing who started the game, you won’t be able to tell whose turn it is now and whether the given move is valid. Ultimately, you can’t properly assess the situation so that the artificial intelligence can make the right decision. To fix that, begin by specifying the GameState
as another immutable data class consisting of the grid of cells and the starting player’s mark.
03:02
By convention, the player who marks the cells with crosses starts the game, hence the default value of Mark("X")
for the starting player’s mark. However, you can change it according to your preference by supplying a different value at runtime.
03:16 Now, add a cached property returning the mark of the player who should make the next move.
03:31 The current player’s mark will be the same as the starting player’s mark when the grid is empty or when both players have marked an equal number of cells.
03:39
In practice, you only need to check the latter condition because a blank grid implies that both players have zero marks on the grid. To determine the other player’s mark, you can take advantage of the .other
property in the Mark
enum.
03:54 Next up, you’ll add some properties for evaluating the current state of the game. For example, you can tell that the game hasn’t started yet when a grid is blank or contains exactly nine empty cells.
04:13 This is where the grid’s properties come in handy. Conversely, you can conclude that the game has finished when there’s a clear winner or there’s a tie.
04:34
The .winner
property that you’ll implement in a bit will return a Mark
instance or None
, whereas the .tie
property will be a Boolean value.
04:43 A tie is where neither player has won, which means there’s no winner, and all of the squares are filled, leaving no empty cells.
05:01
Both the .game_over
and .tie
properties rely on the .winner
property, which they delegate to. Finding a winner is slightly more difficult, though. You can, for example, try to match the current grid of cells against a predefined collection of winning patterns with regular expressions.
05:22 There are eight winning patterns: three horizontal lines, three vertical lines,
05:34
and two diagonal lines. You define them using templates resembling regular expressions. The templates contain question mark (?
) placeholders for the concrete player’s mark.
06:02 You iterate over those templates and replace the question marks with both players’ marks to synthesize two regular expressions per pattern, one containing each player’s mark.
06:16
When the cells match a winning pattern, then you return the corresponding mark, the mark of the winner. Otherwise, you return None
. Knowing the winner is one thing, but you may also want to know the matched winning cells to differentiate them visually.
06:32 In this case, you can add a similar property. This will use a list comprehension to return a list of integer indices of the winning cells.
07:01
Now, you may be concerned about having a bit of code duplication here between .winner
and .winning_cells
, which violates the Don’t Repeat Yourself (DRY) principle, here.
07:11 That’s okay. The Zen of Python says that practicality beats purity, and in this case, extracting the common denominator would provide little value but make the code much less readable.
07:24 It usually makes sense to start thinking about refactoring your code when there are at least three instances of a duplicated code fragment because then there’s a high chance that you’ll need to reuse the same piece of code even more.
07:37
GameState
is starting to look pretty good. It can correctly recognize all possible game states, but it does lack proper validation, making it prone to runtime errors.
07:48 In the next video, you’ll rectify that by codifying and enforcing a few tic-tac-toe rules.
Become a Member to join the conversation.