Record energy loss and exercise meeting geometry
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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, "ожиданий БК"),
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user