ndm/
rollset.rs

1// Copyright (C) 2021 Ben Stern
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#![forbid(unsafe_code)]
5
6use core::convert::Infallible;
7
8use std::error::Error;
9use std::fmt::{Display, Debug, Error as FmtError, Formatter};
10use std::str::FromStr;
11
12use lazy_static::lazy_static;
13use regex::Regex;
14use serde::{Deserialize, Serialize};
15
16use crate::{Dice, DiceParseError};
17
18lazy_static! {
19    // ?x ignores space/comments in the regex, not in the string we're checking
20    static ref PLUS_MINUS_RE: Regex = Regex::new("(?P<op>[-+\u{2212}])").expect("Couldn't compile PLUS_MINUS_RE");
21    static ref TIMES_RE: Regex = Regex::new("(?P<before>[\\d\\s])(?P<op>[*\u{00d7}xX])(?P<after>[\\d\\s])").expect("Couldn't compile TIMES_RE");
22}
23
24#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
25enum DiceWords {
26    Dice(Dice),
27    Bonus(u16),
28    Plus,
29    Minus,
30    Times,
31    Multiplier(f32),
32    Comment(String),
33    Other(String),
34    Total(i32),
35}
36
37impl Display for DiceWords {
38    fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
39        match self {
40            DiceWords::Dice(d) => write!(fmt, "{}", d),
41            DiceWords::Bonus(d) => write!(fmt, "{}", d),
42            DiceWords::Multiplier(d) => write!(fmt, "{}", d),
43            DiceWords::Plus => write!(fmt, "+"),
44            DiceWords::Minus => write!(fmt, "\u{2212}"),
45            DiceWords::Times => write!(fmt, "\u{00d7}"),
46            DiceWords::Other(s) | DiceWords::Comment(s) => write!(fmt, "|{}|", s),
47            DiceWords::Total(t) => write!(fmt, "= {}", t),
48        }
49    }
50}
51
52impl FromStr for DiceWords {
53    type Err = Infallible;
54
55    fn from_str(line: &str) -> Result<Self, Self::Err> {
56        if let Ok(d) = line.parse() {
57            Ok(DiceWords::Dice(d))
58        } else if let Ok(n) = line.parse() {
59            Ok(DiceWords::Bonus(n))
60        } else if let Ok(f) = line.parse() {
61            Ok(DiceWords::Multiplier(f))
62        } else if line == "+" {
63            Ok(DiceWords::Plus)
64        } else if (line == "-") || (line == "\u{2212}") {
65            Ok(DiceWords::Minus)
66        } else if (line == "x") || (line == "X") || (line == "*") || (line == "\u{00d7}") {
67            Ok(DiceWords::Times)
68        } else if line.starts_with('#') {
69            Ok(DiceWords::Comment(line.to_string()))
70        } else {
71            Ok(DiceWords::Other(line.to_string()))
72        }
73    }
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum RollParseError {
78    DiceError(DiceParseError),
79    InvalidOrder,
80    MissingRoll,
81    Failure(String),
82}
83
84impl Display for RollParseError {
85    fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
86        write!(fmt, "{:?}", self)
87    }
88}
89
90impl Error for RollParseError {}
91
92#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
93pub struct RollSet {
94    words: Vec<DiceWords>,
95}
96
97/// In the syntax below, capital letters are variables and lower-case letters
98/// are verbatim.  Optional fields are surrounded with [], mandatory fields are
99/// surrounded with <>.
100///
101/// In use, the parsed syntax is case insensitive and ignores internal spaces.
102///
103/// Dice may have no more than 1000 sides, not as a technical reason but because
104/// the number of times a d1001 or d2000 are needed should be vanishingly low.
105///
106/// Note that dice math is left to right:
107/// - 1 + 2 * 3 = 9, not 7
108/// - 1 + 2 * 3 + 4 * 5 = 65, not 27 (nor 29)
109/// - There's no way to force precedence except for ordering
110/// - This is a feature; 2d6 + 4 * 2 + 1d6 is a common pattern
111///
112/// Line syntax: `<M | X | R | C> [M | X | R | C]...`
113///
114/// Example: `lance 2d6 + 4 * 2 slashing + 1d6 fire`
115///
116/// - M: a modifier, defined below
117/// - X: a multiplier, defined below
118/// - R: a roll, defined below
119/// - C: a comment (anything that doesn't match one of the above)
120///
121/// Bonus syntax: `<+ | -> N`
122///
123/// Examples: `-5` or `+ 4`
124///
125/// - the sign is required
126/// - N is the value, and must be an integer
127/// - "+1.5" will be interpreted as +1 ".5"
128///
129/// Multiplier syntax: `<x | *> F`
130///
131/// Examples: `x 1.5` or `*2`
132///
133/// - x, X, and * are accepted
134/// - \x00d7 and \x2a09 aren't accepted and will cause this to be parsed as a
135///   comment
136/// - F is either an integer or a floating point number
137/// - results are rounded down
138///
139/// Examples (sample use case in parens)
140/// - `3d8` (roll 3d8)
141/// - `d20lh19!` (roll a keen d20 which explodes, coloring 19s & 20s but not 1s)
142///   (not implemented)
143/// - `2d20/l` (disadvantage, roll 2d20 but only keep the lower one)
144/// - `4d6/h3` (use a typical method to generate a stat)
145///
146/// Longer examples with surrounding modifiers and comments:
147/// - `3d8` bludgeoning (a damage roll)
148/// - `d20l2h2+8 to hit` (threat on 19-20, fail on 1-2)
149/// - `2d20/l+4 fort save with disadvantage`
150/// - `4d6/h3 4d6/h3 4d6/h3 4d6/h3 4d6/h3 4d6/h3 chargen` (the reader should
151///   ignore the total shown; external syntax should be created to roll without
152///   totalling)
153///
154/// - The leading plus is optional
155///   - implemented
156///   - It's normally used to join 2 rolls, but it isn't required
157///   - Negative rolls aren't parsed as such (the "-" is considered a comment)
158/// - N: the number of dice to roll
159///   - implemented
160///   - If missing, 1 is assumed
161///   - An explicit 0 is an error (to avoid interpreting 0d6 as "0" 1d6)
162/// - M: the number of sides
163///   - implemented
164///   - 0 is an error
165///   - 1 is legal but largely useless
166///   - f (or F) means Fate dice
167///   - If this is missing then the roll can't be parsed as such and will be
168///     interpreted as a comment
169/// - X: the number of dice to keep
170///   - not implemented
171///   - /hX will mean keep the highest X rolls
172///   - /lX will mean keep the lowest X rolls
173///   - incompatible with `!` (bang)
174///   - X > N is an error
175///   - X = N is ignored entirely
176///   - X = 0 is an error
177///   - must come after H and L if they're present
178/// - ! means exploding
179///   - implemented for highest number only
180///   - must be the last thing on the dice roll
181///   - E means explode on E or higher, not just the highest number
182///   - E <= 1 is an error
183///   - E > N is ignored (but not an error)
184///   - Fate dice and one-sided dice can't explode
185///   - exploded dice don't count against the limit per roll (explosions don't
186///     usually last long anyway)
187
188impl FromStr for RollSet {
189    type Err = RollParseError;
190
191    fn from_str(line: &str) -> Result<Self, Self::Err> {
192        let mut last_word = DiceWords::Other("".to_string());
193        let mut words = Vec::new();
194        let mut roll_found = false;
195        let mut total = 0;
196        let pm_repl = PLUS_MINUS_RE.replace_all(line, " $op ");
197        let replaced = TIMES_RE.replace_all(&pm_repl, "$before $op $after");
198        let mut each = replaced.split_whitespace();
199        while let Some(word) = each.next() {
200            let parsed = word.parse::<DiceWords>().unwrap();
201            //println!("{}", word);
202            match (&last_word, &parsed) {
203                (DiceWords::Dice(_), DiceWords::Dice(_)) | // 3d4 3d4
204                (DiceWords::Dice(_), DiceWords::Multiplier(_)) | // 3d6 4.4
205                (DiceWords::Dice(_), DiceWords::Bonus(_)) | // 4d6 4
206
207                (DiceWords::Bonus(_), DiceWords::Dice(_)) | // 4 4d6
208                (DiceWords::Bonus(_), DiceWords::Bonus(_)) | // 4 4
209                (DiceWords::Bonus(_), DiceWords::Multiplier(_)) | // 4 4.4
210
211                (DiceWords::Times, DiceWords::Dice(_)) | // * 4d6
212
213                (DiceWords::Plus, DiceWords::Plus) | // + +
214                (DiceWords::Plus, DiceWords::Minus) | // + -
215                (DiceWords::Plus, DiceWords::Times) | // + *
216                (DiceWords::Plus, DiceWords::Multiplier(_)) | // + 4.4
217                (DiceWords::Plus, DiceWords::Comment(_)) | // + # foo
218                (DiceWords::Plus, DiceWords::Other(_)) | // + foo // should this be allowed?
219
220                (DiceWords::Minus, DiceWords::Plus) | // - +
221                (DiceWords::Minus, DiceWords::Minus) | // - -
222                (DiceWords::Minus, DiceWords::Times) | // - *
223                (DiceWords::Minus, DiceWords::Multiplier(_)) | // - 4.4
224                (DiceWords::Minus, DiceWords::Comment(_)) | // - # foo
225                (DiceWords::Minus, DiceWords::Other(_)) | // - foo // should this be allowed?
226
227                (DiceWords::Times, DiceWords::Plus) | // * +
228                (DiceWords::Times, DiceWords::Minus) | // * - (this means * -4 doesn't work)
229                (DiceWords::Times, DiceWords::Times) | // * *
230                (DiceWords::Times, DiceWords::Comment(_)) | // * # foo
231                (DiceWords::Times, DiceWords::Other(_)) | // * foo
232
233                (DiceWords::Multiplier(_), DiceWords::Dice(_)) | // 4.4 4d6
234                (DiceWords::Multiplier(_), DiceWords::Bonus(_)) | // 4.4 4
235                (DiceWords::Multiplier(_), DiceWords::Multiplier(_)) | // 4.4 4.4
236
237                (DiceWords::Other(_), DiceWords::Bonus(_)) | // fire 4
238                (DiceWords::Other(_), DiceWords::Multiplier(_))
239
240                => { // fire 4.4
241                    //eprintln!("{:?} {:?}", last_word, parsed);
242                    return Err(RollParseError::InvalidOrder);
243                },
244
245                (DiceWords::Dice(_), DiceWords::Plus) | // 4d6 +
246                (DiceWords::Dice(_), DiceWords::Minus) | // 4d6 -
247                (DiceWords::Dice(_), DiceWords::Times) | // 4d6 *
248                (DiceWords::Dice(_), DiceWords::Other(_)) | // 4d6 fire
249
250                (DiceWords::Bonus(_), DiceWords::Plus) | // 4 +
251                (DiceWords::Bonus(_), DiceWords::Minus) | // 4 -
252                (DiceWords::Bonus(_), DiceWords::Times) | // 4 *
253                (DiceWords::Bonus(_), DiceWords::Other(_)) | // 4 fire
254
255                (DiceWords::Multiplier(_), DiceWords::Plus) | // 4.4 +
256                (DiceWords::Multiplier(_), DiceWords::Minus) | // 4.4 -
257                (DiceWords::Multiplier(_), DiceWords::Times) | // 4.4 *
258                (DiceWords::Multiplier(_), DiceWords::Other(_)) | // 1.5 fire
259
260                (DiceWords::Other(_), DiceWords::Plus) | // fire +
261                (DiceWords::Other(_), DiceWords::Minus) | // fire -
262                (DiceWords::Other(_), DiceWords::Times) // fire *
263                => {
264                    words.push(parsed.clone());
265                    last_word = parsed;
266                },
267
268                (DiceWords::Plus, DiceWords::Bonus(b)) => { // + 4
269                    total += *b as i32;
270                    words.push(parsed.clone());
271                    last_word = parsed;
272                },
273
274                (DiceWords::Minus, DiceWords::Bonus(b)) => { // + 4
275                    total -= *b as i32;
276                    words.push(parsed.clone());
277                    last_word = parsed;
278                },
279
280                (DiceWords::Times, DiceWords::Bonus(b)) => {
281                    total *= *b as i32;
282                    last_word = DiceWords::Multiplier(*b as f32);
283                    words.push(last_word.clone());
284                }
285
286                (DiceWords::Times, DiceWords::Multiplier(b)) => {
287                    total = (total as f32 * b) as i32;
288                    words.push(parsed.clone());
289                    last_word = parsed;
290                }
291
292                (DiceWords::Dice(_), DiceWords::Comment(s)) | // 4d6 # first
293                (DiceWords::Bonus(_), DiceWords::Comment(s)) | // 4 # foo
294                (DiceWords::Other(_), DiceWords::Comment(s)) | // fire # foo
295                (DiceWords::Multiplier(_), DiceWords::Comment(s)) // 1.5 # foo
296                => {
297                    //println!("Pushing in total");
298                    words.push(DiceWords::Total(total));
299                    last_word = DiceWords::Comment(each.fold(s.to_string(), |acc, x| acc + " " + x));
300                    //println!("now adding comment: {:?}", last_word);
301                    words.push(last_word.clone());
302                    break;
303                },
304
305                (DiceWords::Other(_), DiceWords::Dice(d)) | // 1st attack 4d6 (also initial condition)
306                (DiceWords::Plus, DiceWords::Dice(d)) | // + 4d6
307                (DiceWords::Minus, DiceWords::Dice(d)) => { // - 4d6
308                    last_word = parsed.clone();
309                    words.push(parsed.clone());
310                    roll_found = true;
311                    total += d.total();
312                },
313
314                (_, DiceWords::Total(_)) |
315                (DiceWords::Total(_), _) |
316                (DiceWords::Comment(_), _) => {
317                    // should be impossible, last_word is never set to Comment or Total
318                    return Err(RollParseError::Failure(line.to_string()));
319                },
320
321                (DiceWords::Other(t), DiceWords::Other(s)) => { // foo bar
322                    words.pop();
323                    last_word = DiceWords::Other(format!("{} {}", t, s));
324                    words.push(last_word.clone());
325                },
326            }
327        }
328        if !roll_found {
329            Err(RollParseError::MissingRoll)
330        } else if matches!(last_word, DiceWords::Times | DiceWords::Plus | DiceWords::Minus) {
331            Err(RollParseError::InvalidOrder)
332        } else {
333            //println!("last_word: {:?}", last_word);
334            if !matches!(last_word, DiceWords::Comment(_)) {
335                //println!("pushing in total due to lack of comment");
336                words.push(DiceWords::Total(total));
337            }
338            Ok(RollSet { words })
339        }
340    }
341}
342
343impl Display for RollSet {
344    fn fmt(&self, fmt: &mut Formatter) -> Result<(), FmtError> {
345        write!(fmt, "{}", self.words.iter().map(|w| format!("{}", w)).collect::<Vec<String>>().join(" "))
346    }
347}
348
349#[cfg(test)]
350mod test {
351    use super::{Dice, DiceWords, RollParseError, RollSet};
352    use crate::expect_dice_similar;
353
354    macro_rules! expect_rollset_similar {
355        ($text: literal, $expect: expr) => {
356            for (r, w) in $text.parse::<RollSet>().unwrap().words.iter().zip($expect) {
357                match (r, w) {
358                    (DiceWords::Dice(parsed), DiceWords::Dice(provided)) => {
359                        expect_dice_similar!(parsed, provided);
360                    },
361                    (DiceWords::Bonus(parsed), DiceWords::Bonus(provided)) => assert_eq!(parsed, provided),
362                    (DiceWords::Multiplier(parsed), DiceWords::Multiplier(provided))  => assert_eq!(parsed, provided),
363                    (DiceWords::Comment(parsed), DiceWords::Comment(provided)) => assert_eq!(parsed, provided),
364                    (x, y) => assert_eq!(x, y),
365                }
366            }
367        }
368    }
369
370    #[test]
371    fn four() {
372        assert_eq!("4".parse::<RollSet>(), Err(RollParseError::InvalidOrder));
373        assert_eq!("4+1d4".parse::<RollSet>(), Err(RollParseError::InvalidOrder));
374    }
375
376    #[test]
377    fn exploding_1d20plus4() {
378        expect_rollset_similar!("1d20+4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Plus, DiceWords::Bonus(4) ]);
379    }
380
381    #[test]
382    fn r_1d6plus3() {
383        expect_rollset_similar!("1d6+3", &[ DiceWords::Dice(Dice::new(1, 6).unwrap()), DiceWords::Plus, DiceWords::Bonus(3) ]);
384    }
385
386    #[test]
387    fn caps_1d20plus4() {
388        expect_rollset_similar!("1D20+4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Plus, DiceWords::Bonus(4) ]);
389    }
390
391    #[test]
392    fn caps_1d20minus4() {
393        expect_rollset_similar!("1D20 - 4", &[ DiceWords::Dice(Dice::new(1, 20).unwrap()), DiceWords::Minus, DiceWords::Bonus(4) ]);
394    }
395
396    #[test]
397    fn comment_1d20() {
398        expect_rollset_similar!("1d20 for a test", &[
399            DiceWords::Dice(Dice::new(1, 20).unwrap()),
400            DiceWords::Other("for a test".to_string()),
401        ]);
402    }
403
404    #[test]
405    fn t_mult() {
406        expect_rollset_similar!("1d20 * 1.5", &[
407            DiceWords::Dice(Dice::new(1, 20).unwrap()),
408            DiceWords::Times,
409            DiceWords::Multiplier(1.5),
410        ]);
411    }
412
413    #[test]
414    fn spacesplus() {
415        expect_rollset_similar!("1d6+4+11d99!+3dF+ 4 +3 + 2 + 1d5", &[
416            DiceWords::Dice(Dice::new(1, 6).unwrap()),
417            DiceWords::Plus,
418            DiceWords::Bonus(4),
419            DiceWords::Plus,
420            DiceWords::Dice(Dice::new_extended(11, 99, 0, 99).unwrap()),
421            DiceWords::Plus,
422            DiceWords::Dice(Dice::new(3, 0).unwrap()),
423            DiceWords::Plus,
424            DiceWords::Bonus(4),
425            DiceWords::Plus,
426            DiceWords::Bonus(3),
427            DiceWords::Plus,
428            DiceWords::Bonus(2),
429            DiceWords::Plus,
430            DiceWords::Dice(Dice::new(1, 5).unwrap()),
431        ]);
432    }
433
434    #[test]
435    fn spacesminus() {
436        expect_rollset_similar!("1d6-4+11d99-3dF+ 4 +3 - 2 - 1d5", &[
437            DiceWords::Dice(Dice::new(1, 6).unwrap()),
438            DiceWords::Minus,
439            DiceWords::Bonus(4),
440            DiceWords::Plus,
441            DiceWords::Dice(Dice::new(11, 99).unwrap()),
442            DiceWords::Minus,
443            DiceWords::Dice(Dice::new(3, 0).unwrap()),
444            DiceWords::Plus,
445            DiceWords::Bonus(4),
446            DiceWords::Plus,
447            DiceWords::Bonus(3),
448            DiceWords::Minus,
449            DiceWords::Bonus(2),
450            DiceWords::Minus,
451            DiceWords::Dice(Dice::new(1, 5).unwrap()),
452        ]);
453    }
454
455#[cfg(test)]
456    macro_rules! unwrap_dice {
457        ($d: expr) => {
458            if let DiceWords::Dice(roll) = $d {
459                roll
460            } else {
461                panic!("not a roll: {:?}", $d);
462            }
463        }
464    }
465
466    #[test]
467    fn coinflip() {
468        let rs = "1d2 # 1 = foo, 2=bar".parse::<RollSet>().unwrap();
469        expect_dice_similar!(unwrap_dice!(&rs.words[0]), Dice::new(1, 2).unwrap());
470        assert_eq!(rs.words[2], DiceWords::Comment("# 1 = foo, 2=bar".to_string()));
471    }
472
473    // older tests
474
475    #[test]
476    fn compact() {
477        let rs = "2d6+4 for a test".parse::<RollSet>().unwrap();
478        assert_eq!(rs.words.len(), 5);
479
480        let mut iter = rs.words.iter();
481
482        let roll = unwrap_dice!(iter.next().unwrap());
483        assert_eq!(roll.rolls().len(), 2);
484        assert_eq!(roll.sides(), 6);
485
486        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
487        assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
488        assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
489        assert!(matches!(iter.next().unwrap(), &DiceWords::Total(_)));
490    }
491
492    #[test]
493    fn whitespace_positive() {
494        let rs = "2d6 + 4 for a test".parse::<RollSet>().unwrap();
495        assert_eq!(rs.words.len(), 5);
496
497        let mut iter = rs.words.iter();
498
499        let roll = unwrap_dice!(iter.next().unwrap());
500        assert_eq!(roll.rolls().len(), 2);
501        assert_eq!(roll.sides(), 6);
502
503        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
504        assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
505        assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
506        assert!(matches!(iter.next().unwrap(), DiceWords::Total(_)));
507    }
508
509    #[test]
510    fn whitespace_negative() {
511        let rs = "2d6 - 4 for a test".parse::<RollSet>().unwrap();
512        assert_eq!(rs.words.len(), 5);
513
514        let mut iter = rs.words.iter();
515
516        let roll = unwrap_dice!(iter.next().unwrap());
517        assert_eq!(roll.rolls().len(), 2);
518        assert_eq!(roll.sides(), 6);
519
520        assert_eq!(iter.next().unwrap(), &DiceWords::Minus);
521        assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
522        assert_eq!(iter.next().unwrap(), &DiceWords::Other("for a test".to_string()));
523        assert!(matches!(iter.next().unwrap(), DiceWords::Total(_)));
524    }
525
526    #[test]
527    fn simple() {
528        let rs = "d20".parse::<RollSet>().unwrap();
529        assert_eq!(rs.words.len(), 2);
530
531        let roll = unwrap_dice!(&rs.words[0]);
532        assert_eq!(roll.rolls().len(), 1);
533        assert_eq!(roll.sides(), 20);
534
535        assert!(matches!(rs.words[1], DiceWords::Total(_)));
536    }
537
538    #[test]
539    fn no_bonus_suffix() {
540        let rs = "3d6 Int".parse::<RollSet>().unwrap();
541        assert_eq!(rs.words.len(), 3);
542
543        let roll = unwrap_dice!(&rs.words[0]);
544        assert_eq!(roll.rolls().len(), 3);
545        assert_eq!(roll.sides(), 6);
546
547        assert_eq!(rs.words[1], DiceWords::Other("Int".to_string()));
548        assert!(matches!(rs.words[2], DiceWords::Total(_)));
549    }
550
551    #[test]
552    fn only_bonus() {
553        let rs = "d8+2".parse::<RollSet>().unwrap();
554        assert_eq!(rs.words.len(), 4);
555
556        let roll = unwrap_dice!(&rs.words[0]);
557        assert_eq!(roll.rolls().len(), 1);
558        assert_eq!(roll.sides(), 8);
559
560        assert_eq!(rs.words[1], DiceWords::Plus);
561        assert_eq!(rs.words[2], DiceWords::Bonus(2));
562        assert!(matches!(rs.words[3], DiceWords::Total(_)));
563    }
564
565    #[test]
566    fn regular_minus() {
567        let rs = "d8-2".parse::<RollSet>().unwrap();
568        assert_eq!(rs.words.len(), 4);
569
570        let roll = unwrap_dice!(&rs.words[0]);
571        assert_eq!(roll.rolls().len(), 1);
572        assert_eq!(roll.sides(), 8);
573
574        assert_eq!(rs.words[1], DiceWords::Minus);
575        assert_eq!(rs.words[2], DiceWords::Bonus(2));
576        assert!(matches!(rs.words[3], DiceWords::Total(_)));
577    }
578
579    #[test]
580    fn unicrud_minus() {
581        let rs = "d8\u{2212}2".parse::<RollSet>().unwrap();
582        assert_eq!(rs.words.len(), 4);
583
584        let roll = unwrap_dice!(&rs.words[0]);
585        assert_eq!(roll.rolls().len(), 1);
586        assert_eq!(roll.sides(), 8);
587
588        assert_eq!(rs.words[1], DiceWords::Minus);
589        assert_eq!(rs.words[2], DiceWords::Bonus(2));
590        assert!(matches!(rs.words[3], DiceWords::Total(_)));
591    }
592
593    #[test]
594    fn test_only_penalty() {
595        let rs = "3d8-2".parse::<RollSet>().unwrap();
596        assert_eq!(rs.words.len(), 4);
597
598        let roll = unwrap_dice!(&rs.words[0]);
599        assert_eq!(roll.rolls().len(), 3);
600        assert_eq!(roll.sides(), 8);
601
602        assert_eq!(rs.words[1], DiceWords::Minus);
603        assert_eq!(rs.words[2], DiceWords::Bonus(2));
604    }
605
606    #[test]
607    fn bonus_and_roll() {
608        let rs = "d8+2 slashing".parse::<RollSet>().unwrap();
609        assert_eq!(rs.words.len(), 5);
610
611        let roll = unwrap_dice!(&rs.words[0]);
612        assert_eq!(roll.sides(), 8);
613        assert_eq!(roll.rolls().len(), 1);
614
615        assert_eq!(rs.words[1], DiceWords::Plus);
616        assert_eq!(rs.words[2], DiceWords::Bonus(2));
617        assert_eq!(rs.words[3], DiceWords::Other("slashing".to_string()));
618    }
619
620    #[test]
621    fn two_dice_sizes() {
622        let rs = "2d5 + 3d9".parse::<RollSet>().unwrap();
623        assert_eq!(rs.words.len(), 4);
624        let mut iter = rs.words.iter();
625
626        let d = unwrap_dice!(iter.next().unwrap());
627        assert_eq!(d.rolls().len(), 2);
628        assert_eq!(d.sides(), 5);
629
630        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
631
632        let d = unwrap_dice!(iter.next().unwrap());
633        assert_eq!(d.rolls().len(), 3);
634        assert_eq!(d.sides(), 9);
635    }
636
637    #[test]
638    fn two_rolls_text() {
639        let rs = "2d5 + 3d9 for foo, 4d6 for bar".parse::<RollSet>().unwrap();
640        assert_eq!(rs.words.len(), 7);
641        let mut iter = rs.words.iter();
642
643        let d = unwrap_dice!(iter.next().unwrap());
644        assert_eq!(d.rolls().len(), 2);
645        assert_eq!(d.sides(), 5);
646
647        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
648
649        let d = unwrap_dice!(iter.next().unwrap());
650        assert_eq!(d.rolls().len(), 3);
651        assert_eq!(d.sides(), 9);
652
653        assert_eq!(iter.next().unwrap(), &DiceWords::Other("for foo,".to_string()));
654
655        let d = unwrap_dice!(iter.next().unwrap());
656        assert_eq!(d.rolls().len(), 4);
657        assert_eq!(d.sides(), 6);
658
659        assert_eq!(iter.next().unwrap(), &DiceWords::Other("for bar".to_string()));
660    }
661
662    #[test]
663    fn roll_no_d0() {
664        let rsr = "3d0".parse::<RollSet>();
665        assert_eq!(rsr, Err(RollParseError::MissingRoll));
666
667        let rsr = "0d5".parse::<RollSet>();
668        assert_eq!(rsr, Err(RollParseError::MissingRoll));
669    }
670
671    #[test]
672    fn roll_d1() {
673        let rs = "3d1".parse::<RollSet>().unwrap();
674        assert_eq!(rs.words.len(), 2);
675
676        let roll = unwrap_dice!(&rs.words[0]);
677        assert_eq!(roll.sides(), 1);
678        assert_eq!(roll.total(), 3);
679        assert_eq!(rs.words[1], DiceWords::Total(3));
680    }
681
682    // Using the dice roller as a calculator is no longer allowed.
683
684    #[test]
685    fn roll_no_dice() {
686        let rsr = "4".parse::<RollSet>();
687        assert_eq!(rsr, Err(RollParseError::InvalidOrder));
688    }
689
690    #[test]
691    fn roll_no_dice_plus() {
692        let rsr = "+4".parse::<RollSet>();
693        assert_eq!(rsr, Err(RollParseError::MissingRoll));
694    }
695
696    #[test]
697    fn roll_simple_math() {
698        let rsr = "2 +4-1".parse::<RollSet>();
699        assert_eq!(rsr, Err(RollParseError::InvalidOrder));
700    }
701
702    #[test]
703    fn roll_bad_dice() {
704        assert_eq!("4d-4".parse::<RollSet>(), Err(RollParseError::MissingRoll));
705        assert_eq!("ddd".parse::<RollSet>(), Err(RollParseError::MissingRoll));
706    }
707
708    #[test]
709    fn leading_space() {
710        let rs = " 4d20 * 1.5".parse::<RollSet>().unwrap();
711        assert_eq!(rs.words.len(), 4);
712
713        let d = unwrap_dice!(&rs.words[0]);
714        assert_eq!(d.sides(), 20);
715        assert_eq!(d.rolls().len(), 4);
716
717        assert_eq!(rs.words[1], DiceWords::Times);
718        assert_eq!(rs.words[2], DiceWords::Multiplier(1.5));
719        assert!(matches!(rs.words[3], DiceWords::Total(_)));
720    }
721
722    #[test]
723    fn leading_tab() {
724        let rs = "\u{0009}4d20*1.5".parse::<RollSet>().unwrap();
725        //println!("{:?}", rs.words);
726        assert_eq!(rs.words.len(), 4);
727
728        let d = unwrap_dice!(&rs.words[0]);
729        assert_eq!(d.sides(), 20);
730        assert_eq!(d.rolls().len(), 4);
731
732        assert_eq!(rs.words[1], DiceWords::Times);
733        assert_eq!(rs.words[2], DiceWords::Multiplier(1.5));
734        assert!(matches!(rs.words[3], DiceWords::Total(_)));
735    }
736
737    #[test]
738    fn add_constant() {
739        let rs = "1d1+4".parse::<RollSet>().unwrap();
740        //println!("{:?}", rs.words);
741        assert_eq!(rs.words.len(), 4);
742
743        let roll = unwrap_dice!(&rs.words[0]);
744        assert_eq!(roll.sides(), 1);
745        assert_eq!(roll.total(), 1);
746        assert_eq!(rs.words[1..], [ DiceWords::Plus, DiceWords::Bonus(4), DiceWords::Total(5) ]);
747    }
748
749    #[test]
750    fn add_to_many() {
751        let rs = "d20 + d6 + 4 to hit".parse::<RollSet>().unwrap();
752        assert_eq!(rs.words.len(), 7);
753
754        let mut iter = rs.words.iter();
755
756        let d = unwrap_dice!(iter.next().unwrap());
757        assert_eq!(d.sides(), 20);
758        assert_eq!(d.rolls().len(), 1);
759
760        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
761
762        let d = unwrap_dice!(iter.next().unwrap());
763        assert_eq!(d.sides(), 6);
764        assert_eq!(d.rolls().len(), 1);
765
766        assert_eq!(iter.next().unwrap(), &DiceWords::Plus);
767        assert_eq!(iter.next().unwrap(), &DiceWords::Bonus(4));
768        assert_eq!(iter.next().unwrap(), &DiceWords::Other("to hit".to_string()));
769    }
770
771    #[test]
772    fn add_constant_str() {
773        let rs = "d1+4".parse::<RollSet>().unwrap();
774        assert_eq!("1d1 [1] + 4 = 5".to_string(), format!("{}", rs));
775    }
776
777    #[test]
778    fn many_rollv_str() {
779        let rs = "d1 + 2d1 - 4 to hit".parse::<RollSet>().unwrap();
780        assert_eq!("1d1 [1] + 2d1 [2] \u{2212} 4 |to hit| = -1", format!("{}", rs));
781    }
782
783    #[test]
784    fn multiply_even() {
785        let rs = "10d1*1.5".parse::<RollSet>().unwrap();
786        assert_eq!("10d1 [10] \u{00d7} 1.5 = 15", format!("{}", rs));
787    }
788
789    #[test]
790    fn multiply_odd() {
791        let rs = "11d1*1.5".parse::<RollSet>().unwrap();
792        assert_eq!("11d1 [11] \u{00d7} 1.5 = 16", format!("{}", rs));
793    }
794
795    #[test]
796    fn multiply_ooo() {
797        let rs = "1d1*1.5 + 1d1 * 1.5".parse::<RollSet>().unwrap();
798        assert_eq!("1d1 [1] \u{00d7} 1.5 + 1d1 [1] \u{00d7} 1.5 = 3", format!("{}", rs));
799        // vs (1 * 1.5 = 1) + (1 * 1.5 = 1) = 2
800    }
801
802    #[test]
803    fn more_like_chat() {
804        let line = "/roll d1 + 2d1 \u{2212} 4 to hit; 1d1 for damage";
805        let chatter: Vec<&str> = line.splitn(2, char::is_whitespace).collect();
806        assert_eq!(chatter.len(), 2);
807        assert!(chatter[0].starts_with("/r"));
808
809        let rolls: Result<String, RollParseError> = chatter[1].split(';').try_fold("".to_string(), |acc, x| {
810            let rs = x.parse::<RollSet>()?;
811            if acc.is_empty() {
812                Ok(format!("rolls {}", rs))
813            } else {
814                Ok(acc + &format!("; {}", rs))
815            }
816        });
817
818        assert!(rolls.is_ok());
819        assert_eq!(rolls.unwrap(), "rolls 1d1 [1] + 2d1 [2] \u{2212} 4 |to hit| = -1; 1d1 [1] |for damage| = 1");
820    }
821
822    #[test]
823    fn dice_from_str() {
824        assert_eq!(Ok(Dice::new(1, 1).unwrap()), "1d1".parse());
825    }
826
827    #[test]
828    fn big_dice_bad() {
829        assert_eq!("d10000".parse::<RollSet>(), Err(RollParseError::MissingRoll));
830        assert_eq!("1d10000".parse::<RollSet>(), Err(RollParseError::MissingRoll));
831        assert_eq!("10001d10".parse::<RollSet>(), Err(RollParseError::MissingRoll));
832    }
833
834    #[test]
835    fn comment() {
836        assert_eq!(format!("{}", "4d1 # foo".parse::<RollSet>().unwrap()), "4d1 [4] = 4 |# foo|");
837    }
838
839    #[test]
840    fn ends_with_operator() {
841        assert!("1d6+".parse::<RollSet>().is_err());
842        assert!("1d6x".parse::<RollSet>().is_err());
843        assert!("1d6-".parse::<RollSet>().is_err());
844    }
845
846    #[test]
847    fn multiplier_other_ok() {
848        assert!("1d20+4+1d6-3*1.5 for ham + 4 \u{2212} 1d6 foo".parse::<RollSet>().is_ok());
849    }
850
851    #[test]
852    fn times() {
853        assert!("1d6x4".parse::<RollSet>().is_ok());
854        assert!("1d6X4".parse::<RollSet>().is_ok());
855        assert!("1d6*4".parse::<RollSet>().is_ok());
856        assert!("1d6\u{00d7}4".parse::<RollSet>().is_ok());
857        assert!("1d6 complex".parse::<RollSet>().is_ok());
858        assert!("1d6 x".parse::<RollSet>().is_err());
859        assert!("1d6x".parse::<RollSet>().is_err());
860    }
861}