🎉 Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io 🎉
Avatar of angelikatyborska

angelikatyborska's solution

to D&D Character in the Elixir Track

Published at Oct 28 2019 · 0 comments
Instructions
Test suite
Solution

For a game of Dungeons & Dragons, each player starts by generating a character they can play with. This character has, among other things, six abilities; strength, dexterity, constitution, intelligence, wisdom and charisma. These six abilities have scores that are determined randomly. You do this by rolling four 6-sided dice and record the sum of the largest three dice. You do this six times, once for each ability.

Your character's initial hitpoints are 10 + your character's constitution modifier. You find your character's constitution modifier by subtracting 10 from your character's constitution, divide by 2 and round down.

Write a random character generator that follows the rules above.

For example, the six throws of four dice may look like:

  • 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength.
  • 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity.
  • 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution.
  • 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence.
  • 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom.
  • 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma.

Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6.

Notes

Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice. One such language is Troll.

dnd_character_test.exs

defmodule DndCharacterTest do
  use ExUnit.Case

  import DndCharacter, only: [modifier: 1, ability: 0, character: 0]

  # canonical data version: 1.1.0

  describe "ability modifier" do
    # @tag :pending
    test "for score 3 is -4" do
      assert modifier(3) === -4
    end

    @tag :pending
    test "for score 4 is -3" do
      assert modifier(4) === -3
    end

    @tag :pending
    test "for score 5 is -3" do
      assert modifier(5) === -3
    end

    @tag :pending
    test "for score 6 is -2" do
      assert modifier(6) === -2
    end

    @tag :pending
    test "for score 7 is -2" do
      assert modifier(7) === -2
    end

    @tag :pending
    test "for score 8 is -1" do
      assert modifier(8) === -1
    end

    @tag :pending
    test "for score 9 is -1" do
      assert modifier(9) === -1
    end

    @tag :pending
    test "for score 10 is 0" do
      assert modifier(10) === 0
    end

    @tag :pending
    test "for score 11 is 0" do
      assert modifier(11) === 0
    end

    @tag :pending
    test "for score 12 is +1" do
      assert modifier(12) === 1
    end

    @tag :pending
    test "for score 13 is +1" do
      assert modifier(13) === 1
    end

    @tag :pending
    test "for score 14 is +2" do
      assert modifier(14) === 2
    end

    @tag :pending
    test "for score 15 is +2" do
      assert modifier(15) === 2
    end

    @tag :pending
    test "for score 16 is +3" do
      assert modifier(16) === 3
    end

    @tag :pending
    test "for score 17 is +3" do
      assert modifier(17) === 3
    end

    @tag :pending
    test "for score 18 is +4" do
      assert modifier(18) === 4
    end
  end

  describe "random ability" do
    @tag :pending
    test "is within range" do
      Enum.each(1..200, fn _ -> assert ability() in 3..18 end)
    end
  end

  describe "random character" do
    setup do
      %{character: character()}
    end

    @tag :pending
    test "has valid hitpoints", %{character: character} do
      assert character.hitpoints === 10 + modifier(character.constitution)
    end

    @tag :pending
    test "has each ability only calculated once", %{character: character} do
      assert character.strength === character.strength
    end
  end
end

test_helper.exs

ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)
defmodule DndCharacter do
  @type t :: %__MODULE__{
          strength: pos_integer(),
          dexterity: pos_integer(),
          constitution: pos_integer(),
          intelligence: pos_integer(),
          wisdom: pos_integer(),
          charisma: pos_integer(),
          hitpoints: pos_integer()
        }

  defstruct ~w[strength dexterity constitution intelligence wisdom charisma hitpoints]a

  @spec modifier(pos_integer()) :: integer()
  def modifier(score) do
    Integer.floor_div(score - 10, 2)
  end

  @spec ability :: pos_integer()
  def ability do
    1..4
    |> Enum.map(fn _ -> k6_roll() end)
    |> Enum.sort()
    |> Enum.take(-3)
    |> Enum.sum()
  end

  @spec character :: t()
  def character do
    character = %__MODULE__{
      strength: ability(),
      dexterity: ability(),
      constitution: ability(),
      intelligence: ability(),
      wisdom: ability(),
      charisma: ability()
    }

    %{character | hitpoints: 10 + modifier(character.constitution)}
  end

  defp k6_roll() do
    :rand.uniform(6)
  end
end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?