I’ve been picking up a bit of Elixir on the side. Having done some Erlang previously the language is quite familiar - and feels more user friendly (though that may be my Python background talking). I am running through Etudes for Elixir and Chapter 9 - implementing the WAR card game, felt worthy of a post.

I don’t profess to writing ‘proper’ Elixir - this was more an exercise in how to handle asynchronous messaging with something requiring ordering. There is no focus on error handling and unit testing.

The WAR game

Given a (shuffled) deck of 52 cards, the game is deterministic. You can find more on Wikipedia but in a nutshell players divide the deck into 2 equal piles and ‘battle’ the top card repeatedly. Whoever has the highest rank wins his opponent’s cards (suit is completely ignored) and places them at the bottom of their pile. Ties are dealt with by putting 3 cards face down and one more up (though rules differ). The game is one when one of the player no longer has any cards to play.

Modelling the deck

Our deck will be represented as a {<suit>, <rank>} tuple. As WAR is suit-agnostic, we don’t really need to. But this will help us track the cards in the deck. We use list comprehensions to easily generate tuples:

defmodule Cards do
  @moduledoc "Represents a deck of 52 cards shuffle(cards), do: cards

  @doc "A, K, Q, J are 13, 12, 11, 10 respectively"
  def make_deck do
    for suit <- ["C","D","H","S"], rank <- 2..13, do: {suit, rank}
  end

  def shuffle(cards), do: cards

end

For now the shuffle method won’t do anything - we’ll take another stab at this later.

Entities

Our design will have 2 entities - player and dealer (so 2 players and one dealer). The player, once spawned, will respond to instructions from the dealer.

defmodule Player do

  def loop(name, cards) do
    IO.puts("#{name} has #{Enum.count(cards)} cards")
    receive do
      {:receive, new_cards} ->
        IO.puts("#{name} received #{Enum.count(new_cards)} from dealer")
        loop(name, cards ++ new_cards)
      {:give, from, num_cards} ->
        IO.puts("#{name} was asked to give #{num_cards} cards to #{inspect from}")
        {cards_to_give, remaining_cards} = Enum.split(cards, num_cards)
        send(from, {name, cards_to_give})
        loop(name, remaining_cards)
      :show ->
        #TODO this should probably send something back
        IO.puts(Enum.join(["#{name}'s deck:" | Enum.map(cards, &("#{elem(&1,1)}#{elem(&1,0)}"))], " "))
        loop(name, cards)
      :stop ->
        IO.puts("#{name} is shutting down")
        :ok
    end
  end

end

We don’t need :show for the game but it makes debugging easier - and :stop is there so we don’t leave processed running unnecessarily. Let’s take what we have so far for a spin:

iex(27)> deck = Cards.make_deck
[{"C", 2}, {"C", 3}, {"C", 4}, {"C", 5}, {"C", 6}, {"C", 7}, {"C", 8}, {"C", 9},
 {"C", 10}, {"C", 11}, {"C", 12}, {"C", 13}, {"D", 2}, {"D", 3}, {"D", 4},
 {"D", 5}, {"D", 6}, {"D", 7}, {"D", 8}, {"D", 9}, {"D", 10}, {"D", 11},
 {"D", 12}, {"D", 13}, {"H", 2}, {"H", 3}, {"H", 4}, {"H", 5}, {"H", 6},
 {"H", 7}, {"H", 8}, {"H", 9}, {"H", 10}, {"H", 11}, {"H", 12}, {"H", 13},
 {"S", 2}, {"S", 3}, {"S", 4}, {"S", 5}, {"S", 6}, {"S", 7}, {"S", 8}, {"S", 9},
 {"S", 10}, {"S", 11}, {"S", 12}, {"S", 13}]
iex(28)> {cards, _} = Enum.split(deck, 5)
{[{"C", 2}, {"C", 3}, {"C", 4}, {"C", 5}, {"C", 6}],
 [{"C", 7}, {"C", 8}, {"C", 9}, {"C", 10}, {"C", 11}, {"C", 12}, {"C", 13},
  {"D", 2}, {"D", 3}, {"D", 4}, {"D", 5}, {"D", 6}, {"D", 7}, {"D", 8},
  {"D", 9}, {"D", 10}, {"D", 11}, {"D", 12}, {"D", 13}, {"H", 2}, {"H", 3},
  {"H", 4}, {"H", 5}, {"H", 6}, {"H", 7}, {"H", 8}, {"H", 9}, {"H", 10},
  {"H", 11}, {"H", 12}, {"H", 13}, {"S", 2}, {"S", 3}, {"S", 4}, {"S", 5},
  {"S", 6}, {"S", 7}, {"S", 8}, {"S", 9}, {"S", 10}, {"S", 11}, {"S", 12},
  {"S", 13}]}
iex(29)> player1 = spawn(Player, :loop, ["Bob", cards])
Bob has 5 cards
#PID<0.178.0>
iex(30)> send(player1, :show)
Bob's deck: 2C 3C 4C 5C 6C
{:show}
Bob has 5 cards
iex(31)> send(player1, {:receive, [{"S", 13}]})
Bob received 1 from dealer
{:receive, [{"S", 13}]}
Bob has 6 cards
iex(33)> send(player1, {:give, self(), 3})
Bob was asked to give 3 cards to #PID<0.101.0>
{:give, #PID<0.101.0>, 3}
Bob has 3 cards

Note that if Bob doesn’t have enough cards, we’ll just get an empty list:

iex(34)> send(player1, {:give, self(), 3})
Bob received :take 3 from #PID<0.101.0>
{:give, #PID<0.101.0>, 3}
Bob has 0 cards
iex(35)> send(player1, {:give, self(), 3})
Bob received :take 3 from #PID<0.101.0>
{:give, #PID<0.101.0>, 3}
Bob has 0 cards
iex(36)> flush
{"Bob", [{"C", 2}, {"C", 3}, {"C", 4}]}
{"Bob", [{"C", 5}, {"C", 6}, {"S", 13}]}
{"Bob", []}
:ok
iex(37)> send(player1, :stop)
Bob is shutting down
:stop

The dealer is arguably the most interesting entity - this is because it has to deal with the asynchronicity of the messages. When it asks players for cards, the replies won’t necessarily come back in the same order.

Setting the game up however is easy. For simplicity we’re leveraging Process.register so we can access the players without having to pass PIDs around (though we need to use Process.whereis to pattern-match in the receive call - there must be a better way). The tricky bit here is to make sure we wait until all players have performed the required actions before proceeding. For this we will introduce the concept of count, which keeps track of the number of replies received. The first two lists represent the cards laid out for each player:

defmodule Dealer do

  def init() do
    deck = Cards.shuffle(Cards.make_deck())
    {dp1, dp2} = Enum.split(deck, 26)
    player1 = spawn(Player, :loop, ["Bob", dp1])
    Process.register(player1, :player1)
    player2 = spawn(Player, :loop, ["Alice", dp2])
    Process.register(player2, :player2)
    loop([], [], 0)
  end

  def loop([], [], 0) do
    send(:player1, {:give, self(), 1})
    send(:player2, {:give, self(), 1})
    pid1 = Process.whereis(:player1)
    pid2 = Process.whereis(:player2)
    receive do
      {^pid1, cards} ->
        loop(cards, [], 1)
      {^pid2, cards} ->
        loop([], cards, 1)
    end
  end

  def loop(p1, p2, count) when count < 2 do
    IO.puts("p1: #{inspect p1}, p2: #{inspect p2}")
    pid1 = Process.whereis(:player1)
    pid2 = Process.whereis(:player2)
    receive do
      {^pid1, cards} ->
        loop(cards, p2, count + 1)
      {^pid2, cards} ->
        loop(p1, cards, count + 1)
    end
  end

It’s a little muddy but after requesting players to hand over a card we then need to be ready to accept incoming messages. Because messages are async, we could have 2 receive clauses in loop([], [], 0) - but a new definition with a guard feels a little neater (though as we’ll see below we can do better using states and maps).

We can then list out the end conditions:

  def stop() do
    send(:player1, :stop)
    send(:player2, :stop)
  end

  def loop([], p2, count) when count == 2 do
    IO.puts("p1: [], p2: #{inspect p2} - P2 wins!")
    stop()
  end

  def loop(p1, [], count) when count == 2 do
    IO.puts("p1: #{inspect p1}, p2: [] - P1 wins!")
    stop()
  end

  def loop([], [], count) when count == 2 do
    IO.puts("It's a tie!")
    stop()
  end

This takes care of when a player (or both!) runs out of cards. Now for the main game logic:

  def loop([h1|t1], [h2|t2], count) when count == 2 do
    IO.puts("p1: #{inspect h1}, p2: #{inspect h2}")

    {_, v1} = h1
    {_, v2} = h2
    cond do
      (v1 > v2) ->
        IO.puts("p1 wins this round as #{v1} > #{v2}")
        send(:player1, {:receive, [h1|t1] ++ [h2|t2]})
        loop([], [], 0)
      (v2 > v1) ->
        IO.puts("p2 wins this round as #{v2} > #{v1}")
        send(:player1, {:receive, [h2|t2] ++ [h1|t1]})
        loop([], [], 0)
      (v1 == v2) ->
        IO.puts("tie! as #{v1} == #{v2}")
        send(:player1, {:give, self(), 3})
        send(:player2, {:give, self(), 3})
        loop([h1|t1], [h2|t2], 0)
    end
  end

Which takes care of all possible outcomes. We define sample_run to convince ourselves it works as expected:

  def sample_run() do
    {dp1, dp2} = {[{"C",1},{"C",8}], [{"S", 3}, {"S",8}]}
    player1 = spawn(Player, :loop, ["Bob", dp1])
    Process.register(player1, :player1)
    player2 = spawn(Player, :loop, ["Alice", dp2])
    Process.register(player2, :player2)
    loop([], [], 0)
  end

And here is the corresponding output:

iex(7)> Dealer.sample_run
Bob has 2 cards
Alice has 2 cards
Bob was asked to give 1 cards to #PID<0.101.0>
Alice was asked to give 1 cards to #PID<0.101.0>
Bob has 1 cards
Alice has 1 cards
p1: [{"C", 1}], p2: []
p1: {"C", 1}, p2: {"S", 3}
p2 wins this round as 3 > 1
Alice received 2 from dealer
Bob was asked to give 1 cards to #PID<0.101.0>
Alice has 3 cards
Bob has 0 cards
p1: [{"C", 8}], p2: []
Alice was asked to give 1 cards to #PID<0.101.0>
Alice has 2 cards
p1: {"C", 8}, p2: {"S", 8}
tie! as 8 == 8
p1: [{"C", 8}], p2: [{"S", 8}]
Bob was asked to give 3 cards to #PID<0.101.0>
Alice was asked to give 3 cards to #PID<0.101.0>
Bob has 0 cards
Alice has 0 cards
p1: [], p2: [{"S", 8}]
p1: [], p2: [{"S", 3}, {"C", 1}] - P2 wins!
Bob is shutting down
Alice is shutting down
:stop

Success!

Further enhancements

It’s a game that could arguably be played with a number of decks and more than 2 players. The code above is tied to 2 players - making the change would probably mean using some sort of state machine with the state stored in a map of sorts.