Powered by AppSignal & Oban Pro

Battle Map

exercises/battle_map.livemd

Battle Map

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 Consumable ProtocolRecursion

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.

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 Battle Map 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 Consumable ProtocolRecursion