From 0aa2712fcafbf33a4a7a5dbfbabcaa6752f8c030 Mon Sep 17 00:00:00 2001 From: Chris Cadden Date: Sun, 11 Feb 2024 10:20:45 -0500 Subject: [PATCH] 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 --- config.json | 8 ++ .../dnd-character/.docs/instructions.md | 31 +++++ .../practice/dnd-character/.meta/config.json | 20 +++ .../practice/dnd-character/.meta/example.rb | 43 +++++++ .../practice/dnd-character/.meta/tests.toml | 72 +++++++++++ .../practice/dnd-character/dnd_character.rb | 17 +++ .../dnd-character/dnd_character_test.rb | 119 ++++++++++++++++++ 7 files changed, 310 insertions(+) create mode 100644 exercises/practice/dnd-character/.docs/instructions.md create mode 100644 exercises/practice/dnd-character/.meta/config.json create mode 100644 exercises/practice/dnd-character/.meta/example.rb create mode 100644 exercises/practice/dnd-character/.meta/tests.toml create mode 100644 exercises/practice/dnd-character/dnd_character.rb create mode 100644 exercises/practice/dnd-character/dnd_character_test.rb diff --git a/config.json b/config.json index 3ce3f83d89..e1ab066170 100644 --- a/config.json +++ b/config.json @@ -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", diff --git a/exercises/practice/dnd-character/.docs/instructions.md b/exercises/practice/dnd-character/.docs/instructions.md new file mode 100644 index 0000000000..b0a603591e --- /dev/null +++ b/exercises/practice/dnd-character/.docs/instructions.md @@ -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 diff --git a/exercises/practice/dnd-character/.meta/config.json b/exercises/practice/dnd-character/.meta/config.json new file mode 100644 index 0000000000..092784b0c0 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/config.json @@ -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" +} diff --git a/exercises/practice/dnd-character/.meta/example.rb b/exercises/practice/dnd-character/.meta/example.rb new file mode 100644 index 0000000000..f1627306dd --- /dev/null +++ b/exercises/practice/dnd-character/.meta/example.rb @@ -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 diff --git a/exercises/practice/dnd-character/.meta/tests.toml b/exercises/practice/dnd-character/.meta/tests.toml new file mode 100644 index 0000000000..719043b253 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/tests.toml @@ -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" diff --git a/exercises/practice/dnd-character/dnd_character.rb b/exercises/practice/dnd-character/dnd_character.rb new file mode 100644 index 0000000000..45a76798a8 --- /dev/null +++ b/exercises/practice/dnd-character/dnd_character.rb @@ -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 diff --git a/exercises/practice/dnd-character/dnd_character_test.rb b/exercises/practice/dnd-character/dnd_character_test.rb new file mode 100644 index 0000000000..1edbd2f22b --- /dev/null +++ b/exercises/practice/dnd-character/dnd_character_test.rb @@ -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 %s must not change if called more than once. + It was %s, is now %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