We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Okay so now I want to work on all the Components and the initial display for the MultiPokerLive liveview. This will involve us getting the Room from the GenServer that is named within the params.
First I want to work on a new plug that will allow a user to not be logged in so long as a logged in user has made a room. For this I want both Logged-in users and Guests to be able to have a guest_id that will be stored within the session.
# game_site_web/plugs/ensure_guest_id.ex
defmodule GameSiteWeb.Plugs.EnsureGuestId do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if get_session(conn, :guest_id) do
conn
else
put_session(conn, :guest_id, System.unique_integer([:positive]))
end
end
end
# Now add that to the pipeline game_site_web/router.ex
...
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {GameSiteWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
# This line here.
plug GameSiteWeb.Plugs.EnsureGuestId
plug :fetch_current_user
end
...
With that set we need to now make a few changes to the code that we have within the Room, because right now we are letting the liveview set all the Player information we want to just pass in the current_viewer_id and then let the GenServer take care of the rest.
defmodule GameSite.MultiPoker.Player do
defstruct player_id: nil,
viewer_id: nil,
ready?: false,
chips: 1000,
current_bet: 0,
folded?: false,
seat_position: nil,
hand: [],
connected?: true
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)
%__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
}
end
def change(%__MODULE__{} = player, opts \\ []) do
updates = Enum.into(opts, %{})
struct(player, updates)
end
def set_ready(%__MODULE__{} = player, ready) do
%__MODULE__{player | ready?: ready}
end
end
defmodule GameSite.MultiPoker.Room do
use GenServer
alias GameSite.MultiPoker.{GameLogic, Player}
defstruct players: %{},
room_id: nil,
room_status: :waiting,
host_id: nil,
max_players: 6,
phase: :pre_flop,
deck: [],
community_cards: [],
small_blind: 10,
big_blind: 20,
current_player_turn: nil,
pot: 0,
current_hand_number: 0,
dealer_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
]
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, 10),
big_blind: Keyword.get(opts, :big_blind, 20),
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)
}
end
def change(%__MODULE__{} = room, opts) do
valid_opts =
Enum.filter(opts, fn {key, _value} ->
key in @allowed_keys
end)
struct(room, valid_opts)
end
def start_link(%{room_id: room_id} = attrs) do
GenServer.start_link(__MODULE__, attrs, name: via(room_id))
end
# viewer-facing actions
def add_player(pid, viewer_id) do
GenServer.call(pid, {:add_player, viewer_id})
end
def player_fold(pid, viewer_id) do
GenServer.cast(pid, {:player_fold, viewer_id})
end
def player_bet(pid, viewer_id, amount) do
GenServer.cast(pid, {:player_bet, viewer_id, amount})
end
def remove_player(pid, viewer_id) do
GenServer.cast(pid, {:remove_player, viewer_id})
end
def update_player(pid, viewer_id, opts) do
GenServer.cast(pid, {:update_player, viewer_id, opts})
end
@impl true
def handle_cast({:player_fold, viewer_id}, %__MODULE__{} = state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
{:noreply, GameLogic.player_fold(state, player_id)}
end
end
@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 ->
{:noreply, GameLogic.player_bet(state, player_id, amount)}
end
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 ->
new_players = Map.delete(state.players, player_id)
{:noreply, %__MODULE__{state | players: new_players}}
end
end
@impl true
def handle_cast({:update_player, viewer_id, opts}, %__MODULE__{} = state) do
case find_player_id_by_viewer_id(state, viewer_id) do
nil ->
{:noreply, state}
player_id ->
new_players =
case Map.fetch(state.players, player_id) do
{:ok, player} ->
updated_player = Player.change(player, opts)
Map.put(state.players, player_id, updated_player)
:error ->
state.players
end
{:noreply, %__MODULE__{state | players: new_players}}
end
end
@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)
{:reply, {:ok, player}, %__MODULE__{state | players: new_players}}
end
player_id ->
{:reply, {:ok, Map.fetch!(state.players, player_id)}, state}
end
end
def viewer_state(%__MODULE__{} = room, current_viewer_id) do
case get_player_by_viewer_id_from_room(room, current_viewer_id) do
nil ->
%{
action_state: :not_joined,
player_chips: 0,
player_id: nil
}
%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_id: player.player_id
}
end
end
defp find_player_id_by_viewer_id(%__MODULE__{} = state, viewer_id) do
state.players
|> Map.values()
|> Enum.find_value(fn player ->
if player.viewer_id == viewer_id, do: player.player_id
end)
end
defp next_player_id(%__MODULE__{} = state) do
used_ids = Map.keys(state.players)
Enum.find(1..state.max_players, fn id -> id not in used_ids end)
end
end
defmodule GameSite.MultiPoker do
alias GameSite.MultiPoker.{Player, Room}
alias Ecto.UUID
@room_supervisor GameSite.MultiPoker.RoomSupervisor
@registry GameSite.MultiPoker.RoomRegistry
def create_room(viewer_id) do
case get_room_by_host(viewer_id) do
{room_id, _pid} ->
{:error, :already_has_room, room_id}
nil ->
room_id = UUID.generate()
host_player = Player.new(1, viewer_id)
case DynamicSupervisor.start_child(
@room_supervisor,
{Room, %{room_id: room_id, host: host_player}}
) do
{:ok, _pid} -> {:ok, room_id}
error -> error
end
end
end
end
defmodule GameSiteWeb.MultiPokerLive.Lobby do
...
@impl true
def mount(_params, session, socket) do
socket =
socket
|> set_current_viewer_id(session)
rooms =
if connected?(socket) do
list_room_summaries()
else
[]
end
{:ok, 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
{:noreply, redirect(socket, to: "/multi-poker/#{room_id}")}
end
def set_current_viewer_id(%{assigns: %{current_user: current_user}} = socket, _session)
when not is_nil(current_user) do
assign(socket, :current_viewer_id, "user:#{current_user.id}")
end
end
With this you will now need to be sure that with any GameLogic you will pass the viewer_id and then convert it to the player_id. Okay so now that we have that we can allow a user to have something that is set by the site for any guest or logged in user so that we can use that to assign the current_viewer_id for any user. Let’s start to build the game_board.ex
defmodule GameSiteWeb.MultiPokerLive.GameBoard do
use GameSiteWeb, :live_view
use Phoenix.Component
attr(:phase, :atom, required: true)
attr(:current_player_turn, :integer, required: true)
attr(:pot, :integer, required: true)
attr(:dealer_player_id, :integer, required: true)
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-2 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 Turn</p>
<p class="text-base font-medium">
{player_label(@current_player_turn)}
</p>
</div>
<div class="bg-gray-50 rounded-lg p-3">
<p class="text-sm text-gray-500">Dealer</p>
<p class="text-base font-medium">
{player_label(@dealer_player_id)}
</p>
</div>
</div>
</section>
"""
end
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">
<%= 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}
/>
<% end %>
<.community_cards community_cards={@community_cards} />
</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)
def player_hand(assigns) do
~H"""
<div class="space-y-2">
<div class="text-center font-medium">
Player {@player_id}
<%= if @player_id == @current_player_turn do %>
<span class="ml-2 text-green-600">(Current Turn)</span>
<% end %>
</div>
<.card_row cards={@player_hand} total_slots={2} reveal?={@show_hand} />
</div>
"""
end
attr(:community_cards, :list, required: true)
def community_cards(assigns) do
~H"""
<div class="space-y-2">
<div class="text-center font-medium">Community Cards</div>
<.card_row cards={@community_cards} total_slots={5} reveal?={true} />
</div>
"""
end
attr(:cards, :list, required: true)
attr(:total_slots, :integer, required: true)
attr(:reveal?, :boolean, required: true)
def card_row(assigns) do
~H"""
<div class="flex flex-wrap justify-center gap-4 min-h-[7rem] md:min-h-[8rem]">
<%= for card <- fill_cards(@cards, @total_slots) do %>
<div class="flex flex-col items-center">
<%= cond do %>
<% card && @reveal? -> %>
<img
src={card_image_url(card)}
alt={card_to_string(card)}
class="w-20 h-28 border rounded shadow transition"
/>
<div class="mt-1 text-sm text-center">
{card_to_string(card)}
</div>
<% true -> %>
<img
src={card_back()}
alt="Hidden card"
class="w-20 h-28 border rounded shadow transition"
/>
<% end %>
</div>
<% end %>
</div>
"""
end
attr(:action_state, :atom, required: true)
attr(:player_chips, :integer, required: true)
attr(:bet_amount, :integer, required: false, default: 0)
def player_actions(assigns) do
~H"""
<%= if @action_state == :your_turn do %>
<.player_controls player_chips={@player_chips} bet_amount={@bet_amount} disabled={false} />
<% else %>
<.player_controls player_chips={@player_chips} bet_amount={@bet_amount} disabled={true} />
<% end %>
"""
end
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">
<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="button"
phx-click="player-bet"
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>
</div>
"""
end
defp button_class(base, true) do
"#{base} opacity-50 cursor-not-allowed pointer-events-none rounded-lg px-4 py-2 text-white font-medium shadow"
end
defp button_class(base, false) do
"#{base} rounded-lg px-4 py-2 text-white font-medium shadow transition"
end
defp format_phase(phase) do
phase
|> Atom.to_string()
|> String.replace("_", " ")
|> String.split()
|> Enum.map_join(" ", &String.capitalize/1)
end
defp player_label(nil), do: "—"
defp player_label(player_id), do: "Player #{player_id}"
defp card_image_url({rank, suit}) do
"/images/cards/#{suit}_#{rank}.png"
end
defp card_to_string({rank, suit}), do: "#{rank} of #{String.capitalize(suit)}"
defp card_back(), do: "/images/logo.svg"
defp fill_cards(cards, total_slots) do
cards
|> Enum.take(total_slots)
|> then(fn trimmed ->
trimmed ++ List.duplicate(nil, total_slots - length(trimmed))
end)
end
end
Along with the above code I needed to add a way to get a Player from the viewer_id so I added in this function set to the Room
def get_player_by_viewer_id(pid, viewer_id) do
GenServer.call(pid, {:get_player_by_viewer_id, viewer_id})
end
@impl true
def handle_call({:get_player_by_viewer_id, viewer_id}, _from, %__MODULE__{} = state) do
player =
state.players
|> Map.values()
|> Enum.find(fn player -> player.viewer_id == viewer_id end)
{:reply, player, state}
end
That was a decent amount of work so I’ll call it there next time we need to add in all the functions for dealing with all the actions and then start to work on PubSub to keep every subscriber updated on any changes to the Room while we move from action to action.