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
, andplace_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'swith
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)
3 Comments
I'm having trouble understanding, how is the module attribute used in this project? It seems like the check_player function is using the case statement to check whether a player is valid.
The module attribute
@players
is just a variable that exists only inside the module. It equals the tuple{:x, :o}
, which are the two valid players.What
check_player
does is ensure that the input is valid, so you have that part right!. Since it returns tuples with either:ok
or:error
, it's convenient to put into a pipeline of functions. If the input is valid, it gets passed on. If not, it returns an error.The fastest way to get a feel for what's happening is to start up
iex
and experiment!It seems to me that using a list over a tuple for
@players
could simplify thecheck_player
function usingEnum.member?
predicate function.