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