## DEV Community is a community of 695,394 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

# diceroller, a sample Rust project

Nicolas Frankel
Dev Advocate | Former developer | Former architect | Former teacher | Still learning and blogging.
Originally published at blog.frankel.ch ・9 min read

For me, the best learning process is regularly switching between learning and doing, theory and practice. The last post was research; hence, this one will be coding.

I've been a player of Role-Playing Games since I'm 11. Of course, I've played Dungeons & Dragons (mainly the so-called Advanced Edition), but after a few years, I've taken upon Champions and its HERO system. The system is based on points allotment and allows virtually everything regarding a character's abilities. For a brief description of the system, please check this brilliant albeit brilliant Stack Exchange answer. I've developed a sample application to handle (a part of) the damage generation subsystem for this post.

## Rolling dice

In RPG, some actions may either succeed or fail, e.g. climbing a cliff or hitting an enemy: the success depends on rolling dice. The HERO system is no different.

For that reason, our first task should be the modeling of rolling a dice. In RPGs, dice are not limited to being 6-sided.

``````struct Die {
faces: u8,
}
``````

Now that we have defined a dice, we need to be able to roll it: it entails randomness. Let's add the relevant crate to our build:

``````[dependencies]
rand = "0.8.4"
``````

The crate offers several PNRGs. We are not developing a lottery application; the default is good enough.

``````impl Die {
pub fn roll(self) -> u8 {
let mut rng = rand::thread_rng();            // 1
rng.gen_range(1..=self.faces)                // 2
}
}
``````
1. Retrieve the lazily-initialized thread-local PRNG
2. Return a random number between 1 and the number of faces, both ends inclusive

At this point, it's possible to create a dice:

``````let d6 = Die { faces: 6 };
``````

For better developer experience, we should create utility functions to create dice:

``````impl Die {
pub fn new(faces: u8) -> Die {
Die { faces }
}
pub fn d2() -> Die {
Self::new(2)
}
pub fn d4() -> Die {
Self::new(4)
}
pub fn d6() -> Die {
Self::new(6)
}
// Many more functions for other dice
}
``````

## DRY with macros

The above code is clearly not DRY. All `dN` functions look the same. It would be helpful to create a macro that parameterizes `N` so we could write a single function, and the compiler would generate the different implementations for us:

``````macro_rules! gen_dice_fn_for {
( \$( \$x:expr ),* ) => {
\$(
pub fn d\$x() -> Die {                         // 2
Self::new(\$x)                             // 3
}
)*
};
}

impl Die {
pub fn new(faces: u8) -> Die {
Die { faces }
}
gen_dice_fn_for![2, 4, 6, 8, 10, 12, 20, 30, 100];   // 4
}
``````
1. Don't warn if it's not used, it's expected
2. Parameterize function name
3. Parameterize function body
4. Enjoy!

But the code doesn't compile:

``````error: expected one of `(` or `<`, found `2`
--> src/droller/dice.rs:9:21
|
9  |             pub fn d\$x() -> Die {
|                     ^^ expected one of `(` or `<`
...
21 |     gen_dice_fn_for![2, 4, 6, 8, 10, 12, 20, 30, 100];
|     -------------------------------------------------- in this macro invocation
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
``````

Rust macros don't allow to parameterize the function's name, only its body.
After some research, I found the `paste` crate:

This crate provides a flexible way to paste together identifiers in a macro, including using pasted identifiers to define new items.

Let's add the crate to our project:

``````[dependencies]
paste = "1.0.5"
``````

Then use it:

``````macro_rules! gen_dice_fn_for {
( \$( \$x:expr ),* ) => {
paste! {                            // 1
\$(
pub fn [<d\$x>]() -> Die {       // 2
Self::new(\$x)
}
)*
}
};
}
``````
1. Open the `paste` directive
2. Generate a function name using `x`

## Default die

We now have plenty of different dice to use. Yet, in the HERO System, the only die is the standard d6. In some cases, you'd roll half a d6, i.e., a d3, but this is rare.

It is a good case for the `Default` trait. Rust defines it as:

``````pub trait Default: Sized {
/// Returns the "default value" for a type.
///
/// Default values are often some kind of initial value, identity value, or anything else that
/// may make sense as a default.
#[stable(feature = "rust1", since = "1.0.0")]
fn default() -> Self;
}
``````

It makes sense to implement `Default` for `Die` and return a 6-sided die.

``````impl Default for Die {
fn default() -> Self {
Die::d6()
}
}
``````

We can now call `Die::default()` to get a d6.

## Non-zero checks

Using a `u8` prevents having invalid a negative number of faces. But a dice should have at least one side. Hence, we could benefit from adding a non-zero check when creating a new `Die`.

The most straightforward way is to add an `if` check at the start of the `new()` and `dN()` functions. But I did a bit of research and stumbled upon the non-zero integer types. We can rewrite our `Die` implementation accordingly:

``````impl Die {
pub fn new(faces: u8) -> Die {
let faces = NonZeroU8::new(faces)       // 1
.unwrap()                           // 2
.get();                             // 3
Die { faces }
}
}
``````
1. Wrap the `u8` into a non-zero type
2. Unwrap it into an `Option`
3. Get the underlying `u8` value if it's strictly positive or `panic` otherwise

When I wrote the code, I thought it was a good idea. As I'm writing the blog post, I think this is a good sample of overengineering.

The idea is to fail fast. Otherwise, we would need to cope with the `Option` type throughout the application. `if faces == 0 { panic!("Value must be strictly positive {}", faces); }` would be much simpler and achieve the same. KISS.

## Rolling for damage

RPGs imply fights, and fights mean dealing damage to your opponents. The HERO system is no different. It models two properties of a character: its ability to stay conscious and stay alive, respectively the `STUN` and `BODY` characteristics.

The damage itself can be of two different types: blunt trauma, i.e. `NormalDamage`, and `KillingDamage`. Let's focus on the former type first.

For each normal damage die, the rules are simple:

• The number of `STUN` damage is the roll
• The number of `BODY` depends on the roll: `0` for `1`, `2` for `6`, and `1` in all other cases.

We can implement it as the following:

``````pub struct Damage {
pub stun: u8,
pub body: u8,
}

pub struct NormalDamageDice {
number: u8,
}

impl NormalDamageDice {
pub fn new(number: u8) -> NormalDamageDice {
let number = NonZeroU8::new(number).unwrap().get();
NormalDamageDice { number }
}
pub fn roll(self) -> Damage {
let mut stun = 0;
let mut body = 0;
for _ in 0..self.number {
let die = Die::default();
let roll = die.roll();
stun += roll;
if roll == 1 {
} else if roll == 6 {
body += 2
} else {
body += 1
}
}
Damage { stun, body }
}
}
``````

While it works, it involves mutability. Let's rewrite a functional version:

``````impl NormalDamageDice {
pub fn roll(self) -> Damage {
(0..self.number)                     // 1
.map(|_| Die::default())         // 2
.map(|die| die.roll())           // 3
.map(|stun| {
let body = match stun {      // 4
1 => 0,
6 => 2,
_ => 1,
};
Damage { stun, body }        // 5
})
.sum()                           // 6
}
}
``````
1. For each damage die
2. Create a d6
3. Roll it
5. Create the `Damage` with the `STUN` and `BODY`
6. Aggregate it

The above code doesn't compile:

``````error[E0277]: the trait bound `NormalDamage: Sum` is not satisfied
--> src/droller/damage.rs:89:14
|
89 |             .sum::<NormalDamage>();
|              ^^^ the trait `Sum` is not implemented for `NormalDamage`
``````

Rust doesn't know how to add two `Damage` together! It's as simple as adding their `STUN` and `BODY`. To fix the compilation error, we need to implement the `Sum` trait for `NormalDamage`.

``````impl Sum for NormalDamage {
fn sum<I: Iterator<Item = Self>>(iter: I) - Self {
iter.fold(NormalDamage::zero(), |dmg1, dmg2| NormalDamage {
stun: dmg1.stun + dmg2.stun,
body: dmg1.body + dmg2.body,
})
}
}
``````

## Printing Damage

So far, to print a `Damage`, we need to its `stun` and `body` properties:

``````let one_die = NormalDamageDice::new(1);
let damage = one_die.roll();
println!("stun: {}, body: {}", damage.stun, damage.body);
``````

Printing `Damage` is a pretty standard use case. We want to write the following:

``````let one_die = NormalDamageDice::new(1);
let damage = one_die.roll();
println!("damage: {}", damage);
``````

For that, we need to implement `Display` for `Damage`:

``````impl Display for Damage {
fn fmt(&self, f: &mut Formatter<'_>) - std::fmt::Result {
write!(f, "stun: {}, body: {}", self.stun, self.body)
}
}
``````

I believe doing that for most of your `struct` is a good practice.

## Making Damage a trait

The next step is to implement `KillingDamageDice`. The computation is different than for normal damage. For each die, we roll the `BODY`. Then we roll for a multiplier. The `STUN` is the `BODY` times `mult`. Our current code rolls `mult`, but we don't store it in the `Damage` structure. To do that, we need to introduce a `KillingDamage` structure:

``````pub struct KillingDamage {
pub body: u8,
pub mult: u8,
}
``````

But with this approach, we cannot get the `STUN` amount. Hence, the next step is to make `Damage` a trait.

``````pub trait Damage {
fn stun(self) -> u8;
fn body(self) -> u8;
}

impl Damage for NormalDamage {
fn stun(self) -> u8 {
self.stun
}
fn body(self) -> u8 {
self.body
}
}

impl Damage for KillingDamage {
fn stun(self) -> u8 {
self.body * self.mult
}
fn body(self) -> u8 {
self.body
}
}
``````

At this point, the code doesn't compile anymore as Rust functions cannot return a trait.

``````error[E0277]: the size for values of type `(dyn Damage + 'static)` cannot be known at compilation time
--> src/droller/damage.rs:86:26
|
86 |     pub fn roll(self) -> Damage {
|                          ^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `(dyn Damage + 'static)`
= note: the return type of a function must have a statically known size
``````

The fix is straightforward with the `Box` type.

Boxes don’t have performance overhead, other than storing their data on the heap instead of on the stack. But they don’t have many extra capabilities either. You’ll use them most often in these situations:

• When you have a type whose size can’t be known at compile time and you want to use a value of that type in a context that requires an exact size

Let's wrap the return value in a `Box` to correct the compilation error.

``````pub fn roll(self) -> Box<dyn Damage> {
// let damage = ...
Box::new(damage)
}
``````

It now compiles successfully.

## Display for traits

With `Damage` being a trait, we need to change the `println!()` part of the application:

``````let normal_die = NormalDamageDice::new(1);
let normal_dmg = normal_die.roll();
println!("normal damage: {}", normal_dmg);
let killing_die = KillingDamageDice::new(1);
let killing_dmg = killing_die.roll();
println!("killing damage: {}", killing_dmg);
``````

But this snippet doesn't compile:

``````error[E0277]: `dyn Damage` doesn't implement `std::fmt::Display`
--> src/main.rs:8:35
|
8 |     println!("normal damage: {}", normal_dmg);
|                                   ^^^^^^^^^^ `dyn Damage` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `dyn Damage`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required because of the requirements on the impl of `std::fmt::Display` for `Box<dyn Damage>`
= note: required by `std::fmt::Display::fmt`
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
``````

To fix that, we need to make `Damage` a "subtrait" of `Display`.

``````pub trait Damage: Display {
fn stun(self) -> u8;
fn body(self) -> u8;
}
``````

Finally, we need to implement `Display` for `NormalDamage` and `KillingDamage`.

## Conclusion

In this post, I wrote about my steps in implementing damage rolling for the HERO System and the most exciting bits on Rust. The project doesn't stop there yet. I may continue to develop it to deepen my understanding further as it makes for an excellent use case.

As a side note, you might have noticed that I didn't write any test. It's not an oversight. The reason for that is because randomness makes most low-level tests useless. On a meta-level, and despite widespread beliefs, it means one can design in increments without TDD.

The complete source code for this post can be found on Github:

## ajavageek / diceroller

To go further:

Originally published at A Java Geek on July 25th, 2021

## Discussion (3)

YJDoc2

Hey, really nice post 👍
I really liked the step by step explanation, and liked the writing style as well, intermixing of code and explanation was well spaced, so it didn't feel too much code or too much text at any point :)
Thanks for the post 😁
PS : I think some of your code example has syntax issue? Eg in return Box or sum impl, a '<' seems to have converted to '//' .

Nicolas Frankel

Thanks for your feedback. I've fixed the syntax.

chenge

Good post!
Rust without TDD.