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

190 lines
5.2 KiB
Rust

//! Adversarial rule gates (TRAFFIC-06): shortcuts must not pass, and
//! completion text must never confirm a behaviour the rules cannot see.
//!
//! Because every scenario is a labelled practice preview, these tests pin
//! the HONEST behaviour: finish-only shortcuts complete as PRACTICE (with
//! practice wording), and no verdict string claims sign compliance,
//! yielding, correct exits, hazard response, or parking orientation.
use makepad_widgets::Vec3f;
use nigig_traffic::traffic::geom::CourseFrame;
use nigig_traffic::traffic::objective::{evaluate, EvalContext, Verdict};
use nigig_traffic::traffic::road::{BayBox, BayOrientation, LaneBox, RoadMeta, RoundaboutBox};
use nigig_traffic::traffic::scenario::Objective;
fn meta() -> RoadMeta {
RoadMeta {
finish: Vec3f {
x: 40.0,
y: 0.0,
z: 0.0,
},
finish_radius: 4.0,
frame: CourseFrame::from_start_finish([0.0, 0.0], [40.0, 0.0]),
lane_boxes: vec![LaneBox {
center: Vec3f {
x: 0.0,
y: 0.0,
z: -1.75,
},
half: Vec3f {
x: 20.0,
y: 0.5,
z: 1.75,
},
lane_y: -1.75,
}],
bays: vec![BayBox {
center: Vec3f {
x: 6.0,
y: 0.0,
z: 5.25,
},
half: Vec3f {
x: 1.3,
y: 0.1,
z: 1.3,
},
orientation: BayOrientation::Perpendicular,
}],
roundabout: Some(RoundaboutBox {
center: Vec3f::default(),
radius: 12.0,
}),
speed_limit_kmh: 50.0,
}
}
fn at(meta: &RoadMeta, x: f32, speed: f32) -> EvalContext<'_> {
let dx = x - 40.0;
EvalContext {
meta,
player_pos: Vec3f { x, y: 0.0, z: 0.0 },
player_half: Vec3f {
x: 0.9,
y: 0.5,
z: 1.9,
},
player_speed: speed,
player_forward_x: 1.0,
player_yaw: -std::f32::consts::FRAC_PI_2,
rel_heading: 0.0,
hazard_ids: &[],
nearest_hazard_clearance_sq: f32::MAX,
dist_to_finish_sq: dx * dx,
}
}
#[test]
fn finish_only_shortcut_completes_as_practice_not_proof() {
let m = meta();
// Roundabout: teleport to the finish without entering/circulating.
let c = at(&m, 40.0, 8.0);
let v = evaluate(
&c,
&Objective::Roundabout {
enter: true,
exit_at: Some(2),
give_way: true,
},
);
match v {
Verdict::Complete(msg) => {
assert!(
msg.contains("Practice"),
"must be labelled practice, got: {msg}"
);
assert!(
!msg.contains("signal") && !msg.contains("exit"),
"must not confirm exit/signal behaviour, got: {msg}"
);
}
other => panic!("finish must complete practice, got {other:?}"),
}
}
#[test]
fn no_completion_text_claims_assessed_behaviour() {
let m = meta();
let finish = at(&m, 40.0, 8.0);
let cases = [
Objective::Roundabout {
enter: true,
exit_at: Some(2),
give_way: true,
},
Objective::Signs {
signs: vec![nigig_traffic::traffic::road::SignType::Stop],
},
Objective::Priority {
yield_: true,
stop: false,
},
Objective::Hazards {
avoid_collision: true,
brake: true,
},
Objective::Speed { limit_kmh: 50.0 },
Objective::FreeDrive,
];
// "obeyed", "respected", "avoided", "compliance", "correct exit",
// "collision" (as a confirmed event) must not appear in completion text.
let banned = [
"obeyed",
"respected",
"avoided",
"compliance",
"collision",
"correct exit",
"lanes held",
];
for o in &cases {
if let Verdict::Complete(msg) = evaluate(&finish, o) {
for b in banned {
assert!(!msg.contains(b), "{o:?}: completion claims '{b}': {msg}");
}
} else {
panic!("{o:?}: finish must complete");
}
}
}
#[test]
fn wrong_bay_parking_cannot_complete() {
let m = meta();
// Stopped far from the bay: mistake, never complete.
let mut c = at(&m, 6.0, 0.0);
c.player_pos = Vec3f {
x: 6.0,
y: 0.0,
z: 30.0,
};
assert_eq!(
evaluate(
&c,
&Objective::Parking {
bay_index: Some(0),
orientation: BayOrientation::Perpendicular
}
),
Verdict::Mistake("Neat stop — position the car inside the bay first.")
);
}
#[test]
fn forbidden_uturn_attempt_fails_even_as_practice() {
let m = meta();
let mut c = at(&m, 10.0, 8.0);
c.rel_heading = std::f64::consts::PI;
c.player_forward_x = -1.0;
assert_eq!(
evaluate(
&c,
&Objective::UTurn {
legal: false,
open_area: true
}
),
Verdict::Fail("You attempted a forbidden U-turn.")
);
}