Home Posts Post Search Tag Search

PokerLive - 02 - Supervisor Tree for Room
Published on: 2026-03-27 Tags: elixir, Testing, LiveView, Game Site, Phoenix, Poker, GenServer, Player, Room

First we need to have the application set up the names of the Supervisors that we will call by name later in the MultiPoker game. These will be responsible for managing the Rooms and the Players.


Be sure to add something like this to your application.ex

  def start(_type, _args) do
    children = [
      ...
      {Registry, keys: :unique, name: GameSite.MultiPoker.RoomRegistry},
      {DynamicSupervisor, strategy: :one_for_one, name: GameSite.MultiPoker.RoomSupervisor},
      ...
      GameSiteWeb.Endpoint
    ]
   ...
  end

Okay with that out of the way we can work on the creation and naming of the rooms.

defmodule GameSite.MultiPoker do
  alias GameSite.MultiPoker.{Room, Player}

  @room_supervisor GameSite.MultiPoker.RoomSupervisor
  @registry GameSite.MultiPoker.RoomRegistry

  def create_room(user_id) do
    room_id = Ecto.UUID.generate()
    player = Player.new(user_id)

    case start_room(room_id, player) do
      {:ok, _pid} -> {:ok, room_id}
      {:error, reason} -> {:error, reason}
    end
  end

  def get_room_pid(room_id) do
    case Registry.lookup(@registry, room_id) do
      [{pid, _}] -> {:ok, pid}
      [] -> :error
    end
  end

  def get_room(room_id) do
    case get_room_pid(room_id) do
      {:ok, pid} -> {:ok, Room.get_state(pid)}
      :error -> :error
    end
  end

  defp start_room(room_id, host_player) do
    spec = %{
      id: room_id,
      start: {Room, :start_link, [host_player, [room_id: room_id]]}
    }

    DynamicSupervisor.start_child(@room_supervisor, spec)
  end
end

Before I go onto the testing I want to note that currently we are not naming the Rooms when we create the GenServer so we need to go back to the Room context and make sure that we have a way of associating that with the correct Supervisor.

  def start_link(host, opts \\ []) do
    room_id = Keyword.fetch!(opts, :room_id)

    GenServer.start_link(
      __MODULE__,
      {host, opts},
      name: via(room_id)
    )
  end

  defp via(room_id) do
    {:via, Registry, {GameSite.MultiPoker.RoomRegistry, room_id}}
  end

Okay lets go into some testing!!!

# First let's start an iex session with a Erlang node name
iex --sname game_site -S mix phx.server
# Then in an other terminal run this too connect
iex --sname debug --remsh game_site@$(hostname)
# This should get us up and running

Okay so we have a terminal running and one connected let’s start the testing

iex(game_site@DESKTOP-ANBE9HK)1> Node.self()
:"game_site@DESKTOP-ANBE9HK"
iex(game_site@DESKTOP-ANBE9HK)2> {:ok, room_id} = GameSite.MultiPoker.create_room(123)
{:ok, "b3ef3892-5143-4b90-a323-99d63ced50a6"}
iex(game_site@DESKTOP-ANBE9HK)3> {:ok, pid} = GameSite.MultiPoker.get_room_pid(room_id)
{:ok, #PID<0.959.0>}
iex(game_site@DESKTOP-ANBE9HK)4> {:ok, room} = GameSite.MultiPoker.get_room(room_id)
{:ok,
 %GameSite.MultiPoker.Room{
   players: %{
     123 => %GameSite.MultiPoker.Player{
       player_id: 123,
       ready?: false,
       chips: 1000,
       current_bet: 0,
       folded?: false,
       seat_position: nil,
       hand: [],
       connected?: true
     }
   },
   room_id: "b3ef3892-5143-4b90-a323-99d63ced50a6",
   room_status: :waiting,
   host_id: 123,
   full: false
 }}
iex(game_site@DESKTOP-ANBE9HK)5> room.room_id
"b3ef3892-5143-4b90-a323-99d63ced50a6"
iex(game_site@DESKTOP-ANBE9HK)6> room.host_id
123
iex(game_site@DESKTOP-ANBE9HK)7> room.players
%{
  123 => %GameSite.MultiPoker.Player{
    player_id: 123,
    ready?: false,
    chips: 1000,
    current_bet: 0,
    folded?: false,
    seat_position: nil,
    hand: [],
    connected?: true
  }
}
iex(game_site@DESKTOP-ANBE9HK)8> room.players[123]
%GameSite.MultiPoker.Player{
  player_id: 123,
  ready?: false,
  chips: 1000,
  current_bet: 0,
  folded?: false,
  seat_position: nil,
  hand: [],
  connected?: true
}
iex(game_site@DESKTOP-ANBE9HK)9> player2 = GameSite.MultiPoker.Player.new(456)
%GameSite.MultiPoker.Player{
  player_id: 456,
  ready?: false,
  chips: 1000,
  current_bet: 0,
  folded?: false,
  seat_position: nil,
  hand: [],
  connected?: true
}
iex(game_site@DESKTOP-ANBE9HK)10> GameSite.MultiPoker.Room.add_player(pid, player2)
:ok
iex(game_site@DESKTOP-ANBE9HK)11> {:ok, room} = GameSite.MultiPoker.get_room(room_id)
{:ok,
 %GameSite.MultiPoker.Room{
   players: %{
     123 => %GameSite.MultiPoker.Player{
       player_id: 123,
       ready?: false,
       chips: 1000,
       current_bet: 0,
       folded?: false,
       seat_position: nil,
       hand: [],
       connected?: true
     },
     456 => %GameSite.MultiPoker.Player{
       player_id: 456,
       ready?: false,
       chips: 1000,
       current_bet: 0,
       folded?: false,
       seat_position: nil,
       hand: [],
       connected?: true
     }
   },
   room_id: "b3ef3892-5143-4b90-a323-99d63ced50a6",
   room_status: :waiting,
   host_id: 123,
   full: false
 }}
iex(game_site@DESKTOP-ANBE9HK)12> room.players
%{
  123 => %GameSite.MultiPoker.Player{
    player_id: 123,
    ready?: false,
    chips: 1000,
    current_bet: 0,
    folded?: false,
    seat_position: nil,
    hand: [],
    connected?: true
  },
  456 => %GameSite.MultiPoker.Player{
    player_id: 456,
    ready?: false,
    chips: 1000,
    current_bet: 0,
    folded?: false,
    seat_position: nil,
    hand: [],
    connected?: true
  }
}
iex(game_site@DESKTOP-ANBE9HK)13> GameSite.MultiPoker.Room.remove_player(pid, player2)
:ok
iex(game_site@DESKTOP-ANBE9HK)14> {:ok, room} = GameSite.MultiPoker.get_room(room_id)
{:ok,
 %GameSite.MultiPoker.Room{
   players: %{
     123 => %GameSite.MultiPoker.Player{
       player_id: 123,
       ready?: false,
       chips: 1000,
       current_bet: 0,
       folded?: false,
       seat_position: nil,
       hand: [],
       connected?: true
     }
   },
   room_id: "b3ef3892-5143-4b90-a323-99d63ced50a6",
   room_status: :waiting,
   host_id: 123,
   full: false
 }}
iex(game_site@DESKTOP-ANBE9HK)15> room.players
%{
  123 => %GameSite.MultiPoker.Player{
    player_id: 123,
    ready?: false,
    chips: 1000,
    current_bet: 0,
    folded?: false,
    seat_position: nil,
    hand: [],
    connected?: true
  }
}
iex(game_site@DESKTOP-ANBE9HK)16> GameSite.MultiPoker.Room.update_status(pid, :ready)
:ok
iex(game_site@DESKTOP-ANBE9HK)17> {:ok, room} = GameSite.MultiPoker.get_room(room_id)
{:ok,
 %GameSite.MultiPoker.Room{
   players: %{
     123 => %GameSite.MultiPoker.Player{
       player_id: 123,
       ready?: false,
       chips: 1000,
       current_bet: 0,
       folded?: false,
       seat_position: nil,
       hand: [],
       connected?: true
     }
   },
   room_id: "b3ef3892-5143-4b90-a323-99d63ced50a6",
   room_status: :ready,
   host_id: 123,
   full: false
 }}
iex(game_site@DESKTOP-ANBE9HK)18> room.room_status
:ready

Let’s go over what we did there:

We first stared a Server with a named node.
Then we used an other terminal to connect to that node.

Once we were in the iex session we:
Create a room
Find the pid
Read room state
   Then inspect
Verify the host player is stored correctly
Test adding a player manually
   Create another player:
   Then add them to the room:
   Then re-read state:
Test removing a player
Test room status update

You should be able to work with all of this to see what other things you can do. Let’s look into changing a player within a room. Right now the only way to do this would be to remove the player from with remove_player the room and then add them back into the room with add_player. So let’s take a minute to add that to the GenServer

  def handle_cast({:update_player, player_id, opts}, %__MODULE__{players: players} = state) do
    new_players =
      case Map.fetch(players, player_id) do
        {:ok, player} ->
          updated_player = Player.change(player, opts)
          Map.put(players, player_id, updated_player)

        :error ->
          players
      end

    {:noreply, %__MODULE__{state | players: new_players}}
  end

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

Quick note on this I choose to only pass the player.id so that we can keep as little being passed around. Some might argue that you should pass the entire struct but you might not need to as you only need the player.id to removed the player as that is what we have them stored under. Okay so let’s test it.

iex(game_site@DESKTOP-ANBE9HK)24> GameSite.MultiPoker.Room.update_player(pid, 123, %{chips: 200, ready?: false})
:ok
iex(game_site@DESKTOP-ANBE9HK)25> GameSite.MultiPoker.Room.get_state(pid)
%GameSite.MultiPoker.Room{
  players: %{
    123 => %GameSite.MultiPoker.Player{
      player_id: 123,
      ready?: false,
      chips: 200,
      current_bet: 0,
      folded?: false,
      seat_position: nil,
      hand: [],
      connected?: true
    }
  },
  room_id: "b3ef3892-5143-4b90-a323-99d63ced50a6",
  room_status: :ready,
  host_id: 123,
  full: false
}

I would take some time now and add some test for the current modules that you have. Things like did the Player get created correctly? Did the Player change as it should. For the Room create a setup that creates the Room and then test it was created with the right values, then change and see what happens. Lastly for the MultiPoker make sure that you can create the room with the create_room/1 and then test to see that you get the right errors for bad params.