docs+tools: the per-entity OB flag is refuted, and the counter decrements when the player kills

With the pilot finally shooting, the experiment completed both halves. Sample A
at counter 12 over 120 entities gave 2 offsets where exactly 12 entities share a
value; the counter then went 12 -> 11 and NEITHER survived. So within +-0x400 of
an entity's position triple there is no 4-byte word whose shared-value population
tracks REMAINING OB.

The limits are recorded as part of the result, because they bound it: the test
asks which entities share an EXACT 32-bit value, so a single bit ORed into a word
that also carries health or a timer would never show up - a bit-level version of
the same differential is the follow-on. Anything outside the window, or on
entities that entities2 cannot see (it types by position CHANGING, so stationary
objectives are invisible), is untested too, and the populations differed a lot
between samples - 120 against 194.

Separately: REMAINING OB went 12 -> 11, the first decrement of this whole
investigation, while pilot.py logged 411 fire=1 samples and the HUD reached YOU
KILLED WARPLANES 0003. Stated carefully - it does NOT show the counter counts
kills, since an earlier run had the hostile population fall by a third with no
movement; it shows some kills close something the counter tracks.

Two robustness fixes: ob_flag retries an empty entity sample (one void run was
caused by exactly that), and Pad releases everything on interpreter exit - a
file-backed pad PERSISTS after its writer dies, so a tool killed mid-press would
leave a button held and the game would walk through menus on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Sylpheed RE agent
2026-08-23 23:21:08 +00:00
parent e65b730af0
commit 26421d5266
5 changed files with 94 additions and 1 deletions

View File

@@ -0,0 +1,11 @@
{
"nA": 12,
"nB": 11,
"hud_a": 12,
"hud_b": null,
"entities_a": 120,
"entities_b": 194,
"class_matches": [],
"candidates_a": 2,
"survivors": []
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -145,3 +145,66 @@ drawn from runs that assumed the pilot was shooting.
This run did **not** freeze in ~25 minutes. Of the seven Stage 02 runs that have
reached flight, **three froze** — so it is common but not the majority, and a run
that survives the first minute or two seems to keep going.
---
## 🔴 2026-08-23 (final) — the per-entity flag hypothesis is REFUTED, on a transition it was not selected by
With the pilot actually shooting (`pilot-never-fires.md`), the experiment this
file was written for finally completed both halves:
```
HUD=12 RAM=12
[A] counter=12 entities=120 96 turrets, 16 attackers, 7 wingmen, 1 player
[A] classes whose head-count equals the counter: NONE
[A] offsets where exactly 12 entities agree: 2
pos+0x0238 = 00000006 {12 × e007_ADAN_Turret}
pos+0x0250 = 239d6732 {12 × e010_ADAN_Attacker_S}
counter 12 -> 11
[B] counter=11 entities=194
[B] of 2 candidates, 0 still hold exactly 11
```
([`captures/ob-flag-refuted-stage02.json`](captures/ob-flag-refuted-stage02.json))
**Both candidates die on the verification.** So: within ±0x400 of an entity's
position triple there is **no 4-byte word whose shared-value population tracks
`REMAINING OB`**. The simplest reading of "objectives are marked in the entity
object" is dead.
### What that does and does not rule out — the limits are the result too
* 🔴 **Ruled out:** a per-entity word, in that window, holding a common value on
exactly the objective entities.
***Not ruled out — a flag packed into a word that also varies.** The test asks
which entities share an *exact* 32-bit value. A single bit ORed into a word
that also carries health, a timer or a state machine would never produce a
shared value, and this method cannot see it. A bit-level version of the same
differential is the obvious follow-on.
***Not ruled out — anything outside the window**, or on entities the
enumeration cannot see: `entities2.typed` types entities by their position
*changing*, so a stationary objective is invisible to it.
* ⚠️ **The populations differ a lot between samples** — 120 entities at A, 194 at
B — so a flag on entities that spawned between the two would not have been
tested either.
## ✅ And the counter DECREMENTS when the player kills
`REMAINING OB` went **12 → 11** during this run — the first decrement seen in
this whole investigation — while `pilot.py` logged **411** `fire=1` samples and
the HUD's own tally reached **`YOU KILLED WARPLANES 0003`**
([`captures/stage02-you-killed-warplanes-0003.png`](captures/stage02-you-killed-warplanes-0003.png)).
That is worth stating carefully. It does **not** show that the counter counts
kills — the earlier fifteen-minute run had the hostile population fall by a third
with no movement at all. What it shows is that *some* kills close *something*
the counter tracks, which is exactly the behaviour an objective counter should
have, and it is the first time the player's own guns have been part of the
experiment rather than a bystander.
## One more robustness fix, from a void run
`ob_flag.py` now **retries an empty sample**. `entities2.moving()` types entities
by position changing, so a sample that lands on a load or loses the race with
another scanner returns nothing — and a zero-entity sample silently yields zero
candidates and a run that proves nothing. Seen exactly that way once, with the
following sample finding 81 entities moments later.

View File

@@ -11,6 +11,7 @@ then per frame a timestamp, the input vector, and every instance's raw window.
Usage: flight_probe.py <out.bin> [seconds]
"""
import atexit
import os
import struct
import sys
@@ -59,6 +60,11 @@ class Pad:
"LT": 0.0, "RT": 0.0}
self.buttons = set()
self._write()
# A file-backed pad has a failure mode the FIFO did not: the state
# PERSISTS after the writer dies. A tool killed mid-press leaves the
# button held forever, and the game then walks through menus on its own
# with nobody touching it. Release everything on exit.
atexit.register(self.reset)
def _write(self):
parts = []

View File

@@ -110,7 +110,20 @@ def main():
"ob_hunt.py before trusting anything below", flush=True)
return 2
# RETRY an empty sample. entities2.moving() types entities by their position
# CHANGING between two reads, so a sample that lands on a load, a stall, or
# simply loses the race with another scanner comes back with nothing — and a
# zero-entity sample silently produces zero candidates and a void run. Seen
# once, with sample B finding 81 entities moments later.
for _ in range(6):
nA, entsA, gA = sample(w, defs, out, "A")
if len(entsA) >= 10:
break
print("[A] too few entities — retrying", flush=True)
time.sleep(5)
if len(entsA) < 10:
print("[A] never got a usable entity sample", flush=True)
return 4
hist = Counter(nm for _, nm in entsA)
print(f"[A] class histogram vs counter {nA}:", flush=True)
for nm, k in hist.most_common(12):