1use 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
56const WORLD_TAG: &str = "repame-sim/world";
58
59#[derive(Resource)]
67pub struct Intent<I>(Option<I>);
68
69impl<I> Intent<I> {
70 pub fn get(&self) -> Option<&I> {
72 self.0.as_ref()
73 }
74
75 pub fn set(&mut self, input: I) {
77 self.0 = Some(input);
78 }
79
80 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#[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 pub const fn new(seed: u64) -> Self {
112 Self {
113 seed,
114 intents: Vec::new(),
115 hashes: Vec::new(),
116 }
117 }
118
119 pub const fn seed(&self) -> u64 {
121 self.seed
122 }
123
124 pub fn ticks(&self) -> u64 {
126 self.intents.len() as u64
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.intents.is_empty()
132 }
133
134 pub fn intents(&self) -> &[I] {
136 &self.intents
137 }
138
139 pub fn intents_mut(&mut self) -> &mut [I] {
145 &mut self.intents
146 }
147
148 pub fn hashes(&self) -> &[u64] {
151 &self.hashes
152 }
153
154 pub fn intent(&self, tick: u64) -> Option<&I> {
156 self.intents.get(tick as usize)
157 }
158
159 pub fn hash(&self, tick: u64) -> Option<u64> {
161 self.hashes.get(tick as usize).copied()
162 }
163
164 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 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 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 pub fn replay<F>(&self, setup: F) -> Vec<u64>
226 where
227 F: FnMut() -> Setup,
228 {
229 self.replay_with(setup, |_, _| {})
230 }
231
232 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 pub fn verify<F>(&self, setup: F) -> Result<(), VerifyError>
267 where
268 F: FnMut() -> Setup,
269 {
270 self.verify_with(setup, |_, _| {})
271 }
272
273 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
308pub struct Setup {
313 pub sim: Sim,
315 pub hasher: WorldHasher,
318}
319
320#[derive(Clone, Default)]
333pub struct WorldHasher {
334 entries: Vec<Arc<dyn Fn(&World, &mut Hash64) + Send + Sync>>,
335}
336
337impl WorldHasher {
338 pub fn new() -> Self {
340 Self {
341 entries: Vec::new(),
342 }
343 }
344
345 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 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 pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
387 self.register::<C>(|value, hash| value.hash(hash))
388 }
389
390 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
449pub struct Divergence {
450 pub tick: u64,
455 pub expected: u64,
457 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#[derive(Clone, Debug, PartialEq, Eq)]
473pub enum VerifyError {
474 UnhashedTicks {
478 ticks: u64,
480 hashes: u64,
482 },
483 Diverged {
485 divergence: Divergence,
487 replayed: Vec<u64>,
490 },
491}
492
493impl VerifyError {
494 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 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
525 struct Press {
526 up: bool,
528 spawn: bool,
530 }
531
532 #[derive(Component, Clone, Copy, Debug)]
534 struct Height(f32);
535
536 #[derive(Component, Clone, Copy, Debug)]
538 struct Speed(u32);
539
540 #[derive(Resource, Debug, Default, PartialEq, Eq)]
542 struct Score(u32);
543
544 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 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 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 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 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 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 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 #[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 #[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 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 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}