j153: the open-loop ruler for "hold when trapped" + the A/B proposal (NOT run)

Measures, on the recorded fixtures and with NO counterfactual replay: how long
until a safe tile appears at a forced pick, whether the enemy had just fired,
whether our own tile is already hot, and all three split by distance. Registers
nothing and changes no default: the knob TR_TFIL_HOLD_WHEN_TRAPPED and its
implementation live in the concurrently edited working tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:16:53 +02:00
parent a01141c959
commit 94ffc63160
2 changed files with 334 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# j153 proposal (NOT RUN) — `TR_TFIL_HOLD_WHEN_TRAPPED`: hold when no safe tile exists
**Status: awaiting the owner's approval. No battle, no A/B arm, no tournament has
been started.** The knob exists, is registered, defaults to today's behaviour
byte-for-byte, and is guarded offline.
## The owner's claim
> "if no tile is found to go, to not choose the less dangerous, but to stay
> still! the next tick probably the situation already changed and we did not
> commit to any dangerous place."
Today, when the safe set (`pathMaxHeat <= PathDangerThreshold`, after the
`CoolestLevels = 2` filter) is too small to draw from, the picker **promotes the
2 least-hot blocked tiles** and moves to one of them. The proposal replaces that
with `speed 0.0` for that tick only.
## The knob
| env | default | meaning |
|---|---|---|
| `TR_TFIL_HOLD_WHEN_TRAPPED` | `0` (off) | `1` = hold when the safe set is **empty**; today = promote the 2 least-hot blocked tiles |
- **One tick, never latched.** The hold is taken at the pick site, and the pick
site only runs when `commitTicks == 0`, so the next tick re-evaluates the field
from scratch. There is deliberately **no max-hold knob** in j153: a counter can
only add a way to get stuck. (j154, in flight, adds
`TR_TFIL_HOLD_MAX_TICKS` = 0 = off as a *separate* default-off knob.)
- **It never interrupts a live commitment.** The hold replaces a *replan*, not a
commitment in progress: with `TR_TFIL_COMMIT_ARRIVAL=1` or
`TR_TFIL_NOREV_SPEED=4` armed the two do not fight, because the hold branch is
downstream of the commitment block and only runs at `commitTicks == 0`.
- **The gun keeps firing.** `computeMove` never emits a fire command (the gun
lives in the bot's `go()` loop), and the hold `return`s *after* the bullet
tracking, so the fire tracker's state on a held tick is bit-identical to a
non-held tick (guarded in `common_libs/tests/test_tfil_commit_env.nim`).
## What the offline harness can and cannot say
Per `docs/offline_harness_trust.md` the replay harness is trustworthy only for
per-gun single-tick prediction on a fixed enemy trajectory; it scored **0/6** on
closed-loop questions. "Hold vs move" is a **counterfactual closed-loop** question,
so this document contains **no** damage-taken comparison for holding. Only
open-loop descriptors of the recorded field are reported
(`common_libs/tests/measure_tfil_hold_window.nim`).
## Proposed A/B (needs approval)
```
TOURNAMENT_NIMCACHE=/tmp/nc_j153 \
tools/ab/tournament_run.sh \
--arms tools/ab/arms_hold_trapped.txt \
--panel tools/ab/panel_movement.txt \
--runs 14 --rounds 7 --conc 7 --wait-arena 45 \
--reference hold0 \
--outdir /tmp/ab/j153_hold
python3 tools/ab/tournament_analyze.py /tmp/ab/j153_hold --reference hold0
```
Arms file (frozen 15-opponent movement panel, 14 runs/arm, 7 rounds):
```
hold0 | | control = today's promote-the-2 fallback
hold1 | TR_TFIL_HOLD_WHEN_TRAPPED=1 | hold one tick when the safe set is empty
hold8 | TR_TFIL_HOLD_WHEN_TRAPPED=1 TR_TFIL_HOLD_MAX_TICKS=8 | bounded hold (only if j154 lands)
```
**Primary metrics: damage/run and round-win rate** (NOT hit rate — a movement
arm's value flows through the closed loop). **Mechanism metrics:** incoming hit
rate, % of picks held, mean distance-to-enemy at a hold, tick-share at speed 0.
**MDE, stated up front:** ~0.28 wins/run at 14 runs/arm; a two-arm session is
~1 h. Resolving ~0.10 wins/run needs ~2.2 h / ~2,900 battles. A 1 h two-arm run
can only reject effects at or above ~0.28 wins/run — anything smaller is a null
by construction, and must be reported as such.
**Prior, stated plainly:** four mechanism-positive / outcome-null results in a
row on this campaign. **A null is the most likely outcome.** A null with a
mechanism hit (holds fire, distance at hold is large) would mean: holding is
achievable and does not by itself buy rounds; ship nothing. A null *without* a
mechanism hit means the arm never bound and the A/B is void, not negative.
## Decision each duration supports
| duration | supports |
|---|---|
| 1 h (2 arms × 14 runs) | reject/accept only ≥0.28 wins/run. Mechanism check only. |
| 2.2 h (~2,900 battles, 3 arms) | resolve ~0.10 wins/run. Still not a small-effect test. |
| any null | no change to the shipped default. `TR_TFIL_HOLD_WHEN_TRAPPED` stays 0. |