storylets/
context.rs

1use crate::grammar;
2
3use std::collections::hash_map::DefaultHasher;
4use std::collections::{HashMap, HashSet};
5use std::convert::TryInto;
6use std::hash::{Hash, Hasher};
7use std::str::FromStr;
8
9use rand::Rng;
10use regex::{Regex, RegexBuilder};
11use throne::{PhraseGroup, PhraseString};
12use unidecode::unidecode;
13
14pub struct Context {
15    active_script_id: Option<u64>,
16    throne_context: throne::Context,
17    quality_properties: HashMap<String, QualityProperty>,
18}
19
20pub struct Script {
21    id: u64,
22    throne_context: throne::Context,
23    quality_properties: HashMap<String, QualityProperty>,
24}
25
26#[derive(Clone, Debug, Default)]
27pub struct QualityProperty {
28    pub title: Option<String>,
29    level_descriptions: Vec<QualityLevelDescription>,
30    level_change_descriptions: Vec<QualityLevelChangeDescription>,
31    hide_level: bool,
32    hide_level_change: bool,
33    is_thing: bool,
34    is_global: bool,
35}
36
37impl QualityProperty {
38    fn pluralized_title(&self) -> Option<String> {
39        if self.is_thing {
40            self.title.as_ref().map(|s| grammar::pluralize(&s))
41        } else {
42            self.title.clone()
43        }
44    }
45
46    fn description_for_level(&self, level: i32) -> Option<String> {
47        let mut level_description = None;
48
49        for d in self.level_descriptions.iter() {
50            if level >= d.level {
51                level_description = Some(d.description.to_string());
52            } else {
53                break;
54            }
55        }
56
57        if self.is_thing {
58            let description = self.title.as_ref().map(|s| {
59                if level > 1 {
60                    grammar::pluralize(&s)
61                } else {
62                    s.to_string()
63                }
64            });
65
66            if let Some(level_description) = level_description {
67                if self.hide_level {
68                    description.map(|description| format!("{} {}", level_description, description))
69                } else {
70                    description.map(|description| {
71                        format!("{} {} ({})", level, level_description, description)
72                    })
73                }
74            } else {
75                if self.hide_level {
76                    description
77                } else {
78                    description.map(|description| format!("{} {}", level, description))
79                }
80            }
81        } else {
82            let description = self.title.as_ref().map(|s| s.to_string());
83
84            if let Some(level_description) = level_description {
85                if self.hide_level {
86                    description.map(|description| format!("{}: {}", description, level_description))
87                } else {
88                    description.map(|description| {
89                        format!("{}: {} ({})", description, level_description, level)
90                    })
91                }
92            } else {
93                if self.hide_level {
94                    description
95                } else {
96                    description.map(|description| format!("{}: {}", description, level))
97                }
98            }
99        }
100    }
101
102    fn description_for_level_change(&self, level_before: i32, level_after: i32) -> Option<String> {
103        let diff = level_after - level_before;
104
105        let mut level_before_description_idx = None;
106        let mut level_after_description_idx = None;
107
108        for (i, d) in self.level_change_descriptions.iter().enumerate() {
109            if level_before >= d.level {
110                level_before_description_idx = Some(i);
111            } else {
112                break;
113            }
114        }
115
116        for (i, d) in self.level_change_descriptions.iter().enumerate() {
117            if level_after >= d.level {
118                level_after_description_idx = Some(i);
119            } else {
120                break;
121            }
122        }
123
124        let level_change_description =
125            match (level_after_description_idx, level_before_description_idx) {
126                (Some(a), None) => Some(&self.level_change_descriptions[a].description),
127                (Some(a), Some(b)) if a != b => {
128                    Some(&self.level_change_descriptions[a].description)
129                }
130                _ => None,
131            };
132
133        if self.is_thing {
134            let plural_description = self.title.as_ref().map(|s| grammar::pluralize(&s));
135
136            let diff_description = self.title.as_ref().map(|s| {
137                if diff.abs() > 1 {
138                    grammar::pluralize(&s)
139                } else {
140                    s.to_string()
141                }
142            });
143
144            let description = self.title.as_ref().map(|s| {
145                if level_after > 1 {
146                    grammar::pluralize(&s)
147                } else {
148                    s.to_string()
149                }
150            });
151
152            if let Some(level_change_description) = level_change_description {
153                if self.hide_level_change {
154                    Some(level_change_description.to_string())
155                } else {
156                    Some(format!(
157                        "{} (new total: {})",
158                        level_change_description, level_after
159                    ))
160                }
161            } else {
162                if self.hide_level_change {
163                    plural_description.map(|plural_description| {
164                        format!(
165                            "You {} some {}",
166                            if diff > 0 { "gained" } else { "lost" },
167                            plural_description
168                        )
169                    })
170                } else {
171                    if level_before == 0 {
172                        description.map(|description| {
173                            format!("You now have {} {}", level_after, description)
174                        })
175                    } else {
176                        diff_description.map(|diff_description| {
177                            format!(
178                                "You {} {} {} (new total: {})",
179                                if diff > 0 { "gained" } else { "lost" },
180                                diff.abs(),
181                                diff_description,
182                                level_after
183                            )
184                        })
185                    }
186                }
187            }
188        } else {
189            let description = self.title.as_ref().map(|s| s.to_string());
190
191            if let Some(level_change_description) = level_change_description {
192                if self.hide_level_change {
193                    Some(level_change_description.to_string())
194                } else {
195                    Some(format!(
196                        "{} (new level: {})",
197                        level_change_description, level_after
198                    ))
199                }
200            } else {
201                if self.hide_level_change {
202                    if level_before == 0 {
203                        description.map(|description| {
204                            format!("You now have the '{}' quality", description)
205                        })
206                    } else if level_before == 0 {
207                        description.map(|description| {
208                            format!("You no longer have the '{}' quality", description)
209                        })
210                    } else {
211                        description.map(|description| {
212                            format!(
213                                "Your '{}' quality {}",
214                                description,
215                                if diff > 0 { "increased" } else { "decreased" },
216                            )
217                        })
218                    }
219                } else {
220                    if level_before == 0 {
221                        description.map(|description| {
222                            format!("Your '{}' quality is now {}", description, level_after)
223                        })
224                    } else {
225                        description.map(|description| {
226                            format!(
227                                "Your '{}' quality {} by {} (new level: {})",
228                                description,
229                                if diff > 0 { "increased" } else { "decreased" },
230                                diff.abs(),
231                                level_after
232                            )
233                        })
234                    }
235                }
236            }
237        }
238    }
239}
240
241#[derive(Clone, Debug)]
242struct QualityLevelDescription {
243    level: i32,
244    description: String,
245}
246
247#[derive(Clone, Debug)]
248struct QualityLevelChangeDescription {
249    level: i32,
250    description: String,
251}
252
253#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
254pub struct Card {
255    pub id: String,
256    pub title: String,
257    pub description: String,
258    pub requirements: Vec<CardRequirement>,
259    pub branches: Vec<Branch>,
260}
261
262#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
263pub struct Branch {
264    pub id: String,
265    pub title: String,
266    pub description: String,
267    pub failed: bool,
268    pub requirements: Vec<BranchRequirement>,
269
270    result_weights: Vec<ResultWeight>,
271}
272
273impl Branch {
274    pub fn get_requirement_descriptions(&self, context: &Context) -> Vec<String> {
275        if !self.failed {
276            return vec![];
277        }
278
279        self.requirements
280            .iter()
281            .filter(|r| r.failed)
282            .filter_map(|r| {
283                let description = context
284                    .quality_properties
285                    .get(&r.quality)
286                    .and_then(|properties| properties.title.as_ref().map(|s| s.to_string()));
287
288                if let Some(description) = description {
289                    Some(r.condition.failure_description(&description))
290                } else {
291                    None
292                }
293            })
294            .collect()
295    }
296
297    pub fn get_difficulty_descriptions(&self, context: &Context) -> Vec<String> {
298        self.result_weights
299            .iter()
300            .filter_map(|w| {
301                if let Some(quality_id) = &w.difficulty_quality {
302                    let description = context
303                        .quality_properties
304                        .get(quality_id)
305                        .and_then(|properties| properties.title.as_ref().map(|s| s.to_string()));
306
307                    if let Some(description) = description {
308                        return Some(format!(
309                            "Your '{}' quality gives you a {}% chance of success",
310                            description,
311                            100 - w.probability
312                        ));
313                    }
314                }
315
316                None
317            })
318            .collect()
319    }
320}
321
322#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
323pub struct ResultWeight {
324    result: String,
325    probability: i32,
326    difficulty_quality: Option<String>,
327}
328
329#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
330pub struct BranchResult {
331    pub id: String,
332    pub title: String,
333    pub description: String,
334    pub effects: Vec<BranchResultEffect>,
335}
336
337impl BranchResult {
338    pub fn append_change_quality_effect(&mut self, effect: &ChangeQualityEffect) {
339        self.effects.push(BranchResultEffect::QualityChanged {
340            quality: effect.quality.clone(),
341            diff: effect.diff,
342            value: effect.value,
343            description: effect.description.clone(),
344        });
345    }
346}
347
348#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
349pub enum BranchResultEffect {
350    QualityChanged {
351        quality: String,
352        diff: i32,
353        value: i32,
354        description: Option<String>,
355    },
356    AddPhrase {
357        phrase: throne::VecPhrase,
358        phrase_string: String,
359    },
360}
361
362#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
363pub struct CardRequirement {
364    pub quality: String,
365    pub failed: bool,
366}
367
368#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
369pub struct BranchRequirement {
370    pub quality: String,
371    pub condition: RequirementCondition,
372    pub failed: bool,
373}
374
375#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
376pub enum RequirementCondition {
377    Exists,
378    Missing,
379    Eq(i32),
380    Neq(i32),
381    Lt(i32),
382    LtEq(i32),
383    Gt(i32),
384    GtEq(i32),
385}
386
387impl RequirementCondition {
388    fn failure_description(&self, quality_description: &str) -> String {
389        match self {
390            RequirementCondition::Exists => {
391                format!("You must have the '{}' quality", quality_description)
392            }
393            RequirementCondition::Missing => {
394                format!("You already have the '{}' quality", quality_description)
395            }
396            RequirementCondition::Eq(level) => format!(
397                "Your '{}' quality must be equal to {}",
398                quality_description, level
399            ),
400            RequirementCondition::Neq(level) => format!(
401                "Your '{}' quality should not be equal to {}",
402                quality_description, level
403            ),
404            RequirementCondition::Lt(level) => format!(
405                "Your '{}' quality must be less than {}",
406                quality_description, level
407            ),
408            RequirementCondition::LtEq(level) => format!(
409                "Your '{}' quality must be less than or equal to {}",
410                quality_description, level
411            ),
412            RequirementCondition::Gt(level) => format!(
413                "Your '{}' quality must be greater than {}",
414                quality_description, level
415            ),
416            RequirementCondition::GtEq(level) => format!(
417                "Your '{}' quality must be greater than or equal to {}",
418                quality_description, level
419            ),
420        }
421    }
422}
423
424impl RequirementCondition {
425    fn from_str(s: &str, level: Option<i32>) -> Self {
426        let expect_msg = format!("Missing level for {} condition", s);
427        match s {
428            "exists" => RequirementCondition::Exists,
429            "missing" => RequirementCondition::Missing,
430            "eq" => RequirementCondition::Eq(level.expect(&expect_msg)),
431            "neq" => RequirementCondition::Neq(level.expect(&expect_msg)),
432            "lt" => RequirementCondition::Lt(level.expect(&expect_msg)),
433            "lt-eq" => RequirementCondition::LtEq(level.expect(&expect_msg)),
434            "gt" => RequirementCondition::Gt(level.expect(&expect_msg)),
435            "gt-eq" => RequirementCondition::GtEq(level.expect(&expect_msg)),
436            _ => unreachable!("Unhandled condition: {}", s),
437        }
438    }
439}
440
441#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
442pub struct Quality {
443    pub id: String,
444    pub value: i32,
445    pub title: Option<String>,
446    pub description: Option<String>,
447}
448
449#[derive(Debug, Eq, PartialEq)]
450pub struct ChangeQualityEffect {
451    pub quality: String,
452    pub diff: i32,
453    pub value: i32,
454    pub description: Option<String>,
455}
456
457impl Context {
458    pub fn new() -> Self {
459        Context {
460            active_script_id: None,
461            throne_context: throne::Context::from_text(""),
462            quality_properties: HashMap::new(),
463        }
464    }
465
466    pub fn from_text(text: &str) -> Self {
467        let script = Script::from_text(text);
468        Self::from_script(script)
469    }
470
471    #[cfg(test)]
472    fn from_throne_text(text: &str) -> Self {
473        let script = Script::from_throne_text(text);
474        Self::from_script(script)
475    }
476
477    fn from_script(script: Script) -> Self {
478        let mut context = Self {
479            active_script_id: None,
480            throne_context: throne::ContextBuilder::new().build(),
481            quality_properties: HashMap::new(),
482        };
483        context.set_active_script(&script);
484        context
485    }
486
487    pub fn set_active_script(&mut self, script: &Script) {
488        if self.active_script_id == Some(script.id) {
489            return;
490        }
491
492        self.quality_properties.retain(|_, props| props.is_global);
493        for quality in self.get_qualities() {
494            if self
495                .quality_properties
496                .get(&quality.id)
497                .filter(|props| props.is_global)
498                .is_none()
499            {
500                self.set_quality(&quality.id, 0);
501            }
502        }
503
504        self.reset_state();
505
506        let mut new_context = script.throne_context.clone();
507        new_context.extend_state_from_context(&self.throne_context);
508
509        self.active_script_id = Some(script.id);
510        self.throne_context = new_context;
511        self.quality_properties
512            .extend(script.quality_properties.clone());
513    }
514
515    pub fn draw_cards(&mut self) -> Vec<Card> {
516        self.reset_state();
517
518        let throne_context = &mut self.throne_context;
519
520        let difficulty_probability_atom =
521            throne_context.str_to_existing_atom("difficulty-probability");
522
523        throne_context.append_state("#draw-cards");
524
525        let mut core = &mut throne_context.core;
526        let string_cache = &throne_context.string_cache;
527
528        throne::update(&mut core, |p: &throne::Phrase| match p[0].atom {
529            a if difficulty_probability_atom == Some(a) => {
530                let n = throne::StringCache::atom_to_number(p[1].atom)
531                    .expect("difficulty-probability must be passed a number");
532                let n_difficulty = throne::StringCache::atom_to_number(p[2].atom)
533                    .expect("difficulty-probability must be passed a number");
534
535                let prob_float = ((n as f32 / n_difficulty as f32) * 0.6).max(0.01).min(1.0);
536                let prob = 100 - (prob_float * 100.0).round() as i32;
537
538                let mut p = p.to_vec();
539                p[3] = throne::Token::new_number(prob, 0, 0);
540                Some(p)
541            }
542            _ => unreachable!(p.to_string(&string_cache)),
543        });
544
545        self.get_cards()
546    }
547
548    pub fn select_branch(&mut self, branch: &str) -> BranchResult {
549        let throne_context = &mut self.throne_context;
550
551        let test_probability_atom = throne_context.str_to_existing_atom("test-probability");
552
553        throne_context.append_state(&format!("#apply-branch {}", branch));
554
555        let mut core = &mut throne_context.core;
556        let string_cache = &throne_context.string_cache;
557
558        // ensure that we only test each result once
559        let mut tested_results_set = HashSet::new();
560
561        throne::update(&mut core, |p: &throne::Phrase| match p[0].atom {
562            a if test_probability_atom == Some(a) => {
563                let result_atom = p
564                    .get(1)
565                    .map(|t| t.atom)
566                    .expect("test-probability missing first argument");
567
568                if tested_results_set.contains(&result_atom) {
569                    return None;
570                }
571
572                tested_results_set.insert(result_atom);
573
574                let prob = p
575                    .get(2)
576                    .and_then(|t| throne::StringCache::atom_to_number(t.atom))
577                    .expect("test-probability must be passed a number");
578
579                let mut rng = rand::thread_rng();
580                if rng.gen_ratio(
581                    prob.try_into()
582                        .expect("Probability should be a positive number"),
583                    100,
584                ) {
585                    Some(p.to_vec())
586                } else {
587                    None
588                }
589            }
590            _ => unreachable!(p.to_string(&string_cache)),
591        });
592
593        let throne_context = &self.throne_context;
594        let branch_result = if let (
595            Some(branch_atom),
596            Some(result_atom),
597            Some(title_atom),
598            Some(description_atom),
599            Some(branch_id_atom),
600        ) = (
601            throne_context.str_to_existing_atom("branch"),
602            throne_context.str_to_existing_atom("result"),
603            throne_context.str_to_existing_atom("title"),
604            throne_context.str_to_existing_atom("description"),
605            throne_context.str_to_existing_atom(branch),
606        ) {
607            throne_context
608                .find_phrases_exactly4(
609                    Some(&branch_atom),
610                    Some(&branch_id_atom),
611                    Some(&result_atom),
612                    None,
613                )
614                .get(0)
615                .map(|p| {
616                    let result_id_atom = p[3].atom;
617                    let result_id = throne_context.atom_to_str(result_id_atom).to_string();
618
619                    let title = throne_context
620                        .find_phrase3(Some(&result_atom), Some(&result_id_atom), Some(&title_atom))
621                        .and_then(|p| p.get(3))
622                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
623                        .expect(&format!("Missing title for result '{}'", result_id));
624
625                    let description = throne_context
626                        .find_phrase3(
627                            Some(&result_atom),
628                            Some(&result_id_atom),
629                            Some(&description_atom),
630                        )
631                        .and_then(|p| p.get(3))
632                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
633                        .expect(&format!("Missing description for result '{}'", result_id));
634
635                    let effects = self.get_branch_result_effects(&result_id);
636
637                    BranchResult {
638                        id: result_id,
639                        title,
640                        description: format_description(&description),
641                        effects,
642                    }
643                })
644        } else {
645            None
646        }
647        .expect(&format!(
648            "Missing result for branch '{}'. Did you draw cards before selecting the branch?",
649            branch
650        ));
651
652        self.reset_state();
653
654        branch_result
655    }
656
657    fn get_branch_result_effects(&self, result_id: &str) -> Vec<BranchResultEffect> {
658        let throne_context = &self.throne_context;
659
660        let mut effects = if let (Some(result_atom), Some(effect_atom), Some(result_id_atom)) = (
661            throne_context.str_to_existing_atom("result"),
662            throne_context.str_to_existing_atom("effect"),
663            throne_context.str_to_existing_atom(result_id),
664        ) {
665            throne_context
666                .find_phrases3(
667                    Some(&result_atom),
668                    Some(&result_id_atom),
669                    Some(&effect_atom),
670                )
671                .iter()
672                .map(|p| {
673                    let effect_type = p.get(3).map(|t| throne_context.atom_to_str(t.atom));
674
675                    match effect_type {
676                        Some("quality-changed") => {
677                            let quality_id_atom = p
678                                .get(4)
679                                .map(|t| t.atom)
680                                .expect("quality-changed effect missing quality");
681
682                            let quality = throne_context.atom_to_str(quality_id_atom).to_string();
683
684                            let n_before = p
685                                .get(5)
686                                .map(|t| {
687                                    throne_context.atom_to_number(t.atom).unwrap_or_else(|| {
688                                        panic!(
689                                            "quality-changed n_before '{}' is not a number",
690                                            throne_context.atom_to_str(t.atom)
691                                        )
692                                    })
693                                })
694                                .expect("quality-changed effect missing n_before");
695
696                            let n = p
697                                .get(6)
698                                .map(|t| {
699                                    throne_context.atom_to_number(t.atom).unwrap_or_else(|| {
700                                        panic!(
701                                            "quality-changed n '{}' is not a number",
702                                            throne_context.atom_to_str(t.atom)
703                                        )
704                                    })
705                                })
706                                .expect("quality-changed effect missing n");
707
708                            let description =
709                                self.quality_properties
710                                    .get(&quality)
711                                    .and_then(|properties| {
712                                        properties.description_for_level_change(n_before, n)
713                                    });
714
715                            BranchResultEffect::QualityChanged {
716                                quality,
717                                diff: n - n_before,
718                                value: n,
719                                description: description.map(|s| format_description(&s)),
720                            }
721                        }
722                        Some("add-phrase") => {
723                            let phrase = p
724                                .get_group(4)
725                                .expect("Missing phrase for add-phrase result")
726                                .normalize();
727                            let phrase_string =
728                                throne::build_phrase(&phrase, &throne_context.string_cache);
729
730                            BranchResultEffect::AddPhrase {
731                                phrase,
732                                phrase_string,
733                            }
734                        }
735                        _ => {
736                            unreachable!(format!("unhandled result effect type: {:?}", effect_type))
737                        }
738                    }
739                })
740                .collect()
741        } else {
742            vec![]
743        };
744
745        effects.sort();
746
747        effects
748    }
749
750    pub fn set_quality(&mut self, id: &str, value: i32) -> Option<ChangeQualityEffect> {
751        self.change_quality_internal(id, value, false)
752    }
753
754    pub fn set_quality_bool(&mut self, id: &str, value: bool) -> Option<ChangeQualityEffect> {
755        self.set_quality(id, if value { 1 } else { 0 })
756    }
757
758    pub fn change_quality(&mut self, id: &str, change: i32) -> Option<ChangeQualityEffect> {
759        self.change_quality_internal(id, change, true)
760    }
761
762    fn change_quality_internal(
763        &mut self,
764        id: &str,
765        change: i32,
766        relative: bool,
767    ) -> Option<ChangeQualityEffect> {
768        let quality_atom = self.throne_context.str_to_atom("quality");
769        let quality_id_atom = self.throne_context.str_to_atom(id);
770
771        let mut remove_phrase_id = None;
772        let mut n_before = 0;
773
774        for phrase_id in self.throne_context.core.state.iter() {
775            let phrase = self.throne_context.core.state.get(phrase_id);
776            match (
777                phrase.get(0).map(|t| t.atom),
778                phrase.get(1).map(|t| t.atom),
779                phrase.get(2).map(|t| t.atom),
780            ) {
781                (Some(a1), Some(a2), Some(a3)) if a1 == quality_atom && a2 == quality_id_atom => {
782                    remove_phrase_id = Some(phrase_id);
783                    n_before = self
784                        .throne_context
785                        .atom_to_number(a3)
786                        .expect(&format!("n_before is not a number for quality '{}'", id));
787                    break;
788                }
789                _ => (),
790            }
791        }
792
793        let n_after = (if relative { n_before + change } else { change }).max(0);
794
795        if n_before == n_after {
796            return None;
797        }
798
799        if let Some(remove_phrase_id) = remove_phrase_id {
800            let mut new_phrase = self
801                .throne_context
802                .core
803                .state
804                .get(remove_phrase_id)
805                .to_vec();
806            new_phrase[2].atom = throne::StringCache::number_to_atom(n_after);
807
808            self.throne_context.core.state.remove(remove_phrase_id);
809
810            if n_after > 0 {
811                self.throne_context.core.state.push(new_phrase);
812            }
813        } else {
814            if n_after > 0 {
815                self.throne_context
816                    .append_state(&format!("quality {} {}", id, change));
817            }
818        }
819
820        let description = self
821            .quality_properties
822            .get(id)
823            .and_then(|properties| properties.description_for_level_change(n_before, n_after));
824
825        Some(ChangeQualityEffect {
826            quality: id.to_string(),
827            diff: n_after - n_before,
828            value: n_after,
829            description,
830        })
831    }
832
833    pub fn get_throne_context(&self) -> &throne::Context {
834        &self.throne_context
835    }
836
837    fn get_cards(&self) -> Vec<Card> {
838        let throne_context = &self.throne_context;
839        let mut cards = if let (Some(card_atom), Some(title_atom), Some(description_atom)) = (
840            throne_context.str_to_existing_atom("card"),
841            throne_context.str_to_existing_atom("title"),
842            throne_context.str_to_existing_atom("description"),
843        ) {
844            throne_context
845                .find_phrases_exactly2(Some(&card_atom), None)
846                .iter()
847                .filter_map(|p| {
848                    let card_id_atom = p[1].atom;
849                    let card_id = throne_context.atom_to_str(card_id_atom).to_string();
850                    let requirements = self.get_card_requirements(&card_id);
851                    let branches = self.get_branches(&card_id);
852
853                    let title = throne_context
854                        .find_phrase3(Some(&card_atom), Some(&card_id_atom), Some(&title_atom))
855                        .and_then(|p| p.get(3))
856                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
857                        .expect(&format!("Missing title for card '{}'", card_id));
858
859                    let description = throne_context
860                        .find_phrase3(
861                            Some(&card_atom),
862                            Some(&card_id_atom),
863                            Some(&description_atom),
864                        )
865                        .and_then(|p| p.get(3))
866                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
867                        .unwrap_or("".to_string());
868
869                    if branches.len() == 0 {
870                        return None;
871                    }
872
873                    Some(Card {
874                        id: card_id,
875                        title,
876                        description: format_description(&description),
877                        requirements,
878                        branches,
879                    })
880                })
881                .collect()
882        } else {
883            vec![]
884        };
885
886        cards.sort();
887
888        cards
889    }
890
891    fn get_card_requirements(&self, card_id: &str) -> Vec<CardRequirement> {
892        let throne_context = &self.throne_context;
893
894        if let (Some(card_atom), Some(card_id_atom)) = (
895            throne_context.str_to_existing_atom("card"),
896            throne_context.str_to_existing_atom(card_id),
897        ) {
898            let mut requirements = if let Some(passed_quality_atom) =
899                throne_context.str_to_existing_atom("passed-quality")
900            {
901                throne_context
902                    .find_phrases3(
903                        Some(&card_atom),
904                        Some(&card_id_atom),
905                        Some(&passed_quality_atom),
906                    )
907                    .iter()
908                    .map(|p| {
909                        let quality = throne_context.atom_to_str(p[4].atom).to_string();
910                        CardRequirement {
911                            quality,
912                            failed: false,
913                        }
914                    })
915                    .collect()
916            } else {
917                vec![]
918            };
919
920            requirements.sort();
921
922            requirements
923        } else {
924            vec![]
925        }
926    }
927
928    fn get_branches(&self, card_id: &str) -> Vec<Branch> {
929        let throne_context = &self.throne_context;
930
931        let mut branches = if let (
932            Some(card_atom),
933            Some(card_id_atom),
934            Some(branch_atom),
935            Some(title_atom),
936            Some(description_atom),
937            Some(result_probability_atom),
938        ) = (
939            throne_context.str_to_existing_atom("card"),
940            throne_context.str_to_existing_atom(card_id),
941            throne_context.str_to_existing_atom("branch"),
942            throne_context.str_to_existing_atom("title"),
943            throne_context.str_to_existing_atom("description"),
944            throne_context.str_to_existing_atom("result-probability"),
945        ) {
946            throne_context
947                .find_phrases_exactly4(
948                    Some(&card_atom),
949                    Some(&card_id_atom),
950                    Some(&branch_atom),
951                    None,
952                )
953                .iter()
954                .map(|p| {
955                    let branch_id_atom = p[3].atom;
956                    let branch_id = throne_context.atom_to_str(branch_id_atom).to_string();
957                    let requirements = self.get_branch_requirements(&branch_id);
958                    let failed = requirements.iter().any(|r| r.failed);
959
960                    let title = throne_context
961                        .find_phrase3(Some(&branch_atom), Some(&branch_id_atom), Some(&title_atom))
962                        .and_then(|p| p.get(3))
963                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
964                        .expect(&format!("Missing title for branch '{}'", branch_id));
965
966                    let description = throne_context
967                        .find_phrase3(
968                            Some(&branch_atom),
969                            Some(&branch_id_atom),
970                            Some(&description_atom),
971                        )
972                        .and_then(|p| p.get(3))
973                        .map(|t| throne_context.atom_to_str(t.atom).to_string())
974                        .unwrap_or("".to_string());
975
976                    let result_weights = throne_context
977                        .find_phrases3(
978                            Some(&branch_atom),
979                            Some(&branch_id_atom),
980                            Some(&result_probability_atom),
981                        )
982                        .iter()
983                        .map(|p| {
984                            let result_id_atom = p[3].atom;
985                            let probability_atom = p[4].atom;
986                            let quality_id_atom = p.get(5).map(|t| t.atom);
987
988                            let result_id = throne_context.atom_to_str(result_id_atom);
989                            let probability = throne_context
990                                .atom_to_number(probability_atom)
991                                .expect("Result probability is not a number");
992                            let quality_id =
993                                quality_id_atom.map(|a| throne_context.atom_to_str(a).to_string());
994
995                            ResultWeight {
996                                result: result_id.to_string(),
997                                probability,
998                                difficulty_quality: quality_id,
999                            }
1000                        })
1001                        .collect();
1002
1003                    Branch {
1004                        id: branch_id,
1005                        title,
1006                        description: format_description(&description),
1007                        failed,
1008                        requirements,
1009                        result_weights,
1010                    }
1011                })
1012                .collect()
1013        } else {
1014            vec![]
1015        };
1016
1017        branches.sort();
1018
1019        branches
1020    }
1021
1022    fn get_branch_requirements(&self, branch_id: &str) -> Vec<BranchRequirement> {
1023        let throne_context = &self.throne_context;
1024
1025        if let (Some(branch_atom), Some(branch_id_atom)) = (
1026            throne_context.str_to_existing_atom("branch"),
1027            throne_context.str_to_existing_atom(branch_id),
1028        ) {
1029            let mut passed_requirements = if let Some(passed_quality_atom) =
1030                throne_context.str_to_existing_atom("passed-quality")
1031            {
1032                throne_context
1033                    .find_phrases3(
1034                        Some(&branch_atom),
1035                        Some(&branch_id_atom),
1036                        Some(&passed_quality_atom),
1037                    )
1038                    .iter()
1039                    .map(|p| {
1040                        let quality = throne_context.atom_to_str(p[4].atom).to_string();
1041
1042                        let level = p.get(5).and_then(|t| throne_context.atom_to_number(t.atom));
1043                        let condition = RequirementCondition::from_str(
1044                            throne_context.atom_to_str(p[3].atom),
1045                            level,
1046                        );
1047                        BranchRequirement {
1048                            quality,
1049                            condition,
1050                            failed: false,
1051                        }
1052                    })
1053                    .collect()
1054            } else {
1055                vec![]
1056            };
1057
1058            let mut failed_requirements = if let Some(failed_quality_atom) =
1059                throne_context.str_to_existing_atom("failed-quality")
1060            {
1061                throne_context
1062                    .find_phrases3(
1063                        Some(&branch_atom),
1064                        Some(&branch_id_atom),
1065                        Some(&failed_quality_atom),
1066                    )
1067                    .iter()
1068                    .map(|p| {
1069                        let quality = throne_context.atom_to_str(p[4].atom).to_string();
1070
1071                        let level = p.get(5).and_then(|t| throne_context.atom_to_number(t.atom));
1072                        let condition = RequirementCondition::from_str(
1073                            throne_context.atom_to_str(p[3].atom),
1074                            level,
1075                        );
1076                        BranchRequirement {
1077                            quality,
1078                            condition,
1079                            failed: true,
1080                        }
1081                    })
1082                    .collect()
1083            } else {
1084                vec![]
1085            };
1086
1087            let mut requirements = vec![];
1088            requirements.append(&mut passed_requirements);
1089            requirements.append(&mut failed_requirements);
1090
1091            requirements.sort();
1092
1093            requirements
1094        } else {
1095            vec![]
1096        }
1097    }
1098
1099    pub fn get_qualities(&self) -> Vec<Quality> {
1100        let throne_context = &self.throne_context;
1101
1102        let mut qualities =
1103            if let Some(quality_atom) = throne_context.str_to_existing_atom("quality") {
1104                throne_context
1105                    .find_phrases_exactly3(Some(&quality_atom), None, None)
1106                    .iter()
1107                    .map(|p| {
1108                        let id_atom = p[1].atom;
1109                        let id = throne_context.atom_to_str(id_atom).to_string();
1110                        let value = throne_context.atom_to_number(p[2].atom).unwrap();
1111
1112                        let properties = self.quality_properties.get(&id);
1113                        let title = properties.and_then(|properties| properties.pluralized_title());
1114                        let description = properties
1115                            .and_then(|properties| properties.description_for_level(value));
1116
1117                        Quality {
1118                            id,
1119                            value,
1120                            title,
1121                            description: description.map(|s| format_description(&s)),
1122                        }
1123                    })
1124                    .collect()
1125            } else {
1126                vec![]
1127            };
1128
1129        qualities.sort();
1130
1131        qualities
1132    }
1133
1134    pub fn get_quality_properties(&self, id: &str) -> Option<&QualityProperty> {
1135        self.quality_properties.get(id)
1136    }
1137
1138    fn reset_state(&mut self) {
1139        let throne_context = &mut self.throne_context;
1140
1141        // retain quality state
1142        let quality_atom = throne_context.str_to_atom("quality");
1143
1144        let mut new_state = throne::State::new();
1145        for phrase_id in throne_context.core.state.iter() {
1146            let p = throne_context.core.state.get(phrase_id);
1147            if p[0].atom == quality_atom {
1148                new_state.push(p.to_vec());
1149            }
1150        }
1151        throne_context.core.state = new_state;
1152    }
1153}
1154
1155impl Script {
1156    pub fn from_text(text: &str) -> Self {
1157        let text = expand_concise_syntax(text);
1158        Self::from_throne_text(&text)
1159    }
1160
1161    fn from_throne_text(text: &str) -> Self {
1162        let base_txt = include_str!("storylets.throne");
1163        let quality_properties = read_quality_properties(&text);
1164
1165        let throne_context = throne::ContextBuilder::new()
1166            .text(&(text.to_string() + "\n" + base_txt))
1167            .build();
1168
1169        let mut s = DefaultHasher::new();
1170        text.hash(&mut s);
1171        let id = s.finish();
1172
1173        Self {
1174            id,
1175            throne_context,
1176            quality_properties,
1177        }
1178    }
1179}
1180
1181fn read_quality_properties(txt: &str) -> HashMap<String, QualityProperty> {
1182    let quality_regex = RegexBuilder::new(r"^\s*<<(quality[\w-]+) ([\w-]*).*$")
1183        .multi_line(true)
1184        .build()
1185        .unwrap();
1186
1187    let mut all_quality_properties: HashMap<String, QualityProperty> = HashMap::new();
1188    for caps in quality_regex.captures_iter(txt) {
1189        let quality_id = caps.get(2).unwrap().as_str();
1190        let quality_properties = all_quality_properties
1191            .entry(quality_id.to_string())
1192            .or_insert(Default::default());
1193
1194        let property_type = caps.get(1).unwrap().as_str();
1195        let split = caps.get(0).unwrap().as_str().split(" ").collect::<Vec<_>>();
1196
1197        let get_final_string = |start_idx| {
1198            split
1199                .get(start_idx..)
1200                .filter(|s| s.len() > 0)
1201                .map(|s| s.join(" ").trim_matches('`').to_string())
1202        };
1203
1204        match property_type {
1205            "quality-title" => {
1206                quality_properties.title = Some(get_final_string(2).expect(&format!(
1207                    "Missing title for quality-title for quality '{}'",
1208                    quality_id
1209                )));
1210            }
1211            "quality-level-description" => {
1212                quality_properties
1213                    .level_descriptions
1214                    .push(QualityLevelDescription {
1215                        level: split
1216                            .get(2)
1217                            .and_then(|s| i32::from_str(s).ok())
1218                            .expect(&format!(
1219                                "Missing level for quality-level-description for \
1220                 quality '{}'",
1221                                quality_id
1222                            )),
1223                        description: get_final_string(3).expect(&format!(
1224                            "Missing description for quality-level-description for \
1225               quality '{}'",
1226                            quality_id
1227                        )),
1228                    });
1229            }
1230            "quality-level-change-description" => {
1231                quality_properties
1232                    .level_change_descriptions
1233                    .push(QualityLevelChangeDescription {
1234                        level: split
1235                            .get(2)
1236                            .and_then(|s| i32::from_str(s).ok())
1237                            .expect(&format!(
1238                                "Missing level for quality-level-change-description for \
1239                 quality '{}'",
1240                                quality_id
1241                            )),
1242                        description: get_final_string(3).expect(&format!(
1243                            "Missing description for quality-level-change-description \
1244               for quality '{}'",
1245                            quality_id
1246                        )),
1247                    });
1248            }
1249            "quality-hide-level" => quality_properties.hide_level = true,
1250            "quality-hide-level-change" => quality_properties.hide_level_change = true,
1251            "quality-is-thing" => quality_properties.is_thing = true,
1252            "quality-is-global" => quality_properties.is_global = true,
1253            _ => unreachable!("Unhandled quality property type: {}", property_type),
1254        }
1255    }
1256
1257    let mut quality_properties = HashMap::new();
1258    for (quality_id, mut properties) in all_quality_properties {
1259        properties.level_descriptions.sort_by_key(|d| d.level);
1260        properties
1261            .level_change_descriptions
1262            .sort_by_key(|d| d.level);
1263
1264        quality_properties.insert(quality_id, properties);
1265    }
1266    quality_properties
1267}
1268
1269fn expand_concise_syntax(text: &str) -> String {
1270    let mut out_lines = vec![];
1271
1272    enum CurrentType {
1273        Card,
1274        Branch,
1275        Result,
1276        Quality,
1277    }
1278
1279    impl CurrentType {
1280        fn prefix(&self) -> &'static str {
1281            match self {
1282                CurrentType::Card => "card",
1283                CurrentType::Branch => "branch",
1284                CurrentType::Result => "result",
1285                CurrentType::Quality => "quality",
1286            }
1287        }
1288    }
1289
1290    let mut current_type: Option<CurrentType> = None;
1291    let mut current_type_id: Option<String> = None;
1292    let mut current_card_id: Option<String> = None;
1293    let mut current_branch_id: Option<String> = None;
1294
1295    let mut id_counter = 0;
1296
1297    let banned_id_chars_re = Regex::new(r"[^a-zA-Z0-9'_-]").unwrap();
1298
1299    let generate_id = |current_type: &Option<CurrentType>, id_counter: &mut i32| {
1300        let prefix = current_type
1301            .as_ref()
1302            .map(|t| t.prefix())
1303            .expect("Missing a type declaration");
1304
1305        *id_counter += 1;
1306        format!("{}-{}", prefix, *id_counter)
1307    };
1308
1309    let write_relation = |current_type: &Option<CurrentType>,
1310                          current_type_id: &Option<String>,
1311                          current_card_id: &Option<String>,
1312                          current_branch_id: &Option<String>,
1313                          out_lines: &mut Vec<String>| {
1314        let current_type_id = current_type_id.as_ref().expect("Missing type id");
1315
1316        match current_type {
1317            Some(CurrentType::Branch) => {
1318                out_lines.push(format!(
1319                    "<<card-branch {} {}",
1320                    current_card_id.as_ref().expect(&format!(
1321                        "Branch '{}' must be preceded by a card declaration",
1322                        current_type_id
1323                    )),
1324                    current_type_id
1325                ));
1326            }
1327            Some(CurrentType::Result) => {
1328                out_lines.push(format!(
1329                    "<<branch-result {} {}",
1330                    current_branch_id.as_ref().expect(&format!(
1331                        "Result '{}' must be preceded by a branch declaration",
1332                        current_type_id
1333                    )),
1334                    current_type_id
1335                ));
1336            }
1337            _ => (),
1338        }
1339    };
1340
1341    for (line_i, l) in text.lines().map(|l| l.trim()).enumerate() {
1342        let mut split = l.split_whitespace();
1343
1344        match split.next() {
1345            Some("card") => {
1346                current_type = Some(CurrentType::Card);
1347                current_type_id = split.next().map(|s| s.to_string());
1348                current_card_id = current_type_id.clone();
1349            }
1350            Some("branch") => {
1351                current_type = Some(CurrentType::Branch);
1352                current_type_id = split.next().map(|s| s.to_string());
1353                if current_type_id.is_some() {
1354                    write_relation(
1355                        &current_type,
1356                        &current_type_id,
1357                        &current_card_id,
1358                        &current_branch_id,
1359                        &mut out_lines,
1360                    );
1361                }
1362                current_branch_id = current_type_id.clone();
1363            }
1364            Some("result") => {
1365                current_type = Some(CurrentType::Result);
1366                current_type_id = split.next().map(|s| s.to_string());
1367                if current_type_id.is_some() {
1368                    write_relation(
1369                        &current_type,
1370                        &current_type_id,
1371                        &current_card_id,
1372                        &current_branch_id,
1373                        &mut out_lines,
1374                    );
1375                }
1376            }
1377            Some("quality") => {
1378                current_type = Some(CurrentType::Quality);
1379                current_type_id = split.next().map(|s| s.to_string());
1380            }
1381            Some("title") => {
1382                let prefix = current_type.as_ref().map(|t| t.prefix()).expect(&format!(
1383                    "Line {}: Missing preceding type declaration",
1384                    line_i,
1385                ));
1386
1387                let title = l
1388                    .get("title".len()..)
1389                    .map(|s| s.trim())
1390                    .filter(|s| s.len() > 0)
1391                    .expect(&format!("Line {}: Missing text for title", line_i));
1392
1393                if current_type_id.is_none() {
1394                    let decoded = unidecode(title).to_lowercase();
1395                    let generated_id = banned_id_chars_re.replace_all(&decoded, "-");
1396
1397                    id_counter += 1;
1398                    current_type_id = Some(format!("{}-{}", generated_id, id_counter));
1399
1400                    match current_type {
1401                        Some(CurrentType::Card) => {
1402                            current_card_id = current_type_id.clone();
1403                        }
1404                        Some(CurrentType::Branch) => {
1405                            current_branch_id = current_type_id.clone();
1406                        }
1407                        _ => (),
1408                    }
1409
1410                    write_relation(
1411                        &current_type,
1412                        &current_type_id,
1413                        &current_card_id,
1414                        &current_branch_id,
1415                        &mut out_lines,
1416                    );
1417                }
1418
1419                out_lines.push(format!(
1420                    "<<{}-title {} `{}`",
1421                    prefix,
1422                    current_type_id.as_ref().unwrap(),
1423                    title
1424                ));
1425            }
1426            Some("description") => {
1427                let prefix = current_type.as_ref().map(|t| t.prefix()).expect(&format!(
1428                    "Line {}: Missing preceding type declaration",
1429                    line_i,
1430                ));
1431
1432                let description = l
1433                    .get("description".len()..)
1434                    .map(|s| s.trim())
1435                    .filter(|s| s.len() > 0)
1436                    .expect(&format!("Line {}: Missing text for description", line_i));
1437
1438                if current_type_id.is_none() {
1439                    current_type_id = Some(generate_id(&current_type, &mut id_counter));
1440
1441                    match current_type {
1442                        Some(CurrentType::Card) => {
1443                            current_card_id = current_type_id.clone();
1444                        }
1445                        Some(CurrentType::Branch) => {
1446                            current_branch_id = current_type_id.clone();
1447                        }
1448                        _ => (),
1449                    }
1450
1451                    write_relation(
1452                        &current_type,
1453                        &current_type_id,
1454                        &current_card_id,
1455                        &current_branch_id,
1456                        &mut out_lines,
1457                    );
1458                }
1459
1460                out_lines.push(format!(
1461                    "<<{}-description {} `{}`",
1462                    prefix,
1463                    current_type_id.as_ref().unwrap(),
1464                    description
1465                ));
1466            }
1467            Some(command @ "level-description") | Some(command @ "level-change-description") => {
1468                let prefix = current_type.as_ref().map(|t| t.prefix()).expect(&format!(
1469                    "Line {}: Missing preceding type declaration",
1470                    line_i,
1471                ));
1472
1473                let n = split
1474                    .next()
1475                    .expect(&format!("Line {}: Missing level for {}", line_i, command));
1476
1477                let description = split.collect::<Vec<_>>().join(" ");
1478                if description.is_empty() {
1479                    panic!("Line {}: Missing description for {}", line_i, command);
1480                }
1481
1482                if current_type_id.is_none() {
1483                    current_type_id = Some(generate_id(&current_type, &mut id_counter));
1484                }
1485
1486                out_lines.push(format!(
1487                    "<<{}-{} {} {} `{}`",
1488                    prefix,
1489                    command,
1490                    current_type_id.as_ref().unwrap(),
1491                    n,
1492                    description
1493                ));
1494            }
1495            Some(command) => {
1496                let prefix = current_type.as_ref().map(|t| t.prefix()).expect(&format!(
1497                    "Line {}: Missing preceding type declaration",
1498                    line_i,
1499                ));
1500
1501                if current_type_id.is_none() {
1502                    current_type_id = Some(generate_id(&current_type, &mut id_counter));
1503
1504                    match current_type {
1505                        Some(CurrentType::Card) => {
1506                            current_card_id = current_type_id.clone();
1507                        }
1508                        Some(CurrentType::Branch) => {
1509                            current_branch_id = current_type_id.clone();
1510                        }
1511                        _ => (),
1512                    }
1513
1514                    write_relation(
1515                        &current_type,
1516                        &current_type_id,
1517                        &current_card_id,
1518                        &current_branch_id,
1519                        &mut out_lines,
1520                    );
1521                }
1522
1523                out_lines.push(
1524                    format!(
1525                        "<<{}-{} {} {}",
1526                        prefix,
1527                        command,
1528                        current_type_id.as_ref().unwrap(),
1529                        split.collect::<Vec<_>>().join(" ")
1530                    )
1531                    .trim()
1532                    .to_string(),
1533                );
1534            }
1535            None => (),
1536        }
1537    }
1538
1539    out_lines.join("\n")
1540}
1541
1542fn format_description(s: &str) -> String {
1543    s.replace("\\n", "\n")
1544}
1545
1546#[cfg(test)]
1547mod tests {
1548    use super::*;
1549
1550    #[test]
1551    fn test_set_quality() {
1552        let mut context = Context::from_throne_text("<<quality-title test `Test`");
1553        context.throne_context.print();
1554        context.set_quality("test", 2);
1555
1556        assert_eq!(
1557            context.get_qualities(),
1558            vec![Quality {
1559                id: "test".to_string(),
1560                value: 2,
1561                title: Some("Test".to_string()),
1562                description: Some("Test: 2".to_string()),
1563            }]
1564        );
1565
1566        let result = context.set_quality("test", 0);
1567
1568        assert_eq!(
1569            result,
1570            Some(ChangeQualityEffect {
1571                quality: "test".to_string(),
1572                diff: -2,
1573                value: 0,
1574                description: Some("Your 'Test' quality decreased by 2 (new level: 0)".to_string())
1575            })
1576        );
1577        assert_eq!(context.get_qualities(), vec![]);
1578    }
1579
1580    #[test]
1581    fn test_change_quality() {
1582        let mut context = Context::from_throne_text("<<quality-title test `Test`");
1583        context.throne_context.print();
1584        context.change_quality("test", 2);
1585        context.change_quality("test", 3);
1586
1587        assert_eq!(
1588            context.get_qualities(),
1589            vec![Quality {
1590                id: "test".to_string(),
1591                value: 5,
1592                title: Some("Test".to_string()),
1593                description: Some("Test: 5".to_string())
1594            }]
1595        );
1596
1597        let result = context.change_quality("test", -6);
1598
1599        assert_eq!(
1600            result,
1601            Some(ChangeQualityEffect {
1602                quality: "test".to_string(),
1603                diff: -5,
1604                value: 0,
1605                description: Some("Your 'Test' quality decreased by 5 (new level: 0)".to_string())
1606            })
1607        );
1608        assert_eq!(context.get_qualities(), vec![]);
1609    }
1610
1611    #[test]
1612    fn test_populate_cards() {
1613        let mut context = Context::from_throne_text(include_str!("basic-test.throne"));
1614
1615        context.draw_cards();
1616
1617        context.throne_context.print();
1618
1619        let cards = context.get_cards();
1620        assert_eq!(
1621            cards,
1622            vec![
1623                Card {
1624                    id: "treasure".to_string(),
1625                    title: "Treasure for all".to_string(),
1626                    description: "You see a gleaming metal chest".to_string(),
1627                    requirements: vec![CardRequirement {
1628                        quality: "treasure-open".to_string(),
1629                        failed: false
1630                    }],
1631                    branches: vec![
1632                        Branch {
1633                            id: "kick-chest".to_string(),
1634                            title: "Kick the chest".to_string(),
1635                            description: "It looks very sturdy, but then again you're very strong."
1636                                .to_string(),
1637                            failed: true,
1638                            requirements: vec![BranchRequirement {
1639                                quality: "strength".to_string(),
1640                                condition: RequirementCondition::Gt(2),
1641                                failed: true
1642                            }],
1643                            result_weights: vec![],
1644                        },
1645                        Branch {
1646                            id: "open-chest".to_string(),
1647                            title: "Open the chest".to_string(),
1648                            description: "The obvious choice - you'd be a fool not to \
1649                            take the opportunity."
1650                                .to_string(),
1651                            failed: false,
1652                            requirements: vec![],
1653                            result_weights: vec![],
1654                        }
1655                    ],
1656                },
1657                Card {
1658                    id: "waterfall".to_string(),
1659                    title: "Angel Falls".to_string(),
1660                    description: "The waterfall is crashing down and your throat is parched."
1661                        .to_string(),
1662                    requirements: vec![],
1663                    branches: vec![Branch {
1664                        id: "drink".to_string(),
1665                        title: "Satisfy your thirst".to_string(),
1666                        description:
1667                            "The water looks crystal clear, but is it really safe to drink."
1668                                .to_string(),
1669                        failed: false,
1670                        requirements: vec![],
1671                        result_weights: vec![],
1672                    }]
1673                }
1674            ]
1675        );
1676    }
1677
1678    #[test]
1679    fn test_card_quality_missing_failed() {
1680        let mut context = Context::from_throne_text(include_str!("basic-test.throne"));
1681
1682        context.set_quality("treasure-open", 1);
1683        context.draw_cards();
1684
1685        context.throne_context.print();
1686
1687        let cards = context.get_cards();
1688        assert_eq!(
1689            cards,
1690            vec![Card {
1691                id: "waterfall".to_string(),
1692                title: "Angel Falls".to_string(),
1693                description: "The waterfall is crashing down and your throat is parched."
1694                    .to_string(),
1695                requirements: vec![],
1696                branches: vec![Branch {
1697                    id: "drink".to_string(),
1698                    title: "Satisfy your thirst".to_string(),
1699                    description: "The water looks crystal clear, but is it really safe to drink."
1700                        .to_string(),
1701                    failed: false,
1702                    requirements: vec![],
1703                    result_weights: vec![],
1704                }]
1705            }]
1706        );
1707
1708        let a1 = context.throne_context.str_to_atom("card");
1709        let a2 = context.throne_context.str_to_atom("treasure");
1710        let a3 = context.throne_context.str_to_atom("failed-quality");
1711        let a4 = context.throne_context.str_to_atom("missing");
1712        let a5 = context.throne_context.str_to_atom("treasure-open");
1713        assert_eq!(
1714            context
1715                .throne_context
1716                .find_phrases_exactly5(Some(&a1), Some(&a2), Some(&a3), Some(&a4), Some(&a5))
1717                .len(),
1718            1
1719        );
1720    }
1721
1722    #[test]
1723    fn test_branch_quality_gt_failed() {
1724        let mut context = Context::from_throne_text(include_str!("basic-test.throne"));
1725
1726        context.set_quality("strength", 2);
1727        context.draw_cards();
1728
1729        context.throne_context.print();
1730
1731        let branches = context.get_branches("treasure");
1732        assert_eq!(
1733            branches.iter().find(|b| b.id == "kick-chest"),
1734            Some(&Branch {
1735                id: "kick-chest".to_string(),
1736                title: "Kick the chest".to_string(),
1737                description: "It looks very sturdy, but then again you're very strong.".to_string(),
1738                failed: true,
1739                requirements: vec![BranchRequirement {
1740                    quality: "strength".to_string(),
1741                    condition: RequirementCondition::Gt(2),
1742                    failed: true
1743                }],
1744                result_weights: vec![],
1745            })
1746        );
1747    }
1748
1749    #[test]
1750    fn test_branch_quality_gt_failed_message() {
1751        let mut context =
1752            Context::from_throne_text(include_str!("requirement-messages-test.throne"));
1753
1754        context.set_quality("strength", 2);
1755        context.draw_cards();
1756
1757        context.throne_context.print();
1758
1759        let branches = context.get_branches("treasure");
1760        let branch = branches
1761            .iter()
1762            .find(|b| b.id == "kick-chest")
1763            .expect("Branch not found");
1764        assert_eq!(
1765            branch.get_requirement_descriptions(&context),
1766            vec!["Your 'Strength' quality must be greater than 2"]
1767        );
1768    }
1769
1770    #[test]
1771    fn test_branch_quality_gt_passed() {
1772        let mut context = Context::from_throne_text(include_str!("basic-test.throne"));
1773
1774        context.set_quality("strength", 3);
1775        context.draw_cards();
1776
1777        context.throne_context.print();
1778
1779        let branches = context.get_branches("treasure");
1780        assert_eq!(
1781            branches.iter().find(|b| b.id == "kick-chest"),
1782            Some(&Branch {
1783                id: "kick-chest".to_string(),
1784                title: "Kick the chest".to_string(),
1785                description: "It looks very sturdy, but then again you're very strong.".to_string(),
1786                failed: false,
1787                requirements: vec![BranchRequirement {
1788                    quality: "strength".to_string(),
1789                    condition: RequirementCondition::Gt(2),
1790                    failed: false
1791                }],
1792                result_weights: vec![],
1793            })
1794        );
1795    }
1796
1797    #[test]
1798    fn test_select_branch_set_quality() {
1799        let mut context = Context::from_throne_text(include_str!("basic-test.throne"));
1800
1801        context.draw_cards();
1802
1803        context.throne_context.print();
1804
1805        assert_eq!(context.get_qualities(), vec![]);
1806
1807        let branch_result = context.select_branch("open-chest");
1808        assert_eq!(branch_result.id, "success");
1809        assert_eq!(
1810            branch_result.effects,
1811            vec![BranchResultEffect::QualityChanged {
1812                quality: "treasure-open".to_string(),
1813                diff: 1,
1814                value: 1,
1815                description: None
1816            }]
1817        );
1818
1819        context.throne_context.print();
1820
1821        assert_eq!(
1822            context.get_qualities(),
1823            vec![Quality {
1824                id: "treasure-open".to_string(),
1825                value: 1,
1826                title: None,
1827                description: None,
1828            }]
1829        );
1830    }
1831
1832    #[test]
1833    fn test_select_branch_change_quality() {
1834        let mut context = Context::from_throne_text(include_str!("quality-change-test.throne"));
1835
1836        context.draw_cards();
1837
1838        context.throne_context.print();
1839
1840        assert_eq!(
1841            context.get_qualities(),
1842            vec![Quality {
1843                id: "jewel-bags".to_string(),
1844                value: 1,
1845                title: None,
1846                description: None
1847            }]
1848        );
1849
1850        let branch_result = context.select_branch("open-chest");
1851
1852        context.throne_context.print();
1853
1854        assert_eq!(branch_result.id, "open-success");
1855        assert_eq!(
1856            branch_result.effects,
1857            vec![BranchResultEffect::QualityChanged {
1858                quality: "jewel-bags".to_string(),
1859                diff: 1,
1860                value: 2,
1861                description: None
1862            }]
1863        );
1864
1865        context.draw_cards();
1866
1867        let branch_result = context.select_branch("kick-chest");
1868
1869        context.throne_context.print();
1870
1871        assert_eq!(branch_result.id, "kick-fail");
1872        assert_eq!(
1873            branch_result.effects,
1874            vec![BranchResultEffect::QualityChanged {
1875                quality: "jewel-bags".to_string(),
1876                diff: -2,
1877                value: 0,
1878                description: None
1879            }]
1880        );
1881
1882        assert_eq!(context.get_qualities(), vec![]);
1883    }
1884
1885    #[test]
1886    fn test_select_branch_add_phrase() {
1887        let mut context = Context::from_throne_text(include_str!("add-phrase-test.throne"));
1888
1889        context.draw_cards();
1890
1891        let branch_result = context.select_branch("open-chest");
1892
1893        context.throne_context.print();
1894
1895        assert_eq!(branch_result.id, "success");
1896        assert_eq!(
1897            branch_result.effects,
1898            vec![BranchResultEffect::AddPhrase {
1899                phrase: throne::tokenize(
1900                    "open-treasure (1 2 3)",
1901                    &mut context.throne_context.string_cache
1902                ),
1903                phrase_string: "(open-treasure (1 2 3))".to_string()
1904            }]
1905        );
1906    }
1907
1908    #[test]
1909    fn test_quality_descriptions() {
1910        let mut context =
1911            Context::from_throne_text(include_str!("quality-description-test.throne"));
1912
1913        context.set_quality("season", 1);
1914        context.set_quality("strength", 1);
1915        context.set_quality("jewels", 1);
1916
1917        context.throne_context.print();
1918
1919        assert_eq!(
1920            context.get_qualities(),
1921            vec![
1922                Quality {
1923                    id: "jewels".to_string(),
1924                    value: 1,
1925                    title: Some("Jewels".to_string()),
1926                    description: Some("1 Jewel".to_string()),
1927                },
1928                Quality {
1929                    id: "season".to_string(),
1930                    value: 1,
1931                    title: Some("Current season".to_string()),
1932                    description: Some("Current season: Spring".to_string()),
1933                },
1934                Quality {
1935                    id: "strength".to_string(),
1936                    value: 1,
1937                    title: Some("Strength".to_string()),
1938                    description: Some("Strength: 1".to_string()),
1939                },
1940            ]
1941        );
1942
1943        context.set_quality("season", 2);
1944        context.set_quality("strength", 2);
1945        context.set_quality("jewels", 2);
1946
1947        context.throne_context.print();
1948
1949        assert_eq!(
1950            context.get_qualities(),
1951            vec![
1952                Quality {
1953                    id: "jewels".to_string(),
1954                    value: 2,
1955                    title: Some("Jewels".to_string()),
1956                    description: Some("2 Jewels".to_string()),
1957                },
1958                Quality {
1959                    id: "season".to_string(),
1960                    value: 2,
1961                    title: Some("Current season".to_string()),
1962                    description: Some("Current season: Summer".to_string()),
1963                },
1964                Quality {
1965                    id: "strength".to_string(),
1966                    value: 2,
1967                    title: Some("Strength".to_string()),
1968                    description: Some("Strength: 2".to_string()),
1969                },
1970            ]
1971        );
1972    }
1973
1974    #[test]
1975    fn test_quality_change_descriptions_increase() {
1976        let mut context =
1977            Context::from_throne_text(include_str!("quality-description-test.throne"));
1978
1979        context.draw_cards();
1980
1981        let result = context.select_branch("increase-season");
1982
1983        context.throne_context.print();
1984
1985        assert_eq!(
1986            result.effects,
1987            vec![BranchResultEffect::QualityChanged {
1988                quality: "season".to_string(),
1989                diff: 1,
1990                value: 1,
1991                description: Some("Spring is upon us".to_string())
1992            }]
1993        );
1994
1995        context.draw_cards();
1996
1997        let result = context.select_branch("increase-strength");
1998
1999        context.throne_context.print();
2000
2001        assert_eq!(
2002            result.effects,
2003            vec![BranchResultEffect::QualityChanged {
2004                quality: "strength".to_string(),
2005                diff: 1,
2006                value: 1,
2007                description: Some("Your 'Strength' quality is now 1".to_string())
2008            }]
2009        );
2010
2011        context.draw_cards();
2012
2013        let result = context.select_branch("increase-jewels");
2014
2015        context.throne_context.print();
2016
2017        assert_eq!(
2018            result.effects,
2019            vec![BranchResultEffect::QualityChanged {
2020                quality: "jewels".to_string(),
2021                diff: 1,
2022                value: 1,
2023                description: Some("You now have 1 Jewel".to_string())
2024            }]
2025        );
2026
2027        context.draw_cards();
2028
2029        let result = context.select_branch("increase-season");
2030
2031        context.throne_context.print();
2032
2033        assert_eq!(
2034            result.effects,
2035            vec![BranchResultEffect::QualityChanged {
2036                quality: "season".to_string(),
2037                diff: 1,
2038                value: 2,
2039                description: Some("Summer is upon us".to_string())
2040            }]
2041        );
2042
2043        context.draw_cards();
2044
2045        let result = context.select_branch("increase-strength");
2046
2047        context.throne_context.print();
2048
2049        assert_eq!(
2050            result.effects,
2051            vec![BranchResultEffect::QualityChanged {
2052                quality: "strength".to_string(),
2053                diff: 1,
2054                value: 2,
2055                description: Some(
2056                    "Your 'Strength' quality increased by 1 (new level: 2)".to_string()
2057                )
2058            }]
2059        );
2060
2061        context.draw_cards();
2062
2063        let result = context.select_branch("increase-jewels");
2064
2065        context.throne_context.print();
2066
2067        assert_eq!(
2068            result.effects,
2069            vec![BranchResultEffect::QualityChanged {
2070                quality: "jewels".to_string(),
2071                diff: 1,
2072                value: 2,
2073                description: Some("You gained 1 Jewel (new total: 2)".to_string())
2074            }]
2075        );
2076
2077        context.draw_cards();
2078
2079        let result = context.select_branch("increase-season-2");
2080
2081        context.throne_context.print();
2082
2083        assert_eq!(
2084            result.effects,
2085            vec![BranchResultEffect::QualityChanged {
2086                quality: "season".to_string(),
2087                diff: 2,
2088                value: 4,
2089                description: Some("Winter is upon us".to_string())
2090            }]
2091        );
2092
2093        context.draw_cards();
2094
2095        let result = context.select_branch("increase-strength-2");
2096
2097        context.throne_context.print();
2098
2099        assert_eq!(
2100            result.effects,
2101            vec![BranchResultEffect::QualityChanged {
2102                quality: "strength".to_string(),
2103                diff: 2,
2104                value: 4,
2105                description: Some("You're slightly stronger (new level: 4)".to_string())
2106            }]
2107        );
2108
2109        context.draw_cards();
2110
2111        let result = context.select_branch("increase-jewels-2");
2112
2113        context.throne_context.print();
2114
2115        assert_eq!(
2116            result.effects,
2117            vec![BranchResultEffect::QualityChanged {
2118                quality: "jewels".to_string(),
2119                diff: 2,
2120                value: 4,
2121                description: Some("You gained 2 Jewels (new total: 4)".to_string())
2122            }]
2123        );
2124    }
2125
2126    #[test]
2127    fn test_quality_change_descriptions_decrease() {
2128        let mut context =
2129            Context::from_throne_text(include_str!("quality-description-test.throne"));
2130
2131        context.set_quality("season", 4);
2132        context.set_quality("strength", 4);
2133        context.set_quality("jewels", 4);
2134
2135        context.draw_cards();
2136
2137        let result = context.select_branch("decrease-season-2");
2138
2139        context.throne_context.print();
2140
2141        assert_eq!(
2142            result.effects,
2143            vec![BranchResultEffect::QualityChanged {
2144                quality: "season".to_string(),
2145                diff: -2,
2146                value: 2,
2147                description: Some("Summer is upon us".to_string())
2148            }]
2149        );
2150
2151        context.draw_cards();
2152
2153        let result = context.select_branch("decrease-strength-2");
2154
2155        context.throne_context.print();
2156
2157        assert_eq!(
2158            result.effects,
2159            vec![BranchResultEffect::QualityChanged {
2160                quality: "strength".to_string(),
2161                diff: -2,
2162                value: 2,
2163                description: Some(
2164                    "Your 'Strength' quality decreased by 2 (new level: 2)".to_string()
2165                )
2166            }]
2167        );
2168
2169        context.draw_cards();
2170
2171        let result = context.select_branch("decrease-jewels-2");
2172
2173        context.throne_context.print();
2174
2175        assert_eq!(
2176            result.effects,
2177            vec![BranchResultEffect::QualityChanged {
2178                quality: "jewels".to_string(),
2179                diff: -2,
2180                value: 2,
2181                description: Some("You lost 2 Jewels (new total: 2)".to_string())
2182            }]
2183        );
2184    }
2185
2186    #[test]
2187    fn test_quality_change_descriptions_lose() {
2188        let mut context =
2189            Context::from_throne_text(include_str!("quality-description-test.throne"));
2190
2191        context.set_quality("season", 2);
2192        context.set_quality("strength", 2);
2193        context.set_quality("jewels", 2);
2194
2195        context.draw_cards();
2196
2197        let result = context.select_branch("decrease-season-2");
2198
2199        context.throne_context.print();
2200
2201        assert_eq!(
2202            result.effects,
2203            vec![BranchResultEffect::QualityChanged {
2204                quality: "season".to_string(),
2205                diff: -2,
2206                value: 0,
2207                description: Some("The seasons no longer pass".to_string())
2208            }]
2209        );
2210
2211        context.draw_cards();
2212
2213        let result = context.select_branch("decrease-strength-2");
2214
2215        context.throne_context.print();
2216
2217        assert_eq!(
2218            result.effects,
2219            vec![BranchResultEffect::QualityChanged {
2220                quality: "strength".to_string(),
2221                diff: -2,
2222                value: 0,
2223                description: Some(
2224                    "Your 'Strength' quality decreased by 2 (new level: 0)".to_string()
2225                )
2226            }]
2227        );
2228
2229        context.draw_cards();
2230
2231        let result = context.select_branch("decrease-jewels-2");
2232
2233        context.throne_context.print();
2234
2235        assert_eq!(
2236            result.effects,
2237            vec![BranchResultEffect::QualityChanged {
2238                quality: "jewels".to_string(),
2239                diff: -2,
2240                value: 0,
2241                description: Some("You lost 2 Jewels (new total: 0)".to_string())
2242            }]
2243        );
2244    }
2245
2246    #[test]
2247    fn test_urgency() {
2248        let mut context = Context::from_throne_text(include_str!("urgency-test.throne"));
2249
2250        context.draw_cards();
2251
2252        context.throne_context.print();
2253
2254        let cards = context.get_cards();
2255        assert_eq!(cards.len(), 1);
2256        assert_eq!(cards[0].id, "waterfall".to_string());
2257
2258        let result = context.select_branch("continue");
2259
2260        context.throne_context.print();
2261
2262        assert_eq!(result.id, "passed");
2263
2264        context.draw_cards();
2265
2266        context.throne_context.print();
2267
2268        let cards = context.get_cards();
2269        assert_eq!(cards.len(), 2);
2270        assert_eq!(
2271            cards.iter().map(|c| c.id.clone()).collect::<Vec<_>>(),
2272            vec!["treasure".to_string(), "treasure2".to_string()]
2273        );
2274    }
2275
2276    #[test]
2277    fn test_result_requirements() {
2278        let mut context = Context::from_throne_text(include_str!("result-requirement-test.throne"));
2279
2280        context.draw_cards();
2281        let result = context.select_branch("open-chest");
2282
2283        context.throne_context.print();
2284
2285        assert_eq!(result.id, "pass");
2286
2287        context.set_quality("force-fail", 1);
2288        context.draw_cards();
2289        let result = context.select_branch("open-chest");
2290
2291        context.throne_context.print();
2292
2293        assert_eq!(result.id, "fail");
2294    }
2295
2296    #[test]
2297    fn test_concise_syntax() {
2298        let actual = expand_concise_syntax(include_str!("concise-test.throne"));
2299
2300        let expected =
2301"<<quality-is-thing quality-1
2302<<quality-level-change-description quality-1 3 `Autumn is upon us`
2303<<card-title treasure `Treasure for all`
2304<<card-description treasure `You see a gleaming metal chest`
2305<<card-required-quality-missing treasure treasure-open
2306<<card-branch treasure open-the-chest-2
2307<<branch-title open-the-chest-2 `Open the chest`
2308<<branch-description open-the-chest-2 `The obvious choice - you\'d be a fool not to take the opportunity.`
2309<<branch-result open-the-chest-2 you\'re-rich--3
2310<<result-title you\'re-rich--3 `You\'re rich!`
2311<<result-description you\'re-rich--3 `The chest is filled with gold and precious jewels.`
2312<<result-set-quality you\'re-rich--3 treasure-open 1
2313<<card-branch treasure kick-the-chest-4
2314<<branch-title kick-the-chest-4 `Kick the chest`
2315<<branch-description kick-the-chest-4 `It looks very sturdy, but then again you\'re very strong.`
2316<<branch-required-quality-gt kick-the-chest-4 strength 2
2317<<card-title angel-falls-5 `Angel Falls`
2318<<card-description angel-falls-5 `The waterfall is crashing down and your throat is parched.`
2319<<card-branch angel-falls-5 satisfy-your-thirst-6
2320<<branch-title satisfy-your-thirst-6 `Satisfy your thirst`
2321<<branch-description satisfy-your-thirst-6 `The water looks crystal clear, but is it really safe to drink.`";
2322
2323        assert_eq!(actual, expected);
2324    }
2325
2326    #[test]
2327    fn test_difficulty_probability() {
2328        let mut context = Context::from_throne_text(include_str!("difficulty-test.throne"));
2329
2330        context.draw_cards();
2331
2332        let cards = context.get_cards();
2333        let branch = &cards[0].branches[0];
2334
2335        context.throne_context.print();
2336
2337        assert_eq!(
2338            branch.result_weights,
2339            vec![ResultWeight {
2340                result: "fail".to_string(),
2341                probability: 99, // 1% is minimum,
2342                difficulty_quality: Some("strength".to_string()),
2343            }]
2344        );
2345
2346        context.set_quality("strength", 4);
2347        context.draw_cards();
2348
2349        let cards = context.get_cards();
2350        let branch = &cards[0].branches[0];
2351
2352        context.throne_context.print();
2353
2354        assert_eq!(
2355            branch.result_weights,
2356            vec![ResultWeight {
2357                result: "fail".to_string(),
2358                probability: 76,
2359                difficulty_quality: Some("strength".to_string())
2360            }]
2361        );
2362
2363        let descriptions = branch.get_difficulty_descriptions(&context);
2364        assert_eq!(
2365            descriptions,
2366            vec!["Your 'Strength' quality gives you a 24% chance of success".to_string()],
2367        );
2368    }
2369
2370    #[test]
2371    fn test_difficulty_distribution() {
2372        let mut context = Context::from_throne_text(include_str!("difficulty-test.throne"));
2373
2374        {
2375            let mut pass_count = 0;
2376            let mut fail_count = 0;
2377            for _ in 0..100 {
2378                context.draw_cards();
2379
2380                let result = context.select_branch("open-chest");
2381                if result.id == "pass" {
2382                    pass_count += 1;
2383                } else if result.id == "fail" {
2384                    fail_count += 1;
2385                }
2386            }
2387
2388            assert!(
2389                pass_count <= 3,
2390                format!("{} should be <= {}", pass_count, 3)
2391            );
2392            assert!(
2393                fail_count >= 100 - 3,
2394                format!("{} should be <= {}", fail_count, 100 - 3)
2395            );
2396        }
2397
2398        context.set_quality("strength", 4);
2399
2400        {
2401            let count = 500;
2402
2403            let mut pass_count = 0;
2404            let mut fail_count = 0;
2405            for _ in 0..count {
2406                context.draw_cards();
2407
2408                let result = context.select_branch("open-chest");
2409                if result.id == "pass" {
2410                    pass_count += 1;
2411                } else if result.id == "fail" {
2412                    fail_count += 1;
2413                }
2414            }
2415
2416            context.throne_context.print();
2417
2418            assert!(
2419                pass_count >= count * 21 / 100,
2420                format!("{} should be >= {}", pass_count, count * 21 / 100)
2421            );
2422            assert!(
2423                pass_count <= count * 27 / 100,
2424                format!("{} should be <= {}", pass_count, count * 27 / 100)
2425            );
2426
2427            assert!(
2428                fail_count >= count * (100 - 27) / 100,
2429                format!("{} should be >= {}", fail_count, count * (100 - 27) / 100)
2430            );
2431            assert!(
2432                fail_count <= count * (100 - 21) / 100,
2433                format!("{} should be <= {}", fail_count, count * (100 - 21) / 100)
2434            );
2435        }
2436    }
2437
2438    #[test]
2439    fn test_set_active_script() {
2440        let script1 = Script::from_throne_text(
2441            "
2442quality local 1
2443quality global 1
2444
2445<<quality-is-global global
2446",
2447        );
2448
2449        let script2 = Script::from_throne_text(
2450            "
2451quality other 1
2452",
2453        );
2454
2455        let mut context = Context::new();
2456
2457        context.set_active_script(&script1);
2458
2459        assert_eq!(
2460            context.get_qualities(),
2461            vec![
2462                Quality {
2463                    id: "global".to_string(),
2464                    value: 1,
2465                    title: None,
2466                    description: None
2467                },
2468                Quality {
2469                    id: "local".to_string(),
2470                    value: 1,
2471                    title: None,
2472                    description: None
2473                }
2474            ]
2475        );
2476
2477        context.set_active_script(&script2);
2478
2479        assert_eq!(
2480            context.get_qualities(),
2481            vec![
2482                Quality {
2483                    id: "global".to_string(),
2484                    value: 1,
2485                    title: None,
2486                    description: None,
2487                },
2488                Quality {
2489                    id: "other".to_string(),
2490                    value: 1,
2491                    title: None,
2492                    description: None
2493                }
2494            ]
2495        );
2496    }
2497}