1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//! Mutable runtime state.
//!
//! `RuntimeState` holds everything that changes turn-to-turn. It is the only
//! thing that serializes to a save file. The authored content in
//! [`crate::interactive_fiction::data::World`] is never mutated at runtime.
use crate::interactive_fiction::data::choice::Choice;
use crate::interactive_fiction::data::entity::EntityLocation;
use crate::interactive_fiction::data::ids::{
DialogueId, EndingId, EntityId, EventName, FlagKey, ItemId, NodeId, QuestId, RoomId, RuleId,
StatKey, TimerId,
};
use crate::interactive_fiction::data::value::Value;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
/// Where a single item currently is.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ItemLocation {
/// On the floor in a specific room.
Room(RoomId),
/// In the player's inventory.
Inventory,
/// Carried by an entity (typically a character — objects rarely
/// hold items, but the engine doesn't forbid it).
HeldBy(EntityId),
/// Removed from play entirely.
Nowhere,
}
/// A single entry in the scrolling transcript.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TranscriptEntry {
/// Narrative description or rule-emitted text.
Narration(String),
/// Echo of the player's picked choice.
PlayerAction(String),
/// Dialogue line attributed to a speaker.
Dialogue { speaker: String, text: String },
/// System/meta message ("saved.", "cannot go that way.").
System(String),
/// A horizontal rule / separator for room transitions.
Separator,
}
/// The entire mutable state of a single run.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RuntimeState {
/// Where the player currently is.
pub current_room: RoomId,
/// Where the player was before the most recent move (for "go back" UX).
pub previous_room: Option<RoomId>,
/// Location of every item that has a location.
pub item_locations: BTreeMap<ItemId, ItemLocation>,
/// Location of each entity (character or object). Missing entries
/// are treated the same as `EntityLocation::Nowhere`.
pub entity_locations: BTreeMap<EntityId, EntityLocation>,
/// Current disposition of each character entity. Objects are not
/// tracked here.
pub dispositions: BTreeMap<EntityId, i64>,
/// Flags set by effects.
pub flags: BTreeMap<FlagKey, Value>,
/// Numeric stats.
pub stats: BTreeMap<StatKey, i64>,
/// Monotonic turn counter.
pub turn: u32,
/// Set of rooms the player has ever occupied.
pub visited: BTreeSet<RoomId>,
/// Current stage of each quest.
pub quest_stages: BTreeMap<QuestId, NodeId>,
/// Every stage each quest has ever been at (for `Condition::QuestReached`).
pub quest_history: BTreeMap<QuestId, BTreeSet<NodeId>>,
/// If `Some`, the player is currently inside a dialogue at this node.
pub active_dialogue: Option<(DialogueId, NodeId)>,
/// Remaining turns for each timer that is currently running.
/// Timers not present in this map are not running.
pub timers_remaining: BTreeMap<TimerId, u32>,
/// Timers that have expired this run.
pub timers_expired: BTreeSet<TimerId>,
/// Timers that were cancelled (not expired) this run.
pub timers_cancelled: BTreeSet<TimerId>,
/// Events scheduled to fire on a specific turn.
pub scheduled_events: Vec<ScheduledEvent>,
/// Rules that have ever fired.
pub rules_fired: BTreeSet<RuleId>,
/// Most recent turn on which each rule fired (for cooldowns).
pub rule_last_fired: BTreeMap<RuleId, u32>,
/// Endings that have been unlocked across this run.
pub unlocked_endings: BTreeSet<EndingId>,
/// Scrolling transcript.
pub transcript: VecDeque<TranscriptEntry>,
/// If non-empty, these override normal choice assembly until one is picked.
pub pending_choices: Vec<Choice>,
/// If set, the game is over and this is the ending that fired.
pub game_over: Option<EndingId>,
/// Deterministic RNG state (xorshift64).
pub rng_state: u64,
/// The noun most recently targeted by a player action (take, examine,
/// drop, use, open, ...). Bare verbs like `drink` or `read` consult
/// this when the player types without a noun.
#[serde(default)]
pub last_noun: Option<String>,
}
pub const TRANSCRIPT_CAPACITY: usize = 10_000;
/// A pending scheduled event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduledEvent {
pub event: EventName,
/// Turn on which the event should fire (compared against `RuntimeState.turn`).
pub fires_on_turn: u32,
}
impl RuntimeState {
/// Push a transcript entry, trimming the front if the buffer is full.
pub fn push_transcript(&mut self, entry: TranscriptEntry) {
if self.transcript.len() >= TRANSCRIPT_CAPACITY {
self.transcript.pop_front();
}
self.transcript.push_back(entry);
}
/// Advance the deterministic RNG and return the raw 64-bit output.
pub fn advance_rng(&mut self) -> u64 {
let mut state = self.rng_state;
if state == 0 {
state = 0x9E3779B97F4A7C15;
}
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
self.rng_state = state;
state
}
/// Return a value in `[0, bound)` using the deterministic RNG.
pub fn random_index(&mut self, bound: usize) -> usize {
if bound == 0 {
0
} else {
(self.advance_rng() as usize) % bound
}
}
/// Return a value in `[0, 100)` for percentage checks. Combined with
/// `roll < percent`, this makes `Condition::Chance(100)` always fire and
/// `Condition::Chance(0)` never fire.
pub fn random_percent(&mut self) -> u8 {
(self.advance_rng() % 100) as u8
}
}