Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5cb83cc
add replay-gated generalist v3
subodhbgwt Jul 30, 2026
b3b83e0
measurement fixes + the heal-lock experiment
subodhbgwt Aug 1, 2026
f499f67
ship variant B (live v2) + the rush sparring partner
subodhbgwt Aug 1, 2026
c189db2
the delivery thesis: eight measured negatives and the architecture wall
subodhbgwt Aug 1, 2026
5fdfbb5
five more measured negatives: the allocation wall is real
subodhbgwt Aug 1, 2026
88acaeb
trace Pantheon builder-by-builder: they specialise like us, and scale…
subodhbgwt Aug 1, 2026
d6bae08
ammo is not the constraint either - and v2 is validated on the ladder
subodhbgwt Aug 1, 2026
b522200
port v6's recall: the team's only working answer to Pantheon
subodhbgwt Aug 2, 2026
34d026c
the pattern, confirmed across four top teams - and a retraction
subodhbgwt Aug 2, 2026
c6b02bc
THE TRANSITION: first variant all session to beat the live bot
subodhbgwt Aug 2, 2026
02339b6
ship v8 econ-transition (58% at 336, CI 53-63, kills 183-116); trans_…
subodhbgwt Aug 2, 2026
a4f5595
prearmor: seat denial traced from Flotte - neutral vs our lineage, +8…
subodhbgwt Aug 2, 2026
4fcccb1
flanking and prearmour both rejected; two process failures caught
subodhbgwt Aug 2, 2026
d827b7a
add every local bot folder
subodhbgwt Aug 2, 2026
93273e1
push everything: tests, docs, analysis tooling, evidence and artifacts
subodhbgwt Aug 2, 2026
1163bc9
add v17 gun-control successor
subodhbgwt Aug 3, 2026
8655263
record v18 unranked results
subodhbgwt Aug 3, 2026
a202c66
record v18 activation and repeat scrims
subodhbgwt Aug 3, 2026
9f3deb2
normalize line endings after full push
subodhbgwt Aug 3, 2026
4990ee4
Merge remote-tracking branch 'origin/main' into claude/measurement-fixes
subodhbgwt Aug 3, 2026
95e3d1f
scout Pantheon at #1 vs our v22, port both their mechanisms, both rej…
subodhbgwt Aug 3, 2026
ebfa9d4
launcher thesis: mechanism reproduced exactly, every version still loses
subodhbgwt Aug 3, 2026
72c7d29
launcher: 12 implementations, mechanism fully reproduced, still 45%
subodhbgwt Aug 3, 2026
2dfa6af
fix all three latent bugs; all three gate neutral
subodhbgwt Aug 3, 2026
d80e24d
launcher-as-sensor: mechanism VERIFIED, payoff too small; v24 confirm…
subodhbgwt Aug 3, 2026
39a5301
LAUNCHER-HANDOFF.md: everything needed to finish the launcher without…
subodhbgwt Aug 3, 2026
d4cebee
v29 diagnosis (Phase 0) + two experiments; the constraint is TRAVEL, …
subodhbgwt Aug 4, 2026
c8642b6
Phase 1/2 complete on v29: four experiments, none separate from the p…
subodhbgwt Aug 4, 2026
cafaf7d
engine 2.3.4: gunner nerf inverts the unit economy; new noise floor i…
subodhbgwt Aug 4, 2026
f4297b9
WORKFLOW: record the 2.3.4 rebalance and the new 54% noise floor
subodhbgwt Aug 4, 2026
9be1e00
diagnostics on 2.3.6: noise band is 46-54%, and one invariant survive…
subodhbgwt Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 3 additions & 2 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Auto detect text files and perform LF normalization
* text=auto
bots/champion/*.py whitespace=-blank-at-eol
bots/generalist-v2/*.py whitespace=-blank-at-eol
bots/generalist-v3/*.py whitespace=-blank-at-eol
95 changes: 95 additions & 0 deletions LAUNCHER-HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Launcher tech — what works, what doesn't, and what's left (2026-08-03)

Thirteen attempts, all rejected. Best 45% vs live v22. This is everything you
need to finish it without repeating any of it. Branch: `claude/measurement-fixes`.

## What Pantheon actually does (traced from replays, not guessed)

```
t1 launcher built 2 tiles from THEIR core (18 from ours), OFF the core->core axis
t2 builder 3 (3,4) -> (4,10) jumped 7
t3 builder 5 (2,4) -> (4,10) jumped 8 <- same landing tile, a rally point
t4 builder 8 (4,4) -> (1,6) jumped 5
t5 builder 11 (4,15)-> (1,17) jumped 5
then the thrown builder BOLTS: 14,13,12,11,10,9,8,7,6,5 — one tile per turn,
no detour, no economy work — and the first gunner lands t13 at distance 4.
```

Three to four throws per game, all in the first five turns. The throw buys ~6
tiles; the bolt does the rest. Without it that is a t21 gun.

Engine facts, measured directly:
- pickup range 1-2 tiles, throw up to 7, 1-turn cooldown, 20 Ti (cost scales)
- `LAUNCHER_VISION_RADIUS_SQ` 26 vs a builder's 20
- ammo converts 1:1, same turn, once per team per turn

## THREE LANDMINES IN OUR CODEBASE — hit these and the mechanism silently dies

1. **Non-builder entities have no map.** Every entity gets its own `Player` and
only `builderBot()` calls `setupMap()`, so in a launcher/gunner/sentinel
`mapPf.enemyCorePos` and `teamCore` are permanently `None`. A throw routine
written against `mapPf.enemyCorePos` compiles, runs, and never fires — no
error. Cost me a whole day. Fix is in `exp_v23_fix_storemap`: builders publish
the enemy core to **slot 9**, non-builders read it. Slots 0-8 are taken
(0 numSpawned, 1-6 map share, 7 team core, 8 symmetry mask).
2. **`mapPf.moveTo` dead-ends against our own conveyor ring** — our conveyors are
impassable to us. It then waits `2 + (id % 8)` turns (3-10!) before trying a
**random** direction. HANDOFF records this costing aegis-v1 seven turns "2
tiles short". Sidestep fix in `exp_v23_fix_stuck` (neutral, safe).
3. **Our own buildings block our own exit.** Placing the pad on the tile nearest
the enemy — the obvious choice — parks it in the doorway every builder walks
through. Traced: a builder bouncing 22<->21 for eight turns where the pad-less
parent marched straight out. Put the pad **one tile perpendicular** to the
core->enemy axis. 41% of our conveyors also land in that corridor.

## What each attempt proved (don't redo these)

| variant | result | what it proved |
|---|---|---|
| v1 pad gated on resources+role | 52% | pad went up too late to matter |
| v2 + routing, 6 pads by accident | — | **pad COUNT is not the issue**: six beat one by nothing |
| v3 pad t1, 1/game, right placement | — | **0 launches** — landmine 1 |
| v4 target via store slot 9 | — | 8.5 launches from t2, but first gun t24->t26: it was throwing the ECONOMY crew |
| v5 attackers claim rides (slot 10) | 40% | 17.1 launches, first gun t20. Riders kept coming back for more |
| v6 one-ride-per-builder flag | — | 0.8 launches: flag set on CLAIM, and one claim survives per turn, so losers disqualify themselves |
| v7 geometry (throw must clear the radius) | 38% | 8.8 launches, first gun t20 |
| v8 rider given the store fallback | — | **deadlock**: riders waited at the pad t6->t89 |
| v9 wait timeout | — | oscillation 21<->22 all game |
| v11 no detour, claim only if already beside it | 45% | **3.8 launches/game = Pantheon's exact rate** |
| v12 pad off the exit lane | 45% | bolt restored (22,21,20,18,17,...) — landmine 3 |
| combo v7 + forward armour | 40% | armour does not rescue it |
| eye (sensor only, no throwing) | 43% | **symmetry DOES resolve 2-3 turns earlier** (measured: 1,2,3,3,3 vs 4,5,5,5,5) — real, but does not pay |

## What is still unsolved — the actual remaining problem

**Our thrown builders do not bolt.** Pantheon's marches one tile per turn
straight in. Ours lands and re-enters `runAttack`, whose `findGunnerSpot` pulls
it sideways, and if it drifts back near the pad the ride logic grabs it again.
Every version that fixed the ride broke the march and vice versa.

If you want to finish this, that is the piece: **a thrown builder should be in a
committed "bolt" state — straight-line march to the enemy core, no gunner-spot
search, no re-riding — until it is inside the firing band.** Detect the landing
(position jumped >1 tile in a turn) and latch the state.

## Also worth knowing

- v24 beat Pantheon 3-2 and CtrlAltDefeat 3-2 in fresh unrated scrims; v22 went
0-5 vs Pantheon the same day. v24 is NOT a regression — the raw 80%-vs-60%
ladder gap is schedule (v24 played 40 rounds vs Pantheon). Like-for-like it is
-5 points at 1.2 sigma.
- Our first gun vs Pantheon is now **t21 to their t14** with zero launchers,
where this morning it was t31 to t13. v23/v24 already bought most of the tempo
the launcher was supposed to buy — which is why the launcher's remaining upside
may be small.
- **The one asymmetry left: they build 3.2 barriers/game, we build none.**

## Tools added

- `builder_trace.py` — attributes each building to the builder standing next to
it, giving a per-builder timeline. This is what cracked Orizon, Prompt
Engineers and Pantheon. Use it before theorising.
- `heal_audit.py` — per-side core damage vs healing, offline, no API needed.
- oarena (github.com/frkns/oarena) for 2D replay viewing. Its venv needs a `.pth`
pointing at the fcode site-packages, because a venv created from another venv
does not inherit it. `oarena serve` on :7878.
244 changes: 244 additions & 0 deletions MEASUREMENT-FINDINGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# Measurement findings — 2026-08-01, prod engine 2.3.3

Everything here is measured, not inferred. Method notes matter more than the
numbers: several confident claims made during this session were wrong, and the
errors all came from trusting a derived log instead of a direct run.

## Champion

**`bots/generalist-v3` = the live bot (v90) = champion.** Nothing beats it.
All figures are full gates vs generalist-v3, 168 games (21 maps x 4 seeds x
both sides, `--tle 10`):

| challenger | result |
|---|---|
| OogwayNEW | 21% |
| OogwayOld | 14% |
| aegis-v1 | 14% |
| generalist-v2 | 49% (tied — same lineage) |
| exp_bounded_home_counter | 48% |
| exp_healer_suppression | 48% |
| exp_local_responder | 53% |
| **byte-identical control** | **50-52%** |

No swap was made. Nothing was submitted or activated.

## The gate is calibrated — but the band is +/-8, not +/-3

`bots/exp_emergency_countertrade` is byte-identical to `bots/generalist-v3`
(md5 `bf31fa04b650`). Gating it against itself measures pure noise:

- Single draws: n=168 -> 50%; n=336 -> 52%; n=288 (18 maps) -> 51%.
- **CORRECTED 2026-08-01: one draw per n cannot estimate a spread.** Three
repeats of the same 168-game control gave **49%, 56%, 51%** — the middle one
clears the bar and PROMOTES a bot over itself.
- Binomial SD at n=168 is 3.9 points, so the 95% band is +/-7.6 and identical
code clears 55% about **10% of the time**. At n=336 that drops to 3%.
- **A 168-game PROMOTE is a 1-in-10 coin flip. Use seeds=8 (336) to decide.**
gate.py now prints the 95% CI and warns when it spans 50%.

Games are not reproducible at all: 46 of the 48 bots/ call `random.*` unseeded,
and `--seed` seeds the ENGINE, not the bot's Python RNG. Two identical SERIAL
runs flipped 8/20 winners on `sprint` (10/20 with `--tle 0`); seeding the bot
RNG gave 0/16. So this is not the worker pool and not the TLE - never diff two
runs game by game, and never treat a seed as a control variable.

A hypothesis was tested and REJECTED: that seat-locked maps were compressing
real gains toward 50% and burying good bots. Dropping the locked maps moved the
control 52% -> 51% and the best candidate 53% -> 53%. **No meaningful gain — do
not redesign the gate for this.** The three exp_* bots above were correctly
rejected; they are not better.

Rerun the control after any engine bump. It is the only way to know what a
number means.

## Per-map gate output — RESOLVED 2026-08-01

The old `"<bot> first"` label was a wrong WORD, not wrong data. It always named
the bot passed as `bot_a` (verified against `fcode run` and the replay: bot_a is
team 0). Since execution interleaves ABABAB, nobody is "first", so the label now
reads `as A` and the CSV column is `side_a`.

**`gate_results.csv` was never wrong, and past per-map analyses are not invalid
on these grounds.** Whole-history A-seat win rates are strongly structured
(`bridge` 84%, `jackpot` 67% ... `skerry` 22%, `duel` 29%) - a scrambled column
would sit at 50% everywhere.

The 8/16-vs-20/20 "contradiction" was never one: 8/16 is exactly what a
seat-locked map produces when the candidate holds each seat half the time. The
old summary collapsed both seats into one number and hid it. gate.py now prints
the per-seat split and flags the lock:
`strait 4/8 as A 0/4 as B 4/4 SEAT-LOCKED (side B won every game)`.
Lock detection off the gate is now trustworthy; per-map WIN RATES still are not
(n=8, SD 17.7 points).

Per-map lines are noise regardless: identical code produced `sweden` 1/8 and
`longship` 6/8 in one run. Measure seat effects with `scripts/seat_check.sh`
(both orders, explicit), never by parsing gate output.

## Turn order

- Execution order is by **global entity id**, interleaved `ABABAB`
(A-core, B-core, A-builder, B-builder...). It is NOT "all of team A, then
all of team B". Measured with an instrumented probe bot, not inferred.
- With identical code the second player wins **59%** pool-wide.
- Verified seat-locked (direct runs, both orders, 20 games, identical code):
`duel`, `showdown`, `skerry`, `strait` — all **20/20** to the second-listed
bot. `sprint` is balanced (9/20). **The other 16 maps were never measured
directly** — any figure for them came from the unreliable gate log.
- Seat bias is bounded by skill: across a large gap (generalist-v3 vs
OogwayOld, 86-14) skill won 29/32 on locked maps. The seat only decides
between closely matched bots — which is the top-of-ladder regime.
- All 21 maps are **exactly symmetric** (terrain, ore, and core placement
under rot180/mirrorX/mirrorY), so spawn quality is ruled out. Turn order is
the only remaining asymmetry.
- Do not call this "RNG" or try to fix it with more seeds: on `duel` the seed
is inert.

## Reference opponent

**`bots/champion` is stale — it is `oogerebus3`, two generations behind live.**
Screened against it, ten bots scored 91-100% and then lost to the live bot;
OogwayNEW screened 83% and lost 21-79. It inverts rankings. Gate against the
CURRENT LIVE BOT and always state which reference a number came from.

A 12-game screen can only ELIMINATE (0-25% = broken). It can never promote —
identical code reads up to 67% at n=12.

## Repo hygiene

- `main` is a superset of every remote branch; `origin/betterOogway` is behind
it (deletions only). Nothing left to merge.
- Byte-identical duplicates: `champion`==`oogerebus3`,
`exp_emergency_countertrade`==`generalist-v3`,
`exp_generalist_stack`==`generalist-v2`,
`core-sniper-v1`==`exp_core_sniper`, `frozen-erebus-v1`==`oogerebus`.
`*_trace` bots are instrumented twins, not independent results — counting
them separately overstates evidence.

## Ladder diagnosis, 200 live games (2026-08-01, bot v1, 11:25-14:08)

Team Oogway, **rank #1 of 86**, rating 1675, **62% overall**. Rivals:
OopsGotYourElo 54/95, Besvikomat 12/25, Ouroboros 5/10, Pantheon 4/10.
Everyone else 75-100%.

**Seat order does not matter live: 63% as A, 62% as B.** The seat effect that
dominates local mirror gates is absent when skill differs, so the spawn-timing
lead below is aimed at something the ladder cannot see. Deprioritised.

**The loss is a game-length window.** Against the top four rivals:

| turns | win rate |
|---|---|
| <150 | 27/40 (68%) |
| **150-299** | **10/34 (29%)** |
| 300-899 | 12/21 (57%) |
| 900+ cap | 26/45 (58%) |

Controlled for opponent it holds - OopsGotYourElo alone (n=95): 64% / **40%** /
60% / 64%. The 24 losses in the window span 4 opponents, 10 maps, and seats
12/12, and 23 of 24 are `core_destroyed`. It is a general mechanism, not a map.

**Mechanism: our siege damage gets healed to nothing.** From 85 downloaded
replays (`fcode-gate-artifacts/v90-midgame-replays`):

| group | dmg to our core | dmg to enemy core | our gunners | fully healed |
|---|---|---|---|---|
| WIN <150 | 93 | 633 | 13 | 0/17 |
| **LOSS 150-299** | **722** | **268** | **30** | **12/23** |
| LOSS 300+ | 416 | 260 | 46 | 11/18 |
| WIN 150+ | 537 | 1446 | **122** | 8/20 |

In the failure window we have MORE gunners than the enemy (30 vs 23) and still
deal a third of the damage, because half those games are fully healed. Winning
a long game took 122 gunners - 4x - i.e. the only answer the bot currently has
to healing is overwhelming it. Four separate `quarry` losses ended at exactly
t184 with heal fraction 1.0, so the failure is deterministic and repeatable.

**This does NOT reproduce locally.** Same buckets from `gate_results.csv`
(1844 games): vs OogwayOld 98/75/52/41, vs lastpop2 90/100/100/86, vs
generalist-v2 flat 48/55/53/55. Local opponents either die before t150 or are
same-lineage mirrors. **A local gate cannot show a gain on this class of change
— it can only prove nothing broke.** The gain is only observable on the ladder.

The MECHANISM is locally reproducible even though the win rate is not: mirror
games hit the same heal-lock (`sprint` t350, core B took 490 and healed 490 =
100%). Measure it with `heal_audit.py` (new, offline, no API needed).

## The delivery thesis: eight experiments, all rejected, one architecture wall

The kill budget from GameConstants: core 500 HP, gunner 10 dmg / 2 ammo on a
1-turn cooldown, ammo converts 1:1 once per team per turn and is usable the same
turn. A kill is 50 shots = 100 ammo + 10 guns = **200 Ti against a 500 bank**.
Ten guns in the band end a game in 5 turns of fire; Pantheon's median game is 58
turns, so ~55 of them are logistics. That framing is right, and every attempt to
exploit it still failed. What each one cost us is the useful part:

| experiment | vs shipped B | cause |
|---|---|---|
| `exp_core_pressure` (gun placement radius) | 46%, 43% vs rush | own-half guns ARE defensive value |
| `exp_relay` (launcher road, grafted) | 33% | road delayed first gun t35 -> t58 |
| `strike-v1` (delivery-first, no econ) | 27% | 100% placement but only 1.9 guns - starved |
| `strike-mass` (12 builders, no econ) | 13% | same starvation, worse |
| `strike-econ` (lean supply line) | not gated | harvester code never fired: store slot 1 collides with the map-share channel, and beeline paths never pass ore |
| `spar_pantheon` (all siege + damped econ) | 49%, kills 79-79 | allocation gain and economy loss cancel exactly |
| `exp_swarm` (all siege, econ untouched) | - | harvesters still went to 0.0: econ lives in the non-sieger branch |
| `exp_siege3` (three siegers) | - | 1 building, 0 titanium collected, dead t62 |

**The wall: role allocation and economy are the same knob.** Economy work happens
only in the non-sieger branch, so every builder moved to delivery is a builder
removed from income. 2 siegers -> 4.6 guns + 4.0 harvesters; 3 siegers -> 0 and
0; all siegers -> 4.5 and 0. Pantheon gets 18.7 guns on the enemy core from an
economy the same size as ours (4.1 harvesters), which means their builders farm
AND deliver. That is architectural, not a parameter, and no variant of this
engine reached it.

Verified engine facts worth keeping regardless:
- `launch(bot_pos, target)` throws a friendly builder: pickup range 1-2 tiles,
throw up to 7, 1-turn cooldown, 20 Ti base (cost scales per launcher).
Launchers spaced ~6 apart give one hop per turn. WE BUILD ZERO; so does
Pantheon (0 in 25 games); SmartFridge builds ~1 at t17-18 mid-map.
- Ammo is 1:1 with titanium, once per team per turn, usable immediately.
- Whether an ENEMY builder can be launched is UNRESOLVED - the probe returned 0
legal targets for enemies, but also 0 for the friendly control in the same
run, so it proves nothing.


## THE PATTERN, confirmed across four top teams (2026-08-02)

Profiled in games AGAINST US, so it is like-for-like on map and matchup:

| team | 1st gun | guns on OUR core | conveyors | harvesters | barriers | vs us |
|---|---|---|---|---|---|---|
| **Oogway (us)** | t24 | **2.2** | 13.4 | 3.0 | 0.0 | - |
| Orizon (#3) | t18 | 16.0 | 8.0 | 1.9 | 0.2 | 13-2 |
| Prompt Engineers (#4) | t26 | 14.0 | 3.2 | 1.0 | 0.0 | 17-3 |
| The Flotte Experience (#5) | t22 | 6.5 | **0.0** | **0.0** | 5.7 | 22-3 |
| Pantheon (#1) | t22 | 18.7 | 17.7 | 4.1 | 39.8 | 10-5 (v6) |

Every team above us lands 3-8x more guns on our core than we land on theirs, and
every one runs a LEANER economy. Flotte builds no economy at all - zero
conveyors, zero harvesters, barriers instead - and beats us 22-3 while LOSING to
OopsGotYourElo 5-15, a team we beat 70%. That is a specific counter to us, not a
strength gap.

RETRACTION: I earlier dismissed guns-on-core as "churn" because Pantheon's guns
live 8 turns and ours live 30. That compared Pantheon against top opponents with
our bot against my own failed variants - not like for like. Four teams measured
in games against us all show the same gap. The metric is real; the dismissal was
wrong, and the sixteen rejected experiments failed on EXECUTION, not thesis.

Flotte is the existence proof that zero economy works, which is exactly what
strike-v1 attempted (27%, 1.9 guns) and botched.

## Open lead for generalist-v3

Action order follows entity id, and id is assigned at spawn — so **spawn timing
is a controllable lever**. A unit spawned later acts after more enemy units and
sees a more current board. Falsifiable cheaply: one variant that delays its
first spawn a round vs one that does not, gated normally against v3.

Already banked in v3, do not redo: `_direct_siege_plans` (main.py ~638) only
accepts gunner seats where `can_fire_from(...)` succeeds against an enemy core
tile, so it cannot plant out-of-range gunners. That was the atoll/lastpopperian
failure mode and it is fixed.
Loading