use crate::components::{
Behavior, BehaviorExpr, BehaviorNode, BehaviorSource, BodyDynamics, Collider, PhysicsConfig,
Prop, PropCollider, Transform,
};
use crate::ecs::SYSTEMS;
use crate::ecs::{ScheduleMode, StepResult, World};
fn fnv(hash: &mut u64, bytes: &[u8]) {
for &b in bytes {
*hash ^= b as u64;
*hash = hash.wrapping_mul(0x100000001b3);
}
}
fn hash_f32s(hash: &mut u64, values: &[f32]) {
for v in values {
fnv(hash, &v.to_bits().to_le_bytes());
}
}
fn hash_world(world: &World) -> u64 {
let mut h = 0xcbf29ce484222325u64;
for t in world.query::<Transform>() {
hash_f32s(&mut h, &t.position);
hash_f32s(&mut h, &t.rotation_deg);
hash_f32s(&mut h, &t.scale);
}
fnv(&mut h, &(world.component_count() as u64).to_le_bytes());
if let Some(events) = world.events::<crate::components::ContactEvent>() {
fnv(&mut h, &(events.len() as u64).to_le_bytes());
}
h
}
fn build_world() -> World {
let mut world = World::new();
world.add_component(Behavior {
on: BehaviorSource::Tick,
scope: vec!["Prop".into()],
body: vec![BehaviorNode::SetTransform {
entity: BehaviorExpr::SelfEntity,
position: Some(BehaviorExpr::Add(
Box::new(BehaviorExpr::Position(Box::new(BehaviorExpr::SelfEntity))),
Box::new(BehaviorExpr::Vec3([0.01, 0.0, 0.0])),
)),
rotation_deg: None,
scale: None,
}],
..Default::default()
});
world.add_component(Behavior {
on: BehaviorSource::Tick,
body: vec![BehaviorNode::Set {
var: "beat".into(),
value: BehaviorExpr::Int(1),
add: true,
}],
..Default::default()
});
world.add_component(Behavior {
on: BehaviorSource::Tick,
body: vec![
BehaviorNode::Set {
var: "pace".into(),
value: BehaviorExpr::Mul(
Box::new(BehaviorExpr::Var("beat".into())),
Box::new(BehaviorExpr::Int(3)),
),
add: false,
},
BehaviorNode::SetTransform {
entity: BehaviorExpr::First("props".into()),
position: None,
rotation_deg: Some(BehaviorExpr::Vec3([0.0, 1.0, 0.0])),
scale: None,
},
],
queries: vec![crate::components::BehaviorQuery {
name: "props".into(),
has: vec!["Prop".into()],
}],
..Default::default()
});
for i in 0..200 {
world.add_component(Prop {
position: [i as f32 * 2.0, 0.5, 0.0],
..Default::default()
});
}
world.add_component(PhysicsConfig::default());
for i in 0..256 {
let (stack, level) = (i / 8, i % 8);
let e = world.push(Transform {
position: [
stack as f32 * 25.0,
0.6 + level as f32 * 1.05,
0.05 * level as f32,
],
rotation_deg: [0.0; 3],
scale: [1.0; 3],
});
world.insert(
e,
Collider(PropCollider {
shape: "cuboid".into(),
half_extents: [0.4, 0.4, 0.4],
..Default::default()
}),
);
world.insert(
e,
BodyDynamics {
restitution: 0.6,
..Default::default()
},
);
}
world
}
fn run(mode: ScheduleMode, ticks: u32) -> u64 {
let mut world = build_world();
world.insert_resource(mode);
world.start(SYSTEMS).expect("world starts");
for _ in 0..ticks {
assert_eq!(world.step(), StepResult::Continue);
}
hash_world(&world)
}
#[test]
fn parallel_schedule_state_matches_serial() {
let serial = run(ScheduleMode::Serial, 240);
let parallel = run(ScheduleMode::Parallel, 240);
assert_eq!(
serial, parallel,
"parallel schedule diverged from serial world state",
);
}
#[test]
fn repeated_serial_runs_are_reproducible() {
assert_eq!(
run(ScheduleMode::Serial, 120),
run(ScheduleMode::Serial, 120)
);
}
#[test]
fn the_world_actually_moves() {
let start = {
let mut world = build_world();
world.start(SYSTEMS).expect("world starts");
hash_world(&world)
};
assert_ne!(
start,
run(ScheduleMode::Serial, 120),
"the gate world must produce motion, or the hash proves nothing",
);
}