Skip to content

Commit

Permalink
new dnd-character exercise (#203)
Browse files Browse the repository at this point in the history
* new dnd-character exercise

* removed debugging line (belatedly)
  • Loading branch information
colinleach authored Nov 15, 2023
1 parent 5bc1ef6 commit 5ea6739
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 0 deletions.
9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,15 @@
"prerequisites": [],
"difficulty": 2
},
{
"slug": "dnd-character",
"name": "Dnd Character",
"uuid": "4cfffba1-0f61-4c18-b2a2-7890c1465227",
"practices": [],
"prerequisites": [],
"difficulty": 1,
"topics": null
},
{
"slug": "all-your-base",
"name": "All Your Base",
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
29 changes: 29 additions & 0 deletions exercises/practice/dnd-character/.meta/Example.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Here is an example solution for the Dnd Character exercise
*/
component {

// simple implementation has disadvantage of breaking encapsulation
this.strength = ability();
this.dexterity = ability();
this.constitution = ability();
this.intelligence = ability();
this.wisdom = ability();
this.charisma = ability();
this.hitpoints = 10 + abilityModifier( this.constitution );

function abilityModifier( score ) {
return floor((score - 10) / 2)
}

function ability() {
var rolls = [roll(), roll(), roll(), roll()];
arraySort(rolls, 'numeric', 'desc');
return rolls.slice(1, 3).sum();
}

private function roll() {
return randRange(1, 6);
}

}
7 changes: 7 additions & 0 deletions exercises/practice/dnd-character/.meta/ExampleTest.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
component extends="DndCharacterTest" {

function beforeAll(){
SUT = createObject( 'Solution' );
}

}
17 changes: 17 additions & 0 deletions exercises/practice/dnd-character/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": ["colinleach"],
"files": {
"solution": [
"DndCharacter.cfc"
],
"test": [
"DndCharacterTest.cfc"
],
"example": [
".meta/Example.cfc"
]
},
"blurb": "Randomly generate Dungeons & Dragons characters.",
"source": "Simon Shine, Erik Schierboom",
"source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945"
}
67 changes: 67 additions & 0 deletions exercises/practice/dnd-character/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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"
15 changes: 15 additions & 0 deletions exercises/practice/dnd-character/DndCharacter.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Your implementation of the Dnd Character exercise
*/
component {
// Implement strength, dexterity, constitution, wisdom, charisma, hitpoints

function abilityModifier( score ) {
// Implement me here
}

function ability() {
// Implement me here
}

}
75 changes: 75 additions & 0 deletions exercises/practice/dnd-character/DndCharacterTest.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
component extends="testbox.system.BaseSpec" {

function beforeAll(){
SUT = createObject( 'DndCharacter' );
}

function inRange( score ) {
return score >= 3 && score <= 18;
}

function run(){

describe( "My Dnd Character class", function(){

describe( 'ability modifier', function(){

it( 'ability modifier for score 3 is -4', function(){
expect( SUT.abilityModifier( 3 ) ).toBe( -4 );
});

it( 'ability modifier for score 4 is -43', function(){
expect( SUT.abilityModifier( 4 ) ).toBe( -3 );
});

it( 'ability modifier for score 5 is -3', function(){
expect( SUT.abilityModifier( 5 ) ).toBe( -3 );
});

it( 'ability modifier for score 6 is -2', function(){
expect( SUT.abilityModifier( 6 ) ).toBe( -2 );
});

it( 'ability modifier for score 7 is -2', function(){
expect( SUT.abilityModifier( 7 ) ).toBe( -2 );
});

it( 'ability modifier for score 8 is -1', function(){
expect( SUT.abilityModifier( 8 ) ).toBe( -1 );
});
});

describe( 'random', function(){

it( 'random ability is within range', function(){
for (i=1; i<=10; i++) {
expect( SUT.ability() ).toSatisfy( inRange );
}
});

it( 'random character is valid', function(){
for (i=1; i<=10; i++) {
expect( SUT.strength ).toSatisfy( inRange );
expect( SUT.dexterity ).toSatisfy( inRange );
expect( SUT.constitution ).toSatisfy( inRange );
expect( SUT.intelligence ).toSatisfy( inRange );
expect( SUT.wisdom ).toSatisfy( inRange );
expect( SUT.charisma ).toSatisfy( inRange );
expect( SUT.hitpoints ).toBe( 10 + SUT.abilityModifier( SUT.constitution ))
}
});

it( 'each ability is only calculated once', function(){
expect( SUT.strength ).toBe( SUT.strength );
expect( SUT.dexterity ).toBe( SUT.dexterity );
expect( SUT.constitution ).toBe( SUT.constitution );
expect( SUT.intelligence ).toBe( SUT.intelligence );
expect( SUT.wisdom ).toBe( SUT.wisdom );
expect( SUT.charisma ).toBe( SUT.charisma );
});

});

});
}
}
103 changes: 103 additions & 0 deletions exercises/practice/dnd-character/TestRunner.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* I am a CommandBox task runner which you can use to test your implementation of this exercise against the
* provided test suite. To use me, open the CommandBox CLI and run this:
*
* CommandBox> task run TestRunner
*
* To start up a test watcher that will automatically rerun the test suite every time you save a file change, run this:
*
* CommandBox> task run TestRunner --watcher
*
*/
component {

/**
* @solution Runs the tests against the solution
* @watcher Start up a file watch that re-runs the tests on file changes. Use Ctrl-C to stop
*/
function run( boolean solution=false, boolean watcher=false ) {

ensureTestBox();

if( watcher ) {

// Tabula rasa
command( 'cls' ).run();

// Start watcher
watch()
.paths( '*.cfc' )
.inDirectory( getCWD() )
.withDelay( 500 )
.onChange( function() {

// Clear the screen
command( 'cls' )
.run();

// This is neccessary so changes to tests get picked up right away.
pagePoolClear();

runTests( solution );

} )
.start();

} else {
runTests( solution );
}

}

/**
* Make sure the TestBox framework is installed
*/
private function ensureTestBox() {
var excerciseRoot = getCWD();
var testBoxRoot = excerciseRoot & '/testbox';

if( !directoryExists( testBoxRoot ) ) {

print.boldYellowLine( 'Installing some missing dependencies for you!' ).toConsole();
command( 'install' )
.inWorkingDirectory( excerciseRoot )
.run();
}

// Bootstrap TestBox framework
filesystemUtil.createMapping( '/testbox', testBoxRoot );
}

/**
* Invoke TestBox to run the test suite
*/
private function runTests( boolean solution=false ) {

// Create TestBox and run the tests
testData = new testbox.system.TestBox()
.runRaw( directory = {
// Find all CFCs...
mapping = filesystemUtil.makePathRelative( getCWD() ),
// ... in this directory ...
recurse = false,
// ... whose name ends in "test"
filter = function( path ) {
return path.reFind( ( solution ? 'Solution' : '' ) & 'Test.cfc$' );
}
} )
.getMemento();

// Print out the results with ANSI formatting for the CLI
getInstance( 'CLIRenderer@testbox-commands' )
.render( print, testData, true );

print.toConsole();

// Set proper exit code
if( testData.totalFail || testData.totalError ) {
setExitCode( 1 );
}
}

}

8 changes: 8 additions & 0 deletions exercises/practice/dnd-character/box.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"dependencies":{
"testbox":"^2.5.0+107"
},
"installPaths":{
"testbox":"testbox/"
}
}
Loading

0 comments on commit 5ea6739

Please sign in to comment.