diff --git a/src/lib.rs b/src/lib.rs index 8d42251..d07f7aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,77 +1,102 @@ +use anyhow::Result; +use rand::seq::SliceRandom; +use rand::Rng; use regex::Regex; use thiserror::Error; -use anyhow::Result; -use rand::Rng; -use rand::seq::SliceRandom; #[cfg(test)] mod lib_test; #[derive(Debug, PartialEq, Clone)] enum DiceFilter { - DropLowest(usize), DropHighest(usize), KeepLowest(usize), KeepHighest(usize), + 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, - }, + 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, + #[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()})?, - } + 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()?; + 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 }); - } + 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() { - Err(SegmentError::IncompleteFilter)?; - } - let filter = filter_amount.and_then(|amount| (filter_op.map(|op| construct_dice_filter(op, amount)))).transpose()?; + 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}) + Ok(Segment::DiceRoll { + op, + count, + size, + filter, + }) } fn parse_dice_segments(cmd: &str) -> Result> { - let regex = Regex::new(r#"(?x) + let regex = Regex::new( + r#"(?x) (?P[+\-/*])? \s* (?: @@ -85,154 +110,188 @@ fn parse_dice_segments(cmd: &str) -> Result> { (?P\d+) ) ) - "#).expect("Failed to compile regex"); + "#, + ) + .expect("Failed to compile regex"); - regex.captures_iter(cmd).map(construct_dice_segment).collect() + regex + .captures_iter(cmd) + .map(construct_dice_segment) + .collect() } #[derive(Debug, PartialEq, Clone)] struct RollWithModifier { - diceroll: Option, - modifiers: Vec + diceroll: Option, + modifiers: Vec, } fn group_modifiers_to_dicerolls(segments: &[Segment]) -> Vec { - let mut results = Vec::new(); + let mut results = Vec::new(); - let mut current_diceroll : Option = None; - let mut current_modifiers = 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.len() > 0 { + 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.clone(), - modifiers: current_modifiers.clone(), + diceroll: current_diceroll, + 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.len() > 0 { - let with_modifier = RollWithModifier { - diceroll: current_diceroll.clone(), - modifiers: current_modifiers.clone(), - }; - - current_modifiers.clear(); - - results.push(with_modifier); - } - - results + results } #[derive(Debug, PartialEq, Clone)] struct Roll { - operator: char, - results: Vec, - total: i32, + 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), + #[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); - }; +fn roll_dice_segments( + rwms: &[RollWithModifier], + mut rng: R, +) -> Result, RollError> { + rwms.iter() + .map(|rwm| { + let mut results = Vec::new(); + let mut total = 0; - if let Some(Segment::DiceRoll{count, size, filter, ..}) = &rwm.diceroll { - results = (0..*count).map(|_| { - rng.gen_range(1, size + 1) - }).collect(); + // 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); + }; - 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(); - } + if let Some(Segment::DiceRoll { + count, + size, + filter, + .. + }) = &rwm.diceroll + { + results = (0..*count).map(|_| rng.gen_range(1, size + 1)).collect(); - 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)); - }, - } - } - } + 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); - Ok(Roll{operator, results, total}) + 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, }) - .collect::,_>>() -} - -fn roll_dice(s: &str, mut rng: R) -> Result<(i32, Vec<(Roll, Vec)>)> { - 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((total, rolls_with_modifiers)) } diff --git a/src/lib_test.rs b/src/lib_test.rs index dbed58c..ad1c21c 100644 --- a/src/lib_test.rs +++ b/src/lib_test.rs @@ -1,145 +1,249 @@ -use rand::{rngs::StdRng, SeedableRng}; use super::*; +use rand::{rngs::StdRng, SeedableRng}; #[test] fn test_parse_dice_segments() { - let mut results = parse_dice_segments("2d6").expect("Failed to unpack results"); - - assert_eq!(results, vec![ Segment::DiceRoll{ op: '+', count: 2, size: 6, filter: None } ]); - - results = parse_dice_segments("2d6 + 1d20").expect("Failed to unpack results"); - - assert_eq!(results, vec![ - Segment::DiceRoll{ op: '+', count: 2, size: 6, filter: None }, - Segment::DiceRoll{ op: '+', count: 1, size: 20, filter: None }, - ]); - - results = parse_dice_segments("4d6d1").expect("Failed to unpack results"); - - assert_eq!(results, vec![ - Segment::DiceRoll{ op: '+', count: 4, size: 6, filter: Some(DiceFilter::DropLowest(1)) }, - ]); - - results = parse_dice_segments("3d8+8").expect("Failed to unpack results"); + let mut results = parse_dice_segments("2d6").expect("Failed to unpack results"); - assert_eq!(results, vec![ - Segment::DiceRoll{ op: '+', count: 3, size: 8, filter: None }, - Segment::Modifier{ op: '+', amount: 8 }, - ]); - - results = parse_dice_segments("3d8kl3 - 2d1dh1 / 2").expect("Failed to unpack results"); - - assert_eq!(results, vec![ - Segment::DiceRoll{ op: '+', count: 3, size: 8, filter: Some(DiceFilter::KeepLowest(3)) }, - Segment::DiceRoll{ op: '-', count: 2, size: 1, filter: Some(DiceFilter::DropHighest(1)) }, - Segment::Modifier{ op: '/', amount: 2 }, - ]); + assert_eq!( + results, + vec![Segment::DiceRoll { + op: '+', + count: 2, + size: 6, + filter: None + }] + ); + + results = parse_dice_segments("2d6 + 1d20").expect("Failed to unpack results"); + + assert_eq!( + results, + vec![ + Segment::DiceRoll { + op: '+', + count: 2, + size: 6, + filter: None + }, + Segment::DiceRoll { + op: '+', + count: 1, + size: 20, + filter: None + }, + ] + ); + + results = parse_dice_segments("4d6d1").expect("Failed to unpack results"); + + assert_eq!( + results, + vec![Segment::DiceRoll { + op: '+', + count: 4, + size: 6, + filter: Some(DiceFilter::DropLowest(1)) + },] + ); + + results = parse_dice_segments("3d8+8").expect("Failed to unpack results"); + + assert_eq!( + results, + vec![ + Segment::DiceRoll { + op: '+', + count: 3, + size: 8, + filter: None + }, + Segment::Modifier { op: '+', amount: 8 }, + ] + ); + + results = parse_dice_segments("3d8kl3 - 2d1dh1 / 2").expect("Failed to unpack results"); + + assert_eq!( + results, + vec![ + Segment::DiceRoll { + op: '+', + count: 3, + size: 8, + filter: Some(DiceFilter::KeepLowest(3)) + }, + Segment::DiceRoll { + op: '-', + count: 2, + size: 1, + filter: Some(DiceFilter::DropHighest(1)) + }, + Segment::Modifier { op: '/', amount: 2 }, + ] + ); } #[test] fn test_group_modifiers_to_dicerolls() { - let segments = parse_dice_segments("2d20 / 2 + 2 + 4d6 * 2 * 2").expect("Failed to unpack results"); + let segments = + parse_dice_segments("2d20 / 2 + 2 + 4d6 * 2 * 2").expect("Failed to unpack results"); - let results = group_modifiers_to_dicerolls(&segments); + let results = group_modifiers_to_dicerolls(&segments); - assert_eq!(results, vec![ - RollWithModifier{ - diceroll: Some(Segment::DiceRoll{ op: '+', count: 2, size: 20, filter: None }), - modifiers: vec![ - Segment::Modifier{ op: '/', amount: 2 }, - Segment::Modifier{ op: '+', amount: 2 }, - ] - }, - RollWithModifier{ - diceroll: Some(Segment::DiceRoll{ op: '+', count: 4, size: 6, filter: None }), - modifiers: vec![ - Segment::Modifier{ op: '*', amount: 2 }, - Segment::Modifier{ op: '*', amount: 2 }, - ] - }, - ]); + assert_eq!( + results, + vec![ + RollWithModifier { + diceroll: Some(Segment::DiceRoll { + op: '+', + count: 2, + size: 20, + filter: None + }), + modifiers: vec![ + Segment::Modifier { op: '/', amount: 2 }, + Segment::Modifier { op: '+', amount: 2 }, + ] + }, + RollWithModifier { + diceroll: Some(Segment::DiceRoll { + op: '+', + count: 4, + size: 6, + filter: None + }), + modifiers: vec![ + Segment::Modifier { op: '*', amount: 2 }, + Segment::Modifier { op: '*', amount: 2 }, + ] + }, + ] + ); } #[test] fn test_group_modifiers_to_dicerolls_no_diceroll() { - let segments = parse_dice_segments("* 2 * 2").expect("Failed to unpack results"); + let segments = parse_dice_segments("* 2 * 2").expect("Failed to unpack results"); - let results = group_modifiers_to_dicerolls(&segments); + let results = group_modifiers_to_dicerolls(&segments); - assert_eq!(results, vec![ - RollWithModifier{ - diceroll: None, - modifiers: vec![ - Segment::Modifier{ op: '*', amount: 2 }, - Segment::Modifier{ op: '*', amount: 2 }, - ] - }, - ]); + assert_eq!( + results, + vec![RollWithModifier { + diceroll: None, + modifiers: vec![ + Segment::Modifier { op: '*', amount: 2 }, + Segment::Modifier { op: '*', amount: 2 }, + ] + },] + ); } #[test] fn test_roll_dice_segments() { - let mut rng = StdRng::seed_from_u64(2); + let mut rng = StdRng::seed_from_u64(2); - let segments = parse_dice_segments("4d6").expect("Failed to unpack results"); - let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); - let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); + let segments = parse_dice_segments("4d6").expect("Failed to unpack results"); + let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); + let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); - assert_eq!(results, Ok(vec![Roll{operator: '+', results: vec![4, 5, 5, 1], total: 15}])); + assert_eq!( + results, + Ok(vec![Roll { + operator: '+', + results: vec![4, 5, 5, 1], + total: 15 + }]) + ); - let segments = parse_dice_segments("4d6d1 + 2").expect("Failed to unpack results"); - let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); - let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); + let segments = parse_dice_segments("4d6d1 + 2").expect("Failed to unpack results"); + let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); + let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); - assert_eq!(results, Ok(vec![Roll{operator: '+', results: vec![1, 1, 5], total: 9}])); + assert_eq!( + results, + Ok(vec![Roll { + operator: '+', + results: vec![1, 1, 5], + total: 9 + }]) + ); - let segments = parse_dice_segments("+ 2 + 2d12kh1 / 2").expect("Failed to unpack results"); - let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); - let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); + let segments = parse_dice_segments("+ 2 + 2d12kh1 / 2").expect("Failed to unpack results"); + let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); + let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); - assert_eq!(results, Ok(vec![ - Roll{operator: '+', results: vec![], total: 2}, - Roll{operator: '+', results: vec![12], total: 6}, - ])); + assert_eq!( + results, + Ok(vec![ + Roll { + operator: '+', + results: vec![], + total: 2 + }, + Roll { + operator: '+', + results: vec![12], + total: 6 + }, + ]) + ); - let segments = parse_dice_segments("1d6 * 1d6 + 2 / 2").expect("Failed to unpack results"); - let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); - let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); + let segments = parse_dice_segments("1d6 * 1d6 + 2 / 2").expect("Failed to unpack results"); + let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); + let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); - assert_eq!(results, Ok(vec![ - Roll{operator: '+', results: vec![1], total: 1}, - Roll{operator: '*', results: vec![6], total: 4}, - ])); + assert_eq!( + results, + Ok(vec![ + Roll { + operator: '+', + results: vec![1], + total: 1 + }, + Roll { + operator: '*', + results: vec![6], + total: 4 + }, + ]) + ); } #[test] fn test_roll_dice() { + let input = "4d6d2 + 2 - 1d4"; - let input = "4d6d2 + 2 - 1d4"; + let mut rng = StdRng::seed_from_u64(2); - let mut rng = StdRng::seed_from_u64(2); + let result = roll_dice(input, &mut rng); - let result = roll_dice(input, &mut rng); - - let expected = (8i32, vec![ - (Roll { - operator: '+', - results: vec![5, 5], - total: 12 - }, - vec![Segment::Modifier { op: '+', amount: 2 }]), - - (Roll { - operator: '-', - results: vec![4], - total: 4 - }, - vec![])]); - - if let Ok(result) = result { - assert_eq!(result, expected); - } else { - panic!("Expected Ok, was: {:?}", result); - } + let expected = RollSet { + total: 8i32, + rolls: vec![ + ( + Roll { + operator: '+', + results: vec![5, 5], + total: 12, + }, + vec![Segment::Modifier { op: '+', amount: 2 }], + ), + ( + Roll { + operator: '-', + results: vec![4], + total: 4, + }, + vec![], + ), + ], + }; + if let Ok(result) = result { + assert_eq!(result, expected); + } else { + panic!("Expected Ok, was: {:?}", result); + } }