Files
airdef/airdef-mod/src/sim/raid.rs
T

634 lines
26 KiB
Rust

//! Raids to order: what flies (kinds and numbers), from where, and at what.
//! The smart enemy plans: it knows where the systems are and what they see,
//! sends decoys first to draw their missiles, sends Lancets at the guns and
//! fast missiles at the long-range systems, routes its drones round the
//! systems' reach and low under their radars (through the dead zones and
//! behind hills), and times every launch so that all of it arrives at once.
use super::*;
use std::collections::BinaryHeap;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Approach {
/// From one sector, ±20°.
Sector,
/// From two sectors far apart.
TwoSectors,
/// From every side.
AllSides,
/// From one sector, a kind at a time, 15 s apart.
Waves,
}
impl Approach {
pub const ALL: [Approach; 4] = [Approach::Sector, Approach::TwoSectors, Approach::AllSides, Approach::Waves];
pub fn name(self) -> &'static str {
match self {
Approach::Sector => "один сектор",
Approach::TwoSectors => "два сектора",
Approach::AllSides => "со всех сторон",
Approach::Waves => "волнами",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Aim {
/// The marked objects.
Objects,
/// The objects, and a third of the drones at air defence.
ObjectsAndSams,
/// The planner chooses targets and routes, and times the strike.
Smart,
}
impl Aim {
pub const ALL: [Aim; 3] = [Aim::Objects, Aim::ObjectsAndSams, Aim::Smart];
pub fn name(self) -> &'static str {
match self {
Aim::Objects => "только объекты",
Aim::ObjectsAndSams => "объекты и ПВО",
Aim::Smart => "умный противник",
}
}
}
pub struct RaidOrder {
pub kinds: Vec<(DroneKind, u32)>,
pub approach: Approach,
/// How far out the drones start (ballistic missiles from their own range).
pub dist: f64,
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,
hits: u32,
launched: u32,
fired: u32,
sams: usize,
objects: f64,
size: u32,
tally: record::Tally,
}
/// The planner's grid: cells this big, at most this many a side.
pub(super) const CELL: f64 = 16.0;
pub(super) 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.
pub(super) const WORK_PER_TICK: usize = 40_000;
/// The planner's grid over a raid: its corner, its cells' size and how many a side.
pub(super) struct Grid {
pub(super) min: DVec3,
pub(super) cell: f64,
pub(super) size: i32,
}
impl Grid {
pub(super) fn index(&self, p: DVec3) -> (i32, i32) {
let n = self.size;
((((p.x - self.min.x) / self.cell) as i32).clamp(0, n - 1), (((p.z - self.min.z) / self.cell) as i32).clamp(0, n - 1))
}
fn centre(&self, (i, j): (i32, i32), y: f64) -> DVec3 {
DVec3::new(self.min.x + (i as f64 + 0.5) * self.cell, y, self.min.z + (j as f64 + 0.5) * self.cell)
}
}
/// The cheapest way over `grid` from `from` to `to` by its prices (A*): points along it, `to` last.
pub(super) 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);
let goal = grid.index(to);
let mut best = vec![f32::INFINITY; (n * n) as usize];
let mut came = vec![u32::MAX; (n * n) as usize];
let mut open = BinaryHeap::new();
best[at(start)] = 0.0;
open.push(Open(0.0, start));
let h = |(i, j): (i32, i32)| (((i - goal.0) as f64).powi(2) + ((j - goal.1) as f64).powi(2)).sqrt();
while let Some(Open(f, c)) = open.pop() {
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 {
continue;
}
for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)] {
let nb = (c.0 + di, c.1 + dj);
if nb.0 < 0 || nb.1 < 0 || nb.0 >= n || nb.1 >= n {
continue;
}
let len = if di != 0 && dj != 0 { std::f32::consts::SQRT_2 } else { 1.0 };
// The target's own cell is where it must go, whatever covers it.
let price = if nb == goal { len } else { len * prices[at(nb)] };
let ng = g + price;
if ng < best[at(nb)] {
best[at(nb)] = ng;
came[at(nb)] = at(c) as u32;
open.push(Open(f64::from(ng) + h(nb), nb));
}
}
}
if came[at(goal)] == u32::MAX {
return (vec![to], opened);
}
let mut cells = vec![goal];
let mut c = at(goal);
while came[c] != u32::MAX && c != at(start) {
c = came[c] as usize;
cells.push(((c as i32) / n, (c as i32) % n));
}
cells.reverse();
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, opened)
}
/// A cell in the A* queue: the cheapest first.
#[derive(PartialEq)]
struct Open(f64, (i32, i32));
impl Eq for Open {}
impl Ord for Open {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.0.total_cmp(&self.0)
}
}
impl PartialOrd for Open {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Sim {
/// 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(&centre) = objects.first() else { return 0 };
// One raid is planned at a time: another order waits for this one's drones to be sent.
if self.plan.is_some() {
out.messages.push("Противник ещё готовит прошлый налёт: подожди пару секунд".into());
return 0;
}
let total: u32 = order.kinds.iter().map(|k| k.1).sum();
if total == 0 {
return 0;
}
let smart = order.aim == Aim::Smart;
// A smart enemy comes from where its raids got through best.
let bearing = if smart { self.best_bearing() } else { self.rng.f() * TAU };
let second = bearing + std::f64::consts::PI * (0.6 + self.rng.f() * 0.6);
// 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 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 items = Vec::new();
for (group, &(kind, count)) in order.kinds.iter().enumerate() {
let spec = kind.spec();
for _ in 0..count {
let a = match order.approach {
Approach::Sector | Approach::Waves => bearing + self.rng.s() * 0.35,
Approach::TwoSectors => (if self.rng.chance(0.5) { bearing } else { second }) + self.rng.s() * 0.3,
Approach::AllSides => self.rng.f() * TAU,
};
let object = objects[(self.rng.f() * objects.len() as f64) as usize % objects.len()];
let (target, at_sam) = self.choose_target(kind, object, order.aim);
let aim = target + DVec3::new(self.rng.s() * 4.0, 0.0, self.rng.s() * 4.0);
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);
d.sector = ew::sector(a) as u8;
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 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 now = self.tick;
let arrive = plan.done.iter().map(|(_, t, e, g)| t + e - g).fold(0.0, f64::max);
for (mut d, time, early, gap) in plan.done {
d.smart = plan.smart;
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 {
kills: self.kills,
hits: self.hits,
launched: self.launched,
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: plan.total,
tally: self.tally,
});
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.
pub(super) fn make_drone(&mut self, kind: DroneKind, from: DVec3, dir: DVec3, target: DVec3) -> Drone {
let id = self.id();
let spec = kind.spec();
let (vel, flight) = if spec.ballistic.is_some() {
let flat = horizontal(target - from);
let time = (flat.length() / spec.speed).max(30.0);
let up = (target.y - from.y + 0.5 * GRAVITY * time * time) / time;
(flat / time + DVec3::Y * up, Flight::Ballistic)
} else {
(dir.normalize_or(DVec3::NEG_Z) * spec.speed, Flight::Cruise)
};
Drone {
id,
kind,
pos: from,
vel,
target,
flight,
roll: 0.0,
spin: 0.0,
age: 0,
engaged: 0,
route: Vec::new(),
low: false,
jammed: false,
sector: 0,
smart: false,
}
}
/// What a drone goes for: an object, or (by its kind and the raid's aim) a system. Returns the
/// point and the system's id.
fn choose_target(&mut self, kind: DroneKind, object: DVec3, aim: Aim) -> (DVec3, Option<u32>) {
let alive: Vec<&Sam> = self.sams.iter().filter(|s| s.alive()).collect();
if alive.is_empty() || kind.spec().decoy || aim == Aim::Objects {
return (object, None);
}
let pick = |list: &[&Sam], r: f64| -> Option<(DVec3, u32)> {
(!list.is_empty()).then(|| {
let s = list[(r * list.len() as f64) as usize % list.len()];
(s.pos, s.id)
})
};
let r = self.rng.f();
let roll = self.rng.f();
if aim == Aim::ObjectsAndSams {
return if roll < 0.33 { pick(&alive, r).map_or((object, None), |(p, id)| (p, Some(id))) } else { (object, None) };
}
// Smart: each kind at what it is good against; what gave itself away first (a jamming
// station, a radar, a system that fired).
let known = self.known_units();
let known_ew: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind == SamKind::Ew && known.iter().any(|k| k.0 == s.id)).collect();
if matches!(kind, DroneKind::Lancet | DroneKind::Kh22) && !known_ew.is_empty() && roll < 0.6 {
let s = known_ew[(r * known_ew.len() as f64) as usize % known_ew.len()];
return (s.pos, Some(s.id));
}
// Short-range systems (guns, Tor, P1-SUN) for the Lancets; long-range ones and the radars
// that feed them for the fast missiles.
let guns: Vec<&Sam> = alive.iter().copied().filter(|s| s.kind.armed() && s.kind.range() <= 200.0).collect();
let long: Vec<&Sam> =
alive.iter().copied().filter(|s| s.kind.range() >= 300.0 || s.kind == SamKind::Radar).collect();
let choice = match kind {
DroneKind::Lancet if roll < 0.75 => pick(&guns, r).or_else(|| pick(&alive, r)),
DroneKind::Kh22 | DroneKind::Zircon if roll < 0.45 => pick(&long, r),
DroneKind::Iskander | DroneKind::Kinzhal if roll < 0.3 => pick(&long, r),
DroneKind::Shahed | DroneKind::Geran3 if roll < 0.2 => pick(&alive, r),
_ => None,
};
choice.map_or((object, None), |(p, id)| (p, Some(id)))
}
/// 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.
pub(super) 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;
let mut prices = vec![1.0f32; (n * n) as usize];
// Known jamming stations: drones that navigate by satellite keep away.
if kind.jammable() {
let known = self.known_units();
for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known.iter().any(|k| k.0 == s.id)) {
let (ci, cj) = grid.index(s.pos);
let r = (EW_RADIUS / grid.cell).ceil() as i32;
for i in (ci - r).max(0)..=(ci + r).min(n - 1) {
for j in (cj - r).max(0)..=(cj + r).min(n - 1) {
if horizontal(grid.centre((i, j), s.pos.y) - s.pos).length() < EW_RADIUS {
prices[(i * n + j) as usize] += 12.0;
}
}
}
}
}
for s in self.sams.iter().filter(|s| s.alive() && s.roles & spec.class.bit() != 0) {
let sp = s.kind.spec();
let (lo, hi) = sp.missile.as_ref().map_or((0.0, 0.0), |m| (m.min, m.max));
let gun = sp.gun.as_ref().map_or(0.0, |g| g.range);
// A gun needs its own radar; a missile also flies on the tracks of any radar within data-link range.
let own = sight::reach(s, kind, height);
let net = self
.sams
.iter()
.filter(|r| r.alive() && r.id != s.id && r.pos.distance(s.pos) <= sight::LINK)
.map(|r| sight::reach(r, kind, height))
.fold(0.0, f64::max);
let (hi, gun) = (hi.min(own.max(net)), gun.min(own));
let far = hi.max(gun);
let at = sight::antenna(s);
let horizon = self.horizons.get(&s.id);
// Only the cells round it.
let (ci, cj) = grid.index(s.pos);
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 {
continue;
}
let missile = (lo..hi).contains(&d);
if !(missile || d < gun) {
continue;
}
if horizon.is_some_and(|h| h.hides(at, p)) {
continue;
}
prices[(i * n + j) as usize] += if missile { 30.0 } else { 18.0 };
}
}
}
(prices, cost)
}
/// A planned raid's drones take off when their time comes.
pub(super) fn release_queue(&mut self) {
let now = self.tick;
if self.queue.iter().all(|q| q.0 > now) {
return;
}
let (go, wait): (Vec<_>, Vec<_>) = std::mem::take(&mut self.queue).into_iter().partition(|q| q.0 <= now);
self.queue = wait;
for (_, d) in go {
self.drones.push(d);
self.launched += 1;
}
}
/// 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.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;
let through = self.hits - w.hits;
let launched = self.launched - w.launched;
// A system taken away during the raid takes its count along: never below none.
let fired: u32 = self.sams.iter().map(|s| s.fired).sum::<u32>().saturating_sub(w.fired);
let lost = w.sams.saturating_sub(self.sams.iter().filter(|s| s.alive()).count());
let objects = if self.target_health.is_empty() {
String::new()
} else {
let now: f64 = self.target_health.iter().sum();
format!(", объекты {:.0}% (потеряно {:.0}%)", now / self.target_health.len() as f64, (w.objects - now) / self.target_health.len() as f64)
};
out.messages.push(format!(
"Налёт окончен: сбито {kills} из {launched} (заявлено {}), прорвалось {through}; ракет ПВО {fired}; потеряно комплексов {lost}{objects}",
w.size
));
// Only what was counted; nothing is guessed for the rest.
let why = self.tally.since(w.tally).text();
if !why.is_empty() {
out.messages.push(format!("Неудачи ПВО: {why}"));
}
self.wave = None;
}
}
fn route_length(from: DVec3, route: &[DVec3], to: DVec3) -> f64 {
let mut at = from;
let mut len = 0.0;
for p in route.iter().chain(std::iter::once(&to)) {
len += horizontal(*p - at).length();
at = *p;
}
len
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_smart_raid_plans_and_flies_in_time() {
let mut sim = Sim::new(7);
for (i, kind) in SamKind::ALL.into_iter().enumerate() {
let id = sim.id();
sim.sams.push(Sam::new(id, kind, DVec3::new(-30.0 + 15.0 * i as f64, 70.0, 0.0), 0.0));
}
let mut ground = Ground::default();
let mut out = Out::default();
for _ in 0..30 {
sim.step(&mut ground, None, &mut out);
}
let t = std::time::Instant::now();
let order = RaidOrder {
kinds: vec![(DroneKind::Gerbera, 8), (DroneKind::Shahed, 12), (DroneKind::Kh101, 4), (DroneKind::Iskander, 2)],
approach: Approach::AllSides,
dist: 300.0,
aim: Aim::Smart,
};
sim.order_raid(&order, &[DVec3::new(0.0, 70.0, -40.0)], &mut out);
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();
out = Out::default();
sim.step(&mut ground, None, &mut out);
assert!(s.elapsed().as_millis() < 200, "tick {k} took {:?}", s.elapsed());
}
eprintln!("4000 ticks in {:?}, kills {} hits {}", t.elapsed(), sim.kills, sim.hits);
assert!(sim.queue.is_empty() && !sim.drones.iter().any(|d| d.alive()), "the raid never ended");
}
#[test]
fn systems_ready_together_send_one_missile_at_a_drone() {
let mut sim = Sim::new(3);
// Vertical launchers of one kind side by side: the drone comes into their reach in the
// same tick, and they fire without turning.
for i in 0..6 {
let id = sim.id();
sim.sams.push(Sam::new(id, SamKind::Tor, DVec3::new(2.0 * i as f64, 70.0, 0.0), 0.0));
}
let mut ground = Ground::default();
let mut out = Out::default();
for _ in 0..30 {
sim.step(&mut ground, None, &mut out);
}
sim.add_drone(DroneKind::Shahed, DVec3::new(0.0, 120.0, -400.0), DVec3::Z, DVec3::new(0.0, 70.0, 0.0));
let id = sim.drones[0].id;
for _ in 0..1200 {
sim.step(&mut ground, None, &mut out);
let on_it = sim.missiles.iter().filter(|m| m.target == id).count();
assert!(on_it <= 1, "{on_it} missiles on one Shahed");
}
assert!(sim.sams.iter().any(|s| s.fired > 0), "nothing was fired");
}
}
#[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::ARMED[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);
}
}