diff --git a/src/lib.rs b/src/lib.rs index dc885ab..23f054f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,55 @@ use nom::{branch::alt, character::complete, IResult}; use rand::Rng; +#[derive(Debug)] +pub struct Buckets { + _buckets: [u64; S], + _offsets: [i64; S], +} + +impl Buckets { + /// Creates a new bucketer to store the values that can + /// be produced by roller in constant-spaced bucket. That + /// is, each bucket represents the count of a fixed range, + /// and all buckets have the same size range. + pub fn new(roller: &Roller) -> Self { + let min = roller.min(); + let max = roller.max(); + // Divide the number of buckets we have to work with by the range + // Store the step size + let step = ((max-min) as usize) / S + 1; + let mut _offsets = [0; S]; + let mut cur = min; + for val in _offsets.iter_mut() { + *val = cur; + cur += step as i64; + } + Self { + _buckets: [0; S], + _offsets, + } + } + + pub fn insert(&mut self, val: i64) { + // Binary search to find insertion point + let mut pp = self._offsets.partition_point(|p| *p < val); + if pp == self._buckets.len() { + // This value was beyond the last bucket; go ahead and store if in the + // last bucket + pp = self.buckets().len()-1; + } + self._buckets[pp] += 1; + } + + pub fn buckets(&self) -> &[u64] { + &self._buckets[..] + } + + pub fn labels(&self) -> &[i64] { + &self._offsets[..] + } +} + pub struct Roller { exprs: [Option; S], } @@ -31,7 +80,7 @@ impl Roller { } Ok(Roller{exprs}) } - pub fn roll(&self, rng: &mut R) -> u64 { + pub fn roll(&self, rng: &mut R) -> i64 { let mut sum = 0; for expr in &self.exprs { if expr.is_none() { @@ -39,8 +88,38 @@ impl Roller { } let cmd = expr.as_ref().unwrap(); match cmd.oper { - Oper::Add => sum += cmd.term.val(rng), - Oper::Sub => sum -= cmd.term.val(rng), + Oper::Add => sum += cmd.term.val(rng) as i64, + Oper::Sub => sum -= cmd.term.val(rng) as i64, + }; + } + sum + } + + pub fn min(&self) -> i64 { + let mut sum = 0; + for expr in &self.exprs { + if expr.is_none() { + break; + } + let cmd = expr.as_ref().unwrap(); + match cmd.oper { + Oper::Add => sum += cmd.term.min() as i64, + Oper::Sub => sum -= cmd.term.min() as i64, + }; + } + sum + } + + pub fn max(&self) -> i64 { + let mut sum = 0; + for expr in &self.exprs { + if expr.is_none() { + break; + } + let cmd = expr.as_ref().unwrap(); + match cmd.oper { + Oper::Add => sum += cmd.term.max() as i64, + Oper::Sub => sum -= cmd.term.max() as i64, }; } sum @@ -64,6 +143,20 @@ impl Term { Term::Roll(r) => r.val(rng), } } + + fn min(&self) -> u64 { + match self { + Term::Const(c) => *c, + Term::Roll(r) => r.min(), + } + } + + fn max(&self) -> u64 { + match self { + Term::Const(c) => *c, + Term::Roll(r) => r.max(), + } + } } #[derive(Clone, Copy)] @@ -85,6 +178,16 @@ impl Roll { } total } + + fn min(&self) -> u64 { + // Roll a 1 on each dice + self.reps + } + + fn max(&self) -> u64 { + // Roll max on each dice + self.reps * self.dice + } } @@ -127,13 +230,13 @@ mod tests { fn roll_many_d6s() { let mut rng = StdRng::seed_from_u64(1337); let roller = Roller::<1024>::parse("1d6").unwrap(); - let mut results = [0u64; 6]; + let mut buckets = Buckets::<6>::new(&roller); for _ in 0..1000 { - results[(roller.roll(&mut rng)-1) as usize] += 1; + buckets.insert(roller.roll(&mut rng)); } // We should have some in each bucket - for x in results { - assert!(x > 50); + for x in buckets.buckets() { + assert!(*x > 50); } } @@ -141,45 +244,72 @@ mod tests { fn roll_many_d6s_plus_2() { let mut rng = StdRng::seed_from_u64(1337); let roller = Roller::<1024>::parse("1d6+2").unwrap(); - let mut results = [0u64; 6+2]; + let mut buckets = Buckets::<6>::new(&roller); for _ in 0..1000 { - results[(roller.roll(&mut rng)-1) as usize] += 1; + buckets.insert(roller.roll(&mut rng)); } - // Can't get a 1 or 2, but should have many others - assert!(results[0] == 0); - assert!(results[1] == 0); - for x in &results[2..] { + // We should have some in each bucket + for x in buckets.buckets() { assert!(*x > 50); } + + // The first bucket should be 3 (1 on a d6, +2) + assert_eq!(buckets.labels()[0], 3); + // Last bucket should be 8 + assert_eq!(buckets.labels()[5], 8); } #[test] fn roll_many_d6s_plus_1d6() { let mut rng = StdRng::seed_from_u64(1337); let roller = Roller::<1024>::parse("1d6+1d6").unwrap(); - let mut results = [0u64; 6+6]; + let mut buckets = Buckets::<6>::new(&roller); for _ in 0..1000 { - results[(roller.roll(&mut rng)-1) as usize] += 1; + buckets.insert(roller.roll(&mut rng)); } - // Its impossible to roll 1 - assert!(results[0] == 0); - for x in &results[1..] { + // We should have some in each bucket + for x in buckets.buckets() { assert!(*x > 1); } + + // The first bucket should be 2 + assert_eq!(buckets.labels()[0], 2); + // Last bucket should be 12 + assert_eq!(buckets.labels()[5], 12); } #[test] fn roll_many_d6s_plus_1d6_minus_one() { let mut rng = StdRng::seed_from_u64(1337); let roller = Roller::<1024>::parse("1d6+1d6-1").unwrap(); - let mut results = [0u64; 6+6]; + let mut buckets = Buckets::<6>::new(&roller); for _ in 0..1000 { - results[(roller.roll(&mut rng)-1) as usize] += 1; + buckets.insert(roller.roll(&mut rng)); } - // Its impossible to roll 12 - assert!(results[11] == 0); - for x in &results[..11] { + // We should have some in each bucket + for x in buckets.buckets() { assert!(*x > 1); } + + // The first bucket should be 1 + assert_eq!(buckets.labels()[0], 1); + // Last bucket should be 11 + assert_eq!(buckets.labels()[5], 11); + } + + #[test] + fn negative_result() { + let mut rng = StdRng::seed_from_u64(1337); + let roller = Roller::<1024>::parse("1d6-5").unwrap(); + for _ in 0..1000 { + roller.roll(&mut rng); + } + } + + #[test] + fn min_max() { + let roller = Roller::<1024>::parse("2d6+1d8+1").unwrap(); + assert_eq!(roller.min(), 4); + assert_eq!(roller.max(), 21); } } \ No newline at end of file