Skip to main content

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;