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 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 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 ¤t_type,
1356 ¤t_type_id,
1357 ¤t_card_id,
1358 ¤t_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 ¤t_type,
1370 ¤t_type_id,
1371 ¤t_card_id,
1372 ¤t_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 ¤t_type,
1412 ¤t_type_id,
1413 ¤t_card_id,
1414 ¤t_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(¤t_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 ¤t_type,
1453 ¤t_type_id,
1454 ¤t_card_id,
1455 ¤t_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(¤t_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(¤t_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 ¤t_type,
1516 ¤t_type_id,
1517 ¤t_card_id,
1518 ¤t_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, 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}