shuyi_core/tick.rs
1//! Rules that step in time (ADR-0003 §2): a game whose rules run 60 fixed
2//! steps a second implements [`TickSim`]. Each step takes a [`TickInput`],
3//! what each seat's player pressed, held and released in it, and a run is
4//! its seed and its inputs, a [`TickRecord`], which [`replay`] plays again,
5//! giving the state's hash after each step.
6//!
7//! The rules time things in steps with [`timer`](crate::timer), never with
8//! a frame's seconds, so a run steps the same at any frame rate.
9//! `shuyi_ui::tick::TickDriver` steps a simulation as many times as a frame
10//! has steps due, and gives each step what the player did since the last
11//! one, so a record of the steps replays the same whatever frame rate made
12//! it.
13//!
14//! ```
15//! use shuyi_core::tick::{ActionId, SeatInput, TickInput, TickSim};
16//!
17//! /// A counter of the steps its one action was held.
18//! #[derive(Default)]
19//! struct Held(u32);
20//!
21//! impl TickSim for Held {
22//! type Event = ();
23//!
24//! fn step(&mut self, input: &TickInput) -> Vec<()> {
25//! self.0 += u32::from(input.is_held(0, ActionId(0)));
26//! Vec::new()
27//! }
28//!
29//! fn hash(&self) -> u64 {
30//! u64::from(self.0)
31//! }
32//! }
33//!
34//! let mut sim = Held::default();
35//! let held = TickInput::one(SeatInput::new(vec![], vec![ActionId(0)], vec![]));
36//! sim.step(&held);
37//! sim.step(&TickInput::default());
38//! assert_eq!(sim.hash(), 1);
39//! ```
40
41use alloc::vec::Vec;
42
43use serde::{Deserialize, Serialize};
44
45use crate::made::{Made, Mismatch};
46
47/// The most seats a [`TickInput`] holds: four players at one machine.
48pub const MAX_SEATS: usize = 4;
49
50/// An action, by its place in the game's list of actions (`ActionSet::ALL`
51/// in `shuyi-assets`): 0 is the first. The core names no game's actions,
52/// so a step's input holds these; `shuyi_ui::tick::ActionIds` turns a
53/// game's action into its id and back.
54#[derive(
55 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
56)]
57#[serde(transparent)]
58pub struct ActionId(pub u16);
59
60/// What one seat's player did in one step: the actions pressed, held and
61/// released, each by its [`ActionId`] and at most once in each list.
62///
63/// - `pressed`: the actions that went down since the last step, in the
64/// order they went down.
65/// - `held`: the actions down at the end of the frame the step ran in, in
66/// the game's order. A tap (down and up again between two steps) is
67/// pressed without being held.
68/// - `released`: the actions that came up, in the order they came up. A
69/// release is always in a step after its press's, so a step that has an
70/// action both released and pressed let it go and pressed it again.
71#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(deny_unknown_fields)]
73// What a step's input holds may grow (axes and the pointer, ticket 1121;
74// ADR-0039).
75#[non_exhaustive]
76pub struct SeatInput {
77 /// The actions that went down since the last step, in order.
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 pub pressed: Vec<ActionId>,
80 /// The actions down at the end of the frame the step ran in.
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub held: Vec<ActionId>,
83 /// The actions that came up since the last step, in order.
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub released: Vec<ActionId>,
86}
87
88impl SeatInput {
89 /// A seat's input with these `pressed`, `held` and `released` actions.
90 pub fn new(pressed: Vec<ActionId>, held: Vec<ActionId>, released: Vec<ActionId>) -> Self {
91 Self {
92 pressed,
93 held,
94 released,
95 }
96 }
97
98 /// Whether `action` went down since the last step.
99 pub fn is_pressed(&self, action: ActionId) -> bool {
100 self.pressed.contains(&action)
101 }
102
103 /// Whether `action` is down.
104 pub fn is_held(&self, action: ActionId) -> bool {
105 self.held.contains(&action)
106 }
107
108 /// Whether `action` came up since the last step.
109 pub fn is_released(&self, action: ActionId) -> bool {
110 self.released.contains(&action)
111 }
112
113 /// Whether nothing was pressed, held or released.
114 pub fn is_empty(&self) -> bool {
115 self.pressed.is_empty() && self.held.is_empty() && self.released.is_empty()
116 }
117}
118
119/// What one step of a [`TickSim`] is given: one [`SeatInput`] per seat,
120/// indexed by seat number from 0, at most [`MAX_SEATS`]. A game with one
121/// player has seat 0 alone.
122#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124// It may grow (ADR-0039): built with `new` or `one`.
125#[non_exhaustive]
126pub struct TickInput {
127 /// Each seat's input, by seat number.
128 pub seats: Vec<SeatInput>,
129}
130
131impl TickInput {
132 /// The input of `seats`, by seat number from 0: the first
133 /// [`MAX_SEATS`] of them.
134 pub fn new(mut seats: Vec<SeatInput>) -> Self {
135 seats.truncate(MAX_SEATS);
136 Self { seats }
137 }
138
139 /// The input of seat 0 alone, a game with one player's.
140 pub fn one(seat: SeatInput) -> Self {
141 Self {
142 seats: [seat].into(),
143 }
144 }
145
146 /// Seat `seat`'s input, if it has a seat that far.
147 pub fn seat(&self, seat: usize) -> Option<&SeatInput> {
148 self.seats.get(seat)
149 }
150
151 /// Whether seat `seat` pressed `action` since the last step.
152 pub fn is_pressed(&self, seat: usize, action: ActionId) -> bool {
153 self.seat(seat).is_some_and(|s| s.is_pressed(action))
154 }
155
156 /// Whether seat `seat` holds `action`.
157 pub fn is_held(&self, seat: usize, action: ActionId) -> bool {
158 self.seat(seat).is_some_and(|s| s.is_held(action))
159 }
160
161 /// Whether seat `seat` let go of `action` since the last step.
162 pub fn is_released(&self, seat: usize, action: ActionId) -> bool {
163 self.seat(seat).is_some_and(|s| s.is_released(action))
164 }
165}
166
167/// A game's rules that step in time: 60 fixed steps a second
168/// ([`STEPS_PER_SECOND`](crate::STEPS_PER_SECOND)), each given the players'
169/// input for it.
170///
171/// # Contract
172///
173/// - **Deterministic.** The same state and input always give the same
174/// events and the same new state, on every platform. Luck comes from a
175/// seeded RNG kept in the state ([`SimRng`](crate::SimRng)); there is no
176/// clock, no I/O and no float in the state or the step. So a run is
177/// rebuilt from its seed and its inputs ([`TickRecord`], [`replay`]).
178/// - **Time is steps.** One call of [`step`](Self::step) is one step.
179/// Timers advance by 1 in it ([`timer`](crate::timer)).
180/// - **Events are the only way out.** What a screen shows or plays (a
181/// sound, say) comes from the events [`step`](Self::step) returns, so a
182/// step run again after a rewind plays nothing twice
183/// (`shuyi_ui::tick::TickDriver` hands each step's events on once).
184/// - [`hash`](Self::hash) changes when the state does and is the same on
185/// every platform: most simply `state_hash(self)`'s
186/// ([`state_hash`](crate::state_hash)).
187pub trait TickSim {
188 /// What happened in a step.
189 type Event;
190
191 /// Runs one step with `input`, and says what happened.
192 fn step(&mut self, input: &TickInput) -> Vec<Self::Event>;
193
194 /// The hash of the state, which a replay compares step by step.
195 fn hash(&self) -> u64;
196}
197
198/// One run of a [`TickRecord`]: an input and the steps in a row that saw
199/// it.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(deny_unknown_fields)]
202pub struct Run {
203 /// How many steps in a row saw it: at least 1.
204 pub steps: u32,
205 /// What they saw.
206 pub input: TickInput,
207}
208
209/// A run of a [`TickSim`]: the stamp of the build that made it
210/// ([`Made`]), the seed its state was built from, and the input of each
211/// step, oldest first. Steps in a row with the same input are written once
212/// with their count, so a minute of a dot held right is one line.
213///
214/// It holds what each step saw, after the driver gave each press and
215/// release to its step, so it replays the same steps whatever frame rate
216/// made it. Written as `(made: …, seed: …, runs: [(steps: 30, input:
217/// (seats: [(held: [3])])), …])`.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(deny_unknown_fields)]
220// Its runs stay merged, and it may grow (ADR-0039): built with `new` and
221// `push`.
222#[non_exhaustive]
223pub struct TickRecord {
224 /// The build that made it.
225 made: Made,
226 /// The seed the run's first state was built from.
227 seed: u64,
228 /// The steps' inputs, run-length encoded, oldest first.
229 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 runs: Vec<Run>,
231}
232
233impl TickRecord {
234 /// A record of no step yet, of a run made by `made` from `seed`.
235 pub fn new(made: Made, seed: u64) -> Self {
236 Self {
237 made,
238 seed,
239 runs: Vec::new(),
240 }
241 }
242
243 /// The build that made it.
244 pub fn made(&self) -> &Made {
245 &self.made
246 }
247
248 /// The seed the run's first state was built from.
249 pub fn seed(&self) -> u64 {
250 self.seed
251 }
252
253 /// The steps' inputs as written: each with how many steps in a row saw
254 /// it.
255 pub fn runs(&self) -> &[Run] {
256 &self.runs
257 }
258
259 /// Records the next step's input.
260 pub fn push(&mut self, input: TickInput) {
261 match self.runs.last_mut() {
262 Some(last) if last.input == input && last.steps < u32::MAX => last.steps += 1,
263 _ => self.runs.push(Run { steps: 1, input }),
264 }
265 }
266
267 /// How many steps it holds.
268 pub fn len(&self) -> u64 {
269 self.runs.iter().map(|run| u64::from(run.steps)).sum()
270 }
271
272 /// Whether it holds no step.
273 pub fn is_empty(&self) -> bool {
274 self.len() == 0
275 }
276
277 /// Each step's input, oldest first.
278 pub fn inputs(&self) -> impl Iterator<Item = &TickInput> {
279 self.runs.iter().flat_map(|run| {
280 let steps = usize::try_from(run.steps).unwrap_or(usize::MAX);
281 core::iter::repeat_n(&run.input, steps)
282 })
283 }
284
285 /// Keeps its first `steps` steps and forgets the rest: after a rewind
286 /// to step `steps`, the steps after it are recorded again as they run.
287 pub fn truncate(&mut self, steps: u64) {
288 let mut left = steps;
289 let mut keep = 0;
290 for run in &mut self.runs {
291 if left == 0 {
292 break;
293 }
294 let kept = u32::try_from(left).unwrap_or(u32::MAX).min(run.steps);
295 run.steps = kept;
296 left -= u64::from(kept);
297 keep += 1;
298 }
299 self.runs.truncate(keep);
300 }
301}
302
303/// Plays `record` on `sim`, the state the caller built from the record's
304/// [`seed`](TickRecord::seed), and gives the state's hash after each step,
305/// one per step of the record, oldest first. `sim` is left at the record's
306/// end. A record whose stamp isn't `made`, this build's, is refused before
307/// anything steps, naming each part that differs ([`Made::check`]): another
308/// build may step the same inputs differently without a bug in either.
309pub fn replay<S: TickSim>(
310 sim: &mut S,
311 record: &TickRecord,
312 made: &Made,
313) -> Result<Vec<u64>, Mismatch> {
314 record.made.check(made)?;
315 Ok(record
316 .inputs()
317 .map(|input| {
318 sim.step(input);
319 sim.hash()
320 })
321 .collect())
322}
323
324#[cfg(test)]
325mod tests;