Game board (Tictac Part 1)

We go over a process for making a flexible game board and logic for placing pieces in tic-tac-toe. This involves many new Elixir concepts: Comprehensions, MapSet, module structs, enforced keys, with and more.

Creating structs

Since tic-tac-toe boards are made of squares which have uniform properties, we'll model them with an Elixir struct. To do this, we'll create a Square module that defines a struct and a new function for creating it.

@enforce_keys specifies which properties a square must have. defstruct specifies the properties a square can have.

In each case, we'll specify only :row and :col properties, so our squares must have a :row property, a :col property and nothing else.

The Square.new/2 function has two definition heads. The first catches calls to new(row, col) where row and col each are a number in 1..3. It returns a tuple with :ok and the new square. The other function head will catch everything else.

Elixir Comprehensions (5:28)

In our main Tictac module, we need a way to get a set of all the squares on our board. We could use Enum to do this but it's quicker and simpler to use a comprehension. Elixir comprehensions are very similar to those seen in Python, CoffeeScript, Rust and other languages.

for c <- 1..3, r <- 1..3, do: %Square{col: c, row: r} will generate all the squares on our game board, much like a call Enum.map that iterates over rows nested inside another Enum.map that iterates over columns would.

MapSets (8:19)

The ideal data type to use for holding all the squares on the board is the MapSet. MapSets hold key, value pairs just as maps do, but the items are unique. This makes sense since it's impossible to have a 2-D game board with multiple squares at the same row and column.

In addition to holding a list of squares, the game board will also hold information about what is in the board. The game board will be a map, where squares are keys and the values are the contents of the squares (e.g. :empty, :x or :o). Note that map keys have to be unique, which is another selling point of keeping squares in a map set!

Implementing game play (12:15)

We need to make the following functions:

  • check_player: takes a player as input and returns {:ok, player} if the player is valid or {:error, :invalid_player if it isn't.
  • place_piece: takes a board, a location and a player as input and returns either, {:error, :invalid_location}, {:error, :occupied} or {:ok, updated_board}.
  • play_at: takes a board, column, row and player as input and chains calls to valid_player, Square.new, and place_piece. If any return an error, play_at aborts and returns the error. Otherwise, it returns the updated board state. The chaining is done with Elixir's with statement (see: 22:09).

Challenge

Part 1: Use comprehensions to generate a deck of cards from lists of suits and ranks

Part 2: Check out this guide on comprehensions... and experiment!

(solution here)

Back to index