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:
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.