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_playerif 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_ataborts and returns the error. Otherwise, it returns the updated board state. The chaining is done with Elixir'swithstatement (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
@playersis just a variable that exists only inside the module. It equals the tuple{:x, :o}, which are the two valid players.What
check_playerdoes is ensure that the input is valid, so you have that part right!. Since it returns tuples with either:okor: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
iexand experiment!It seems to me that using a list over a tuple for
@playerscould simplify thecheck_playerfunction usingEnum.member?predicate function.