From 941e86a05c599f73a6931f0a8ebe11b7dd35ae61 Mon Sep 17 00:00:00 2001 From: Jesse Kroon <156132898+jesse-kroon@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:27:12 +0100 Subject: [PATCH] Add "D&D Character" practice exercise (#2753) --- config.json | 12 ++ .../dnd-character/.docs/instructions.md | 31 ++++ .../practice/dnd-character/.meta/config.json | 21 +++ .../practice/dnd-character/.meta/example.go | 56 +++++++ exercises/practice/dnd-character/.meta/gen.go | 68 ++++++++ .../practice/dnd-character/.meta/tests.toml | 72 +++++++++ .../practice/dnd-character/cases_test.go | 145 ++++++++++++++++++ .../practice/dnd-character/dnd_character.go | 26 ++++ .../dnd-character/dnd_character_test.go | 75 +++++++++ exercises/practice/dnd-character/go.mod | 3 + 10 files changed, 509 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.go create mode 100644 exercises/practice/dnd-character/.meta/gen.go create mode 100644 exercises/practice/dnd-character/.meta/tests.toml create mode 100644 exercises/practice/dnd-character/cases_test.go create mode 100644 exercises/practice/dnd-character/dnd_character.go create mode 100644 exercises/practice/dnd-character/dnd_character_test.go create mode 100644 exercises/practice/dnd-character/go.mod diff --git a/config.json b/config.json index ec479d9f6..bbf538156 100644 --- a/config.json +++ b/config.json @@ -753,6 +753,18 @@ "math" ] }, + { + "slug": "dnd-character", + "name": "D&D Character", + "uuid": "fb42541e-6334-495f-b63b-e3c3d4ac1ed6", + "practices": [], + "prerequisites": [], + "difficulty": 3, + "topics": [ + "structs", + "randomness" + ] + }, { "slug": "etl", "name": "ETL", diff --git a/exercises/practice/dnd-character/.docs/instructions.md b/exercises/practice/dnd-character/.docs/instructions.md new file mode 100644 index 000000000..b0a603591 --- /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 000000000..f266fae11 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/config.json @@ -0,0 +1,21 @@ +{ + "authors": ["jesse-kroon"], + "contributors": ["andrerfcsantos"], + "files": { + "solution": [ + "dnd_character.go" + ], + "test": [ + "dnd_character_test.go" + ], + "editor": [ + "cases_test.go" + ], + "example": [ + ".meta/example.go" + ] + }, + "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.go b/exercises/practice/dnd-character/.meta/example.go new file mode 100644 index 000000000..34812f9a2 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/example.go @@ -0,0 +1,56 @@ +package dndcharacter + +import ( + "math" + "math/rand" + "slices" +) + +type Character struct { + Strength int + Dexterity int + Constitution int + Intelligence int + Wisdom int + Charisma int + Hitpoints int +} + +// Modifier calculates the ability modifier for a given ability score +func Modifier(score int) int { + return int(math.Floor(float64(score-10) / 2.0)) +} + +// Ability uses randomness to generate the score for an ability +func Ability() int { + var scores []int + var sum int + + for i := 0; i < 4; i++ { + roll := RollDice() + sum += roll + scores = append(scores, roll) + } + + return sum - slices.Min(scores) +} + +// GenerateCharacter creates a new Character with random scores for abilities +func GenerateCharacter() Character { + character := Character{ + Strength: Ability(), + Dexterity: Ability(), + Constitution: Ability(), + Intelligence: Ability(), + Wisdom: Ability(), + Charisma: Ability(), + } + + character.Hitpoints = 10 + Modifier(character.Constitution) + + return character +} + +func RollDice() int { + return rand.Intn(6) + 1 +} diff --git a/exercises/practice/dnd-character/.meta/gen.go b/exercises/practice/dnd-character/.meta/gen.go new file mode 100644 index 000000000..06c6346f7 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/gen.go @@ -0,0 +1,68 @@ +package main + +import ( + "log" + "text/template" + + "../../../../gen" +) + +func main() { + t, err := template.New("").Parse(tmpl) + if err != nil { + log.Fatal(err) + } + var j = map[string]interface{}{ + "modifier": &[]modifierTestCase{}, + "ability": &[]emptyTestCase{}, + "character": &[]emptyTestCase{}, + } + if err := gen.Gen("dnd-character", j, t); err != nil { + log.Fatal(err) + } +} + +// Problem specifications have the 'ability' and 'character' properties, +// and the test generator expects them to be present. +// However, for these properties, generating test cases automatically +// from the 'input' and the 'expected' objects is not trivial. +// To satisfy the test generator, we create an emptyTestCase for these properties +// and implement the tests manually. +// In the future we might adapt the test generator to also cover this exercise. +type emptyTestCase struct{} + +type modifierTestInput struct { + Score int `json:"score"` +} + +type modifierTestCase struct { + Description string `json:"description"` + Input modifierTestInput `json:"input"` + Expected int `json:"expected"` +} + +var tmpl = ` +package dndcharacter + +{{.Header}} + +type modifierTestInput struct { + Score int +} + +var modifierTests = []struct { + description string + input modifierTestInput + expected int +}{ + {{range .J.modifier}} + { + description: {{printf "%q" .Description}}, + input: modifierTestInput { + Score: {{printf "%d" .Input.Score}}, + }, + expected: {{printf "%d" .Expected}}, + }, + {{end}} +} +` diff --git a/exercises/practice/dnd-character/.meta/tests.toml b/exercises/practice/dnd-character/.meta/tests.toml new file mode 100644 index 000000000..719043b25 --- /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/cases_test.go b/exercises/practice/dnd-character/cases_test.go new file mode 100644 index 000000000..b000387a4 --- /dev/null +++ b/exercises/practice/dnd-character/cases_test.go @@ -0,0 +1,145 @@ +package dndcharacter + +// This is an auto-generated file. Do not change it manually. Run the generator to update the file. +// See https://github.com/exercism/go#synchronizing-tests-and-instructions +// Source: exercism/problem-specifications +// Commit: 02209d7 Reimplement test case in DnD Character (#2338) + +type modifierTestInput struct { + Score int +} + +var modifierTests = []struct { + description string + input modifierTestInput + expected int +}{ + + { + description: "ability modifier for score 3 is -4", + input: modifierTestInput{ + Score: 3, + }, + expected: -4, + }, + + { + description: "ability modifier for score 4 is -3", + input: modifierTestInput{ + Score: 4, + }, + expected: -3, + }, + + { + description: "ability modifier for score 5 is -3", + input: modifierTestInput{ + Score: 5, + }, + expected: -3, + }, + + { + description: "ability modifier for score 6 is -2", + input: modifierTestInput{ + Score: 6, + }, + expected: -2, + }, + + { + description: "ability modifier for score 7 is -2", + input: modifierTestInput{ + Score: 7, + }, + expected: -2, + }, + + { + description: "ability modifier for score 8 is -1", + input: modifierTestInput{ + Score: 8, + }, + expected: -1, + }, + + { + description: "ability modifier for score 9 is -1", + input: modifierTestInput{ + Score: 9, + }, + expected: -1, + }, + + { + description: "ability modifier for score 10 is 0", + input: modifierTestInput{ + Score: 10, + }, + expected: 0, + }, + + { + description: "ability modifier for score 11 is 0", + input: modifierTestInput{ + Score: 11, + }, + expected: 0, + }, + + { + description: "ability modifier for score 12 is +1", + input: modifierTestInput{ + Score: 12, + }, + expected: 1, + }, + + { + description: "ability modifier for score 13 is +1", + input: modifierTestInput{ + Score: 13, + }, + expected: 1, + }, + + { + description: "ability modifier for score 14 is +2", + input: modifierTestInput{ + Score: 14, + }, + expected: 2, + }, + + { + description: "ability modifier for score 15 is +2", + input: modifierTestInput{ + Score: 15, + }, + expected: 2, + }, + + { + description: "ability modifier for score 16 is +3", + input: modifierTestInput{ + Score: 16, + }, + expected: 3, + }, + + { + description: "ability modifier for score 17 is +3", + input: modifierTestInput{ + Score: 17, + }, + expected: 3, + }, + + { + description: "ability modifier for score 18 is +4", + input: modifierTestInput{ + Score: 18, + }, + expected: 4, + }, +} diff --git a/exercises/practice/dnd-character/dnd_character.go b/exercises/practice/dnd-character/dnd_character.go new file mode 100644 index 000000000..5c048c3f6 --- /dev/null +++ b/exercises/practice/dnd-character/dnd_character.go @@ -0,0 +1,26 @@ +package dndcharacter + +type Character struct { + Strength int + Dexterity int + Constitution int + Intelligence int + Wisdom int + Charisma int + Hitpoints int +} + +// Modifier calculates the ability modifier for a given ability score +func Modifier(score int) int { + panic("Please implement the Modifier() function") +} + +// Ability uses randomness to generate the score for an ability +func Ability() int { + panic("Please implement the Ability() function") +} + +// GenerateCharacter creates a new Character with random scores for abilities +func GenerateCharacter() Character { + panic("Please implement the GenerateCharacter() function") +} diff --git a/exercises/practice/dnd-character/dnd_character_test.go b/exercises/practice/dnd-character/dnd_character_test.go new file mode 100644 index 000000000..a4fb06f09 --- /dev/null +++ b/exercises/practice/dnd-character/dnd_character_test.go @@ -0,0 +1,75 @@ +package dndcharacter + +import "testing" + +func TestModifier(t *testing.T) { + for _, tc := range modifierTests { + t.Run(tc.description, func(t *testing.T) { + actual := Modifier(tc.input.Score) + if actual != tc.expected { + t.Fatalf("Modifier(%d) = %d, want %d", tc.input.Score, actual, tc.expected) + } + }) + } +} + +func TestAbility(t *testing.T) { + t.Run("should generate ability score within accepted range", func(t *testing.T) { + for i := 0; i < 1000; i++ { + got := Ability() + if !inAcceptedRange(got) { + t.Fatalf("Ability() returned a score for an ability outside the accepted range. Got %d, expected a value between 3 and 18 inclusive.", got) + } + } + }) +} + +func TestGenerateCharacter(t *testing.T) { + t.Run("should generate a character with random ability scores", func(t *testing.T) { + for i := 0; i < 1000; i++ { + character := GenerateCharacter() + + assertCharacterAbilityScoreInRange(t, "Charisma", character.Charisma) + assertCharacterAbilityScoreInRange(t, "Strength", character.Strength) + assertCharacterAbilityScoreInRange(t, "Dexterity", character.Dexterity) + assertCharacterAbilityScoreInRange(t, "Wisdom", character.Wisdom) + assertCharacterAbilityScoreInRange(t, "Intelligence", character.Intelligence) + assertCharacterAbilityScoreInRange(t, "Constitution", character.Constitution) + + expectedHitpoints := 10 + Modifier(character.Constitution) + if character.Hitpoints != expectedHitpoints { + t.Fatalf("Got %d hitpoints for a character with %d constitution, expected %d hitpoints", character.Hitpoints, character.Constitution, expectedHitpoints) + } + } + }) +} + +func inAcceptedRange(score int) bool { + return score >= 3 && score <= 18 +} + +func assertCharacterAbilityScoreInRange(t *testing.T, ability string, score int) { + t.Helper() + + if !inAcceptedRange(score) { + t.Fatalf("GenerateCharacter() created a character with a %s score of %d, but the score for an ability is expected to be between 3 and 18 inclusive", ability, score) + } +} + +func BenchmarkModifier(b *testing.B) { + for i := 0; i < b.N; i++ { + Modifier(i) + } +} + +func BenchmarkAbility(b *testing.B) { + for i := 0; i < b.N; i++ { + Ability() + } +} + +func BenchmarkCharacter(b *testing.B) { + for i := 0; i < b.N; i++ { + GenerateCharacter() + } +} diff --git a/exercises/practice/dnd-character/go.mod b/exercises/practice/dnd-character/go.mod new file mode 100644 index 000000000..039a5795a --- /dev/null +++ b/exercises/practice/dnd-character/go.mod @@ -0,0 +1,3 @@ +module dnd-character + +go 1.18