Secondary Process Registry
Get game state by join_code
I need to update how the game LiveView fetches the initial game state when the LV mounts so it uses a join_code
instead of a game_id
.
I first update the router path slugs to use /:join_code
instead of /:game_id
and update the mount
function to match this change.
To preserve the existing behavior, I map join_code
to game_id
which is used elsewhere in the mount function.
def mount(%{"join_code" => join_code}, _, socket) do
game_id = join_code
# ...
end
game_id
will still be passed to the GameEngine
module which will map to the game session processes so I will need to extract the id
from the game state and assign to the socket during mount.
To get the game state by join_code
, I will need to create a new function which will be used to replace the call to GameEngine.get_game(game_id)
.
I start by creating a new test in GameEngineTest
module which will be used to design and validate the behavior of this new function.
defmodule Minotaur.GameEngineTest do
# ...
describe "get_game_by_join_code/1" do
setup [:create_game, :start_game_session]
test "should return game state for matching code", ctx do
assert {:ok, game} = GameEngine.get_game_by_join_code(ctx.join_code)
assert ctx.game.id == game.id
end
end
end
I then iterate through running the test and resolving any errors until the test compiles.
I implement the setup helpers and add an empty get_game_by_join_code
function.
defmodule Minotaur.GameEngineTest do
# …
defp create_game(_) do
[join_code: "AABB", game: game_fixture()]
end
defp start_game_session(ctx) do
{:ok, _pid} = GameEngine.continue_game(ctx.join_code, ctx.game)
:ok
end
end
defmodule Minotaur.GameEngineTest do
# …
def get_game_by_join_code(_join_code) do
end
end
Once the test compiles, I continue iterating until the test is passing.
I follow the pattern of the body of get_game
which looks up the process by game_id
and update get_game_by_join_code
to instead get the game session from a different registry by join code.
def get_game(game_id) do
game = GenServer.call(via_tuple(game_id), :get_game)
{:ok, game}
catch
:exit, {:noproc, _} -> {:error, :game_not_alive}
end
def get_game_by_join_code(join_code) do
via_tuple = {:via, Horde.Registry, {SessionJoinCodeRegistry, join_code}}
game = GenServer.call(via_tuple, :get_game)
{:ok, game}
catch
:exit, {:noproc, _} -> {:error, :game_not_alive}
end
SessionJoinCodeRegistry
doesn’t exist so I define it as a child Registry process of the Minotaur.Application
module next.
I then update GameEngine.SessionServer
to register new game processes with this new registry during the init
callback.
def init({%Game{} = game, join_code}) do
Process.flag(:trap_exit, true)
Horde.Registry.register(SessionJoinCodeRegistry, join_code, self())
game_state = get_stashed_state(game.id, game)
{:ok, {game_state, join_code}, {:continue, :continue_game}}
end
The test is passing and I now have a function to look up game state by join code.
In the game LiveView mount
callback, I replace the call to get_game
with the new get_game_by_join_code
and use that game state result to set the socket assigns for :game_id
.
defmodule MinotaurWeb.GameLive do
# …
def mount(%{"join_code" => join_code}, _, socket) do
%{current_user: current_user} = socket.assigns
with {:ok, game} <- GameEngine.get_game_by_join_code(join_code),
{:ok, status} <- GameEngine.get_player_status(game, current_user.id) do
socket =
socket
|> assign(:status, status)
|> assign(:game_id, game.id)
join_game(socket, game, status)
else
{:error, :game_not_alive} ->
{:ok, push_navigate(assign(socket, :notice, :game_not_live), to: "/")}
{:error, :player_not_found} ->
{:ok, push_navigate(assign(socket, :notice, :player_not_found), to: "/")}
_ ->
{:ok, assign(socket, :notice, :unknown_mount_error)}
end
end
# …
end
This change should allow me to use different values for the game id
and the session join_code
which is what I’ll tackle next.
UUID for game id
I add the uuid
package to my project mix.exs
file under deps
and run mix deps.get
to install it.
defp deps do
[
# …
{:uuid, "~> 1.1"}
]
end
When a new game is created by calling GameEngine.create_game(join_code, users)
, a new Game
struct is created by the GameEngine.Session.create_game/2
function which is currently being passed the join_code
as its first argument assigning that value to the game id
.
def module Minotaur.GameEngine do
# …
def create_game(join_code, users) do
{:ok, game} = Session.create_game(join_code, users)
spec = SessionServer.child_spec(join_code: join_code, game_state: game)
Horde.DynamicSupervisor.start_child(SessionSupervisor, spec)
end
end
Since join_code
now has no relation to the Game
struct, I will remove the join_code
argument from GameEngine.Session.create_game
function definition and instead generate a unique game id each time the function is called.
I create a new test case to start working toward this desired behavior.
defmodule Minotaur.GameEngine.Session.CreateGameTest do
alias Minotaur.GameEngine.Session
describe "create_game/2 with users" do
setup [:create_users]
test "should create a unique game id", %{users: users} do
{:ok, game1} = Session.create_game("AAAA", users)
{:ok, game2} = Session.create_game("AAAA", users)
{:ok, game3} = Session.create_game("AAAA", users)
assert game1.id != game2.id
assert game1.id != game3.id
assert game2.id != game3.id
end
# …
end
end
Before breaking the existing function definition by removing the game_id
argument, I will first change the underlying behavior until the test passes.
Once the behavior is updated, I’ll refactor the function to remove the unused argument.
The test is currently failing since the game sessions are all using the same game_id
argument which is being set to the game id
value.
A simple change to the create_game
function allows the test to pass:
def create_game(_game_id, [_ | _] = users) do
game_id = UUID.uuid4()
world = generate_world()
players = create_players(users)
game = %Game{id: game_id, players: players, world: world}
{:ok, game}
end
Now that the test is passing, I remove the unused game_id
argument from the definition and from all callers of the function.
def module Minotaur.GameEngine do
# …
def create_game(join_code, users) do
{:ok, game} = Session.create_game(users)
spec = SessionServer.child_spec(join_code: join_code, game_state: game)
Horde.DynamicSupervisor.start_child(SessionSupervisor, spec)
end
end
These recent changes were all to make persisting game records in the database with a unique identifier.
I will next return to building the get_active_games_for_user
function that I was working on the other week.