1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
//! Dice expression

use owo_colors::OwoColorize;
use rand::Rng;

use crate::regex;
use crate::Pcg;
use std::{fmt::Display, str::FromStr};

/// A description of a dice roll
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Roll {
    /// Number of dice
    pub amount: u16,
    /// Number of sides
    pub sides: u16,
}

/// Error from [`Roll::from_str`]
#[derive(Debug, thiserror::Error)]
pub enum RollParseError {
    #[error("The input is not a dice roll")]
    NoMatch,
    #[error("Invalid dice roll: {0}")]
    Invalid(String),
}

impl FromStr for Roll {
    type Err = RollParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let re = regex!(r"(\d+)?d(\d+|%)");

        let caps = re.captures(s).ok_or(RollParseError::NoMatch)?;

        let amount = caps.get(1).map_or(Ok(1), |m| {
            m.as_str()
                .parse::<u16>()
                .map_err(|e| RollParseError::Invalid(format!("bad amount: {e}")))
                .and_then(|a| {
                    if a == 0 {
                        Err(RollParseError::Invalid("amount can't be 0".to_string()))
                    } else {
                        Ok(a)
                    }
                })
        })?;
        let sides = match &caps[2] {
            "%" => 100,
            num => num
                .parse::<u16>()
                .map_err(|e| RollParseError::Invalid(format!("bad number of sides: {e}")))
                .and_then(|s| {
                    if s == 0 {
                        Err(RollParseError::Invalid(
                            "number of sides can't be 0".to_string(),
                        ))
                    } else {
                        Ok(s)
                    }
                })?,
        };
        Ok(Roll { amount, sides })
    }
}

impl Display for Roll {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        use owo_colors::AnsiColors::*;
        let color = match self.sides {
            1 => BrightBlack,
            4 => BrightGreen,
            6 => BrightBlue,
            8 => BrightRed,
            10 => BrightCyan,
            12 => BrightYellow,
            20 => BrightMagenta,
            _ => BrightWhite,
        };

        if self.amount > 1 {
            write!(f, "{}", self.amount.color(color).italic())?;
        }
        write!(f, "{}{}", "d".color(color), self.sides.color(color))
    }
}

/// Result of a [`Roll`] evaluation
///
/// The [`Display`] [alternate modifier](std::fmt#sign0) will only print
/// [`RollResult::total`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RollResult {
    /// Original roll description
    pub roll: Roll,
    dice: Vec<u16>,
    total: u32,
}

impl Roll {
    pub(crate) fn eval(&self, rng: &mut Pcg) -> RollResult {
        let dice: Vec<_> = (0..self.amount)
            .map(|_| rng.gen_range(1..=self.sides))
            .collect();
        RollResult {
            roll: *self,
            total: dice.iter().map(|&v| v as u32).sum(),
            dice,
        }
    }
}

impl RollResult {
    /// Results obtained
    pub fn dice(&self) -> &[u16] {
        &self.dice
    }

    /// Total value
    pub fn total(&self) -> u32 {
        self.total
    }
}

impl Display for RollResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if f.alternate() {
            return self.total.fmt(f);
        }

        write!(f, "{}: ", self.roll)?;
        if self.dice.len() == 1 {
            write!(f, "{}", self.dice[0])
        } else {
            write!(f, "[{}", self.dice[0])?;
            for val in &self.dice[1..] {
                write!(f, "{}{val}", "+".dimmed())?;
            }
            write!(f, "] = {}", self.total)
        }
    }
}