Code cleanup

This commit is contained in:
Ådne E. Moldesæter 2020-08-27 20:49:06 +02:00
parent 6a1f5b6035
commit 56a37a738f
2 changed files with 433 additions and 270 deletions

View File

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

View File

@ -1,145 +1,249 @@
use rand::{rngs::StdRng, SeedableRng};
use super::*; use super::*;
use rand::{rngs::StdRng, SeedableRng};
#[test] #[test]
fn test_parse_dice_segments() { fn test_parse_dice_segments() {
let mut results = parse_dice_segments("2d6").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: 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![ assert_eq!(
Segment::DiceRoll{ op: '+', count: 3, size: 8, filter: None }, results,
Segment::Modifier{ op: '+', amount: 8 }, vec![Segment::DiceRoll {
]); op: '+',
count: 2,
results = parse_dice_segments("3d8kl3 - 2d1dh1 / 2").expect("Failed to unpack results"); size: 6,
filter: None
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 }, 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] #[test]
fn test_group_modifiers_to_dicerolls() { 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![ assert_eq!(
RollWithModifier{ results,
diceroll: Some(Segment::DiceRoll{ op: '+', count: 2, size: 20, filter: None }), vec![
modifiers: vec![ RollWithModifier {
Segment::Modifier{ op: '/', amount: 2 }, diceroll: Some(Segment::DiceRoll {
Segment::Modifier{ op: '+', amount: 2 }, op: '+',
] count: 2,
}, size: 20,
RollWithModifier{ filter: None
diceroll: Some(Segment::DiceRoll{ op: '+', count: 4, size: 6, filter: None }), }),
modifiers: vec![ modifiers: vec![
Segment::Modifier{ op: '*', amount: 2 }, Segment::Modifier { op: '/', amount: 2 },
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] #[test]
fn test_group_modifiers_to_dicerolls_no_diceroll() { 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![ assert_eq!(
RollWithModifier{ results,
diceroll: None, vec![RollWithModifier {
modifiers: vec![ diceroll: None,
Segment::Modifier{ op: '*', amount: 2 }, modifiers: vec![
Segment::Modifier{ op: '*', amount: 2 }, Segment::Modifier { op: '*', amount: 2 },
] Segment::Modifier { op: '*', amount: 2 },
}, ]
]); },]
);
} }
#[test] #[test]
fn test_roll_dice_segments() { 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 segments = parse_dice_segments("4d6").expect("Failed to unpack results");
let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments);
let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); 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 segments = parse_dice_segments("4d6d1 + 2").expect("Failed to unpack results");
let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments);
let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); 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 segments = parse_dice_segments("+ 2 + 2d12kh1 / 2").expect("Failed to unpack results");
let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments);
let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); let results = roll_dice_segments(&rolls_with_modifiers, &mut rng);
assert_eq!(results, Ok(vec![ assert_eq!(
Roll{operator: '+', results: vec![], total: 2}, results,
Roll{operator: '+', results: vec![12], total: 6}, 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 segments = parse_dice_segments("1d6 * 1d6 + 2 / 2").expect("Failed to unpack results");
let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments); let rolls_with_modifiers = group_modifiers_to_dicerolls(&segments);
let results = roll_dice_segments(&rolls_with_modifiers, &mut rng); let results = roll_dice_segments(&rolls_with_modifiers, &mut rng);
assert_eq!(results, Ok(vec![ assert_eq!(
Roll{operator: '+', results: vec![1], total: 1}, results,
Roll{operator: '*', results: vec![6], total: 4}, Ok(vec![
])); Roll {
operator: '+',
results: vec![1],
total: 1
},
Roll {
operator: '*',
results: vec![6],
total: 4
},
])
);
} }
#[test] #[test]
fn test_roll_dice() { 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 = RollSet {
total: 8i32,
let expected = (8i32, vec![ rolls: vec![
(Roll { (
operator: '+', Roll {
results: vec![5, 5], operator: '+',
total: 12 results: vec![5, 5],
}, total: 12,
vec![Segment::Modifier { op: '+', amount: 2 }]), },
vec![Segment::Modifier { op: '+', amount: 2 }],
(Roll { ),
operator: '-', (
results: vec![4], Roll {
total: 4 operator: '-',
}, results: vec![4],
vec![])]); total: 4,
},
if let Ok(result) = result { vec![],
assert_eq!(result, expected); ),
} else { ],
panic!("Expected Ok, was: {:?}", result); };
}
if let Ok(result) = result {
assert_eq!(result, expected);
} else {
panic!("Expected Ok, was: {:?}", result);
}
} }