nigig-org/crates/apps/nigig-traffic/tests/scenario_replays.rs

90 lines
3.1 KiB
Rust

//! Named success/violation replays per category (TRAFFIC-06/08 gate).
//!
//! Headless and deterministic: tick-indexed normalized input through the
//! production `TrafficWorld::tick` path. Every production scenario must
//! build, tick without panic, and keep a stable terminal result. Practice
//! honesty is pinned: finish-marker completion never claims an assessed
//! behaviour, and every hazard scenario spawns its actor.
use nigig_traffic::traffic::road::spawns_for;
use nigig_traffic::traffic::scenario::{all, Category};
use nigig_traffic::traffic::{Phase, TrafficWorld};
fn drive_until_settled(index: usize, ticks: u64) -> TrafficWorld {
let mut w = TrafficWorld::new();
w.load_scenario(index);
let input = makepad_game_blocks::DriveInput::default();
for _ in 0..ticks {
w.tick(&input);
}
w
}
#[test]
fn every_scenario_builds_ticks_and_settles_deterministically() {
let scenarios = all();
assert!(scenarios.len() >= 47, "catalog has {}", scenarios.len());
for (i, s) in scenarios.iter().enumerate() {
let a = drive_until_settled(i, 600);
let b = drive_until_settled(i, 600);
assert_eq!(a.run.tick, 600, "{}", s.id);
assert_eq!(a.run.phase, b.run.phase, "{}: nondeterministic", s.id);
assert_eq!(
a.verdict_counts(),
b.verdict_counts(),
"{}: nondeterministic",
s.id
);
// Idle input never completes or fails: the finish marker is far and
// no input was given. (A scenario that self-completes with no
// driving would be finish-circle laundering of nothing.)
assert!(
matches!(a.run.phase, Phase::Driving),
"{}: idle run left Driving ({:?})",
s.id,
a.run.phase
);
}
}
#[test]
fn hazard_scenarios_spawn_their_actor() {
for s in all().iter().filter(|s| s.category == Category::Hazards) {
let spawns = spawns_for(s);
assert_eq!(spawns.len(), 1, "{}: hazard lesson with no hazard", s.id);
}
}
#[test]
fn priority_scenarios_field_a_crossing_car() {
for s in all().iter().filter(|s| s.category == Category::Priority) {
let spawns = spawns_for(s);
assert_eq!(spawns.len(), 1, "{}: priority lesson with no traffic", s.id);
}
}
#[test]
fn same_input_replay_gives_same_result_twice() {
// Deterministic attempts (invariant 10): fixed scenario + seeded start
// + tick-indexed input ⇒ same trace/result.
fn replay() -> (Phase, [u64; 4], i32, u32) {
let mut w = TrafficWorld::new();
w.load_scenario(0);
for i in 0..600 {
let input = makepad_game_blocks::DriveInput {
steer: if i % 120 < 60 { 0.5 } else { -0.5 },
throttle: if i < 400 { 1.0 } else { 0.0 },
brake: if i >= 400 { 1.0 } else { 0.0 },
..makepad_game_blocks::DriveInput::default()
};
w.tick(&input);
}
(
w.run.phase,
w.verdict_counts(),
w.run.score.total_points,
w.run.score.mistakes,
)
}
assert_eq!(replay(), replay());
}