j144 TFIL: arrival-based commitment + hysteresis + no mid-flight reversal
The owner's live-GUI report was correct on all four counts, and all four are
one bug: the commitment is cancelled by our own tile-boundary crossing
(96.1% of picks, 3793/3946, mean hold 5.06 ticks) while the bot is still
accelerating, and the picker is an unconstrained uniform draw over every
safe tile, so the new target can land in the mirror direction at |speed| < 4.
New knobs, all env-gated and default = today's behaviour (byte-for-byte
default parity guard re-run and green, 51 checks):
TR_TFIL_COMMIT_ARRIVAL hold the committed tile until we are ON it; the
tick knob becomes a MINIMUM dwell. 0 = shipped.
TR_TFIL_COMMIT_MARGIN leave only if the best alternative is at least
this much cooler on the same pathMaxHeat scale.
0 = shipped.
TR_TFIL_NOREV_SPEED while |speed| is below this, a mid-flight switch
may not take a tile >90 deg off the travel
direction. 0 = shipped. norevPool() never returns
an empty pool: with every candidate behind us it
takes the least-bad turn.
Offline gate (recorded DrussGT fixture, 20026 ticks): mean hold 4.1 -> 24.0
ticks, abandoned-before-arrival 92.8% -> 40.5%, committed tile actually
reached 3.3% -> 17.2%, opposite-direction slow mid-flight switches 394 -> 64
(-84%). 'TR_TFIL_TILE_REPLAN=off' alone - what cc11ede's arm B already tried -
only gets the hold to 13.6, which is why that A/B could not find this.
strafe is untouched: it imports only heatDecay/bulletMagScale/Pillar*, none
of which this touches. TR_MOVEMENT default stays strafe. Registered in
env_report.nim + knownEnvNames() + .env.example. Arms pre-registered in
docs/movement_campaign.md and tools/ab/arms_tfil_commit.txt.
This commit is contained in:
@@ -2653,3 +2653,172 @@ checks. **INFERRED:** that the correction size (`3*power`, `damage`) is
|
||||
unchanged — it is read straight from the server events, not re-derived.
|
||||
**NOT MEASURED:** the live movement/damage effect of the fix (the change is
|
||||
~1.11% of fires, far below any panel's MDE; no panel was run, per scope).
|
||||
|
||||
---
|
||||
|
||||
## TFIL commitment: arrival-based + reversal hysteresis (j144)
|
||||
|
||||
> **Pre-registration — written and committed BEFORE any battle.** The arms file
|
||||
> `tools/ab/arms_tfil_commit.txt` and this section's protocol are the frozen
|
||||
> binary's provenance; the live numbers are appended below afterwards.
|
||||
|
||||
### 1. The owner's report, and the mechanism confirmed in the code
|
||||
|
||||
Owner, live GUI with `TR_MOVEMENT=tfil` (verbatim):
|
||||
|
||||
> *"TFIL move: i see that when the tile to go is selected in just a few ticks,
|
||||
> the bot is still accelerating and the target changes even if the path is still
|
||||
> good, and choose a tile that is opposite way, in the meantime bullet arrived
|
||||
> and hit the bot."*
|
||||
|
||||
Read against `common_libs/movements/the_floor_is_lava.nim`, all four of his
|
||||
observations are correct and they are all the same bug:
|
||||
|
||||
**(a) what ends a commitment early.** Three exits exist. The dominant one is the
|
||||
self-tile crossing, at the top of `computeMove`:
|
||||
|
||||
```nim
|
||||
of ttrSelf:
|
||||
...
|
||||
if curTileCol != m.lastTileCol or curTileRow != m.lastTileRow:
|
||||
m.commitTicks = 0
|
||||
```
|
||||
|
||||
With `GridSize = 36` and speed up to 8 px/tick the bot crosses a boundary every
|
||||
~5 ticks, so the 15-tick commitment is cancelled by the very motion it commands.
|
||||
|
||||
**(b) a mere boundary crossing DOES re-plan, and it dominates.** The offline
|
||||
replay on the recorded DrussGT fixture (20026 ticks) attributes **3793 of 3946
|
||||
picks (96.1%)** to `rrTileSelf` — the 96.9% an earlier job measured is still
|
||||
true of the current code, within RNG noise. The mean decision interval is
|
||||
**5.06 ticks**.
|
||||
|
||||
**(c) the new target CAN be the mirror direction while the speed is still low.**
|
||||
Nothing in the picker constrains the direction of a new target relative to the
|
||||
current travel direction — `chosen = rand(candidates.high)` is a uniform draw
|
||||
over every safe tile. And the tile-crossing cancel fires while the bot is still
|
||||
accelerating toward a target it has not reached, so the new pick lands exactly
|
||||
in that window. Measured on the fixture: **1287 mid-flight switches to a tile
|
||||
more than 90 deg off the travel direction, 394 of them at |speed| < 4 px/tick**
|
||||
(half of `MaxSpeed`).
|
||||
|
||||
**(d) nothing compares the committed tile against the best alternative.** The
|
||||
commitment block only asks one question — "has the committed tile's lava risen
|
||||
by more than `DangerReplanThreshold` (25)?" — and otherwise just decrements a
|
||||
counter. There is no notion of "a better tile exists" at all.
|
||||
|
||||
### 2. What the EARLIER A/B (`cc11ede`) covered — and what it did not
|
||||
|
||||
`cc11ede` ran the five-arm commitment A/B at 10-14 runs/arm on real DrussGT
|
||||
(490 rounds) and found **no arm beat the shipped mover on damage/run or round
|
||||
wins** (best p = 0.16, and the D arm's promising +20.99 in block 1 decayed to
|
||||
+3.36 in the replication block). That result stands and this section does not
|
||||
reinterpret it.
|
||||
|
||||
But those arms tested something **adjacent**, not this fix:
|
||||
|
||||
| `cc11ede` arm | what it changed | what it did NOT do |
|
||||
|---|---|---|
|
||||
| B `TR_TFIL_TILE_REPLAN=off` | removes the boundary cancel | keeps a **fixed 15-tick dwell** — the target is still abandoned long before the bot arrives |
|
||||
| C `B + TR_TFIL_NO_REV=1` | soft 3:1 down-weight of rearward tiles | a **weight, never a filter**: a rearward tile can still win, and it does not know whether the target was reached |
|
||||
| D `TR_TFIL_TILE_REPLAN=enemy` | re-keys the cancel to the enemy's tile | still a boundary cancel, just a different tile |
|
||||
| E `TR_TFIL_COMMIT_TICKS=30` | doubles the dwell | still fixed-length, never arrival-based |
|
||||
|
||||
None of them made the commitment **arrival-based**, none compared the committed
|
||||
tile against the best alternative (**hysteresis**), and none conditioned the
|
||||
no-reversal rule on the bot's **speed** (arm C's preference is speed-blind and
|
||||
soft). This section's fix is exactly the part their arms left untested — and,
|
||||
per the numbers below, the part that actually removes the pathology. The honest
|
||||
reading of `cc11ede` is: *the levers it pulled do not work*, not *the mechanism
|
||||
does not exist*.
|
||||
|
||||
### 3. The protocol (pre-registered)
|
||||
|
||||
* Harness: `tools/ab/tournament_run.sh` + `tournament_analyze.py`, FROZEN panel
|
||||
`tools/ab/panel_movement.txt` (15 opponents, unchanged), `TR_MOVEMENT=tfil`
|
||||
pinned explicitly on every arm.
|
||||
* Arms: `tfil` (reference) vs `arrive` vs `arrive_hyst` vs `arrive_hyst_norev`.
|
||||
* **Verdict metrics (standing campaign convention, unchanged):** damage/run and
|
||||
ROUND WINS. An arm is better only if one improves with the cross-opponent test
|
||||
at p < 0.05 while the other does not degrade. The incoming hit rate is the
|
||||
**mechanism being claimed**, never the verdict.
|
||||
* One frozen binary from `git archive HEAD`; every arm differs only by its env
|
||||
dict. No per-arm rebuild.
|
||||
* Power: see the results table's MDE. The unit of evidence is the NUMBER OF
|
||||
OPPONENTS (15), and runs/arm only shrink each opponent's error bar.
|
||||
|
||||
### 4. The offline gate (cheap, and it is a VETO not a win claim)
|
||||
|
||||
`common_libs/tests/measure_tfil_arrival.nim` replays the recorded fixture through
|
||||
the REAL `computeMove` and measures the owner's failure mode directly.
|
||||
|
||||
| arm | picks | mean hold (ticks) | held<needed | reached | rev/100 ticks | rev while slow | opposite switch | opposite mid-flight | mean abs(angle) | flips (>135 deg) | opp mid-flight while SLOW |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| tfil (shipped) | 3946 | 4.08 | 92.8% | 3.3% | 6.6 | 416 (10.5%) | 1323 | 1287 | 140.0 deg | 733 | 394 |
|
||||
| commit-only | 1372 | 13.60 | 64.7% | 5.0% | 2.7 | 275 (20.0%) | 543 | 521 | 140.8 deg | 308 | 257 |
|
||||
| arrive | 511 | 38.19 | 14.3% | 13.5% | 1.0 | 97 (19.0%) | 195 | 177 | 144.3 deg | 117 | 89 |
|
||||
| arrive+hyst | 810 | 23.72 | 41.9% | 16.3% | 1.6 | 127 (15.7%) | 315 | 266 | 137.6 deg | 144 | 102 |
|
||||
| arrive+hyst+norev | 802 | 23.97 | 40.5% | 17.2% | 1.4 | 98 (12.2%) | 278 | 225 | 138.5 deg | 124 | 64 |
|
||||
| norev-alone | 3945 | 4.08 | 92.5% | 2.8% | 5.8 | 244 (6.2%) | 1164 | 1133 | 141.7 deg | 694 | 230 |
|
||||
|
||||
| arm | mean needed | mean held | mean held - needed | abandoned early |
|
||||
|---|---:|---:|---:|---:|
|
||||
| tfil (shipped) | 19.0 | 4.1 | -15.0 | 92.8% |
|
||||
| commit-only | 20.2 | 13.6 | -6.6 | 64.7% |
|
||||
| arrive | 20.4 | 38.2 | 17.8 | 14.3% |
|
||||
| arrive+hyst | 19.8 | 23.7 | 3.9 | 41.9% |
|
||||
| arrive+hyst+norev | 19.0 | 24.0 | 5.0 | 40.5% |
|
||||
| norev-alone | 18.8 | 4.1 | -14.7 | 92.5% |
|
||||
|
||||
| arm | tile_self | tile_enemy | danger | expiry | arrival | hyst |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| tfil (shipped) | 3793 | 0 | 22 | 116 | 0 | 0 |
|
||||
| commit-only | 0 | 0 | 86 | 1271 | 0 | 0 |
|
||||
| arrive | 0 | 0 | 159 | 284 | 53 | 0 |
|
||||
| arrive+hyst | 0 | 0 | 141 | 148 | 115 | 391 |
|
||||
| arrive+hyst+norev | 0 | 0 | 147 | 155 | 122 | 363 |
|
||||
| norev-alone | 3793 | 0 | 20 | 117 | 0 | 0 |
|
||||
|
||||
Reading the offline table, column by column, against the owner's report:
|
||||
|
||||
* **mean hold** 4.1 → 24.0 ticks (the distance needs ~19), and **"held < needed"**
|
||||
— the share of commitments dropped before the bot could physically arrive —
|
||||
falls from **92.8% to 40.5%**. That is the "the commitment is broken after only
|
||||
a few ticks" complaint, measured.
|
||||
* **reached** — the share of commitments that actually end with the bot standing
|
||||
on the tile it chose — rises **3.3% → 17.2% (5.2x)**.
|
||||
* **opposite mid-flight while SLOW** — the exact failure mode, a switch to a tile
|
||||
more than 90 deg off the travel direction, made at |speed| < 4 px/tick, on a
|
||||
target not yet reached — falls **394 → 64 (−84%)**. With
|
||||
`TR_TFIL_NOREV_SPEED` set on its own it is 394 → 230 (−42%).
|
||||
* The residual 64 is not leakage: it is the **all-rearward case** where every
|
||||
safe tile is behind the bot (boxed in, or the field only offers rearward
|
||||
space). There the reversal is unavoidable and the code takes the *least bad*
|
||||
turn instead of a uniform draw — `norevPool` never returns an empty pool, and
|
||||
its invariant is unit-tested: **with a forward candidate available, a slow
|
||||
mid-flight switch is never rearward.**
|
||||
* `commit-only` (`TR_TFIL_TILE_REPLAN=off`, i.e. what `cc11ede`'s arm B already
|
||||
tried) sits in the middle: it removes the boundary cancel but keeps the fixed
|
||||
dwell, so the hold is still 6.6 ticks short of what the distance needs and
|
||||
64.7% of commitments are still abandoned early. **That is the concrete reason
|
||||
the earlier A/B could not have found this fix.**
|
||||
|
||||
**Veto result: PASS.** The mechanism the owner reported is present in the shipped
|
||||
mover at the rate he describes, and the fix removes most of it. This is a static
|
||||
replay of a recorded game — it says the *decision logic* changed, nothing about
|
||||
whether that is worth points. Only the live A/B below can say that.
|
||||
|
||||
**Guards** (`common_libs/tests/test_tfil_commit_env.nim`, 51 checks, all green):
|
||||
|
||||
* the byte-for-byte default-parity guard still passes with **all three new knobs
|
||||
unset** — the shipped default path is unchanged, golden included;
|
||||
* four new `norevPool` invariant checks (fails on any implementation that filters
|
||||
without the all-rearward escape);
|
||||
* a control check that the pathology is really there (`> 20`, measured 394), so
|
||||
the improvement checks cannot pass vacuously;
|
||||
* `TR_TFIL_COMMIT_ARRIVAL` → zero `rrTileSelf` and non-zero `rrArrival`;
|
||||
`TR_TFIL_COMMIT_MARGIN` → non-zero `rrHyst`, never on a boundary crossing.
|
||||
|
||||
### 5. LIVE A/B — pre-registered arms
|
||||
|
||||
*(results appended below after the battles)*
|
||||
|
||||
Reference in New Issue
Block a user