Files
BT411/scratchpad/night13/ghostcensus.py
T
Joe DiPrimaandClaude Opus 5 4642129e76 #108 peer-side WRECK receipt: make ghosts countable
The un-wreck receipt had no partner, so counting ghosts in a field log meant
pairing it against

    [BTrender] wreck: 'thrdbr.bgf' missing -> gendbr.bgf fallback

which is a MISSING-ASSET WARNING, not a death -- it only prints for chassis
whose wreck model is absent.  Night 13's census found ONE ghost while testers
reported many, and there was no way to separate a real count from a chassis
accident.

Emit one ungated line for every REPLICANT entering the wreck state, symmetric
with the existing un-wreck line, so a log's ghost count is exactly
(wreck-enters minus un-wrecks) per entity:

    [wreck]   replicant H:E entered wreck state (mode X->9) at (x,z)
    [respawn] replicant H:E un-wrecked + warp   (mode 9->1) at (x,z)

Verified 2-node (200s, force-damage victim): 5 enters, 5 exits, exactly
paired -- while the old marker printed ZERO times in the same run.  That gap
is the point: five real deaths, invisible to what the census was reading.

Also lands the night-13 census tooling (ghostcensus.py) and the eject benches
that did NOT reproduce, with their failure modes in the headers so the next
attempt does not repeat them: five rigs failed to trigger a punch-out at all
(BT_BTNTEST never reached the mapper for 0x3D or 0x14; BT_EJECT_AT did not
fire either).  Panic-eject replication remains UNTESTED by bench.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
2026-08-07 07:48:04 -05:00

86 lines
3.3 KiB
Python

"""Night 13: census every 'wreck with no following un-wreck' across all logs.
A peer's view of a mech death is a pair:
[BTrender] wreck: ... (entity=H:E) <- the wreck appears
[respawn] replicant H:E un-wrecked + warp <- it comes back
An UNPAIRED wreck -- one with no un-wreck before that log's session ends --
is the ghost signature. Report per SESSION so 'was it just one game?' is
answerable, and dump the lines immediately before the first one.
"""
import re, sys, os, io
LOGDIR = r"C:\git\bt411\scratchpad\night13"
SESS = re.compile(r"===== BT411 SESSION.*?local=(\S+ \S+)")
WRECK = re.compile(r"\[BTrender\] wreck:.*?entity=(\d+:\d+)")
UNWRECK = re.compile(r"\[respawn\] replicant (\d+:\d+) un-wrecked")
EJECT = re.compile(r"\[eject\]|PUNCH-OUT")
def scan(path):
sessions = [] # (startline, stamp)
events = [] # (line, kind, ent)
with io.open(path, "r", encoding="latin-1", errors="replace") as f:
for n, line in enumerate(f, 1):
m = SESS.search(line)
if m:
sessions.append((n, m.group(1)))
continue
m = WRECK.search(line)
if m:
events.append((n, "wreck", m.group(1))); continue
m = UNWRECK.search(line)
if m:
events.append((n, "unwreck", m.group(1))); continue
if EJECT.search(line):
events.append((n, "eject", "-"))
return sessions, events
def session_of(sessions, line):
idx, stamp = 0, "?"
for i, (sl, st) in enumerate(sessions):
if sl <= line:
idx, stamp = i + 1, st
else:
break
return idx, stamp
for fn in sorted(os.listdir(LOGDIR)):
if not fn.endswith(".log") or fn.startswith("FAILURE"):
continue
path = os.path.join(LOGDIR, fn)
sessions, events = scan(path)
# pair wrecks to the next un-wreck of the same entity IN THE SAME SESSION
pending = {} # ent -> (line, sessidx)
unpaired = []
for (n, kind, ent) in events:
si = session_of(sessions, n)[0]
if kind == "wreck":
if ent in pending and pending[ent][1] == si:
unpaired.append(pending[ent]) # wreck superseded by another wreck
pending[ent] = (n, si)
elif kind == "unwreck":
if ent in pending and pending[ent][1] == si:
del pending[ent]
for ent, (n, si) in pending.items():
unpaired.append((n, si, ent))
norm = []
for u in unpaired:
norm.append(u if len(u) == 3 else (u[0], u[1], "?"))
norm.sort()
print("=" * 72)
print("%s sessions=%d wrecks=%d unwrecks=%d ejects=%d"
% (fn, len(sessions),
sum(1 for e in events if e[1] == "wreck"),
sum(1 for e in events if e[1] == "unwreck"),
sum(1 for e in events if e[1] == "eject")))
if not norm:
print(" no unpaired wrecks")
continue
bysess = {}
for (n, si, ent) in norm:
bysess.setdefault(si, []).append((n, ent))
for si in sorted(bysess):
stamp = sessions[si - 1][1] if 0 < si <= len(sessions) else "?"
print(" SESSION %d (%s): %d unpaired -> %s"
% (si, stamp, len(bysess[si]),
", ".join("%s@%d" % (e, n) for n, e in bysess[si][:6])))