We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
PokerLive - 07 - Cleanup
Published on: 2026-04-06
Tags:
Blog, Side Project, Testing, LiveView, Game Site, Phoenix, Poker, GenServer, Player, Room, Supervisor Tree, Game Logic
We are almost done here we have a few things to clean up first.
Redirect the User if they try to access a room that doesn’t exist
#MultiPokerLive
@impl true
def mount(params, session, socket) do
socket =
socket
|> set_current_viewer_id(session)
case MultiPoker.get_room(params["room"]) do
{:ok, room} ->
if connected?(socket) do
PubSub.subscribe_room(room.room_id)
end
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
|> put_flash(:error, "That room does not exist.")
|> push_navigate(to: "/multi-poker")
{:ok, socket}
end
end
Only allow a user to join the table if it is not at max player count
# MultiPokerLive
@impl true
def render(assigns) do
~H"""
...
<div column-2>
<GameBoard.join_game
viewer_state={@viewer_state}
room_status={@room.room_status}
room_full={room_full(@room)}
/>
...
</div>
<% end %>
"""
end
defp room_full(%Room{players: players}) do
map_size(players) >= 6
end
# GameBoard
attr(:viewer_state, :map, required: true)
attr(:room_status, :atom, required: true)
attr(:room_full, :boolean, required: true)
def join_game(assigns) do
~H"""
<div class="flex justify-center">
<%= if @room_status == :waiting and @viewer_state.action_state == :not_joined and not @room_full do %>
<button phx-click="join-game" class="px-4 py-2 bg-blue-600 text-white rounded">
Join Game
</button>
<% end %>
<%= if @viewer_state.action_state != :not_joined do %>
<button phx-click="leave-game" class="px-4 py-2 bg-red-600 text-white rounded">
Leave Game
</button>
<% end %>
</div>
"""
end
Remove ability to leave game while a hand is active.
# GameBoard
attr(:viewer_state, :map, required: true)
attr(:room_status, :atom, required: true)
attr(:room_full, :boolean, required: true)
def join_game(assigns) do
~H"""
<div class="flex justify-center">
<%= if @room_status == :waiting and @viewer_state.action_state == :not_joined and not @room_full do %>
<button phx-click="join-game" class="px-4 py-2 bg-blue-600 text-white rounded">
Join Game
</button>
<% end %>
<%= if @viewer_state.action_state != :not_joined and @room_status == :waiting do %>
<button phx-click="leave-game" class="px-4 py-2 bg-red-600 text-white rounded">
Leave Game
</button>
<% end %>
</div>
"""
end
Add in the Blinds to the players.
# Game Logic
def start_hand(%Room{} = room) do
room
|> advance_to_next_dealer()
|> reset_room_for_new_hand()
|> reset_players_for_new_hand()
|> set_blinds()
|> shuffle_new_deck()
|> deal_player_hole_cards()
|> set_first_player_turn()
end
def set_blinds(%Room{players: players} = room) do
{small_blind_player, big_blind_player} = find_next_seated_players_for_blinds(room)
new_players =
players
|> Map.update(small_blind_player.player_id, small_blind_player, fn player ->
Player.change(player,
current_bet: room.small_blind,
chips: player.chips - room.small_blind
)
end)
|> Map.update(big_blind_player.player_id, big_blind_player, fn player ->
Player.change(player,
current_bet: room.big_blind,
chips: player.chips - room.big_blind
)
end)
room
|> Room.change(current_round_max_bet: room.big_blind)
|> Room.change(players: new_players)
end
defp find_next_seated_players_for_blinds(%Room{dealer_player_id: dealer_player_id} = room) do
small_blind_player = next_seated_player(room, dealer_player_id)
big_blind_player =
case small_blind_player do
nil -> nil
player -> next_seated_player(room, player.player_id)
end
{small_blind_player, big_blind_player}
end
< br />
At the Showdown the first Player gets one more action.
# GameLogic
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)
|> end_hand()
end
end
Ready indicator for UI so show which players need to ready up.
# GameBoard
attr(:players, :map, required: true)
attr(:current_player_turn, :integer, required: true)
attr(:community_cards, :list, required: true)
attr(:current_viewer_id, :string, required: true)
def game_table(assigns) do
~H"""
<div class="space-y-6">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
<div class="text-center font-medium mb-3">Community Cards</div>
<.community_cards community_cards={@community_cards} />
</div>
<div class="space-y-4">
<%= for {_id, player} <- @players do %>
<.player_hand
player_id={player.player_id}
player_hand={player.hand}
current_player_turn={@current_player_turn}
show_hand={player.viewer_id == @current_viewer_id}
chips={player.chips}
current_bet={player.current_bet}
folded?={player.folded?}
ready?={player.ready?}
/>
<% end %>
</div>
</div>
"""
end
attr(:player_id, :integer, required: true)
attr(:player_hand, :list, required: true)
attr(:current_player_turn, :integer, required: false, default: nil)
attr(:show_hand, :boolean, required: true)
attr(:chips, :integer, required: true)
attr(:current_bet, :integer, required: true)
attr(:folded?, :boolean, required: true)
attr(:ready?, :boolean, required: true)
def player_hand(assigns) do
~H"""
<div class={[
"rounded-xl border bg-white p-4 shadow-sm",
@ready? && "border-red-500",
(not @ready? and @player_id == @current_player_turn) &&
"border-green-500 ring-2 ring-green-200",
(not @ready? and @player_id != @current_player_turn) &&
"border-gray-200"
]}>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="md:flex-1">
<div class="mb-2 font-medium text-left">
Player {@player_id}
<%= if @player_id == @current_player_turn do %>
<span class="ml-2 text-green-600">(Current Turn)</span>
<% end %>
<%= if @folded? do %>
<span class="ml-2 text-red-600">(Folded)</span>
<% end %>
</div>
<div class="flex justify-start">
<.card_row cards={@player_hand} total_slots={2} reveal?={@show_hand} />
</div>
</div>
<div class="min-w-[180px] rounded-lg bg-gray-50 p-3 text-sm text-gray-700 md:text-right">
<div class="font-semibold text-gray-900 mb-2">Player Stats</div>
<div>Chips: {@chips}</div>
<div>Current Bet: {@current_bet}</div>
</div>
</div>
</div>
"""
end
Add green border to the winning hand and show all cards
# Room
defstruct players: %{},
room_id: nil,
room_status: :waiting,
host_id: nil,
max_players: 6,
phase: :pre_flop,
deck: [],
community_cards: [],
small_blind: 50,
big_blind: 100,
current_player_turn: nil,
pot: 0,
current_hand_number: 0,
dealer_player_id: nil,
current_round_max_bet: 0,
winning_hand: nil,
winning_player_id: nil
@allowed_keys [
:players,
:room_id,
:room_status,
:host_id,
:max_players,
:phase,
:deck,
:community_cards,
:small_blind,
:big_blind,
:current_player_turn,
:pot,
:current_hand_number,
:dealer_player_id,
:current_round_max_bet,
:winning_hand,
:winning_player_id
]
def new(%Player{} = host, opts \\ []) do
host_id = host.player_id
%__MODULE__{
players: %{host_id => host},
room_id: Keyword.get(opts, :room_id),
room_status: Keyword.get(opts, :room_status, :waiting),
host_id: host_id,
max_players: Keyword.get(opts, :max_players, 6),
phase: Keyword.get(opts, :phase, :pre_flop),
deck: Keyword.get(opts, :deck, []),
community_cards: Keyword.get(opts, :community_cards, []),
small_blind: Keyword.get(opts, :small_blind, 50),
big_blind: Keyword.get(opts, :big_blind, 100),
current_player_turn: Keyword.get(opts, :current_player_turn, host_id),
pot: Keyword.get(opts, :pot, 0),
current_hand_number: Keyword.get(opts, :current_hand_number, 0),
dealer_player_id: Keyword.get(opts, :dealer_player_id, host_id),
current_round_max_bet: Keyword.get(opts, :current_round_max_bet, 0),
winning_hand: Keyword.get(opts, :winning_hand, nil),
winning_player_id: Keyword.get(opts, :winning_player_id, nil)
}
end
# GameLogic
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,
winning_player_id: winner_player_id
}
:error ->
room
end
end
defp reset_room_for_new_hand(%Room{} = room) do
Room.change(room,
phase: :pre_flop,
deck: [],
community_cards: [],
pot: 0,
current_hand_number: room.current_hand_number + 1,
current_round_max_bet: 0,
winning_hand: nil,
winning_player_id: nil
)
end
# GameBoard
attr(:players, :map, required: true)
attr(:current_player_turn, :integer, required: true)
attr(:community_cards, :list, required: true)
attr(:current_viewer_id, :string, required: true)
attr(:phase, :atom, required: true)
attr(:winning_player_id, :integer, required: true)
def game_table(assigns) do
~H"""
<div class="space-y-6">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
<div class="text-center font-medium mb-3">Community Cards</div>
<.community_cards community_cards={@community_cards} />
</div>
<div class="space-y-4">
<%= for {_id, player} <- @players do %>
<.player_hand
player_id={player.player_id}
player_hand={player.hand}
current_player_turn={@current_player_turn}
show_hand={player.viewer_id == @current_viewer_id or @phase == :showdown}
chips={player.chips}
current_bet={player.current_bet}
folded?={player.folded?}
ready?={player.ready?}
winning_player_id={@winning_player_id}
/>
<% end %>
</div>
</div>
"""
end
attr(:player_id, :integer, required: true)
attr(:player_hand, :list, required: true)
attr(:current_player_turn, :integer, required: false, default: nil)
attr(:show_hand, :boolean, required: true)
attr(:chips, :integer, required: true)
attr(:current_bet, :integer, required: true)
attr(:folded?, :boolean, required: true)
attr(:ready?, :boolean, required: true)
attr(:winning_player_id, :integer, required: true)
def player_hand(assigns) do
~H"""
<div class={player_border_class(assigns)}>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="md:flex-1">
<div class="mb-2 font-medium text-left">
Player {@player_id}
<%= if @player_id == @current_player_turn do %>
<span class="ml-2 text-green-600">(Current Turn)</span>
<% end %>
<%= if @folded? do %>
<span class="ml-2 text-red-600">(Folded)</span>
<% end %>
</div>
<div class="flex justify-start">
<.card_row cards={@player_hand} total_slots={2} reveal?={@show_hand} />
</div>
</div>
<div class="min-w-[180px] rounded-lg bg-gray-50 p-3 text-sm text-gray-700 md:text-right">
<div class="font-semibold text-gray-900 mb-2">Player Stats</div>
<div>Chips: {@chips}</div>
<div>Current Bet: {@current_bet}</div>
</div>
</div>
</div>
"""
end
defp player_border_class(assigns) do
base = "rounded-xl border bg-white p-4 shadow-sm"
cond do
assigns.player_id == assigns.winning_player_id ->
"#{base} border-green-600 ring-2 ring-green-300"
assigns.ready? ->
"#{base} border-red-500"
assigns.player_id == assigns.current_player_turn ->
"#{base} border-green-500 ring-2 ring-green-200"
true ->
"#{base} border-gray-200"
end
end
Add a marker to the current dealer. Also clean up the ScoreBoard so it shows less.
# MultiPokerLive
<GameBoard.game_table
players={@room.players}
current_player_turn={@room.current_player_turn}
community_cards={@room.community_cards}
current_viewer_id={@current_viewer_id}
phase={@room.phase}
winning_player_id={@room.winning_player_id}
dealer_player_id={@room.dealer_player_id}
/>
# GameBoard
attr(:players, :map, required: true)
attr(:current_player_turn, :integer, required: true)
attr(:community_cards, :list, required: true)
attr(:current_viewer_id, :string, required: true)
attr(:phase, :atom, required: true)
attr(:winning_player_id, :integer, required: true)
attr(:dealer_player_id, :integer, required: false, default: nil)
def game_table(assigns) do
~H"""
<div class="space-y-6">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
<div class="text-center font-medium mb-3">Community Cards</div>
<.community_cards community_cards={@community_cards} />
</div>
<div class="space-y-4">
<%= for {_id, player} <- @players do %>
<.player_hand
player_id={player.player_id}
player_hand={player.hand}
current_player_turn={@current_player_turn}
show_hand={
player.viewer_id == @current_viewer_id or
@phase == :showdown or
not is_nil(@winning_player_id)
}
chips={player.chips}
current_bet={player.current_bet}
folded?={player.folded?}
ready?={player.ready?}
winning_player_id={@winning_player_id}
dealer_player_id={@dealer_player_id}
/>
<% end %>
</div>
</div>
"""
end
attr(:player_id, :integer, required: true)
attr(:player_hand, :list, required: true)
attr(:current_player_turn, :integer, required: false, default: nil)
attr(:show_hand, :boolean, required: true)
attr(:chips, :integer, required: true)
attr(:current_bet, :integer, required: true)
attr(:folded?, :boolean, required: true)
attr(:ready?, :boolean, required: true)
attr(:winning_player_id, :integer, required: true)
attr(:dealer_player_id, :integer, required: false, default: nil)
def player_hand(assigns) do
~H"""
<div class={player_border_class(assigns)}>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="md:flex-1">
<div class="mb-2 font-medium text-left">
Player {@player_id}
<%= if @player_id == @dealer_player_id do %>
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-700">
Dealer
</span>
<% end %>
<%= if @player_id == @current_player_turn do %>
<span class="ml-2 text-green-600">(Current Turn)</span>
<% end %>
<%= if @folded? do %>
<span class="ml-2 text-red-600">(Folded)</span>
<% end %>
</div>
<div class="flex justify-start">
<.card_row cards={@player_hand} total_slots={2} reveal?={@show_hand} />
</div>
</div>
<div class="min-w-[180px] rounded-lg bg-gray-50 p-3 text-sm text-gray-700 md:text-right">
<div class="font-semibold text-gray-900 mb-2">Player Stats</div>
<div>Chips: {@chips}</div>
<div>Current Bet: {@current_bet}</div>
</div>
</div>
</div>
"""
end
attr(:phase, :atom, required: true)
attr(:current_player_turn, :integer, required: false, default: nil)
attr(:pot, :integer, required: true)
attr(:dealer_player_id, :integer, required: false, default: nil)
attr(:current_round_max_bet, :integer, required: true)
attr(:winning_hand, :map, required: false, default: nil)
def score_board(assigns) do
~H"""
<section class="bg-white shadow-md rounded-xl p-4 border border-gray-200">
<h2 class="text-lg font-semibold mb-4 text-center">Table Info</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div class="bg-gray-50 rounded-lg p-3">
<p class="text-sm text-gray-500">Phase</p>
<p class="text-base font-medium">{format_phase(@phase)}</p>
</div>
<div class="bg-gray-50 rounded-lg p-3">
<p class="text-sm text-gray-500">Pot</p>
<p class="text-base font-medium">{@pot}</p>
</div>
<div class="bg-gray-50 rounded-lg p-3">
<p class="text-sm text-gray-500">Current Round Max Bet</p>
<p class="text-base font-medium">{@current_round_max_bet}</p>
</div>
</div>
</section>
"""
end
Fold and All-in Logic
# Player
defstruct player_id: nil,
viewer_id: nil,
ready?: false,
chips: 1000,
current_bet: 0,
folded?: false,
seat_position: nil,
hand: [],
connected?: true,
waiting?: false,
total_contribution: 0
def new(player_id, viewer_id, opts \\ []) do
ready = Keyword.get(opts, :ready?, false)
chips = Keyword.get(opts, :chips, 1000)
current_bet = Keyword.get(opts, :current_bet, 0)
folded = Keyword.get(opts, :folded?, false)
seat_position = Keyword.get(opts, :seat_position, nil)
hand = Keyword.get(opts, :hand, [])
connected = Keyword.get(opts, :connected?, true)
waiting = Keyword.get(opts, :waiting?, false)
total_contribution = Keyword.get(opts, :total_contribution, 0)
%__MODULE__{
player_id: player_id,
viewer_id: viewer_id,
ready?: ready,
chips: chips,
current_bet: current_bet,
folded?: folded,
seat_position: seat_position,
hand: hand,
connected?: connected,
waiting?: waiting,
total_contribution: total_contribution
}
end
# GameLogic
# Add this line to any player_bet or all in
# total_contribution: player.total_contribution + amount,
defp maybe_advance_round(%Room{} = room) do
players = Map.values(room.players)
in_hand =
Enum.reject(players, & &1.folded?)
can_act =
Enum.reject(players, fn p -> p.folded? or p.chips == 0 end)
cond do
length(in_hand) <= 1 ->
end_hand_by_fold(room)
can_act == [] ->
auto_runout_to_end(room)
betting_round_complete?(room) ->
advance_phase_and_deal(room)
true ->
advance_to_next_player(room)
end
end
defp auto_runout_to_end(%Room{phase: :river} = room) do
room
|> Room.change(phase: :showdown)
|> end_hand()
end
defp auto_runout_to_end(%Room{} = room) do
room
|> advance_phase_and_deal()
|> auto_runout_to_end()
end
def end_hand_by_fold(%Room{} = room) do
room
|> resolve_winner_by_fold()
|> reset_players_to_not_ready()
|> Room.change(
room_status: :waiting,
current_player_turn: nil
)
end
def resolve_winner_by_fold(%Room{} = room) do
active_players =
room.players
|> Map.values()
|> Enum.reject(& &1.folded?)
case active_players do
[] ->
room
[%Player{player_id: winner_player_id} | _rest] ->
award_pot(room, winner_player_id, nil)
end
end
Redo the Logic for Restarts so Rooms Don’t Auto Restart
# Room
use GenServer, restart: :temporary
Add in a Badge to Show Which Player You Are
attr(:players, :map, required: true)
attr(:current_player_turn, :integer, required: true)
attr(:community_cards, :list, required: true)
attr(:current_viewer_id, :string, required: true)
attr(:phase, :atom, required: true)
attr(:winning_player_id, :integer, required: false, default: nil)
attr(:dealer_player_id, :integer, required: false, default: nil)
def game_table(assigns) do
~H"""
<div class="space-y-6">
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm">
<div class="text-center font-medium mb-3">Community Cards</div>
<.community_cards community_cards={@community_cards} />
</div>
<div class="space-y-4">
<%= for {_id, player} <- @players do %>
<.player_hand
player_id={player.player_id}
player_hand={player.hand}
current_player_turn={@current_player_turn}
show_hand={
player.viewer_id == @current_viewer_id or
@phase == :showdown or
not is_nil(@winning_player_id)
}
chips={player.chips}
current_bet={player.current_bet}
folded?={player.folded?}
ready?={player.ready?}
winning_player_id={@winning_player_id}
dealer_player_id={@dealer_player_id}
is_current_viewer={player.viewer_id == @current_viewer_id}
/>
<% end %>
</div>
</div>
"""
end
attr(:player_id, :integer, required: true)
attr(:player_hand, :list, required: true)
attr(:current_player_turn, :integer, required: false, default: nil)
attr(:show_hand, :boolean, required: true)
attr(:chips, :integer, required: true)
attr(:current_bet, :integer, required: true)
attr(:folded?, :boolean, required: true)
attr(:ready?, :boolean, required: true)
attr(:winning_player_id, :integer, required: false, default: nil)
attr(:dealer_player_id, :integer, required: false, default: nil)
attr(:is_current_viewer, :boolean, required: true)
def player_hand(assigns) do
~H"""
<div class={player_border_class(assigns)}>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div class="md:flex-1">
<div class="mb-2 font-medium text-left">
Player {@player_id}
<%= if @player_id == @dealer_player_id do %>
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-semibold text-blue-700">
Dealer
</span>
<% end %>
<%= if @is_current_viewer do %>
<span class="ml-2 inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-semibold text-indigo-700">
You
</span>
<% end %>
<%= if @player_id == @current_player_turn do %>
<span class="ml-2 text-green-600">(Current Turn)</span>
<% end %>
<%= if @folded? do %>
<span class="ml-2 text-red-600">(Folded)</span>
<% end %>
</div>
<div class="flex justify-start">
<.card_row cards={@player_hand} total_slots={2} reveal?={@show_hand} />
</div>
</div>
<div class="min-w-[180px] rounded-lg bg-gray-50 p-3 text-sm text-gray-700 md:text-right">
<div class="font-semibold text-gray-900 mb-2">Player Stats</div>
<div>Chips: {@chips}</div>
<div>Current Bet: {@current_bet}</div>
</div>
</div>
</div>
"""
end
Mark Players as Busted at the End of the Hand. Then Remove Them from the table at Hand Start. Also Remove the Ability to Ready Up.
# Player
defstruct player_id: nil,
viewer_id: nil,
ready?: false,
chips: 1000,
current_bet: 0,
folded?: false,
seat_position: nil,
hand: [],
connected?: true,
waiting?: false,
total_contribution: 0,
busted?: false
def new(player_id, viewer_id, opts \\ []) do
ready = Keyword.get(opts, :ready?, false)
chips = Keyword.get(opts, :chips, 1000)
current_bet = Keyword.get(opts, :current_bet, 0)
folded = Keyword.get(opts, :folded?, false)
seat_position = Keyword.get(opts, :seat_position, nil)
hand = Keyword.get(opts, :hand, [])
connected = Keyword.get(opts, :connected?, true)
waiting = Keyword.get(opts, :waiting?, false)
total_contribution = Keyword.get(opts, :total_contribution, 0)
busted = Keyword.get(opts, :busted?, false)
%__MODULE__{
player_id: player_id,
viewer_id: viewer_id,
ready?: ready,
chips: chips,
current_bet: current_bet,
folded?: folded,
seat_position: seat_position,
hand: hand,
connected?: connected,
waiting?: waiting,
total_contribution: total_contribution,
busted?: busted
}
end
# GameLogic
def start_hand(%Room{} = room) do
room
|> remove_busted_players()
|> advance_to_next_dealer()
|> reset_room_for_new_hand()
|> reset_players_for_new_hand()
|> set_blinds()
|> 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_and_busted()
|> Room.change(
room_status: :waiting,
current_player_turn: nil
)
end
def end_hand_by_fold(%Room{} = room) do
room
|> resolve_winner_by_fold()
|> reset_players_to_not_ready_and_busted()
|> Room.change(
room_status: :waiting,
current_player_turn: nil
)
end
def reset_players_to_not_ready_and_busted(%Room{players: players} = room) do
new_players =
Enum.into(players, %{}, fn {player_id, player} ->
updated_player =
Player.change(player,
ready?: false,
busted?: player.chips == 0
)
{player_id, updated_player}
end)
%Room{room | players: new_players}
end
defp can_start_hand?(%Room{room_status: :waiting, players: players}) do
eligible_players =
players
|> Map.values()
|> Enum.reject(& &1.busted?)
length(eligible_players) >= 2 and Enum.all?(eligible_players, & &1.ready?)
end
defp can_start_hand?(_state), do: false
defp remove_busted_players(%Room{players: players} = room) do
new_players =
players
|> Enum.reject(fn {_player_id, player} -> player.busted? end)
|> Map.new()
%Room{room | players: new_players}
end
# 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,
busted?: false
}
%Player{} = player ->
action_state =
cond do
player.busted? -> :busted
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?,
busted?: player.busted?
}
end
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 ->
case Map.get(state.players, player_id) do
%{busted?: true} ->
{:noreply, state}
_player ->
new_state =
state
|> GameLogic.mark_player_ready(player_id)
|> GameLogic.maybe_start_hand()
if new_state != state, do: PubSub.broadcast_room_updated(new_state)
{:noreply, new_state}
end
end
end
# GameBoard
attr(:game_state, :atom, required: true)
attr(:viewer_state, :map, required: true)
def player_ready(assigns) do
~H"""
<div class="flex justify-center">
<%= if @game_state == :waiting and
@viewer_state.action_state != :not_joined and
not @viewer_state.ready? and not @viewer_state.busted? do %>
<button phx-click="player-ready" class="px-4 py-2 bg-blue-600 text-white rounded">
Ready!
</button>
<% end %>
</div>
"""
end
Add in a Helper Button that Will Show the Rules.
defmodule GameSiteWeb.MultiPokerLive.InstructionHelper do
use Phoenix.Component
def helper_bubble(assigns) do
~H"""
<div id="poker-rules-help" phx-hook="HelpBubble" class="relative inline-block">
<button
type="button"
data-help-button
class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-200 text-sm font-bold text-gray-700 hover:bg-gray-300"
aria-label="Show game rules"
>
?
</button>
<div
data-help-panel
class="absolute left-1/2 top-full z-50 mt-2 w-80 -translate-x-1/2 rounded-xl border border-gray-200 bg-white p-4 shadow-lg"
>
<h3 class="mb-2 text-sm font-semibold text-gray-900">Poker Rules</h3>
<div class="space-y-2 text-sm text-gray-700">
<p>Each player is dealt 2 hole cards.</p>
<p>Betting happens across pre-flop, flop, turn, and river.</p>
<p>If all but one player folds, that player wins immediately.</p>
<p>If all remaining players are all-in, the board runs out automatically.</p>
<p>The best 5-card hand wins.</p>
<p>
If you bust, you can come back with 1000 chips after leaving the table or being removed.
</p>
</div>
</div>
</div>
"""
end
end
# MultiPokerLive
@impl true
def render(assigns) do
~H"""
<%= if @room == nil do %>
Room is loading...
<% else %>
<GameBoard.score_board
phase={@room.phase}
current_player_turn={@room.current_player_turn}
pot={@room.pot}
dealer_player_id={@room.dealer_player_id}
current_round_max_bet={@room.current_round_max_bet}
winning_hand={@room.winning_hand}
/>
<div class="ml-4">
<InstructionHelper.helper_bubble />
</div>
<GameBoard.game_table
players={@room.players}
current_player_turn={@room.current_player_turn}
community_cards={@room.community_cards}
current_viewer_id={@current_viewer_id}
phase={@room.phase}
winning_player_id={@room.winning_player_id}
dealer_player_id={@room.dealer_player_id}
/>
<GameBoard.player_actions
room_status={@room.room_status}
action_state={@viewer_state.action_state}
player_chips={@viewer_state.player_chips}
player_current_bet={@viewer_state.player_current_bet}
bet_amount={get_current_min_bet_needed(@room, @current_viewer_id)}
/>
<div column-2>
<GameBoard.join_game
viewer_state={@viewer_state}
room_status={@room.room_status}
room_full={room_full(@room)}
/>
<GameBoard.player_ready game_state={@room.room_status} viewer_state={@viewer_state} />
</div>
<% end %>
"""
end
#app.js
let Hooks = {}
Hooks.HelpBubble = {
mounted() {
const button = this.el.querySelector("[data-help-button]")
const panel = this.el.querySelector("[data-help-panel]")
if (!button || !panel) return
panel.style.display = "none"
const closePanel = (event) => {
if (!this.el.contains(event.target)) {
panel.style.display = "none"
}
}
const togglePanel = () => {
panel.style.display = panel.style.display === "none" ? "block" : "none"
}
button.addEventListener("click", togglePanel)
document.addEventListener("click", closePanel)
this.cleanup = () => {
button.removeEventListener("click", togglePanel)
document.removeEventListener("click", closePanel)
}
},
destroyed() {
this.cleanup?.()
}
}
Wow that is everything that I wanted from my Poker Game. Take some time and write exhaustive test for every module.