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
269
270
271
272
273
274
275
276
277
278
279
280
// Copyright (C) 2021 Josh Wright

//! Library and binaries to provide tools and functions for managing Dungeons and Dragons 5th Edition.
//!
//! This documentation covers the features provided by the library,
//! including functions, structs, and enums that can be useful to a D&D program.

use rand::Rng;

/// Enum for type of die.
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Die {
    D4 = 4,
    D6 = 6,
    D8 = 8,
    D10 = 10,
    D12 = 12,
    D20 = 20,
    D100 = 100,
}

/// Struct to represent multiple dice of a single type to be rolled, such as 2d6.
#[derive(PartialEq, Debug)]
pub struct DiceToRoll {
    pub die: Die,
    pub number: u32,
}

impl DiceToRoll {
    pub const fn new(number: u32, die: Die) -> DiceToRoll {
        return DiceToRoll {
            die: die,
            number: number,
        };
    }

    //TODO: This should really be a const fn,
    // but Rust does not currently support control flow in const fn
    /// Generate a `DiceToRoll` from a string such as '2d6'.
    pub fn from_string(s: String) -> Result<DiceToRoll, String> {
        let mut d = false; // if the d has been declared in the string yet
        let mut num_str: String = "".to_string(); // number of rolls
        let mut die_str: String = "".to_string(); // type of die
        for ch in s.to_lowercase().chars() {
            if ch == 'd' {
                d = true;
                if num_str == "" {
                    num_str = "1".to_string();
                }
            } else if d {
                die_str.push(ch);
            } else {
                num_str.push(ch);
            }
        }

        let number = match num_str.parse::<u32>() {
            Ok(x) => x,
            Err(_) => return Err("Number is not a valid integer".to_string()),
        };

        let die_int = match die_str.parse::<u32>() {
            Ok(x) => x,
            Err(_) => return Err("Die is not a valid integer".to_string()),
        };

        let die: Die = match die_int {
            4 => Die::D4,
            6 => Die::D6,
            8 => Die::D8,
            10 => Die::D10,
            12 => Die::D12,
            20 => Die::D20,
            100 => Die::D100,
            _ => return Err("Die is not a valid die type".to_string()),
        };

        return Ok(DiceToRoll::new(number, die));
    }
}

/// Struct to represent multiple rolled dice of a single type.
/// Stores individual rolls and a grand total.
pub struct RolledDice {
    pub die: Die,
    pub rolls: Vec<u32>,
    pub total: u32,
}

/// Struct to represent multiple rolled dice of multiple types.
/// Stores a vec of `RolledDice` and a grand total.
pub struct RolledDiceBatch {
    pub types: Vec<RolledDice>,
    pub total: u32,
}

/// Generate a block of PC stats.
///
/// This is done by calculating each stat by rolling four d6 and dropping the lowest.
/// Returns an array of `u8`.
pub fn gen_stats() -> [u8; 6] {
    let mut stats: [u8; 6] = [0; 6];
    let mut rng = rand::thread_rng();

    for i in 0..6 {
        let mut rolls: [u8; 4] = [0; 4];
        for j in 0..4 {
            rolls[j] = rng.gen_range(1, 7);
        }
        rolls.sort();

        // Skip the first (lowest) roll when adding
        for r in rolls.iter().skip(1) {
            stats[i] += r;
        }
    }
    stats.sort();
    return stats;
}

/// Simulate rolling dice.
///
/// Takes a vec of Rolls as a parameter.
/// Each individual roll is calculated and returned in a RolledDiceBatch.
pub fn roll_dice(rolls: Vec<DiceToRoll>) -> RolledDiceBatch {
    let mut ret = RolledDiceBatch {
        types: Vec::new(),
        total: 0,
    };
    let mut rng = rand::thread_rng();

    // die_type represents each type of die given as a parameter
    for die_type in rolls.iter() {
        // die_rolls is a vector containing each individual roll of the die type (inner vecs of return)
        let mut die_rolls = RolledDice {
            die: die_type.die,
            rolls: Vec::new(),
            total: 0,
        };
        for _ in 0..die_type.number {
            // this_roll is the value of an individual roll
            let this_roll: u32 = rng.gen_range(1, die_type.die as u32 + 1);
            die_rolls.rolls.push(this_roll);
            die_rolls.total += this_roll;
            ret.total += this_roll;
        }
        ret.types.push(die_rolls);
    }

    return ret;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_dicetoroll_fromstring_4d8() {
        let s = "4d8".to_string();
        let result = DiceToRoll::from_string(s).expect("DiceToRoll::from_string(\"d4d8\" panicked");
        let expect = DiceToRoll::new(4, Die::D8);
        assert_eq!(expect, result);
    }

    #[test]
    fn test_dicetoroll_fromstring_d20() {
        let s = "d20".to_string();
        let result = DiceToRoll::from_string(s).expect("DiceToRoll::from_string(\"d20\") panicked");
        let expect = DiceToRoll::new(1, Die::D20);
        assert_eq!(expect, result);
    }

    #[test]
    fn test_dicetoroll_fromstring_2d7() {
        let s = "2d7".to_string();
        DiceToRoll::from_string(s).expect_err("DiceToRoll::from_string(\"2d7\") passed");
    }

    #[test]
    fn test_stats() {
        let s1 = gen_stats();
        let s2 = gen_stats();
        let s3 = gen_stats();
        let s4 = gen_stats();
        let s5 = gen_stats();
        let test_array = [s1, s2, s3, s4, s5];

        for test in test_array.iter() {
            let mut i = 0;
            while i < 6 {
                assert!(test[i] <= 18);
                assert!(test[i] >= 3);
                i += 1;
            }
        }
    }

    #[test]
    fn test_roll_none() {
        let r = roll_dice(Vec::new());
        assert!(r.types.len() == 0);
        assert!(r.total == 0);
    }

    #[test]
    fn test_roll_d20() {
        let d20 = DiceToRoll::new(1, Die::D20);

        let mut v: Vec<DiceToRoll> = Vec::new();
        v.push(d20);

        let r = roll_dice(v);

        assert!(r.types.len() == 1); // one type of die
        assert!(r.types[0].die == Die::D20); // die is d20
        assert!(r.types[0].rolls.len() == 1); // one die roll
        assert!(r.types[0].rolls[0] >= 1); // assert 1 to 20
        assert!(r.types[0].rolls[0] <= 20); // ""
        assert!(r.types[0].rolls[0] == r.total); // one roll = total
    }

    #[test]
    fn test_roll_4d6() {
        let dice = DiceToRoll::new(4, Die::D6);

        let mut v: Vec<DiceToRoll> = Vec::new();
        v.push(dice);

        let r = roll_dice(v);

        assert!(r.types.len() == 1); // one type of die
        assert!(r.types[0].die == Die::D6); // die is D6
        assert!(r.types[0].rolls.len() == 4); // four rolls

        // assert each roll is between 1 and 6
        for i in 0..4 {
            assert!(r.types[0].rolls[i] >= 1);
            assert!(r.types[0].rolls[i] <= 6);
        }
        // assert the total is between 4 and 24
        assert!(r.types[0].total >= 4);
        assert!(r.types[0].total <= 24);
    }

    #[test]
    fn test_roll_6d6_4d8() {
        let d6 = DiceToRoll::new(6, Die::D6);
        let d8 = DiceToRoll::new(4, Die::D8);

        let mut v: Vec<DiceToRoll> = Vec::new();
        v.push(d6);
        v.push(d8);

        let r = roll_dice(v);

        assert!(r.types.len() == 2); // two types of dice
        assert!(r.types[0].die == Die::D6); // first die is d6
        assert!(r.types[0].rolls.len() == 6); // six d6 rolls

        // assert each d6 roll is between 1 and 6
        for i in 0..6 {
            assert!(r.types[0].rolls[i] >= 1);
            assert!(r.types[0].rolls[i] <= 6);
        }
        assert!(r.types[1].die == Die::D8); // second die is d8
        assert!(r.types[1].rolls.len() == 4); // four d8 rolls

        // assert each d8 roll is between 1 and 8
        for i in 0..4 {
            assert!(r.types[1].rolls[i] >= 1);
            assert!(r.types[1].rolls[i] <= 8);
        }

        // assert the total equals the subtotals
        assert!(r.total == r.types[0].total + r.types[1].total);
        // assert the total is between 10 and 68
        assert!(r.total >= 10);
        assert!(r.total <= 68);
    }
}