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.
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user