Skip to content

Commit

Permalink
dnd-character: add new exercise (#782)
Browse files Browse the repository at this point in the history
This exercise was created in exercism/problem-specifications#1397.

It is a basic exercise in random generators, so the idea I had for the
Haskell track was to practice making QuickCheck generators rather than
simply use the standard random generator.

For this reason, QuickCheck is added as a root dependency.

The 1.1.0 test case added in exercism/problem-specifications#1416 does
not apply to QuickCheck generators.
  • Loading branch information
sshine authored Dec 14, 2018
1 parent 5780e1a commit be093ec
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 0 deletions.
10 changes: 10 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,16 @@
"maybe"
]
},
{
"slug": "dnd-character",
"uuid": "138db4fc-ee70-11e8-8eb2-f2801f1b9fd1",
"core": false,
"unlocked_by": "pangram",
"difficulty": 3,
"topics": [
"random"
]
},
{
"slug": "anagram",
"uuid": "67c53230-82e8-4207-a811-abec27bef381",
Expand Down
6 changes: 6 additions & 0 deletions exercises/dnd-character/.meta/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Hints

This exercise uses QuickCheck generators. Here are some resources to get started:

- [A QuickCheck Tutorial: Generators](https://www.stackbuilders.com/news/a-quickcheck-tutorial-generators)
- The documentation for [Test.QuickCheck.Gen](http://hackage.haskell.org/package/QuickCheck-2.12.6.1/docs/Test-QuickCheck-Gen.html)
101 changes: 101 additions & 0 deletions exercises/dnd-character/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# D&D Character

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].

[DND]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons
[Troll]: http://hjemmesider.diku.dk/~torbenm/Troll/

## Hints

This exercise uses QuickCheck generators. Here are some resources to get started:

- [A QuickCheck Tutorial: Generators](https://www.stackbuilders.com/news/a-quickcheck-tutorial-generators)
- The documentation for [Test.QuickCheck.Gen](http://hackage.haskell.org/package/QuickCheck-2.12.6.1/docs/Test-QuickCheck-Gen.html)



## Getting Started

For installation and learning resources, refer to the
[exercism help page](http://exercism.io/languages/haskell).

## Running the tests

To run the test suite, execute the following command:

```bash
stack test
```

#### If you get an error message like this...

```
No .cabal file found in directory
```

You are probably running an old stack version and need
to upgrade it.

#### Otherwise, if you get an error message like this...

```
No compiler found, expected minor version match with...
Try running "stack setup" to install the correct GHC...
```

Just do as it says and it will download and install
the correct compiler version:

```bash
stack setup
```

## Running *GHCi*

If you want to play with your solution in GHCi, just run the command:

```bash
stack ghci
```

## Feedback, Issues, Pull Requests

The [exercism/haskell](https://github.com/exercism/haskell) repository on
GitHub is the home for all of the Haskell exercises.

If you have feedback about an exercise, or want to help implementing a new
one, head over there and create an issue. We'll do our best to help you!

## Source

Simon Shine, Erik Schierboom [https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945](https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945)

## Submitting Incomplete Solutions
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
17 changes: 17 additions & 0 deletions exercises/dnd-character/examples/success-standard/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: dnd-character

dependencies:
- base
- QuickCheck

library:
exposed-modules: DND
source-dirs: src

tests:
test:
main: Tests.hs
source-dirs: test
dependencies:
- dnd-character
- hspec
37 changes: 37 additions & 0 deletions exercises/dnd-character/examples/success-standard/src/DND.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{-# LANGUAGE RecordWildCards #-}
module DND (Character(..), ability, modifier, character) where

import Control.Monad (replicateM)
import Test.QuickCheck.Gen (Gen, choose)

data Character = Character
{ name :: String
, strength :: Int
, dexterity :: Int
, constitution :: Int
, intelligence :: Int
, wisdom :: Int
, charisma :: Int
, hitpoints :: Int
}
deriving (Show, Eq)

modifier :: Int -> Int
modifier a = (a - 10) `div` 2

ability :: Gen Int
ability = do
ds <- replicateM 4 (choose (1, 6))
return (sum ds - minimum ds)

character :: Gen Character
character = do
let name = "Bob"
strength <- ability
dexterity <- ability
constitution <- ability
intelligence <- ability
wisdom <- ability
charisma <- ability
let hitpoints = 10 + modifier constitution
return Character{..}
22 changes: 22 additions & 0 deletions exercises/dnd-character/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: dnd-character
version: 1.1.0.0

dependencies:
- base
- QuickCheck

library:
exposed-modules: DND
source-dirs: src
ghc-options: -Wall
# dependencies:
# - foo # List here the packages you
# - bar # want to use in your solution.

tests:
test:
main: Tests.hs
source-dirs: test
dependencies:
- dnd-character
- hspec
31 changes: 31 additions & 0 deletions exercises/dnd-character/src/DND.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module DND ( Character(..)
, ability
, modifier
, character
) where

import Test.QuickCheck (Gen)

data Character = Character
{ name :: String
, strength :: Int
, dexterity :: Int
, constitution :: Int
, intelligence :: Int
, wisdom :: Int
, charisma :: Int
, hitpoints :: Int
}
deriving (Show, Eq)

modifier :: Int -> Int
modifier =
error "You need to implement this function."

ability :: Gen Int
ability =
error "You need to implement this generator."

character :: Gen Character
character =
error "You need to implement this generator."
1 change: 1 addition & 0 deletions exercises/dnd-character/stack.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
resolver: lts-12.4
67 changes: 67 additions & 0 deletions exercises/dnd-character/test/Tests.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-# LANGUAGE RecordWildCards #-}

import Data.Foldable (for_)
import Test.Hspec (Spec, describe, it, shouldBe)
import Test.Hspec.Runner (configFastFail, defaultConfig, hspecWith)
import Test.QuickCheck
import Text.Printf (printf)

import DND (Character(..), ability, modifier, character)

data Case = Case { input :: Int
, expected :: Int
}

type Ability = Character -> Int

main :: IO ()
main = hspecWith defaultConfig {configFastFail = True} specs

specs :: Spec
specs = do
describe "modifier" $ for_ cases test

describe "ability" $ it "generates values within range" $
property (forAll ability abilityWithinRange)

describe "character" $ it "generates valid characters" $
property (forAll character characterIsValid)
where
test Case{..} = it description assertion
where
description = printf "computes the modifier for %2d to be %2d" input expected
assertion = modifier input `shouldBe` expected

cases :: [Case]
cases = map (uncurry Case)
[ (18, 4), (17, 3), (16, 3), (15, 2), (14, 2)
, (13, 1), (12, 1), (11, 0), (10, 0), (9, -1)
, (8, -1), (7, -2), (6, -2), (5, -3), (4, -3)
, (3, -4) ]

abilityWithinRange :: Int -> Bool
abilityWithinRange ability' = ability' >= 3 && ability' <= 18

characterIsValid :: Character -> Property
characterIsValid character' =
conjoin $
hitpointsAddUp character' :
map (abilityWithinRange' character') abilities
where
abilities = [ ("strength", strength)
, ("dexterity", dexterity)
, ("constitution", constitution)
, ("intelligence", intelligence)
, ("wisdom", wisdom)
, ("charisma", charisma) ]

abilityWithinRange' :: Character -> (String, Ability) -> Property
abilityWithinRange' character' (name, ability') =
counterexample msg (abilityWithinRange (ability' character'))
where msg = "The '" ++ name ++ "' ability is out of range"

hitpointsAddUp :: Character -> Property
hitpointsAddUp Character{..} =
counterexample msg (hitpoints === 10 + modifier constitution)
where msg = "The 'hitpoints' are not 10 + constitution modifier"

0 comments on commit be093ec

Please sign in to comment.