Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add D&D Character exercise #1666

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a9f81d
Merge branch 'exercism:main' into main
dem4ron Mar 14, 2023
bd24a13
Merge branch 'exercism:main' into main
dem4ron Mar 14, 2023
6e69740
Merge branch 'exercism:main' into main
dem4ron Apr 1, 2023
b75d4e6
Add difficulty as int, further format fn name
dem4ron Apr 3, 2023
62570bb
Add util fn to escape double quotes
dem4ron Apr 3, 2023
88fb1c4
Simplify escape double quotes algo
dem4ron Apr 3, 2023
a93d31a
Add missing end line
dem4ron Apr 3, 2023
912b761
Add dnd character exercise
dem4ron Apr 4, 2023
23bbd89
Change wording
dem4ron Apr 5, 2023
81fce76
Remove first ignore
dem4ron Apr 5, 2023
f49433e
Format example, improve one test
dem4ron Apr 5, 2023
20f9f83
Better parameters
dem4ron Apr 5, 2023
004db46
Same for stub file
dem4ron Apr 5, 2023
2e25f85
Improve valid char test case
dem4ron Apr 5, 2023
d3843c4
Merge branch 'exercism:main' into main
dem4ron Apr 7, 2023
fd9a12c
Merge branch 'main' into add-dnd-character
dem4ron Apr 7, 2023
c2bdc3b
Improve exemplar solution
dem4ron Apr 7, 2023
2d99477
Rename macro
dem4ron Apr 7, 2023
5ac82f2
Improve last test, add missing end line
dem4ron Apr 7, 2023
dceaa30
Merge branch 'exercism:main' into main
dem4ron Apr 7, 2023
5ec087e
Simplify exercise
dem4ron Apr 7, 2023
ed1112a
Merge branch 'main' of https://github.com/exercism/rust
dem4ron Apr 7, 2023
226a0e7
Merge branch 'main' of https://github.com/dem4ron/rust-exercism
dem4ron Apr 7, 2023
66e74eb
Merge branch 'main' into add-dnd-character
dem4ron Apr 7, 2023
82e2081
Fill in struct details in stub file
dem4ron Apr 7, 2023
1336170
Fix exemplar's struct
dem4ron Apr 8, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ bin/configlet.exe
bin/exercise
bin/exercise.exe
bin/generator-utils/ngram
bin/generator-utils/escape_double_quotes
exercises/*/*/Cargo.lock
exercises/*/*/clippy.log
canonical_data.json
Expand Down
2 changes: 1 addition & 1 deletion bin/add_practice_exercise
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ message "info" "Adding instructions and configuration files..."
uuid=$(bin/configlet uuid)

# Add exercise-data to global config.json
jq --arg slug "$slug" --arg uuid "$uuid" --arg name "$exercise_name" --arg difficulty "$exercise_difficulty" \
jq --arg slug "$slug" --arg uuid "$uuid" --arg name "$exercise_name" --argjson difficulty "$exercise_difficulty" \
'.exercises.practice += [{slug: $slug, name: $name, uuid: $uuid, practices: [], prerequisites: [], difficulty: $difficulty}]' \
config.json >config.json.tmp
# jq always rounds whole numbers, but average_run_time needs to be a float
Expand Down
14 changes: 7 additions & 7 deletions bin/generate_tests
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#!/usr/bin/env bash


# Exit if anything fails.
set -euo pipefail


# see comment in generator-utils/utils.sh
# shellcheck source=bin/generator-utils/utils.sh
# shellcheck source=./generator-utils/utils.sh
source ./bin/generator-utils/utils.sh

if [ ! -e bin/generator-utils/escape_double_quotes ]; then
message "info" "Building util function"
cd util/escape_double_quotes && ./build && cd ../..
fi

digest_template() {
local template
template=$(cat bin/test_template)
template=$(bin/generator-utils/escape_double_quotes bin/test_template)
# Turn every token into a jq command

echo "$template" | sed 's/${\([^}]*\)\}\$/$(echo $case | jq -r '\''.\1'\'')/g'
Expand All @@ -35,7 +38,6 @@ EOT
# Flattens canonical json, extracts only the objects with a uuid
cases=$(echo "$canonical_json" | jq '[ .. | objects | with_entries(select(.key | IN("uuid", "description", "input", "expected", "property"))) | select(. != {}) | select(has("uuid")) ]')


# Shellcheck doesn't recognize that `case` is not unused

# shellcheck disable=SC2034
Expand All @@ -45,9 +47,8 @@ jq -c '.[]' <<<"$cases" | while read -r case; do
eval_template="$(digest_template | sed -e "s/\$(\(.*\))/\$\(\1\)/g")"
eval_template="$(eval "echo \"$eval_template\"")"


# Turn function name into snake_case
formatted_template=$(echo "$eval_template" | sed -e ':loop' -e 's/\(fn[^(]*\)[ -]/\1_/g' -e 't loop' | sed 's/fn_/fn /')
formatted_template=$(echo "$eval_template" | sed -E -e '/^fn/!b' -e 's/[^a-zA-Z0-9_{}()[:space:]-]//g' -e 's/([[:upper:]])/ \L\1/g' -e 's/(fn[[:space:]]+)([a-z0-9_-]+)/\1\L\2/g' -e 's/ /_/g' -e 's/_\{/\{/g' -e 's/-/_/g' | sed 's/fn_/fn /' | sed 's/__\+/_/g')

# Push to test file
echo "$formatted_template" >>"$test_file"
Expand All @@ -58,4 +59,3 @@ done
rustfmt "$test_file"

message "success" "Generated tests successfully! Check out ${test_file}"

5 changes: 2 additions & 3 deletions bin/test_template
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#[test]
#[ignore]
fn ${description}$() {

assert_eq!(${property}$(${input}$), ${expected}$);
let expected = "${expected}$";
assert_eq!(${property}$(${input}$), expected);
}

9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,15 @@
"strings"
],
"status": "deprecated"
},
{
"slug": "dnd-character",
"name": "D&D Character",
"uuid": "8f52127a-ad81-46a2-b734-a76117138aae",
"practices": [],
"prerequisites": [],
"difficulty": 1,
"topics":["structs", "randomness"]
}
],
"foregone": [
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
8 changes: 8 additions & 0 deletions exercises/practice/dnd-character/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Generated by Cargo
# Will have compiled files and executables
/target/
**/*.rs.bk

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock
Cargo.lock
20 changes: 20 additions & 0 deletions exercises/practice/dnd-character/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"authors": [
"dem4ron"
],
"files": {
"solution": [
"src/lib.rs",
"Cargo.toml"
],
"test": [
"tests/dnd-character.rs"
],
"example": [
".meta/example.rs"
]
},
"blurb": "Randomly generate Dungeons & Dragons characters.",
"source": "Simon Shine, Erik Schierboom",
"source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945"
}
73 changes: 73 additions & 0 deletions exercises/practice/dnd-character/.meta/example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use rand::prelude::*;

pub fn modifier(ability_score: u8) -> f32 {
((ability_score as i8 - 10) as f32 / 2.0).floor()
}

pub struct Character {
strength: u8,
dexterity: u8,
constitution: u8,
intelligence: u8,
wisdom: u8,
charisma: u8,
hitpoints: u8,
}

impl Character {
pub fn new() -> Self {
let constitution = Character::calculate_ability_score(Character::roll_four_dice());
Character {
strength: Character::calculate_ability_score(Character::roll_four_dice()),
dexterity: Character::calculate_ability_score(Character::roll_four_dice()),
constitution,
intelligence: Character::calculate_ability_score(Character::roll_four_dice()),
wisdom: Character::calculate_ability_score(Character::roll_four_dice()),
charisma: Character::calculate_ability_score(Character::roll_four_dice()),
hitpoints: 10 + modifier(constitution) as u8,
}
}

pub fn roll_four_dice() -> [u8; 4] {
let mut rng = thread_rng();
let mut rolls = [0; 4];
for i in 0..4 {
rolls[i] = rng.gen_range(1..=4)
}
rolls
}

pub fn calculate_ability_score(ability_dice_rolls: [u8; 4]) -> u8 {
let mut ability_dice_rolls = ability_dice_rolls;
ability_dice_rolls.sort();
ability_dice_rolls[1..].iter().sum()
}
Comment on lines +31 to +44
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider having just one random_ability/generate_ability/random_ability_score function?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions could also be moved outside of the Character's impl, as they don't rely on the character.


pub fn strength(&self) -> u8 {
self.strength
}

pub fn dexterity(&self) -> u8 {
self.dexterity
}

pub fn constitution(&self) -> u8 {
self.constitution
}

pub fn intelligence(&self) -> u8 {
self.intelligence
}

pub fn wisdom(&self) -> u8 {
self.wisdom
}

pub fn charisma(&self) -> u8 {
self.charisma
}

pub fn hitpoints(&self) -> u8 {
self.hitpoints
}
}
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"
9 changes: 9 additions & 0 deletions exercises/practice/dnd-character/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "dnd_character"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.5"
47 changes: 47 additions & 0 deletions exercises/practice/dnd-character/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pub fn modifier(_ability_score: u8) -> f32 {
unimplemented!("Implement a utility function that calculates the ability modifier for a given ability score")
}

pub struct Character {}

impl Character {
pub fn new() -> Self {
unimplemented!("Create a function that generates a new random character with the following ability scores and hitpoints")
}

pub fn roll_four_dice() -> [u8; 4] {
unimplemented!("Create a utility function that rolls four dice")
}

pub fn calculate_ability_score(_ability_dice_rolls: [u8; 4]) -> u8 {
unimplemented!("Calculate the ability score from the given rolled dice")
}

pub fn strength(&self) -> u8 {
unimplemented!("Return the strength score of the character")
}

pub fn dexterity(&self) -> u8 {
unimplemented!("Return the dexterity score of the character")
}

pub fn constitution(&self) -> u8 {
unimplemented!("Return the constitution score of the character")
}

pub fn intelligence(&self) -> u8 {
unimplemented!("Return the intelligence score of the character")
}

pub fn wisdom(&self) -> u8 {
unimplemented!("Return the wisdom score of the character")
}

pub fn charisma(&self) -> u8 {
unimplemented!("Return the charisma score of the character")
}

pub fn hitpoints(&self) -> u8 {
unimplemented!("Return the hitpoints of the character")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly torn whether these methods are a good or a bad idea.

  • The plus: users are free to implement the struct as they see fit
  • The downside: it seems like quite boilerplatey

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true about the downside, but I think it is challenging to avoid boilerplate code in this exercise, given its classic object-oriented nature.

I personally enjoy having these getter methods, as it's allowing me to access attributes like Character.strength(). I included these methods in the stub file to ensure that the rust-analyzer doesn't show any errors when processing the test file.

on the other hand it is a perfect spot to write a macro, as I just did in the updated example file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll let @petertseng weigh in. The methods seem like overkill to me over regular struct access, but 🤷

}
Loading