Raid planner: plan over several ticks at full detail instead of in one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,9 @@ pub fn summary(sim: &Sim, air: &[&Drone], player: DVec3) -> Vec<Line> {
|
||||
));
|
||||
}
|
||||
}
|
||||
if sim.planning() {
|
||||
lines.push(line("Противник планирует налёт…".into(), YELLOW));
|
||||
}
|
||||
if !sim.queue.is_empty() {
|
||||
lines.push(line(format!("Ещё взлетает: {}", sim.queue.len()), PINK));
|
||||
}
|
||||
|
||||
@@ -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<raid::Plan>,
|
||||
/// Each marked object's health, 0 to 100 (`targets`' order).
|
||||
pub target_health: Vec<f64>,
|
||||
/// 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<DVec3>, 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);
|
||||
|
||||
+176
-53
@@ -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<DroneKind, Vec<f32>>,
|
||||
routes: HashMap<(DroneKind, i32, i32, i32), Vec<DVec3>>,
|
||||
items: Vec<Item>,
|
||||
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<DVec3> {
|
||||
fn plan_route(grid: &Grid, prices: &[f32], from: DVec3, to: DVec3) -> (Vec<DVec3>, 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<DVec3>
|
||||
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<DVec3>
|
||||
}
|
||||
}
|
||||
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<DVec3>
|
||||
let mut route: Vec<DVec3> = 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<DroneKind, Vec<f32>> = HashMap::new();
|
||||
let mut routes: HashMap<(DroneKind, i32, i32, i32), Vec<DVec3>> = 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<f32> {
|
||||
fn price_grid(&self, grid: &Grid, kind: DroneKind) -> (Vec<f32>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user