1use crate::notation::Notation;
9use rand::{Rng, RngExt};
10use std::cmp::Reverse;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum RollType {
16 Normal(Notation),
18 Advantage(Notation),
20 Disadvantage(Notation),
22 Critical(Notation),
25 AbilityScores,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Roll {
32 pub dice: Vec<u32>,
35 pub total: u32,
38}
39
40fn roll_dice(count: u32, sides: u32, rng: &mut impl Rng) -> Vec<u32> {
41 (0..count).map(|_| rng.random_range(1..=sides)).collect()
42}
43
44fn roll_once(notation: Notation, rng: &mut impl Rng) -> Roll {
45 let dice = roll_dice(notation.count(), notation.sides(), rng);
46 let sum: u32 = dice.iter().sum();
47 let total = notation.modifier().map_or(sum, |m| m.apply(sum));
48
49 Roll { dice, total }
50}
51
52fn roll_ability_score(rng: &mut impl Rng) -> Roll {
53 let dice = roll_dice(4, 6, rng);
54 let sum: u32 = dice.iter().sum();
55 let lowest = dice.iter().min().copied().unwrap_or(0);
56 let total = sum - lowest;
57
58 Roll { dice, total }
59}
60
61pub fn roll(roll_type: RollType, rng: &mut impl Rng) -> Vec<Roll> {
79 match roll_type {
80 RollType::Normal(notation) => vec![roll_once(notation, rng)],
81 RollType::Critical(notation) => vec![roll_once(notation.critical(), rng)],
82 RollType::Advantage(notation) => {
83 let mut rolls = vec![roll_once(notation, rng), roll_once(notation, rng)];
84 rolls.sort_by_key(|roll| Reverse(roll.total));
85
86 rolls
87 }
88 RollType::Disadvantage(notation) => {
89 let mut rolls = vec![roll_once(notation, rng), roll_once(notation, rng)];
90 rolls.sort_by_key(|roll| roll.total);
91
92 rolls
93 }
94 RollType::AbilityScores => (0..6).map(|_| roll_ability_score(rng)).collect(),
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use rand::{SeedableRng, rngs::StdRng};
102 use rstest::{fixture, rstest};
103
104 use crate::notation::Operator;
105 use crate::test_helpers::{modifier, notation};
106
107 const RANDOM_SEED: u64 = 42;
108
109 #[fixture]
110 fn rng() -> StdRng {
111 StdRng::seed_from_u64(RANDOM_SEED)
112 }
113
114 mod roll_dice {
115 use super::*;
116
117 #[rstest]
118 fn returns_count_dice(mut rng: StdRng) {
119 let count = 3;
120 let sides = 6;
121
122 let result = roll_dice(count, sides, &mut rng);
123
124 assert_eq!(result.len(), count as usize);
125 }
126
127 #[rstest]
128 fn every_die_is_within_range_of_sides(mut rng: StdRng) {
129 let count = 100;
130 let sides = 10;
131
132 let result = roll_dice(count, sides, &mut rng);
133
134 assert!(
135 result.iter().all(|die| (1..=sides).contains(die)),
136 "a die was out of range: {result:?}"
137 );
138 }
139
140 #[rstest]
141 fn every_possible_face_can_come_up(mut rng: StdRng) {
142 let count = 1_000;
143 let sides = 6;
144
145 let result = roll_dice(count, sides, &mut rng);
146
147 let missing: Vec<u32> = (1..=6).filter(|face| !result.contains(face)).collect();
148 assert!(missing.is_empty(), "faces never rolled: {missing:?}");
149 }
150 }
151
152 mod roll_once {
153 use super::*;
154
155 #[rstest]
156 fn rolls_count_dice(mut rng: StdRng) {
157 let notation = notation(2, 6, None);
158
159 let result = roll_once(notation, &mut rng);
160
161 assert_eq!(result.dice.len(), 2);
162 }
163
164 #[rstest]
165 fn applies_modifier_to_total(mut rng: StdRng) {
166 let notation = notation(3, 1, Some(modifier(Operator::Add, 2)));
167
168 let result = roll_once(notation, &mut rng);
169
170 assert_eq!(result.total, 5);
171 }
172
173 #[rstest]
174 fn total_is_sum_of_dice_with_no_modifier(mut rng: StdRng) {
175 let notation = notation(2, 6, None);
176
177 let result = roll_once(notation, &mut rng);
178
179 assert_eq!(result.total, result.dice.iter().sum::<u32>());
180 }
181
182 #[rstest]
183 fn leaves_dice_unmodified_when_applying_modifier(mut rng: StdRng) {
184 let notation = notation(1, 1, Some(modifier(Operator::Add, 5)));
185
186 let result = roll_once(notation, &mut rng);
187
188 assert_eq!(result.dice, vec![1]);
189 assert_eq!(result.total, 6);
190 }
191 }
192
193 mod roll_ability_score {
194 use super::*;
195 #[rstest]
196 fn rolls_four_dice(mut rng: StdRng) {
197 let result = roll_ability_score(&mut rng);
198
199 assert_eq!(result.dice.len(), 4);
200 }
201
202 #[rstest]
203 fn dice_have_six_sides(mut rng: StdRng) {
204 let result = roll_ability_score(&mut rng);
205
206 assert!(
207 result.dice.iter().all(|die| (1..=6).contains(die)),
208 "a die was outside 1..=6: {:?}",
209 result.dice
210 );
211 }
212
213 #[rstest]
214 fn total_drops_the_lowest_die(mut rng: StdRng) {
215 let result = roll_ability_score(&mut rng);
216
217 let sum: u32 = result.dice.iter().sum();
218 let lowest = *result.dice.iter().min().unwrap();
219 assert_eq!(result.total, sum - lowest);
220 }
221
222 #[rstest]
223 fn total_is_between_3_and_18(mut rng: StdRng) {
224 let result = roll_ability_score(&mut rng);
225
226 assert!(
227 (3..=18).contains(&result.total),
228 "total out of range: {}",
229 result.total
230 );
231 }
232 }
233
234 mod roll {
235 use super::*;
236
237 #[rstest]
238 fn normal_returns_one_roll(mut rng: StdRng) {
239 let roll_type = RollType::Normal(notation(2, 6, None));
240
241 let result = roll(roll_type, &mut rng);
242
243 assert_eq!(result.len(), 1);
244 let roll = &result[0];
245 assert_eq!(roll.dice.len(), 2);
246 }
247
248 #[rstest]
249 fn critical_doubles_original_dice(mut rng: StdRng) {
250 let roll_type = RollType::Critical(notation(1, 8, Some(modifier(Operator::Add, 2))));
251
252 let result = roll(roll_type, &mut rng);
253
254 assert_eq!(result.len(), 1);
255 let roll = &result[0];
256 assert_eq!(roll.dice.len(), 2);
257 }
258
259 #[rstest]
260 fn critical_applies_modifier_once(mut rng: StdRng) {
261 let roll_type = RollType::Critical(notation(1, 1, Some(modifier(Operator::Add, 2))));
262
263 let result = roll(roll_type, &mut rng);
264
265 assert_eq!(result[0].total, 4);
266 }
267
268 #[rstest]
269 fn advantage_returns_two_rolls_of_count_dice(mut rng: StdRng) {
270 let roll_type = RollType::Advantage(notation(1, 20, None));
271 let result = roll(roll_type, &mut rng);
272
273 assert_eq!(result.len(), 2);
274 assert_eq!(result[0].dice.len(), 1);
275 assert_eq!(result[1].dice.len(), 1);
276 }
277
278 #[rstest]
279 fn advantage_returns_rolls_sorted_highest_to_lowest(mut rng: StdRng) {
280 let roll_type = RollType::Advantage(notation(1, 20, Some(modifier(Operator::Add, 2))));
281
282 let result = roll(roll_type, &mut rng);
283
284 assert!(result[0].total >= result[1].total);
285 }
286
287 #[rstest]
288 fn disadvantage_returns_two_rolls_of_count_dice(mut rng: StdRng) {
289 let roll_type = RollType::Disadvantage(notation(1, 20, None));
290
291 let result = roll(roll_type, &mut rng);
292
293 assert_eq!(result.len(), 2);
294 assert_eq!(result[0].dice.len(), 1);
295 assert_eq!(result[1].dice.len(), 1);
296 }
297
298 #[rstest]
299 fn disadvantage_returns_rolls_sorted_lowest_to_highest(mut rng: StdRng) {
300 let roll_type =
301 RollType::Disadvantage(notation(1, 20, Some(modifier(Operator::Add, 2))));
302
303 let result = roll(roll_type, &mut rng);
304
305 assert!(result[0].total <= result[1].total);
306 }
307
308 #[rstest]
309 fn ability_scores_returns_six_rolls(mut rng: StdRng) {
310 let result = roll(RollType::AbilityScores, &mut rng);
311
312 assert_eq!(result.len(), 6);
313 }
314 }
315}