Skip to main content

repame_sim/
tape.rs

1//! The intent tape, the canonical world hash, and the replay that checks them.
2//!
3//! A run is reproducible when three things are pinned: the world it starts from
4//! (the seed and the spawn history), the intent fed in on every tick, and the
5//! code. [`Tape`] holds the input side of that as data, one input per tick, plus
6//! the canonical world hash every tick left behind, so a replay is checked tick
7//! by tick instead of only at its end. [`Tape::verify`] replays a tape and names
8//! the first tick whose hash differs, which is the harness the crate docs ask
9//! for: run one trace twice, hash the world per tick, diff.
10//!
11//! # The hashing order rule
12//!
13//! [`WorldHasher::hash`] folds, in this order, and never in the order `bevy_ecs`
14//! hands things back:
15//!
16//! 1. A world tag, then the sorted ids of every live entity.
17//! 2. Each registered component, in registration order: how many entities match,
18//!    then one `(entity id, value hash)` pair per match, sorted ascending by
19//!    entity id.
20//! 3. Each registered resource, in registration order: a presence byte, then the
21//!    value hash when it exists.
22//!
23//! `bevy_ecs` iterates archetypes and tables in creation order and rows in the
24//! order entities entered them, so two runs that reach the same state through a
25//! different spawn/despawn history iterate differently (the warning at the top
26//! of the crate docs). Sorting is what makes the digest describe state rather
27//! than layout: a divergence report has to name a gameplay difference, not a
28//! table reordering. Entity ids are folded because they *are* state: identical
29//! spawn histories produce identical ids, so a run that spawned one entity too
30//! many changes the digest the moment it does.
31//!
32//! What the digest does not cover: state nobody registered, and anything the
33//! process gives one run and not the other (wall-clock time, thread-scheduled
34//! work). The registered set is part of the replay contract, so register the
35//! same components and resources in the same order on both sides.
36
37use std::fmt;
38use std::hash::Hash;
39use std::sync::Arc;
40
41use bevy_ecs::prelude::*;
42use serde::{Deserialize, Serialize};
43
44use crate::Sim;
45use crate::hash::Hash64;
46
47/// Domain tag: the first thing a world digest folds.
48const WORLD_TAG: &str = "repame-sim/world";
49
50/// The input for the tick about to run.
51///
52/// One convention the tape can drive without glue: a game's input system reads
53/// this resource, the app fills it from the device before each tick, and a
54/// replay fills it from the recording. A game that keeps its own action
55/// resources instead hands [`Tape::replay_with`] a closure that copies the
56/// intent where the game expects it.
57#[derive(Resource)]
58pub struct Intent<I>(Option<I>);
59
60impl<I> Intent<I> {
61    /// This tick's input, if the app or the tape provided one.
62    pub fn get(&self) -> Option<&I> {
63        self.0.as_ref()
64    }
65
66    /// Set this tick's input.
67    pub fn set(&mut self, input: I) {
68        self.0 = Some(input);
69    }
70
71    /// Drop the input, the state a live app starts a tick in before polling.
72    pub fn clear(&mut self) {
73        self.0 = None;
74    }
75}
76
77impl<I> Default for Intent<I> {
78    fn default() -> Self {
79        Self(None)
80    }
81}
82
83/// A recorded run: one intent per tick, plus the canonical world hash each tick
84/// left behind, both in tick order.
85///
86/// A tape is the artifact a bug report carries: the seed, what the player asked
87/// for on every tick, and what the world looked like. Serialisable, so it can be
88/// written next to the save it came from.
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct Tape<I> {
91    seed: u64,
92    intents: Vec<I>,
93    hashes: Vec<u64>,
94}
95
96impl<I> Tape<I> {
97    /// An empty tape for a run seeded with `seed`.
98    ///
99    /// The seed is carried because a tape without it is not replayable: the
100    /// factory a replay is handed reads [`seed`](Self::seed) to rebuild the same
101    /// starting world.
102    pub const fn new(seed: u64) -> Self {
103        Self {
104            seed,
105            intents: Vec::new(),
106            hashes: Vec::new(),
107        }
108    }
109
110    /// The seed the recording run started from.
111    pub const fn seed(&self) -> u64 {
112        self.seed
113    }
114
115    /// How many ticks are recorded. What a replay runs.
116    pub fn ticks(&self) -> u64 {
117        self.intents.len() as u64
118    }
119
120    /// Whether no tick has been recorded yet.
121    pub fn is_empty(&self) -> bool {
122        self.intents.is_empty()
123    }
124
125    /// Every recorded intent, in tick order.
126    pub fn intents(&self) -> &[I] {
127        &self.intents
128    }
129
130    /// Every recorded intent, to edit a tape before replaying it.
131    ///
132    /// Rewriting a tick's intent while keeping the recorded hashes is how a
133    /// divergence is bisected: the replay then reports that tick (or says the
134    /// edit changed nothing), instead of a hunt through a whole session.
135    pub fn intents_mut(&mut self) -> &mut [I] {
136        &mut self.intents
137    }
138
139    /// Every recorded hash, in tick order. Shorter than the tape when the intent
140    /// tail was never hashed.
141    pub fn hashes(&self) -> &[u64] {
142        &self.hashes
143    }
144
145    /// The intent of `tick`, or `None` past the end.
146    pub fn intent(&self, tick: u64) -> Option<&I> {
147        self.intents.get(tick as usize)
148    }
149
150    /// The recorded world hash of `tick`, or `None` if that tick was not hashed.
151    pub fn hash(&self, tick: u64) -> Option<u64> {
152        self.hashes.get(tick as usize).copied()
153    }
154
155    /// Appends one tick's intent. Returns the tick it landed on.
156    ///
157    /// Call it before the tick runs, so the tape is in the order the game saw.
158    pub fn record_intent(&mut self, intent: I) -> u64 {
159        let tick = self.intents.len() as u64;
160        self.intents.push(intent);
161        tick
162    }
163
164    /// Appends the canonical world hash of the tick just recorded, in tick
165    /// order: tick `n`'s hash is the world after tick `n` ran.
166    ///
167    /// A tape may stop hashing early (an intent-only tail cannot be verified,
168    /// see [`verify`](Self::verify)), but never hashes twice for one tick, and
169    /// never more hashes than ticks.
170    ///
171    /// # Panics
172    ///
173    /// If every recorded intent already has a hash. That is a recording loop
174    /// hashing twice for one tick, which would misalign every later comparison.
175    pub fn record_hash(&mut self, world_hash: u64) {
176        assert!(
177            self.hashes.len() < self.intents.len(),
178            "record_hash with no tick to belong to: {} ticks, {} hashes",
179            self.intents.len(),
180            self.hashes.len()
181        );
182        self.hashes.push(world_hash);
183    }
184
185    /// One tick of a replay: the recorded intent into [`Intent`], then `commit`,
186    /// then the tick, then the canonical hash of the world it left. Returns that
187    /// hash.
188    fn step(
189        &self,
190        sim: &mut Sim,
191        hasher: &WorldHasher,
192        tick: usize,
193        commit: &mut impl FnMut(&mut World, &I),
194    ) -> u64
195    where
196        I: Clone + Send + Sync + 'static,
197    {
198        let intent = &self.intents[tick];
199        sim.world.resource_mut::<Intent<I>>().set(intent.clone());
200        commit(&mut sim.world, intent);
201        sim.tick();
202        hasher.hash(&sim.world)
203    }
204}
205
206impl<I> Tape<I>
207where
208    I: Clone + Send + Sync + 'static,
209{
210    /// Replays the tape into a fresh world and returns the canonical hash of
211    /// every tick, in tick order.
212    ///
213    /// `setup` is called once per replay and must rebuild the same starting
214    /// world every time, so a caller can replay one tape twice and diff the two
215    /// logs itself, or compare them against a golden list.
216    pub fn replay<F>(&self, setup: F) -> Vec<u64>
217    where
218        F: FnMut() -> Setup,
219    {
220        self.replay_with(setup, |_, _| {})
221    }
222
223    /// [`replay`](Self::replay), running `commit` on each tick's intent after
224    /// [`Intent`] is set and before the tick runs.
225    ///
226    /// This is the hook for a game whose systems read their own action
227    /// resources:
228    ///
229    /// ```ignore
230    /// let commit = |world: &mut World, input: &Press| {
231    ///     world.resource_mut::<Keys>().0 = *input;
232    /// };
233    /// let hashes = tape.replay_with(|| fresh(seed), commit);
234    /// ```
235    pub fn replay_with<F, C>(&self, mut setup: F, mut commit: C) -> Vec<u64>
236    where
237        F: FnMut() -> Setup,
238        C: FnMut(&mut World, &I),
239    {
240        let Setup { mut sim, hasher } = setup();
241        sim.world.insert_resource(Intent::<I>::default());
242        let mut hashes = Vec::with_capacity(self.intents.len());
243        for tick in 0..self.intents.len() {
244            hashes.push(self.step(&mut sim, &hasher, tick, &mut commit));
245        }
246        hashes
247    }
248
249    /// Replays the tape and compares every tick's hash against the recording,
250    /// returning the first tick that differs.
251    ///
252    /// The replay stops at the divergence: a world that already disagrees is not
253    /// worth stepping further, and a game that would panic on it panics here
254    /// instead of reporting the tick it went wrong at. Pair this with
255    /// [`record_intent`](Self::record_intent) and
256    /// [`record_hash`](Self::record_hash).
257    pub fn verify<F>(&self, setup: F) -> Result<(), VerifyError>
258    where
259        F: FnMut() -> Setup,
260    {
261        self.verify_with(setup, |_, _| {})
262    }
263
264    /// [`verify`](Self::verify), running `commit` on each tick's intent after
265    /// [`Intent`] is set and before the tick runs.
266    pub fn verify_with<F, C>(&self, mut setup: F, mut commit: C) -> Result<(), VerifyError>
267    where
268        F: FnMut() -> Setup,
269        C: FnMut(&mut World, &I),
270    {
271        if self.hashes.len() != self.intents.len() {
272            return Err(VerifyError::UnhashedTicks {
273                ticks: self.intents.len() as u64,
274                hashes: self.hashes.len() as u64,
275            });
276        }
277        let Setup { mut sim, hasher } = setup();
278        sim.world.insert_resource(Intent::<I>::default());
279        let mut replayed = Vec::with_capacity(self.intents.len());
280        for tick in 0..self.intents.len() {
281            let actual = self.step(&mut sim, &hasher, tick, &mut commit);
282            let expected = self.hashes[tick];
283            replayed.push(actual);
284            if expected != actual {
285                return Err(VerifyError::Diverged {
286                    divergence: Divergence {
287                        tick: tick as u64,
288                        expected,
289                        actual,
290                    },
291                    replayed,
292                });
293            }
294        }
295        Ok(())
296    }
297}
298
299/// A fresh world to replay into, with the hasher that folds it.
300///
301/// A tape is checked by building the same starting world twice: the recording
302/// side built it once, every replay builds it again. That factory returns this.
303pub struct Setup {
304    /// The sim to step, with the game's systems registered.
305    pub sim: Sim,
306    /// What the digest covers. Register the same components and resources, in
307    /// the same order, as the recording run did.
308    pub hasher: WorldHasher,
309}
310
311/// The canonical world hash: which state the digest covers, and in what order.
312///
313/// The fold order is pinned in the module docs. An empty hasher digests the
314/// entity list alone, which is a real check in a game whose state *is* its
315/// entities, but most callers register the components and resources that decide
316/// gameplay:
317///
318/// ```ignore
319/// let mut hasher = WorldHasher::new();
320/// hasher.register::<Height>(|height, hash| hash.f32(height.0));
321/// hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
322/// ```
323#[derive(Clone, Default)]
324pub struct WorldHasher {
325    entries: Vec<Arc<dyn Fn(&World, &mut Hash64) + Send + Sync>>,
326}
327
328impl WorldHasher {
329    /// A hasher that folds the entity list and nothing else.
330    pub fn new() -> Self {
331        Self {
332            entries: Vec::new(),
333        }
334    }
335
336    /// Registers a component: one `(entity, value)` fold per entity that has it.
337    ///
338    /// Write the value's fields in a fixed order. A float folds its bits
339    /// (`hash.f32(value)`), so the sign of zero and a NaN payload are covered
340    /// exactly; a field left out is a field the digest cannot report.
341    pub fn register<C: Component>(
342        &mut self,
343        hash_value: impl Fn(&C, &mut Hash64) + Send + Sync + 'static,
344    ) -> &mut Self {
345        self.entries
346            .push(Arc::new(move |world: &World, hash: &mut Hash64| {
347                // A component this world never registered matches no entity, so
348                // it folds the same zero count an empty query folds.
349                let Some(mut query) = world.try_query::<(Entity, &C)>() else {
350                    hash.u64(0);
351                    return;
352                };
353                let mut rows: Vec<(u64, u64)> = Vec::new();
354                for (entity, value) in query.iter(world) {
355                    let mut row = Hash64::new();
356                    hash_value(value, &mut row);
357                    rows.push((entity.to_bits(), row.digest()));
358                }
359                rows.sort_unstable_by_key(|(entity, _)| *entity);
360                hash.u64(rows.len() as u64);
361                for (entity, value) in rows {
362                    hash.u64(entity);
363                    hash.u64(value);
364                }
365            }));
366        self
367    }
368
369    /// [`register`](Self::register) for a component whose own `Hash`
370    /// implementation is yours.
371    ///
372    /// The fold is whatever `Hash` writes, and every integer writer is pinned by
373    /// [`Hash64`], but a `Hash` impl from `std` or from a dependency may change
374    /// its byte layout in a later release. For a digest a save file or a golden
375    /// test pins, write the fields explicitly with
376    /// [`register`](Self::register).
377    pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
378        self.register::<C>(|value, hash| value.hash(hash))
379    }
380
381    /// Registers a resource: a presence byte, then the value hash when it
382    /// exists.
383    ///
384    /// Presence is state of its own, so a resource missing from one run and
385    /// present in the other folds differently even when its payload is a zero.
386    pub fn register_resource<R: Resource>(
387        &mut self,
388        hash_value: impl Fn(&R, &mut Hash64) + Send + Sync + 'static,
389    ) -> &mut Self {
390        self.entries.push(Arc::new(
391            move |world: &World, hash: &mut Hash64| match world.get_resource::<R>() {
392                Some(value) => {
393                    let mut presence = Hash64::new();
394                    hash_value(value, &mut presence);
395                    hash.u8(1);
396                    hash.u64(presence.digest());
397                }
398                None => hash.u8(0),
399            },
400        ));
401        self
402    }
403
404    /// [`register_resource`](Self::register_resource) for a resource whose own
405    /// `Hash` implementation is yours.
406    pub fn register_hashed_resource<R: Resource + Hash>(&mut self) -> &mut Self {
407        self.register_resource::<R>(|value, hash| value.hash(hash))
408    }
409
410    /// The canonical digest of `world`: one number that moves whenever any
411    /// registered state moves.
412    ///
413    /// Folds in the order the module docs pin, which is not `bevy_ecs`'s
414    /// iteration order, so a run that reaches the same state through a different
415    /// spawn/despawn history digests the same.
416    pub fn hash(&self, world: &World) -> u64 {
417        let mut hash = Hash64::new();
418        hash.str(WORLD_TAG);
419        let mut entities: Vec<u64> = world
420            .iter_entities()
421            .map(|entity| entity.id().to_bits())
422            .collect();
423        entities.sort_unstable();
424        hash.usize(entities.len());
425        for bits in entities {
426            hash.u64(bits);
427        }
428        for entry in &self.entries {
429            entry(world, &mut hash);
430        }
431        hash.digest()
432    }
433}
434
435/// The first tick a replay disagreed with the recording at.
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub struct Divergence {
438    /// Tick the two runs first differed on. Ticks before it hashed the same, so
439    /// the cause is in what this tick's systems read or in the intent it took:
440    /// [`Tape::intent`] has that tick's input, and the recording's own hash log
441    /// is one [`Tape::hash`] call away.
442    pub tick: u64,
443    /// Hash the recording left for this tick.
444    pub expected: u64,
445    /// Hash this replay produced for it.
446    pub actual: u64,
447}
448
449impl fmt::Display for Divergence {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        write!(
452            f,
453            "tick {}: recorded {:#018x}, replayed {:#018x}",
454            self.tick, self.expected, self.actual
455        )
456    }
457}
458
459/// Why a tape could not be checked.
460#[derive(Clone, Debug, PartialEq, Eq)]
461pub enum VerifyError {
462    /// The tape's hash log does not have one entry per tick, so a replay cannot
463    /// be checked tick for tick. An intent-only tape (replayed for its visuals,
464    /// say) lands here.
465    UnhashedTicks {
466        /// Ticks recorded.
467        ticks: u64,
468        /// Hashes recorded.
469        hashes: u64,
470    },
471    /// The replay's world differed from the recording's.
472    Diverged {
473        /// The first tick that differed, with both hashes.
474        divergence: Divergence,
475        /// Every hash this replay produced, up to and including the diverging
476        /// tick: how far the two runs agreed before this.
477        replayed: Vec<u64>,
478    },
479}
480
481impl VerifyError {
482    /// The first divergent tick, when the failure was a divergence rather than a
483    /// tape that cannot be checked.
484    pub fn tick(&self) -> Option<u64> {
485        match self {
486            Self::Diverged { divergence, .. } => Some(divergence.tick),
487            Self::UnhashedTicks { .. } => None,
488        }
489    }
490}
491
492impl fmt::Display for VerifyError {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        match self {
495            Self::UnhashedTicks { ticks, hashes } => write!(
496                f,
497                "tape has {ticks} ticks and {hashes} hashes; verify needs one hash per tick"
498            ),
499            Self::Diverged { divergence, .. } => write!(f, "diverged at {divergence}"),
500        }
501    }
502}
503
504impl std::error::Error for VerifyError {}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509    use crate::rng::{Rng, SimRng, WorldSeed};
510
511    /// One tick of recorded input: what a player could ask for.
512    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
513    struct Press {
514        /// Hold the accelerator.
515        up: bool,
516        /// Spawn one body.
517        spawn: bool,
518    }
519
520    /// A body's height, folded into the digest.
521    #[derive(Component, Clone, Copy, Debug)]
522    struct Height(f32);
523
524    /// A spawned body's speed, folded into the digest.
525    #[derive(Component, Clone, Copy, Debug)]
526    struct Speed(u32);
527
528    /// Score, folded into the digest.
529    #[derive(Resource, Debug, Default, PartialEq, Eq)]
530    struct Score(u32);
531
532    /// One tick of gameplay: the only place a tick's intent is read.
533    fn play(
534        intent: Res<Intent<Press>>,
535        mut score: ResMut<Score>,
536        mut rng: ResMut<SimRng>,
537        mut bodies: Query<&mut Height>,
538        mut commands: Commands,
539    ) {
540        let Some(press) = intent.get() else {
541            return;
542        };
543        if press.up {
544            score.0 += 1;
545            for mut height in &mut bodies {
546                height.0 += 1.0;
547            }
548        }
549        if press.spawn {
550            let speed: u32 = rng.random_range(0_u32..4);
551            commands.spawn(Speed(speed));
552        }
553    }
554
555    /// What a replay must agree on: the bodies, the score, and the stream.
556    fn digest() -> WorldHasher {
557        let mut hasher = WorldHasher::new();
558        hasher.register::<Height>(|height, hash| hash.f32(height.0));
559        hasher.register::<Speed>(|speed, hash| hash.u32(speed.0));
560        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
561        hasher.register_resource::<SimRng>(|rng, hash| {
562            for word in rng.state_words() {
563                hash.u64(word);
564            }
565        });
566        hasher
567    }
568
569    /// A fresh world for one run, exactly as the recording run built it.
570    fn fresh(seed: u64) -> Setup {
571        let mut sim = Sim::with_default_step();
572        sim.world.insert_resource(WorldSeed::new(seed));
573        sim.world.insert_resource(SimRng::seeded(seed));
574        sim.world.insert_resource(Intent::<Press>::default());
575        sim.world.init_resource::<Score>();
576        sim.world.spawn(Height(0.0));
577        sim.add_system(play);
578        Setup {
579            sim,
580            hasher: digest(),
581        }
582    }
583
584    /// The greedy policy the tape is recorded from: a recorded tape exercises
585    /// states a hand-written script cannot know about.
586    fn press(tick: u64, score: u32) -> Press {
587        Press {
588            up: (score + tick as u32) % 3 != 0,
589            spawn: tick % 7 == 3,
590        }
591    }
592
593    fn record(seed: u64, ticks: u64) -> Tape<Press> {
594        let Setup { mut sim, hasher } = fresh(seed);
595        let mut tape = Tape::new(seed);
596        for tick in 0..ticks {
597            let input = press(tick, sim.world.resource::<Score>().0);
598            tape.record_intent(input);
599            sim.world.resource_mut::<Intent<Press>>().set(input);
600            sim.tick();
601            tape.record_hash(hasher.hash(&sim.world));
602        }
603        tape
604    }
605
606    #[test]
607    fn a_recorded_run_verifies_hash_for_hash() {
608        let tape = record(7, 60);
609        assert_eq!(tape.seed(), 7);
610        assert_eq!(tape.ticks(), 60);
611        assert_eq!(tape.hashes().len(), 60);
612        assert_eq!(tape.hash(0), Some(tape.hashes()[0]));
613        assert_eq!(tape.hash(60), None, "past the end there is nothing");
614
615        // The recording's own hashes are what an identical replay produces, and
616        // two replays of one tape agree tick for tick.
617        assert_eq!(tape.replay(|| fresh(7)), tape.hashes().to_vec());
618        assert_eq!(tape.replay(|| fresh(7)), tape.replay(|| fresh(7)));
619        assert!(tape.verify(|| fresh(7)).is_ok());
620    }
621
622    #[test]
623    fn a_different_seed_diverges_at_its_first_tick() {
624        let tape = record(7, 60);
625        let error = tape
626            .verify(|| fresh(8))
627            .expect_err("a different seed is a different run");
628        assert_eq!(error.tick(), Some(0), "the stream itself is already other");
629        match error {
630            VerifyError::Diverged {
631                divergence,
632                replayed,
633            } => {
634                assert_eq!(replayed.len(), 1, "the replay stops at the divergence");
635                assert_eq!(replayed[0], divergence.actual);
636                assert_ne!(divergence.expected, divergence.actual);
637                assert_eq!(divergence.expected, tape.hashes()[0]);
638            }
639            other => panic!("expected a divergence, got {other}"),
640        }
641    }
642
643    #[test]
644    fn a_perturbed_tick_is_the_reported_tick() {
645        let tape = record(7, 40);
646        let mut poisoned = tape.clone();
647        let edited_tick = 12;
648        let before = poisoned.intents()[edited_tick];
649        poisoned.intents_mut()[edited_tick] = Press {
650            up: !before.up,
651            spawn: before.spawn,
652        };
653
654        let error = poisoned
655            .verify(|| fresh(7))
656            .expect_err("the edited tick must change the world");
657        assert_eq!(error.tick(), Some(edited_tick as u64));
658        match error {
659            VerifyError::Diverged {
660                divergence,
661                replayed,
662            } => {
663                assert_eq!(divergence.tick, edited_tick as u64);
664                assert_eq!(divergence.expected, tape.hashes()[edited_tick]);
665                assert_eq!(divergence.actual, replayed[edited_tick]);
666                assert_ne!(divergence.expected, divergence.actual);
667                assert_eq!(
668                    replayed[..edited_tick],
669                    tape.hashes()[..edited_tick],
670                    "every tick before the edit agreed"
671                );
672            }
673            other => panic!("expected a divergence, got {other}"),
674        }
675    }
676
677    #[test]
678    fn an_edited_tick_that_changes_nothing_still_verifies() {
679        let tape = record(7, 40);
680        let mut edited = tape.clone();
681        // The first tick that pressed nothing: re-recording it as nothing is
682        // the one edit that cannot change the world.
683        let quiet = edited
684            .intents()
685            .iter()
686            .position(|press| !press.up && !press.spawn)
687            .expect("the policy leaves a tick quiet");
688        edited.intents_mut()[quiet] = Press {
689            up: false,
690            spawn: false,
691        };
692        assert!(edited.verify(|| fresh(7)).is_ok());
693    }
694
695    #[test]
696    fn a_tape_is_serialisable() {
697        // A tape travels: written next to the save it came from, or attached to
698        // a bug report. The crate carries no format, only the bounds.
699        fn assert_serde<T: Serialize + serde::de::DeserializeOwned>() {}
700        assert_serde::<Tape<Press>>();
701    }
702
703    #[test]
704    fn a_tape_without_hashes_replays_but_cannot_be_verified() {
705        let mut tape = Tape::<Press>::new(7);
706        assert!(tape.is_empty());
707        for tick in 0..8u64 {
708            let input = press(tick, tick as u32);
709            tape.record_intent(input);
710            assert_eq!(tape.intent(tick), Some(&input));
711        }
712        assert_eq!(tape.ticks(), 8);
713        assert_eq!(tape.replay(|| fresh(7)).len(), 8, "an intent tape replays");
714        let error = tape
715            .verify(|| fresh(7))
716            .expect_err("with no hashes there is nothing to check");
717        assert_eq!(
718            error,
719            VerifyError::UnhashedTicks {
720                ticks: 8,
721                hashes: 0
722            }
723        );
724        assert_eq!(error.tick(), None);
725    }
726
727    /// A game that keeps its own action resources, the way games predating the
728    /// tape do: `commit` is what feeds it.
729    #[derive(Resource, Default)]
730    struct Keys(Press);
731
732    fn play_keys(keys: Res<Keys>, mut score: ResMut<Score>) {
733        if keys.0.up {
734            score.0 += 1;
735        }
736    }
737
738    fn keys_world(seed: u64) -> Setup {
739        let mut sim = Sim::with_default_step();
740        sim.world.insert_resource(SimRng::seeded(seed));
741        sim.world.init_resource::<Keys>();
742        sim.world.init_resource::<Score>();
743        sim.add_system(play_keys);
744        let mut hasher = WorldHasher::new();
745        hasher.register_resource::<Keys>(|keys, hash| hash.bool(keys.0.up));
746        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
747        Setup { sim, hasher }
748    }
749
750    #[test]
751    fn a_commit_closure_feeds_a_game_with_its_own_input_resources() {
752        let mut fresh = || keys_world(3);
753        let Setup { mut sim, hasher } = fresh();
754        let mut tape = Tape::new(3);
755        for tick in 0..12u64 {
756            let input = Press {
757                up: tick % 2 == 0,
758                spawn: false,
759            };
760            tape.record_intent(input);
761            sim.world.resource_mut::<Keys>().0 = input;
762            sim.tick();
763            tape.record_hash(hasher.hash(&sim.world));
764        }
765
766        let commit = |world: &mut World, input: &Press| world.resource_mut::<Keys>().0 = *input;
767        assert_eq!(tape.replay_with(&mut fresh, commit), tape.hashes().to_vec());
768        assert!(tape.verify_with(&mut fresh, commit).is_ok());
769        assert!(
770            tape.verify(&mut fresh).is_err(),
771            "without the commit the game never sees the input"
772        );
773    }
774
775    #[test]
776    #[should_panic(expected = "no tick to belong to")]
777    fn record_hash_refuses_a_second_hash_for_one_tick() {
778        let mut tape = Tape::<Press>::new(7);
779        tape.record_intent(Press {
780            up: true,
781            spawn: false,
782        });
783        tape.record_hash(1);
784        tape.record_hash(2);
785    }
786
787    #[test]
788    fn intent_carries_one_tick_of_input() {
789        let mut intent = Intent::<Press>::default();
790        assert_eq!(intent.get(), None);
791        intent.set(Press {
792            up: true,
793            spawn: false,
794        });
795        assert_eq!(
796            intent.get(),
797            Some(&Press {
798                up: true,
799                spawn: false
800            })
801        );
802        intent.clear();
803        assert_eq!(intent.get(), None);
804    }
805
806    /// Rows are folded in entity order, not in the order `bevy_ecs` yields them.
807    /// The assertion on the raw iteration orders is what keeps this test honest:
808    /// if `bevy_ecs` ever stops iteration from depending on history, the test
809    /// says so instead of passing for the wrong reason.
810    #[test]
811    fn the_digest_does_not_depend_on_iteration_order() {
812        #[derive(Component)]
813        struct Mark;
814
815        fn rows(world: &mut World) -> Vec<u64> {
816            let mut query = world.query::<(Entity, &Height)>();
817            query
818                .iter(world)
819                .map(|(entity, _)| entity.to_bits())
820                .collect()
821        }
822
823        let mut a = World::new();
824        let first = a.spawn(Height(1.0)).id();
825        a.spawn(Height(2.0));
826
827        let mut b = World::new();
828        let also_first = b.spawn(Height(1.0)).id();
829        b.spawn(Height(2.0));
830        // Moving the first row out of the table and back leaves it at the end:
831        // same entity, same value, different position in `bevy_ecs`'s order.
832        b.entity_mut(also_first).insert(Mark);
833        b.entity_mut(also_first).remove::<Mark>();
834
835        assert_eq!(first, also_first, "both worlds allocate the same ids");
836        assert_ne!(
837            rows(&mut a),
838            rows(&mut b),
839            "the hazard this guards against must be real"
840        );
841
842        let mut hasher = WorldHasher::new();
843        hasher.register::<Height>(|height, hash| hash.f32(height.0));
844        assert_eq!(hasher.hash(&a), hasher.hash(&b));
845    }
846
847    #[test]
848    fn a_missing_resource_is_not_a_zero_valued_resource() {
849        let mut hasher = WorldHasher::new();
850        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
851
852        let mut present = World::new();
853        present.insert_resource(Score(0));
854        let absent = World::new();
855        assert_ne!(hasher.hash(&present), hasher.hash(&absent));
856    }
857
858    #[test]
859    fn a_registered_component_moves_the_digest() {
860        let mut hasher = WorldHasher::new();
861        hasher.register::<Height>(|height, hash| hash.f32(height.0));
862
863        let mut one = World::new();
864        one.spawn(Height(1.0));
865        let mut other = World::new();
866        other.spawn(Height(1.5));
867        assert_ne!(hasher.hash(&one), hasher.hash(&other));
868
869        let mut twin = World::new();
870        twin.spawn(Height(1.0));
871        assert_eq!(hasher.hash(&one), hasher.hash(&twin));
872
873        // State nobody registered is state the digest cannot report: two worlds
874        // with the same entities and the same registered components digest the
875        // same even though an unregistered component differs.
876        let mut quiet = World::new();
877        quiet.spawn(Speed(1));
878        let mut loud = World::new();
879        loud.spawn(Speed(2));
880        assert_eq!(hasher.hash(&quiet), hasher.hash(&loud));
881    }
882
883    #[test]
884    fn register_hashed_folds_a_derived_hash() {
885        #[derive(Component, Hash)]
886        struct Tally {
887            wins: u32,
888            losses: u32,
889        }
890
891        let mut hasher = WorldHasher::new();
892        hasher.register_hashed::<Tally>();
893
894        let mut one = World::new();
895        one.spawn(Tally { wins: 1, losses: 2 });
896        let mut twin = World::new();
897        twin.spawn(Tally { wins: 1, losses: 2 });
898        let mut other = World::new();
899        other.spawn(Tally { wins: 1, losses: 3 });
900        assert_eq!(hasher.hash(&one), hasher.hash(&twin));
901        assert_ne!(hasher.hash(&one), hasher.hash(&other));
902    }
903
904    #[test]
905    fn an_empty_hasher_still_sees_the_entity_list() {
906        let hasher = WorldHasher::new();
907        let mut one = World::new();
908        one.spawn_empty();
909        assert_eq!(hasher.hash(&World::new()), hasher.hash(&World::new()));
910        assert_ne!(hasher.hash(&one), hasher.hash(&World::new()));
911    }
912}