Monster Spawner
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
])
Navigation
Home Report An Issue DominoesSupervised Mix ProjectsOverview
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)
Commit Your Progress
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Monster Spawner exercise"
$ git push
We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.