diff --git a/config.json b/config.json index 358e2586..5fd332f7 100644 --- a/config.json +++ b/config.json @@ -276,6 +276,14 @@ "prerequisites": [], "difficulty": 1 }, + { + "slug": "dnd-character", + "name": "D&D Character", + "uuid": "f018dfdd-001f-4ec7-a46c-9dcea229c4c4", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "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 00000000..e14e7949 --- /dev/null +++ b/exercises/practice/dnd-character/.docs/instructions.md @@ -0,0 +1,32 @@ +# 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 recording 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 above rules. + +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. + +~~~~exercism/note +Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice. +One such language is [Troll][troll]. + +[troll]: https://di.ku.dk/Ansatte/?pure=da%2Fpublications%2Ftroll-a-language-for-specifying-dicerolls(84a45ff0-068b-11df-825d-000ea68e967b)%2Fexport.html +~~~~ + +[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons diff --git a/exercises/practice/dnd-character/.docs/introduction.md b/exercises/practice/dnd-character/.docs/introduction.md new file mode 100644 index 00000000..5301f618 --- /dev/null +++ b/exercises/practice/dnd-character/.docs/introduction.md @@ -0,0 +1,10 @@ +# Introduction + +After weeks of anticipation, you and your friends get together for your very first game of [Dungeons & Dragons][dnd] (D&D). +Since this is the first session of the game, each player has to generate a character to play with. +The character's abilities are determined by rolling 6-sided dice, but where _are_ the dice? +With a shock, you realize that your friends are waiting for _you_ to produce the dice; after all it was your idea to play D&D! +Panicking, you realize you forgot to bring the dice, which would mean no D&D game. +As you have some basic coding skills, you quickly come up with a solution: you'll write a program to simulate dice rolls. + +[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons diff --git a/exercises/practice/dnd-character/.meta/config.json b/exercises/practice/dnd-character/.meta/config.json new file mode 100644 index 00000000..42c3ca7d --- /dev/null +++ b/exercises/practice/dnd-character/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "lib/DndCharacter.pm" + ], + "test": [ + "t/dnd-character.t" + ], + "example": [ + ".meta/solutions/lib/DndCharacter.pm" + ] + }, + "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/solutions/lib/DndCharacter.pm b/exercises/practice/dnd-character/.meta/solutions/lib/DndCharacter.pm new file mode 100644 index 00000000..747d287b --- /dev/null +++ b/exercises/practice/dnd-character/.meta/solutions/lib/DndCharacter.pm @@ -0,0 +1,44 @@ +use strict; +use warnings; +use experimental qw; +use Feature::Compat::Class; + +class DndCharacter; + +use POSIX qw; + +sub modifier ( $class, $score ) { + return floor( ( $score - 10 ) / 2 ); +} + +sub ability { + my $d6 = sub { 1 + int( rand(6) ) }; + my $min = 6; + my $sum = 0; + for $_ ( 1 .. 4 ) { + my $die = $d6->(); + $sum += $die; + $min = $die if $min > $die; + } + return $sum - $min; +} + +field $strength :reader; +field $dexterity :reader; +field $constitution :reader; +field $intelligence :reader; +field $wisdom :reader; +field $charisma :reader; +field $hitpoints :reader; + +ADJUST { + $strength = __CLASS__->ability; + $dexterity = __CLASS__->ability; + $constitution = __CLASS__->ability; + $intelligence = __CLASS__->ability; + $wisdom = __CLASS__->ability; + $charisma = __CLASS__->ability; + $hitpoints = __CLASS__->modifier($constitution) + 10; +} + +1; diff --git a/exercises/practice/dnd-character/.meta/solutions/t/dnd-character.t b/exercises/practice/dnd-character/.meta/solutions/t/dnd-character.t new file mode 120000 index 00000000..f077d6e9 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/solutions/t/dnd-character.t @@ -0,0 +1 @@ +../../../t/dnd-character.t \ No newline at end of file diff --git a/exercises/practice/dnd-character/.meta/template-data.yaml b/exercises/practice/dnd-character/.meta/template-data.yaml new file mode 100644 index 00000000..50fa8b11 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/template-data.yaml @@ -0,0 +1,109 @@ +class: true +properties: + modifier: + test: |- + use Data::Dmp; + sprintf(<<~'END', $case->{input}{score}, $case->{expected}, dmp($case->{description})); + is( + DndCharacter->modifier(%d), + %d, + %s + ); + END + + ability: + test: |- + use Data::Dmp; + sprintf(<<~'END', dmp($case->{description})); + ok( + lives { + for my $i (1..100) { + my $score = DndCharacter->ability(); + die if $score < 3 || $score > 18; + } + }, + %s + ); + END + + character: + test: |- + use Data::Dmp; + if ($case->{description} eq 'random character is valid') { + sprintf(<<~'END', dmp($case->{description})); + ok( + lives { + for my $i (1..100) { + my $character = DndCharacter->new(); + die if $character->strength < 3 || $character->strength > 18; + die if $character->dexterity < 3 || $character->dexterity > 18; + die if $character->constitution < 3 || $character->constitution > 18; + die if $character->intelligence < 3 || $character->intelligence > 18; + die if $character->wisdom < 3 || $character->wisdom > 18; + die if $character->charisma < 3 || $character->charisma > 18; + die if $character->hitpoints != 10 + DndCharacter->modifier($character->constitution); + } + }, + %s + ); + END + } + elsif ($case->{description} eq 'each ability is only calculated once') { + sprintf(<<~'END', dmp($case->{description})); + ok( + lives { + for my $i (1..100) { + my $character = DndCharacter->new(); + die if $character->strength != $character->strength; + die if $character->dexterity != $character->dexterity; + die if $character->constitution != $character->constitution; + die if $character->intelligence != $character->intelligence; + die if $character->wisdom != $character->wisdom; + die if $character->charisma != $character->charisma; + } + }, + %s + ); + END + } + +stub: |- + # implement the DndCharacter class + +example: |- + use POSIX qw; + + sub modifier ($class, $score) { + return floor(($score - 10) / 2); + } + + sub ability { + my $d6 = sub { 1 + int(rand(6)) }; + my $min = 6; + my $sum = 0; + for $_ (1..4) { + my $die = $d6->(); + $sum += $die; + $min = $die if $min > $die; + } + return $sum - $min; + } + + + field $strength :reader; + field $dexterity :reader; + field $constitution :reader; + field $intelligence :reader; + field $wisdom :reader; + field $charisma :reader; + field $hitpoints :reader; + + ADJUST { + $strength = __CLASS__->ability; + $dexterity = __CLASS__->ability; + $constitution = __CLASS__->ability; + $intelligence = __CLASS__->ability; + $wisdom = __CLASS__->ability; + $charisma = __CLASS__->ability; + $hitpoints = __CLASS__->modifier($constitution) + 10; + } diff --git a/exercises/practice/dnd-character/.meta/tests.toml b/exercises/practice/dnd-character/.meta/tests.toml new file mode 100644 index 00000000..719043b2 --- /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/lib/DndCharacter.pm b/exercises/practice/dnd-character/lib/DndCharacter.pm new file mode 100644 index 00000000..5f12accd --- /dev/null +++ b/exercises/practice/dnd-character/lib/DndCharacter.pm @@ -0,0 +1,8 @@ +use v5.40; +use experimental qw; + +class DndCharacter; + +# implement the DndCharacter class + +1; diff --git a/exercises/practice/dnd-character/t/dnd-character.t b/exercises/practice/dnd-character/t/dnd-character.t new file mode 100755 index 00000000..a018e6ad --- /dev/null +++ b/exercises/practice/dnd-character/t/dnd-character.t @@ -0,0 +1,146 @@ +#!/usr/bin/env perl +use Test2::V0; + +use FindBin qw<$Bin>; +use lib "$Bin/../lib", "$Bin/../local/lib/perl5"; + +use DndCharacter (); + +is( # begin: 1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37 + DndCharacter->modifier(3), + -4, + "ability modifier: ability modifier for score 3 is -4" +); # end: 1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37 + +is( # begin: cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c + DndCharacter->modifier(4), + -3, + "ability modifier: ability modifier for score 4 is -3" +); # end: cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c + +is( # begin: 5b519fcd-6946-41ee-91fe-34b4f9808326 + DndCharacter->modifier(5), + -3, + "ability modifier: ability modifier for score 5 is -3" +); # end: 5b519fcd-6946-41ee-91fe-34b4f9808326 + +is( # begin: dc2913bd-6d7a-402e-b1e2-6d568b1cbe21 + DndCharacter->modifier(6), + -2, + "ability modifier: ability modifier for score 6 is -2" +); # end: dc2913bd-6d7a-402e-b1e2-6d568b1cbe21 + +is( # begin: 099440f5-0d66-4b1a-8a10-8f3a03cc499f + DndCharacter->modifier(7), + -2, + "ability modifier: ability modifier for score 7 is -2" +); # end: 099440f5-0d66-4b1a-8a10-8f3a03cc499f + +is( # begin: cfda6e5c-3489-42f0-b22b-4acb47084df0 + DndCharacter->modifier(8), + -1, + "ability modifier: ability modifier for score 8 is -1" +); # end: cfda6e5c-3489-42f0-b22b-4acb47084df0 + +is( # begin: c70f0507-fa7e-4228-8463-858bfbba1754 + DndCharacter->modifier(9), + -1, + "ability modifier: ability modifier for score 9 is -1" +); # end: c70f0507-fa7e-4228-8463-858bfbba1754 + +is( # begin: 6f4e6c88-1cd9-46a0-92b8-db4a99b372f7 + DndCharacter->modifier(10), + 0, + "ability modifier: ability modifier for score 10 is 0" +); # end: 6f4e6c88-1cd9-46a0-92b8-db4a99b372f7 + +is( # begin: e00d9e5c-63c8-413f-879d-cd9be9697097 + DndCharacter->modifier(11), + 0, + "ability modifier: ability modifier for score 11 is 0" +); # end: e00d9e5c-63c8-413f-879d-cd9be9697097 + +is( # begin: eea06f3c-8de0-45e7-9d9d-b8cab4179715 + DndCharacter->modifier(12), + 1, + "ability modifier: ability modifier for score 12 is +1" +); # end: eea06f3c-8de0-45e7-9d9d-b8cab4179715 + +is( # begin: 9c51f6be-db72-4af7-92ac-b293a02c0dcd + DndCharacter->modifier(13), + 1, + "ability modifier: ability modifier for score 13 is +1" +); # end: 9c51f6be-db72-4af7-92ac-b293a02c0dcd + +is( # begin: 94053a5d-53b6-4efc-b669-a8b5098f7762 + DndCharacter->modifier(14), + 2, + "ability modifier: ability modifier for score 14 is +2" +); # end: 94053a5d-53b6-4efc-b669-a8b5098f7762 + +is( # begin: 8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2 + DndCharacter->modifier(15), + 2, + "ability modifier: ability modifier for score 15 is +2" +); # end: 8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2 + +is( # begin: c3ec871e-1791-44d0-b3cc-77e5fb4cd33d + DndCharacter->modifier(16), + 3, + "ability modifier: ability modifier for score 16 is +3" +); # end: c3ec871e-1791-44d0-b3cc-77e5fb4cd33d + +is( # begin: 3d053cee-2888-4616-b9fd-602a3b1efff4 + DndCharacter->modifier(17), + 3, + "ability modifier: ability modifier for score 17 is +3" +); # end: 3d053cee-2888-4616-b9fd-602a3b1efff4 + +is( # begin: bafd997a-e852-4e56-9f65-14b60261faee + DndCharacter->modifier(18), + 4, + "ability modifier: ability modifier for score 18 is +4" +); # end: bafd997a-e852-4e56-9f65-14b60261faee + +ok( # begin: 4f28f19c-2e47-4453-a46a-c0d365259c14 + lives { + for my $i ( 1 .. 100 ) { + my $score = DndCharacter->ability(); + die if $score < 3 || $score > 18; + } + }, + "random ability is within range" +); # end: 4f28f19c-2e47-4453-a46a-c0d365259c14 + +ok( # begin: 385d7e72-864f-4e88-8279-81a7d75b04ad + lives { + for my $i ( 1 .. 100 ) { + my $character = DndCharacter->new(); + die if $character->strength < 3 || $character->strength > 18; + die if $character->dexterity < 3 || $character->dexterity > 18; + die if $character->constitution < 3 || $character->constitution > 18; + die if $character->intelligence < 3 || $character->intelligence > 18; + die if $character->wisdom < 3 || $character->wisdom > 18; + die if $character->charisma < 3 || $character->charisma > 18; + die if $character->hitpoints != 10 + DndCharacter->modifier( $character->constitution ); + } + }, + "random character is valid" +); # end: 385d7e72-864f-4e88-8279-81a7d75b04ad + +ok( # begin: dca2b2ec-f729-4551-84b9-078876bb4808 + lives { + for my $i ( 1 .. 100 ) { + my $character = DndCharacter->new(); + die if $character->strength != $character->strength; + die if $character->dexterity != $character->dexterity; + die if $character->constitution != $character->constitution; + die if $character->intelligence != $character->intelligence; + die if $character->wisdom != $character->wisdom; + die if $character->charisma != $character->charisma; + } + }, + "each ability is only calculated once" +); # end: dca2b2ec-f729-4551-84b9-078876bb4808 + +done_testing;