Tests added: - physics_materials.rs: 15 tests (fire, smoke, steam, grass, dirt, bone, wood, stone, lava temp) - physics_interactions.rs: 12 tests (lava+water, fire spread, acid interactions, sand+water) - player_controls.rs: 12 tests (move L/R, jump, double jump, rapid input, wait, continuous) - collision_robust.rs: 10 tests (walls, ceiling, sliding, corridor, slope, dirt wall, unstick) - ragdoll_death.rs: 7 tests (death transition, ragdoll fall, progressive damage, corpse) - determinism.rs: 8 tests (same seed, replay exact, replay partial, recording, 100-tick) - edge_cases.rs: 19 tests (boundary, stress 20 goblins, fill/clear/paint, gravity, camera) - 6 new JSON scenarios (fire chain, lava pool, sand bury, entity fall, steam, acid wall) Fixes: - AABB resolve_aabb_y: add vertical overlap check (was pushing player down when jumping) - AABB resolve_aabb_x: velocity-aware resolution (player was stuck against walls) - check_on_ground: use fractional position check (was always true) - jump_force 1.5 (was 0.8) for 27-body entity - Gravity read from self.verlet (was from clone, SetGravity didn't work) - u8 overflow in lava color (saturating_add) 109 Rust tests, 14 JSON scenarios, 0 warnings, 0 failures
155 lines
5.2 KiB
Rust
155 lines
5.2 KiB
Rust
use verbatim::ai::GameSession;
|
|
use verbatim::ai::AiAction;
|
|
use verbatim::ai::ReplayPlayer;
|
|
|
|
fn setup() -> GameSession {
|
|
let mut s = GameSession::new_seeded(42);
|
|
s.init_empty();
|
|
s.clear_area(90, 90, 50, 50);
|
|
s.perform_action(&AiAction::FillRect { x: 80, y: 130, w: 80, h: 15, material: "stone".into() });
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn same_seed_same_player_position() {
|
|
let mut s1 = GameSession::new_seeded(777);
|
|
s1.init();
|
|
s1.step(30);
|
|
let mut s2 = GameSession::new_seeded(777);
|
|
s2.init();
|
|
s2.step(30);
|
|
let p1 = s1.get_player().unwrap();
|
|
let p2 = s2.get_player().unwrap();
|
|
assert!((p1.pos[0] - p2.pos[0]).abs() < 0.01, "x mismatch");
|
|
assert!((p1.pos[1] - p2.pos[1]).abs() < 0.01, "y mismatch");
|
|
}
|
|
|
|
#[test]
|
|
fn same_seed_same_entity_count() {
|
|
let mut s1 = GameSession::new_seeded(555);
|
|
s1.init();
|
|
s1.step(60);
|
|
let mut s2 = GameSession::new_seeded(555);
|
|
s2.init();
|
|
s2.step(60);
|
|
assert_eq!(s1.get_entities().len(), s2.get_entities().len(), "entity count should match");
|
|
}
|
|
|
|
#[test]
|
|
fn same_seed_same_grid_state() {
|
|
let mut s1 = GameSession::new_seeded(333);
|
|
s1.init();
|
|
s1.step(20);
|
|
let mut s2 = GameSession::new_seeded(333);
|
|
s2.init();
|
|
s2.step(20);
|
|
for y in 100..150 {
|
|
for x in 100..150 {
|
|
let c1 = s1.get_cell(x, y);
|
|
let c2 = s2.get_cell(x, y);
|
|
assert_eq!(c1.material, c2.material, "material mismatch at ({},{})", x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: different seeds currently produce the same world because
|
|
// the CA RNG is internal (not seeded from GameSession).
|
|
// This is a known limitation — world gen is deterministic regardless of seed.
|
|
// Seed matters for replay recording/determinism within a session.
|
|
|
|
#[test]
|
|
fn replay_exact_match() {
|
|
let mut s = GameSession::new_seeded(888);
|
|
s.init();
|
|
s.set_recording(true);
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(10);
|
|
s.perform_action(&AiAction::Jump);
|
|
s.step(10);
|
|
s.perform_action(&AiAction::MoveLeft);
|
|
s.step(10);
|
|
let state_orig = s.get_state();
|
|
s.save_replay("/tmp/verbatim_replay_exact.json").expect("save");
|
|
|
|
let player = ReplayPlayer::load("/tmp/verbatim_replay_exact.json").expect("load");
|
|
let s2 = player.play();
|
|
let state_replay = s2.get_state();
|
|
|
|
assert_eq!(state_orig.tick, state_replay.tick, "tick mismatch");
|
|
if let (Some(p1), Some(p2)) = (&state_orig.player, &state_replay.player) {
|
|
assert!((p1.pos[0] - p2.pos[0]).abs() < 0.1, "x mismatch: {} vs {}", p1.pos[0], p2.pos[0]);
|
|
assert!((p1.pos[1] - p2.pos[1]).abs() < 0.1, "y mismatch: {} vs {}", p1.pos[1], p2.pos[1]);
|
|
assert!((p1.health - p2.health).abs() < 1.0, "health mismatch");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn replay_stop_at_tick() {
|
|
let mut s = GameSession::new_seeded(444);
|
|
s.init();
|
|
s.set_recording(true);
|
|
s.step(20);
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.step(20);
|
|
s.save_replay("/tmp/verbatim_replay_partial.json").expect("save");
|
|
|
|
let player = ReplayPlayer::load("/tmp/verbatim_replay_partial.json").expect("load");
|
|
let s_half = player.play_until_tick(10);
|
|
assert_eq!(s_half.tick(), 10, "should stop at tick 10, got {}", s_half.tick());
|
|
|
|
let s_full = player.play();
|
|
assert_eq!(s_full.tick(), 40, "full replay should reach tick 40, got {}", s_full.tick());
|
|
}
|
|
|
|
#[test]
|
|
fn recording_captures_all_actions() {
|
|
let mut s = GameSession::new_seeded(123);
|
|
s.init();
|
|
s.set_recording(true);
|
|
s.perform_action(&AiAction::MoveRight);
|
|
s.perform_action(&AiAction::Jump);
|
|
s.perform_action(&AiAction::MoveLeft);
|
|
s.step(5);
|
|
s.save_replay("/tmp/verbatim_replay_capture.json").expect("save");
|
|
|
|
let player = ReplayPlayer::load("/tmp/verbatim_replay_capture.json").expect("load");
|
|
let event_count = player.recording().events.len();
|
|
assert!(event_count >= 4, "recording should have at least 4 events (3 actions + 1 step), got {}", event_count);
|
|
}
|
|
|
|
#[test]
|
|
fn determinism_with_spawn_and_damage() {
|
|
let mut s1 = setup();
|
|
s1.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 135.0, y: 120.0 });
|
|
s1.perform_action(&AiAction::DamageEntity { id: 1, amount: 20.0 });
|
|
s1.step(30);
|
|
|
|
let mut s2 = setup();
|
|
s2.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 135.0, y: 120.0 });
|
|
s2.perform_action(&AiAction::DamageEntity { id: 1, amount: 20.0 });
|
|
s2.step(30);
|
|
|
|
let e1 = s1.get_entities().into_iter().find(|e| e.id == 1).unwrap();
|
|
let e2 = s2.get_entities().into_iter().find(|e| e.id == 1).unwrap();
|
|
assert!((e1.health - e2.health).abs() < 0.01, "health should match: {} vs {}", e1.health, e2.health);
|
|
assert!((e1.pos[0] - e2.pos[0]).abs() < 0.1, "x should match");
|
|
assert!((e1.pos[1] - e2.pos[1]).abs() < 0.1, "y should match");
|
|
}
|
|
|
|
#[test]
|
|
fn hundred_tick_determinism() {
|
|
let mut s1 = GameSession::new_seeded(99);
|
|
s1.init();
|
|
s1.perform_action(&AiAction::MoveRight);
|
|
s1.step(100);
|
|
|
|
let mut s2 = GameSession::new_seeded(99);
|
|
s2.init();
|
|
s2.perform_action(&AiAction::MoveRight);
|
|
s2.step(100);
|
|
|
|
let p1 = s1.get_player().unwrap();
|
|
let p2 = s2.get_player().unwrap();
|
|
assert!((p1.pos[0] - p2.pos[0]).abs() < 0.01, "100-tick determinism failed: {} vs {}", p1.pos[0], p2.pos[0]);
|
|
}
|