pilot: real ballistics from the solved Shell records — and the metric that says it did not help
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.
This commit is contained in:
BIN
docs/re/captures/kill-counters-all-runs.png
Normal file
BIN
docs/re/captures/kill-counters-all-runs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
@@ -121,6 +121,37 @@ destroyer the avoidance expected to clear by 365 units — against a hull of rad
|
||||
`BIG_RADIUS` now gets a physical keep-out of **its own radius + 800**, with braking
|
||||
inside it, whatever its faction; the next run survived its full 330 s untouched.
|
||||
|
||||
## Ballistics from the disc data — and the measurement that invalidates the metric
|
||||
|
||||
The solved `Shell` records give the player's guns exactly
|
||||
(`Shell_TCAF_DeltaSaber_{NoseGun,Gun,Beam}_P`, all ✅ CONFIRMED):
|
||||
**`Velocity` 8000**, **`LifeTime` 0.5 s**, **`MaximumRange` 4000** — self-consistent,
|
||||
since 8000 × 0.5 = 4000 — plus shell `Radius` 20–30 and `Power` 15/30/40.
|
||||
|
||||
Two things in `pilot.py` were plainly wrong against those numbers, and both are fixed:
|
||||
|
||||
- **Lead used our own speed as the shell speed.** Flight time was `d / max(our_speed,
|
||||
300)`, i.e. 400–2000 u/s instead of 8000 — every shot led **4–16× too far ahead**.
|
||||
- **`FIRE_RANGE` was 5000**, past the range at which the shells expire.
|
||||
|
||||
**But the outcome metric says none of this has been shown to help.** The HUD's own
|
||||
counters — `YOU KILLED: WARSHIPS` / `WARPLANES` — read **0000 / 0000 at the end of
|
||||
every run**, including the nearest-fighter baseline. The pilot is not killing
|
||||
anything in any configuration, so "fraction of frames with the guns on" (12 % → 5 % →
|
||||
1 frame in 2639 as the firing gate was varied) was never measuring lethality. The
|
||||
corrections above are right on the physics and fix demonstrably wrong code; **they are
|
||||
not evidence of improvement**, and none is claimed.
|
||||
|
||||
The firing gate itself produced one clean result worth keeping: gating on the target's
|
||||
angular half-size **alone** (2.7° at 2584 units for a fighter) is far tighter than the
|
||||
steering loop can hold the nose, and firing collapsed to 1 frame in 2639. Angular size
|
||||
belongs in the gate as a **floor** that opens it up close, never as a cap.
|
||||
|
||||
**Open (next):** find out why nothing dies. The cheap decisive test is whether the
|
||||
`RB` command discharges the gun at all — the HUD carries a live ammo count
|
||||
(`MAIN MPM 00300`, which the weapon RE matched to `LoadingCount`), so holding fire and
|
||||
watching that number settles "we never shoot" vs "we shoot and miss" in one run.
|
||||
|
||||
## Reimplementation notes
|
||||
|
||||
- Defeat conditions for an escort stage are readable as: protected-asset
|
||||
|
||||
@@ -31,10 +31,11 @@ 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 asset is *not*
|
||||
in danger early — it sat at a full 25000 for the first ~170 s and then lost
|
||||
~600 HP/min, i.e. ~40 minutes to sink from full. So permanently orbiting it
|
||||
would throw away most of the mission for nothing. The policy that fits the
|
||||
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
|
||||
@@ -63,6 +64,19 @@ 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
|
||||
@@ -73,8 +87,10 @@ ASSET_NAME = os.environ.get("SYLPH_ASSET", "Acropolis")
|
||||
|
||||
class Pilot:
|
||||
KP, KD = 2.2, 0.45
|
||||
FIRE_CONE = math.radians(9)
|
||||
FIRE_RANGE = 5000.0
|
||||
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
|
||||
@@ -143,7 +159,7 @@ class Pilot:
|
||||
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))
|
||||
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
|
||||
|
||||
@@ -172,9 +188,27 @@ class Pilot:
|
||||
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."""
|
||||
speed = max(float(np.linalg.norm(me_v)), 1.0)
|
||||
best, bestscore = None, 1e18
|
||||
for off, nm, p, v, r, hard in hos:
|
||||
if hard:
|
||||
@@ -183,11 +217,11 @@ class Pilot:
|
||||
d = float(np.linalg.norm(rel))
|
||||
if d < 1e-3:
|
||||
continue
|
||||
lead = p + v * (d / max(speed, 300.0))
|
||||
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), score
|
||||
best, bestscore = (off, nm, lead, lead - me_p, d, r), score
|
||||
return best
|
||||
|
||||
def threat_vector(self, me_p, hos):
|
||||
@@ -308,16 +342,15 @@ class Pilot:
|
||||
# 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 in atk:
|
||||
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)
|
||||
best = (total, off, nm, p, v, d_me, r)
|
||||
if best is not None:
|
||||
_, off, nm, p, v, d_me = best
|
||||
speed_ref = max(speed, 300.0)
|
||||
lead = p + v * (d_me / speed_ref)
|
||||
tgt = (off, nm, lead, lead - me_p, d_me)
|
||||
_, 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:
|
||||
@@ -394,7 +427,8 @@ class Pilot:
|
||||
# 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)
|
||||
aim_ok = abs(aim[0]) < self.FIRE_CONE and abs(aim[1]) < self.FIRE_CONE
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user