use std::fmt;
use std::hash::Hash;
use std::sync::Arc;
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
use crate::Sim;
use crate::hash::Hash64;
const WORLD_TAG: &str = "repame-sim/world";
#[derive(Resource)]
pub struct Intent<I>(Option<I>);
impl<I> Intent<I> {
pub fn get(&self) -> Option<&I> {
self.0.as_ref()
}
pub fn set(&mut self, input: I) {
self.0 = Some(input);
}
pub fn clear(&mut self) {
self.0 = None;
}
}
impl<I> Default for Intent<I> {
fn default() -> Self {
Self(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tape<I> {
seed: u64,
intents: Vec<I>,
hashes: Vec<u64>,
}
impl<I> Tape<I> {
pub const fn new(seed: u64) -> Self {
Self {
seed,
intents: Vec::new(),
hashes: Vec::new(),
}
}
pub const fn seed(&self) -> u64 {
self.seed
}
pub fn ticks(&self) -> u64 {
self.intents.len() as u64
}
pub fn is_empty(&self) -> bool {
self.intents.is_empty()
}
pub fn intents(&self) -> &[I] {
&self.intents
}
pub fn intents_mut(&mut self) -> &mut [I] {
&mut self.intents
}
pub fn hashes(&self) -> &[u64] {
&self.hashes
}
pub fn intent(&self, tick: u64) -> Option<&I> {
self.intents.get(tick as usize)
}
pub fn hash(&self, tick: u64) -> Option<u64> {
self.hashes.get(tick as usize).copied()
}
pub fn record_intent(&mut self, intent: I) -> u64 {
let tick = self.intents.len() as u64;
self.intents.push(intent);
tick
}
pub fn record_hash(&mut self, world_hash: u64) {
assert!(
self.hashes.len() < self.intents.len(),
"record_hash with no tick to belong to: {} ticks, {} hashes",
self.intents.len(),
self.hashes.len()
);
self.hashes.push(world_hash);
}
fn step(
&self,
sim: &mut Sim,
hasher: &WorldHasher,
tick: usize,
commit: &mut impl FnMut(&mut World, &I),
) -> u64
where
I: Clone + Send + Sync + 'static,
{
let intent = &self.intents[tick];
sim.world.resource_mut::<Intent<I>>().set(intent.clone());
commit(&mut sim.world, intent);
sim.tick();
hasher.hash(&sim.world)
}
}
impl<I> Tape<I>
where
I: Clone + Send + Sync + 'static,
{
pub fn replay<F>(&self, setup: F) -> Vec<u64>
where
F: FnMut() -> Setup,
{
self.replay_with(setup, |_, _| {})
}
pub fn replay_with<F, C>(&self, mut setup: F, mut commit: C) -> Vec<u64>
where
F: FnMut() -> Setup,
C: FnMut(&mut World, &I),
{
let Setup { mut sim, hasher } = setup();
sim.world.insert_resource(Intent::<I>::default());
let mut hashes = Vec::with_capacity(self.intents.len());
for tick in 0..self.intents.len() {
hashes.push(self.step(&mut sim, &hasher, tick, &mut commit));
}
hashes
}
pub fn verify<F>(&self, setup: F) -> Result<(), VerifyError>
where
F: FnMut() -> Setup,
{
self.verify_with(setup, |_, _| {})
}
pub fn verify_with<F, C>(&self, mut setup: F, mut commit: C) -> Result<(), VerifyError>
where
F: FnMut() -> Setup,
C: FnMut(&mut World, &I),
{
if self.hashes.len() != self.intents.len() {
return Err(VerifyError::UnhashedTicks {
ticks: self.intents.len() as u64,
hashes: self.hashes.len() as u64,
});
}
let Setup { mut sim, hasher } = setup();
sim.world.insert_resource(Intent::<I>::default());
let mut replayed = Vec::with_capacity(self.intents.len());
for tick in 0..self.intents.len() {
let actual = self.step(&mut sim, &hasher, tick, &mut commit);
let expected = self.hashes[tick];
replayed.push(actual);
if expected != actual {
return Err(VerifyError::Diverged {
divergence: Divergence {
tick: tick as u64,
expected,
actual,
},
replayed,
});
}
}
Ok(())
}
}
pub struct Setup {
pub sim: Sim,
pub hasher: WorldHasher,
}
#[derive(Clone, Default)]
pub struct WorldHasher {
entries: Vec<Arc<dyn Fn(&World, &mut Hash64) + Send + Sync>>,
}
impl WorldHasher {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn register<C: Component>(
&mut self,
hash_value: impl Fn(&C, &mut Hash64) + Send + Sync + 'static,
) -> &mut Self {
self.entries
.push(Arc::new(move |world: &World, hash: &mut Hash64| {
let Some(mut query) = world.try_query::<(Entity, &C)>() else {
hash.u64(0);
return;
};
let mut rows: Vec<(u64, u64)> = Vec::new();
for (entity, value) in query.iter(world) {
let mut row = Hash64::new();
hash_value(value, &mut row);
rows.push((entity.to_bits(), row.digest()));
}
rows.sort_unstable_by_key(|(entity, _)| *entity);
hash.u64(rows.len() as u64);
for (entity, value) in rows {
hash.u64(entity);
hash.u64(value);
}
}));
self
}
pub fn register_hashed<C: Component + Hash>(&mut self) -> &mut Self {
self.register::<C>(|value, hash| value.hash(hash))
}
pub fn register_resource<R: Resource>(
&mut self,
hash_value: impl Fn(&R, &mut Hash64) + Send + Sync + 'static,
) -> &mut Self {
self.entries.push(Arc::new(
move |world: &World, hash: &mut Hash64| match world.get_resource::<R>() {
Some(value) => {
let mut presence = Hash64::new();
hash_value(value, &mut presence);
hash.u8(1);
hash.u64(presence.digest());
}
None => hash.u8(0),
},
));
self
}
pub fn register_hashed_resource<R: Resource + Hash>(&mut self) -> &mut Self {
self.register_resource::<R>(|value, hash| value.hash(hash))
}
pub fn hash(&self, world: &World) -> u64 {
let mut hash = Hash64::new();
hash.str(WORLD_TAG);
let mut entities: Vec<u64> = world
.iter_entities()
.map(|entity| entity.id().to_bits())
.collect();
entities.sort_unstable();
hash.usize(entities.len());
for bits in entities {
hash.u64(bits);
}
for entry in &self.entries {
entry(world, &mut hash);
}
hash.digest()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Divergence {
pub tick: u64,
pub expected: u64,
pub actual: u64,
}
impl fmt::Display for Divergence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"tick {}: recorded {:#018x}, replayed {:#018x}",
self.tick, self.expected, self.actual
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VerifyError {
UnhashedTicks {
ticks: u64,
hashes: u64,
},
Diverged {
divergence: Divergence,
replayed: Vec<u64>,
},
}
impl VerifyError {
pub fn tick(&self) -> Option<u64> {
match self {
Self::Diverged { divergence, .. } => Some(divergence.tick),
Self::UnhashedTicks { .. } => None,
}
}
}
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnhashedTicks { ticks, hashes } => write!(
f,
"tape has {ticks} ticks and {hashes} hashes; verify needs one hash per tick"
),
Self::Diverged { divergence, .. } => write!(f, "diverged at {divergence}"),
}
}
}
impl std::error::Error for VerifyError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::{Rng, SimRng, WorldSeed};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
struct Press {
up: bool,
spawn: bool,
}
#[derive(Component, Clone, Copy, Debug)]
struct Height(f32);
#[derive(Component, Clone, Copy, Debug)]
struct Speed(u32);
#[derive(Resource, Debug, Default, PartialEq, Eq)]
struct Score(u32);
fn play(
intent: Res<Intent<Press>>,
mut score: ResMut<Score>,
mut rng: ResMut<SimRng>,
mut bodies: Query<&mut Height>,
mut commands: Commands,
) {
let Some(press) = intent.get() else {
return;
};
if press.up {
score.0 += 1;
for mut height in &mut bodies {
height.0 += 1.0;
}
}
if press.spawn {
let speed: u32 = rng.random_range(0_u32..4);
commands.spawn(Speed(speed));
}
}
fn digest() -> WorldHasher {
let mut hasher = WorldHasher::new();
hasher.register::<Height>(|height, hash| hash.f32(height.0));
hasher.register::<Speed>(|speed, hash| hash.u32(speed.0));
hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
hasher.register_resource::<SimRng>(|rng, hash| {
for word in rng.state_words() {
hash.u64(word);
}
});
hasher
}
fn fresh(seed: u64) -> Setup {
let mut sim = Sim::with_default_step();
sim.world.insert_resource(WorldSeed::new(seed));
sim.world.insert_resource(SimRng::seeded(seed));
sim.world.insert_resource(Intent::<Press>::default());
sim.world.init_resource::<Score>();
sim.world.spawn(Height(0.0));
sim.add_system(play);
Setup {
sim,
hasher: digest(),
}
}
fn press(tick: u64, score: u32) -> Press {
Press {
up: (score + tick as u32) % 3 != 0,
spawn: tick % 7 == 3,
}
}
fn record(seed: u64, ticks: u64) -> Tape<Press> {
let Setup { mut sim, hasher } = fresh(seed);
let mut tape = Tape::new(seed);
for tick in 0..ticks {
let input = press(tick, sim.world.resource::<Score>().0);
tape.record_intent(input);
sim.world.resource_mut::<Intent<Press>>().set(input);
sim.tick();
tape.record_hash(hasher.hash(&sim.world));
}
tape
}
#[test]
fn a_recorded_run_verifies_hash_for_hash() {
let tape = record(7, 60);
assert_eq!(tape.seed(), 7);
assert_eq!(tape.ticks(), 60);
assert_eq!(tape.hashes().len(), 60);
assert_eq!(tape.hash(0), Some(tape.hashes()[0]));
assert_eq!(tape.hash(60), None, "past the end there is nothing");
assert_eq!(tape.replay(|| fresh(7)), tape.hashes().to_vec());
assert_eq!(tape.replay(|| fresh(7)), tape.replay(|| fresh(7)));
assert!(tape.verify(|| fresh(7)).is_ok());
}
#[test]
fn a_different_seed_diverges_at_its_first_tick() {
let tape = record(7, 60);
let error = tape
.verify(|| fresh(8))
.expect_err("a different seed is a different run");
assert_eq!(error.tick(), Some(0), "the stream itself is already other");
match error {
VerifyError::Diverged {
divergence,
replayed,
} => {
assert_eq!(replayed.len(), 1, "the replay stops at the divergence");
assert_eq!(replayed[0], divergence.actual);
assert_ne!(divergence.expected, divergence.actual);
assert_eq!(divergence.expected, tape.hashes()[0]);
}
other => panic!("expected a divergence, got {other}"),
}
}
#[test]
fn a_perturbed_tick_is_the_reported_tick() {
let tape = record(7, 40);
let mut poisoned = tape.clone();
let edited_tick = 12;
let before = poisoned.intents()[edited_tick];
poisoned.intents_mut()[edited_tick] = Press {
up: !before.up,
spawn: before.spawn,
};
let error = poisoned
.verify(|| fresh(7))
.expect_err("the edited tick must change the world");
assert_eq!(error.tick(), Some(edited_tick as u64));
match error {
VerifyError::Diverged {
divergence,
replayed,
} => {
assert_eq!(divergence.tick, edited_tick as u64);
assert_eq!(divergence.expected, tape.hashes()[edited_tick]);
assert_eq!(divergence.actual, replayed[edited_tick]);
assert_ne!(divergence.expected, divergence.actual);
assert_eq!(
replayed[..edited_tick],
tape.hashes()[..edited_tick],
"every tick before the edit agreed"
);
}
other => panic!("expected a divergence, got {other}"),
}
}
#[test]
fn an_edited_tick_that_changes_nothing_still_verifies() {
let tape = record(7, 40);
let mut edited = tape.clone();
let quiet = edited
.intents()
.iter()
.position(|press| !press.up && !press.spawn)
.expect("the policy leaves a tick quiet");
edited.intents_mut()[quiet] = Press {
up: false,
spawn: false,
};
assert!(edited.verify(|| fresh(7)).is_ok());
}
#[test]
fn a_tape_is_serialisable() {
fn assert_serde<T: Serialize + serde::de::DeserializeOwned>() {}
assert_serde::<Tape<Press>>();
}
#[test]
fn a_tape_without_hashes_replays_but_cannot_be_verified() {
let mut tape = Tape::<Press>::new(7);
assert!(tape.is_empty());
for tick in 0..8u64 {
let input = press(tick, tick as u32);
tape.record_intent(input);
assert_eq!(tape.intent(tick), Some(&input));
}
assert_eq!(tape.ticks(), 8);
assert_eq!(tape.replay(|| fresh(7)).len(), 8, "an intent tape replays");
let error = tape
.verify(|| fresh(7))
.expect_err("with no hashes there is nothing to check");
assert_eq!(
error,
VerifyError::UnhashedTicks {
ticks: 8,
hashes: 0
}
);
assert_eq!(error.tick(), None);
}
#[derive(Resource, Default)]
struct Keys(Press);
fn play_keys(keys: Res<Keys>, mut score: ResMut<Score>) {
if keys.0.up {
score.0 += 1;
}
}
fn keys_world(seed: u64) -> Setup {
let mut sim = Sim::with_default_step();
sim.world.insert_resource(SimRng::seeded(seed));
sim.world.init_resource::<Keys>();
sim.world.init_resource::<Score>();
sim.add_system(play_keys);
let mut hasher = WorldHasher::new();
hasher.register_resource::<Keys>(|keys, hash| hash.bool(keys.0.up));
hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
Setup { sim, hasher }
}
#[test]
fn a_commit_closure_feeds_a_game_with_its_own_input_resources() {
let mut fresh = || keys_world(3);
let Setup { mut sim, hasher } = fresh();
let mut tape = Tape::new(3);
for tick in 0..12u64 {
let input = Press {
up: tick % 2 == 0,
spawn: false,
};
tape.record_intent(input);
sim.world.resource_mut::<Keys>().0 = input;
sim.tick();
tape.record_hash(hasher.hash(&sim.world));
}
let commit = |world: &mut World, input: &Press| world.resource_mut::<Keys>().0 = *input;
assert_eq!(tape.replay_with(&mut fresh, commit), tape.hashes().to_vec());
assert!(tape.verify_with(&mut fresh, commit).is_ok());
assert!(
tape.verify(&mut fresh).is_err(),
"without the commit the game never sees the input"
);
}
#[test]
#[should_panic(expected = "no tick to belong to")]
fn record_hash_refuses_a_second_hash_for_one_tick() {
let mut tape = Tape::<Press>::new(7);
tape.record_intent(Press {
up: true,
spawn: false,
});
tape.record_hash(1);
tape.record_hash(2);
}
#[test]
fn intent_carries_one_tick_of_input() {
let mut intent = Intent::<Press>::default();
assert_eq!(intent.get(), None);
intent.set(Press {
up: true,
spawn: false,
});
assert_eq!(
intent.get(),
Some(&Press {
up: true,
spawn: false
})
);
intent.clear();
assert_eq!(intent.get(), None);
}
#[test]
fn the_digest_does_not_depend_on_iteration_order() {
#[derive(Component)]
struct Mark;
fn rows(world: &mut World) -> Vec<u64> {
let mut query = world.query::<(Entity, &Height)>();
query
.iter(world)
.map(|(entity, _)| entity.to_bits())
.collect()
}
let mut a = World::new();
let first = a.spawn(Height(1.0)).id();
a.spawn(Height(2.0));
let mut b = World::new();
let also_first = b.spawn(Height(1.0)).id();
b.spawn(Height(2.0));
b.entity_mut(also_first).insert(Mark);
b.entity_mut(also_first).remove::<Mark>();
assert_eq!(first, also_first, "both worlds allocate the same ids");
assert_ne!(
rows(&mut a),
rows(&mut b),
"the hazard this guards against must be real"
);
let mut hasher = WorldHasher::new();
hasher.register::<Height>(|height, hash| hash.f32(height.0));
assert_eq!(hasher.hash(&a), hasher.hash(&b));
}
#[test]
fn a_missing_resource_is_not_a_zero_valued_resource() {
let mut hasher = WorldHasher::new();
hasher.register_resource::<Score>(|score, hash| hash.u32(score.0));
let mut present = World::new();
present.insert_resource(Score(0));
let absent = World::new();
assert_ne!(hasher.hash(&present), hasher.hash(&absent));
}
#[test]
fn a_registered_component_moves_the_digest() {
let mut hasher = WorldHasher::new();
hasher.register::<Height>(|height, hash| hash.f32(height.0));
let mut one = World::new();
one.spawn(Height(1.0));
let mut other = World::new();
other.spawn(Height(1.5));
assert_ne!(hasher.hash(&one), hasher.hash(&other));
let mut twin = World::new();
twin.spawn(Height(1.0));
assert_eq!(hasher.hash(&one), hasher.hash(&twin));
let mut quiet = World::new();
quiet.spawn(Speed(1));
let mut loud = World::new();
loud.spawn(Speed(2));
assert_eq!(hasher.hash(&quiet), hasher.hash(&loud));
}
#[test]
fn register_hashed_folds_a_derived_hash() {
#[derive(Component, Hash)]
struct Tally {
wins: u32,
losses: u32,
}
let mut hasher = WorldHasher::new();
hasher.register_hashed::<Tally>();
let mut one = World::new();
one.spawn(Tally { wins: 1, losses: 2 });
let mut twin = World::new();
twin.spawn(Tally { wins: 1, losses: 2 });
let mut other = World::new();
other.spawn(Tally { wins: 1, losses: 3 });
assert_eq!(hasher.hash(&one), hasher.hash(&twin));
assert_ne!(hasher.hash(&one), hasher.hash(&other));
}
#[test]
fn an_empty_hasher_still_sees_the_entity_list() {
let hasher = WorldHasher::new();
let mut one = World::new();
one.spawn_empty();
assert_eq!(hasher.hash(&World::new()), hasher.hash(&World::new()));
assert_ne!(hasher.hash(&one), hasher.hash(&World::new()));
}
}