Powered by AppSignal & Oban Pro

Battle Map

exercises/battle_map.livemd

Battle Map

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.8.0", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Return Home Report An Issue

Overview

You’re developing a 2D tactical combat game.

Characters in your game fight on a grid of potentially infinite size.

Character Attack

You’re going to create a Character.can_attack?/3 function that expects the following:

  1. A struct
  2. The player’s coordinate {x, y}
  3. An enemy’s coordinate {x, y}

It should then return a boolean.

Wizard

A Wizard can attack in straight or diagonal lines of any length.

We’ve created a Wizard struct below. It does not need to store any information in the struct.

defmodule Wizard do
  defstruct []
end

A Barbarian can attack in 2 square radius.

We’ve created a Barbarian struct below. It does not need to store any information in the struct.

defmodule Barbarian do
  defstruct []
end

You should be able to check if a character can attack a coordinate {x, y} given their starting location in {x, y}.

Character.can_attack?(%Barbarian{}, {4, 4}, {6, 6})
true

Character.can_attack?(%Wizard{}, {4, 4}, {6, 6})
true

Example Solution

defprotocol Character do
  def can_attack?(character, origin, target)
end

defimpl Character, for: Wizard do
  def can_attack?(_character, {init_x, init_y}, {x, y}) do
    x_diff = init_x - x
    y_diff = init_y - y

    init_x == x || init_y == y || abs(x_diff) == abs(y_diff)
  end
end

defimpl Character, for: Barbarian do
  def can_attack?(_character, {init_x, init_y}, {x, y}) do
    x_diff = init_x - x
    y_diff = init_y - y

    abs(x_diff) <= 2 &amp;&amp; abs(y_diff) <= 2
  end
end

Create implementations for the Character protocol as documented below. We’ve provided a full suite of tests for you.

defprotocol Character do
  @doc """

  Determine if a character type can attack based on it's
  current position and target position on a grid.

  ## Examples

    iex> Character.can_attack?(%Barbarian{}, {4, 4}, {6, 6})
    true

    iex> Character.can_attack?(%Wizard{}, {4, 4}, {7, 7})
    true
  """
  def can_attack?(character, origin, target)
end

ExUnit.start(auto_run: false)

defmodule CharacterTests do
  use ExUnit.Case

  describe "Barbarian" do
    test "can attack within two squares of current position" do
      for x <- 2..6, y <- 2..6 do
        assert Character.can_attack?(%Barbarian{}, {4, 4}, {x, y})
      end
    end

    test "cannot attack beyond two squares of current position" do
      refute Character.can_attack?(%Barbarian{}, {4, 4}, {1, 1})
      refute Character.can_attack?(%Barbarian{}, {4, 4}, {7, 7})
      refute Character.can_attack?(%Barbarian{}, {4, 4}, {7, 1})
      refute Character.can_attack?(%Barbarian{}, {4, 4}, {1, 7})
    end

    test "logic is not hardcoded to the {4, 4} position" do
      refute Character.can_attack?(%Barbarian{}, {3, 3}, {6, 6})
    end
  end

  describe "Wizard" do
    test "can attack in eight directions" do
      # up, up-right, right, down-right, down, down-left, left, up-left
      assert Character.can_attack?(%Wizard{}, {4, 4}, {4, 5})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {5, 5})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {5, 4})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {5, 3})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {4, 3})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {3, 3})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {3, 4})
      assert Character.can_attack?(%Wizard{}, {4, 4}, {3, 5})
    end

    test "cannot attack invalid squares" do
      refute Character.can_attack?(%Wizard{}, {4, 4}, {6, 5})
      refute Character.can_attack?(%Wizard{}, {4, 4}, {2, 5})
      refute Character.can_attack?(%Wizard{}, {4, 4}, {3, 2})
      refute Character.can_attack?(%Wizard{}, {4, 4}, {6, 3})
    end
  end
end

ExUnit.run()

Custom Character (BONUS)

Create your own customer character with an attack pattern than you decide on. It should not match the existing characters.

For example, you might create an Archer who can only attack in a 3 radius square.

Character.can_attack?(%Archer{}, {4, 4}, {7, 7})
true

Implement your custom character below.

Mark As Completed

file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")

save_name =
  case Path.basename(__DIR__) do
    "reading" -> "battle_map_reading"
    "exercises" -> "battle_map_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 battle-map-exercise
$ git add .
$ git commit -m "finish battle map exercise"
$ git push origin battle-map-exercise

Create a pull request from your battle-map-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
Consumable Protocol Recursion