Skip to main content

inkling/line/
condition.rs

1//! Conditions for displaying choices, lines or other content.
2//!
3//! The base of this module is the `Condition` struct which is in essence the root
4//! of a set of conditions which must be fulfilled for some content to be displayed.
5//! From this root we can evaluate the entire condition tree linked to it.
6//!
7//! Since `Condition` is the large container for a condition, there are several
8//! smaller pieces working as the glue. `ConditionItem` is a container for each
9//! individual part of a condition.
10//!
11//! For example, if a condition is `(i > 2) and (i < 5)` then the entire string
12//! represents the `Condition` while the individual `i > 2` and `i < 5` parts are
13//! `ConditionItem`. Each individual `ConditionItem` can be negated: `not i > 2`,
14//! and so on.
15//!
16//! `Ink` supports two types of links between conditions: `and` and `or` (no exclusive
17//! or). These are linked to `ConditionItem`s through the `AndOr` struct. So when
18//! the full `Condition` is evaluating it will check this enum along with the item
19//! to assert whether the condition passes.
20//!
21//! Finally comes the representation of single statements. These are contained in
22//! the `ConditionKind` enum which has items for `true` and `false` if a super
23//! simple item is created, `StoryCondition` if the condition has to access the
24//! running story state to be evaluated (this will almost always be the case)
25//! and `Nested` for nested conditions.
26//!
27//! A note about `StoryCondition`: this represents a condition created by the user
28//! in the script. This module is not responsible for evaluating it based on
29//! the story state. The module is responsible for ensuring that conditions and logic
30//! works correctly through nesting and whatnot. See `Condition` and its methods for
31//! more information.
32
33use crate::{
34    error::{
35        parse::validate::{ExpressionKind, InvalidVariableExpression, ValidationError},
36        utils::MetaData,
37    },
38    knot::Address,
39    line::{Expression, Variable},
40    process::check_condition,
41    story::validate::{ValidateContent, ValidationData},
42};
43
44use std::{cmp::Ordering, error::Error};
45
46#[cfg(feature = "serde_support")]
47use crate::utils::OrderingDerive;
48#[cfg(feature = "serde_support")]
49use serde::{Deserialize, Serialize};
50
51#[derive(Clone, Debug, PartialEq)]
52#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
53/// Condition for displaying some content or choice in the story.
54pub struct Condition {
55    /// First condition to evaluate.
56    pub root: ConditionItem,
57    /// Ordered set of `and`/`or` conditions to compare the first condition to.
58    pub items: Vec<AndOr>,
59}
60
61#[derive(Clone, Debug, PartialEq)]
62#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
63/// Base item in a condition.
64///
65/// Will evaluate to a single `true` or `false` but may have to evaluate a group
66/// of conditions. This is not done by this module or struct! This struct only
67/// implements the framework through which choices can be created, parsed and
68/// ensured that all items in a condition are true if told.
69///
70/// The evaluation of each individual condition is performed by the `evaluate`
71/// method. This takes a closure for the caller and applies it to the item,
72/// producing the result which is linked to the rest of the conditions to
73/// determine the final true or false value.
74pub struct ConditionItem {
75    /// Negate the condition upon evaluation.
76    pub negate: bool,
77    /// Kind of condition.
78    pub kind: ConditionKind,
79}
80
81#[derive(Clone, Debug, PartialEq)]
82#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
83/// Condition variants.
84pub enum ConditionKind {
85    /// Always `true`.
86    True,
87    /// Always `false`.
88    False,
89    /// Nested `Condition` which has to be evaluated as a group.
90    Nested(Box<Condition>),
91    /// Single condition to evaluate.
92    Single(StoryCondition),
93}
94
95#[derive(Clone, Debug, PartialEq)]
96#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
97/// Condition to show some content in a story.
98pub enum StoryCondition {
99    /// Compares two variables from an `x > y` (or similar) comparative statement.
100    ///
101    /// Variable `Address` variants will evaluate to their value (see the `as_value`
102    /// method for [`Variable`][crate::line::Variable]), then compare using that.
103    ///
104    /// Equal-to comparisons (`==`) can be made for all variable types. Less-than (`<`)
105    /// and greater-than (`>`) comparisons are only allowed for `Int` and `Float` variants.
106    /// An error is raised if another variant is used like that.
107    Comparison {
108        /// Left hand side variable.
109        lhs_variable: Expression,
110        /// Right hand side variable.
111        rhs_variable: Expression,
112        /// Order comparison between the two.
113        ///
114        /// Applies from the left hand side variable to the right hand side. Meaning that
115        /// for eg. `lhs > rhs` the ordering will be `Ordering::Greater`.
116        #[cfg_attr(feature = "serde_support", serde(with = "OrderingDerive"))]
117        ordering: Ordering,
118    },
119    /// Assert that the variable value is "true".
120    ///
121    /// This is evaluated differently for different variable types.
122    ///
123    /// *   Boolean variables evaluate directly.
124    /// *   Number variables (integers and floats) are `true` if they are non-zero.
125    /// *   String variables are `true` if they have non-zero length.
126    ///
127    /// Variable `Address` variants will evaluate their value (see the `as_value` method
128    /// for [`Variable`][crate::line::Variable]), then as above.
129    ///
130    /// Variable `Divert` variants will never evaluate to `true` or `false`, but raise
131    /// and error. They are not supposed to be used like this.
132    IsTrueLike { variable: Variable },
133}
134
135#[derive(Clone, Debug, PartialEq)]
136#[cfg_attr(feature = "serde_support", derive(Deserialize, Serialize))]
137/// Container for `and`/`or` variants of conditions to evaluate in a list.
138pub enum AndOr {
139    And(ConditionItem),
140    Or(ConditionItem),
141}
142
143impl Condition {
144    /// Evaluate the condition with the given evaluator closure.
145    ///
146    /// This closure will be called on every item in the `Condition` as all parts
147    /// are walked through.
148    pub fn evaluate<F, E>(&self, evaluator: &F) -> Result<bool, E>
149    where
150        F: Fn(&StoryCondition) -> Result<bool, E>,
151        E: Error,
152    {
153        self.items
154            .iter()
155            .fold(inner_eval(&self.root, evaluator), |acc, next_condition| {
156                acc.and_then(|current| match next_condition {
157                    AndOr::And(item) => inner_eval(item, evaluator).map(|next| current && next),
158                    AndOr::Or(item) => inner_eval(item, evaluator).map(|next| current || next),
159                })
160            })
161    }
162}
163
164/// Match against and evaluate the items.
165fn inner_eval<F, E>(item: &ConditionItem, evaluator: &F) -> Result<bool, E>
166where
167    F: Fn(&StoryCondition) -> Result<bool, E>,
168    E: Error,
169{
170    let mut result = match &item.kind {
171        ConditionKind::True => Ok(true),
172        ConditionKind::False => Ok(false),
173        ConditionKind::Nested(condition) => condition.evaluate(evaluator),
174        ConditionKind::Single(ref kind) => evaluator(kind),
175    }?;
176
177    if item.negate {
178        result = !result;
179    }
180
181    Ok(result)
182}
183
184/// Constructor struct for `Condition`.
185pub struct ConditionBuilder {
186    root: ConditionItem,
187    items: Vec<AndOr>,
188}
189
190impl ConditionBuilder {
191    /// Create the constructor with a condition kind.
192    pub fn from_kind(kind: &ConditionKind, negate: bool) -> Self {
193        let root = ConditionItem {
194            kind: kind.clone(),
195            negate,
196        };
197
198        ConditionBuilder {
199            root,
200            items: Vec::new(),
201        }
202    }
203
204    /// Finalize the `Condition` and return it.
205    pub fn build(self) -> Condition {
206        Condition {
207            root: self.root,
208            items: self.items,
209        }
210    }
211
212    /// Add an `and` item to the condition list.
213    pub fn and(&mut self, kind: &ConditionKind, negate: bool) {
214        self.items.push(AndOr::And(ConditionItem {
215            kind: kind.clone(),
216            negate,
217        }));
218    }
219
220    /// Add an `or` item to the condition list.
221    pub fn or(&mut self, kind: &ConditionKind, negate: bool) {
222        self.items.push(AndOr::Or(ConditionItem {
223            kind: kind.clone(),
224            negate,
225        }));
226    }
227
228    /// Extend the `items` list with the given slice.
229    pub fn extend(&mut self, items: &[AndOr]) {
230        self.items.extend_from_slice(items);
231    }
232}
233
234impl ValidateContent for Condition {
235    fn validate(
236        &mut self,
237        error: &mut ValidationError,
238        current_location: &Address,
239        meta_data: &MetaData,
240        data: &ValidationData,
241    ) {
242        let num_errors = error.num_errors();
243
244        self.root
245            .kind
246            .validate(error, current_location, meta_data, data);
247
248        self.items.iter_mut().for_each(|item| match item {
249            AndOr::And(item) | AndOr::Or(item) => {
250                item.kind.validate(error, current_location, meta_data, data)
251            }
252        });
253
254        if num_errors == error.num_errors() {
255            if let Err(err) = check_condition(self, &data.follow_data) {
256                error.variable_errors.push(InvalidVariableExpression {
257                    expression_kind: ExpressionKind::Condition,
258                    kind: err.into(),
259                    meta_data: meta_data.clone(),
260                });
261            }
262        }
263    }
264}
265
266impl ValidateContent for ConditionKind {
267    fn validate(
268        &mut self,
269        error: &mut ValidationError,
270        current_location: &Address,
271        meta_data: &MetaData,
272        data: &ValidationData,
273    ) {
274        match self {
275            ConditionKind::True | ConditionKind::False => (),
276            ConditionKind::Nested(condition) => {
277                condition.validate(error, current_location, meta_data, data)
278            }
279            ConditionKind::Single(kind) => kind.validate(error, current_location, meta_data, data),
280        }
281    }
282}
283
284impl ValidateContent for StoryCondition {
285    fn validate(
286        &mut self,
287        error: &mut ValidationError,
288        current_location: &Address,
289        meta_data: &MetaData,
290        data: &ValidationData,
291    ) {
292        match self {
293            StoryCondition::Comparison {
294                ref mut lhs_variable,
295                ref mut rhs_variable,
296                ..
297            } => {
298                lhs_variable.validate(error, current_location, meta_data, data);
299                rhs_variable.validate(error, current_location, meta_data, data);
300            }
301            StoryCondition::IsTrueLike { variable } => {
302                variable.validate(error, current_location, meta_data, data)
303            }
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    use std::fmt;
313
314    use ConditionKind::{False, True};
315
316    impl From<StoryCondition> for Condition {
317        fn from(kind: StoryCondition) -> Self {
318            ConditionBuilder::from_kind(&kind.into(), false).build()
319        }
320    }
321
322    impl From<StoryCondition> for ConditionKind {
323        fn from(kind: StoryCondition) -> Self {
324            ConditionKind::Single(kind)
325        }
326    }
327
328    impl From<&StoryCondition> for ConditionKind {
329        fn from(kind: &StoryCondition) -> Self {
330            ConditionKind::Single(kind.clone())
331        }
332    }
333
334    impl Condition {
335        pub fn story_condition(&self) -> &StoryCondition {
336            &self.root.kind.story_condition()
337        }
338
339        pub fn with_and(mut self, kind: ConditionKind) -> Self {
340            let item = ConditionItem {
341                kind,
342                negate: false,
343            };
344
345            self.items.push(AndOr::And(item));
346            self
347        }
348
349        pub fn with_or(mut self, kind: ConditionKind) -> Self {
350            let item = ConditionItem {
351                kind,
352                negate: false,
353            };
354
355            self.items.push(AndOr::Or(item));
356            self
357        }
358    }
359
360    impl ConditionKind {
361        pub fn nested(&self) -> &Condition {
362            match self {
363                ConditionKind::Nested(condition) => condition,
364                other => panic!(
365                    "tried to extract nested `Condition`, but item was not `ConditionKind::Nested` \
366                     (was: {:?})",
367                     other
368                ),
369            }
370        }
371
372        pub fn story_condition(&self) -> &StoryCondition {
373            match self {
374                ConditionKind::Single(story_condition) => story_condition,
375                other => panic!(
376                    "tried to extract `StoryCondition`, but item was not `ConditionKind::Single` \
377                     (was: {:?})",
378                    other
379                ),
380            }
381        }
382    }
383
384    impl AndOr {
385        pub fn nested(&self) -> &Condition {
386            match self {
387                AndOr::And(item) | AndOr::Or(item) => item.kind.nested(),
388            }
389        }
390
391        pub fn story_condition(&self) -> &StoryCondition {
392            match self {
393                AndOr::And(item) | AndOr::Or(item) => item.kind.story_condition(),
394            }
395        }
396
397        pub fn is_and(&self) -> bool {
398            match self {
399                AndOr::And(..) => true,
400                _ => false,
401            }
402        }
403
404        pub fn is_or(&self) -> bool {
405            match self {
406                AndOr::Or(..) => true,
407                _ => false,
408            }
409        }
410    }
411
412    #[derive(Debug)]
413    struct MockError;
414
415    impl fmt::Display for MockError {
416        fn fmt(&self, _f: &mut fmt::Formatter) -> fmt::Result {
417            unreachable!();
418        }
419    }
420
421    impl Error for MockError {}
422
423    #[test]
424    fn condition_links_from_left_to_right() {
425        let f = |kind: &StoryCondition| match kind {
426            _ => Err(MockError),
427        };
428
429        assert!(ConditionBuilder::from_kind(&True.into(), false)
430            .build()
431            .evaluate(&f)
432            .unwrap());
433
434        assert!(!ConditionBuilder::from_kind(&False.into(), false)
435            .build()
436            .evaluate(&f)
437            .unwrap());
438
439        assert!(ConditionBuilder::from_kind(&True.into(), false)
440            .build()
441            .with_and(True.into())
442            .evaluate(&f)
443            .unwrap());
444
445        assert!(!ConditionBuilder::from_kind(&True.into(), false)
446            .build()
447            .with_and(False.into())
448            .evaluate(&f)
449            .unwrap());
450
451        assert!(ConditionBuilder::from_kind(&False.into(), false)
452            .build()
453            .with_and(False.into())
454            .with_or(True)
455            .evaluate(&f)
456            .unwrap());
457
458        assert!(!ConditionBuilder::from_kind(&False.into(), false)
459            .build()
460            .with_and(False)
461            .with_or(True)
462            .with_and(False)
463            .evaluate(&f)
464            .unwrap());
465    }
466
467    #[test]
468    fn conditions_can_be_negated() {
469        let f = |kind: &StoryCondition| match kind {
470            _ => Err(MockError),
471        };
472
473        assert!(ConditionBuilder::from_kind(&False.into(), true)
474            .build()
475            .evaluate(&f)
476            .unwrap());
477    }
478}