use std::ops::Add; use rand::rngs::ThreadRng; use rand::Rng; pub const NUM_DIE_LEVELS: usize = 6; pub const DIE_VALUES: [u8; NUM_DIE_LEVELS] = [4, 6, 8, 10, 12, 20]; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Die(u8); pub const D_4: Die = Die(0); pub const D_6: Die = Die(1); pub const D_8: Die = Die(2); pub const D_10: Die = Die(3); pub const D_12: Die = Die(4); pub const D_20: Die = Die(5); #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct RiskDie(Die); pub const DELTA_4: RiskDie = RiskDie(Die(0)); pub const DELTA_6: RiskDie = RiskDie(Die(1)); pub const DELTA_8: RiskDie = RiskDie(Die(2)); pub const DELTA_10: RiskDie = RiskDie(Die(3)); pub const DELTA_12: RiskDie = RiskDie(Die(4)); pub const DELTA_20: RiskDie = RiskDie(Die(5)); #[derive(Debug, PartialEq, Eq)] pub enum Error { InvalidLevel, InvalidValue, } impl Die { pub fn from_level(level: u8) -> Result { if (level as usize) < DIE_VALUES.len() { Ok(Self(level)) } else { Err(Error::InvalidLevel) } } pub fn from_value(value: u8) -> Result { for (i, val) in DIE_VALUES.iter().enumerate() { if *val == value { return Ok(Self(i as u8)); } } Err(Error::InvalidValue) } pub fn level(&self) -> u8 { self.0 } pub fn value(&self) -> u8 { DIE_VALUES[self.0 as usize] } #[tarpaulin::skip] pub fn roll(&self) -> u8 { let mut rng = rand::thread_rng(); self.roll_with_rng(&mut rng) } pub fn roll_with_rng(&self, rng: &mut ThreadRng) -> u8 { rng.gen_range(1..=self.value()) } pub fn can_be_lowered(&self) -> bool { self.0 > 0 } pub fn level_down(mut self) -> Option { if self.can_be_lowered() { self.0 -= 1; Some(self) } else { /* Resource depleted */ None } } pub fn can_be_upped(&self) -> bool { self.0 < NUM_DIE_LEVELS as u8 - 1 } pub fn level_up(mut self) -> Self { if self.can_be_upped() { self.0 += 1; } self } } impl RiskDie { pub fn from_level(level: u8) -> Result { Ok(Self { 0: Die::from_level(level)?, }) } pub fn from_value(value: u8) -> Result { Ok(Self { 0: Die::from_value(value)?, }) } pub fn level(&self) -> u8 { self.0.level() } pub fn value(&self) -> u8 { self.0.value() } /// Roll the die. If the die shows 3 or less, it returns the next lower die. /// If it is more, it returns itself. If it was already the lowest die value /// and it would be stepped down it returns `None`. #[tarpaulin::skip] pub fn roll(self) -> Option { let mut rng = rand::thread_rng(); self.roll_with_rng(&mut rng) } pub fn roll_with_rng(self, rng: &mut ThreadRng) -> Option { let rolled = self.0.roll_with_rng(rng); if rolled > 3 { Some(self) } else { self.level_down() } } pub fn can_be_lowered(&self) -> bool { self.0.can_be_lowered() } pub fn level_down(self) -> Option { self.0.level_down().map(|d| RiskDie(d)) } pub fn can_be_upped(&self) -> bool { self.0.can_be_upped() } pub fn level_up(self) -> Self { RiskDie(self.0.level_up()) } } #[rustfmt::skip] const DELTA_ADDITION_TABLE: [[u8; NUM_DIE_LEVELS]; NUM_DIE_LEVELS] = [ /* ------------------- with d4 d6 d8 d10 d12 d20 */ /* delta 4 combinations */ [1, 2, 3, 4, 4, 5], /* delta 6 combinations */ [2, 2, 3, 4, 5, 5], /* delta 8 combinations */ [3, 3, 4, 4, 5, 5], /* delta 10 combinations */ [4, 4, 4, 5, 5, 5], /* delta 12 combinations */ [4, 5, 5, 5, 5, 5], /* delta 20 combinations */ [5, 5, 5, 5, 5, 5], ]; impl Add for RiskDie { type Output = Self; fn add(self, other: Self) -> Self::Output { RiskDie(Die( DELTA_ADDITION_TABLE[self.0.level() as usize][other.0.level() as usize] )) } } #[cfg(test)] mod tests { use super::*; #[test] fn create_risk_die_from_level() { let die = RiskDie::from_level(0).expect("Unable to create die for correct level"); assert_eq!(die.value(), 4); let die = RiskDie::from_level(1).expect("Unable to create die for correct level"); assert_eq!(die.value(), 6); let die = RiskDie::from_level(2).expect("Unable to create die for correct level"); assert_eq!(die.value(), 8); let die = RiskDie::from_level(3).expect("Unable to create die for correct level"); assert_eq!(die.value(), 10); let die = RiskDie::from_level(4).expect("Unable to create die for correct level"); assert_eq!(die.value(), 12); let die = RiskDie::from_level(5).expect("Unable to create die for correct level"); assert_eq!(die.value(), 20); } #[test] fn create_risk_die_from_invalid_level() { assert_eq!( RiskDie::from_level(6).expect_err("Should not be able to create die from level"), Error::InvalidLevel ); } #[test] fn create_risk_die_from_value() { let die = RiskDie::from_value(4).expect("Unable to create die from correct value"); assert_eq!(die.level(), 0); let die = RiskDie::from_value(6).expect("Unable to create die from correct value"); assert_eq!(die.level(), 1); let die = RiskDie::from_value(8).expect("Unable to create die from correct value"); assert_eq!(die.level(), 2); let die = RiskDie::from_value(10).expect("Unable to create die from correct value"); assert_eq!(die.level(), 3); let die = RiskDie::from_value(12).expect("Unable to create die from correct value"); assert_eq!(die.level(), 4); let die = RiskDie::from_value(20).expect("Unable to create die from correct value"); assert_eq!(die.level(), 5); } #[test] fn create_risk_die_from_invalid_value() { assert_eq!(RiskDie::from_value(5), Err(Error::InvalidValue)); assert_eq!(RiskDie::from_value(21), Err(Error::InvalidValue)); assert_eq!(RiskDie::from_value(0), Err(Error::InvalidValue)); } #[test] fn risk_die_level_down() { assert_eq!(DELTA_4.level_down(), None); assert_eq!(DELTA_6.level_down(), Some(DELTA_4)); assert_eq!(DELTA_8.level_down(), Some(DELTA_6)); assert_eq!(DELTA_10.level_down(), Some(DELTA_8)); assert_eq!(DELTA_12.level_down(), Some(DELTA_10)); assert_eq!(DELTA_20.level_down(), Some(DELTA_12)); } #[test] fn risk_die_level_up() { assert_eq!(DELTA_4.level_up(), DELTA_6); assert_eq!(DELTA_6.level_up(), DELTA_8); assert_eq!(DELTA_8.level_up(), DELTA_10); assert_eq!(DELTA_10.level_up(), DELTA_12); assert_eq!(DELTA_12.level_up(), DELTA_20); assert_eq!(DELTA_20.level_up(), DELTA_20); } #[test] fn risk_die_can_be_lowered() { assert!(!DELTA_4.can_be_lowered()); assert!(DELTA_6.can_be_lowered()); assert!(DELTA_8.can_be_lowered()); assert!(DELTA_10.can_be_lowered()); assert!(DELTA_12.can_be_lowered()); assert!(DELTA_20.can_be_lowered()); } #[test] fn risk_die_can_be_upped() { assert!(DELTA_4.can_be_upped()); assert!(DELTA_6.can_be_upped()); assert!(DELTA_8.can_be_upped()); assert!(DELTA_10.can_be_upped()); assert!(DELTA_12.can_be_upped()); assert!(!DELTA_20.can_be_upped()); } #[test] fn add_risk_dice() { assert_eq!(DELTA_4 + DELTA_4, DELTA_6); assert_eq!(DELTA_4 + DELTA_6, DELTA_8); assert_eq!(DELTA_4 + DELTA_8, DELTA_10); assert_eq!(DELTA_4 + DELTA_10, DELTA_12); assert_eq!(DELTA_4 + DELTA_12, DELTA_12); assert_eq!(DELTA_4 + DELTA_20, DELTA_20); assert_eq!(DELTA_6 + DELTA_4, DELTA_8); assert_eq!(DELTA_6 + DELTA_6, DELTA_8); assert_eq!(DELTA_6 + DELTA_8, DELTA_10); assert_eq!(DELTA_6 + DELTA_10, DELTA_12); assert_eq!(DELTA_6 + DELTA_12, DELTA_20); assert_eq!(DELTA_6 + DELTA_20, DELTA_20); assert_eq!(DELTA_8 + DELTA_4, DELTA_10); assert_eq!(DELTA_8 + DELTA_6, DELTA_10); assert_eq!(DELTA_8 + DELTA_8, DELTA_12); assert_eq!(DELTA_8 + DELTA_10, DELTA_12); assert_eq!(DELTA_8 + DELTA_12, DELTA_20); assert_eq!(DELTA_8 + DELTA_20, DELTA_20); assert_eq!(DELTA_10 + DELTA_4, DELTA_12); assert_eq!(DELTA_10 + DELTA_6, DELTA_12); assert_eq!(DELTA_10 + DELTA_8, DELTA_12); assert_eq!(DELTA_10 + DELTA_10, DELTA_20); assert_eq!(DELTA_10 + DELTA_12, DELTA_20); assert_eq!(DELTA_10 + DELTA_20, DELTA_20); assert_eq!(DELTA_12 + DELTA_4, DELTA_12); assert_eq!(DELTA_12 + DELTA_6, DELTA_20); assert_eq!(DELTA_12 + DELTA_8, DELTA_20); assert_eq!(DELTA_12 + DELTA_10, DELTA_20); assert_eq!(DELTA_12 + DELTA_12, DELTA_20); assert_eq!(DELTA_12 + DELTA_20, DELTA_20); assert_eq!(DELTA_20 + DELTA_4, DELTA_20); assert_eq!(DELTA_20 + DELTA_6, DELTA_20); assert_eq!(DELTA_20 + DELTA_8, DELTA_20); assert_eq!(DELTA_20 + DELTA_10, DELTA_20); assert_eq!(DELTA_20 + DELTA_12, DELTA_20); assert_eq!(DELTA_20 + DELTA_20, DELTA_20); } }