1#![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 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
97impl 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 match (&last_word, &parsed) {
203 (DiceWords::Dice(_), DiceWords::Dice(_)) | (DiceWords::Dice(_), DiceWords::Multiplier(_)) | (DiceWords::Dice(_), DiceWords::Bonus(_)) | (DiceWords::Bonus(_), DiceWords::Dice(_)) | (DiceWords::Bonus(_), DiceWords::Bonus(_)) | (DiceWords::Bonus(_), DiceWords::Multiplier(_)) | (DiceWords::Times, DiceWords::Dice(_)) | (DiceWords::Plus, DiceWords::Plus) | (DiceWords::Plus, DiceWords::Minus) | (DiceWords::Plus, DiceWords::Times) | (DiceWords::Plus, DiceWords::Multiplier(_)) | (DiceWords::Plus, DiceWords::Comment(_)) | (DiceWords::Plus, DiceWords::Other(_)) | (DiceWords::Minus, DiceWords::Plus) | (DiceWords::Minus, DiceWords::Minus) | (DiceWords::Minus, DiceWords::Times) | (DiceWords::Minus, DiceWords::Multiplier(_)) | (DiceWords::Minus, DiceWords::Comment(_)) | (DiceWords::Minus, DiceWords::Other(_)) | (DiceWords::Times, DiceWords::Plus) | (DiceWords::Times, DiceWords::Minus) | (DiceWords::Times, DiceWords::Times) | (DiceWords::Times, DiceWords::Comment(_)) | (DiceWords::Times, DiceWords::Other(_)) | (DiceWords::Multiplier(_), DiceWords::Dice(_)) | (DiceWords::Multiplier(_), DiceWords::Bonus(_)) | (DiceWords::Multiplier(_), DiceWords::Multiplier(_)) | (DiceWords::Other(_), DiceWords::Bonus(_)) | (DiceWords::Other(_), DiceWords::Multiplier(_))
239
240 => { return Err(RollParseError::InvalidOrder);
243 },
244
245 (DiceWords::Dice(_), DiceWords::Plus) | (DiceWords::Dice(_), DiceWords::Minus) | (DiceWords::Dice(_), DiceWords::Times) | (DiceWords::Dice(_), DiceWords::Other(_)) | (DiceWords::Bonus(_), DiceWords::Plus) | (DiceWords::Bonus(_), DiceWords::Minus) | (DiceWords::Bonus(_), DiceWords::Times) | (DiceWords::Bonus(_), DiceWords::Other(_)) | (DiceWords::Multiplier(_), DiceWords::Plus) | (DiceWords::Multiplier(_), DiceWords::Minus) | (DiceWords::Multiplier(_), DiceWords::Times) | (DiceWords::Multiplier(_), DiceWords::Other(_)) | (DiceWords::Other(_), DiceWords::Plus) | (DiceWords::Other(_), DiceWords::Minus) | (DiceWords::Other(_), DiceWords::Times) => {
264 words.push(parsed.clone());
265 last_word = parsed;
266 },
267
268 (DiceWords::Plus, DiceWords::Bonus(b)) => { total += *b as i32;
270 words.push(parsed.clone());
271 last_word = parsed;
272 },
273
274 (DiceWords::Minus, DiceWords::Bonus(b)) => { 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)) | (DiceWords::Bonus(_), DiceWords::Comment(s)) | (DiceWords::Other(_), DiceWords::Comment(s)) | (DiceWords::Multiplier(_), DiceWords::Comment(s)) => {
297 words.push(DiceWords::Total(total));
299 last_word = DiceWords::Comment(each.fold(s.to_string(), |acc, x| acc + " " + x));
300 words.push(last_word.clone());
302 break;
303 },
304
305 (DiceWords::Other(_), DiceWords::Dice(d)) | (DiceWords::Plus, DiceWords::Dice(d)) | (DiceWords::Minus, DiceWords::Dice(d)) => { 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 return Err(RollParseError::Failure(line.to_string()));
319 },
320
321 (DiceWords::Other(t), DiceWords::Other(s)) => { 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 if !matches!(last_word, DiceWords::Comment(_)) {
335 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 #[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 #[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 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 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 }
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}