Files
SirRoboGarage/tools/fixtures/TR_BRIDGE_FIXTURES.md
T
SirStone 17c99f542f feat(fixtures): closed-loop DrussGT captures from real Tank Royale battles
The classic captures were OPEN-LOOP: replayed DrussGT never dodged OUR
bullets. These come from real TR battles through the working Java bridge, so
the recording contains genuine reactions to ModularBot's live fire. The
open-loop caveat is gone (perfect-information remains).

PRIMARY RESULT - the boss beats us badly. DrussGT 1447 - ModularBot 300 over
15 rounds, ModularBot winning only round 5 (DrussGT died at tick 1893). Rounds
are long, not truncated: mean 1335 ticks, ModularBot got off 1134 shots.
  ModularBot  1134 shots /  60 hits =  5.3% real hit rate
  DrussGT     1400 shots / 169 hits = 12.1% real hit rate
So DrussGT's gun is ~2.3x more accurate than our entire rack, on top of far
better movement. That is the number to move.

Also captured: shield-on variant (DrussGT 939-287, 9/10 - ModularBot takes
round 1 to the known shield warm-up), and vs SpinBot 1175-0, Crazy 1080-1,
Corners 1659-0. 20,026 + 12,629 + 10,824 + 11,507 + 2,575 ticks.

Movement statistics match the classic set within ~0.04 on the perpendicular
and radial fractions, so this is the same wave surfer in TR physics:
  TR vs modularbot: perp 0.967, radial 0.001, 52.8% at full speed,
                    reversing 46.3%, median range 464 px.

CLOSED LOOP PROVEN, not asserted. ModularBot's fire is a heat-limited near
metronome (median interval 14 ticks), which gives a usable exogenous clock:
  - event-locked |delta heading| oscillates 0.96 -> 2.69 deg about a 1.47 deg
    mean with the fire period, almost every lag outside the 95% band of a
    400-iteration phase-shuffled null;
  - cross-correlation of |delta heading| against the fire impulse peaks at
    r = +0.111, lag 12 ticks, permutation p = 0.005 (null peak mean +0.016);
  - OWN-FIRE CONTROL is flat, so the oscillation is enemy-driven rather than
    an internal cadence;
  - range response is weak (~3 px over 30 ticks, near noise) and is therefore
    NOT claimed, and per-bullet dodging is not claimed either because the
    bullet detector (shield) is off.

CAVEATS: still perfect-information (observer gives true positions every tick,
unlike the live bot's stale between-scan WorldState) so these remain optimistic
vs live play; and they are open-loop AT REPLAY TIME - 'closed_loop' describes
the capture, not a later replay. TR conversion residual is ~1.5 deg mean
because the TR server moves along the pre-turn heading, vs 0.000 deg for the
classic captures.

Adds analyze_closed_loop.py (PSTH event-locking, phase-shuffle permutation
null, cross-correlation, own-fire control) and per-round result sidecars.
2026-09-21 01:18:13 +02:00

201 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tank-Royale bridge fixtures — DrussGT vs our bots (closed loop at capture time)
Fixtures captured from **real Tank Royale battles** in which the *real,
unmodified* classic DrussGT 3.1.4159 plays through the Java bridge in
`tools/robocode_shim/`, against opponents whose shots it actually had to dodge.
They complement the classic-Robocode set in
[`DRUSSGT_FIXTURES.md`](DRUSSGT_FIXTURES.md).
## Why these exist — the fidelity gap they close
`DRUSSGT_FIXTURES.md` documents two gaps in the classic captures:
1. **Open loop.** Replayed DrussGT never dodges *our* bullets, so our gun
scores optimistically against it.
2. **Perfect information.** The observer reports true positions every tick,
unlike the live bot's stale between-scan `WorldState`.
These TR-bridge fixtures close gap **1, at capture time**: the recorded
trajectory is one in which DrussGT was actively reacting to **our bot's real
bullets** (ModularBot's gun) in Tank Royale physics. The coupling is measured in
[Closed-loop evidence](#closed-loop-evidence) below.
Gap **2 remains.** These are still observer captures: true positions every tick,
no fog of war, no sensor latency. They are therefore **optimistic relative to
live play**. And because a fixture is a *recording*, replaying it against a
*different* gun is still open-loop — the closed loop exists only in the battle
that produced the file, not at replay time.
## Provenance / capture mechanism
* Bot host: `tools/robocode_shim/` (`DrussGTBridge` + `ClassicPeer`). See the
shim README §5 for the physics/API details and §5.9 for the known divergences.
* Observer: `tools/robocode_shim/src/robocode_shim/TrBattleCapture.java`, which
runs the Tank Royale battle-runner API (embedded server) and writes the shared
JSONL format, plus a `.rounds.json` sidecar and a raw `.results.json`.
* Commands (shield **off** unless stated):
```bash
tools/robocode_shim/run_bridge_battle.sh \
/tmp/capture_bots/ModularBot 15 /tmp/cap/tr_drussgt_vs_modularbot.jsonl
# TR_EVENTS_OUT=... adds the bullet-event sidecar used for the loop analysis
```
* DrussGT runs as the **pure wave surfer** (`DrussMoveGT`), i.e. the
`EnergyDome` shield is disabled (`DRUSSGT_SHIELD` unset). `DRUSSGT_SHIELD=1`
was used only for the `_shield` file. The ModularBot binary is a frozen
snapshot (md5 `d39dbdc326132cd371cd12f97bfb0a16`) copied to `/tmp`; no Nim build
was invoked during capture.
## Format
Same field layout as the classic fixtures
(`ex/ey/eh/es/ee`, `sx/sy/sh/ss/se`, continuous global `tick`), but the `meta`
line is distinct:
```json
{"meta":{"adversary":"ModularBot","source":"tr-bridge","closed_loop":true,
"perfect_info":true,"arena":{"w":800,"h":600},"note":"..."}}
```
`source` is `"tr-bridge"` (classic files say `"classic-robocode"`),
`closed_loop` is `true`, and the `note` states the capture-time loop and the
remaining open-loop/perfect-info caveats. `e*` is always DrussGT; `s*` is the
adversary. Round boundaries are in `drussgt_meta/<file>.jsonl.rounds.json`;
per-round running scores are in `drussgt_meta/<file>.jsonl.results.json`.
## Files
| File | rounds | ticks | bytes | adversary | shield |
|---|---:|---:|---:|---|---|
| `tr_drussgt_vs_modularbot.jsonl` | 15 | 20,026 | 2,976,439 | **ModularBot (our bot)** | off |
| `tr_drussgt_vs_modularbot_shield.jsonl` | 10 | 12,629 | 1,872,874 | ModularBot | **on** |
| `tr_drussgt_vs_spinbot.jsonl` | 10 | 10,824 | 1,597,273 | sample SpinBot | off |
| `tr_drussgt_vs_crazy.jsonl` | 10 | 11,507 | 1,701,186 | sample Crazy | off |
| `tr_drussgt_vs_corners.jsonl` | 10 | 2,575 | 379,680 | sample Corners | off |
The primary benchmark is **`tr_drussgt_vs_modularbot.jsonl`** (shield off):
20,026 ticks over 15 rounds, mean 1,335 ticks/round (min 845, max 1,893). The
rounds are *not* short — DrussGT does not trivially rush ModularBot — so
ModularBot generated a real shot stream: **1,134 shots** in that run.
## Movement statistics vs the classic captures
Produced by `tools/robocode_fixture_capture/analyze.py` (values are fractions,
median range in px). Classic rows are copied from `DRUSSGT_FIXTURES.md`.
| fixture | mean speed | full ≥7.5 | rest <0.5 | neg. vel | mean \|Δh\| | \|Δh\|>5° | median dist | perp frac | radial frac | mean\|sin\| |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| **TR** modularbot | 5.922 | 0.528 | 0.037 | 0.463 | 1.629 | 0.109 | 463.9 | **0.967** | **0.001** | 0.971 |
| **TR** modularbot (shield) | 5.826 | 0.533 | 0.070 | 0.466 | 1.574 | 0.097 | 450.0 | 0.959 | 0.001 | 0.967 |
| **TR** spinbot | 6.981 | 0.749 | 0.019 | 0.487 | 1.596 | 0.091 | 443.2 | 0.982 | 0.001 | 0.979 |
| **TR** crazy | 7.040 | 0.767 | 0.019 | 0.413 | 2.035 | 0.094 | 423.1 | 0.883 | 0.005 | 0.946 |
| **TR** corners | 6.954 | 0.770 | 0.037 | 0.489 | 2.158 | 0.111 | 524.5 | 0.883 | 0.026 | 0.941 |
| classic spinbot | 6.59 | 0.701 | 0.067 | 0.464 | 1.81 | 0.094 | 402 | 0.962 | 0.001 | 0.971 |
| classic crazy | 6.16 | 0.668 | 0.137 | 0.439 | 2.01 | 0.083 | 378 | 0.829 | 0.010 | 0.934 |
| classic corners | 6.01 | 0.586 | 0.092 | 0.419 | 2.09 | 0.125 | 504 | 0.920 | 0.004 | 0.964 |
| classic drussgt (mirror) | 5.26 | 0.411 | 0.066 | 0.457 | 1.29 | 0.082 | 526 | 0.951 | 0.001 | 0.972 |
The wave-surfer signature — strongly **perpendicular** to the opponent
(perp frac 0.88–0.98), near-zero **radial** fraction, ~41–49% **reversals**,
near-full speed — is present in the TR captures and matches the classic set
closely (perp/radial within ~0.04 of the matching classic opponent). The TR runs
are slightly faster and shorter-ranged, as before, because the rounds are
shorter and the shield is off. This is surfing, not a straight line or a stall.
## Battle outcomes
**DrussGT vs ModularBot, 15 rounds, shield off:** DrussGT 1447 – 300,
14 rounds won (ModularBot won round 5). Per-round ticks and running scores:
| round | ticks | DrussGT score | ModularBot score | winner |
|---:|---:|---:|---:|---|
| 1 | 1252 | 71 | 8 | DrussGT |
| 2 | 1416 | 152 | 20 | DrussGT |
| 3 | 845 | 286 | 36 | DrussGT |
| 4 | 1446 | 385 | 68 | DrussGT |
| 5 | 1893 | 399 | 156 | **ModularBot** |
| 6 | 1080 | 499 | 156 | DrussGT |
| 7 | 1460 | 606 | 188 | DrussGT |
| 8 | 1213 | 708 | 200 | DrussGT |
| 9 | 1759 | 808 | 228 | DrussGT |
| 10 | 1380 | 910 | 236 | DrussGT |
| 11 | 1127 | 1026 | 252 | DrussGT |
| 12 | 1258 | 1144 | 272 | DrussGT |
| 13 | 1330 | 1240 | 280 | DrussGT |
| 14 | 1474 | 1336 | 292 | DrussGT |
| 15 | 1093 | 1447 | 300 | DrussGT |
Scores are the server's **cumulative** running totals at round end.
DrussGT: 1400 bullets / 169 hits / 60 hits taken. ModularBot: 1,134 bullets /
60 hits / 169 hits taken; 29 bullet-bullet.
**Adversaries (shield off, 10 rounds each):** SpinBot 1175–0, Crazy 1080–1,
Corners 1659–0; all 10 rounds won in each. **ModularBot shield on, 10 rounds:**
DrussGT 939–287, 9 rounds won (ModularBot won round 1).
## Closed-loop evidence
Claim under test: *at capture time DrussGT's movement is coupled to ModularBot's
live fire*, not an independent open-loop trajectory. Measured with
`tools/robocode_shim/analyze_closed_loop.py` on
`tr_drussgt_vs_modularbot.jsonl` + `tools/robocode_shim/evidence/tr_drussgt_vs_modularbot.events.json`:
```bash
python3 tools/robocode_shim/analyze_closed_loop.py \
tools/fixtures/tr_drussgt_vs_modularbot.jsonl \
tools/robocode_shim/evidence/tr_drussgt_vs_modularbot.events.json
```
What the run establishes (numbers from that exact run):
* **ModularBot fires on an exogenous, heat-limited clock.** Fire interval
median 14 ticks (p10 14, p90 20). It is a near-metronome, so it is not
following DrussGT tick-by-tick.
* **DrussGT's turning is strongly phase-locked to ModularBot's fire ticks.**
Event-locking `|Δheading|` to the adversary's fire times gives an oscillating
response (dips ≈0.96–1.35°, peaks ≈2.1–2.7° about a ≈1.47° mean) with the
fire period (~13–14 ticks). With 15 rounds, 1,134 fires, almost every lag is
outside the 95% band of a null that **circularly shifts each round's fire
times** (permutation null). The peaks are the surf re-committing on each new
enemy wave (fire detection); the lulls are the committed run between waves.
* **The lock is enemy-specific, not an internal clock.** The same analysis
locked to DrussGT's **own** fire times is essentially flat (only lag 0 is
marginally outside the null). So the oscillation is driven by the *incoming*
fire, not by DrussGT's own gun cadence.
* **Cross-correlation `|Δheading|` vs fire impulse:** peak r = **+0.111** at
lag **12** ticks, permutation p = **0.005** (null peak mean +0.016, max
+0.028). Reversal rate: peak r = **+0.025** at lag 29, p = 0.035.
* **Range keeping responds weakly.** Mean range rises monotonically from
478.3 px at the fire to 481.7 px 30 ticks later — a ~3 px drift, near the
noise floor, so it is not claimed as strong evidence.
**Honest reading.** The PSTH and cross-correlation are significant against a
phase-shuffled null and are absent in the own-fire control, so DrussGT's
movement *is* temporally coupled to ModularBot's firing at capture time. The
effect size is modest (peak r ≈ 0.11) — expected, because DrussGT surfs almost
all the time regardless, and the lock is modulated by the periodic wave train.
What is **not** shown is that each individual bullet is dodged on approach: the
shield (which detects individual bullets) is off here, so the coupling is the
wave-level reaction to the enemy **energy drop / fire**, which is exactly how
this surfer decides to move. No claim is made beyond that.
## Caveats
1. **PERFECT INFORMATION.** Observer captures report true positions every tick;
the live bot's `WorldState` is stale between radar scans. Optimistic vs live
play.
2. **OPEN LOOP AT REPLAY TIME.** These are recordings. DrussGT reacts to
ModularBot's bullets in the file that was captured, not to whatever gun later
replays the trajectory. The `closed_loop:true` flag describes the capture,
not the replay.
3. **Physics divergences** from classic Robocode remain (move/turn ordering,
distance bookkeeping, no exact trajectory equivalence). See the shim README
§5.9. The classic captures validate to 0.000°; TR converts at ~1.5° mean
(median ~0.05°) because of the engine's pre-turn move ordering.
4. The shield-on file is included for completeness; its round 1 is the known
shield warm-up case (shorter, more stationary) — the shield-off files are the
primary set.
5. No round in these captures aborted or timed out; every round ended in a
death (see winners table).