Skip to content

Commit 0aa2712

Browse files
ccaddenkotp
andauthored
Add dnd_character exercise (#1629)
* 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]>
1 parent 85ec6ab commit 0aa2712

File tree

7 files changed

+310
-0
lines changed

7 files changed

+310
-0
lines changed

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@
284284
"prerequisites": [],
285285
"difficulty": 2
286286
},
287+
{
288+
"slug": "dnd-character",
289+
"name": "D&D Character",
290+
"uuid": "24f396d0-1fdf-4489-8976-8a3fb9a64adf",
291+
"practices": [],
292+
"prerequisites": [],
293+
"difficulty": 2
294+
},
287295
{
288296
"slug": "matrix",
289297
"name": "Matrix",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Instructions
2+
3+
For a game of [Dungeons & Dragons][dnd], each player starts by generating a character they can play with.
4+
This character has, among other things, six abilities; strength, dexterity, constitution, intelligence, wisdom and charisma.
5+
These six abilities have scores that are determined randomly.
6+
You do this by rolling four 6-sided dice and record the sum of the largest three dice.
7+
You do this six times, once for each ability.
8+
9+
Your character's initial hitpoints are 10 + your character's constitution modifier.
10+
You find your character's constitution modifier by subtracting 10 from your character's constitution, divide by 2 and round down.
11+
12+
Write a random character generator that follows the rules above.
13+
14+
For example, the six throws of four dice may look like:
15+
16+
- 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength.
17+
- 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity.
18+
- 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution.
19+
- 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence.
20+
- 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom.
21+
- 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma.
22+
23+
Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6.
24+
25+
## Notes
26+
27+
Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice.
28+
One such language is [Troll][troll].
29+
30+
[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons
31+
[troll]: https://di.ku.dk/Ansatte/?pure=da%2Fpublications%2Ftroll-a-language-for-specifying-dicerolls(84a45ff0-068b-11df-825d-000ea68e967b)%2Fexport.html
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"authors": [
3+
"mr-sigma",
4+
"kotp"
5+
],
6+
"files": {
7+
"solution": [
8+
"dnd_character.rb"
9+
],
10+
"test": [
11+
"dnd_character_test.rb"
12+
],
13+
"example": [
14+
".meta/example.rb"
15+
]
16+
},
17+
"blurb": "Randomly generate Dungeons & Dragons characters.",
18+
"source": "Simon Shine, Erik Schierboom",
19+
"source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945"
20+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module Die
2+
module_function
3+
4+
def roll(roll: 4, die: 6, remove_lowest: 1)
5+
roll.times.map { rand(1..die) }.sort.pop(roll - remove_lowest).sum
6+
end
7+
end
8+
9+
module Modifiable
10+
def modifier(ability_score)
11+
ability_score / 2 - 5
12+
end
13+
end
14+
15+
class DndCharacter
16+
extend Modifiable
17+
18+
BASE_HITPOINTS = 10
19+
20+
private_constant :BASE_HITPOINTS
21+
22+
private
23+
def initialize
24+
@strength = Die.roll
25+
@dexterity = Die.roll
26+
@constitution = Die.roll
27+
@intelligence = Die.roll
28+
@wisdom = Die.roll
29+
@charisma = Die.roll
30+
31+
@hitpoints = BASE_HITPOINTS + self.class.modifier(constitution)
32+
end
33+
34+
public
35+
36+
attr_reader :strength,
37+
:dexterity,
38+
:constitution,
39+
:intelligence,
40+
:wisdom,
41+
:charisma,
42+
:hitpoints
43+
end
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37]
13+
description = "ability modifier -> ability modifier for score 3 is -4"
14+
15+
[cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c]
16+
description = "ability modifier -> ability modifier for score 4 is -3"
17+
18+
[5b519fcd-6946-41ee-91fe-34b4f9808326]
19+
description = "ability modifier -> ability modifier for score 5 is -3"
20+
21+
[dc2913bd-6d7a-402e-b1e2-6d568b1cbe21]
22+
description = "ability modifier -> ability modifier for score 6 is -2"
23+
24+
[099440f5-0d66-4b1a-8a10-8f3a03cc499f]
25+
description = "ability modifier -> ability modifier for score 7 is -2"
26+
27+
[cfda6e5c-3489-42f0-b22b-4acb47084df0]
28+
description = "ability modifier -> ability modifier for score 8 is -1"
29+
30+
[c70f0507-fa7e-4228-8463-858bfbba1754]
31+
description = "ability modifier -> ability modifier for score 9 is -1"
32+
33+
[6f4e6c88-1cd9-46a0-92b8-db4a99b372f7]
34+
description = "ability modifier -> ability modifier for score 10 is 0"
35+
36+
[e00d9e5c-63c8-413f-879d-cd9be9697097]
37+
description = "ability modifier -> ability modifier for score 11 is 0"
38+
39+
[eea06f3c-8de0-45e7-9d9d-b8cab4179715]
40+
description = "ability modifier -> ability modifier for score 12 is +1"
41+
42+
[9c51f6be-db72-4af7-92ac-b293a02c0dcd]
43+
description = "ability modifier -> ability modifier for score 13 is +1"
44+
45+
[94053a5d-53b6-4efc-b669-a8b5098f7762]
46+
description = "ability modifier -> ability modifier for score 14 is +2"
47+
48+
[8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2]
49+
description = "ability modifier -> ability modifier for score 15 is +2"
50+
51+
[c3ec871e-1791-44d0-b3cc-77e5fb4cd33d]
52+
description = "ability modifier -> ability modifier for score 16 is +3"
53+
54+
[3d053cee-2888-4616-b9fd-602a3b1efff4]
55+
description = "ability modifier -> ability modifier for score 17 is +3"
56+
57+
[bafd997a-e852-4e56-9f65-14b60261faee]
58+
description = "ability modifier -> ability modifier for score 18 is +4"
59+
60+
[4f28f19c-2e47-4453-a46a-c0d365259c14]
61+
description = "random ability is within range"
62+
63+
[385d7e72-864f-4e88-8279-81a7d75b04ad]
64+
description = "random character is valid"
65+
66+
[2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe]
67+
description = "each ability is only calculated once"
68+
include = false
69+
70+
[dca2b2ec-f729-4551-84b9-078876bb4808]
71+
description = "each ability is only calculated once"
72+
reimplements = "2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
=begin
2+
Write your code for the 'D&D Character' exercise in this file. Make the tests in
3+
`dnd_character_test.rb` pass.
4+
5+
To get started with TDD, see the `README.md` file in your
6+
`ruby/dnd-character` directory.
7+
=end
8+
9+
class DndCharacter
10+
def self.modifier
11+
# Your code here
12+
end
13+
14+
def initialize
15+
# Your code here
16+
end
17+
end
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require 'minitest/autorun'
2+
require_relative 'dnd_character'
3+
4+
module Helpers
5+
BASE_HITPOINTS = 10
6+
7+
def attributes
8+
%i[strength dexterity constitution intelligence wisdom charisma]
9+
end
10+
end
11+
12+
class DndCharacterTest < Minitest::Test
13+
include Helpers
14+
15+
def test_modifier_score_3
16+
assert_equal(-4, DndCharacter.modifier(3))
17+
end
18+
19+
def test_modifier_score_4
20+
assert_equal(-3, DndCharacter.modifier(4))
21+
end
22+
23+
def test_modifier_score_5
24+
assert_equal(-3, DndCharacter.modifier(5))
25+
end
26+
27+
def test_modifier_score_6
28+
assert_equal(-2, DndCharacter.modifier(6))
29+
end
30+
31+
def test_modifier_score_7
32+
assert_equal(-2, DndCharacter.modifier(7))
33+
end
34+
35+
def test_modifier_score_8
36+
assert_equal(-1, DndCharacter.modifier(8))
37+
end
38+
39+
def test_modifier_score_9
40+
assert_equal(-1, DndCharacter.modifier(9))
41+
end
42+
43+
def test_modifier_score_10
44+
assert_equal 0, DndCharacter.modifier(10)
45+
end
46+
47+
def test_modifier_score_11
48+
assert_equal 0, DndCharacter.modifier(11)
49+
end
50+
51+
def test_modifier_score_12
52+
assert_equal 1, DndCharacter.modifier(12)
53+
end
54+
55+
def test_modifier_score_13
56+
assert_equal 1, DndCharacter.modifier(13)
57+
end
58+
59+
def test_modifier_score_14
60+
assert_equal 2, DndCharacter.modifier(14)
61+
end
62+
63+
def test_modifier_score_15
64+
assert_equal 2, DndCharacter.modifier(15)
65+
end
66+
67+
def test_modifier_score_16
68+
assert_equal 3, DndCharacter.modifier(16)
69+
end
70+
71+
def test_modifier_score_17
72+
assert_equal 3, DndCharacter.modifier(17)
73+
end
74+
75+
def test_modifier_score_18
76+
assert_equal 4, DndCharacter.modifier(18)
77+
end
78+
79+
# rubocop:disable Style/FormatString, Style/RedundantPercentQ
80+
def test_random_character_stats
81+
100.times do
82+
character = DndCharacter.new
83+
allowed_range = (3..18)
84+
expected_hitpoints = BASE_HITPOINTS +
85+
DndCharacter.modifier(character.constitution)
86+
informative_message = %q(The character's %s must be within %s)
87+
88+
attributes.each do |attribute|
89+
assert_includes allowed_range, character.send(attribute),
90+
informative_message % [attribute, allowed_range]
91+
end
92+
93+
informative_message = %q(The character's %s must be %s)
94+
95+
assert_equal expected_hitpoints, character.hitpoints,
96+
informative_message % ['hitpoints', expected_hitpoints]
97+
end
98+
end
99+
100+
def test_stats_calculated_once
101+
informative_message = <<~EXPLAIN
102+
The character's %<attribute>s must not change if called more than once.
103+
It was %<first>s, is now %<second>s.
104+
EXPLAIN
105+
106+
100.times do
107+
character = DndCharacter.new
108+
109+
(attributes << :hitpoints).each do |attribute|
110+
first = character.send(attribute)
111+
second = character.send(attribute)
112+
113+
assert_equal first, second,
114+
informative_message % { attribute:, first:, second: }
115+
end
116+
end
117+
# rubocop:enable Style/FormatString, Style/RedundantPercentQ
118+
end
119+
end

0 commit comments

Comments
 (0)