Playing Poker with Elixir (pt. 1)

When learning a new language or framework, I find it's helpful to just dive in and try to build something. So far in this series, we've looked at brunch and explored ecto, but we haven't built anything concrete yet. Let's change that!

I've always loved poker - in another lifetime, I actually played it for a living. Building a poker app with Elixir and Phoenix seems like a great fit - Elixir/Erlang has had notable success in gaming applications, and Phoenix's channels should be valuable for managing communication between the players and the app.

Shuffle up and deal

We'll be gradually building a web-based Texas Hold 'Em application. We need a place to get started, and it's pretty obvious we'll need cards, so let's build a deck:

defmodule Poker.Deck do
  defmodule Card do
    defstruct [:rank, :suit]
  end

  def new do
    for rank <- ranks, suit <- suits do 
      %Card{rank: rank, suit: suit}
    end |> Enum.shuffle
  end

  defp ranks, do: Enum.to_list(2..14)
  defp suits, do: [:spades, :clubs, :hearts, :diamonds]
end

Since creating a card doesn't make much sense outside of a deck, we embedded the Card struct inside of it. We've also defined new/0, which will return a 52-card, shuffled deck. The for comprehension is well-suited to the task; the two generators will give us every possible combination of rank and suit. We're using integers to represent a card's rank, and values of 11 through 14 represent ranks of Jack and higher.

Although it still feels wrong to use the word "new" in a functional language, remember we're returning data, not an object - in fact, several of Elixir's built-in types (keyword lists, maps, sets, etc.) do exactly the same.

The joys of pattern matching

To determine the winner, we'll need to be able to rank a hand. We'll use pattern matching to accomplish this. Our patterns will start with the highest-value hands and proceed to the lowest, and the first match will determine the value of the hand.

defmodule Poker.Ranking do
  def evaluate(cards) do
    cards |> Enum.map(&to_tuple/1) |> Enum.sort |> eval
  end

  defp to_tuple(
    %Poker.Deck.Card{rank: rank, suit: suit}
  ), do: {rank, suit}

  defp eval(
    [{10, s}, {11, s}, {12, s}, {13, s}, {14, s}]
  ), do: :royal_flush
end

First, we convert the card struct to a tuple, then, we sort the tuples so we can match them predictably. Elixir sorts identically sized tuples by the first element, breaking ties by the subsequent elements - so we'll be able to match cards in order of ascending rank.

After we've done that, our code to identify a royal flush looks much like the definition of one. The repeated s variable ensures the cards will share a single suit, and the 10 through 14 represent the card ranks required. Let's move on to straight flushes:

  defp eval(
    [{a, s}, {_b, s}, {_c, s}, {_d, s}, {e, s}]
  ) when e - a == 4, do: :straight_flush
  defp eval(
    [{2, s}, {3, s}, {4, s}, {5, s}, {14, s}]
  ), do: :straight_flush

We relaxed the restriction on specific ranks, but added a guard clause. We know the cards are sorted ascending and cannot repeat ranks (since they're all of the same suit), so if the high card is four ranks above the low card, we've hit our straight flush. Since the ace can serve as a low card as well as high, we have to add another function head to cover that case. Let's match a few more:

  defp eval(
    [{a, _}, {a, _}, {a, _}, {a, _}, {b, _}]
  ), do: :four_of_a_kind
  defp eval(
    [{b, _}, {a, _}, {a, _}, {a, _}, {a, _}]
  ), do: :four_of_a_kind

  defp eval(
    [{a, _}, {a, _}, {a, _}, {b, _}, {b, _}]
  ), do: :full_house
  defp eval(
    [{b, _}, {b, _}, {a, _}, {a, _}, {a, _}]
  ), do: :full_house

We're using the same strategy to match identical ranks as we did for suits above. We need to allow two patterns for each hand because after sorting our cards there are two possibilities - a > b or b > a.

The patterns for the remainder of possible poker hands follow directly from these, so I won't go through them here. If you're interested seeing in the full code, it's available on github.

Some observations

Pattern matching enables a completely different approach to this problem. With a couple dozen patterns and two guard clauses, we can rank every poker hand. The approach is declarative: the function definitions look like the hands they are describing.

In language without pattern matching we'd have to take a different approach. Having solved this problem before in Ruby, I found the code to be much more complex. Each hand type requires its own bit of imperative code, and the overall solution feels much less clear.

And the winner is...

If you're familiar with Hold 'Em, you know that determining a player's hand isn't as simple as looking at five cards. Each player holds two cards privately (called hole cards) and the final hand is determined by combining those cards with the five public ones (called the board) into the best possible hand.

We'll solve this by computing every possible hand from the seven available cards and choosing the one with the highest evaluation.

  def best_possible_hand(board, hand) do
    board ++ hand
      |> combinations(5)
      |> Stream.map(&{evaluate(&1), &1})
      |> Enum.max
  end

The combinations function returns a list of all 5-combinations of the 7 cards. We evaluate each one, keeping the five cards along with our evaluation in a tuple. Recall that because Elixir orders tuples element by element, Enum.max will pick the pair with the highest evaluation.

Currently, our evaluate function returns an atom describing the strength of the hand. To make this new code work, we need to modify evaluate to return greater values for stronger hands. But before we do, let's do a quick refresher on poker hands.

In poker, hands are first ranked by their category, but ties within those categories are handled by the ranks of the individual cards themselves. For example, a nines over sixes full-house will beat nines over fives, and a ten-high straight flush will beat a five-high one.

Knowing this, let's change our eval functions to return tuples. The first element will represent the overall hand category, and the second will represent the hand's strength within the category.

  defp eval(
    [{10, s}, {11, s}, {12, s}, {13, s}, {14, s}]
  ), do: {10, nil} 

  defp eval(
    [{a, s}, {b, s}, {c, s}, {d, s}, {e, s}]
  ) when e - a == 4, do: {9, e}
  defp eval(
    [{2, s}, {3, s}, {4, s}, {5, s}, {14, s}]
  ), do: {9, 5}

  defp eval(
    [{a, _}, {a, _}, {a, _}, {a, _}, {b, _}]
  ), do: {8, {a,b}}
  defp eval(
    [{b, _}, {a, _}, {a, _}, {a, _}, {a, _}]
  ), do: {8, {a,b}}

  defp eval(
    [{a, _}, {a, _}, {a, _}, {b, _}, {b, _}]
  ), do: {7, {a,b}}
  defp eval(
    [{b, _}, {b, _}, {a, _}, {a, _}, {a, _}]
  ), do: {7, {a,b}}

The bindings from our patterns are very useful here - we can plug them directly into our resulting tuples to serve as the tiebreaking element. Here, the full houses mentioned above would rank {7, {9,6}} vs. {7, {9,5}}, and the straight flushes would rank {9, 10} vs. {9, 5}.

Note that for a royal flush, our ranking tuple is {10, nil}. If we had left out the second element, our royal flush would rank as the worst possible hand! This is because tuples with more elements always compare greater than smaller ones. In other words, {10} < {9, 1}, but {10, nil} > {9, 1}.

Wrapping up...

I hope you enjoyed this post. The code for it is available on GitHub. In the next post, I'll extend this code to play an actual hand using Elixir processes.