Playing Poker with Elixir (pt. 2)

In the previous post, we started building a poker application with Elixir. We created a data type to model a deck of cards, and built a module to rank hands using pattern matching.

Today, we'll start playing hands of poker between players. To do so, we'll throw processes into the mix. Although a poker hand is not concurrent (each player acts in sequence), processes will allow us to maintain the changing state of a poker hand in a language where data is immutable.

At the poker room

Before we build anything, let's take a moment to think about how poker works in the real world and see if we use it as a model for our system.

At a poker room, poker is played at a table. Players sit at a specific seat and buy into the game by trading cash for chips. When they're done playing, they can cash out and leave the table.

When the players are ready, the dealer will start a hand. Two players are forced to make initial bets (called blinds), and then all players are dealt their private two cards. There's a round of betting where each player, in turn, chooses to check, bet, or fold.

The dealer then reveals the first three public cards (called the flop), and there is another round of betting. Then another card is dealt publicly (called the turn) followed by a betting round. The last public card (the river) is dealt, and a final betting round occurs. The winner is revealed, and chips are awarded to the appropriate players.

Players, Tables, and Hands

Our app's processes will directly map to the description above:

Process Communication

To participate at a table, a player process will be able to send it sit, buy_in, cash_out, and leave messages. The table will keep track of who is seated and the balance of each player.

When the table receives the deal message, it will start a hand process, which will coordinate directly with the players. The hand will send hand_state messages to update each player as the hand plays out, and accept bet or fold messages from the players when it's their turn to act.

Introducing the Hand GenServer

Today, we'll focus on the hand process. We'll be using the GenServer behaviour to implement our processes. Using it offers many advantages over a raw process which I won't go into here. Learn You Some Erlang has an excellent chapter on why a generic server abstraction is a good idea.

In general, it's good practice to expose an interface that hides nitty-gritty implementation details. When using GenServer, this is particularly important - we don't want the rest of our code to have to know the details of our specific messages, or even that they're calling into a separate process. This is what our API will look like:

defmodule Poker.Hand do
  use GenServer

  def start_link(players, config \\ [])

  def start_link(players, config) when length(players) > 1 do
    GenServer.start_link(__MODULE__, [players, config])
  end

  def start_link(_players, _opts), do: {:error, :not_enough_players}

  def bet(hand, amount) do
    GenServer.call(hand, {:bet, amount})
  end

  def check(hand) do
    GenServer.call(hand, {:bet, 0})
  end

  def fold(hand) do
    GenServer.call(hand, :fold)
  end
end

We define function named start_link, which is the customary name for a function starting a linked process. It's first argument is a list of player pids involved in the hand. Eventually, we'll need to do something different here - we can't rely on the player processes not crashing or disconnecting - but for now, this will work.

The options argument will allow us to configure the sizes of the forced bets (blinds). Since we'd like to use defaults, Elixir warns us to define a function head with no body to avoid ambiguity.

Our other functions map to the messages we defined above, and use GenServer.call/3 to send messages to the hand server. These functions will let the players bet or fold during the hand and change the server's state accordingly.

Wait a minute... did you say change state?

In Elixir, data is immutable. For example, updating a key in a map returns a brand new map, rather than modifying the original one. This seems to present a problem. Other languages, for instance, Ruby, can keep state in variables on objects, which change as we call the object's methods. So how do we allow for mutable state when the language itself seems to prevent it?

For this purpose, we can imagine that our Elixir process is an object and the messages we send it are method calls. The key to how this works is recursion. A process can be started by calling a function with some initial state. It blocks, waiting to receive a message. Once it does, it can compute a new state based on its existing one and the message. Then, it can call the same function with the new state. At its core the GenServer abstraction follows this pattern.

GenServer callbacks

The GenServer behaviour defines a set of callbacks that our module can implement. These callbacks represent the interface that GenServer expects of our code. To complete our module, we'll need to implement some of these callbacks.

We'll start with init/1. It is responsible for setting up the server's initial state, and there's quite a bit we need to keep track of for a poker hand:

  • The phase of the hand (pre-flop, flop, turn, river)
  • The players still involved in the hand
  • Their private cards
  • The public cards visible to everyone
  • The remainder of the deck for dealing additional cards
  • The total number of chips in the pot
  • The players waiting to act in a betting round, and their required bets
def init([players, config]) do
  <<a::size(32), b::size(32), c::size(32)>> = :crypto.rand_bytes(12)
  :random.seed({a, b, c})

  {small_blind_amount, big_blind_amount} = get_blinds(config)
  [small_blind_player, big_blind_player|remaining_players] = players

  to_act =
    Enum.map(remaining_players, &{&1, big_blind_amount}) ++
    [
      {small_blind_player, big_blind_amount - small_blind_amount},
      {big_blind_player, 0}
    ]

  {hands, deck} = deal(Poker.Deck.new, players)

  state = %{
    phase: :pre_flop,
    players: players,
    pot: small_blind_amount + big_blind_amount,
    board: [],
    hands: hands,
    deck: deck,
    to_act: to_act
  }

  update_players(state)

  {:ok, state}
end

defp get_blinds(config) do
  big_blind   = Keyword.get(config, :big_blind, 10)
  small_blind = Keyword.get(config, :small_blind, div(big_blind, 2))
  {small_blind, big_blind}
end

There is a lot going on here, so I'll try to break it down, step by step. First things first: before shuffling the deck, we need to seed Erlang's random number generator. By default, the generator uses the same seed in every new process. If we didn't do this, we'd get the exact same deal every single time. Oops!

Next, we set up the list of actions for the first betting round. We'll represent an action as a {player, to_call} tuple, where to_call stores the minimum amount the player needs to bet to continue in the hand. The first item in the list represents the next player that needs to act, and we'll remove items from the list as they act.

In the first round, two players make forced bets called blinds, and the remaining players must call the big blind to continue. Our get_blinds helper function provides us with some default values if none were passed.

After we've set up the actions, we'll deal cards:

defp deal(deck, players) do
  {hands, deck} = Enum.map_reduce players, deck, fn (player, [card_one,card_two|deck]) ->
    {{player, [card_one, card_two]}, deck}
  end

  {Enum.into(hands, %{}), deck}
end

Elixir includes a function called Enum.map_reduce/3 in its standard library which is very convenient for this purpose. We map the players into their hands, one at a time, reducing the deck by two cards at each step. The result is a list of {hand, player} tuples and the remainder of the deck. At the end, Enum.into turns the tuples into a map for easy access later.

Once we've got our state set up, we'd like to let our players know about it:

defp update_players(state) do
  Enum.each state.players, fn (player) ->
    hand = Map.fetch! state.hands, player
    hand_state = %{
      hand: hand,
      active: player_active?(player, state),
      board: state.board,
      pot: state.pot
    }
    send player, {:hand_state, hand_state}
  end

  state
end

defp player_active?(p, %{to_act: [{p, _}|_]}), do: true
defp player_active?(_player, _state), do: false

We're using a plain message send to update the players on the hand state, which includes their private cards, the public cards, the number of chips in the pot, and a flag indicating whether or not they are the active player. Here and in much of the following code, we'll match on the first item in the to_act list to get the active player.

Since Enum.each returns :ok, we'll return the passed state as a result of our function so we can chain this function later.

Check, bet, or raise?

The next callback we need to implement is handle_call/3. Whenever we use GenServer.call to send a message, our server process will call handle_call, passing the message, sender, and its current state. We can pattern match on the message to handle the different types of requests.

We'll start with the bet message. Here are the two error cases:

def handle_call(
  {:bet, _}, {p_one, _}, state = %{to_act: [{p_two, _}|_]}
) when p_one != p_two do
  {:reply, {:error, :not_active}, state}
end

def handle_call(
  {:bet, amount}, _from, state = %{to_act: [{_, to_call}|_]}
) when amount < to_call do
  {:reply, {:error, :not_enough}, state}
end

The first case ignores messages from inactive players. The second argument to handle_call is a tuple containing the caller's pid, and the first element in our to_act list contains the player we are waiting for. Our guard checks if the two pids are not equal - if that's the case, our caller is not the active player.

Since pattern matching proceeds from top to bottom, all subsequent cases will involve messages from the active player. In the second case, the player is not betting the required amount.

There are three potential success cases:

  • A player calls and the betting round is over
  • A player calls and there are more players yet to act
  • A player raises and the remaining players must respond

Here are the first two:

def handle_call(
  {:bet, amount}, _from, state = %{to_act: [{_, to_call}]}
) when amount == to_call do
  updated_state = update_in(state.pot, &(&1 + amount)) |>
    advance_phase |>
    update_players

  {:reply, :ok, updated_state}
end

def handle_call(
  {:bet, amount}, _from, state = %{to_act: [{_, to_call}|to_act]}
) when amount == to_call do
  updated_state = update_in(state.pot, &(&1 + amount)) |>
    put_in([:to_act], to_act) |>
    update_players

  {:reply, :ok, updated_state}
end

In the first case, since the to_act list only has one item, we know this betting round is over (I'll cover advance_phase later). In the second, we just proceed down the to_act list. In both cases, we update the pot and let the players know the hand has changed.

Raising is a bit more difficult. We need to make sure all the other players call the raise before the hand proceeds, but some players may have already called (and no longer need to act), while others may be still waiting to call a previous bet.

def handle_call(
  {:bet, amount}, _from, 
  state = %{to_act: [{player, to_call}|remaining_actions]}
) when amount > to_call do
  raised_amount = amount - to_call

  previous_callers = state.players |>
    Stream.concat(state.players) |>
    Stream.drop_while(&(&1 != player)) |>
    Stream.drop(1 + length(remaining_actions)) |>
    Stream.take_while(&(&1 != player))

  to_act = Enum.map(remaining_actions, fn {player, to_call} ->
    {player, to_call + raised_amount}
  end) ++ Enum.map(previous_callers, fn player ->
    {player, raised_amount}
  end)

  updated_state = 
    %{state | to_act: to_act, pot: state.pot + amount} |>
    update_players

  {:reply, :ok, updated_state}
end

This is our most complicated code so far! First, we calculate previous_callers, which are the players that have already called the current bet and are no longer in the to_act list. We repeat the list of players once, since we might have to wrap from the end of the list to the head - and we grab a sublist of players, starting after the last player in to_act and ending before the player that just raised.

We then create a new to_act list, incrementing the existing items in the list by the raised amount and adding new elements to the end of the list for the previous callers.

Okay, I give up!

Handling the fold message is much simpler. We eliminate the player from the list of players in state so they won't participate in subsequent rounds, and update to_act to point to the next player. If there are no remaining players to act, we advance the hand to the next phase. As with bets, we ignore folds from inactive players.

def handle_call(
  :fold, {player, _}, state = %{to_act: [{player, _}]}
) do
  updated_state = state |>
    update_in([:players], &(List.delete(&1, player))) |> 
    advance_phase |> 
    update_players
  {:reply, :ok, updated_state}
end

def handle_call(
  :fold, {player, _}, state = %{to_act: [{player, _}|to_act]}
) do
  updated_state = state |>
    update_in([:players], &(List.delete(&1, player))) |> 
    put_in([:to_act], to_act) |>
    update_players

  {:reply, :ok, updated_state}
end

def handle_call(:fold, _from, state) do
  {:reply, {:error, :not_active}, state}
end

All we're lacking now is the implementation of advance_phase, which is called when a betting round is over. The rules for advancement are simple. At any point, if there is only one player left, they are declared the winner. If we're advancing to the flop, turn, or river, we need to deal the appropriate number of cards to the board and set up a new betting round.

defp advance_phase(state = %{players: [winner]}) do
  declare_winner(winner, state)
end

defp advance_phase(state = %{phase: :pre_flop}) do
  advance_board(state, :flop, 3)
end

defp advance_phase(state = %{phase: :flop}) do
  advance_board(state, :turn, 1)
end

defp advance_phase(state = %{phase: :turn}) do
  advance_board(state, :river, 1)
end

defp advance_board(state, phase, num_cards) do
  to_act = Enum.map(state.players, &{&1, 0})

  {additional_cards, deck} = Enum.split(state.deck, num_cards)

  %{state |
    phase: phase,
    board: state.board ++ additional_cards,
    deck: deck,
    to_act: to_act
  }
end

The advance_board helper does all the work. In subsequent betting rounds, each player has the opportunity to bet, but bets aren't required. Enum.split/2 lets us the reveal the right number of cards from the deck. We update to the next phase and start a new betting round.

Finally, if we finish the betting round after the river with more than one player remaining, we need to evaluate the final hands to decide the winners:

defp advance_phase(state = %{phase: :river}) do
  ranked_players = [{winning_ranking,_}|_] =
    state.players |>
    Stream.map(fn player ->
      {ranking, _} = Poker.Ranking.best_possible_hand(state.board, state.hands[player])
      {ranking, player}
    end) |>
    Enum.sort

  ranked_players |>
    Stream.take_while(fn {ranking, _} ->
      ranking == winning_ranking
    end) |>
    Enum.map(&elem(&1, 1)) |>
    declare_winner(state)

  state
end

For each remaining player, we use the best_possible_hand function we defined last week to combine their two private cards with the five public ones. We create tuples with the ranking/player combinations and sort them. Since it's possible for players to tie, we take elements from the sorted list until we find one with a lower ranking.

Wrapping up

Thanks for making it through this post! If you'd like to see the app so far, the code is available on GitHub.

At a couple hundred lines, it's a sizable module, but we're still not done. We skipped doing any of the accounting for the hand. Adding it will introduce all-ins and multiple pots. We'll address this in the next post.