use anyhow::Result; use rand::seq::SliceRandom; use rand::Rng; use regex::Regex; use thiserror::Error; #[cfg(test)] mod lib_test; #[derive(Debug, PartialEq, Clone)] enum DiceFilter { DropLowest(usize), DropHighest(usize), KeepLowest(usize), KeepHighest(usize), } #[derive(Debug, PartialEq, Clone)] enum Segment { DiceRoll { op: char, count: i32, size: i32, filter: Option, }, Modifier { op: char, amount: i32, }, } #[derive(Error, Debug, PartialEq)] enum SegmentError { #[error("No dice size was given")] SizeIsMissing, #[error("Invalid filter operator: {op}")] InvalidFilterOperator { op: String }, #[error("Incomplete filter")] IncompleteFilter, } fn construct_dice_filter(op: &str, amount: usize) -> Result { match op.to_lowercase().as_str() { "d" | "dl" => Ok(DiceFilter::DropLowest(amount)), "dh" => Ok(DiceFilter::DropHighest(amount)), "k" | "kh" => Ok(DiceFilter::KeepHighest(amount)), "kl" => Ok(DiceFilter::KeepLowest(amount)), _ => Err(SegmentError::InvalidFilterOperator { op: op.to_owned() }.into()), } } fn construct_dice_segment(cap: regex::Captures<'_>) -> Result { let op = cap .name("op") .and_then(|i| i.as_str().chars().next()) .unwrap_or('+'); let modifier = cap.name("mod").map(|i| i.as_str().parse()).transpose()?; if let Some(amount) = modifier { return Ok(Segment::Modifier { op, amount }); } let count: i32 = cap .name("count") .map(|i| i.as_str().parse()) .transpose()? .unwrap_or(1); let size: i32 = cap .name("size") .map(|i| i.as_str().parse()) .transpose()? .ok_or(SegmentError::SizeIsMissing)?; let filter_amount = cap .name("filter") .map(|i| i.as_str().parse::()) .transpose()?; let filter_op = cap.name("filter_op").map(|op| op.as_str()); /* dbg!(filter_amount); dbg!(filter_op); */ if filter_amount.is_some() != filter_op.is_some() { return Err(SegmentError::IncompleteFilter.into()); } let filter = filter_amount .and_then(|amount| (filter_op.map(|op| construct_dice_filter(op, amount)))) .transpose()?; Ok(Segment::DiceRoll { op, count, size, filter, }) } fn parse_dice_segments(cmd: &str) -> Result> { let regex = Regex::new( r#"(?x) (?P[+\-/*])? \s* (?: (?: (?P\d+)? # count (optional) d (?P\d+) # dice size (?P[dk][hl]?)? (?P\d+)? ) | (?: (?P\d+) ) ) "#, ) .expect("Failed to compile regex"); regex .captures_iter(cmd) .map(construct_dice_segment) .collect() } #[derive(Debug, PartialEq, Clone)] struct RollWithModifier { diceroll: Option, modifiers: Vec, } fn group_modifiers_to_dicerolls(segments: &[Segment]) -> Vec { let mut results = Vec::new(); let mut current_diceroll: Option = None; let mut current_modifiers = Vec::new(); for segment in segments { if let Segment::DiceRoll { .. } = segment { if current_diceroll.is_some() || !current_modifiers.is_empty() { let with_modifier = RollWithModifier { diceroll: current_diceroll.clone(), modifiers: current_modifiers.clone(), }; current_modifiers.clear(); results.push(with_modifier); } current_diceroll = Some(segment.clone()); } else if let Segment::Modifier { .. } = segment { current_modifiers.push(segment.clone()); } } if current_diceroll.is_some() || !current_modifiers.is_empty() { let with_modifier = RollWithModifier { diceroll: current_diceroll, modifiers: current_modifiers.clone(), }; current_modifiers.clear(); results.push(with_modifier); } results } #[derive(Debug, PartialEq, Clone)] struct Roll { operator: char, results: Vec, total: i32, } #[derive(Error, Debug, PartialEq, Clone)] enum RollError { #[error("Roll failed. Empty RollWithModifier")] EmptyRollWithModifier, #[error("Roll failed. Invalid filter operator '{0}'")] InvalidFilterOperator(char), #[error("Roll failed. Invalid segment operator '{0}'")] InvalidOperator(char), } fn roll_dice_segments( rwms: &[RollWithModifier], mut rng: R, ) -> Result, RollError> { rwms.iter() .map(|rwm| { let mut results = Vec::new(); let mut total = 0; // store first operator let operator = if let Some(Segment::DiceRoll { op, .. }) = rwm.diceroll { op } else if let Some(Segment::Modifier { op, .. }) = rwm.modifiers.iter().next() { *op } else { return Err(RollError::EmptyRollWithModifier); }; if let Some(Segment::DiceRoll { count, size, filter, .. }) = &rwm.diceroll { results = (0..*count).map(|_| rng.gen_range(1, size + 1)).collect(); results.sort(); if let Some(filter) = filter { match filter { DiceFilter::DropLowest(n) => { results = results.into_iter().skip(*n).collect(); } DiceFilter::DropHighest(n) => { let len = results.len(); results = results.into_iter().take(len - n).collect(); } DiceFilter::KeepLowest(n) => { results = results.into_iter().take(*n).collect(); } DiceFilter::KeepHighest(n) => { let len = results.len(); results = results.into_iter().skip(len - n).collect(); } } } results.shuffle(&mut rng); total = results.iter().sum(); } for segment in &rwm.modifiers { if let Segment::Modifier { op, amount } = segment { match op { '+' => { total += amount; } '-' => { total -= amount; } '/' => { total /= amount; } '*' => { total *= amount; } _ => { return Err(RollError::InvalidFilterOperator(*op)); } } } } Ok(Roll { operator, results, total, }) }) .collect::, _>>() } #[derive(Debug, PartialEq, Clone)] struct RollSet { total: i32, rolls: Vec<(Roll, Vec)>, } fn roll_dice(s: &str, mut rng: R) -> Result { let segments = parse_dice_segments(s)?; let groups = group_modifiers_to_dicerolls(&segments); let rolls = roll_dice_segments(&groups, &mut rng)?; let total = rolls.iter().try_fold(0, |acc, roll| match roll.operator { '+' => Ok(acc + roll.total), '-' => Ok(acc - roll.total), '*' => Ok(acc * roll.total), '/' => Ok(acc / roll.total), _ => Err(RollError::InvalidOperator(roll.operator)), })?; let rolls_with_modifiers = rolls .into_iter() .zip(groups.into_iter().map(|grp| grp.modifiers)) .collect(); Ok(RollSet { total, rolls: rolls_with_modifiers, }) }