diceroller, a sample Rust project
Tags: beginners, rust, rpg, randomness
Published: 2021-07-25 16:07:01

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](https://rpg.stackexchange.com/questions/6383/how-does-hero-system-differ-from-other-systems#answer-6389). I've developed a sample application to handle (a part of) the damage generation subsystem for this post.

## Rolling dice

In <abbr title="Role-Playing Game">RPG</abbr>, some actions may either succeed or fail, <em>e.g.</em> 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.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/laxkk02j81go6po7dfh5.jpg)

```rust
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:

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

The crate offers several <abbr title="Pseudo-Random Number Generator">PNRG</abbr>s. We are not developing a lottery application; the default is good enough.

```rust
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:

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

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

```rust
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 <abbr title="Dont' Repeat Yourself">DRY</abbr>. All <code>dN</code> functions look the same. It would be helpful to create a macro that parameterizes <code>N</code> so we could write a single function, and the compiler would generate the different implementations for us:

```rust
macro_rules! gen_dice_fn_for {
    ( $( $x:expr ),* ) => {
        $(
            #[allow(dead_code)]                           // 1
            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 <code>paste</code> crate:

>This crate provides a flexible way to paste together identifiers in a macro, including using pasted identifiers to define new items.
>
> -- [crates.io](https://crates.io/crates/paste)

Let's add the crate to our project:

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

Then use it:

```rust
macro_rules! gen_dice_fn_for {
    ( $( $x:expr ),* ) => {
        paste! {                            // 1
            $(
            #[allow(dead_code)]
            pub fn [<d$x>]() -> Die {       // 2
                Self::new($x)
            }
            )*
        }
    };
}
```

1. Open the <code>paste</code> directive
2. Generate a function name using <code>x</code>

## 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, <em>i.e.</em>, a d3, but this is rare.

It is a good case for the <code>Default</code> trait. Rust defines it as:

```rust
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 <code>Default</code> for <code>Die</code> and return a 6-sided die.

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

We can now call <code>Die::default()</code> to get a d6.

## Non-zero checks

Using a <code>u8</code> 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 <code>Die</code>.

The most straightforward way is to add an <code>if</code> check at the start of the <code>new()</code> and <code>dN()</code> functions. But I did a bit of research and stumbled upon the [non-zero integer types](https://doc.rust-lang.org/std/num/index.html). We can rewrite our <code>Die</code> implementation accordingly:

```rust
impl Die {
    pub fn new(faces: u8) -> Die {
        let faces = NonZeroU8::new(faces)       // 1
            .unwrap()                           // 2
            .get();                             // 3
        Die { faces }
    }
}
```

1. Wrap the <code>u8</code> into a non-zero type
2. Unwrap it into an <code>Option<NonZeroU8></code>
3. Get the underlying <code>u8</code> value if it's strictly positive or <code>panic</code> 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](https://blog.frankel.ch/example-overengineering).

The idea is to fail fast. Otherwise, we would need to cope with the <code>Option</code> type throughout the application. <code>if faces == 0 { panic!("Value must be strictly positive {}", faces); }</code> would be much simpler and achieve the same. <abbr title="Keep It Simple and Stupid">KISS</abbr>.

## 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 <code>STUN</code> and <code>BODY</code> characteristics.

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

For each normal damage die, the rules are simple:

* The number of <code>STUN</code> damage is the roll
* The number of <code>BODY</code> depends on the roll: <code>0</code> for <code>1</code>, <code>2</code> for <code>6</code>, and <code>1</code> in all other cases.

We can implement it as the following:

```rust
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 <em>mutability</em>. Let's rewrite a functional version:

```rust
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
4. Implement the business rule
5. Create the <code>Damage</code> with the <code>STUN</code> and <code>BODY</code>
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 <code>Damage</code> together! It's as simple as adding their <code>STUN</code> and <code>BODY</code>. To fix the compilation error, we need to implement the <code>Sum</code> trait for <code>NormalDamage</code>.

```rust
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 <code>Damage</code>, we need to its <code>stun</code> and <code>body</code> properties:

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

Printing <code>Damage</code> is a pretty standard use case. We want to write the following:

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

For that, we need to implement <code>Display</code> for <code>Damage</code>:

```rust
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 <code>struct</code> is a good practice.

## Making Damage a trait

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

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

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

```rust
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 <code>Box</code> 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
>
> -- [Using Box<T> to Point to Data on the Heap](https://doc.rust-lang.org/book/ch15-01-box.html)

Let's wrap the return value in a <code>Box</code> to correct the compilation error.

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

It now compiles successfully.

## Display for traits

With <code>Damage</code> being a trait, we need to change the <code>println!()</code> part of the application:

```rust
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 <code>Damage</code> a "subtrait" of <code>Display</code>.

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

Finally, we need to implement <code>Display</code> for <code>NormalDamage</code> and <code>KillingDamage</code>.

## 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 <abbr title="Test-Driven Design">TDD</abbr>.

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

{% github ajavageek/diceroller %}

**To go further:**

* [The Rust Rand Book](https://rust-random.github.io/book/intro.html)
* [paste: Macros for all your token pasting needs](https://crates.io/crates/paste)
* [std::num::NonZeroU8](https://doc.rust-lang.org/std/num/struct.NonZeroU8.html)
* [Using Box<T> to Point to Data on the Heap](https://doc.rust-lang.org/book/ch15-01-box.html)

_Originally published at [A Java Geek](https://blog.frankel.ch/start-rust/8/) on July 25<sup>th</sup>, 2021_