44
loading...
This website collects cookies to deliver better user experience
struct Die {
faces: u8,
}
[dependencies]
rand = "0.8.4"
impl Die {
pub fn roll(self) -> u8 {
let mut rng = rand::thread_rng(); // 1
rng.gen_range(1..=self.faces) // 2
}
}
let d6 = Die { faces: 6 };
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
}
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 ),* ) => {
$(
#[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
}
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)
paste
crate:This crate provides a flexible way to paste together identifiers in a macro, including using pasted identifiers to define new items.
-- crates.io
[dependencies]
paste = "1.0.5"
macro_rules! gen_dice_fn_for {
( $( $x:expr ),* ) => {
paste! { // 1
$(
#[allow(dead_code)]
pub fn [<d$x>]() -> Die { // 2
Self::new($x)
}
)*
}
};
}
paste
directivex
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;
}
Default
for Die
and return a 6-sided die.impl Default for Die {
fn default() -> Self {
Die::d6()
}
}
Die::default()
to get a d6.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
.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 }
}
}
u8
into a non-zero typeOption
u8
value if it's strictly positive or panic
otherwiseOption
type throughout the application. if faces == 0 { panic!("Value must be strictly positive {}", faces); }
would be much simpler and achieve the same. KISS.STUN
and BODY
characteristics.NormalDamage
, and KillingDamage
. Let's focus on the former type first.STUN
damage is the rollBODY
depends on the roll: 0
for 1
, 2
for 6
, and 1
in all other cases.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 }
}
}
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
}
}
Damage
with the STUN
and BODY
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`
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,
})
}
}
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);
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);
Display
for Damage
:impl Display for Damage {
fn fmt(&self, f: &mut Formatter<'_>) - std::fmt::Result {
write!(f, "stun: {}, body: {}", self.stun, self.body)
}
}
struct
is a good practice.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,
}
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
}
}
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
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:
Box
to correct the compilation error.pub fn roll(self) -> Box<dyn Damage> {
// let damage = ...
Box::new(damage)
}
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);
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)
Damage
a "subtrait" of Display
.pub trait Damage: Display {
fn stun(self) -> u8;
fn body(self) -> u8;
}
Display
for NormalDamage
and KillingDamage
.