1use 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
47const WORLD_TAG: &str = "repame-sim/world";
49
50#[derive(Resource)]
58pub struct Intent<I>(Option<I>);
59
60impl<I> Intent<I> {
61 pub fn get(&self) -> Option<&I> {
63 self.0.as_ref()
64 }
65
66 pub fn set(&mut self, input: I) {
68 self.0 = Some(input);
69 }
70
71 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#[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 pub const fn new(seed: u64) -> Self {
103 Self {
104 seed,
105 intents: Vec::new(),
106 hashes: Vec::new(),
107 }
108 }
109
110 pub const fn seed(&self) -> u64 {
112 self.seed
113 }
114
115 pub fn ticks(&self) -> u64 {
117 self.intents.len() as u64
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.intents.is_empty()
123 }
124
125 pub fn intents(&self) -> &[I] {
127 &self.intents
128 }
129
130 pub fn intents_mut(&mut self) -> &mut [I] {
136 &mut self.intents
137 }
138
139 pub fn hashes(&self) -> &[u64] {
142 &self.hashes
143 }
144
145 pub fn intent(&self, tick: u64) -> Option<&I> {
147 self.intents.get(tick as usize)
148 }
149
150 pub fn hash(&self, tick: u64) -> Option<u64> {
152 self.hashes.get(tick as usize).copied()
153 }
154
155 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 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 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 pub fn replay<F>(&self, setup: F) -> Vec<u64>
217 where
218 F: FnMut() -> Setup,
219 {
220 self.replay_with(setup, |_, _| {})
221 }
222
223 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 pub fn verify<F>(&self, setup: F) -> Result<(), VerifyError>
258 where
259 F: FnMut() -> Setup,
260 {
261 self.verify_with(setup, |_, _| {})
262 }
263
264 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
299pub struct Setup {
304 pub sim: Sim,
306 pub hasher: WorldHasher,
309}
310
311#[derive(Clone, Default)]
324pub struct WorldHasher {
325 entries: Vec<Arc<dyn Fn(&World, &mut Hash64) + Send + Sync>>,
326}
327
328impl WorldHasher {
329 pub fn new() -> Self {
331 Self {
332 entries: Vec::new(),
333 }
334 }
335
336 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 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 pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
378 self.register::<C>(|value, hash| value.hash(hash))
379 }
380
381 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub struct Divergence {
438 pub tick: u64,
443 pub expected: u64,
445 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#[derive(Clone, Debug, PartialEq, Eq)]
461pub enum VerifyError {
462 UnhashedTicks {
466 ticks: u64,
468 hashes: u64,
470 },
471 Diverged {
473 divergence: Divergence,
475 replayed: Vec<u64>,
478 },
479}
480
481impl VerifyError {
482 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 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
513 struct Press {
514 up: bool,
516 spawn: bool,
518 }
519
520 #[derive(Component, Clone, Copy, Debug)]
522 struct Height(f32);
523
524 #[derive(Component, Clone, Copy, Debug)]
526 struct Speed(u32);
527
528 #[derive(Resource, Debug, Default, PartialEq, Eq)]
530 struct Score(u32);
531
532 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 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 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 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 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 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 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 #[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 #[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 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 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}