Skip to main content

text_adventure/
text_adventure.rs

1//! A minimal `SimulationInfo` implementation modeling a text adventure.
2//!
3//! The world is a set of rooms connected by passages. Each passage is an event:
4//! calling it walks forward into the target room, reverting it walks back to the
5//! source room. The access data is the current room index, which the application
6//! looks up in the info to render — info-side rendering of state-side data, the
7//! same split `petri-net-simulation` and `multilinear` use.
8//!
9//! This mirrors the pattern used by real consumers like `petri-net-simulation`
10//! and `multilinear`: `callables`/`revertables` read a cache stored in the state,
11//! while `call`/`revert` recompute that cache from the info after mutating.
12//!
13//! Run with `cargo run --example text_adventure`.
14
15use std::convert::Infallible;
16
17use event_simulation::{OwnedSimulation, Simulation, SimulationInfo, SimulationState};
18
19/// An event: an index into [`Adventure::passages`].
20#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21struct Choice(usize);
22
23/// A directed connection between two rooms, shown to the player as a choice.
24struct Passage {
25    text: String,
26    from: usize,
27    to: usize,
28}
29
30/// The immutable world definition. Shared by every state derived from it.
31struct Adventure {
32    rooms: Vec<String>,
33    passages: Vec<Passage>,
34}
35
36impl Adventure {
37    fn choices_from(&self, room: usize) -> Vec<Choice> {
38        self.matching(|passage| passage.from == room)
39    }
40
41    fn choices_into(&self, room: usize) -> Vec<Choice> {
42        self.matching(|passage| passage.to == room)
43    }
44
45    fn matching(&self, predicate: impl Fn(&Passage) -> bool) -> Vec<Choice> {
46        self.passages
47            .iter()
48            .enumerate()
49            .filter(|(_, passage)| predicate(passage))
50            .map(|(index, _)| Choice(index))
51            .collect()
52    }
53}
54
55/// The mutable state: the current room plus the cached set of available events.
56/// The caches exist because the trait derives `callables`/`revertables` from the
57/// state alone, with no access to the info.
58#[derive(Clone)]
59struct Position {
60    room: usize,
61    callables: Vec<Choice>,
62    revertables: Vec<Choice>,
63}
64
65impl Position {
66    fn at(adventure: &Adventure, room: usize) -> Self {
67        Self {
68            room,
69            callables: adventure.choices_from(room),
70            revertables: adventure.choices_into(room),
71        }
72    }
73}
74
75impl SimulationInfo for Adventure {
76    type State = Position;
77    type StateLoadingError = Infallible;
78    type AccessData = usize;
79    type LoadData = usize;
80    type Event = Choice;
81    type EventContainer<'a> = std::vec::IntoIter<Choice>;
82
83    fn default_state(&self) -> Position {
84        Position::at(self, 0)
85    }
86
87    fn load_state(&self, room: usize) -> Result<Position, Infallible> {
88        Ok(Position::at(self, room.min(self.rooms.len() - 1)))
89    }
90
91    unsafe fn clone_state(&self, state: &Position) -> Position {
92        state.clone()
93    }
94
95    unsafe fn data<'a>(&self, state: &'a Position) -> &'a usize {
96        &state.room
97    }
98
99    fn callables(state: &Position) -> std::vec::IntoIter<Choice> {
100        state.callables.clone().into_iter()
101    }
102
103    fn revertables(state: &Position) -> std::vec::IntoIter<Choice> {
104        state.revertables.clone().into_iter()
105    }
106
107    fn callable(state: &Position, event: Choice) -> bool {
108        state.callables.contains(&event)
109    }
110
111    fn revertable(state: &Position, event: Choice) -> bool {
112        state.revertables.contains(&event)
113    }
114
115    unsafe fn call(&self, state: &mut Position, Choice(index): Choice) {
116        *state = Position::at(self, self.passages[index].to);
117    }
118
119    unsafe fn revert(&self, state: &mut Position, Choice(index): Choice) {
120        *state = Position::at(self, self.passages[index].from);
121    }
122}
123
124fn passage(text: &str, from: usize, to: usize) -> Passage {
125    Passage {
126        text: text.into(),
127        from,
128        to,
129    }
130}
131
132fn main() {
133    let adventure = Adventure {
134        rooms: vec![
135            "You stand at a crossroads.".into(),
136            "A dark cave yawns before you.".into(),
137            "Sunlight breaks over a quiet meadow.".into(),
138        ],
139        passages: vec![
140            passage("Enter the cave", 0, 1),
141            passage("Walk to the meadow", 0, 2),
142            passage("Retreat from the cave", 1, 0),
143        ],
144    };
145
146    let mut simulation = OwnedSimulation::<Adventure>::new(adventure);
147
148    let describe =
149        |simulation: &OwnedSimulation<Adventure>| simulation.rooms[*simulation.data()].clone();
150
151    println!("{}", describe(&simulation));
152    for choice in simulation.callables() {
153        println!("  → {}", simulation.passages[choice.0].text);
154    }
155
156    let Some(first) = simulation.callables().next() else {
157        panic!("crossroads has choices");
158    };
159    assert!(simulation.try_call(first));
160    assert_eq!(simulation.state.room, 1);
161    println!("\n{}", describe(&simulation));
162
163    let Some(back) = simulation.revertables().next() else {
164        panic!("cave can be left");
165    };
166    assert!(simulation.try_revert(back));
167    assert_eq!(simulation.state.room, 0);
168    println!("\nBack: {}", describe(&simulation));
169}