use std::collections::BTreeSet;
use crate::Stat;
use crate::error::{DeepError, Result};
use crate::model::req::{Atom, Clause, PrereqGroup, Reducability, Requirement};
use log::warn;
use winnow::ascii::{Caseless, alpha1, digit1, multispace0};
use winnow::combinator::{alt, delimited, not, opt, preceded, repeat, separated};
use winnow::prelude::*;
use winnow::token::one_of;
pub(crate) fn parse_req(input: &str) -> Result<Requirement> {
let input = input.trim();
requirement
.parse(input)
.map_err(|e| DeepError::Req(e.to_string()))
}
pub(crate) fn requirement(input: &mut &str) -> ModalResult<Requirement> {
let _ = multispace0.parse_next(input)?;
let prefix = opt(alt((prereq_prefix, name_prefix))).parse_next(input)?;
let mut req = bare_requirement.parse_next(input)?;
if let Some((prereqs, name)) = prefix {
req.prereqs = prereqs.into_iter().collect();
req.name = name;
}
Ok(req)
}
fn prereq_prefix(input: &mut &str) -> ModalResult<(Vec<PrereqGroup>, Option<String>)> {
let prereqs: Vec<PrereqGroup> =
separated(1.., prereq_group, (multispace0, ',', multispace0)).parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = "=>".parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let name = opt((identifier, multispace0, ":=", multispace0)).parse_next(input)?;
Ok((prereqs, name.map(|(n, _, _, _)| n)))
}
fn name_prefix(input: &mut &str) -> ModalResult<(Vec<PrereqGroup>, Option<String>)> {
let name = identifier.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = ":=".parse_next(input)?;
let _ = multispace0.parse_next(input)?;
Ok((Vec::new(), Some(name)))
}
pub(crate) fn parse_prereq_group(input: &str) -> Result<PrereqGroup> {
let input = input.trim();
prereq_group_full
.parse(input)
.map_err(|e| DeepError::Req(e.to_string()))
}
fn prereq_group_full(input: &mut &str) -> ModalResult<PrereqGroup> {
let _ = multispace0.parse_next(input)?;
let group = prereq_group.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
Ok(group)
}
pub(crate) fn prereq_group(input: &mut &str) -> ModalResult<PrereqGroup> {
let alts: Vec<String> =
separated(1.., identifier, (multispace0, '|', multispace0)).parse_next(input)?;
Ok(PrereqGroup::any(alts))
}
pub(crate) fn identifier(input: &mut &str) -> ModalResult<String> {
let first = segment.parse_next(input)?;
let rest: Vec<String> = repeat(0.., ns_segment).parse_next(input)?;
if rest.is_empty() {
Ok(first)
} else {
let mut out = first;
for seg in rest {
out.push(':');
out.push_str(&seg);
}
Ok(out)
}
}
fn segment(input: &mut &str) -> ModalResult<String> {
let id: String =
repeat(1.., one_of(('A'..='Z', 'a'..='z', '0'..='9', '_'))).parse_next(input)?;
Ok(id)
}
fn ns_segment(input: &mut &str) -> ModalResult<String> {
preceded((':', not('=')), segment).parse_next(input)
}
fn bare_requirement(input: &mut &str) -> ModalResult<Requirement> {
let clauses = alt((
('(', multispace0, ')').map(|_| Vec::new()),
separated(1.., clause, (multispace0, ',', multispace0)),
))
.parse_next(input)?
.into_iter()
.collect::<BTreeSet<Clause>>();
Ok(Requirement {
name: None,
prereqs: BTreeSet::new(),
clauses,
})
}
fn clause(input: &mut &str) -> ModalResult<Clause> {
let _ = multispace0.parse_next(input)?;
let result = alt((
delimited(('(', multispace0), clause_inner, (multispace0, ')')),
clause_inner,
))
.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
Ok(result)
}
fn clause_inner(input: &mut &str) -> ModalResult<Clause> {
let first = atom.parse_next(input)?;
let rest: Vec<ParsedAtom> = repeat(
0..,
preceded((multispace0, Caseless("OR"), multispace0), atom),
)
.parse_next(input)?;
if rest.is_empty() {
let atom = first.into_atom(false);
Ok(Clause::and().atom(atom))
} else {
let mut clause = Clause::or();
clause = clause.atom(first.into_atom(true));
for parsed in rest {
clause = clause.atom(parsed.into_atom(true));
}
Ok(clause)
}
}
struct ParsedAtom {
stats: Vec<Stat>,
value: i64,
reducability: Option<Reducability>,
}
impl ParsedAtom {
fn into_atom(self, is_or: bool) -> Atom {
let reducability = self.reducability.unwrap_or({
if is_or {
Reducability::Reducible
} else if self.stats.len() > 1 {
Reducability::Reducible
} else {
Reducability::Strict
}
});
if reducability == Reducability::Strict && self.stats.len() > 1 {
warn!(
"You have specified a strict SUM requirement, please note that \
strict SUM requirements' semantics are not properly defined currently. \
You probably don't need it anyways."
);
}
let mut atom = Atom::new(reducability).value(self.value);
for stat in self.stats {
atom.add_stat(stat);
}
atom
}
}
fn atom(input: &mut &str) -> ModalResult<ParsedAtom> {
let _ = multispace0.parse_next(input)?;
let result = alt((
sum_expr_parens,
sum_expr_no_parens,
single_expr_eq, single_expr_prefix, ))
.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
Ok(result)
}
fn sum_expr_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
let _ = '('.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let stats: Vec<Stat> =
separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = '='.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let value = number.parse_next(input)?;
let reducability = opt(reducability_marker).parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = ')'.parse_next(input)?;
Ok(ParsedAtom {
stats,
value,
reducability,
})
}
fn sum_expr_no_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
let first = stat.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = '+'.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let rest: Vec<Stat> =
separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = '='.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let value = number.parse_next(input)?;
let reducability = opt(reducability_marker).parse_next(input)?;
let mut stats = vec![first];
stats.extend(rest);
Ok(ParsedAtom {
stats,
value,
reducability,
})
}
fn single_expr_eq(input: &mut &str) -> ModalResult<ParsedAtom> {
let s = stat.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let _ = '='.parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let value = number.parse_next(input)?;
let reducability = opt(reducability_marker).parse_next(input)?;
Ok(ParsedAtom {
stats: vec![s],
value,
reducability,
})
}
fn single_expr_prefix(input: &mut &str) -> ModalResult<ParsedAtom> {
let value = number.parse_next(input)?;
let reducability = opt(reducability_marker).parse_next(input)?;
let _ = multispace0.parse_next(input)?;
let s = stat.parse_next(input)?;
Ok(ParsedAtom {
stats: vec![s],
value,
reducability,
})
}
fn reducability_marker(input: &mut &str) -> ModalResult<Reducability> {
let c = one_of(['S', 'R', 's', 'r']).parse_next(input)?;
Ok(match c {
'S' | 's' => Reducability::Strict,
'R' | 'r' => Reducability::Reducible,
_ => unreachable!(),
})
}
fn number(input: &mut &str) -> ModalResult<i64> {
digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
}
pub(crate) fn stat(input: &mut &str) -> ModalResult<Stat> {
alpha1
.verify_map(|s: &str| {
let upper = s.to_uppercase();
Stat::from_short_name(&upper)
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use crate::model::req::ClauseType;
use super::*;
#[test]
fn reinforced_armor() {
let req = parse_req("90 FTD").unwrap();
assert_eq!(req.clauses.len(), 1);
let clause = req.clauses.iter().next().unwrap();
assert_eq!(clause.clause_type, ClauseType::And);
assert_eq!(clause.atoms.len(), 1);
let atom = clause.atoms.iter().next().unwrap();
assert!(atom.stats.contains(&Stat::Fortitude));
assert_eq!(atom.value, 90);
assert_eq!(atom.reducability, Reducability::Strict);
}
#[test]
fn bladeharper_variants() {
let variants = [
"25 STR OR 25 AGL, 75 MED OR (LHT + MED + HVY = 90)",
"(25 STR OR 25 AGL), (75 MED OR (LHT + MED + HVY = 90))",
"STR = 25 OR AGL = 25, 75 MED OR (LHT + MED + HVY = 90)",
"(STR = 25 OR AGL = 25), (75 MED OR (LHT + MED + HVY = 90))",
"(STR = 25 OR AGL = 25),(75 MED OR (LHT + MED + HVY = 90))",
"STR=25 OR AGL= 25,med=75 OR (lht + MED +hvy = 90)",
];
let parsed: Vec<Requirement> = variants
.iter()
.map(|s| parse_req(s).unwrap_or_else(|_| panic!("Failed to parse: {s}")))
.collect();
for (i, req) in parsed.iter().enumerate() {
assert_eq!(req.clauses.len(), 2, "variant {i} should have 2 clauses");
}
for i in 1..parsed.len() {
assert_eq!(parsed[0], parsed[i], "variant 0 should equal variant {i}");
}
let req = &parsed[0];
let mut clauses = req.clauses.iter();
let first_clause = clauses.next().unwrap();
assert_eq!(first_clause.clause_type, ClauseType::Or);
assert_eq!(first_clause.atoms.len(), 2);
let second_clause = clauses.next().unwrap();
assert_eq!(second_clause.clause_type, ClauseType::Or);
assert_eq!(second_clause.atoms.len(), 2);
}
#[test]
fn bunch_of_random_stuff() {
parse_req("25R STR, LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
parse_req("(25R STR), LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
parse_req("silentheart := str=25r,lht+med+hvy=75,25CHA OR agl=25r").unwrap();
parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").unwrap();
assert!(parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").is_ok());
assert!(parse_req("35cha OR 35wll OR 35int").is_ok());
assert!(parse_req("35 cha OR 35 wll OR 35 int").is_ok());
assert!(parse_req("()").unwrap().is_empty());
assert!(parse_req("(35 cha").is_err());
assert!(parse_req("35 SBF").is_err());
assert!(parse_req("35CHAOR35WLL").is_err());
}
#[test]
fn explicit_reducability() {
let req = parse_req("25S STR").unwrap();
let atom = req
.clauses
.iter()
.next()
.unwrap()
.atoms
.iter()
.next()
.unwrap();
assert_eq!(atom.reducability, Reducability::Strict);
let req = parse_req("25R STR").unwrap();
let atom = req
.clauses
.iter()
.next()
.unwrap()
.atoms
.iter()
.next()
.unwrap();
assert_eq!(atom.reducability, Reducability::Reducible);
let req = parse_req("25S STR OR 25R AGL").unwrap();
assert_eq!(
req.clauses.iter().next().unwrap().clause_type,
ClauseType::Or
);
}
#[test]
fn prereq_prefix_parsing() {
let req = parse_req("base, armor => reinforced := 90 FTD").unwrap();
assert_eq!(
req.prereqs,
BTreeSet::from([PrereqGroup::single("base"), PrereqGroup::single("armor")])
);
assert_eq!(req.name, Some("reinforced".to_string()));
assert_eq!(req.clauses.len(), 1);
let req = parse_req("base => 90 FTD").unwrap();
assert_eq!(req.prereqs, BTreeSet::from([PrereqGroup::single("base")]));
assert!(req.name.is_none());
let req = parse_req("base, armor => 50 INT, 25 STR OR 25 AGL").unwrap();
assert_eq!(
req.prereqs,
BTreeSet::from([PrereqGroup::single("base"), PrereqGroup::single("armor")])
);
assert_eq!(req.clauses.len(), 2);
}
#[test]
fn qualified_identifiers() {
let req = parse_req("origin:castaway => talent:voidwalker_contract := 90 FTD").unwrap();
assert_eq!(
req.prereqs,
BTreeSet::from([PrereqGroup::single("origin:castaway")])
);
assert_eq!(req.name, Some("talent:voidwalker_contract".to_string()));
assert_eq!(req.clauses.len(), 1);
let req = parse_req("mantra:arc_beam := ()").unwrap();
assert_eq!(req.name, Some("mantra:arc_beam".to_string()));
assert!(req.is_empty());
let req = parse_req("aspect:khan, origin:voidwalker => 25 STR").unwrap();
assert_eq!(
req.prereqs,
BTreeSet::from([
PrereqGroup::single("aspect:khan"),
PrereqGroup::single("origin:voidwalker")
])
);
}
#[test]
fn assign_disambiguation() {
let req = parse_req("foo:= 90 FTD").unwrap();
assert_eq!(req.name, Some("foo".to_string()));
let req = parse_req("talent:foo:= 90 FTD").unwrap();
assert_eq!(req.name, Some("talent:foo".to_string()));
let req = parse_req("origin:castaway=>talent:foo:=90 FTD").unwrap();
assert_eq!(
req.prereqs,
BTreeSet::from([PrereqGroup::single("origin:castaway")])
);
assert_eq!(req.name, Some("talent:foo".to_string()));
}
#[test]
fn or_group_round_trip() {
let req =
parse_req("origin:castaway | origin:lone_warrior => talent:stranded := 25 STR").unwrap();
let group = req.prereqs.iter().next().unwrap();
assert!(!group.is_single());
assert_eq!(req.prereqs.len(), 1);
let rendered = req.to_string();
let reparsed = parse_req(&rendered).unwrap();
assert_eq!(req, reparsed);
let req = parse_req("a:x | a:y, b:z => 40 INT").unwrap();
assert_eq!(req.prereqs.len(), 2);
let reparsed = parse_req(&req.to_string()).unwrap();
assert_eq!(req, reparsed);
}
#[test]
fn casing_and_compactness() {
let req1 = parse_req("25 str or 25 agl").unwrap();
let req2 = parse_req("25 STR or 25 AGL").unwrap();
assert_eq!(req1, req2);
assert!(parse_req("25 Str OR 25 AgL").is_ok());
assert!(parse_req("lht+hvy=90").is_ok());
assert!(parse_req("lht+med+hvy=90").is_ok());
assert!(parse_req("25 STR OR AGL=25,75S MED OR (LHT+MED+HVY=90)").is_ok());
let compact = parse_req("str=25 OR agl=25").unwrap();
let spaced = parse_req("STR = 25 OR AGL = 25").unwrap();
assert_eq!(compact, spaced);
}
}