Skip to content

Commit

Permalink
Add dnd_character exercise (#1629)
Browse files Browse the repository at this point in the history
* bin/configlet create --practice-exercise dnd-character
* Add dnd-character exercise difficulty
* Implement dnd-character test cases
* Add dnd_character.rb stub file
* Add example implementation of DndCharacter class

---------

Co-authored-by: KOTP <[email protected]>
  • Loading branch information
ccadden and kotp authored Feb 11, 2024
1 parent 85ec6ab commit 0aa2712
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@
"prerequisites": [],
"difficulty": 2
},
{
"slug": "dnd-character",
"name": "D&D Character",
"uuid": "24f396d0-1fdf-4489-8976-8a3fb9a64adf",
"practices": [],
"prerequisites": [],
"difficulty": 2
},
{
"slug": "matrix",
"name": "Matrix",
Expand Down
31 changes: 31 additions & 0 deletions exercises/practice/dnd-character/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Instructions

For a game of [Dungeons & Dragons][dnd], 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][troll].

[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons
[troll]: https://di.ku.dk/Ansatte/?pure=da%2Fpublications%2Ftroll-a-language-for-specifying-dicerolls(84a45ff0-068b-11df-825d-000ea68e967b)%2Fexport.html
20 changes: 20 additions & 0 deletions exercises/practice/dnd-character/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"authors": [
"mr-sigma",
"kotp"
],
"files": {
"solution": [
"dnd_character.rb"
],
"test": [
"dnd_character_test.rb"
],
"example": [
".meta/example.rb"
]
},
"blurb": "Randomly generate Dungeons & Dragons characters.",
"source": "Simon Shine, Erik Schierboom",
"source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945"
}
43 changes: 43 additions & 0 deletions exercises/practice/dnd-character/.meta/example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Die
module_function

def roll(roll: 4, die: 6, remove_lowest: 1)
roll.times.map { rand(1..die) }.sort.pop(roll - remove_lowest).sum
end
end

module Modifiable
def modifier(ability_score)
ability_score / 2 - 5
end
end

class DndCharacter
extend Modifiable

BASE_HITPOINTS = 10

private_constant :BASE_HITPOINTS

private
def initialize
@strength = Die.roll
@dexterity = Die.roll
@constitution = Die.roll
@intelligence = Die.roll
@wisdom = Die.roll
@charisma = Die.roll

@hitpoints = BASE_HITPOINTS + self.class.modifier(constitution)
end

public

attr_reader :strength,
:dexterity,
:constitution,
:intelligence,
:wisdom,
:charisma,
:hitpoints
end
72 changes: 72 additions & 0 deletions exercises/practice/dnd-character/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37]
description = "ability modifier -> ability modifier for score 3 is -4"

[cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c]
description = "ability modifier -> ability modifier for score 4 is -3"

[5b519fcd-6946-41ee-91fe-34b4f9808326]
description = "ability modifier -> ability modifier for score 5 is -3"

[dc2913bd-6d7a-402e-b1e2-6d568b1cbe21]
description = "ability modifier -> ability modifier for score 6 is -2"

[099440f5-0d66-4b1a-8a10-8f3a03cc499f]
description = "ability modifier -> ability modifier for score 7 is -2"

[cfda6e5c-3489-42f0-b22b-4acb47084df0]
description = "ability modifier -> ability modifier for score 8 is -1"

[c70f0507-fa7e-4228-8463-858bfbba1754]
description = "ability modifier -> ability modifier for score 9 is -1"

[6f4e6c88-1cd9-46a0-92b8-db4a99b372f7]
description = "ability modifier -> ability modifier for score 10 is 0"

[e00d9e5c-63c8-413f-879d-cd9be9697097]
description = "ability modifier -> ability modifier for score 11 is 0"

[eea06f3c-8de0-45e7-9d9d-b8cab4179715]
description = "ability modifier -> ability modifier for score 12 is +1"

[9c51f6be-db72-4af7-92ac-b293a02c0dcd]
description = "ability modifier -> ability modifier for score 13 is +1"

[94053a5d-53b6-4efc-b669-a8b5098f7762]
description = "ability modifier -> ability modifier for score 14 is +2"

[8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2]
description = "ability modifier -> ability modifier for score 15 is +2"

[c3ec871e-1791-44d0-b3cc-77e5fb4cd33d]
description = "ability modifier -> ability modifier for score 16 is +3"

[3d053cee-2888-4616-b9fd-602a3b1efff4]
description = "ability modifier -> ability modifier for score 17 is +3"

[bafd997a-e852-4e56-9f65-14b60261faee]
description = "ability modifier -> ability modifier for score 18 is +4"

[4f28f19c-2e47-4453-a46a-c0d365259c14]
description = "random ability is within range"

[385d7e72-864f-4e88-8279-81a7d75b04ad]
description = "random character is valid"

[2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe]
description = "each ability is only calculated once"
include = false

[dca2b2ec-f729-4551-84b9-078876bb4808]
description = "each ability is only calculated once"
reimplements = "2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe"
17 changes: 17 additions & 0 deletions exercises/practice/dnd-character/dnd_character.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
=begin
Write your code for the 'D&D Character' exercise in this file. Make the tests in
`dnd_character_test.rb` pass.
To get started with TDD, see the `README.md` file in your
`ruby/dnd-character` directory.
=end

class DndCharacter
def self.modifier
# Your code here
end

def initialize
# Your code here
end
end
119 changes: 119 additions & 0 deletions exercises/practice/dnd-character/dnd_character_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
require 'minitest/autorun'
require_relative 'dnd_character'

module Helpers
BASE_HITPOINTS = 10

def attributes
%i[strength dexterity constitution intelligence wisdom charisma]
end
end

class DndCharacterTest < Minitest::Test
include Helpers

def test_modifier_score_3
assert_equal(-4, DndCharacter.modifier(3))
end

def test_modifier_score_4
assert_equal(-3, DndCharacter.modifier(4))
end

def test_modifier_score_5
assert_equal(-3, DndCharacter.modifier(5))
end

def test_modifier_score_6
assert_equal(-2, DndCharacter.modifier(6))
end

def test_modifier_score_7
assert_equal(-2, DndCharacter.modifier(7))
end

def test_modifier_score_8
assert_equal(-1, DndCharacter.modifier(8))
end

def test_modifier_score_9
assert_equal(-1, DndCharacter.modifier(9))
end

def test_modifier_score_10
assert_equal 0, DndCharacter.modifier(10)
end

def test_modifier_score_11
assert_equal 0, DndCharacter.modifier(11)
end

def test_modifier_score_12
assert_equal 1, DndCharacter.modifier(12)
end

def test_modifier_score_13
assert_equal 1, DndCharacter.modifier(13)
end

def test_modifier_score_14
assert_equal 2, DndCharacter.modifier(14)
end

def test_modifier_score_15
assert_equal 2, DndCharacter.modifier(15)
end

def test_modifier_score_16
assert_equal 3, DndCharacter.modifier(16)
end

def test_modifier_score_17
assert_equal 3, DndCharacter.modifier(17)
end

def test_modifier_score_18
assert_equal 4, DndCharacter.modifier(18)
end

# rubocop:disable Style/FormatString, Style/RedundantPercentQ
def test_random_character_stats
100.times do
character = DndCharacter.new
allowed_range = (3..18)
expected_hitpoints = BASE_HITPOINTS +
DndCharacter.modifier(character.constitution)
informative_message = %q(The character's %s must be within %s)

attributes.each do |attribute|
assert_includes allowed_range, character.send(attribute),
informative_message % [attribute, allowed_range]
end

informative_message = %q(The character's %s must be %s)

assert_equal expected_hitpoints, character.hitpoints,
informative_message % ['hitpoints', expected_hitpoints]
end
end

def test_stats_calculated_once
informative_message = <<~EXPLAIN
The character's %<attribute>s must not change if called more than once.
It was %<first>s, is now %<second>s.
EXPLAIN

100.times do
character = DndCharacter.new

(attributes << :hitpoints).each do |attribute|
first = character.send(attribute)
second = character.send(attribute)

assert_equal first, second,
informative_message % { attribute:, first:, second: }
end
end
# rubocop:enable Style/FormatString, Style/RedundantPercentQ
end
end

0 comments on commit 0aa2712

Please sign in to comment.