From f0fbc397d8a3bfa20df43aa298d501071660ecda Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 23:37:14 +0300 Subject: [PATCH] Raid planner: plan over several ticks at full detail instead of in one Co-Authored-By: Claude Opus 5.5 --- airdef-mod/src/panel.rs | 3 + airdef-mod/src/sim/mod.rs | 8 +- airdef-mod/src/sim/raid.rs | 229 ++++++++++++++++++++++++++++--------- 3 files changed, 186 insertions(+), 54 deletions(-) diff --git a/airdef-mod/src/panel.rs b/airdef-mod/src/panel.rs index 81fcd37..564c98b 100644 --- a/airdef-mod/src/panel.rs +++ b/airdef-mod/src/panel.rs @@ -55,6 +55,9 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec { )); } } + if sim.planning() { + lines.push(line("Противник планирует налёт…".into(), YELLOW)); + } if !sim.queue.is_empty() { lines.push(line(format!("Ещё взлетает: {}", sim.queue.len()), PINK)); } diff --git a/airdef-mod/src/sim/mod.rs b/airdef-mod/src/sim/mod.rs index b6b4552..626590c 100644 --- a/airdef-mod/src/sim/mod.rs +++ b/airdef-mod/src/sim/mod.rs @@ -618,7 +618,7 @@ impl DroneKind { /// Height over the target where the Oreshnik's blocks release their submunitions. const BLOCK_SPLIT: f64 = 300.0; -#[derive(PartialEq)] +#[derive(PartialEq, Clone, Copy)] enum Flight { Cruise, Dive, @@ -628,6 +628,7 @@ enum Flight { Falling, } +#[derive(Clone)] pub struct Drone { pub id: u32, pub kind: DroneKind, @@ -1118,6 +1119,8 @@ pub struct Sim { later: Vec<(u64, DVec3, u32, bool)>, /// A planned raid's drones waiting for their launch tick. pub queue: Vec<(u64, Drone)>, + /// A raid being planned. + plan: Option, /// Each marked object's health, 0 to 100 (`targets`' order). pub target_health: Vec, /// The raid in progress, for its report. @@ -1153,6 +1156,7 @@ impl Sim { horizons: HashMap::new(), later: Vec::new(), queue: Vec::new(), + plan: None, target_health: Vec::new(), wave: None, rec: None, @@ -1241,12 +1245,14 @@ impl Sim { self.fx.clear(); self.blasts.clear(); self.queue.clear(); + self.plan = None; self.wave = None; } /// One game tick. pub fn step(&mut self, ground: &mut Ground, player: Option, out: &mut Out) { self.tick += 1; + self.plan_step(out); self.release_queue(); self.step_drones(ground, player, out); self.step_later(ground, player, out); diff --git a/airdef-mod/src/sim/raid.rs b/airdef-mod/src/sim/raid.rs index 2ae1584..b6c8bb4 100644 --- a/airdef-mod/src/sim/raid.rs +++ b/airdef-mod/src/sim/raid.rs @@ -60,6 +60,32 @@ pub struct RaidOrder { pub aim: Aim, } +/// A drone of a raid being planned. +struct Item { + d: Drone, + from: DVec3, + aim: DVec3, + /// Its route's key (kind, bearing, target), when it gets one. + key: Option<(DroneKind, i32, i32, i32)>, + early: f64, + gap: f64, +} + +/// A raid being planned: its drones, the prices and routes so far, and the planned ones. +pub(super) struct Plan { + grid: Grid, + prices: HashMap>, + routes: HashMap<(DroneKind, i32, i32, i32), Vec>, + items: Vec, + next: usize, + /// (drone, flight time, how early it arrives, its wave's delay) + done: Vec<(Drone, f64, f64, f64)>, + smart: bool, + approach: Approach, + total: u32, + centre: DVec3, +} + /// The raid in progress: the counts at its start, for the report. pub(super) struct WaveStats { kills: u32, @@ -73,7 +99,9 @@ pub(super) struct WaveStats { /// The planner's grid: cells this big, at most this many a side. const CELL: f64 = 16.0; -const CELLS: i32 = 120; +const CELLS: i32 = 140; +/// The planner's work a tick (cells priced and opened): a raid takes a few ticks to plan, never a long one. +const WORK_PER_TICK: usize = 40_000; /// The planner's grid over a raid: its corner, its cells' size and how many a side. struct Grid { @@ -93,7 +121,8 @@ impl Grid { } /// The cheapest way over `grid` from `from` to `to` by its prices (A*): points along it, `to` last. -fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> Vec { +fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> (Vec, usize) { + let mut opened = 0; let n = grid.size; let at = |(i, j): (i32, i32)| (i * n + j) as usize; let start = grid.index(from); @@ -108,6 +137,7 @@ fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> Vec if c == goal { break; } + opened += 1; let g = best[at(c)]; // A stale entry: this cell was reached cheaper since. if f > f64::from(g) + h(c) + 1e-3 { @@ -130,7 +160,7 @@ fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> Vec } } if came[at(goal)] == u32::MAX { - return vec![to]; + return (vec![to], opened); } let mut cells = vec![goal]; let mut c = at(goal); @@ -142,7 +172,7 @@ fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> Vec let mut route: Vec = cells.iter().skip(2).step_by(3).map(|&c| grid.centre(c, to.y)).collect(); route.retain(|p| horizontal(*p - to).length() > grid.cell * 2.0); route.push(to); - route + (route, opened) } /// A cell in the A* queue: the cheapest first. @@ -161,10 +191,10 @@ impl PartialOrd for Open { } impl Sim { - /// Launches a raid on `objects` (at least one point). + /// Orders a raid on `objects` (at least one point). A smart raid is planned over the next ticks + /// (each route found in full, a few a tick) and launched when its plan is ready. pub fn order_raid(&mut self, order: &RaidOrder, objects: &[DVec3], out: &mut Out) -> u32 { let Some(¢re) = objects.first() else { return 0 }; - let now = self.tick; let total: u32 = order.kinds.iter().map(|k| k.1).sum(); if total == 0 { return 0; @@ -172,15 +202,12 @@ impl Sim { let bearing = self.rng.f() * TAU; let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6); let smart = order.aim == Aim::Smart; - // (drone, its flight time to the target in ticks, how early it should arrive, its wave's delay) - let mut planned: Vec<(Drone, f64, f64, f64)> = Vec::new(); // One grid over the whole raid; each kind's prices are worked out once, routes are shared - // by the drones of a kind going from about the same bearing to the same target. + // by the drones of a kind going from the same bearing to the same target. let span = order.dist + 450.0; let cell = CELL.max(2.0 * span / CELLS as f64); let grid = Grid { min: centre - DVec3::new(span, 0.0, span), cell, size: (2.0 * span / cell) as i32 + 1 }; - let mut prices: HashMap> = HashMap::new(); - let mut routes: HashMap<(DroneKind, i32, i32, i32), Vec> = HashMap::new(); + let mut items = Vec::new(); for (group, &(kind, count)) in order.kinds.iter().enumerate() { let spec = kind.spec(); for _ in 0..count { @@ -195,44 +222,82 @@ impl Sim { let dist = spec.ballistic.map_or(order.dist, |far| far.max(order.dist)) + self.rng.f() * 50.0; let height = if spec.ballistic.is_some() { 10.0 } else if spec.cruise > 0.0 { spec.cruise } else { 25.0 }; let from = centre + DVec3::new(a.sin() * dist, height + self.rng.s() * 6.0, a.cos() * dist); - let mut d = self.make_drone(kind, from, horizontal(aim - from), aim); - let mut path = from.distance(aim); - if smart && spec.ballistic.is_none() && !spec.decoy && !at_sam.is_some_and(|_| kind == DroneKind::Lancet) { - let key = (kind, (a.rem_euclid(TAU) / 0.35) as i32, (target.x / 24.0) as i32, (target.z / 24.0) as i32); - let route = match routes.get(&key) { - Some(r) => r.clone(), - None => { - if !prices.contains_key(&kind) { - let p = self.price_grid(&grid, kind); - prices.insert(kind, p); - } - let r = plan_route(&grid, &prices[&kind], from, aim); - routes.insert(key, r.clone()); - r - } - }; - // Each drone a little off the shared line. - d.route = route.iter().map(|p| *p + DVec3::new(self.rng.s() * 6.0, 0.0, self.rng.s() * 6.0)).collect(); - d.low = matches!(spec.class, Class::Drone | Class::Jet | Class::Cruise); - path = route_length(from, &d.route, aim); - } - let time = match d.flight { - Flight::Ballistic => { - let flat = horizontal(aim - from).length(); - (flat / spec.speed).max(30.0) - } - _ => path / spec.speed, - }; + let d = self.make_drone(kind, from, horizontal(aim - from), aim); + let routed = smart && spec.ballistic.is_none() && !spec.decoy && !at_sam.is_some_and(|_| kind == DroneKind::Lancet); + let key = routed.then(|| (kind, (a.rem_euclid(TAU) / 0.35) as i32, (target.x / 24.0) as i32, (target.z / 24.0) as i32)); // Decoys come 15 s ahead, the rest together; "waves" one kind after another. let early = if spec.decoy { 300.0 } else { 0.0 }; - let wave_gap = if order.approach == Approach::Waves { group as f64 * 300.0 } else { 0.0 }; - planned.push((d, time, early, wave_gap)); + let gap = if order.approach == Approach::Waves { group as f64 * 300.0 } else { 0.0 }; + items.push(Item { d, from, aim, key, early, gap }); } } + self.plan = Some(Plan { + grid, + prices: HashMap::new(), + routes: HashMap::new(), + items, + next: 0, + done: Vec::new(), + smart, + approach: order.approach, + total, + centre, + }); + // A raid without routes is ready at once. + self.plan_step(out); + total + } + + /// Works on the raid's plan a while (a tick's share), and launches it when it is ready. + pub(super) fn plan_step(&mut self, out: &mut Out) { + let Some(mut plan) = self.plan.take() else { return }; + let mut work = 0usize; + while plan.next < plan.items.len() && work < WORK_PER_TICK { + let i = plan.next; + let (kind, from, aim, key) = { + let it = &plan.items[i]; + (it.d.kind, it.from, it.aim, it.key) + }; + let spec = kind.spec(); + let mut d = plan.items[i].d.clone(); + let mut path = from.distance(aim); + if let Some(key) = key { + let route = match plan.routes.get(&key) { + Some(r) => r.clone(), + None => { + if !plan.prices.contains_key(&kind) { + let (p, cost) = self.price_grid(&plan.grid, kind); + work += cost; + plan.prices.insert(kind, p); + } + let (r, cost) = plan_route(&plan.grid, &plan.prices[&kind], from, aim); + work += cost; + plan.routes.insert(key, r.clone()); + r + } + }; + // Each drone a little off the shared line. + d.route = route.iter().map(|p| *p + DVec3::new(self.rng.s() * 6.0, 0.0, self.rng.s() * 6.0)).collect(); + d.low = matches!(spec.class, Class::Drone | Class::Jet | Class::Cruise); + path = route_length(from, &d.route, aim); + } + let time = match d.flight { + Flight::Ballistic => (horizontal(aim - from).length() / spec.speed).max(30.0), + _ => path / spec.speed, + }; + plan.done.push((d, time, plan.items[i].early, plan.items[i].gap)); + plan.next += 1; + work += 1; + } + if plan.next < plan.items.len() { + self.plan = Some(plan); + return; + } // Timing: smart raids arrive at once; others leave at once (a moment apart). - let arrive = planned.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max); - for (d, time, early, gap) in planned { - let wait = if smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 }; + let now = self.tick; + let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max); + for (d, time, early, gap) in plan.done { + let wait = if plan.smart { arrive - (early - gap) - time } else { gap + self.rng.f() * 40.0 }; self.queue.push((now + wait.clamp(0.0, 2400.0) as u64, d)); } self.wave = Some(WaveStats { @@ -242,12 +307,16 @@ impl Sim { fired: self.sams.iter().map(|s| s.fired).sum(), sams: self.sams.iter().filter(|s| s.alive()).count(), objects: self.target_health.iter().sum(), - size: total, + size: plan.total, }); - let what = if smart { "умный противник" } else { order.approach.name() }; - out.messages.push(format!("ВОЗДУШНАЯ ТРЕВОГА: налёт, {total} целей ({what})")); - out.sound("minecraft:event.raid.horn", centre, 64.0, 1.0); - total + let what = if plan.smart { "умный противник" } else { plan.approach.name() }; + out.messages.push(format!("ВОЗДУШНАЯ ТРЕВОГА: налёт, {} целей ({what})", plan.total)); + out.sound("minecraft:event.raid.horn", plan.centre, 64.0, 1.0); + } + + /// Whether a raid is still being planned. + pub fn planning(&self) -> bool { + self.plan.is_some() } /// A drone ready to fly from `from`, not yet in the sky. @@ -311,7 +380,8 @@ impl Sim { /// The price of each cell of `grid` for this kind flying low: 1, and much more where a /// system sees it there (its radar's reach for it, the land) and can shoot it. - fn price_grid(&self, grid: &Grid, kind: DroneKind) -> Vec { + fn price_grid(&self, grid: &Grid, kind: DroneKind) -> (Vec, usize) { + let mut cost = 0; let spec = kind.spec(); let height = spec.clearance.max(6.0); let n = grid.size; @@ -329,6 +399,7 @@ impl Sim { let r = (far / grid.cell).ceil() as i32 + 1; for i in (ci - r).max(0)..=(ci + r).min(n - 1) { for j in (cj - r).max(0)..=(cj + r).min(n - 1) { + cost += 1; let p = grid.centre((i, j), s.pos.y + height); let d = horizontal(p - s.pos).length(); if d > far { @@ -345,7 +416,7 @@ impl Sim { } } } - prices + (prices, cost) } /// A planned raid's drones take off when their time comes. @@ -365,7 +436,7 @@ impl Sim { /// When the raid is over: what it cost both sides. pub(super) fn report_wave(&mut self, out: &mut Out) { let Some(w) = &self.wave else { return }; - if !self.queue.is_empty() || self.drones.iter().any(|d| d.alive()) || !self.missiles.is_empty() || self.tick % 20 != 0 { + if self.plan.is_some() || !self.queue.is_empty() || self.drones.iter().any(|d| d.alive()) || !self.missiles.is_empty() || self.tick % 20 != 0 { return; } let kills = self.kills - w.kills; @@ -421,7 +492,12 @@ mod tests { aim: Aim::Smart, }; sim.order_raid(&order, &[DVec3::new(0.0, 70.0, -40.0)], &mut out); - eprintln!("planned in {:?}, queued {}", t.elapsed(), sim.queue.len()); + let mut ticks = 0; + while sim.planning() { + sim.step(&mut ground, None, &mut out); + ticks += 1; + } + eprintln!("planned in {:?} over {ticks} ticks, queued {}", t.elapsed(), sim.queue.len()); let t = std::time::Instant::now(); for k in 0..4000 { let s = std::time::Instant::now(); @@ -433,3 +509,50 @@ mod tests { assert!(sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()), "the raid never ended"); } } + +#[cfg(test)] +mod big { + use super::*; + + #[test] + fn sixty_one_systems_and_a_big_smart_raid() { + let mut sim = Sim::new(11); + for i in 0..61 { + let kind = SamKind::ALL[i % 5]; + let id = sim.id(); + let p = DVec3::new(((i % 8) as f64 - 4.0) * 40.0, 70.0, ((i / 8) as f64 - 4.0) * 40.0); + sim.sams.push(Sam::new(id, kind, p, 0.0)); + } + sim.targets.push(DVec3::new(0.0, 70.0, 0.0)); + sim.target_health.push(100.0); + let mut ground = Ground::default(); + let mut out = Out::default(); + for _ in 0..200 { + sim.step(&mut ground, None, &mut out); + } + let mut w = crate::wave::WaveConfig::default(); + w.counts = [10, 12, 6, 6, 6, 6, 4, 4, 6, 4, 1]; + w.aim = Aim::Smart; + w.approach = Approach::AllSides; + w.dist = 800.0; + let t = std::time::Instant::now(); + sim.order_raid(&w.order(), &sim.targets.clone(), &mut out); + let mut ticks = 0; + let mut slowest = std::time::Duration::ZERO; + while sim.planning() { + let s = std::time::Instant::now(); + sim.step(&mut ground, None, &mut out); + slowest = slowest.max(s.elapsed()); + ticks += 1; + } + eprintln!("planned {} in {:?} over {ticks} ticks, slowest {:?}", sim.queue.len(), t.elapsed(), slowest); + let mut worst = std::time::Duration::ZERO; + for _ in 0..3000 { + let s = std::time::Instant::now(); + out = Out::default(); + sim.step(&mut ground, None, &mut out); + worst = worst.max(s.elapsed()); + } + eprintln!("worst tick {:?}", worst); + } +}