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 {
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 pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
379 self.register::<C>(|value, hash| value.hash(hash))
380 }
381
382 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub struct Divergence {
439 pub tick: u64,
444 pub expected: u64,
446 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#[derive(Clone, Debug, PartialEq, Eq)]
462pub enum VerifyError {
463 UnhashedTicks {
467 ticks: u64,
469 hashes: u64,
471 },
472 Diverged {
474 divergence: Divergence,
476 replayed: Vec<u64>,
479 },
480}
481
482impl VerifyError {
483 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 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
514 struct Press {
515 up: bool,
517 spawn: bool,
519 }
520
521 #[derive(Component, Clone, Copy, Debug)]
523 struct Height(f32);
524
525 #[derive(Component, Clone, Copy, Debug)]
527 struct Speed(u32);
528
529 #[derive(Resource, Debug, Default, PartialEq, Eq)]
531 struct Score(u32);
532
533 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 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 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 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 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 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 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 #[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 #[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 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 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}