Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Monster Spawner

deprecated_monster_spawner.livemd

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 Projects

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)

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.

Navigation

Home Report An Issue DominoesSupervised Mix Projects