Monster Spawner
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.8.0", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
])
Navigation
Setup
Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Overview
Many video games contain Spawners which spawn enemy monsters.
Typically, the spawner has a limit for how many enemy creatures it spawns. If a monster dies, the spawner then re-spawns that monster.
Sound familiar? We can leverage supervisors to create this effect.
flowchart TD
Supervisor
Supervisor --> Monster1
Supervisor --> Monster2
Supervisor --> Monster3
classDef crashed fill:#fe8888;
classDef terminated fill:#fbab04;
classDef restarted stroke:#0cac08,stroke-width:4px
class Monster1 crashed
class Monster1 restarted
Monster
First, you’re going to create a Monster GenServer process that represents a Monster you would fight in a video game.
A monster should start with 100 health. Store health in a map with the :health key.
You should be able to send the Monster process a message to apply damage to the monster. The Monster process should crash and raise "dying!" when its health is 0 or lower.
You should also be able to start the Monster process as a named process by passing a :name option.
Example Solution
defmodule Monster do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, [], name: opts[:name])
end
def attack(monster_pid, amount) do
GenServer.cast(monster_pid, {:attack, amount})
end
def health(monster_pid) do
GenServer.call(monster_pid, :health)
end
@impl true
def init(_opts) do
{:ok, %{health: 100}}
end
@impl true
def handle_cast({:attack, damage}, state) do
new_health = state.health - damage
if new_health <= 0 do
raise "dying!"
else
{:noreply, %{state | health: new_health}}
end
end
@impl true
def handle_call(:health, _from, state) do
{:reply, state.health, state}
end
end
Implement the Monster process as documented below.
defmodule Monster do
use GenServer
@doc """
Start the `Monster` process.
## Examples
iex> {:ok, pid} = Monster.start_link([])
"""
def start_link(opts) do
# IO.inspect/2 to observe when a `Monster` process starts.
IO.inspect(opts, label: "Monster Started")
end
@doc """
Attack a `Monster` process and apply damage.
## Examples
iex> {:ok, pid} = Monster.start_link([])
iex> :sys.get_state(pid)
%{health: 100}
iex> Monster.attack(pid, 30)
iex> :sys.get_state(pid)
%{health: 70}
"""
def attack(monster_pid, amount) do
end
@doc """
Retrieve the `Monster` health.
## Examples
iex> {:ok, pid} = Monster.start_link([])
iex> Monster.health(pid)
100
"""
def health(monster_pid) do
end
@doc """
Callback function to start the `Monster` process.
Monsters should start with a `:health` value in a map.
## Examples
iex> {:ok, pid} = GenServer.start_link(Monster, [])
iex> :sys.get_state(pid)
%{health: 100}
"""
@impl true
def init(_opts) do
end
@doc """
Callback function to damage the `Monster`.
When a `Monster`'s health reaches zero it should crash and raise "dying!".
## Examples
iex> {:ok, pid} = GenServer.start_link(Monster, [])
iex> :sys.get_state(pid)
%{health: 100}
iex> GenServer.cast(pid, {:attack, 20})
iex> :sys.get_state(pid)
%{health: 80}
iex> GenServer.cast(pid, {:attack, 80})
** (RuntimeError) dying!
"""
@impl true
def handle_cast({:attack, damage}, state) do
end
@doc """
Callback function to retrieve the current health of a monster.
## Examples
iex> {:ok, pid} = GenServer.start_link(Monster, [])
iex> GenServer.call(pid, :health)
100
"""
@impl true
def handle_call(:health, _from, state) do
end
end
Supervisor
Create three named Monster processes under a single supervisor. When one Monster process dies after its health reaches zero, another should be restarted in it’s place.
flowchart
S[Supervisor]
M1[Monster 1]
M2[Monster 2]
M3[Monster 3]
S --> M1
S --> M2
S --> M3
classDef crashed fill:#fe8888;
classDef restarted stroke:#0cac08,stroke-width:4px
class M1 crashed
class M1 restarted
Example Solution
children = [
%{
id: :monster1,
start: {Monster, :start_link, [[name: :monster1]]}
},
%{
id: :monster2,
start: {Monster, :start_link, [[name: :monster2]]}
},
%{
id: :monster3,
start: {Monster, :start_link, [[name: :monster3]]}
}
]
Supervisor.start_link(children, strategy: :one_for_one)
Enter your solution below.
Call Monster.attack/1 on your monster processes to ensure they are restarted.
Example Solution
Monster.attack(:monster1, 110)
# logs: "Monster Started: [name: :monster1]"
Bonus: Hero
Create a named Hero process. The Hero process will automatically apply a random amount of damage between 1 and 10 to each monster in the named supervisor every second.
Example Solution
defmodule Hero do
def start_link(opts) do
GenServer.start_link(__MODULE__, [], opts)
end
def init(_opts) do
schedule_attack()
{:ok, []}
end
defp schedule_attack() do
Process.send_after(self(), :attack_monsters, 1000)
end
def handle_info(:attack_monsters, state) do
Supervisor.which_children(:rpg_supervisor)
|> Enum.each(fn
{_name, pid, :worker, [Monster]} -> Monster.attack(pid, Enum.random(1..10))
_ -> :ok
end)
schedule_attack()
{:noreply, state}
end
end
Start the Hero process with three other Monster processes in the same supervisor. Make the supervisor a named supervisor so you can find all of its child processes with Supervisor.which_children/1.
Example Solution
children = [
%{
id: :monster1,
start: {Monster, :start_link, [[]]}
},
%{
id: :monster2,
start: {Monster, :start_link, [[]]}
},
%{
id: :monster3,
start: {Monster, :start_link, [[]]}
},
%{
id: :hero1,
start: {Hero, :start_link, [[name: :arthur]]}
}
]
{:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one, name: :spawner)
Use Kino.Process.sup_tree/2 to visualize the processes under your named supervisor.
Example Solution
Kino.Process.sup_tree(supervisor)
For improved observability, create a function that will print the health value for each Monster process under the named supervisor.
Example Solution
Supervisor.which_children(supervisor)
|> Enum.each(fn
{_name, pid, :worker, [Monster]} -> IO.puts(Monster.health(pid))
_ -> :ok
end)
Mark As Completed
file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")
save_name =
case Path.basename(__DIR__) do
"reading" -> "monster_spawner_reading"
"exercises" -> "monster_spawner_exercise"
end
progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()
default = Map.get(existing_progress, save_name, false)
form =
Kino.Control.form(
[
completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
],
report_changes: true
)
Task.async(fn ->
for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
File.write!(
progress_path,
Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
)
end
end)
form
Commit Your Progress
Run the following in your command line from the curriculum folder to track and save your progress in a Git commit.
Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.
$ git checkout -b monster-spawner-exercise
$ git add .
$ git commit -m "finish monster spawner exercise"
$ git push origin monster-spawner-exercise
Create a pull request from your monster-spawner-exercise branch to your solutions branch.
Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.
DockYard Academy Students Only:
Notify your teacher by including @BrooklinJazz in your PR description to get feedback.
You (or your teacher) may merge your PR into your solutions branch after review.
If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.
Up Next
| Previous | Next |
|---|---|
| Dominoes | Testing GenServers |