17c99f542f
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.
201 lines
10 KiB
Markdown
201 lines
10 KiB
Markdown
# 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).
|