Skip to main content

deepwoken_reqparse/parse/
req.rs

1use crate::model::req::{Atom, Clause, Reducability, Requirement};
2use crate::error::{ReqparseError, Result };
3use crate::Stat;
4use log::warn;
5use winnow::ascii::{alpha1, digit1, multispace0, Caseless};
6use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated};
7use winnow::prelude::*;
8use winnow::token::one_of;
9
10/// Parse a string into a Requirement
11///
12/// If reducibility is unspecified:
13/// - Unspecified atoms in OR clauses are reducible
14/// - Unspecified atoms in AND clauses are strict iff they have a singular stat.
15/// - Unspecified atoms in AND clauses are reducible if they are SUM types
16///
17/// Examples:
18/// - "90 FTD" -> AND clause with strict 90 Fortitude
19/// - "FTD = 90" -> Same thing but diff syntax, "ftd=90", "90ftd" also are valid
20/// - "25 STR OR 25 AGL" -> OR clause with reducible atoms
21/// - "25S STR OR 25 AGL" -> OR clause with asymmetric reducability
22/// - "(LHT + MED + HVY = 90)" -> AND clause with sum atom (reducible by default)
23/// - "(LHT + MED + HVY = 90S)" -> Any stat that make up the sum cannot be reduced
24/// - "25S STR" -> strict atom
25/// - "25R STR" -> reducible atom
26/// - "reinforced = 90 FTD" -> named requirement (assignment syntax)
27pub fn parse_req(input: &str) -> Result<Requirement> {
28    let input = input.trim();
29    requirement
30        .parse(&input)
31        .map_err(|e| ReqparseError::Req(e.to_string()))
32}
33
34// requirement = (identifier ':=')? bare_requirement
35// allows for assignment syntax like "reinforced := 90 FTD"
36pub(crate) fn requirement(input: &mut &str) -> ModalResult<Requirement> {
37    let _ = multispace0.parse_next(input)?;
38
39    // try to parse the assingment prefix and backtrack if it fails
40    let name = opt((identifier, multispace0, ":=", multispace0)).parse_next(input)?;
41
42    // TODO: need to make this uppercase
43    let mut req = bare_requirement.parse_next(input)?;
44
45    if let Some((name, _, _, _)) = name {
46        req.name = Some(name);
47    }
48
49    Ok(req)
50}
51
52// identifier = alpha (alpha | digit | '_')*
53pub(crate) fn identifier(input: &mut &str) -> ModalResult<String> {
54    let first: char = one_of(('A'..='Z', 'a'..='z', '_')).parse_next(input)?;
55    let rest: String =
56        repeat(0.., one_of(('A'..='Z', 'a'..='z', '0'..='9', '_'))).parse_next(input)?;
57    Ok(format!("{}{}", first, rest))
58}
59
60// requirement = '(' ')' | clause (',' clause)*
61fn bare_requirement(input: &mut &str) -> ModalResult<Requirement> {
62    let clauses: Vec<Clause> = alt((
63        // if () then its an empty req
64        ('(', multispace0, ')').map(|_| Vec::new()),
65        // Normal: 1+ clauses (clauses can have their own parens)
66        separated(1.., clause, (multispace0, ',', multispace0)),
67    ))
68    .parse_next(input)?;
69
70    Ok(Requirement {
71        name: None,
72        prereqs: Vec::new(),
73        clauses,
74    })
75}
76
77// clause = '(' clause_inner ')' | clause_inner
78// clause_inner = atom ('OR' atom)*
79// TODO! this is lacking an explicit 'AND', though you
80// can just implicitly create new ANDs by making a new single atom clause!
81fn clause(input: &mut &str) -> ModalResult<Clause> {
82    let _ = multispace0.parse_next(input)?;
83
84    // try (clause) first
85    let result = alt((
86        delimited(('(', multispace0), clause_inner, (multispace0, ')')),
87        clause_inner,
88    ))
89    .parse_next(input)?;
90
91    let _ = multispace0.parse_next(input)?;
92
93    Ok(result)
94}
95
96fn clause_inner(input: &mut &str) -> ModalResult<Clause> {
97    let first = atom.parse_next(input)?;
98    let rest: Vec<ParsedAtom> = repeat(
99        0..,
100        preceded((multispace0, Caseless("OR"), multispace0), atom),
101    )
102    .parse_next(input)?;
103
104    if rest.is_empty() {
105        // single atom -> AND clause
106        let atom = first.into_atom(false);
107        Ok(Clause::and().atom(atom))
108    } else {
109        // multiple atoms -> OR clause (no AND support YET..)
110        let mut clause = Clause::or();
111        clause = clause.atom(first.into_atom(true));
112        for parsed in rest {
113            clause = clause.atom(parsed.into_atom(true));
114        }
115
116        Ok(clause)
117    }
118}
119
120// intermediate atom structure
121struct ParsedAtom {
122    stats: Vec<Stat>,
123    value: i64,
124    reducability: Option<Reducability>,
125}
126
127impl ParsedAtom {
128    fn into_atom(self, is_or: bool) -> Atom {
129        let reducability = self.reducability.unwrap_or_else(|| {
130            if is_or {
131                // OR clause atoms default to reducible
132                Reducability::Reducible
133            } else if self.stats.len() > 1 {
134                // multi-stat (sum) AND atoms default to reducible
135                Reducability::Reducible
136            } else {
137                // single stat AND atoms default to strict
138                Reducability::Strict
139            }
140        });
141
142        if reducability == Reducability::Strict && self.stats.len() > 1 {
143            warn!(
144                "You have specified a strict SUM requirement, please note that \
145                there is a BUG regarding this in the solver code that makes that atom \
146                restrict SOM usage incorrectly. I am \
147                too lazy to fix atm bc nothing will ever need it. \
148                You probably don't need it anyways."
149            )
150        }
151
152        let mut atom = Atom::new(reducability).value(self.value);
153
154        for stat in self.stats {
155            atom.add_stat(stat);
156        }
157
158        atom
159    }
160}
161
162// atom = sum_expr | single_expr
163fn atom(input: &mut &str) -> ModalResult<ParsedAtom> {
164    let _ = multispace0.parse_next(input)?;
165
166    let result = alt((
167        sum_expr_parens,
168        sum_expr_no_parens,
169        single_expr_eq,     // stat '=' value reducability?
170        single_expr_prefix, // value reducability? stat
171    ))
172    .parse_next(input)?;
173
174    let _ = multispace0.parse_next(input)?;
175
176    Ok(result)
177}
178
179// sum_expr_parens = '(' stat ('+' stat)* '=' value reducability? ')'
180fn sum_expr_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
181    let _ = '('.parse_next(input)?;
182    let _ = multispace0.parse_next(input)?;
183
184    let stats: Vec<Stat> =
185        separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
186
187    let _ = multispace0.parse_next(input)?;
188    let _ = '='.parse_next(input)?;
189    let _ = multispace0.parse_next(input)?;
190
191    let value = number.parse_next(input)?;
192    let reducability = opt(reducability_marker).parse_next(input)?;
193
194    let _ = multispace0.parse_next(input)?;
195    let _ = ')'.parse_next(input)?;
196
197    Ok(ParsedAtom {
198        stats,
199        value,
200        reducability,
201    })
202}
203
204// sum_expr_no_parens = stat '+' stat ('+' stat)* '=' value reducability?
205// needs 2 or more stats
206fn sum_expr_no_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
207    let first = stat.parse_next(input)?;
208    let _ = multispace0.parse_next(input)?;
209    let _ = '+'.parse_next(input)?;
210    let _ = multispace0.parse_next(input)?;
211
212    let rest: Vec<Stat> =
213        separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
214
215    let _ = multispace0.parse_next(input)?;
216    let _ = '='.parse_next(input)?;
217    let _ = multispace0.parse_next(input)?;
218
219    let value = number.parse_next(input)?;
220    let reducability = opt(reducability_marker).parse_next(input)?;
221
222    let mut stats = vec![first];
223    stats.extend(rest);
224
225    Ok(ParsedAtom {
226        stats,
227        value,
228        reducability,
229    })
230}
231
232// single_expr_eq = stat '=' value reducability?
233fn single_expr_eq(input: &mut &str) -> ModalResult<ParsedAtom> {
234    let s = stat.parse_next(input)?;
235    let _ = multispace0.parse_next(input)?;
236    let _ = '='.parse_next(input)?;
237    let _ = multispace0.parse_next(input)?;
238    let value = number.parse_next(input)?;
239    let reducability = opt(reducability_marker).parse_next(input)?;
240
241    Ok(ParsedAtom {
242        stats: vec![s],
243        value,
244        reducability,
245    })
246}
247
248// single_expr_prefix = value reducability? stat
249fn single_expr_prefix(input: &mut &str) -> ModalResult<ParsedAtom> {
250    let value = number.parse_next(input)?;
251    let reducability = opt(reducability_marker).parse_next(input)?;
252    let _ = multispace0.parse_next(input)?;
253    let s = stat.parse_next(input)?;
254
255    Ok(ParsedAtom {
256        stats: vec![s],
257        value,
258        reducability,
259    })
260}
261
262fn reducability_marker(input: &mut &str) -> ModalResult<Reducability> {
263    let c = one_of(['S', 'R', 's', 'r']).parse_next(input)?;
264    Ok(match c {
265        'S' | 's' => Reducability::Strict,
266        'R' | 'r' => Reducability::Reducible,
267        _ => unreachable!(),
268    })
269}
270
271fn number(input: &mut &str) -> ModalResult<i64> {
272    digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
273}
274
275fn stat(input: &mut &str) -> ModalResult<Stat> {
276    alpha1
277        .verify_map(|s: &str| {
278            let upper = s.to_uppercase();
279            Stat::from_short_name(&upper)
280        })
281        .parse_next(input)
282}
283
284#[cfg(test)]
285mod tests {
286    use crate::model::req::ClauseType;
287
288    use super::*;
289
290    #[test]
291    fn reinforced_armor() {
292        let req = parse_req("90 FTD").unwrap();
293        assert_eq!(req.clauses.len(), 1);
294
295        let clause = &req.clauses[0];
296        assert_eq!(clause.clause_type, ClauseType::And);
297        assert_eq!(clause.atoms.len(), 1);
298
299        let atom = clause.atoms.iter().next().unwrap();
300        assert!(atom.stats.contains(&Stat::Fortitude));
301        assert_eq!(atom.value, 90);
302        assert_eq!(atom.reducability, Reducability::Strict);
303    }
304
305    #[test]
306    fn bladeharper_variants() {
307        // all valid representations of bladeharper requirements
308        // for testing syntax stuff
309        let variants = [
310            "25 STR OR 25 AGL, 75 MED OR (LHT + MED + HVY = 90)",
311            "(25 STR OR 25 AGL), (75 MED OR (LHT + MED + HVY = 90))",
312            "STR = 25 OR AGL = 25, 75 MED OR (LHT + MED + HVY = 90)",
313            "(STR = 25 OR AGL = 25), (75 MED OR (LHT + MED + HVY = 90))",
314            "(STR = 25 OR AGL = 25),(75 MED OR (LHT + MED + HVY = 90))",
315            "STR=25 OR AGL= 25,med=75 OR (lht + MED +hvy = 90)",
316        ];
317
318        let parsed: Vec<Requirement> = variants
319            .iter()
320            .map(|s| parse_req(s).expect(&format!("Failed to parse: {}", s)))
321            .collect();
322
323        // verify all parse successfully and are equal
324        for (i, req) in parsed.iter().enumerate() {
325            assert_eq!(req.clauses.len(), 2, "variant {} should have 2 clauses", i);
326        }
327
328        // all variants should be equal to each other
329        for i in 1..parsed.len() {
330            assert_eq!(parsed[0], parsed[i], "variant 0 should equal variant {}", i);
331        }
332
333        // verify structure of one of them (then they all are correct)
334        let req = &parsed[0];
335
336        // first clause: 25 STR OR 25 AGL
337        let clause1 = &req.clauses[0];
338        assert_eq!(clause1.clause_type, ClauseType::Or);
339        assert_eq!(clause1.atoms.len(), 2);
340
341        // second clause: 75 MED OR (LHT + MED + HVY = 90)
342        let clause2 = &req.clauses[1];
343        assert_eq!(clause2.clause_type, ClauseType::Or);
344        assert_eq!(clause2.atoms.len(), 2);
345    }
346
347    #[test]
348    fn bunch_of_random_stuff() {
349        // silentheart reqs
350        parse_req("25R STR, LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
351        parse_req("(25R STR), LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
352        parse_req("silentheart := str=25r,lht+med+hvy=75,25CHA OR agl=25r").unwrap();
353        parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").unwrap();
354        assert!(parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").is_ok());
355        // assert!(parse("silentheart := ((str=25r),lht+med+hvy=75,(25CHA OR agl=25r))").is_ok());
356        // assert!(parse("silentheart := ((str=25r),lht+med+hvy=75,(25CHA OR agl=25r))").is_ok());
357
358        // neuro reqs
359        assert!(parse_req("35cha OR 35wll OR 35int").is_ok());
360        assert!(parse_req("35 cha OR 35 wll OR 35 int").is_ok());
361        assert!(parse_req("()").unwrap().is_empty());
362
363        // INVALID BAD REQ
364        assert!(parse_req("(35 cha").is_err());
365        assert!(parse_req("35 SBF").is_err());
366        assert!(parse_req("35CHAOR35WLL").is_err());
367    }
368
369    #[test]
370    fn explicit_reducability() {
371        let req = parse_req("25S STR").unwrap();
372        let atom = req.clauses[0].atoms.iter().next().unwrap();
373        assert_eq!(atom.reducability, Reducability::Strict);
374
375        let req = parse_req("25R STR").unwrap();
376        let atom = req.clauses[0].atoms.iter().next().unwrap();
377        assert_eq!(atom.reducability, Reducability::Reducible);
378
379        let req = parse_req("25S STR OR 25R AGL").unwrap();
380        assert_eq!(req.clauses[0].clause_type, ClauseType::Or);
381    }
382
383    #[test]
384    fn casing_and_compactness() {
385        let req1 = parse_req("25 str or 25 agl").unwrap();
386        let req2 = parse_req("25 STR or 25 AGL").unwrap();
387        assert_eq!(req1, req2);
388
389        assert!(parse_req("25 Str OR 25 AgL").is_ok());
390
391        assert!(parse_req("lht+hvy=90").is_ok());
392        assert!(parse_req("lht+med+hvy=90").is_ok());
393        assert!(parse_req("25 STR OR AGL=25,75S MED OR (LHT+MED+HVY=90)").is_ok());
394
395        let compact = parse_req("str=25 OR agl=25").unwrap();
396        let spaced = parse_req("STR = 25 OR AGL = 25").unwrap();
397        assert_eq!(compact, spaced);
398    }
399}