The project: Scrumchkin Online
About a year ago I created a card game to teach Scrum: Scrumchkin. The game made the learning process more fun and was adopted by Scrum Trainers from several countries, until the pandemic made any in-person class unfeasible.
And that’s where my personal project came from: creating an online version of Scrumchkin. Which would be a great opportunity to play and learn more about Phoenix Liveview.
Initially, I thought of the following structure for the project:
This way, it would be possible to create games in separate processes and have a registry with unique identifiers for each game so that each match could be accessed through a different URL.
Example:
-
The user accesses the URL
http://scrumchkin.com/game/abc123 -
The web application asks the Game Registry where game
abc123is -
The Game Registry finds the PID of the match and returns it to the web application
The Game Registry as a library
Keeping in mind the single responsibility principle, the design above makes evident the existence of 3 different projects: The Game Registry, the Game Server and the Web Interface.
The next paragraphs will talk about some technical aspects of Elixir as a curiosity. If you just want to understand the difference between a library and an OTP application feel free to skip this part :)
Technically the Game Registry is extremely simple: it links a unique ID to a match. It’s basically a dictionary that has a UUID as key and a PID of a GenServer as value for a match.
Initially, I created the Game Registry as a library capable of performing CRUD operations on an ets table:
defmodule GameRegister do
def init() do
:ets.new(:scrumchkin, [:set, :public, :named_table])
end
def save(value) do
key = UUID.uuid1()
:ets.insert_new(:scrumchkin, {key, value})
key
end
def delete(key) do
:ets.delete(:scrumchkin, key)
end
def get(key) do
:scrumchkin
|> :ets.lookup(key)
|> format_result
end
def list_all do
:ets.tab2list(:scrumchkin)
end
defp format_result([]), do: {:error, "Game not found"}
defp format_result(item_list) do
item_list
|> hd
end
end
TL;DR - The library stores the current state of matches and links them to an identifier code. It is capable of listing, getting, saving and deleting matches from the registry.
A small problem
For me to use the ets table, it needed to exist. This means that at some point the init function from the code above would need to be called by my web application.
def init() do
:ets.new(:scrumchkin, [:set, :public, :named_table])
end
But this goes against the single responsibility principle I used to divide this project into smaller parts, right?
The Registry as an application
But what is a dependency as a library? It’s a gear that’s part of a whole; something very similar to a Lego piece. We know where the pins and holes are and we use it to build something bigger.
The dependency on an OTP application is a bit different.
Think of a car. Usually, cars have an engine cooling mechanism that starts when you turn the key and start the car. The car depends on this mechanism to work, but it’s somewhat independent: many times it’s activated when we turn off the car (that fan noise that comes from under the hood, especially on hot days).
This cooling mechanism has interfaces with the car’s engine, but controls its own state. There’s a clear relationship of dependency, but not of control. The engine depends on the cooling system not to overheat, but doesn’t control it.
And the same needed to happen with my Game Registry, which ended up like this:
defmodule GameRegister do
use GenServer
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(stack) do
:ets.new(:scrumchkin, [:set, :public, :named_table])
IO.puts("Scrumchkin table created")
{:ok, stack}
end
def handle_call({:save, game}, _from, state) do
key = UUID.uuid1()
:ets.insert_new(:scrumchkin, {key, game})
{:reply, key, state}
end
def handle_call({:delete, game_id}, _from, state) do
:ets.delete(:scrumchkin, game_id)
{:reply, :ok, state}
end
def handle_call({:get, game_id}, _from, state) do
result =
:scrumchkin
|> :ets.lookup(game_id)
|> format_result
{:reply, result, state}
end
def handle_call(:list_all, _from, state) do
{:reply, :ets.tab2list(:scrumchkin), state}
end
def save(game) do
GenServer.call(__MODULE__, {:save, game})
end
def delete(game_id) do
GenServer.call(__MODULE__, {:delete, game_id})
end
def get(game_id) do
GenServer.call(__MODULE__, {:get, game_id})
end
def list_all do
GenServer.call(__MODULE__, :list_all)
end
defp format_result([]), do: {:error, "Game not found"}
defp format_result(item_list) do
item_list
|> hd
end
end
But… what changes?
My web application is not responsible for creating the ets table. It just says it depends on the Game Registry and that it’s now an extra application.
The change in the mix.exs file is simple:
def application do
[
mod: {Scrumchkin.Application, []},
extra_applications: [:logger, :runtime_tools, :game_register, :game_engine]
]
end
defp deps do
[
{:game_engine, path: "../game_engine"},
{:game_register, path: "../game_register"}
]
end
Now, every time I start my application with mix phx.server, my game registry is automatically started and takes on the responsibility of creating the ets table where it will store the PIDs of Scrumchkin matches.
My web application depends on the Game Registry, but trusts that it can solve its problems on its own.