I’ve been trying to write a Tic Tac Toe game, and I couldn’t get my head around how to write a data model which would serve me best. Briefly, Tic Tac Toe consists of nine tiles grouped into three rows of three tiles. Two players interchangeably place crosses and circles on the board until either they’ve filled the board, or one of the players managed to put three symbols of the same type in a line. Henceforth it seems reasonable to deduce that
- the model should include nine tiles and the active player,
- and that we should somehow calculate whether the game is still in progress or has ended with a tie or one of the players has won.
What about the winner?
I have seen a few examples of Tic Tac Toe which featured game state as a part of the model. I feel like featuring the “state” of the game in the model contradicts with the idea of making impossible states impossible since we could have a board which isn’t full yet and the “state” indicating that the game has ended with a tie (i.e. the board is full).
Moreover, it doesn’t make much sense to have a “state” of the state.
On the other hand, having a state of the game as part of the model could significantly improve performance. Or at least, it feels like it should enhance it since we wouldn’t be calculating the winner over and over again.
Back to the model!
Regardless of the winner, the current state of the model seems very vague. More specifically, how would you structure the board (i.e. the nine tiles)? We know that we need to show three rows of three columns, we also know that there need be a way to identify the tile on click, and we also want to make sure we don’t have to turn the model upside down to calculate the winner.
Approach #1
The first approach is more of an illustrative idea since (I would guess) people coming from JS-ish communities could imagine the state of the game as [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
which leads to the following model.
type alias Board = List (List Tile)
type Tile = Tile ID (Maybe Symbol)
pickTile : ID -> Board -> Board
pickTile id board =
List.map (List.map selectTheTileIfIdsMatch) board
Pros: “intuitively” seems like the right approach,
Cons: logic in the model (i.e. the structure of the view), hard to write the update function since we have to map over a list of lists, we could have two tiles with the same id
or more than nine tiles and the compiler wouldn’t complain.
Approach #2
Considering that we don’t want to have a list of lists and also want to make impossible impossible, we could try with the 9-tuple.
type alias Board = (Tile, Tile, Tile, Tile, Tile, Tile, Tile, Tile, Tile)
type Tile = Tile ID (Maybe Symbol)
pickTile : ID -> Board -> Board
pickTile id board =
case id of
0 -> select
1 -> select
2 -> select
...
8 -> select
Which simply seems weird.
Approach #3
Have a list of tiles.
type alias Board = List Tile
type Tile = Tile ID (Maybe Symbol)
pickTile : ID -> Board -> Board
pickTile id board =
List.map selectTheTileIfIdsMatch board
Pros: it’s cleaner than the first one, no logic in the model, a simple update function
Cons: we could have two tiles with the same id
or more than nine tiles, and the compiler wouldn’t complain.
To determine the winner: have a list of winning combinations (List (List ID)
) and check whether either of the two players satisfies any of them.
The third approach seems very reasonable, yet, we need to find
the tiles when performing the winner calculation which seems very cumbersome, especially considering that we shaped the model to help us prevent situations like that and be deterministic of itself.
My primary goal of this post was to see how I could structure the game to write the model with the least edge cases (i.e. as few Maybe
s in the logic as possible) and make the compiler as useful as it can be. How would you structure the model of Tic Tac Toe to achieve that?