Home Posts Post Search Tag Search

PokerLive - 04 - Create Room and Actual Room
Published on: 2026-03-30 Tags: elixir, Blog, Game Site, Phoenix, Poker, GenServer, Room, Supervisor Tree, Game Logic

Okay so I was doing some testing and I found that we had an issue where a user could make as many rooms as they wanted, so I wanted to add a way to test if a user had made a room and if so they would just be redirected to the room that they created.

# First set the new create room
def create_room(user_id) do
    case get_room_by_host(user_id) do
      {room_id, _pid} ->
        {:error, :already_has_room, room_id}

      nil ->
        room_id = UUID.generate()
        player = Player.new(user_id)

        case DynamicSupervisor.start_child(
               @room_supervisor,
               {Room, %{room_id: room_id, host: player}}
             ) do
          {:ok, _pid} -> {:ok, room_id}
          error -> error
        end
    end
  end

defp get_room_by_host(user_id) do
    Registry.select(@registry, [
      {
        {:"$1", :"$2", :"$3"},
        [],
        [{{:"$1", :"$2"}}]
      }
    ])
    |> Enum.find(fn {_room_id, pid} ->
      case Room.get_state(pid) do
        %{host_id: ^user_id} -> true
        _ -> false
      end
    end)
  end

# Next I needed to use the new way of creating a room
  def start_link(%{room_id: room_id} = attrs) do
    GenServer.start_link(
      __MODULE__,
      attrs,
      name: via(room_id)
    )
  end

  @impl true
  def init(%{host: host, room_id: room_id}) do
    initial_state = new(host, room_id: room_id)
    {:ok, initial_state}
  end

# Lastly I needed to use the new logic for the  "create_room" event.
  @impl true
  def handle_event("create_room", _params, socket) do
    current_user = socket.assigns.current_user

    {socket, room_id} =
      case MultiPoker.create_room(current_user.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

Okay that is a great start to the day let’s move onto the Room and tying that to the Game Logic. We should start by adding in a few more fields to the Room struct.

  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),

      # game state
      phase: Keyword.get(opts, :phase, :pre_flop),
      deck: Keyword.get(opts, :deck, []),
      community_cards: Keyword.get(opts, :community_cards, []),

      # blinds
      small_blind: Keyword.get(opts, :small_blind, 10),
      big_blind: Keyword.get(opts, :big_blind, 20),

      # flow
      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 update_room(pid, opts) do
    GenServer.cast(pid, {:update_room, opts})
  end

  @impl true
  def handle_cast({:update_room, opts}, %__MODULE__{} = state) do
    {:noreply, change(state, opts)}
  end

Okay so now that we have the new keys and defaults, along with the functions to set and update those values, we can move onto the way in which we will update the room. There will be a few files that I create and Ill be sure to add in a comment above the code so you know what I made, if the module name isn’t enough to figure it out.

defmodule GameSite.MultiPoker.Deck do
  @suits ["spades", "clubs", "diamonds", "hearts"]
  @ranks [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

  def create_deck do
    for rank <- @ranks, suit <- @suits do
      {rank, suit}
    end
  end

  def shuffle_cards(deck) do
    Enum.shuffle(deck)
  end

  def choose_n_cards(deck, n) do
    [dealt_cards, deck] =
      Enum.reduce(1..n, [[], deck], fn _, [chosen, remaining] ->
        {card, new_remaining} = List.pop_at(remaining, 0)
        [[card | chosen], new_remaining]
      end)

    [dealt_cards, deck]
  end
end

# room.ex
  alias GameSite.MultiPoker.GameLogic

  def start_hand(pid) do
    GenServer.cast(pid, :start_hand)
  end

  @impl true
  def handle_cast(:start_hand, state) do
    {:noreply, GameSite.MultiPoker.GameLogic.start_hand(state)}
  end

  def advance_phase_and_deal(pid) do
    GenServer.cast(pid, :advance_phase_and_deal)
  end

  @impl true
  def handle_cast(:advance_phase_and_deal, %__MODULE__{} = state) do
    {:noreply, GameLogic.advance_phase_and_deal(state)}
  end

  def player_bet(pid, player_id, amount) do
    GenServer.cast(pid, {:player_bet, player_id, amount})
  end

  @impl true
  def handle_cast({:player_bet, player_id, amount}, state) do
    {:noreply, GameLogic.player_bet(state, player_id, amount)}
  end

  def player_fold(pid, player_id) do
    GenServer.cast(pid,{:player_fold, player_id})
  end

  @impl true
  def handle_cast({:player_fold, player_id}, %__MODULE__{} = state) do
    {:noreply, GameLogic.player_fold(state, player_id)}
  end

# GameLogic
defmodule GameSite.MultiPoker.GameLogic do
  alias GameSite.MultiPoker.{Room, Player, Deck}

  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
      :pre_flop -> deal_community_cards(room, 3, :flop)
      :flop -> deal_community_cards(room, 1, :turn)
      :turn -> deal_community_cards(room, 1, :river)
      :river -> Room.change(room, phase: :showdown)
    end
  end

  def player_bet(%Room{current_player_turn: current_player_turn} = room, player_id, amount)
      when current_player_turn != player_id or amount <= 0 do
    room
  end

  def player_bet(%Room{players: players, pot: pot} = room, player_id, amount) do
    case Map.fetch(players, player_id) do
      {:ok, player} ->
        updated_player =
          Player.change(player,
            current_bet: player.current_bet + amount,
            chips: player.chips - amount
          )

        new_players = Map.put(players, player_id, updated_player)

        %Room{
          room
          | players: new_players,
            pot: pot + amount
        }
        |> advance_to_next_player()

      :error ->
        room
    end
  end

  def player_fold(%Room{current_player_turn: current_player_turn} = room, player_id)
      when current_player_turn != player_id do
    room
  end

  def player_fold(%Room{players: players} = room, player_id) do
    case Map.fetch(players, player_id) do
      {:ok, player} ->
        updated_player = Player.change(player, folded?: true)
        new_players = Map.put(players, player_id, updated_player)

        %Room{room | players: new_players}
        |> advance_to_next_player()

      :error ->
        room
    end
  end

  defp deal_community_cards(%Room{deck: deck, community_cards: board} = room, n, next_phase) do
    [cards, new_deck] = Deck.choose_n_cards(deck, n)
    ordered_cards = Enum.reverse(cards)

    Room.change(room,
      deck: new_deck,
      community_cards: board ++ ordered_cards,
      phase: next_phase
    )
  end

  defp reset_room_for_new_hand(%Room{} = room) do
    Room.change(room,
      phase: :pre_flop,
      deck: [],
      community_cards: [],
      current_player_turn: nil,
      pot: 0,
      current_hand_number: room.current_hand_number + 1
    )
  end

  defp reset_players_for_new_hand(%Room{players: players} = room) do
    new_players =
      Enum.into(players, %{}, fn {player_id, player} ->
        updated_player =
          Player.change(player,
            ready?: false,
            current_bet: 0,
            folded?: false,
            hand: []
          )

        {player_id, updated_player}
      end)

    %Room{room | players: new_players}
  end

  defp shuffle_new_deck(%Room{} = room) do
    shuffled_deck =
      Deck.create_deck()
      |> Deck.shuffle_cards()

    %Room{room | deck: shuffled_deck}
  end

  defp deal_player_hole_cards(%Room{deck: deck, players: players} = room) do
    ordered_players =
      players
      |> Map.values()
      |> Enum.sort_by(& &1.seat_position)

    {new_deck, new_players} =
      Enum.reduce(ordered_players, {deck, %{}}, fn player, {deck_acc, players_acc} ->
        [new_cards, next_deck] = Deck.choose_n_cards(deck_acc, 2)

        updated_player =
          Player.change(player, hand: Enum.reverse(new_cards))

        {next_deck, Map.put(players_acc, player.player_id, updated_player)}
      end)

    %Room{room | deck: new_deck, players: new_players}
  end

  defp set_first_player_turn(%Room{dealer_player_id: dealer_player_id} = room) do
    case next_seated_player(room, dealer_player_id) do
      nil ->
        room

      next_player ->
        Room.change(room, current_player_turn: next_player.player_id)
    end
  end

  defp advance_to_next_player(%Room{current_player_turn: current_player_turn} = room) do
    case next_active_player(room, current_player_turn) do
      nil ->
        room

      next_player ->
        Room.change(room, current_player_turn: next_player.player_id)
    end
  end

  defp advance_to_next_dealer(%Room{dealer_player_id: dealer_player_id} = room) do
    case next_seated_player(room, dealer_player_id) do
      nil ->
        room

      next_player ->
        Room.change(room, dealer_player_id: next_player.player_id)
    end
  end

  defp ordered_players(%Room{players: players}) do
    players
    |> Map.values()
    |> Enum.sort_by(& &1.seat_position)
  end

  defp next_active_player(%Room{} = room, current_player_id) do
    players = ordered_players(room)

    case Enum.find_index(players, fn player -> player.player_id == current_player_id end) do
      nil ->
        Enum.find(players, fn player -> not player.folded? end)

      current_index ->
        players
        |> rotate_after(current_index)
        |> Enum.find(fn player -> not player.folded? end)
    end
  end

  defp next_seated_player(%Room{} = room, current_player_id) do
    players = ordered_players(room)

    case Enum.find_index(players, fn player -> player.player_id == current_player_id end) do
      nil ->
        List.first(players)

      current_index ->
        players
        |> rotate_after(current_index)
        |> List.first()
    end
  end

  defp rotate_after(players, index) do
    {left, right} = Enum.split(players, index + 1)
    right ++ left
  end
end

Well that is a lot to go over. We added in a lot of functionality and we had to update the Room Struct to better reflect the states that we want to maintain. Changing from :dealer_position to :dealer_player_id so that we can move through players correctly. We also added in some functionality for the advancing players after a fold or a bet so that we can keep only the players that need to bet in the game.


For the Room we needed to add in a function that will be able to call the right cast or call to make sure that the Room state is updated within the GenServer.


Lastly we needed to have the logic for Deck to shuffle, create_deck and choose_n_cards.2. This was an easier module as you just need to describe the Rank and Suit and let for and Enum do the work for you.


I would take some time and add some testing for all these modules.

start_hand/1 deals 2 cards per player
flop adds 3 community cards
turn adds 1
river adds 1
only current player can bet/fold
betting reduces chips and increases pot
fold advances turn
dealer advances correctly