We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Okay so today I want to add in the logic for the PubSub. This will take care of 2 things. First it will make sure that anyone that is at the “/multi-poker” route will be updated on any new rooms that are created and that the state of the Room will be updated at anytime that a user takes an action. This will make sure that every user will know the current state of the room. Let’s start off with a new Module PubSub.
defmodule GameSite.MultiPoker.PubSub do
alias Phoenix.PubSub
@pubsub GameSite.PubSub
@lobby_topic "multi_poker:lobby"
def lobby_topic, do: @lobby_topic
def room_topic(room_id), do: "multi_poker:room:#{room_id}"
def subscribe_lobby do
PubSub.subscribe(@pubsub, lobby_topic())
end
def subscribe_room(room_id) do
PubSub.subscribe(@pubsub, room_topic(room_id))
end
def broadcast_lobby_updated do
PubSub.broadcast(@pubsub, lobby_topic(), {:lobby_updated})
end
def broadcast_room_updated(room) do
PubSub.broadcast(@pubsub, room_topic(room.room_id), {:room_updated, room})
end
end
This is a small module that is just here to allow a LiveView to subscribe to a topic, or broadcast to that topic. You should be able to see that we have a subscribe_lobby/0 and a subscribe_room/1. These will leverage the @ variables at the top to allow a LiveView to (onmount) subscribe to the topic. The _subscribe_room/1 will need to pass the room id as there might be quite a few rooms alive at any given point.
Now that we have this setup we need to add that to the Lobby so that we can keep the rooms up-to-date.
defmodule GameSiteWeb.MultiPokerLive.Lobby do
use GameSiteWeb, :live_view
alias GameSite.MultiPoker.{Room, PubSub}
...
@impl true
def mount(_params, session, socket) do
socket =
socket
|> set_current_viewer_id(session)
rooms =
if connected?(socket) do
PubSub.subscribe_lobby()
list_room_summaries()
else
[]
end
{:ok, assign(socket, :rooms, rooms)}
end
@impl true
def handle_info({:lobby_updated}, socket) do
rooms = list_room_summaries()
{:noreply, assign(socket, :rooms, rooms)}
end
@impl true
def handle_event("create_room", _params, socket) do
current_viewer_id = socket.assigns.current_viewer_id
{socket, room_id} =
case MultiPoker.create_room(current_viewer_id) do
{_, room_id} ->
{socket, room_id}
{:error, :already_has_room, room_id} ->
socket =
assign(socket, :rooms, list_room_summaries())
{socket, room_id}
end
PubSub.broadcast_lobby_updated()
{:noreply, redirect(socket, to: "/multi-poker/#{room_id}")}
end
...
def set_current_viewer_id(socket, _session) do
assign(socket, :current_viewer_id, :not_signed_in)
end
end
I only put the new or updated versions of the functions, but you can see that on mount we subscribe and then when a room is created we broadcast.
Now that we have that let’s add some functionality for the Room, keeping in mind that at any point if we change anything in the room we will need to broadcast in order for everyone to see any changes to the room state. We will start by adding in functionality to “join-game.”
#Room
defmodule GameSite.MultiPoker.Room do
use GenServer
alias GameSite.MultiPoker.{GameLogic, Player, PubSub}
...
@impl true
def handle_call({:add_player, viewer_id}, _from, %__MODULE__{} = state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
case next_player_id(state) do
nil ->
{:reply, {:error, :room_full}, state}
player_id ->
player = Player.new(player_id, viewer_id)
new_players = Map.put(state.players, player_id, player)
new_state = %__MODULE__{state | players: new_players}
PubSub.broadcast_room_updated(new_state)
{:reply, {:ok, player}, new_state}
end
player_id ->
{:reply, {:ok, Map.fetch!(state.players, player_id)}, state}
end
end
...
end
# MultiPokerLive
defmodule GameSiteWeb.MultiPokerLive do
use GameSiteWeb, :live_view
alias GameSite.MultiPoker.{GameLogic, Room, Player, PubSub}
...
@impl true
def render(assigns) do
~H"""
<%= if @room == nil do %>
Room is loading...
<% else %>
<p>Room Status: {@room.room_status}</p>
<p>Small Blind: {@room.small_blind}</p>
<p>Big Blind: {@room.big_blind}</p>
<p>Deck:</p>
<pre><%= inspect(@room.deck) %></pre>
<GameBoard.score_board
phase={@room.phase}
current_player_turn={@room.current_player_turn}
pot={@room.pot}
dealer_player_id={@room.dealer_player_id}
/>
<GameBoard.game_table
players={@room.players}
current_player_turn={@room.current_player_turn}
community_cards={@room.community_cards}
current_viewer_id={@current_viewer_id}
/>
<GameBoard.player_actions
action_state={@viewer_state.action_state}
player_chips={@viewer_state.player_chips}
bet_amount={0}
/>
<GameBoard.join_game viewer_state={@viewer_state} />
<% end %>
"""
end
@impl true
def mount(params, session, socket) do
socket =
socket
|> set_current_viewer_id(session)
if connected?(socket) do
case MultiPoker.get_room(params["room"]) do
{:ok, room} ->
PubSub.subscribe_room(room.room_id)
viewer_state = Room.viewer_state(room, socket.assigns.current_viewer_id)
socket =
socket
|> assign(:room, room)
|> assign(:viewer_state, viewer_state)
|> assign(:form, to_form(%{}))
{:ok, socket}
:error ->
socket =
socket
|> assign(:room, :bad_room)
|> assign(:form, to_form(%{}))
{:ok, socket}
end
else
{:ok, assign(socket, :room, nil)}
end
end
@impl true
def handle_info({:room_updated, room}, socket) do
viewer_state = Room.viewer_state(room, socket.assigns.current_viewer_id)
{:noreply,
socket
|> assign(room: room)
|> assign(:viewer_state, viewer_state)}
end
@impl true
def handle_event(
"join-game",
_params,
%{assigns: %{current_viewer_id: viewer_id, room: %Room{room_id: room_id}}} = socket
) do
MultiPoker.add_player(room_id, viewer_id)
{:noreply, socket}
end
...
end
Okay we now have the logic we need to add a player. What is great about this is we don’t need to worry about dealing with constantly keeping the state of the room updated within the LiveView the handle_info does that for us and if we always make sure to broadcast after any update to the Room with the Room module we are fine.
Let’s work on betting next. This will follow the same logic but in this case we are sending a bet amount to the MultiPoker.
#MultiPokerLive
@impl true
def handle_event(
"player-bet",
%{"bet_amount" => bet_amount},
%{assigns: %{current_viewer_id: viewer_id, room: %Room{room_id: room_id}}} = socket
) do
amount = String.to_integer(bet_amount)
MultiPoker.player_bet(room_id, viewer_id, amount)
{:noreply, socket}
end
#MultiPoker
def player_bet(room_id, viewer_id, amount) do
with {:ok, pid} <- get_room_pid(room_id) do
Room.player_bet(pid, viewer_id, amount)
end
end
# Room
@impl true
def handle_cast({:player_bet, viewer_id, amount}, %__MODULE__{} = state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
new_state = GameLogic.player_bet(state, player_id, amount)
if new_state != state, do: PubSub.broadcast_room_updated(new_state)
{:noreply, new_state}
end
end
# GameBoard
attr(:disabled, :boolean, required: true)
attr(:bet_amount, :integer, required: false, default: 0)
attr(:player_chips, :integer, required: true)
def player_controls(assigns) do
~H"""
<div class="space-y-4">
<form phx-submit="player-bet" class="space-y-4">
<div class="max-w-xs mx-auto">
<label for="bet_amount" class="block text-sm font-medium text-gray-700 mb-1">
Bet Amount
</label>
<input
id="bet_amount"
name="bet_amount"
type="number"
min="1"
max={@player_chips}
value={@bet_amount}
disabled={@disabled}
class={[
"w-full rounded-lg border px-3 py-2 shadow-sm focus:outline-none",
"border-gray-300 focus:ring-2 focus:ring-blue-500",
@disabled && "bg-gray-100 cursor-not-allowed opacity-70"
]}
/>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<button
type="button"
phx-click="player-fold"
disabled={@disabled}
class={button_class("bg-red-500 hover:bg-red-600", @disabled)}
>
Fold
</button>
<button
type="button"
phx-click="player-check"
disabled={@disabled}
class={button_class("bg-gray-500 hover:bg-gray-600", @disabled)}
>
Check
</button>
<button
type="submit"
disabled={@disabled}
class={button_class("bg-blue-500 hover:bg-blue-600", @disabled)}
>
Bet
</button>
<button
type="button"
phx-click="player-all-in"
disabled={@disabled}
class={button_class("bg-yellow-500 hover:bg-yellow-600", @disabled)}
>
All In
</button>
</div>
</form>
</div>
"""
end
So that is a new event that will work for us. Go ahead and test it out. It should allow you to bet and then it will advance to the next player. Take some time now to add in the other events that we have within the player_actions component.
Okay I quickly wanted to work on the “leave-game” logic as it will help you to reset the room if you get into a bad state while testing everything that you add.
# MultiPokerLive
@impl true
def handle_info({:room_closed, _room_id}, socket) do
{:noreply, push_navigate(socket, to: "/multi-poker")}
end
def handle_event(
"leave-game",
_params,
%{assigns: %{current_viewer_id: viewer_id, room: %Room{room_id: room_id}}} = socket
) do
MultiPoker.player_leave_game(room_id, viewer_id)
{:noreply, socket}
end
# MultiPoker
def player_leave_game(room_id, viewer_id) do
with {:ok, pid} <- get_room_pid(room_id) do
Room.player_leave_game(pid, viewer_id)
end
end
# Room
def player_leave_game(pid, viewer_id) do
GenServer.cast(pid, {:remove_player, viewer_id})
end
@impl true
def handle_cast({:remove_player, viewer_id}, %__MODULE__{} = state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
if player_id == state.host_id do
PubSub.broadcast_room_closed(state.room_id)
PubSub.broadcast_lobby_updated()
{:stop, :normal, state}
else
new_state =
state
|> remove_player(player_id)
|> maybe_advance_turn(player_id)
if new_state != state do
PubSub.broadcast_room_updated(new_state)
end
{:noreply, new_state}
end
end
end
defp remove_player(%__MODULE__{} = state, player_id) do
new_players = Map.delete(state.players, player_id)
%__MODULE__{state | players: new_players}
end
defp maybe_advance_turn(%__MODULE__{} = state, player_id) do
if state.current_player_turn == player_id do
GameLogic.advance_to_next_player(state)
else
state
end
end
# PubSub
def broadcast_room_closed(room_id) do
PubSub.broadcast(@pubsub, room_topic(room_id), {:room_closed, room_id})
end
Okay we now have the logic that we need for this. First let’s talk about the new handle_event that takes care of the “leave-game” event. As we follow the logic we will get to the Room logic for this event. This event is responsible for checking to see if the viewer_id is the host, if so we need to delete the room and then broadcast new rooms to the Lobby, if not the we need to update the Room and then broadcast the new state of the room. Going back to the MultiPokerLive we see an other handle_info that will deal with the :room_closed event and that makes sure to reroute the users to the Lobby if the room is stopped.
We will now set the event so start the game, with a “ready” event. This will be a button that will tell the room that the player is ready to start the hand and then the GameLogic.start_hand will run.
# MultiPokerLive
@impl true
def render(assigns) do
~H"""
<%= if @room == nil do %>
Room is loading...
<% else %>
...
<div column-2>
<GameBoard.join_game viewer_state={@viewer_state} />
<GameBoard.player_ready game_state={@room.room_status} />
</div>
<% end %>
"""
end
@impl true
def handle_event(
"ready",
_params,
%{assigns: %{current_viewer_id: viewer_id, room: %Room{room_id: room_id}}} = socket
) do
MultiPoker.player_ready(room_id, viewer_id)
{:noreply, socket}
end
# MultiPoker
def player_ready(room_id, viewer_id) do
with {:ok, pid} <- get_room_pid(room_id) do
Room.player_ready(pid, viewer_id)
end
end
# Room
def player_ready(pid, viewer_id) do
GenServer.cast(pid, {:player_ready, viewer_id})
end
@impl true
def handle_cast({:player_ready, viewer_id}, state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
new_state =
state
|> mark_player_ready(player_id)
|> maybe_start_hand()
if new_state != state, do: PubSub.broadcast_room_updated(new_state)
{:noreply, new_state}
end
end
defp mark_player_ready(%__MODULE__{} = state, player_id) do
case Map.fetch(state.players, player_id) do
{:ok, player} ->
updated_player = Player.change(player, ready?: true)
new_players = Map.put(state.players, player_id, updated_player)
%__MODULE__{state | players: new_players}
:error ->
state
end
end
defp maybe_start_hand(%__MODULE__{} = state) do
if can_start_hand?(state) do
state
|> change(room_status: :playing)
|> GameLogic.start_hand()
else
state
end
end
defp can_start_hand?(%__MODULE__{room_status: :waiting, players: players}) do
map_size(players) >= 2 and Enum.all?(Map.values(players), & &1.ready?)
end
defp can_start_hand?(_state), do: false
Okay now we have all we need to start a Hand. This will take care of the logic to see if all players are ready.
We will now work on the logic to start and advance a round. We will start with the logic to start the game using the event “player-ready” this will let the Room know that we are ready to start the Hand.
# MultiPokerLive
@impl true
def handle_event(
"player-ready",
_params,
%{assigns: %{current_viewer_id: viewer_id, room: %Room{room_id: room_id}}} = socket
) do
MultiPoker.player_ready(room_id, viewer_id)
{:noreply, socket}
end
# MultiPoker
def player_ready(room_id, viewer_id) do
with {:ok, pid} <- get_room_pid(room_id) do
Room.player_ready(pid, viewer_id)
end
end
# Room
def player_ready(pid, viewer_id) do
GenServer.cast(pid, {:player_ready, viewer_id})
end
@impl true
def handle_cast({:player_ready, viewer_id}, state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
new_state =
state
|> mark_player_ready(player_id)
|> maybe_start_hand()
if new_state != state, do: PubSub.broadcast_room_updated(new_state)
{:noreply, new_state}
end
end
# GameLogic
def start_hand(%Room{} = room) do
room
|> advance_to_next_dealer()
|> reset_room_for_new_hand()
|> reset_players_for_new_hand()
|> shuffle_new_deck()
|> deal_player_hole_cards()
|> set_first_player_turn()
end
def advance_phase_and_deal(%Room{phase: phase} = room) do
case phase do
:new_hand ->
start_hand(room)
:pre_flop ->
room
|> reset_players_for_new_round()
|> deal_community_cards(3, :flop)
|> set_first_player_turn
:flop ->
room
|> reset_players_for_new_round()
|> deal_community_cards(1, :turn)
|> set_first_player_turn
:turn ->
room
|> reset_players_for_new_round()
|> deal_community_cards(1, :river)
|> set_first_player_turn
:river ->
room
|> Room.change(phase: :showdown)
# Set all players to waiting: true
|> set_first_player_turn
:showdown ->
room
|> end_hand()
end
end
defp maybe_advance_round(%Room{} = room) do
if betting_round_complete?(room) do
advance_phase_and_deal(room)
else
advance_to_next_player(room)
end
end
defp betting_round_complete?(%Room{
players: players,
current_round_max_bet: current_round_max_bet
}) do
active_players =
players
|> Map.values()
|> Enum.reject(& &1.folded?)
case active_players do
[] ->
true
[_one_player_left] ->
true
_ ->
Enum.all?(active_players, fn player ->
player.chips == 0 or
(player.waiting? and player.current_bet == current_round_max_bet)
end)
end
end
def mark_player_ready(%Room{} = state, player_id) do
case Map.fetch(state.players, player_id) do
{:ok, player} ->
updated_player = Player.change(player, ready?: true)
new_players = Map.put(state.players, player_id, updated_player)
%Room{state | players: new_players}
:error ->
state
end
end
def maybe_start_hand(%Room{} = state) do
if can_start_hand?(state) do
state
|> Room.change(room_status: :playing)
|> start_hand()
else
state
end
end
defp can_start_hand?(%Room{room_status: :waiting, players: players}) do
map_size(players) >= 2 and Enum.all?(Map.values(players), & &1.ready?)
end
defp can_start_hand?(_state), do: false
This should be all you need for the advancement of a round. Each time that we do anything we need to check if we are able to advance the round. That check will worry about the players that aren’t folded, then testing against the amount of players, the check to see that all the players have the same value as the current_round_max_bet. Lastly you can see that each round will be advanced by the advance_phase_and_deal depending on the current phase of the Hand.
Okay so finally for this one we will work on the logic for dealing with the endgame. Everything that we have so far will allow us to simply work within the GameLogic module. We will however add in a new Module HandEvaluator that will help us rank all the hands.
# HandEvaluator
defmodule GameSite.MultiPoker.HandEvaluator do
alias GameSite.MultiPoker.{Player, Room}
def evaluate_hands(%Room{community_cards: community_cards, players: players}) do
players
|> Enum.map(fn {_id, %Player{hand: hand, player_id: player_id}} ->
{player_id, community_cards ++ hand}
end)
|> Enum.sort_by(&rank_hand/1, :desc)
end
def rank_hand({_player_id, cards}) do
score_hand(cards)
end
def score_hand(cards) do
cond do
royal_flush_value(cards) != nil ->
{get_rank(:royal_flush), [royal_flush_value(cards)]}
straight_flush_value(cards) != nil ->
{get_rank(:straight_flush), [straight_flush_value(cards)]}
four_of_a_kind_value(cards) != nil ->
{get_rank(:four_of_a_kind), four_of_a_kind_value(cards)}
full_house_value(cards) != nil ->
{get_rank(:full_house), full_house_value(cards)}
flush_value(cards) != nil ->
{get_rank(:flush), flush_value(cards)}
straight_value(cards) != nil ->
{get_rank(:straight), [straight_value(cards)]}
three_of_a_kind_value(cards) != nil ->
{get_rank(:three_of_a_kind), three_of_a_kind_value(cards)}
two_pair_value(cards) != nil ->
{get_rank(:two_pair), two_pair_value(cards)}
pair_value(cards) != nil ->
{get_rank(:pair), pair_value(cards)}
true ->
{get_rank(:high_card), high_card_value(cards)}
end
end
def get_rank(:high_card), do: 1
def get_rank(:pair), do: 2
def get_rank(:two_pair), do: 3
def get_rank(:three_of_a_kind), do: 4
def get_rank(:straight), do: 5
def get_rank(:flush), do: 6
def get_rank(:full_house), do: 7
def get_rank(:four_of_a_kind), do: 8
def get_rank(:straight_flush), do: 9
def get_rank(:royal_flush), do: 10
def royal_flush_value(cards) do
case straight_flush_value(cards) do
14 -> 14
_ -> nil
end
end
def straight_flush_value(cards) do
cards
|> Enum.group_by(fn {_value, suit} -> suit end, fn {value, _suit} -> value end)
|> Enum.map(fn {_suit, values} ->
values
|> Enum.uniq()
|> add_wheel_ace()
|> Enum.sort()
|> straight_high_card_from_values()
end)
|> Enum.reject(&is_nil/1)
|> Enum.max(fn -> nil end)
end
def four_of_a_kind_value(cards) do
counts = value_frequencies(cards)
case Enum.find(counts, fn {_value, count} -> count == 4 end) do
nil ->
nil
{quad_value, _count} ->
kicker =
counts
|> Enum.reject(fn {value, _count} -> value == quad_value end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.max(fn -> nil end)
[quad_value, kicker]
end
end
def full_house_value(cards) do
counts = value_frequencies(cards)
trips =
counts
|> Enum.filter(fn {_value, count} -> count >= 3 end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
case trips do
[] ->
nil
[top_trip | remaining_trips] ->
pair_candidates =
counts
|> Enum.reject(fn {value, count} ->
value == top_trip or count < 2
end)
|> Enum.map(fn {value, _count} -> value end)
pair_value =
(remaining_trips ++ pair_candidates)
|> Enum.sort(:desc)
|> List.first()
if pair_value do
[top_trip, pair_value]
else
nil
end
end
end
def flush_value(cards) do
cards
|> Enum.group_by(fn {_value, suit} -> suit end, fn {value, _suit} -> value end)
|> Enum.map(fn {_suit, values} ->
if length(values) >= 5 do
values
|> Enum.sort(:desc)
|> Enum.take(5)
else
nil
end
end)
|> Enum.reject(&is_nil/1)
|> Enum.max(fn -> nil end)
end
def straight_value(cards) do
cards
|> Enum.map(fn {value, _suit} -> value end)
|> Enum.uniq()
|> add_wheel_ace()
|> Enum.sort()
|> straight_high_card_from_values()
end
def three_of_a_kind_value(cards) do
counts = value_frequencies(cards)
case counts
|> Enum.filter(fn {_value, count} -> count == 3 end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
|> List.first() do
nil ->
nil
trip_value ->
kickers =
counts
|> Enum.reject(fn {value, _count} -> value == trip_value end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
|> Enum.take(2)
[trip_value | kickers]
end
end
def two_pair_value(cards) do
counts = value_frequencies(cards)
pairs =
counts
|> Enum.filter(fn {_value, count} -> count >= 2 end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
case pairs do
[high_pair, low_pair | _rest] ->
kicker =
counts
|> Enum.reject(fn {value, _count} -> value in [high_pair, low_pair] end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.max(fn -> nil end)
[high_pair, low_pair, kicker]
_ ->
nil
end
end
def pair_value(cards) do
counts = value_frequencies(cards)
case counts
|> Enum.filter(fn {_value, count} -> count == 2 end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
|> List.first() do
nil ->
nil
pair_value ->
kickers =
counts
|> Enum.reject(fn {value, _count} -> value == pair_value end)
|> Enum.map(fn {value, _count} -> value end)
|> Enum.sort(:desc)
|> Enum.take(3)
[pair_value | kickers]
end
end
def high_card_value(cards) do
cards
|> Enum.map(fn {value, _suit} -> value end)
|> Enum.uniq()
|> Enum.sort(:desc)
|> Enum.take(5)
end
defp straight_high_card_from_values(values) do
values
|> Enum.chunk_every(5, 1, :discard)
|> Enum.filter(&consecutive?/1)
|> Enum.map(&List.last/1)
|> Enum.max(fn -> nil end)
end
defp consecutive?([a, b, c, d, e]) do
b == a + 1 and c == b + 1 and d == c + 1 and e == d + 1
end
defp add_wheel_ace(values) do
if 14 in values do
[1 | values]
else
values
end
end
defp value_frequencies(cards) do
cards
|> Enum.frequencies_by(fn {value, _suit} -> value end)
|> Enum.to_list()
end
end
# GameLogic
def start_hand(%Room{} = room) do
room
|> advance_to_next_dealer()
|> reset_room_for_new_hand()
|> reset_players_for_new_hand()
|> shuffle_new_deck()
|> deal_player_hole_cards()
|> set_first_player_turn()
end
def end_hand(%Room{} = room) do
room
|> resolve_winner_and_award_pot()
|> reset_players_to_not_ready()
|> Room.change(
room_status: :waiting,
current_player_turn: nil
)
end
def advance_phase_and_deal(%Room{phase: phase} = room) do
case phase do
:new_hand ->
start_hand(room)
:pre_flop ->
room
|> reset_players_for_new_round()
|> deal_community_cards(3, :flop)
|> set_first_player_turn
:flop ->
room
|> reset_players_for_new_round()
|> deal_community_cards(1, :turn)
|> set_first_player_turn
:turn ->
room
|> reset_players_for_new_round()
|> deal_community_cards(1, :river)
|> set_first_player_turn
:river ->
room
|> Room.change(phase: :showdown)
# Set all players to waiting: true
|> set_first_player_turn
:showdown ->
room
|> end_hand()
end
end
def resolve_winner_and_award_pot(%Room{} = room) do
ordered_players = HandEvaluator.evaluate_hands(room)
case ordered_players do
[] ->
room
[{winner_player_id, winning_hand} | _rest] ->
award_pot(room, winner_player_id, winning_hand)
end
end
def award_pot(%Room{players: players, pot: pot} = room, winner_player_id, winning_hand) do
case Map.fetch(players, winner_player_id) do
{:ok, player} ->
updated_player = Player.change(player, chips: player.chips + pot)
new_players = Map.put(players, winner_player_id, updated_player)
%Room{room | players: new_players, winning_hand: winning_hand}
:error ->
room
end
end
def reset_players_to_not_ready(%Room{players: players} = room) do
new_players =
Enum.into(players, %{}, fn {player_id, player} ->
updated_player =
Player.change(player,
ready?: false
)
{player_id, updated_player}
end)
%Room{room | players: new_players}
end
The very last thing that I want to add is that because of all the changes that I made to the GameBoard I needed a more robust viewer_state
# Room
def viewer_state(%__MODULE__{} = room, current_viewer_id) do
case get_player_by_viewer_id_from_room(room, current_viewer_id) do
nil ->
%{
player_id: nil,
action_state: :not_joined,
player_chips: 0,
player_current_bet: 0,
ready?: false
}
%Player{} = player ->
action_state =
cond do
player.folded? -> :folded
player.chips == 0 -> :all_in
room.current_player_turn == player.player_id -> :your_turn
true -> :waiting
end
%{
action_state: action_state,
player_chips: player.chips,
player_current_bet: player.current_bet,
player_id: player.player_id,
ready?: player.ready?
}
end
end