Record energy loss and exercise meeting geometry

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
2026-11-02 23:53:23 +03:00
parent 6b6293f353
commit f3985d5363
3 changed files with 86 additions and 1 deletions
+13 -1
View File
@@ -2957,11 +2957,12 @@ impl Sim {
fn step_missiles(&mut self, ground: &mut Ground, out: &mut Out) {
let now = self.tick;
let mut done: Vec<(u32, Option<u32>, bool, u32)> = Vec::new(); // (missile, target, kill, sam)
let (mut missed, mut expired, mut orphaned) = (0u32, 0u32, 0u32);
let (mut missed, mut expired, mut orphaned, mut energy_lost) = (0u32, 0u32, 0u32, 0u32);
let (mut blocked, mut scrubbed) = (0u32, 0u32);
let mut impacts = HashMap::new();
let mut ignitions = Vec::new();
let mut burnouts = Vec::new();
let mut energy_losses = Vec::new();
#[cfg(test)]
let mut scrub_log = Vec::new();
let sams_fit: HashMap<u32, f64> = self.sams.iter().map(|s| (s.id, 1.0 - 0.3 * (1.0 - s.health.clamp(0.0, 1.0)))).collect();
@@ -3107,6 +3108,13 @@ impl Sim {
done.push((m.id, None, false, m.sam));
continue;
}
// A spent motor cannot sustain guidance once it has lost most of its speed.
if coast && speed < spec.speed * 0.25 && target.is_some() {
energy_lost += 1;
energy_losses.push((m.pos, m.id, m.kind, speed));
done.push((m.id, None, false, m.sam));
continue;
}
if m.age > spec.life || (target.is_none() && m.age > 30) {
// Its target gone (shot down by another, or lost), or its life spent chasing one.
if target.is_none() {
@@ -3145,11 +3153,15 @@ impl Sim {
for (pos, missile, kind) in burnouts {
self.scene_event(pos, record::SceneKind::MotorBurnout { missile, kind });
}
for (pos, missile, kind, speed) in energy_losses {
self.scene_event(pos, record::SceneKind::MotorEnergyLost { missile, kind, speed });
}
#[cfg(test)]
self.scrubs.extend(scrub_log);
self.note(|t| {
t.missed += missed;
t.expired += expired;
t.energy_lost += energy_lost;
t.orphaned += orphaned;
t.blocked += blocked;
t.scrubbed += scrubbed;
+5
View File
@@ -254,6 +254,7 @@ pub enum SceneKind {
MissileLaunch { missile: u32, sam: u32, target: u32, kind: MissileKind, sound: &'static str, pitch: f32 },
MotorIgnition { missile: u32, kind: MissileKind, sound: Option<&'static str> },
MotorBurnout { missile: u32, kind: MissileKind },
MotorEnergyLost { missile: u32, kind: MissileKind, speed: f64 },
TrackAcquired { sam: u32, target: u32, ever_own: bool },
TrackLost { sam: u32, target: u32 },
TrackSource { sam: u32, target: u32, ever_own: bool },
@@ -313,6 +314,8 @@ pub struct Tally {
pub missed: u32,
/// Interceptors whose life ran out with the target still flying.
pub expired: u32,
/// Interceptors whose post-burn speed fell below their guidance threshold.
pub energy_lost: u32,
/// Interceptors whose target was gone (shot down by another, or lost) before they arrived.
pub orphaned: u32,
/// Times a system lost the track of the target it held, which was still in the sky.
@@ -330,6 +333,7 @@ impl Tally {
Tally {
missed: self.missed.saturating_sub(start.missed),
expired: self.expired.saturating_sub(start.expired),
energy_lost: self.energy_lost.saturating_sub(start.energy_lost),
orphaned: self.orphaned.saturating_sub(start.orphaned),
tracks_lost: self.tracks_lost.saturating_sub(start.tracks_lost),
ammo_waits: self.ammo_waits.saturating_sub(start.ammo_waits),
@@ -342,6 +346,7 @@ impl Tally {
let parts: Vec<String> = [
(self.missed, "промахов по взрывателю"),
(self.expired, "ракет выработали ресурс"),
(self.energy_lost, "ракет потеряли скорость после выгорания"),
(self.orphaned, "ракет остались без цели"),
(self.tracks_lost, "потерь трека"),
(self.ammo_waits, "ожиданий БК"),
+68
View File
@@ -1899,3 +1899,71 @@ fn interceptor_lead_depends_on_meeting_geometry() {
assert!(crossing.x > 0.0, "crossing target must be led");
assert!(approaching.x.abs() < 1e-9 && receding.x.abs() < 1e-9);
}
#[test]
fn energy_loss_is_recorded_separately_from_expiry() {
let mut sim = Sim::new(120);
let kind = MissileKind::Pac3;
let pos = DVec3::new(0.0, 100.0, 900.0);
sim.add_drone(DroneKind::Shahed, pos, DVec3::ZERO, DVec3::new(0.0, 100.0, 5000.0));
sim.begin_recording_if_active();
let (target, id) = (sim.drones[0].id, sim.id());
sim.tick = 100;
let fix = Some(Fix { pos, vel: DVec3::ZERO, tick: 100, seeker: false });
sim.missiles.push(Missile { id, kind, pos: DVec3::new(0.0, 100.0, 0.0), vel: DVec3::Z * kind.spec().speed * 0.2, target, sam: 999, age: kind.burn_ticks() + 1, hold: 0, fix, seen: 0 });
sim.step_missiles(&mut Ground::default(), &mut Out::default());
assert!(sim.missiles.is_empty());
assert_eq!(sim.tally.energy_lost, 1);
assert_eq!(sim.tally.expired, 0);
let events = sim.rec.as_ref().unwrap().scene.as_ref().unwrap().events();
assert!(events.iter().any(|e| matches!(e.kind, record::SceneKind::MotorEnergyLost { missile, speed, .. } if missile == id && speed > 0.0)));
}
#[test]
fn equal_speed_head_on_crossing_and_chase_have_distinct_reach_times() {
let fly = |vel: DVec3| {
let mut sim = Sim::new(120);
let mut ground = Ground::default();
let land = Land(|_, _| 64);
let pos = DVec3::new(0.0, 200.0, 400.0);
sim.add_drone(DroneKind::Shahed, pos, DVec3::ZERO, DVec3::new(0.0, 200.0, 5000.0));
sim.drones[0].vel = vel;
let (target, id) = (sim.drones[0].id, sim.id());
sim.missiles.push(Missile { id, kind: MissileKind::Pac3, pos: DVec3::new(0.0, 200.0, 0.0), vel: DVec3::Z * 3.0, target, sam: 999, age: 20, hold: 0, fix: None, seen: 0 });
for step in 1..200 {
sim.tick = step;
sim.drones[0].pos += vel;
sim.missiles[0].fix = Some(Fix { pos: sim.drones[0].pos, vel, tick: sim.tick, seeker: false });
sim.step_missiles(&mut ground, &mut Out::default());
answer_land(&mut ground, &land, sim.tick);
if sim.missiles.is_empty() {
assert_eq!(sim.tally.blocked + sim.tally.scrubbed + sim.tally.expired + sim.tally.energy_lost, 0, "lost before reaching target");
return step;
}
}
panic!("interceptor did not reach the target")
};
let head_on = fly(-DVec3::Z);
let crossing = fly(DVec3::X);
let chase = fly(DVec3::Z);
assert!(head_on < crossing && crossing < chase, "{head_on}, {crossing}, {chase}");
}
#[test]
fn spent_motor_has_less_maneuver_at_low_speed() {
let fly = |age: u32| {
let mut sim = Sim::new(120);
let kind = MissileKind::Pac3;
let at = DVec3::new(500.0, 200.0, 500.0);
sim.add_drone(DroneKind::Shahed, at, DVec3::ZERO, at);
sim.tick = 100;
let (target, id) = (sim.drones[0].id, sim.id());
let fix = Some(Fix { pos: at, vel: DVec3::ZERO, tick: 100, seeker: false });
sim.missiles.push(Missile { id, kind, pos: DVec3::new(0.0, 200.0, 0.0), vel: DVec3::Z * kind.spec().speed * 0.4, target, sam: 999, age, hold: 0, fix, seen: 0 });
sim.step_missiles(&mut Ground::default(), &mut Out::default());
sim.missiles[0].vel.normalize()
};
let powered = fly(30);
let coast = fly(MissileKind::Pac3.burn_ticks() + 1);
assert!(powered.x > coast.x && coast.x > 0.0, "{powered:?} {coast:?}");
}