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
|
`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.
|
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
|
## Reimplementation notes
|
||||||
|
|
||||||
- Defeat conditions for an escort stage are readable as: protected-asset
|
- 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
|
**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
|
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.
|
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*
|
But the measurement in docs/re/mission-escort-state.md says the loss is *slow* —
|
||||||
in danger early — it sat at a full 25000 for the first ~170 s and then lost
|
a few hundred to ~1400 HP/min against 25000, i.e. tens of minutes to sink. (When
|
||||||
~600 HP/min, i.e. ~40 minutes to sink from full. So permanently orbiting it
|
it starts varies: t≈170 s in one run, t≈70 s in another, so do not schedule on
|
||||||
would throw away most of the mission for nothing. The policy that fits the
|
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
|
measurement is: **fight freely until the asset is actually being hurt, then
|
||||||
switch to killing its attackers specifically.** Both the trigger and the target
|
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
|
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
|
SHIELD_OFF = 0x430 # candidate: == definition Shield MaxValue at spawn
|
||||||
DEF_HP = 0x054 # unit-struct-runtime.md
|
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"
|
# 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
|
# 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
|
# 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:
|
class Pilot:
|
||||||
KP, KD = 2.2, 0.45
|
KP, KD = 2.2, 0.45
|
||||||
FIRE_CONE = math.radians(9)
|
FIRE_CONE = math.radians(9) # fallback only; the real gate is angular size
|
||||||
FIRE_RANGE = 5000.0
|
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
|
TURRET_KEEPOUT = 2500.0 # ...and stay this far from things that shoot back
|
||||||
EVADE_QUIET = 5.0 # seconds without damage before re-engaging
|
EVADE_QUIET = 5.0 # seconds without damage before re-engaging
|
||||||
RETIRE_FRAC = 0.30 # hull fraction that sends us home
|
RETIRE_FRAC = 0.30 # hull fraction that sends us home
|
||||||
@@ -143,7 +159,7 @@ class Pilot:
|
|||||||
if d > self.ASSET_GUARD:
|
if d > self.ASSET_GUARD:
|
||||||
continue
|
continue
|
||||||
closing = float(np.dot(norm(rel), v)) # +ve = moving at the asset
|
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])
|
out.sort(key=lambda e: e[0])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -172,9 +188,27 @@ class Pilot:
|
|||||||
out.append((off, nm, p, v, r, "Turret" in nm or r >= navigator.Navigator.BIG_RADIUS))
|
out.append((off, nm, p, v, r, "Turret" in nm or r >= navigator.Navigator.BIG_RADIUS))
|
||||||
return out
|
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):
|
def pick(self, me_p, me_v, fwd, hos):
|
||||||
"""Nearest *fighter*, weighted by how far off the nose it is."""
|
"""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
|
best, bestscore = None, 1e18
|
||||||
for off, nm, p, v, r, hard in hos:
|
for off, nm, p, v, r, hard in hos:
|
||||||
if hard:
|
if hard:
|
||||||
@@ -183,11 +217,11 @@ class Pilot:
|
|||||||
d = float(np.linalg.norm(rel))
|
d = float(np.linalg.norm(rel))
|
||||||
if d < 1e-3:
|
if d < 1e-3:
|
||||||
continue
|
continue
|
||||||
lead = p + v * (d / max(speed, 300.0))
|
lead = self.lead_point(p, v, d)
|
||||||
theta = ang(lead - me_p, fwd)
|
theta = ang(lead - me_p, fwd)
|
||||||
score = d * (1.0 + 3.0 * (theta / math.pi) ** 2)
|
score = d * (1.0 + 3.0 * (theta / math.pi) ** 2)
|
||||||
if score < bestscore:
|
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
|
return best
|
||||||
|
|
||||||
def threat_vector(self, me_p, hos):
|
def threat_vector(self, me_p, hos):
|
||||||
@@ -308,16 +342,15 @@ class Pilot:
|
|||||||
# past three targets to reach a marginally worse fourth.
|
# past three targets to reach a marginally worse fourth.
|
||||||
atk = self.asset_attackers(hos, ast[2])
|
atk = self.asset_attackers(hos, ast[2])
|
||||||
best = None
|
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))
|
d_me = float(np.linalg.norm(p - me_p))
|
||||||
total = score + self.MY_RANGE_WEIGHT * d_me
|
total = score + self.MY_RANGE_WEIGHT * d_me
|
||||||
if best is None or total < best[0]:
|
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:
|
if best is not None:
|
||||||
_, off, nm, p, v, d_me = best
|
_, off, nm, p, v, d_me, r = best
|
||||||
speed_ref = max(speed, 300.0)
|
lead = self.lead_point(p, v, d_me)
|
||||||
lead = p + v * (d_me / speed_ref)
|
tgt = (off, nm, lead, lead - me_p, d_me, r)
|
||||||
tgt = (off, nm, lead, lead - me_p, d_me)
|
|
||||||
want = norm(tgt[3])
|
want = norm(tgt[3])
|
||||||
self.set_throttle(+1 if d_me > 2500.0 else 0)
|
self.set_throttle(+1 if d_me > 2500.0 else 0)
|
||||||
else:
|
else:
|
||||||
@@ -394,7 +427,8 @@ class Pilot:
|
|||||||
# so gating on it means the guns stay cold exactly when the loop is
|
# so gating on it means the guns stay cold exactly when the loop is
|
||||||
# manoeuvring — which is most of a dogfight.
|
# manoeuvring — which is most of a dogfight.
|
||||||
aim = self.sticks(norm(tgt[3]), M, w)[2:] if tgt else (math.pi, math.pi)
|
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)
|
fire = bool(fire and tgt and aim_ok and tgt[4] < self.FIRE_RANGE and pn < 1.2)
|
||||||
|
|
||||||
if not self.dry:
|
if not self.dry:
|
||||||
|
|||||||
Reference in New Issue
Block a user