Shell_TCAF_DeltaSaber_*_P: Velocity 8000, LifeTime 0.5 s, MaximumRange 4000 (self-consistent: 8000 x 0.5 = 4000), all confirmed. Two things were wrong: lead computed flight time as d / OUR speed (400-2000 u/s, so every shot was led 4-16x too far), and FIRE_RANGE was 5000 — past where the shells expire. Both fixed. But the HUD's own kill counters read 0000/0000 at the end of EVERY run including the nearest-fighter baseline, so the pilot kills nothing in any configuration and 'fraction of frames firing' was never measuring lethality. No improvement is claimed. One clean negative result kept: gating on the target's angular half-size alone (2.7 deg at 2584 units) is far tighter than the steering loop can hold the nose — firing collapsed to 1 frame in 2639. Angular size is a floor on the firing cone, never a cap. Next: the HUD carries a live ammo count, so holding fire and watching it settles 'we never shoot' vs 'we shoot and miss' in a single run.
497 lines
22 KiB
Python
497 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
"""A pilot that tries to stay alive, not just to shoot.
|
||
|
||
Every earlier loop flew a straight pursuit and was shot down; the trace from
|
||
ctrl_probe.py shows why — 1500 hull points gone in twelve seconds while sitting
|
||
in a turret's line of fire, with no reaction of any kind. Three measured facts
|
||
make a reaction possible:
|
||
|
||
* **Hull is `position + 0x154`** — at spawn it equals the unit definition's own
|
||
`HP` (1500 for the Delta Saber), it steps down 30/60/90 per hit, and it goes
|
||
negative at death. So damage is observable *as it happens*, not inferred.
|
||
* **`RT` accelerates and `LT` brakes**, and the setting persists: measured
|
||
ground speed went 488 → 1510 under RT, 488 → 174 under LT and then *stayed*
|
||
near 130 with the sticks neutral. (The older note "RT is not the throttle" was
|
||
drawn from a value-scan for a speed field, not from measuring the speed.)
|
||
* **`RB` fires** (autopilot-memory-driven.md).
|
||
|
||
So the loop is a state machine on damage rather than a pure pursuit:
|
||
|
||
ENGAGE chase and shoot the nearest hostile fighter
|
||
DEFEND the escorted asset is being attacked — go kill what is attacking IT
|
||
EVADE entered the moment the hull drops — turn away from the threats,
|
||
full throttle, jink; leave only after several quiet seconds
|
||
RETIRE hull below a floor: break for the friendly capital ship, which the
|
||
mission's own hint says is where you resupply
|
||
|
||
Turrets are treated as threats to be *kept at a distance*, not as targets: the
|
||
objective is the invading fighters, and the turret is what killed every previous
|
||
run.
|
||
|
||
**Why DEFEND exists, and why it is not simply "always guard the asset".** Stage
|
||
02 is an escort: a 240 s run ended in GAME OVER with our own hull at 1500/1500
|
||
because the ACROPOLIS sank while the pilot chased the nearest fighter 2 km away.
|
||
But the measurement in docs/re/mission-escort-state.md says the loss is *slow* —
|
||
a few hundred to ~1400 HP/min against 25000, i.e. tens of minutes to sink. (When
|
||
it starts varies: t≈170 s in one run, t≈70 s in another, so do not schedule on
|
||
it — react to the hull.) So permanently orbiting it would throw away most of the
|
||
mission for nothing. The policy that fits the
|
||
measurement is: **fight freely until the asset is actually being hurt, then
|
||
switch to killing its attackers specifically.** Both the trigger and the target
|
||
choice are read live — every entity's hull is `position + 0x154`, confirmed for
|
||
seven classes, so "is the asset losing hull" and "which hostiles are closing on
|
||
it" are both observable rather than inferred.
|
||
|
||
Usage: pilot.py <config.json> [seconds] [--dry]
|
||
"""
|
||
import json
|
||
import math
|
||
import os
|
||
import struct
|
||
import sys
|
||
import time
|
||
from collections import deque
|
||
|
||
import numpy as np
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
import gmem # noqa: E402
|
||
import navigator # noqa: E402
|
||
from navigator import ang, norm # noqa: E402
|
||
from flight_probe import Pad # noqa: E402
|
||
|
||
HULL_OFF = 0x154 # confirmed: == definition HP at spawn, falls when hit
|
||
SHIELD_OFF = 0x430 # candidate: == definition Shield MaxValue at spawn
|
||
DEF_HP = 0x054 # unit-struct-runtime.md
|
||
|
||
# The player Delta Saber's guns, from the solved Shell records
|
||
# (docs/re/captures/weapon-runtime-fields.csv, all ✅ CONFIRMED):
|
||
# Shell_TCAF_DeltaSaber_{NoseGun,Gun,Beam}_P Velocity 8000, LifeTime 0.5 s,
|
||
# MaximumRange 4000 (= 8000 × 0.5, self-consistent), shell Radius 20–30.
|
||
# Both numbers were previously wrong in this loop, and both mattered:
|
||
# * flight time was computed as d / OUR speed (400–2000 u/s), so every shot
|
||
# was led 4–16× too far ahead of the target;
|
||
# * FIRE_RANGE was 5000, i.e. a quarter of the shots were fired at targets
|
||
# the shells expire before reaching.
|
||
SHELL_VELOCITY = 8000.0
|
||
SHELL_MAX_RANGE = 4000.0
|
||
SHELL_RADIUS = 20.0
|
||
|
||
# Stage 02's protected asset. Named rather than derived: "the biggest friendly"
|
||
# picks the f105 cruiser (30000 HP > the Acropolis's 25000), and "the friendly
|
||
# with the most HP" picks it too, so neither rule finds the right ship. The
|
||
# per-stage asset is mission script, not a property of the entity, so it is
|
||
# configuration here — override with $SYLPH_ASSET for another stage.
|
||
ASSET_NAME = os.environ.get("SYLPH_ASSET", "Acropolis")
|
||
|
||
|
||
class Pilot:
|
||
KP, KD = 2.2, 0.45
|
||
FIRE_CONE = math.radians(9) # fallback only; the real gate is angular size
|
||
FIRE_RANGE = SHELL_MAX_RANGE # the shells simply do not arrive past this
|
||
CONE_MIN = math.radians(2.0)
|
||
CONE_MAX = math.radians(25.0) # close-in the target subtends a lot; let it
|
||
TURRET_KEEPOUT = 2500.0 # ...and stay this far from things that shoot back
|
||
EVADE_QUIET = 5.0 # seconds without damage before re-engaging
|
||
RETIRE_FRAC = 0.30 # hull fraction that sends us home
|
||
HZ = 8.0
|
||
# --- escort ---
|
||
ASSET_GUARD = 9000.0 # hostiles this close to the asset count as its attackers
|
||
ASSET_QUIET = 20.0 # s of no asset damage before dropping out of DEFEND
|
||
ASSET_ALERT = 0.5 # HP of asset damage that counts as "under attack"
|
||
ASSET_STANDOFF = 3500.0 # loiter this far out when guarding with no target
|
||
HULL_CLEARANCE = 800.0 # clearance ON TOP of a capital ship's own radius
|
||
CLOSING_WEIGHT = 4.0 # s of closing-rate credit when ranking attackers
|
||
MY_RANGE_WEIGHT = 0.35 # how much our own distance discounts a target
|
||
|
||
def __init__(self, W, pad, dry=False, log=sys.stdout):
|
||
self.W = W
|
||
self.pad = pad
|
||
self.dry = dry
|
||
self.log = log
|
||
# closest-point-of-approach avoidance is navigator.py's, reused as-is
|
||
self.av = navigator.Navigator(W, pad, dry=True, log=log)
|
||
self.prevM = None
|
||
self.firing = False
|
||
self.throttle = 0 # -1 brake, 0 coast, +1 accelerate
|
||
self.mode = "ENGAGE"
|
||
self.hp_hist = deque(maxlen=64)
|
||
self.hp0 = None
|
||
self.last_hit = -1e9
|
||
self.threat_dir = None
|
||
# escort bookkeeping
|
||
self.def_hp = {} # def_va -> definition HP
|
||
for va in W.defs:
|
||
self.def_hp[va] = self.f32(gmem.va_to_off(va) + DEF_HP)
|
||
self.asset_hist = deque(maxlen=64)
|
||
self.asset_hp0 = None
|
||
self.asset_last_hit = -1e9
|
||
|
||
def f32(self, off):
|
||
b = os.pread(self.W.fd, 4, off)
|
||
if len(b) < 4:
|
||
return float("nan")
|
||
return struct.unpack(">f", b)[0]
|
||
|
||
# ---------------------------------------------------------------- escort
|
||
def asset(self, ents):
|
||
"""The protected ship, and its live hull — same anchor as everyone's."""
|
||
for off, nm, p, v, r in ents:
|
||
if ASSET_NAME in nm:
|
||
hull = self.f32(off + HULL_OFF)
|
||
return off, nm, p, v, r, hull
|
||
return None
|
||
|
||
def asset_attackers(self, hos, a_p):
|
||
"""Hostile fighters near the asset, ranked by how hard they press it.
|
||
|
||
Ranking is distance to the asset *minus* credit for closing on it, so a
|
||
fighter 4 km out and running in outranks one sitting at 2 km drifting
|
||
away. Turrets and hulls are excluded for the same reason as everywhere
|
||
else: they are not killable objectives, they are keep-out zones.
|
||
"""
|
||
out = []
|
||
for off, nm, p, v, r, hard in hos:
|
||
if hard:
|
||
continue
|
||
rel = a_p - p
|
||
d = float(np.linalg.norm(rel))
|
||
if d > self.ASSET_GUARD:
|
||
continue
|
||
closing = float(np.dot(norm(rel), v)) # +ve = moving at the asset
|
||
out.append((d - self.CLOSING_WEIGHT * max(closing, 0.0), off, nm, p, v, d, r))
|
||
out.sort(key=lambda e: e[0])
|
||
return out
|
||
|
||
# ------------------------------------------------------------ own state
|
||
def own(self, off):
|
||
b = os.pread(self.W.fd, 8, off + HULL_OFF)
|
||
hull = struct.unpack_from(">f", b, 0)[0] if len(b) >= 4 else float("nan")
|
||
b2 = os.pread(self.W.fd, 4, off + SHIELD_OFF)
|
||
shield = struct.unpack(">f", b2)[0] if len(b2) == 4 else float("nan")
|
||
return hull, shield
|
||
|
||
def set_throttle(self, want):
|
||
"""RT / LT are a persistent setting, so only send the change."""
|
||
if want == self.throttle or self.dry:
|
||
return
|
||
self.pad.trig("RT", 1.0 if want > 0 else 0.0)
|
||
self.pad.trig("LT", 1.0 if want < 0 else 0.0)
|
||
self.throttle = want
|
||
|
||
# -------------------------------------------------------------- targets
|
||
def hostiles(self, ents, me_off):
|
||
out = []
|
||
for off, nm, p, v, r in ents:
|
||
if off == me_off or navigator.faction(nm) != "ADAN":
|
||
continue
|
||
out.append((off, nm, p, v, r, "Turret" in nm or r >= navigator.Navigator.BIG_RADIUS))
|
||
return out
|
||
|
||
def lead_point(self, p, v, d):
|
||
"""Where to aim: the target moved on by the shell's real flight time."""
|
||
return p + v * (d / SHELL_VELOCITY)
|
||
|
||
def fire_cone(self, d, r):
|
||
"""How far off the nose we will still pull the trigger.
|
||
|
||
The target's angular half-size, atan((r_target + r_shell) / range), is
|
||
the angle that can actually *hit* — but gating on it alone was measured
|
||
to be much worse than the old fixed 9°: at 2584 units a fighter subtends
|
||
2.7°, the steering loop holds the nose to ~10–30°, and firing collapsed
|
||
to 1 frame in 2639. Ammunition is free and the guns are continuous, so
|
||
the angular size belongs here as a **floor** that opens the gate wider
|
||
up close, never as a cap that closes it far out.
|
||
"""
|
||
if d < 1.0:
|
||
return self.CONE_MAX
|
||
return min(self.CONE_MAX, max(self.FIRE_CONE, math.atan2(r + SHELL_RADIUS, d)))
|
||
|
||
def pick(self, me_p, me_v, fwd, hos):
|
||
"""Nearest *fighter*, weighted by how far off the nose it is."""
|
||
best, bestscore = None, 1e18
|
||
for off, nm, p, v, r, hard in hos:
|
||
if hard:
|
||
continue # turrets and hulls are not the objective
|
||
rel = p - me_p
|
||
d = float(np.linalg.norm(rel))
|
||
if d < 1e-3:
|
||
continue
|
||
lead = self.lead_point(p, v, d)
|
||
theta = ang(lead - me_p, fwd)
|
||
score = d * (1.0 + 3.0 * (theta / math.pi) ** 2)
|
||
if score < bestscore:
|
||
best, bestscore = (off, nm, lead, lead - me_p, d, r), score
|
||
return best
|
||
|
||
def threat_vector(self, me_p, hos):
|
||
"""Where the danger is: inverse-square weighted direction to shooters."""
|
||
acc = np.zeros(3)
|
||
for off, nm, p, v, r, hard in hos:
|
||
rel = p - me_p
|
||
d = float(np.linalg.norm(rel))
|
||
if d < 1.0 or d > 8000.0:
|
||
continue
|
||
w = (1500.0 / d) ** 2 * (3.0 if hard else 1.0)
|
||
acc += norm(rel) * w
|
||
return norm(acc) if np.linalg.norm(acc) > 1e-6 else None
|
||
|
||
def friendly_base(self, ents, me_off):
|
||
"""The biggest friendly — the carrier the briefing says to resupply at."""
|
||
best = None
|
||
for off, nm, p, v, r in ents:
|
||
if off == me_off or navigator.faction(nm) != "TCAF":
|
||
continue
|
||
if best is None or r > best[4]:
|
||
best = (off, nm, p, v, r)
|
||
return best
|
||
|
||
# ------------------------------------------------------------ steering
|
||
def sticks(self, want, M, w):
|
||
fwd = M[self.W.fwd_row] * self.W.fwd_sign
|
||
right = M[(self.W.fwd_row + 1) % 3]
|
||
up = np.cross(fwd, right)
|
||
ex, ey, ez = (float(np.dot(want, right)), float(np.dot(want, up)),
|
||
float(np.dot(want, fwd)))
|
||
yaw = math.atan2(ex, ez if abs(ez) > 1e-3 else 1e-3)
|
||
pitch = math.atan2(ey, ez if abs(ez) > 1e-3 else 1e-3)
|
||
if ez < 0: # target behind: commit to a full turn
|
||
yaw = math.copysign(math.pi / 2, ex if ex else 1.0)
|
||
sx = max(-1.0, min(1.0, self.KP * yaw - self.KD * float(np.dot(w, up))))
|
||
sy = max(-1.0, min(1.0, -(self.KP * pitch - self.KD * float(np.dot(w, right)))))
|
||
return sx, sy, yaw, pitch
|
||
|
||
# ---------------------------------------------------------------- step
|
||
def step(self, t, dt):
|
||
ents = self.W.sample(t)
|
||
me = next((e for e in ents if "Player" in e[1]), None)
|
||
if me is None:
|
||
return None
|
||
me_off, me_nm, me_p, me_v, me_r = me
|
||
M = self.W.rot(me_off)
|
||
if M is None:
|
||
return "no-orientation"
|
||
fwd = M[self.W.fwd_row] * self.W.fwd_sign
|
||
right = M[(self.W.fwd_row + 1) % 3]
|
||
up = np.cross(fwd, right)
|
||
speed = float(np.linalg.norm(me_v))
|
||
|
||
hull, shield = self.own(me_off)
|
||
if self.hp0 is None and math.isfinite(hull) and hull > 0:
|
||
self.hp0 = hull
|
||
self.hp_hist.append((t, hull))
|
||
# damage over the last ~2 s; the hull only ever falls, so any drop is a hit
|
||
recent = [h for (ts, h) in self.hp_hist if t - ts <= 2.0]
|
||
dmg = (max(recent) - hull) if recent else 0.0
|
||
if dmg > 0.5:
|
||
self.last_hit = t
|
||
if hull <= 0:
|
||
return "DEAD"
|
||
|
||
# body angular velocity, for the damping term
|
||
w = np.zeros(3)
|
||
if self.prevM is not None and dt > 1e-3:
|
||
D = self.prevM @ M.T
|
||
w = np.array([D[2, 1] - D[1, 2], D[0, 2] - D[2, 0], D[1, 0] - D[0, 1]]) / (2 * dt)
|
||
self.prevM = M
|
||
|
||
hos = self.hostiles(ents, me_off)
|
||
self.threat_dir = self.threat_vector(me_p, hos)
|
||
frac = hull / self.hp0 if self.hp0 else 1.0
|
||
|
||
# ---- the escorted asset, read exactly like our own hull
|
||
ast = self.asset(ents)
|
||
a_frac, a_dmg = 1.0, 0.0
|
||
if ast is not None:
|
||
a_hull = ast[5]
|
||
if self.asset_hp0 is None and math.isfinite(a_hull) and a_hull > 0:
|
||
self.asset_hp0 = a_hull
|
||
self.asset_hist.append((t, a_hull))
|
||
recent = [h for (ts, h) in self.asset_hist if t - ts <= 4.0]
|
||
a_dmg = (max(recent) - a_hull) if recent else 0.0
|
||
if a_dmg > self.ASSET_ALERT:
|
||
self.asset_last_hit = t
|
||
a_frac = a_hull / self.asset_hp0 if self.asset_hp0 else 1.0
|
||
|
||
# ---- mode. Our own survival still outranks the escort: a dead pilot
|
||
# defends nothing, and RETIRE/EVADE are what stopped us being shot down.
|
||
if frac <= self.RETIRE_FRAC:
|
||
self.mode = "RETIRE"
|
||
elif t - self.last_hit < self.EVADE_QUIET:
|
||
self.mode = "EVADE"
|
||
elif ast is not None and t - self.asset_last_hit < self.ASSET_QUIET:
|
||
self.mode = "DEFEND"
|
||
else:
|
||
self.mode = "ENGAGE"
|
||
|
||
tgt = self.pick(me_p, me_v, fwd, hos)
|
||
push, worst = self.av.avoidance(me_p, me_v, me_r, ents, me_off)
|
||
|
||
if self.mode == "EVADE":
|
||
# Away from the guns, plus a jink so a straight escape line is not
|
||
# itself an easy solution for whatever is shooting.
|
||
away = -self.threat_dir if self.threat_dir is not None else fwd
|
||
jink = right * math.sin(t * 1.7) * 0.5 + up * math.cos(t * 2.3) * 0.35
|
||
want = norm(away + jink)
|
||
self.set_throttle(+1)
|
||
fire = False
|
||
elif self.mode == "DEFEND":
|
||
# Kill what is hitting the ship, not what is nearest to us. Among
|
||
# the asset's attackers prefer the one pressing it hardest, with a
|
||
# modest discount for being closer to us so the loop does not fly
|
||
# past three targets to reach a marginally worse fourth.
|
||
atk = self.asset_attackers(hos, ast[2])
|
||
best = None
|
||
for score, off, nm, p, v, d_a, r in atk:
|
||
d_me = float(np.linalg.norm(p - me_p))
|
||
total = score + self.MY_RANGE_WEIGHT * d_me
|
||
if best is None or total < best[0]:
|
||
best = (total, off, nm, p, v, d_me, r)
|
||
if best is not None:
|
||
_, off, nm, p, v, d_me, r = best
|
||
lead = self.lead_point(p, v, d_me)
|
||
tgt = (off, nm, lead, lead - me_p, d_me, r)
|
||
want = norm(tgt[3])
|
||
self.set_throttle(+1 if d_me > 2500.0 else 0)
|
||
else:
|
||
# Nothing on it right now: hold station near the ship instead of
|
||
# wandering off, so the next wave is met at the asset.
|
||
rel = ast[2] - me_p
|
||
d = float(np.linalg.norm(rel))
|
||
want = (norm(rel) if d > ast[4] + self.ASSET_STANDOFF
|
||
else norm(np.cross(rel, up)))
|
||
self.set_throttle(+1 if d > ast[4] + self.ASSET_STANDOFF else 0)
|
||
fire = True
|
||
elif self.mode == "RETIRE":
|
||
base = self.friendly_base(ents, me_off)
|
||
if base is not None:
|
||
rel = base[2] - me_p
|
||
d = float(np.linalg.norm(rel))
|
||
want = norm(rel) if d > base[4] + 400.0 else norm(np.cross(rel, up))
|
||
else:
|
||
want = -self.threat_dir if self.threat_dir is not None else fwd
|
||
self.set_throttle(+1)
|
||
fire = False
|
||
else:
|
||
# Straight lead pursuit and fly *through*. An earlier version orbited
|
||
# once inside a standoff radius and braked while doing it: it then
|
||
# circled one attacker for 40 s at ~700 m, at 60-100 units/s, never
|
||
# inside the firing cone. Overshooting and re-acquiring is better
|
||
# than a stall in the middle of a battle; collision avoidance already
|
||
# keeps a fighter-sized margin.
|
||
want = norm(tgt[3]) if tgt else fwd
|
||
if tgt and tgt[4] > 2500.0:
|
||
self.set_throttle(+1)
|
||
elif tgt and tgt[4] < 500.0 and speed > 900.0:
|
||
self.set_throttle(-1)
|
||
else:
|
||
self.set_throttle(0)
|
||
fire = True
|
||
|
||
# a turret inside its keep-out radius outranks the target
|
||
for off, nm, p, v, r, hard in hos:
|
||
if not hard:
|
||
continue
|
||
d = float(np.linalg.norm(p - me_p))
|
||
if d < self.TURRET_KEEPOUT:
|
||
want = norm(want + norm(me_p - p) * (2.0 * (1.0 - d / self.TURRET_KEEPOUT)))
|
||
break
|
||
|
||
# A capital ship is a wall, whatever its faction. DEFEND flies at the
|
||
# asset — which sits in the middle of the friendly formation — and the
|
||
# first escort run ended with hull 1500 -> DEAD in a single tick at
|
||
# 2026 units/s, 0.6 s from a friendly destroyer that the avoidance
|
||
# thought it would clear by 365 units. A destroyer's own radius is
|
||
# 2000. Closest-point-of-approach with a fighter-sized margin cannot
|
||
# keep us out of something that big, so give every large entity a hard
|
||
# physical keep-out scaled by ITS radius and brake inside it.
|
||
for off, nm, p, v, r in ents:
|
||
if off == me_off or r < navigator.Navigator.BIG_RADIUS:
|
||
continue
|
||
rel = me_p - p
|
||
d = float(np.linalg.norm(rel))
|
||
keep = r + self.HULL_CLEARANCE
|
||
if d < keep:
|
||
want = norm(want + norm(rel) * (2.5 * (1.0 - d / keep)))
|
||
if speed > 900.0:
|
||
self.set_throttle(-1)
|
||
break
|
||
|
||
pn = float(np.linalg.norm(push))
|
||
if pn > 1e-6:
|
||
want = norm(want + push * (3.0 if pn > 0.6 else 1.5))
|
||
|
||
sx, sy, yaw, pitch = self.sticks(want, M, w)
|
||
# The firing gate has to be measured against the TARGET, not against the
|
||
# commanded direction: `want` carries the avoidance and keep-out terms,
|
||
# so gating on it means the guns stay cold exactly when the loop is
|
||
# manoeuvring — which is most of a dogfight.
|
||
aim = self.sticks(norm(tgt[3]), M, w)[2:] if tgt else (math.pi, math.pi)
|
||
cone = self.fire_cone(tgt[4], tgt[5]) if tgt else self.FIRE_CONE
|
||
aim_ok = abs(aim[0]) < cone and abs(aim[1]) < cone
|
||
fire = bool(fire and tgt and aim_ok and tgt[4] < self.FIRE_RANGE and pn < 1.2)
|
||
|
||
if not self.dry:
|
||
self.pad.axis("LX", sx)
|
||
self.pad.axis("LY", sy)
|
||
if fire != self.firing:
|
||
(self.pad.press if fire else self.pad.release)("RB")
|
||
self.firing = fire
|
||
|
||
msg = (f"{self.mode:<7} hull={hull:6.0f} shd={shield:6.0f} spd={speed:6.0f} "
|
||
f"thr={self.throttle:+d} yaw={math.degrees(yaw):+6.1f} "
|
||
f"pit={math.degrees(pitch):+6.1f} aim={math.degrees(aim[0]):+6.1f}"
|
||
f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)}")
|
||
if ast is not None:
|
||
msg += f" ast={a_frac*100:5.1f}%"
|
||
if a_dmg > self.ASSET_ALERT:
|
||
msg += f" ASSET-HIT -{a_dmg:.0f}"
|
||
if dmg > 0.5:
|
||
msg += f" HIT -{dmg:.0f}"
|
||
if tgt:
|
||
msg += f" tgt={tgt[1][3:24]:<21} d={tgt[4]:6.0f}"
|
||
if worst:
|
||
msg += f" | AVOID {worst[1][3:18]} miss={worst[3]:5.0f} t={worst[4]:4.1f}"
|
||
return msg
|
||
|
||
def run(self, secs):
|
||
self.W.scan()
|
||
t0 = time.time()
|
||
last, last_scan = t0, 0.0
|
||
hostiles0 = None
|
||
while time.time() - t0 < secs:
|
||
t = time.time()
|
||
if t - last_scan > 5.0:
|
||
ents = self.W.scan()
|
||
last_scan = t
|
||
n_ad = sum(1 for _, va in ents
|
||
if navigator.faction(self.W.defs[va]) == "ADAN")
|
||
if hostiles0 is None:
|
||
hostiles0 = n_ad
|
||
print(f"[{t-t0:6.1f}] scan: {len(ents)} entities, {n_ad} ADAN "
|
||
f"(start {hostiles0})", file=self.log, flush=True)
|
||
msg = self.step(t, t - last)
|
||
last = t
|
||
if msg is None:
|
||
print(f"[{t-t0:6.1f}] player object gone — stopping",
|
||
file=self.log, flush=True)
|
||
break
|
||
print(f"[{t-t0:6.1f}] {msg}", file=self.log, flush=True)
|
||
if msg == "DEAD":
|
||
break
|
||
time.sleep(max(0.0, 1.0 / self.HZ - (time.time() - t)))
|
||
if not self.dry:
|
||
self.pad.reset()
|
||
print(f"# flew {time.time()-t0:.0f}s", file=self.log, flush=True)
|
||
|
||
|
||
def main():
|
||
cfg = json.load(open(sys.argv[1]))
|
||
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 180.0
|
||
W = navigator.World(cfg)
|
||
Pilot(W, Pad(), dry="--dry" in sys.argv).run(secs)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|