fabulist_core/
engine.rs

1use std::ops::Deref;
2
3use crate::engine::engine_error::{EngineError, EngineErrorType};
4use crate::story::Story;
5use crate::story::story_node::{RcStoryNode, StoryNode};
6
7pub mod engine_error;
8
9pub type EngineResult = Result<(String, RcStoryNode), EngineError>;
10
11pub struct Engine {
12    story: Option<Story>,
13}
14
15impl Engine {
16    pub fn new() -> Self {
17        Self { story: None }
18    }
19    pub fn story(&self) -> &Option<Story> {
20        &self.story
21    }
22    pub fn set_story(&mut self, story: Story) {
23        self.story = Some(story);
24    }
25
26    pub fn reset(&mut self) -> Result<(), EngineError> {
27        let story = self.story.as_mut()
28            .ok_or(EngineError::new(EngineErrorType::NoStory))?;
29        story.set_current(story.start().clone());
30        Ok(())
31    }
32
33    pub fn start(&self) -> EngineResult {
34        let story = self.story.as_ref()
35            .ok_or(EngineError::new(EngineErrorType::NoStory))?;
36        let start_node_key = story.start();
37        let start_node = story.story_nodes().get(start_node_key)
38            .ok_or(EngineError::new(EngineErrorType::StoryNodeDne))?;
39        Ok((start_node_key.clone(), start_node.clone()))
40    }
41
42    pub fn next(&mut self, choice: Option<usize>) -> EngineResult {
43        let story = self.story.as_mut()
44            .ok_or(EngineError::new(EngineErrorType::NoStory))?;
45        let current_node_key = match story.current() {
46            Some(current_node) => current_node,
47            _ => story.start()
48        };
49        let story_context = story.story_context();
50        let get_node_from_key = |node_key: String|
51                                 -> Result<RcStoryNode, EngineError> {
52            Ok(story.story_nodes().get(&node_key)
53                .ok_or(EngineError::new(EngineErrorType::StoryNodeDne))?.clone())
54        };
55
56        // Unwraps node to find the first dialogue it contains
57        let node_dialogue = {
58            let mut search_stack = vec![current_node_key.clone()];
59            let mut result: Option<RcStoryNode> = None;
60            while result.is_none() {
61                let first_query = search_stack.first()
62                    .ok_or(EngineError::new(EngineErrorType::CurrentNoDialogue))?;
63                let node = get_node_from_key(first_query.clone())?;
64                let ref_node = node.borrow();
65                match ref_node.deref() {
66                    StoryNode::Dialogue(_) => result = Some(node.clone()),
67                    StoryNode::Part(part) => {
68                        for story_node in part.story_nodes().iter().rev() {
69                            search_stack.insert(1, story_node.clone());
70                        }
71                    }
72                }
73            }
74            result.ok_or(EngineError::new(EngineErrorType::CurrentNoDialogue))?
75        };
76
77        let ref_node_dialogue = node_dialogue.borrow();
78
79        match ref_node_dialogue.deref() {
80            // If the unwrapped node is a dialogue, proceed with the next function.
81            StoryNode::Dialogue(dialogue) => {
82                // Gets chosen quote from dialogue
83                let quote = if dialogue.has_choices() {
84                    let choice_index = choice
85                        .ok_or(EngineError::new(EngineErrorType::NoChoiceArg))?;
86                    dialogue.quotes().0.get(choice_index)
87                        .ok_or(EngineError::new(EngineErrorType::ChoiceDne))?
88                } else {
89                    dialogue.first_quote()
90                        .ok_or(EngineError::new(EngineErrorType::NoQuotes))?
91                };
92
93                // Parses the quote's next closure
94                let next_closure = quote.story_link().next()
95                    .ok_or(EngineError::new(EngineErrorType::NoNextClosure))?;
96
97                // Gets the next node key from the next closure and creates result
98                let next_node_key = next_closure(story_context);
99                let next_node = story.story_nodes().get(&next_node_key)
100                    .ok_or(EngineError::new(EngineErrorType::StoryNodeDne))?.clone();
101                let result = (next_node_key.clone(), next_node.clone());
102
103                // Changes the context with the quote's change context closure
104                match quote.story_link().change_context() {
105                    Some(change_context_closure) => {
106                        change_context_closure(story.mut_story_context());
107                    }
108                    _ => ()
109                }
110
111                story.set_current(next_node_key);
112                Ok(result)
113            }
114            // If the unwrapped node is anything else, throw an error.
115            _ => return Err(EngineError::new(EngineErrorType::NoDialogue))
116        }
117    }
118}
119
120#[cfg(test)]
121mod engine_tests {
122    use crate::story::story_builder::StoryBuilder;
123    use crate::story::story_node::context::ContextValue;
124    use crate::story::story_node::dialogue::dialogue_builder::DialogueBuilder;
125    use crate::story::story_node::dialogue::quote::quote_builder::QuoteBuilder;
126
127    use super::*;
128
129    #[test]
130    fn it_constructs() {
131        let engine = Engine::new();
132        assert!(engine.story().is_none());
133    }
134
135    #[test]
136    fn start_works() {
137        let story = StoryBuilder::new(String::from("dialogue-1"))
138            .add_story_node(
139                String::from("dialogue-1"),
140                DialogueBuilder::new(String::from("core"))
141                    .build(),
142            )
143            .build();
144
145        let mut engine = Engine::new();
146        engine.set_story(story);
147
148        let (node_key, _) = engine.start().unwrap();
149        assert_eq!(node_key, "dialogue-1");
150    }
151
152    #[test]
153    fn next_works() {
154        let story = StoryBuilder::new(String::from("dialogue-1"))
155            .add_story_node(
156                String::from("dialogue-1"),
157                DialogueBuilder::new(String::from("core"))
158                    .add_quote(
159                        QuoteBuilder::new(String::from("Hello!"))
160                            .next(|_| String::from("dialogue-2"))
161                            .build()
162                    )
163                    .add_quote(
164                        QuoteBuilder::new(String::from("Homie!"))
165                            .next(|_| String::from("dialogue-3"))
166                            .build()
167                    )
168                    .build(),
169            )
170            .add_story_node(
171                String::from("dialogue-2"),
172                DialogueBuilder::new(String::from("jose"))
173                    .add_quote(
174                        QuoteBuilder::new(String::from("Hi there!"))
175                            .build()
176                    )
177                    .build(),
178            )
179            .add_story_node(
180                String::from("dialogue-3"),
181                DialogueBuilder::new(String::from("jose"))
182                    .add_quote(
183                        QuoteBuilder::new(String::from("Who are you?"))
184                            .next(|_| String::from("dialogue-4"))
185                            .build()
186                    )
187                    .build(),
188            )
189            .build();
190
191        let mut engine = Engine::new();
192        engine.set_story(story);
193
194        let (node_1_key, _) = engine.next(Some(0)).unwrap();
195        let _ = engine.reset();
196        let (node_2_key, _) = engine.next(Some(1)).unwrap();
197
198        assert_eq!(node_1_key, "dialogue-2");
199        assert_eq!(node_2_key, "dialogue-3");
200    }
201
202    #[test]
203    fn next_with_context_works() {
204        let story = StoryBuilder::new(String::from("dialogue-0"))
205            .add_context(String::from("is-loved"), ContextValue::Bool(false))
206            .add_story_node(
207                String::from("dialogue-0"),
208                DialogueBuilder::new(String::from("girl"))
209                    .add_quote(
210                        QuoteBuilder::new(String::from("Don't you know?"))
211                            .next(|_| String::from("dialogue-1"))
212                            .build()
213                    )
214                    .add_quote(
215                        QuoteBuilder::new(String::from("I'm sure of it."))
216                            .next(|_| String::from("dialogue-1"))
217                            .change_context(|c| {
218                                *c.get_mut("is-loved")
219                                    .expect("Story to have an `is-loved` context") =
220                                    ContextValue::Bool(true);
221                            })
222                            .build()
223                    )
224                    .build(),
225            )
226            .add_story_node(
227                String::from("dialogue-1"),
228                DialogueBuilder::new(String::from("girl"))
229                    .add_quote(
230                        QuoteBuilder::new(String::from("I love you."))
231                            .next(|c| {
232                                let is_loved = c.get("is-loved")
233                                    .expect("Story to have an `is-loved` context");
234                                return match is_loved {
235                                    ContextValue::Bool(is_loved_bool) => {
236                                        if is_loved_bool.clone() {
237                                            String::from("dialogue-2")
238                                        } else {
239                                            String::from("dialogue-3")
240                                        }
241                                    }
242                                    _ => String::from("dialogue-3")
243                                };
244                            })
245                            .build()
246                    )
247                    .build(),
248            )
249            .add_story_node(
250                String::from("dialogue-2"),
251                DialogueBuilder::new(String::from("boy"))
252                    .add_quote(
253                        QuoteBuilder::new(String::from("I love you too!"))
254                            .build()
255                    )
256                    .build(),
257            )
258            .add_story_node(
259                String::from("dialogue-3"),
260                DialogueBuilder::new(String::from("boy"))
261                    .add_quote(
262                        QuoteBuilder::new(String::from("I'm... sorry."))
263                            .build()
264                    )
265                    .build(),
266            )
267            .build();
268
269
270        let mut engine = Engine::new();
271        engine.set_story(story);
272
273        let _ = engine.next(Some(0)).unwrap();
274        let (node_key, _) = engine.next(Some(0)).unwrap();
275        assert_eq!(node_key, "dialogue-3");
276
277        let _ = engine.reset();
278        let _ = engine.next(Some(1)).unwrap();
279        let (node_key, _) = engine.next(None).unwrap();
280        assert_eq!(node_key, "dialogue-2");
281    }
282}