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.
348                // Folding the zero count keeps that from hashing like an entry
349                // that is missing altogether.
350                let Some(mut query) = world.try_query::<(Entity, &C)>() else {
351                    hash.u64(0);
352                    return;
353                };
354                let mut rows: Vec<(u64, u64)> = Vec::new();
355                for (entity, value) in query.iter(world) {
356                    let mut row = Hash64::new();
357                    hash_value(value, &mut row);
358                    rows.push((entity.to_bits(), row.digest()));
359                }
360                rows.sort_unstable_by_key(|(entity, _)| *entity);
361                hash.u64(rows.len() as u64);
362                for (entity, value) in rows {
363                    hash.u64(entity);
364                    hash.u64(value);
365                }
366            }));
367        self
368    }
369
370    /// [`register`](Self::register) for a component whose own `Hash`
371    /// implementation is yours.
372    ///
373    /// The fold is whatever `Hash` writes, and every integer writer is pinned by
374    /// [`Hash64`], but a `Hash` impl from `std` or from a dependency may change
375    /// its byte layout in a later release. For a digest a save file or a golden
376    /// test pins, write the fields explicitly with
377    /// [`register`](Self::register).
378    pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
379        self.register::<C>(|value, hash| value.hash(hash))
380    }
381
382    /// Registers a resource: a presence byte, then the value hash when it
383    /// exists.
384    ///
385    /// Presence is state of its own, so a resource missing from one run and
386    /// present in the other folds differently even when its payload is a zero.
387    pub fn register_resource<R: Resource>(
388        &mut self,
389        hash_value: impl Fn(&R, &mut Hash64) + Send + Sync + 'static,
390    ) -> &mut Self {
391        self.entries.push(Arc::new(
392            move |world: &World, hash: &mut Hash64| match world.get_resource::<R>() {
393                Some(value) => {
394                    let mut presence = Hash64::new();
395                    hash_value(value, &mut presence);
396                    hash.u8(1);
397                    hash.u64(presence.digest());
398                }
399                None => hash.u8(0),
400            },
401        ));
402        self
403    }
404
405    /// [`register_resource`](Self::register_resource) for a resource whose own
406    /// `Hash` implementation is yours.
407    pub fn register_hashed_resource<R: Resource + Hash>(&mut self) -> &mut Self {
408        self.register_resource::<R>(|value, hash| value.hash(hash))
409    }
410
411    /// The canonical digest of `world`: one number that moves whenever any
412    /// registered state moves.
413    ///
414    /// Folds in the order the module docs pin, which is not `bevy_ecs`'s
415    /// iteration order, so a run that reaches the same state through a different
416    /// spawn/despawn history digests the same.
417    pub fn hash(&self, world: &World) -> u64 {
418        let mut hash = Hash64::new();
419        hash.str(WORLD_TAG);
420        let mut entities: Vec<u64> = world
421            .iter_entities()
422            .map(|entity| entity.id().to_bits())
423            .collect();
424        entities.sort_unstable();
425        hash.usize(entities.len());
426        for bits in entities {
427            hash.u64(bits);
428        }
429        for entry in &self.entries {
430            entry(world, &mut hash);
431        }
432        hash.digest()
433    }
434}
435
436/// The first tick a replay disagreed with the recording at.
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub struct Divergence {
439    /// Tick the two runs first differed on. Ticks before it hashed the same, so
440    /// the cause is in what this tick's systems read or in the intent it took:
441    /// [`Tape::intent`] has that tick's input, and the recording's own hash log
442    /// is one [`Tape::hash`] call away.
443    pub tick: u64,
444    /// Hash the recording left for this tick.
445    pub expected: u64,
446    /// Hash this replay produced for it.
447    pub actual: u64,
448}
449
450impl fmt::Display for Divergence {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        write!(
453            f,
454            "tick {}: recorded {:#018x}, replayed {:#018x}",
455            self.tick, self.expected, self.actual
456        )
457    }
458}
459
460/// Why a tape could not be checked.
461#[derive(Clone, Debug, PartialEq, Eq)]
462pub enum VerifyError {
463    /// The tape's hash log does not have one entry per tick, so a replay cannot
464    /// be checked tick for tick. An intent-only tape (replayed for its visuals,
465    /// say) lands here.
466    UnhashedTicks {
467        /// Ticks recorded.
468        ticks: u64,
469        /// Hashes recorded.
470        hashes: u64,
471    },
472    /// The replay's world differed from the recording's.
473    Diverged {
474        /// The first tick that differed, with both hashes.
475        divergence: Divergence,
476        /// Every hash this replay produced, up to and including the diverging
477        /// tick: how far the two runs agreed before this.
478        replayed: Vec<u64>,
479    },
480}
481
482impl VerifyError {
483    /// The first divergent tick, when the failure was a divergence rather than a
484    /// tape that cannot be checked.
485    pub fn tick(&self) -> Option<u64> {
486        match self {
487            Self::Diverged { divergence, .. } => Some(divergence.tick),
488            Self::UnhashedTicks { .. } => None,
489        }
490    }
491}
492
493impl fmt::Display for VerifyError {
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        match self {
496            Self::UnhashedTicks { ticks, hashes } => write!(
497                f,
498                "tape has {ticks} ticks and {hashes} hashes; verify needs one hash per tick"
499            ),
500            Self::Diverged { divergence, .. } => write!(f, "diverged at {divergence}"),
501        }
502    }
503}
504
505impl std::error::Error for VerifyError {}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use crate::rng::{Rng, SimRng, WorldSeed};
511
512    /// One tick of recorded input: what a player could ask for.
513    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
514    struct Press {
515        /// Hold the accelerator.
516        up: bool,
517        /// Spawn one body.
518        spawn: bool,
519    }
520
521    /// A body's height, folded into the digest.
522    #[derive(Component, Clone, Copy, Debug)]
523    struct Height(f32);
524
525    /// A spawned body's speed, folded into the digest.
526    #[derive(Component, Clone, Copy, Debug)]
527    struct Speed(u32);
528
529    /// Score, folded into the digest.
530    #[derive(Resource, Debug, Default, PartialEq, Eq)]
531    struct Score(u32);
532
533    /// One tick of gameplay: the only place a tick's intent is read.
534    fn play(
535        intent: Res<Intent<Press>>,
536        mut score: ResMut<Score>,
537        mut rng: ResMut<SimRng>,
538        mut bodies: Query<&mut Height>,
539        mut commands: Commands,
540    ) {
541        let Some(press) = intent.get() else {
542            return;
543        };
544        if press.up {
545            score.0 += 1;
546            for mut height in &mut bodies {
547                height.0 += 1.0;
548            }
549        }
550        if press.spawn {
551            let speed: u32 = rng.random_range(0_u32..4);
552            commands.spawn(Speed(speed));
553        }
554    }
555
556    /// What a replay must agree on: the bodies, the score, and the stream.
557    fn digest() -> WorldHasher {
558        let mut hasher = WorldHasher::new();
559        hasher.register::<Height>(|height, hash| hash.f32(height.0));
560        hasher.register::<Speed>(|speed, hash| hash.u32(speed.0));
561        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
562        hasher.register_resource::<SimRng>(|rng, hash| {
563            for word in rng.state_words() {
564                hash.u64(word);
565            }
566        });
567        hasher
568    }
569
570    /// A fresh world for one run, exactly as the recording run built it.
571    fn fresh(seed: u64) -> Setup {
572        let mut sim = Sim::with_default_step();
573        sim.world.insert_resource(WorldSeed::new(seed));
574        sim.world.insert_resource(SimRng::seeded(seed));
575        sim.world.insert_resource(Intent::<Press>::default());
576        sim.world.init_resource::<Score>();
577        sim.world.spawn(Height(0.0));
578        sim.add_system(play);
579        Setup {
580            sim,
581            hasher: digest(),
582        }
583    }
584
585    /// The greedy policy the tape is recorded from: a recorded tape exercises
586    /// states a hand-written script cannot know about.
587    fn press(tick: u64, score: u32) -> Press {
588        Press {
589            up: (score + tick as u32) % 3 != 0,
590            spawn: tick % 7 == 3,
591        }
592    }
593
594    fn record(seed: u64, ticks: u64) -> Tape<Press> {
595        let Setup { mut sim, hasher } = fresh(seed);
596        let mut tape = Tape::new(seed);
597        for tick in 0..ticks {
598            let input = press(tick, sim.world.resource::<Score>().0);
599            tape.record_intent(input);
600            sim.world.resource_mut::<Intent<Press>>().set(input);
601            sim.tick();
602            tape.record_hash(hasher.hash(&sim.world));
603        }
604        tape
605    }
606
607    #[test]
608    fn a_recorded_run_verifies_hash_for_hash() {
609        let tape = record(7, 60);
610        assert_eq!(tape.seed(), 7);
611        assert_eq!(tape.ticks(), 60);
612        assert_eq!(tape.hashes().len(), 60);
613        assert_eq!(tape.hash(0), Some(tape.hashes()[0]));
614        assert_eq!(tape.hash(60), None, "past the end there is nothing");
615
616        // The recording's own hashes are what an identical replay produces, and
617        // two replays of one tape agree tick for tick.
618        assert_eq!(tape.replay(|| fresh(7)), tape.hashes().to_vec());
619        assert_eq!(tape.replay(|| fresh(7)), tape.replay(|| fresh(7)));
620        assert!(tape.verify(|| fresh(7)).is_ok());
621    }
622
623    #[test]
624    fn a_different_seed_diverges_at_its_first_tick() {
625        let tape = record(7, 60);
626        let error = tape
627            .verify(|| fresh(8))
628            .expect_err("a different seed is a different run");
629        assert_eq!(error.tick(), Some(0), "the stream itself is already other");
630        match error {
631            VerifyError::Diverged {
632                divergence,
633                replayed,
634            } => {
635                assert_eq!(replayed.len(), 1, "the replay stops at the divergence");
636                assert_eq!(replayed[0], divergence.actual);
637                assert_ne!(divergence.expected, divergence.actual);
638                assert_eq!(divergence.expected, tape.hashes()[0]);
639            }
640            other => panic!("expected a divergence, got {other}"),
641        }
642    }
643
644    #[test]
645    fn a_perturbed_tick_is_the_reported_tick() {
646        let tape = record(7, 40);
647        let mut poisoned = tape.clone();
648        let edited_tick = 12;
649        let before = poisoned.intents()[edited_tick];
650        poisoned.intents_mut()[edited_tick] = Press {
651            up: !before.up,
652            spawn: before.spawn,
653        };
654
655        let error = poisoned
656            .verify(|| fresh(7))
657            .expect_err("the edited tick must change the world");
658        assert_eq!(error.tick(), Some(edited_tick as u64));
659        match error {
660            VerifyError::Diverged {
661                divergence,
662                replayed,
663            } => {
664                assert_eq!(divergence.tick, edited_tick as u64);
665                assert_eq!(divergence.expected, tape.hashes()[edited_tick]);
666                assert_eq!(divergence.actual, replayed[edited_tick]);
667                assert_ne!(divergence.expected, divergence.actual);
668                assert_eq!(
669                    replayed[..edited_tick],
670                    tape.hashes()[..edited_tick],
671                    "every tick before the edit agreed"
672                );
673            }
674            other => panic!("expected a divergence, got {other}"),
675        }
676    }
677
678    #[test]
679    fn an_edited_tick_that_changes_nothing_still_verifies() {
680        let tape = record(7, 40);
681        let mut edited = tape.clone();
682        // The first tick that pressed nothing: re-recording it as nothing is
683        // the one edit that cannot change the world.
684        let quiet = edited
685            .intents()
686            .iter()
687            .position(|press| !press.up && !press.spawn)
688            .expect("the policy leaves a tick quiet");
689        edited.intents_mut()[quiet] = Press {
690            up: false,
691            spawn: false,
692        };
693        assert!(edited.verify(|| fresh(7)).is_ok());
694    }
695
696    #[test]
697    fn a_tape_is_serialisable() {
698        // A tape travels: written next to the save it came from, or attached to
699        // a bug report. The crate carries no format, only the bounds.
700        fn assert_serde<T: Serialize + serde::de::DeserializeOwned>() {}
701        assert_serde::<Tape<Press>>();
702    }
703
704    #[test]
705    fn a_tape_without_hashes_replays_but_cannot_be_verified() {
706        let mut tape = Tape::<Press>::new(7);
707        assert!(tape.is_empty());
708        for tick in 0..8u64 {
709            let input = press(tick, tick as u32);
710            tape.record_intent(input);
711            assert_eq!(tape.intent(tick), Some(&input));
712        }
713        assert_eq!(tape.ticks(), 8);
714        assert_eq!(tape.replay(|| fresh(7)).len(), 8, "an intent tape replays");
715        let error = tape
716            .verify(|| fresh(7))
717            .expect_err("with no hashes there is nothing to check");
718        assert_eq!(
719            error,
720            VerifyError::UnhashedTicks {
721                ticks: 8,
722                hashes: 0
723            }
724        );
725        assert_eq!(error.tick(), None);
726    }
727
728    /// A game that keeps its own action resources, the way games predating the
729    /// tape do: `commit` is what feeds it.
730    #[derive(Resource, Default)]
731    struct Keys(Press);
732
733    fn play_keys(keys: Res<Keys>, mut score: ResMut<Score>) {
734        if keys.0.up {
735            score.0 += 1;
736        }
737    }
738
739    fn keys_world(seed: u64) -> Setup {
740        let mut sim = Sim::with_default_step();
741        sim.world.insert_resource(SimRng::seeded(seed));
742        sim.world.init_resource::<Keys>();
743        sim.world.init_resource::<Score>();
744        sim.add_system(play_keys);
745        let mut hasher = WorldHasher::new();
746        hasher.register_resource::<Keys>(|keys, hash| hash.bool(keys.0.up));
747        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
748        Setup { sim, hasher }
749    }
750
751    #[test]
752    fn a_commit_closure_feeds_a_game_with_its_own_input_resources() {
753        let mut fresh = || keys_world(3);
754        let Setup { mut sim, hasher } = fresh();
755        let mut tape = Tape::new(3);
756        for tick in 0..12u64 {
757            let input = Press {
758                up: tick % 2 == 0,
759                spawn: false,
760            };
761            tape.record_intent(input);
762            sim.world.resource_mut::<Keys>().0 = input;
763            sim.tick();
764            tape.record_hash(hasher.hash(&sim.world));
765        }
766
767        let commit = |world: &mut World, input: &Press| world.resource_mut::<Keys>().0 = *input;
768        assert_eq!(tape.replay_with(&mut fresh, commit), tape.hashes().to_vec());
769        assert!(tape.verify_with(&mut fresh, commit).is_ok());
770        assert!(
771            tape.verify(&mut fresh).is_err(),
772            "without the commit the game never sees the input"
773        );
774    }
775
776    #[test]
777    #[should_panic(expected = "no tick to belong to")]
778    fn record_hash_refuses_a_second_hash_for_one_tick() {
779        let mut tape = Tape::<Press>::new(7);
780        tape.record_intent(Press {
781            up: true,
782            spawn: false,
783        });
784        tape.record_hash(1);
785        tape.record_hash(2);
786    }
787
788    #[test]
789    fn intent_carries_one_tick_of_input() {
790        let mut intent = Intent::<Press>::default();
791        assert_eq!(intent.get(), None);
792        intent.set(Press {
793            up: true,
794            spawn: false,
795        });
796        assert_eq!(
797            intent.get(),
798            Some(&Press {
799                up: true,
800                spawn: false
801            })
802        );
803        intent.clear();
804        assert_eq!(intent.get(), None);
805    }
806
807    /// Rows are folded in entity order, not in the order `bevy_ecs` yields them.
808    /// The assertion on the raw iteration orders is what keeps this test honest:
809    /// if `bevy_ecs` ever stops iteration from depending on history, the test
810    /// says so instead of passing for the wrong reason.
811    #[test]
812    fn the_digest_does_not_depend_on_iteration_order() {
813        #[derive(Component)]
814        struct Mark;
815
816        fn rows(world: &mut World) -> Vec<u64> {
817            let mut query = world.query::<(Entity, &Height)>();
818            query
819                .iter(world)
820                .map(|(entity, _)| entity.to_bits())
821                .collect()
822        }
823
824        let mut a = World::new();
825        let first = a.spawn(Height(1.0)).id();
826        a.spawn(Height(2.0));
827
828        let mut b = World::new();
829        let also_first = b.spawn(Height(1.0)).id();
830        b.spawn(Height(2.0));
831        // Moving the first row out of the table and back leaves it at the end:
832        // same entity, same value, different position in `bevy_ecs`'s order.
833        b.entity_mut(also_first).insert(Mark);
834        b.entity_mut(also_first).remove::<Mark>();
835
836        assert_eq!(first, also_first, "both worlds allocate the same ids");
837        assert_ne!(
838            rows(&mut a),
839            rows(&mut b),
840            "the hazard this guards against must be real"
841        );
842
843        let mut hasher = WorldHasher::new();
844        hasher.register::<Height>(|height, hash| hash.f32(height.0));
845        assert_eq!(hasher.hash(&a), hasher.hash(&b));
846    }
847
848    #[test]
849    fn a_missing_resource_is_not_a_zero_valued_resource() {
850        let mut hasher = WorldHasher::new();
851        hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
852
853        let mut present = World::new();
854        present.insert_resource(Score(0));
855        let absent = World::new();
856        assert_ne!(hasher.hash(&present), hasher.hash(&absent));
857    }
858
859    #[test]
860    fn a_registered_component_moves_the_digest() {
861        let mut hasher = WorldHasher::new();
862        hasher.register::<Height>(|height, hash| hash.f32(height.0));
863
864        let mut one = World::new();
865        one.spawn(Height(1.0));
866        let mut other = World::new();
867        other.spawn(Height(1.5));
868        assert_ne!(hasher.hash(&one), hasher.hash(&other));
869
870        let mut twin = World::new();
871        twin.spawn(Height(1.0));
872        assert_eq!(hasher.hash(&one), hasher.hash(&twin));
873
874        // State nobody registered is state the digest cannot report: two worlds
875        // with the same entities and the same registered components digest the
876        // same even though an unregistered component differs.
877        let mut quiet = World::new();
878        quiet.spawn(Speed(1));
879        let mut loud = World::new();
880        loud.spawn(Speed(2));
881        assert_eq!(hasher.hash(&quiet), hasher.hash(&loud));
882    }
883
884    #[test]
885    fn register_hashed_folds_a_derived_hash() {
886        #[derive(Component, Hash)]
887        struct Tally {
888            wins: u32,
889            losses: u32,
890        }
891
892        let mut hasher = WorldHasher::new();
893        hasher.register_hashed::<Tally>();
894
895        let mut one = World::new();
896        one.spawn(Tally { wins: 1, losses: 2 });
897        let mut twin = World::new();
898        twin.spawn(Tally { wins: 1, losses: 2 });
899        let mut other = World::new();
900        other.spawn(Tally { wins: 1, losses: 3 });
901        assert_eq!(hasher.hash(&one), hasher.hash(&twin));
902        assert_ne!(hasher.hash(&one), hasher.hash(&other));
903    }
904
905    #[test]
906    fn an_empty_hasher_still_sees_the_entity_list() {
907        let hasher = WorldHasher::new();
908        let mut one = World::new();
909        one.spawn_empty();
910        assert_eq!(hasher.hash(&World::new()), hasher.hash(&World::new()));
911        assert_ne!(hasher.hash(&one), hasher.hash(&World::new()));
912    }
913}