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