logo
  • Start here
  • Episodes
  • Articles
  • Log in
  • Sign up

Game board (Tictac Part 1)

Topics:
With
Comprehensions
Structs
Games

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)

Share

Request a free email course
Back to index

2 Comments

Log in to leave a comment

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.

- johnnyliaw121 · 3 months ago

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!

- alchemist · 3 months ago
Alchemist Camp 2017-2020
Hosted on Digital Ocean
Terms and Conditions