534 lines
24 KiB
Python
534 lines
24 KiB
Python
#!/usr/bin/env python
|
|
# weapons_sweep checker -- ONE comprehensive per-family PASS/FAIL adjudicator
|
|
# for scratchpad/night16/weapons_sweep.sh (2-node mad2-vs-madcat fight).
|
|
#
|
|
# Families / assertions (env on both nodes: BT_DMG_LOG BT_PROJ_LOG BT_DEATH_LOG
|
|
# BT_AMMO_LOG):
|
|
# A1 ENERGY : named [emitter] FIRED lines present AND laser/PPC [dmghit]
|
|
# (type 3/4) land on the victim.
|
|
# A2 AC : type=1 [dmghit] present, ALL of them burst=1 (single panel,
|
|
# trigger-time hitscan); ZERO damage-carrying ballistic rounds in
|
|
# the pool ([projectile] PUSH guided=0 must all be dmg=0 -- the
|
|
# #171 restoration: the visible round is the cosmetic 0xBCD
|
|
# tracer); every damage-carrying DET is reconciled to a guided
|
|
# (missile) push.
|
|
# A3 MISSILE: [ammo] LRM/SRM FIRED receipts, [projectile] IMPACT dmg>0
|
|
# deliveries with burst>1 salvo bursts, type=2 [dmghit]s landing
|
|
# (burst>1 present) -- contact through the 4.0u fuze at
|
|
# drag-governed speeds; plus >=1 death or zone cascade (vitality).
|
|
# A4 NO-REGR: no zone cascades twice within one life per log (the #174
|
|
# guard); every [projectile] BYSTANDER receipt in the 2-mech
|
|
# fight is a TRUE positive of the lane-12 sweep.
|
|
# CALIBRATION NOTE (2026-08-13 run): the original "zero receipts
|
|
# in a 2-mech fight" operationalization assumed a lock is always
|
|
# the enemy mech. The run disproved the ASSUMPTION, not the
|
|
# code: the production aim ray designates SCENERY constantly
|
|
# (the ac_bench lesson), and rounds locked onto a structure DO
|
|
# fly past the other mech -- the binary's world sweep detonates
|
|
# on ANY solid in the path (FUN_0042291c via @004bef78 [T1]), so
|
|
# a receipt there is the sweep WORKING. Forensics on all 11
|
|
# receipts of the calibration run (bys_forensics.py): struck ==
|
|
# the other live mech every time, aim point 146-1016u away from
|
|
# the strike (the round was flying elsewhere), one paired IMPACT
|
|
# delivery each. The check now asserts the actual false-positive
|
|
# conditions per receipt:
|
|
# - struck id == the shooter's OWN mech -> self-strike bug
|
|
# - struck id not a known player mech -> phantom strike
|
|
# - no DET/IMPACT pairing -> lost/duplicated delivery
|
|
# - strike within 30u of the round's aim -> the sweep raced the
|
|
# authentic 4.0u fuze at the lock itself (exclusion suspect)
|
|
# and flags a hit receipt-print cap (24) as unmeasurable.
|
|
# A5 FLOORS : rig-sanity minimums so a quiet rig cannot PASS.
|
|
#
|
|
# --selftest: prove the detector CAN fail (the week's hard rule) by feeding it
|
|
# synthetic pre-fix log shapes (pre-#171 damage-carrying ballistic round,
|
|
# pre-#174 double cascade, a bystander receipt, and an empty/quiet rig) and
|
|
# requiring each to FAIL the matching assertion.
|
|
import io
|
|
import math
|
|
import re
|
|
import sys
|
|
|
|
RX_DMGHIT = re.compile(r"\[dmghit\] mech=(\S+) zone=(-?\d+) vital=(\d+) "
|
|
r"type=(\d+) amt=([\d.eE+\-]+) burst=(\d+)")
|
|
RX_PUSH = re.compile(r"\[projectile\] PUSH target=(\S+) len=(\S+) speed=(\S+) "
|
|
r"dmg=([\d.eE+\-]+) guided=(\d+)")
|
|
RX_DET = re.compile(r"\[projectile\] DET .*?dmg=([\d.eE+\-]+) lead=(\d+)")
|
|
RX_IMPACT = re.compile(r"\[projectile\] IMPACT damage=([\d.eE+\-]+) .*?burst=(\d+)")
|
|
RX_EMIT = re.compile(r"\[emitter\] FIRED '([^']+)'")
|
|
RX_AMMO = re.compile(r"\[ammo\] (\S+) FIRED, rounds left=")
|
|
RX_CASC = re.compile(r"\[cascade\] zone (\d+) DESTROYED")
|
|
RX_BYS = re.compile(r"\[projectile\] BYSTANDER id=(\d+) t=(\S+) at\(([^)]*)\) "
|
|
r"dmg=([\d.eE+\-]+) lockedTgt=(\S+)")
|
|
RX_DETAIM = re.compile(r"\[projectile\] DET at\(([^)]*)\).*?aim\(([^)]*)\)")
|
|
RX_RESET = re.compile(r"Mech::Reset (\d+):(\d+)")
|
|
RX_DEATH = re.compile(r"\[death\] VehicleDead")
|
|
|
|
# rig-sanity floors (A5) -- calibrated against the proven kd_bench/cascade_bench
|
|
# engagement levels (worker's 180s single-shooter evidence run: 200 dmghits).
|
|
FLOOR_DMGHITS_TOTAL = 40
|
|
FLOOR_ENERGY_HITS = 5 # type 3+4 dmghits
|
|
FLOOR_AC_HITS = 3 # type=1 dmghits
|
|
FLOOR_MISSILE_HITS = 3 # type=2 dmghits
|
|
FLOOR_MISSILE_BURSTS = 2 # IMPACT dmg>0 burst>1 deliveries
|
|
FLOOR_EMITTER_FIRED = 10
|
|
FLOOR_VITALITY = 1 # deaths + cascades
|
|
|
|
|
|
def _p3(s):
|
|
try:
|
|
v = [float(x) for x in s.split(",")]
|
|
return v if len(v) == 3 else None
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _dist(a, b):
|
|
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
|
|
|
|
|
def parse(lines):
|
|
lines = list(lines)
|
|
d = {
|
|
"dmghit": [], # (mech, zone, vital, type, amt, burst)
|
|
"push": [], # (dmg, guided)
|
|
"det": [], # (dmg, lead)
|
|
"impact": [], # (dmg, burst)
|
|
"emitter": {}, # name -> count
|
|
"ammo": {}, # name -> count
|
|
"bystander": 0,
|
|
"bys_recs": [], # (line, struckid, at, dmg, lockptr, aim|None, paired)
|
|
"bys_capped": 0, # the 24-receipt print cap was reached
|
|
"own": None, # this node's own mech entity id (first Reset h:ID)
|
|
"mechids": set(), # every player-mech entity id seen in Reset lines
|
|
"deaths": 0,
|
|
"resets": 0,
|
|
"casc_total": 0,
|
|
"casc_worst": {}, # zone -> worst per-life repeat count
|
|
}
|
|
life = {}
|
|
for i, l in enumerate(lines):
|
|
m = RX_DMGHIT.search(l)
|
|
if m:
|
|
d["dmghit"].append((m.group(1), int(m.group(2)), int(m.group(3)),
|
|
int(m.group(4)), float(m.group(5)), int(m.group(6))))
|
|
continue
|
|
m = RX_PUSH.search(l)
|
|
if m:
|
|
d["push"].append((float(m.group(4)), int(m.group(5))))
|
|
continue
|
|
m = RX_BYS.search(l)
|
|
if m:
|
|
struck = int(m.group(1))
|
|
at = _p3(m.group(3))
|
|
if "receipt cap reached" in l:
|
|
d["bys_capped"] = 1
|
|
# the receipt's own detonation follows within a few lines: DET
|
|
# (carries the round's aim) then the mech delivery IMPACT
|
|
aim = None
|
|
paired = False
|
|
for j in range(i + 1, min(i + 5, len(lines))):
|
|
dm = RX_DETAIM.search(lines[j])
|
|
if dm and aim is None:
|
|
aim = _p3(dm.group(2))
|
|
if "[projectile] IMPACT damage=" in lines[j]:
|
|
paired = True
|
|
break
|
|
d["bys_recs"].append((i + 1, struck, at, float(m.group(4)),
|
|
m.group(5), aim, paired))
|
|
d["bystander"] += 1
|
|
continue
|
|
m = RX_DET.search(l)
|
|
if m:
|
|
d["det"].append((float(m.group(1)), int(m.group(2))))
|
|
continue
|
|
m = RX_IMPACT.search(l)
|
|
if m:
|
|
d["impact"].append((float(m.group(1)), int(m.group(2))))
|
|
continue
|
|
m = RX_EMIT.search(l)
|
|
if m:
|
|
d["emitter"][m.group(1)] = d["emitter"].get(m.group(1), 0) + 1
|
|
continue
|
|
m = RX_AMMO.search(l)
|
|
if m:
|
|
d["ammo"][m.group(1)] = d["ammo"].get(m.group(1), 0) + 1
|
|
continue
|
|
m = RX_CASC.search(l)
|
|
if m:
|
|
z = int(m.group(1))
|
|
life[z] = life.get(z, 0) + 1
|
|
if life[z] > d["casc_worst"].get(z, 0):
|
|
d["casc_worst"][z] = life[z]
|
|
d["casc_total"] += 1
|
|
continue
|
|
if "Mech::Reset" in l:
|
|
life = {}
|
|
d["resets"] += 1
|
|
m = RX_RESET.search(l)
|
|
if m:
|
|
mid = int(m.group(2))
|
|
d["mechids"].add(mid)
|
|
if d["own"] is None:
|
|
d["own"] = mid
|
|
continue
|
|
if RX_DEATH.search(l):
|
|
d["deaths"] += 1
|
|
return d
|
|
|
|
|
|
def adjudicate(logs, verbose=True):
|
|
"""logs: {name: parsed-dict}. Returns (fails, table_rows)."""
|
|
fails = []
|
|
|
|
def agg(key):
|
|
out = []
|
|
for nm in logs:
|
|
out.extend(logs[nm][key])
|
|
return out
|
|
|
|
hits = agg("dmghit")
|
|
push = agg("push")
|
|
det = agg("det")
|
|
impact = agg("impact")
|
|
emitter = {}
|
|
ammo = {}
|
|
for nm in logs:
|
|
for k, v in logs[nm]["emitter"].items():
|
|
emitter[k] = emitter.get(k, 0) + v
|
|
for k, v in logs[nm]["ammo"].items():
|
|
ammo[k] = ammo.get(k, 0) + v
|
|
bystander = sum(logs[nm]["bystander"] for nm in logs)
|
|
deaths = sum(logs[nm]["deaths"] for nm in logs)
|
|
resets = sum(logs[nm]["resets"] for nm in logs)
|
|
cascades = sum(logs[nm]["casc_total"] for nm in logs)
|
|
|
|
h_energy = [h for h in hits if h[3] in (3, 4)]
|
|
h_laser = [h for h in hits if h[3] == 3]
|
|
h_ppc = [h for h in hits if h[3] == 4]
|
|
h_ball = [h for h in hits if h[3] == 1]
|
|
h_missile = [h for h in hits if h[3] == 2]
|
|
h_mis_multi = [h for h in h_missile if h[5] > 1]
|
|
ball_multi = [h for h in h_ball if h[5] != 1]
|
|
|
|
push_ballistic = [p for p in push if p[1] == 0]
|
|
push_ballistic_dmg = [p for p in push_ballistic if p[0] != 0.0]
|
|
push_guided_dmg = [p for p in push if p[1] == 1 and p[0] > 0.0]
|
|
det_dmg = [x for x in det if x[0] != 0.0]
|
|
imp_dmg = [x for x in impact if x[0] > 0.0]
|
|
imp_multi = [x for x in imp_dmg if x[1] > 1]
|
|
|
|
ammo_ac = {k: v for k, v in ammo.items() if k.upper().startswith("AFC")}
|
|
ammo_mis = {k: v for k, v in ammo.items()
|
|
if k.upper().startswith(("LRM", "SRM", "STRK", "STREAK", "NARC", "NRK"))}
|
|
|
|
# ---- A1 ENERGY ----
|
|
a1 = []
|
|
if not emitter:
|
|
a1.append("no named [emitter] FIRED lines")
|
|
if len(h_energy) < FLOOR_ENERGY_HITS:
|
|
a1.append("laser/PPC dmghits %d < floor %d" % (len(h_energy), FLOOR_ENERGY_HITS))
|
|
if not h_laser:
|
|
a1.append("no type=3 laser dmghit landed")
|
|
if sum(emitter.values()) < FLOOR_EMITTER_FIRED:
|
|
a1.append("emitter FIRED total %d < floor %d"
|
|
% (sum(emitter.values()), FLOOR_EMITTER_FIRED))
|
|
|
|
# ---- A2 AC HITSCAN (#171) ----
|
|
a2 = []
|
|
if len(h_ball) < FLOOR_AC_HITS:
|
|
a2.append("type=1 dmghits %d < floor %d" % (len(h_ball), FLOOR_AC_HITS))
|
|
if ball_multi:
|
|
a2.append("%d type=1 dmghit(s) with burst!=1 (multi-panel ballistic -- "
|
|
"not the single-panel hitscan)" % len(ball_multi))
|
|
if push_ballistic_dmg:
|
|
a2.append("%d ballistic PUSH(es) with dmg!=0 -- a damage-carrying "
|
|
"ballistic round entered the pool (pre-#171 shape)"
|
|
% len(push_ballistic_dmg))
|
|
if not push_ballistic:
|
|
a2.append("no ballistic (guided=0) PUSH at all -- AC never cycled")
|
|
if not ammo_ac:
|
|
a2.append("no [ammo] AFC* FIRED receipt -- no autocannon cycled a round")
|
|
# every damage-carrying DET must be attributable to a guided (missile) push,
|
|
# per log (pool is node-local)
|
|
for nm in logs:
|
|
nd = len([x for x in logs[nm]["det"] if x[0] != 0.0])
|
|
ng = len([p for p in logs[nm]["push"] if p[1] == 1 and p[0] > 0.0])
|
|
if nd > ng:
|
|
a2.append("%s: %d damage DETs > %d guided damage PUSHes -- an "
|
|
"unguided round detonated carrying damage" % (nm, nd, ng))
|
|
|
|
# ---- A3 MISSILES ----
|
|
a3 = []
|
|
if not ammo_mis:
|
|
a3.append("no [ammo] LRM/SRM FIRED receipt")
|
|
if len(imp_dmg) < FLOOR_MISSILE_HITS:
|
|
a3.append("missile IMPACT deliveries %d < floor %d"
|
|
% (len(imp_dmg), FLOOR_MISSILE_HITS))
|
|
if len(imp_multi) < FLOOR_MISSILE_BURSTS:
|
|
a3.append("salvo bursts (IMPACT dmg>0 burst>1) %d < floor %d"
|
|
% (len(imp_multi), FLOOR_MISSILE_BURSTS))
|
|
if len(h_missile) < FLOOR_MISSILE_HITS:
|
|
a3.append("type=2 dmghits %d < floor %d" % (len(h_missile), FLOOR_MISSILE_HITS))
|
|
if not h_mis_multi:
|
|
a3.append("no type=2 dmghit with burst>1 (no salvo cluster landed)")
|
|
if deaths + resets + cascades < FLOOR_VITALITY:
|
|
a3.append("rig vitality: deaths+resets+cascades = %d < %d"
|
|
% (deaths + resets + cascades, FLOOR_VITALITY))
|
|
|
|
# ---- A4 NO REGRESSION ----
|
|
a4 = []
|
|
for nm in logs:
|
|
dup = {z: c for z, c in logs[nm]["casc_worst"].items() if c > 1}
|
|
if dup:
|
|
a4.append("%s re-descended zones %s within one life (#174 ALIVE)"
|
|
% (nm, dup))
|
|
# BYSTANDER receipts: every receipt must be a TRUE positive (see the
|
|
# calibration note in the header -- rounds locked onto scenery legitimately
|
|
# detonate on the other mech crossing the flight path; the binary's world
|
|
# sweep fires on ANY solid [T1]). False-positive conditions per receipt:
|
|
all_mechids = set()
|
|
for nm in logs:
|
|
all_mechids |= logs[nm]["mechids"]
|
|
bys_true = 0
|
|
for nm in logs:
|
|
own = logs[nm]["own"]
|
|
if logs[nm]["bys_capped"]:
|
|
a4.append("%s: BYSTANDER print cap (24) reached -- receipt count "
|
|
"unmeasurable, rerun a shorter window" % nm)
|
|
for (ln, struck, at, bdmg, lockptr, aim, paired) in logs[nm]["bys_recs"]:
|
|
bad = []
|
|
if own is not None and struck == own:
|
|
bad.append("SELF-STRIKE (sweep hit the shooter's own mech)")
|
|
if all_mechids and struck not in all_mechids:
|
|
bad.append("phantom strike: id %d is no known player mech"
|
|
% struck)
|
|
if aim is None or not paired:
|
|
bad.append("no paired DET/IMPACT delivery (lost or duplicated "
|
|
"round)")
|
|
elif at is not None and _dist(at, aim) < 30.0:
|
|
bad.append("strike within %.0fu of the round's own aim -- the "
|
|
"sweep raced the 4.0u fuze at the lock itself "
|
|
"(target-exclusion suspect)" % _dist(at, aim))
|
|
if bad:
|
|
a4.append("%s:%d BYSTANDER FALSE POSITIVE: %s"
|
|
% (nm, ln, "; ".join(bad)))
|
|
else:
|
|
bys_true += 1
|
|
|
|
# ---- A5 FLOORS ----
|
|
a5 = []
|
|
if len(hits) < FLOOR_DMGHITS_TOTAL:
|
|
a5.append("total dmghits %d < floor %d (quiet rig proves nothing)"
|
|
% (len(hits), FLOOR_DMGHITS_TOTAL))
|
|
|
|
rows = [
|
|
("ENERGY", a1, "emitterFIRED=%d(names=%d) hits t3=%d t4=%d"
|
|
% (sum(emitter.values()), len(emitter),
|
|
len(h_laser), len(h_ppc))),
|
|
("AC", a2, "acFIRED=%d hits t1=%d (burst!=1: %d) ballPUSH=%d "
|
|
"(dmg!=0: %d)"
|
|
% (sum(ammo_ac.values()), len(h_ball), len(ball_multi),
|
|
len(push_ballistic), len(push_ballistic_dmg))),
|
|
("MISSILE", a3, "misFIRED=%d IMPACTdmg=%d(burst>1:%d) hits t2=%d"
|
|
"(burst>1:%d) FIZZ n/a"
|
|
% (sum(ammo_mis.values()), len(imp_dmg), len(imp_multi),
|
|
len(h_missile), len(h_mis_multi))),
|
|
("NO-REGR", a4, "cascades=%d worstRepeat=%d bystander=%d "
|
|
"(true-positive crossfire=%d, false=%d)"
|
|
% (cascades,
|
|
max([c for nm in logs
|
|
for c in logs[nm]["casc_worst"].values()] or [0]),
|
|
bystander, bys_true, bystander - bys_true)),
|
|
("RIG", a5, "dmghits=%d deaths=%d resets=%d DETdmg>0=%d "
|
|
"guidedDmgPUSH=%d"
|
|
% (len(hits), deaths, resets, len(det_dmg),
|
|
len(push_guided_dmg))),
|
|
]
|
|
for fam, fl, _ in rows:
|
|
fails.extend("%s: %s" % (fam, f) for f in fl)
|
|
|
|
if verbose:
|
|
print("=== WEAPONS SWEEP -- per-family verdict ===")
|
|
for fam, fl, info in rows:
|
|
print("%-8s %-4s %s" % (fam, "PASS" if not fl else "FAIL", info))
|
|
for f in fl:
|
|
print(" - %s" % f)
|
|
print("weapon receipts (named, both logs):")
|
|
for k in sorted(set(list(emitter) + list(ammo))):
|
|
n = emitter.get(k, 0) + ammo.get(k, 0)
|
|
src = "emitter" if k in emitter else "ammo"
|
|
print(" %-16s %4d (%s)" % (k, n, src))
|
|
for nm in logs:
|
|
print("%s: dmghits=%d pushes=%d dets=%d impacts=%d casc=%d "
|
|
"resets=%d deaths=%d bys=%d"
|
|
% (nm, len(logs[nm]["dmghit"]), len(logs[nm]["push"]),
|
|
len(logs[nm]["det"]), len(logs[nm]["impact"]),
|
|
logs[nm]["casc_total"], logs[nm]["resets"],
|
|
logs[nm]["deaths"], logs[nm]["bystander"]))
|
|
print("RESULT:", "PASS" if not fails else "FAIL")
|
|
if fails:
|
|
for f in fails:
|
|
print(" FAIL:", f)
|
|
return fails
|
|
|
|
|
|
def load(fn):
|
|
return parse(io.open(fn, encoding="latin-1", errors="replace").read().splitlines())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# --selftest: the detector must FAIL on pre-fix log shapes (negative controls)
|
|
# ---------------------------------------------------------------------------
|
|
HEALTHY = [
|
|
# energy: named emitters + laser/PPC hits
|
|
] + [
|
|
"[emitter] FIRED 'ERMLaser_1' damage=8 heat=2" for _ in range(6)
|
|
] + [
|
|
"[emitter] FIRED 'ERPPC' damage=15 heat=6" for _ in range(6)
|
|
] + [
|
|
"[dmghit] mech=42 zone=%d vital=0 type=3 amt=8 burst=1 lvl 0->0.1" % (i % 5)
|
|
for i in range(8)
|
|
] + [
|
|
"[dmghit] mech=42 zone=2 vital=0 type=4 amt=15 burst=1 lvl 0->0.2",
|
|
] + [
|
|
# AC: named ammo fire, cosmetic tracer pushes, hitscan type=1 burst=1 hits
|
|
"[ammo] AFC50 FIRED, rounds left=93" for _ in range(4)
|
|
] + [
|
|
"[projectile] PUSH target=0x0 len=400 speed=250 dmg=0 guided=0 ttl=5 "
|
|
"mz=(1,12,1) relY=12 lv=(fallback)" for _ in range(4)
|
|
] + [
|
|
"[projectile] DET at(9,2,3) dmg=0 lead=1 tgt=0x0 aim(9,2,3)" for _ in range(4)
|
|
] + [
|
|
"[dmghit] mech=42 zone=%d vital=0 type=1 amt=12.5 burst=1 lvl 0->0.1" % (i % 4)
|
|
for i in range(6)
|
|
] + [
|
|
# missiles: guided pushes (1 lead + visuals), IMPACT bursts, type=2 hits
|
|
"[ammo] LRM15 FIRED, rounds left=110" for _ in range(4)
|
|
] + [
|
|
"[projectile] PUSH target=0x5a len=300 speed=180 dmg=52.5 guided=1 ttl=13 "
|
|
"mz=(1,14,1) relY=14 lv=(auth)" for _ in range(6)
|
|
] + [
|
|
"[projectile] DET at(4,5,6) dmg=52.5 lead=1 tgt=0x5a aim(4,5,6)" for _ in range(6)
|
|
] + [
|
|
"[projectile] IMPACT damage=52.5 subsys=21 v=540 burnLeft=0 burst=9 "
|
|
"(direct dispatch) (zone cyl-resolved)" for _ in range(6)
|
|
] + [
|
|
"[dmghit] mech=42 zone=%d vital=0 type=2 amt=3.5 burst=%d lvl 0->0.3"
|
|
% (i % 6, 4 + (i % 8)) for i in range(24)
|
|
] + [
|
|
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
|
|
"[respawn] Mech::Reset 3:42 healed+moved to (0,0,0) alive=1 zones=22 subsys=34",
|
|
"[cascade] zone 17 DESTROYED -> descend=1 destroySibs=0 crits=3",
|
|
"[death] VehicleDead(-1) dispatched to the owning player",
|
|
# the peer's mech id (a second life witness so 43 is a known player mech)
|
|
"[respawn] Mech::Reset 2:43 healed+moved to (9,0,9) alive=1 zones=22 subsys=32",
|
|
]
|
|
|
|
# a TRUE-positive bystander receipt: struck the OTHER mech (43 != own 42),
|
|
# far from the round's own aim, one paired delivery -- must NOT fail A4.
|
|
BYS_TRUE = [
|
|
"[projectile] BYSTANDER id=43 t=0.7 at(100,10,100) dmg=5 lockedTgt=0BADF00D",
|
|
"[projectile] DET at(100,10,100) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
|
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
|
"(direct dispatch) (zone cyl-resolved)",
|
|
]
|
|
|
|
|
|
def selftest():
|
|
ok = True
|
|
|
|
def expect(name, lines_a, want_frag):
|
|
nonlocal ok
|
|
logs = {"fx_a.log": parse(lines_a), "fx_b.log": parse(HEALTHY)}
|
|
fails = adjudicate(logs, verbose=False)
|
|
hit = any(want_frag in f for f in fails)
|
|
print(" selftest %-28s %s" % (name, "DETECTED" if hit else "** MISSED **"))
|
|
if not hit:
|
|
ok = False
|
|
for f in fails:
|
|
print(" got:", f)
|
|
|
|
print("=== DETECTOR SELF-TEST (negative controls; each MUST fail) ===")
|
|
# 1. pre-#171: a damage-carrying ballistic round + multi-panel ballistic hit
|
|
fx = list(HEALTHY) + [
|
|
"[projectile] PUSH target=0x5a len=400 speed=250 dmg=12 guided=0 ttl=5 "
|
|
"mz=(1,12,1) relY=12 lv=(fallback)",
|
|
"[projectile] DET at(9,2,3) dmg=12 lead=1 tgt=0x5a aim(9,2,3)",
|
|
"[dmghit] mech=42 zone=3 vital=0 type=1 amt=12 burst=3 lvl 0->0.2",
|
|
]
|
|
expect("pre-#171 ballistic round", fx, "damage-carrying ballistic")
|
|
expect("pre-#171 multi-panel t1", fx, "burst!=1")
|
|
# 2. pre-#174: same zone cascades twice inside one life
|
|
fx = list(HEALTHY) + [
|
|
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
|
|
"[cascade] zone 18 DESTROYED -> descend=1 destroySibs=0 crits=4",
|
|
]
|
|
expect("pre-#174 double cascade", fx, "#174 ALIVE")
|
|
# 3. bystander FALSE positives (each must be flagged) ...
|
|
fx = list(HEALTHY) + [
|
|
"[projectile] BYSTANDER id=42 t=0.5 at(50,10,50) dmg=5 lockedTgt=0BADF00D",
|
|
"[projectile] DET at(50,10,50) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
|
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
|
"(direct dispatch) (zone cyl-resolved)",
|
|
]
|
|
expect("bystander SELF-strike", fx, "SELF-STRIKE")
|
|
fx = list(HEALTHY) + [
|
|
"[projectile] BYSTANDER id=7 t=0.31 at(4,5,6) dmg=8 lockedTgt=0x5a",
|
|
]
|
|
expect("bystander phantom strike", fx, "phantom strike")
|
|
fx = list(HEALTHY) + [
|
|
"[projectile] BYSTANDER id=43 t=1 at(499,7,899) dmg=5 lockedTgt=0BADF00D",
|
|
"[projectile] DET at(499,7,899) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
|
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
|
"(direct dispatch) (zone cyl-resolved)",
|
|
]
|
|
expect("bystander fuze-race at lock", fx, "raced the 4.0u fuze")
|
|
fx = list(HEALTHY) + BYS_TRUE + [
|
|
"[projectile] BYSTANDER id=43 t=0.9 at(200,10,200) dmg=5 "
|
|
"lockedTgt=0BADF00D (receipt cap reached)",
|
|
"[projectile] DET at(200,10,200) dmg=5 lead=1 tgt=0BADF00D aim(500,7,900)",
|
|
"[projectile] IMPACT damage=5 subsys=29 v=300 burnLeft=2 burst=3 "
|
|
"(direct dispatch) (zone cyl-resolved)",
|
|
]
|
|
expect("bystander print-cap reached", fx, "cap (24) reached")
|
|
# ... and the TRUE positive must NOT be flagged (no false alarm)
|
|
logs = {"fx_a.log": parse(HEALTHY + BYS_TRUE), "fx_b.log": parse(HEALTHY)}
|
|
fails = adjudicate(logs, verbose=False)
|
|
bysf = [f for f in fails if "BYSTANDER" in f or "SELF" in f]
|
|
print(" selftest %-28s %s" % ("bystander TRUE pos accepted",
|
|
"CLEAN" if not bysf else "** FALSE ALARM **"))
|
|
if bysf:
|
|
ok = False
|
|
for f in bysf:
|
|
print(" got:", f)
|
|
# 4. quiet rig: both logs empty must FAIL floors
|
|
logs = {"fx_a.log": parse([]), "fx_b.log": parse([])}
|
|
fails = adjudicate(logs, verbose=False)
|
|
quiet = any("quiet rig" in f for f in fails) and any("floor" in f for f in fails)
|
|
print(" selftest %-28s %s" % ("quiet rig floors",
|
|
"DETECTED" if quiet else "** MISSED **"))
|
|
ok = ok and quiet
|
|
# 5. the healthy fixture itself must PASS (no false alarms in the detector)
|
|
logs = {"fx_a.log": parse(HEALTHY), "fx_b.log": parse(HEALTHY)}
|
|
fails = adjudicate(logs, verbose=False)
|
|
print(" selftest %-28s %s" % ("healthy fixture passes",
|
|
"CLEAN" if not fails else "** FALSE ALARM **"))
|
|
if fails:
|
|
ok = False
|
|
for f in fails:
|
|
print(" got:", f)
|
|
print("DETECTOR:", "OK -- all negative controls detected" if ok else "BROKEN")
|
|
return ok
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) >= 2 and sys.argv[1] == "--selftest":
|
|
sys.exit(0 if selftest() else 1)
|
|
if len(sys.argv) < 3:
|
|
print("usage: weapons_check.py <log_a> <log_b> | --selftest")
|
|
sys.exit(2)
|
|
logs = {fn: load(fn) for fn in sys.argv[1:]}
|
|
fails = adjudicate(logs, verbose=True)
|
|
sys.exit(0 if not fails else 1)
|