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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
use rand::Rng;
use std::str;
use std::fmt;
use regex::Regex;
#[derive(Debug)]
pub enum DiceRollError{
    RollSyntax(String),
    Undefined(String)
}

#[derive(Debug)]
pub enum DiceRollType{
    Sum,
    Pool
}
pub struct DiceRoll{
    /// Dice Quantity
    dice_qty: isize,
    /// Dice Type
    dice_type: isize,
    /// Increment
    inc: isize, 
    ///Decrement
    dec: isize, 
    ///Target Number. Defaults to 0
    tn: isize,
    /// Type of roll - influences roll_dice() and final_result 
    roll_type: DiceRollType,
    /// Results for each die
    roll_results: Vec<isize>,
    ///Final result - Sum of roll results or number of successes in dicepool
    final_result: isize
}

pub fn run(roll_str: &str, default_tn: isize , is_pool: bool, is_verbose: bool) -> Result<(),DiceRollError> {
    let mut d = roll_str.parse::<DiceRoll>()?;
    if is_pool {d.roll_type = DiceRollType::Pool};
    if default_tn > 0 && d.tn == 0 { d.tn = default_tn };
    d.roll_dice();
    if is_verbose {println!("{:#?}",d)} else {println!("{}",d)};
    Ok(())
}

///Recieves a string of numbers divided by '+' and '-'
fn parse_modifiers(mods_str: String) -> (isize,isize) {
    let mut inc = 0;
    let mut dec = 0;
    let mut aux_str = "".to_string();
    let mut mod_iter = mods_str.chars(); 
    loop {
        let c = mod_iter.next().unwrap_or('\r');
        if c == '+' || c == '-' || c == '\r' {
            if !aux_str.is_empty() {
                let aux_int = aux_str.parse::<isize>().unwrap_or_default();
                if aux_int > 0 {inc += aux_int} else {dec += aux_int};
                aux_str.clear();
            }
            if c == '\r' {break};
        }
        aux_str.push(c);        
    }
    (inc,dec)
}

impl DiceRoll {
    fn new(dice_qty: isize,
               dice_type: isize,
               inc: isize,
               dec: isize,
               tn: isize,
               roll_type: DiceRollType
    ) -> Self {

        if dice_qty <= 0 {panic!("Invalid dice quantity")};
        if dice_type <= 0 {panic!("Invalid dice type")};
        if inc < 0 {panic!("Invalid increment - must be positive or zero")};
        if dec > 0 {panic!("Invalid decrement - must be negative or zero")};
        DiceRoll{
            dice_qty,
            dice_type,
            inc,
            dec,
            tn,
            roll_type,
            roll_results : vec![],
            final_result : 0 //Either the modified sum or the number of dice pool Successes
        }
    }

    pub fn roll_dice(&mut self) {
        //Reset variables
        self.roll_results.clear();
        self.final_result = 0;
        match self.roll_type {
            DiceRollType::Sum => self.roll_sum(),
            DiceRollType::Pool => self.roll_pool()
        }
    }

    fn roll_sum(&mut self) {
        for _i in 0..self.dice_qty {
            self.roll_results.push(rand::thread_rng().gen_range(1, self.dice_type+1));
        }
        self.final_result = self.roll_results.iter().sum::<isize>() + self.inc + self.dec;
    }

    fn roll_pool(&mut self){
        for _i in 0..(self.dice_qty + self.inc + self.dec) { //Number of dice is modified
            let r =rand::thread_rng().gen_range(1, self.dice_type+1);
            if r  >= self.tn {self.final_result+=1;}
            self.roll_results.push(r);
        }
    }
}

impl str::FromStr for DiceRoll {
    type Err = DiceRollError;
    fn from_str(s: &str) -> Result<Self,Self::Err> {
        let re = Regex::new(r"^(?P<dice_qty>\d+)[d|D](?P<dice_type>\d+)(?P<mods>(?:\+\d+|-\d+)+)?(?:#(?P<tn>\d+))?$").unwrap();

        let caps  = re.captures(s).ok_or::<DiceRollError>(DiceRollError::RollSyntax(s.to_string()))?;
        
        //Both dice_qty, tn and dice_type can use unwrap()
        //the Regex will catch any non-numeric character
        let dice_qty = caps.name("dice_qty").unwrap().as_str().parse::<isize>().unwrap();
        let dice_type = caps.name("dice_type").unwrap().as_str().parse::<isize>().unwrap();
        
        let tn = match caps.name("tn") {
            Some(m) => m.as_str().parse::<isize>().unwrap_or_default(),
            None => 0
        };

        let mods: (isize,isize) = match caps.name("mods") { //inc,dec
            Some(m) => {
                parse_modifiers(m.as_str().to_string())   
            }
            None => (0,0)
        };

        let roll_type = DiceRollType::Sum;
        Ok(DiceRoll::new(
            dice_qty,
            dice_type,
            mods.0,
            mods.1,
            tn,
            roll_type
        ))
    }
}

impl fmt::Display for DiceRoll {
    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.roll_type {
            DiceRollType::Sum => {
                let base_result:isize = self.roll_results.iter().sum();
                let mod_str = if self.inc > 0 || self.dec < 0 {format!(" ➔ {}",self.final_result)} else {"".to_string()};
                if self.tn > 0 {
                   let success_str = if self.final_result >= self.tn {" ✔ "} else {" ✗ "};
                    write!(f,"[{}{}{}]",base_result,mod_str,success_str)
                }
                else{
                    write!(f,"[{}{}]",
                           base_result,
                           mod_str,)
                }
            }
            DiceRollType::Pool => {
                let mut results_str = String::from("");
                let success_str = if self.tn>0 {
                    let word = if self.final_result == 1 { "Success" } else { "Successes" };
                    format!("| {} {}",self.final_result,word)
                } else {
                    "".to_string()
                };
                for die_result in &self.roll_results{
                    results_str += &die_result.to_string()[..];
                    results_str += ",";
                }
                results_str.pop();
                write!(f,"[{}{}]",results_str,success_str)
            }
        }
    }
}

impl fmt::Debug for DiceRoll {
   fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Dice Roll")
        .field("Dice quantity", &self.dice_qty)
        .field("Dice type", &self.dice_type)
        .field("Increment", &self.inc)
        .field("Decrement", &self.dec)
        .field("Target number", &self.tn)
        .field("Type", &self.roll_type)
        .field("Results", &format_args!("{:?}",self.roll_results))
        .field("Final Result", &self.final_result)
        .field("Bottomline", &self.to_string())
        .finish()
    }
}

impl fmt::Display for DiceRollError {
    fn fmt(&self,f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self{
            DiceRollError::RollSyntax(m) => {
                write!(f,"Syntax Error on {}: your roll must follow the following pattern: XdY[+i-d][#target]\nRun roll --help to learn more",m)
            }
            DiceRollError::Undefined(m) => {
                write!(f,"Undefined Error: {}",m)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    #[should_panic(expected = "Invalid dice quantity")]
    fn invalid_dice_qty(){
        let _x = DiceRoll::new(-21,6,4,-2,11,DiceRollType::Sum);
    }

    #[test]
    #[should_panic(expected = "Invalid dice type")]
    fn invalid_dice_type(){
        let _x = DiceRoll::new(1,-6,4,-2,11,DiceRollType::Sum);
    }

    #[test]
    fn force_one_sum(){
        let mut x = DiceRoll::new(1,1,2,0,0,DiceRollType::Sum);
        x.roll_dice();
        assert_eq!(x.final_result,3)
    }

    #[test]
    fn force_one_pool(){
        let mut x = DiceRoll::new(2,1,0,-1,1,DiceRollType::Pool);
        x.roll_dice();
        assert_eq!(x.final_result,1)
    }

    #[test]
    fn valid_roll_from_str(){
        let x = "2d20+12-3+1-1-2+4#10".parse::<DiceRoll>();
        assert_eq!(true,x.is_ok());
        let d = x.unwrap();
        assert_eq!(d.dice_qty,2);
        assert_eq!(d.dice_type,20);
        assert_eq!(d.inc,17);
        assert_eq!(d.dec,-6);
        assert_eq!(d.tn,10);
        println!("{:?}",d);
    }
    
    #[test]
    fn invalid_roll_from_str(){
        let x = "2d20+0.4".parse::<DiceRoll>();
        assert_eq!(true,x.is_err());
        println!("{}",x.err().unwrap());

        let x = "Ad10-4".parse::<DiceRoll>();
        assert_eq!(true,x.is_err());
        println!("{}",x.err().unwrap());
    }    
}