The nostalgic Snake Ladder game - Part 1

The nostalgic Snake Ladder game - Part 1

2019, Oct 02    

Its been a while since I invested some time for side projects. After having worked with Elixir for about 6 months, I wanted to go back to basics of Elixir one more time, but this time by trying to follow better conventions and learn them in the process of doing it.

Things I wanted to focus on (which I did):

  1. Power of Pattern Matching
  2. Doctests [still mind blown]
  3. Typespecs in Elixir
  4. Documentation practice

With Phoenix LiveView in mind, I felt picking up a live update game as a project theme. Snake-Ladder is one of my favorite and nostalgic game, and hence chose it. However, the UI and usage of LiveView for the game would be in another blog. I am not sure when that will be up.

The way of my implementation of the game in Elixir is something like this:

  1. Board - struct module
  2. Player - struct module
  3. Game - logic implementation

I’ll try to walk you folks through the code I wrote, covering the aspects of things I focussed on.

1. Pattern matching

Pattern matching is one of those powerful core feature and heart of Elixir programming. Its just about used everywhere in almost all Elixir projects. This repository is no exception.

The Player struct looks something like this:

defmodule SnakeLadder.Player do
  defstruct name: "", won: false, rank: 0, current_position: 0, timestamp: nil

  def make_player(name) do
    %Player{name: name}
  end

  def set_position(%Player{} = player, 100) do
    player
    |> Map.put(:current_position, 100)
    |> mark_won()
  end

  def set_position(%Player{} = player, new_position),
    do: Map.put(player, :current_position, new_position)

  def set_rank(%Player{won: true, current_position: 100} = player, rank)
      when is_integer(rank) do
    Map.put(player, :rank, rank)
  end

  def set_rank(%Player{} = player, rank) when is_integer(rank) do
    player
  end
  .
  .
  .
  def mark_won(%Player{current_position: 100} = player) do
    player
    |> Map.put(:won, true)
    |> Map.put(:timestamp, get_current_timestamp())
  end

  def mark_won(%Player{current_position: _other} = player) do
    Map.put(player, :won, false)
  end

  defp get_current_timestamp() do
    DateTime.utc_now()
    |> DateTime.to_unix()
  end
end

Alright, you get the idea. Let’s see what is happening here when we call those functions of the Player module. Consider we are starting off the game. We ask the user, his name and create the player using make_player/1 function. The function returns the struct Player.

Suppose the player is now jumping position after rolling the dice. We use the set_position/2 to change and record the current position of the player. We know that a roll of dice adds positions up or in case if its a snake in the position, we reset the position to some place else.

What if the player was on 99 and rolled 1? The player would win the game! How can we do this with Pattern Matching technique?

If you look the code snippet above, you can see that there are two identically named function, but the arguments might seem fishy to peoples who are new to Elixir.

The two functions:

  • def set_position(%Player{} = player, 100) do
  • def set_position(%Player{} = player, new_position), do

seems to do the same thing right?

Well, yes and no. We ask Elixir to match for a position=100 and do some additional magic, like setting the user as won by default!

iex> john = Player.make_player("john")
iex> john = Player.set_position(john, 99)
iex> john.name
"john"
iex> john.current_position
99
iex> john.won
false
iex> john.timestamp
nil

Now in the above snippet, when set_position was called with a 99, the second match is used by Elixir since the postion is not 100.

iex> john = Player.make_player("john")
iex> john = Player.set_position(john, 100)
iex> john.name
"john"
iex> john.current_position
100
iex> john.won
true
iex> john.timestamp
12345678

Unlike the case of 99, when we gave 100 for setting the position, the player is marked as won and gets a timestamp. We can use timestamp to rank the players later. There is one more interesting thing happening here. Look at the mark_won/1 function. Imagine this function was called when the postion was 99. Can you guess what might happen?

We know that the player can’t be marked as won. So we have to skip the funtion gracefully (You could be straightforward and throw an error here, cursing user to use the code wisely! :P ) We use the pattern matching to see if the player’s current_postion is 100 and only then we actually mark it as won!

def mark_won(%Player{current_position: 100} = player) do
  player
  |> Map.put(:won, true)
  |> Map.put(:timestamp, get_current_timestamp())
end

In all other cases of current_postion, we simply return the actual player struct after reseting the field.

This is neat compared to too many if-else blocks, and thus it results in lesser lines of code to get things done!

2. Doctests

Many programmers including myself would be swept off of our feet when we first see something like this. How often do you write codebase documentation that can itself serve as Unit Tests!!!

Yes, in Elixir we can actually write down examples in the function documentation that can serve as a unit test for the function. Let’s see how this is done.

a) Write the documentation with examples

#  ./lib/snake_ladder/player.ex
defmodule SnakeLadder.Player do
  alias SnakeLadder.Player
  @doc """
  Set the current position of the player.

  ## Example
      iex> player = SnakeLadder.Player.make_player("john")
      ...> |> SnakeLadder.Player.set_position(4)
      iex> player.current_position
      4

  If the new position for the player is 100, then the player is considered to have won the game.
  The same is marked on to the struct before returning it. :)

  ## Example
      iex> player = SnakeLadder.Player.make_player("john")
      ...> |> SnakeLadder.Player.set_position(100)
      iex> player.won
      true
      iex> player.current_position
      100
  """
  def set_position(%Player{} = player, 100) do
    player
    |> Map.put(:current_position, 100)
    |> mark_won()
  end

  def set_position(%Player{} = player, new_position),
    do: Map.put(player, :current_position, new_position)
.
.
.
end

b) Add doctest to Player unit test script and that’s it!

#  ./test/player_test.exs
defmodule SnakeLadder.PlayerTest do
  use ExUnit.Case
  doctest SnakeLadder.Player
end

c) Run tests!

$ mix do clean, compile --warnings-as-errors
Compiling 4 files (.ex)
Generated snake_ladder app
$ mix test
.......

3. Typespecs in Elixir

Although Elixir is a dynamically typed language, it is useful to make note of what type of arguments a function takes, what type of value it returns. Elixir uses Typespecs which are nothing but notations that help developers declare function signatures with types and even custom data types like in our case, we can define Player as a type.

This is useful as we expect Player struct as an argument in almost all the functions shown above, and each of those function returns the modifed Player struct. What if we could show this in the documentation?

It would help readers understand the functions better and can use them safely! Let’s see how we can define the type and spec for the functions above.

Define Player as a type

Rewind to the section where we defined our Player struct in the beginning. We can define our Player custom type using the @type directive right next to it.

@type player :: %Player{
  name: String.t(),
  won: boolean(),
  rank: integer(),
  current_position: integer(),
  timestamp: integer()
}
defstruct name: "", won: false, rank: 0, current_position: 0, timestamp: nil

When we actually build documentation based on this, we get to see the neat presentation of the Player struct with types for the keys like this:

As we can see, this gives out a lot of information about the Player struct. It clearly specifies what types are expected for each key of the struct.

Add to existing doc

Alright, now that we defined the type, let’s use it in our function docs!

@doc """ .... """
@spec make_player(String.t()) :: player
def make_player(name) do
  %Player{name: name}
end

@spec is another Typespec directive in Elixir that let’s you define function signature. Function signature helps you structure function calls and define other funtions that would support Piping approach.

The def make_player(name) do by itself doesn’t reveal any info on what kind of type name is supposed to be, but adding the @spec on top of it makes everything clear.

It says the function make_player/1 takes exactly one argument of type String and returns ( :: ) a type player. We obviously have seen what the type player is!

Also, when we see this in docs, it shows something like this. Note how hovering over the return spec of the function shows a pop-up of the actual Player type. This is so neat and super helpful when we expand our codebase!

For the function to do its magic, none of these conventions are mandatory. But its always a good practise to add proper and sufficient documentation that would help anyone including you down the roadmap of the development.

Let’s move on to next aspect.

4. Documentation practice

This is more of a wrap up section where we look back at how the codebase evolved from just the game module logic to a fully documented module. When I started off writing the code for the game, I realised that I was going back and forth a lot from one module to another to see how each functions accept arguments. Yes, I wrote every single one of those functions, and still I had to cross check!

Soon, it became annoying for me to waste time on finding the function and then reading through it to figure out how it should be called at someplace else. I decided to take a look at @spec and @type which would simplify my work. Using text editors that shows us the function documentation helps us better when we have defined the spec and type along with the docstring. It also made sense to me, as I was going to host the doc for this project on GitLab pages.

So if you folks are serious about the work you do, start documenting them, help out your fellow developers! Learn about the documentation patterns for your language, use them, learn them. And with that, its a wrap for this one!

I hope this little blog was helpful for some of you out there starting out with Elixir. I’ll be back with Part-2 soon, which would focus on the game logic and the whole module.

unsplash-logoCover photo by Adi Goldstein