Skip to main content

cute_dnd_dice/
lib.rs

1//! Simple library to roll dices
2//!
3//!## Random source
4//!
5//!Relies on `getrandom` by default
6//!
7//!## Usage
8//!
9//!```rust
10//!use cute_dnd_dice::Roll;
11//!
12//!fn main() {
13//!    let roll = Roll::from_str("2d20+10").expect("To parse roll");
14//!    println!("I roll {}", roll.roll());
15//!}
16//!```
17
18#![no_std]
19
20#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))]
21
22#[cfg(feature = "std")]
23extern crate std;
24
25use core::{fmt, ops};
26use core::num::NonZeroU16;
27
28mod random;
29
30#[derive(PartialEq, Eq, Debug)]
31///Possible errors when parsing roll
32pub enum ParseError {
33    ///Couldn't find `d`
34    MissingD,
35    ///Missing dice faces
36    MissingFaces,
37    ///Modifier value is not present
38    MissingModifierValue,
39    ///Invalid number of dices
40    InvalidNum,
41    ///Invalid number of faces
42    InvalidFaces,
43    ///Invalid number of extra
44    InvalidExtra,
45}
46
47impl ParseError {
48    ///Returns text description of error.
49    pub fn desc(&self) -> &'static str {
50        match self {
51            ParseError::MissingD => "'d' is missing",
52            ParseError::MissingFaces => "Number of dice's faces is missing",
53            ParseError::MissingModifierValue => "Modifier for roll is missing",
54            ParseError::InvalidNum => "Number of dices is invalid. Should be positive integer",
55            ParseError::InvalidFaces => "Number of faces is invalid. Should be positive integer",
56            ParseError::InvalidExtra => "Number of extra is invalid. Should be positive integer",
57        }
58    }
59}
60
61impl fmt::Display for ParseError {
62    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
63        write!(fmt, "{}", self.desc())
64    }
65}
66
67#[cfg(feature = "std")]
68impl std::error::Error for ParseError {
69}
70
71#[derive(Eq, PartialEq, Debug, Clone, Copy)]
72///Roll Modifier
73pub enum Modifier {
74    ///Plus variant
75    Plus(u16),
76    ///Minus variant
77    Minus(u16),
78}
79
80impl ops::Add<u16> for Modifier {
81    type Output = Modifier;
82
83    fn add(self, other: u16) -> Self::Output {
84        match self {
85            Modifier::Plus(modifier) => Modifier::Plus(modifier.saturating_add(other)),
86            Modifier::Minus(modifier) => match other >= modifier {
87                #[allow(clippy::suspicious_arithmetic_impl)]
88                true => Modifier::Plus(other - modifier),
89                #[allow(clippy::suspicious_arithmetic_impl)]
90                false => Modifier::Minus(modifier - other),
91            },
92        }
93    }
94}
95
96impl ops::AddAssign<u16> for Modifier {
97    fn add_assign(&mut self, other: u16) {
98        *self = *self + other;
99    }
100}
101
102impl ops::Sub<u16> for Modifier {
103    type Output = Modifier;
104
105    fn sub(self, other: u16) -> Self::Output {
106        match self {
107            Modifier::Minus(modifier) => Modifier::Minus(modifier.saturating_sub(other)),
108            Modifier::Plus(modifier) => match other > modifier {
109                true => Modifier::Minus(other - modifier),
110                false => Modifier::Plus(modifier - other),
111            },
112        }
113    }
114}
115
116impl ops::SubAssign<u16> for Modifier {
117    fn sub_assign(&mut self, other: u16) {
118        *self = *self - other;
119    }
120}
121
122impl Modifier {
123    fn modify(self, value: u16) -> u16 {
124        match self {
125            Modifier::Plus(modifier) => value.saturating_add(modifier),
126            Modifier::Minus(modifier) => value.saturating_sub(modifier),
127        }
128    }
129
130    ///Returns whether modifier is negative.
131    pub fn is_neg(self) -> bool {
132        match self {
133            Modifier::Plus(_) => false,
134            Modifier::Minus(_) => true,
135        }
136    }
137}
138
139impl fmt::Display for Modifier {
140    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
141        match self {
142            Modifier::Plus(0) => Ok(()),
143            Modifier::Plus(value) => write!(fmt, "+{}", value),
144            Modifier::Minus(0) => Ok(()),
145            Modifier::Minus(value) => write!(fmt, "-{}", value),
146        }
147    }
148}
149
150#[derive(Debug)]
151///D&D Roll representation
152pub struct Roll {
153    ///Number of dices
154    pub num: u16,
155    ///Number of faces on dice
156    pub faces: NonZeroU16,
157    ///Bonus to roll
158    pub extra: Modifier,
159}
160
161impl Roll {
162    #[inline]
163    pub const fn new(num: u16, faces: NonZeroU16, extra: Modifier) -> Self {
164        Self {
165            num,
166            faces,
167            extra,
168        }
169    }
170
171    ///Attempts to parse Roll from string `[num]d<faces> [+ <extra>]`
172    pub fn from_str(text: &str) -> Result<Self, ParseError> {
173        const D: &[char] = &['d', 'D'];
174        const PLUS: char = '+';
175        const MINUS: char = '-';
176
177        let text = text.trim();
178
179        let dice_idx = match text.find(D) {
180            Some(idx) => idx,
181            None => return Err(ParseError::MissingD),
182        };
183
184        if dice_idx == text.len() - 1 {
185            return Err(ParseError::MissingFaces);
186        }
187
188        let num = match dice_idx {
189            0 => 1,
190            dice_idx => match text[..dice_idx].trim().parse() {
191                Ok(0) => return Err(ParseError::InvalidNum),
192                Ok(num) => num,
193                Err(_) => return Err(ParseError::InvalidNum),
194            }
195        };
196
197        let extra = text.find(PLUS)
198                        .map(|extra| (extra, false))
199                        .or_else(|| text.find(MINUS).map(|extra| (extra, true)));
200
201        let (extra, dice_end) = match extra {
202            Some((idx, is_extra_neg)) => match text.len() - 1 == idx {
203                true => return Err(ParseError::MissingModifierValue),
204                false => match text[idx+1..].trim().parse() {
205                    Ok(extra) => match is_extra_neg {
206                        true => (Modifier::Minus(extra), idx),
207                        false => (Modifier::Plus(extra), idx),
208                    }
209                    Err(_) => return Err(ParseError::InvalidExtra),
210                },
211            },
212            None => (Modifier::Plus(0), text.len()),
213        };
214
215        let faces = match text[dice_idx+1..dice_end].trim().parse() {
216            Ok(0) => return Err(ParseError::InvalidFaces),
217            Ok(faces) => unsafe {
218                NonZeroU16::new_unchecked(faces)
219            },
220            Err(_) => return Err(ParseError::InvalidFaces),
221        };
222
223        Ok(Self::new(num, faces, extra))
224    }
225
226    ///Returns minimum possible value.
227    pub fn min(&self) -> u16 {
228        self.extra.modify(self.num)
229    }
230
231    ///Returns maximum possible value.
232    pub fn max(&self) -> u16 {
233        let res = self.num.saturating_mul(self.faces.get());
234        self.extra.modify(res)
235    }
236
237    ///Rolls using specified callable to generate random numbers.
238    ///
239    ///```
240    ///fn my_random() -> u64 {
241    ///    0xff
242    ///}
243    ///
244    ///let roll = cute_dnd_dice::Roll::from_str("2d20+1").unwrap();
245    ///assert_eq!(roll.roll_with(my_random), 3);
246    ///```
247    pub fn roll_with(&self, fun: fn() -> u64) -> u16 {
248        #[inline(always)]
249        fn mul_high_u64(a: u64, b: u64) -> u64 {
250            (((a as u128) * (b as u128)) >> 64) as u64
251        }
252
253        let faces = self.faces.get() as u64;
254        let mut result: u16 = 0;
255
256        for _ in 0..self.num {
257            let mut random = fun();
258            let mut hi = mul_high_u64(random, faces);
259            let mut lo = random.wrapping_mul(faces);
260
261            if lo < faces {
262                while lo < (faces.wrapping_neg() % faces) {
263                    random = fun();
264                    hi = mul_high_u64(random, faces);
265                    lo = random.wrapping_mul(faces);
266                }
267            }
268
269            //We generate in range 0..faces and then +1
270            //hence hi != faces
271            debug_assert_ne!(hi, faces);
272            result = result.saturating_add(hi as u16 + 1);
273        }
274
275        self.extra.modify(result)
276    }
277
278    #[inline(always)]
279    ///Calculates roll with default random source.
280    ///
281    ///It provides decent uniform distribution.
282    pub fn roll(&self) -> u16 {
283        const RANDOM: fn() -> u64 = random::random;
284        self.roll_with(RANDOM)
285    }
286}
287
288impl fmt::Display for Roll {
289    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
290        write!(fmt, "{}d{}{}", self.num, self.faces, self.extra)
291    }
292}