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:
@@ -5,6 +5,13 @@ Captured **real, unmodified DrussGT 3.1.4159** movement from classic Robocode
|
|||||||
|
|
||||||
These are **movement-pattern fixtures for gun testing**, not runnable opponents.
|
These are **movement-pattern fixtures for gun testing**, not runnable opponents.
|
||||||
|
|
||||||
|
> **See also:** [`TR_BRIDGE_FIXTURES.md`](TR_BRIDGE_FIXTURES.md) — Tank Royale
|
||||||
|
> bridge captures (`source:"tr-bridge"`) where the recorded DrussGT was
|
||||||
|
> reacting to *our* bot's real bullets at capture time. Same JSONL format, but
|
||||||
|
> `closed_loop:true`. The classic files below remain the higher-fidelity
|
||||||
|
> *trajectory-conversion* reference (validated to 0.000°); the TR files are the
|
||||||
|
> better gun benchmark. Both are perfect-information.
|
||||||
|
|
||||||
## How the data was obtained
|
## How the data was obtained
|
||||||
|
|
||||||
| Artifact | Source | Result |
|
| Artifact | Source | Result |
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{"roundResults":[
|
||||||
|
"[capture] ROUND 1 (turn 223): DrussGT rank=1 score=164 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 2 (turn 218): DrussGT rank=1 score=328 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 3 (turn 353): DrussGT rank=1 score=491 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 4 (turn 247): DrussGT rank=1 score=658 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 5 (turn 200): DrussGT rank=1 score=824 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 6 (turn 256): DrussGT rank=1 score=991 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 7 (turn 250): DrussGT rank=1 score=1160 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 8 (turn 247): DrussGT rank=1 score=1327 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 9 (turn 327): DrussGT rank=1 score=1493 | Corners rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 10 (turn 254): DrussGT rank=1 score=1659 | Corners rank=2 score=0 | "
|
||||||
|
]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rounds":[{"round":1,"startTick":0,"count":223},{"round":2,"startTick":223,"count":218},{"round":3,"startTick":441,"count":353},{"round":4,"startTick":794,"count":247},{"round":5,"startTick":1041,"count":200},{"round":6,"startTick":1241,"count":256},{"round":7,"startTick":1497,"count":250},{"round":8,"startTick":1747,"count":247},{"round":9,"startTick":1994,"count":327},{"round":10,"startTick":2321,"count":254}]}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{"roundResults":[
|
||||||
|
"[capture] ROUND 1 (turn 1453): DrussGT rank=1 score=95 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 2 (turn 666): DrussGT rank=1 score=249 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 3 (turn 1356): DrussGT rank=1 score=355 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 4 (turn 771): DrussGT rank=1 score=470 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 5 (turn 1369): DrussGT rank=1 score=561 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 6 (turn 1182): DrussGT rank=1 score=662 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 7 (turn 813): DrussGT rank=1 score=790 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 8 (turn 1400): DrussGT rank=1 score=899 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 9 (turn 857): DrussGT rank=1 score=987 | Crazy rank=2 score=1 | ",
|
||||||
|
"[capture] ROUND 10 (turn 1640): DrussGT rank=1 score=1080 | Crazy rank=2 score=1 | "
|
||||||
|
]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rounds":[{"round":1,"startTick":0,"count":1453},{"round":2,"startTick":1453,"count":666},{"round":3,"startTick":2119,"count":1356},{"round":4,"startTick":3475,"count":771},{"round":5,"startTick":4246,"count":1369},{"round":6,"startTick":5615,"count":1182},{"round":7,"startTick":6797,"count":813},{"round":8,"startTick":7610,"count":1400},{"round":9,"startTick":9010,"count":857},{"round":10,"startTick":9867,"count":1640}]}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{"roundResults":[
|
||||||
|
"[capture] ROUND 1 (turn 1252): DrussGT rank=1 score=71 | ModularBot rank=2 score=8 | ",
|
||||||
|
"[capture] ROUND 2 (turn 1416): DrussGT rank=1 score=152 | ModularBot rank=2 score=20 | ",
|
||||||
|
"[capture] ROUND 3 (turn 845): DrussGT rank=1 score=286 | ModularBot rank=2 score=36 | ",
|
||||||
|
"[capture] ROUND 4 (turn 1446): DrussGT rank=1 score=385 | ModularBot rank=2 score=68 | ",
|
||||||
|
"[capture] ROUND 5 (turn 1893): DrussGT rank=1 score=399 | ModularBot rank=2 score=156 | ",
|
||||||
|
"[capture] ROUND 6 (turn 1080): DrussGT rank=1 score=499 | ModularBot rank=2 score=156 | ",
|
||||||
|
"[capture] ROUND 7 (turn 1460): DrussGT rank=1 score=606 | ModularBot rank=2 score=188 | ",
|
||||||
|
"[capture] ROUND 8 (turn 1213): DrussGT rank=1 score=708 | ModularBot rank=2 score=200 | ",
|
||||||
|
"[capture] ROUND 9 (turn 1759): DrussGT rank=1 score=808 | ModularBot rank=2 score=228 | ",
|
||||||
|
"[capture] ROUND 10 (turn 1380): DrussGT rank=1 score=910 | ModularBot rank=2 score=236 | ",
|
||||||
|
"[capture] ROUND 11 (turn 1127): DrussGT rank=1 score=1026 | ModularBot rank=2 score=252 | ",
|
||||||
|
"[capture] ROUND 12 (turn 1258): DrussGT rank=1 score=1144 | ModularBot rank=2 score=272 | ",
|
||||||
|
"[capture] ROUND 13 (turn 1330): DrussGT rank=1 score=1240 | ModularBot rank=2 score=280 | ",
|
||||||
|
"[capture] ROUND 14 (turn 1474): DrussGT rank=1 score=1336 | ModularBot rank=2 score=292 | ",
|
||||||
|
"[capture] ROUND 15 (turn 1093): DrussGT rank=1 score=1447 | ModularBot rank=2 score=300 | "
|
||||||
|
]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rounds":[{"round":1,"startTick":0,"count":1252},{"round":2,"startTick":1252,"count":1416},{"round":3,"startTick":2668,"count":845},{"round":4,"startTick":3513,"count":1446},{"round":5,"startTick":4959,"count":1893},{"round":6,"startTick":6852,"count":1080},{"round":7,"startTick":7932,"count":1460},{"round":8,"startTick":9392,"count":1213},{"round":9,"startTick":10605,"count":1759},{"round":10,"startTick":12364,"count":1380},{"round":11,"startTick":13744,"count":1127},{"round":12,"startTick":14871,"count":1258},{"round":13,"startTick":16129,"count":1330},{"round":14,"startTick":17459,"count":1474},{"round":15,"startTick":18933,"count":1093}]}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{"roundResults":[
|
||||||
|
"[capture] ROUND 1 (turn 593): ModularBot rank=1 score=139 | DrussGT rank=2 score=31 | ",
|
||||||
|
"[capture] ROUND 2 (turn 1081): ModularBot rank=1 score=151 | DrussGT rank=2 score=144 | ",
|
||||||
|
"[capture] ROUND 3 (turn 1237): DrussGT rank=1 score=246 | ModularBot rank=2 score=167 | ",
|
||||||
|
"[capture] ROUND 4 (turn 1296): DrussGT rank=1 score=366 | ModularBot rank=2 score=195 | ",
|
||||||
|
"[capture] ROUND 5 (turn 1550): DrussGT rank=1 score=452 | ModularBot rank=2 score=207 | ",
|
||||||
|
"[capture] ROUND 6 (turn 1284): DrussGT rank=1 score=535 | ModularBot rank=2 score=211 | ",
|
||||||
|
"[capture] ROUND 7 (turn 1364): DrussGT rank=1 score=618 | ModularBot rank=2 score=223 | ",
|
||||||
|
"[capture] ROUND 8 (turn 1546): DrussGT rank=1 score=726 | ModularBot rank=2 score=255 | ",
|
||||||
|
"[capture] ROUND 9 (turn 1286): DrussGT rank=1 score=837 | ModularBot rank=2 score=275 | ",
|
||||||
|
"[capture] ROUND 10 (turn 1392): DrussGT rank=1 score=939 | ModularBot rank=2 score=287 | "
|
||||||
|
]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rounds":[{"round":1,"startTick":0,"count":593},{"round":2,"startTick":593,"count":1081},{"round":3,"startTick":1674,"count":1237},{"round":4,"startTick":2911,"count":1296},{"round":5,"startTick":4207,"count":1550},{"round":6,"startTick":5757,"count":1284},{"round":7,"startTick":7041,"count":1364},{"round":8,"startTick":8405,"count":1546},{"round":9,"startTick":9951,"count":1286},{"round":10,"startTick":11237,"count":1392}]}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{"roundResults":[
|
||||||
|
"[capture] ROUND 1 (turn 1334): DrussGT rank=1 score=101 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 2 (turn 1454): DrussGT rank=1 score=184 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 3 (turn 758): DrussGT rank=1 score=284 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 4 (turn 1035): DrussGT rank=1 score=417 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 5 (turn 691): DrussGT rank=1 score=530 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 6 (turn 1013): DrussGT rank=1 score=674 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 7 (turn 1512): DrussGT rank=1 score=779 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 8 (turn 1215): DrussGT rank=1 score=905 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 9 (turn 1119): DrussGT rank=1 score=1031 | Spin Bot rank=2 score=0 | ",
|
||||||
|
"[capture] ROUND 10 (turn 693): DrussGT rank=1 score=1175 | Spin Bot rank=2 score=0 | "
|
||||||
|
]}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"rounds":[{"round":1,"startTick":0,"count":1334},{"round":2,"startTick":1334,"count":1454},{"round":3,"startTick":2788,"count":758},{"round":4,"startTick":3546,"count":1035},{"round":5,"startTick":4581,"count":691},{"round":6,"startTick":5272,"count":1013},{"round":7,"startTick":6285,"count":1512},{"round":8,"startTick":7797,"count":1215},{"round":9,"startTick":9012,"count":1119},{"round":10,"startTick":10131,"count":693}]}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -444,7 +444,13 @@ identity file and a `<DIR>.sh` launcher that runs `robocode_shim.DrussGTBridge`)
|
|||||||
`TrBattleCapture` is the observer: it runs the battle through the Tank Royale
|
`TrBattleCapture` is the observer: it runs the battle through the Tank Royale
|
||||||
battle-runner API and dumps every bot's per-turn state in the *same* JSONL
|
battle-runner API and dumps every bot's per-turn state in the *same* JSONL
|
||||||
fixture format as `tools/robocode_fixture_capture/Capture.java`, so
|
fixture format as `tools/robocode_fixture_capture/Capture.java`, so
|
||||||
`analyze.py` runs on both unchanged.
|
`analyze.py` runs on both unchanged. It writes `"source":"tr-bridge"`,
|
||||||
|
`"closed_loop":true` in the meta, a `.rounds.json` sidecar and a
|
||||||
|
`.results.json` per-round score sidecar. With `--events-out FILE` it also dumps
|
||||||
|
bullet fire / hit / death events, which
|
||||||
|
`tools/robocode_shim/analyze_closed_loop.py` uses to measure how strongly
|
||||||
|
DrussGT's movement is phase-locked to the adversary's live fire (see
|
||||||
|
`tools/fixtures/TR_BRIDGE_FIXTURES.md`).
|
||||||
|
|
||||||
### 5.8 Verification — it is really DrussGT, not a stub
|
### 5.8 Verification — it is really DrussGT, not a stub
|
||||||
|
|
||||||
@@ -543,6 +549,8 @@ tools/robocode_shim/
|
|||||||
DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
|
DrussGTBridge.java the Tank Royale Bot that hosts DrussGT
|
||||||
SmokeTest.java the standalone runtime proof
|
SmokeTest.java the standalone runtime proof
|
||||||
TrBattleCapture.java observer: battle -> classic JSONL fixture format
|
TrBattleCapture.java observer: battle -> classic JSONL fixture format
|
||||||
|
analyze_closed_loop.py PSTH / cross-correlation loop-reactivity analysis
|
||||||
|
evidence/ raw bullet-event sidecars for that analysis
|
||||||
out/ build output (gitignored)
|
out/ build output (gitignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Closed-loop reactivity analysis for the Tank-Royale bridge fixtures.
|
||||||
|
|
||||||
|
Given a fixture JSONL (per-tick DrussGT + adversary state, produced by
|
||||||
|
``TrBattleCapture``) and the matching raw-events JSONL (bullet fire / hit /
|
||||||
|
death events), this measures whether **DrussGT's movement responds to the
|
||||||
|
adversary's live bullets** at capture time -- the property the classic-replay
|
||||||
|
fixtures cannot have.
|
||||||
|
|
||||||
|
Why the raw events: the fixture alone cannot distinguish an enemy *fire* from
|
||||||
|
enemy *damage* (both are energy drops), and it has no bullet positions. The
|
||||||
|
events sidecar gives the exact fire tick, muzzle position, direction and power
|
||||||
|
of every shot, so we can event-lock DrussGT's response and compute predicted
|
||||||
|
bullet-arrival times.
|
||||||
|
|
||||||
|
Outputs
|
||||||
|
-------
|
||||||
|
* fire cadence, hits / misses / bullet-bullet, per-round death victim (who won
|
||||||
|
the round) and DrussGT hits taken;
|
||||||
|
* event-locked PSTH (peri-stimulus time histogram) of DrussGT's response
|
||||||
|
(``|dHeading|``, reversal rate, radial-velocity fraction) as a function of
|
||||||
|
ticks since an adversary fire, with a permutation null band (fire ticks
|
||||||
|
circularly shifted inside each round);
|
||||||
|
* cross-correlation of the fire impulse train with the response, peak lag.
|
||||||
|
|
||||||
|
Note: fixture roles are fixed by ``TrBattleCapture``: e* = DrussGT (subject),
|
||||||
|
s* = adversary. The muzzle-position match is used only to *validate* that owner
|
||||||
|
id mapping independently.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/robocode_shim/analyze_closed_loop.py FIXTURE.jsonl EVENTS.jsonl
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import statistics as st
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def wrap180(a):
|
||||||
|
return ((a + 180.0) % 360.0) - 180.0
|
||||||
|
|
||||||
|
|
||||||
|
def load_fixture(path):
|
||||||
|
rows = []
|
||||||
|
for line in open(path):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
o = json.loads(line)
|
||||||
|
if "tick" in o:
|
||||||
|
rows.append(o)
|
||||||
|
rounds = []
|
||||||
|
for cand in (path + ".rounds.json",
|
||||||
|
os.path.join(os.path.dirname(path), "drussgt_meta",
|
||||||
|
os.path.basename(path) + ".rounds.json")):
|
||||||
|
try:
|
||||||
|
rounds = json.load(open(cand))["rounds"]
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
rounds = []
|
||||||
|
return rows, rounds
|
||||||
|
|
||||||
|
|
||||||
|
def load_events(path):
|
||||||
|
return [json.loads(l) for l in open(path) if l.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def build_rounds(rows, rounds):
|
||||||
|
if not rounds:
|
||||||
|
out, cur = [], [rows[0]]
|
||||||
|
for r in rows[1:]:
|
||||||
|
if math.hypot(r["ex"] - cur[-1]["ex"], r["ey"] - cur[-1]["ey"]) > 20:
|
||||||
|
out.append(cur)
|
||||||
|
cur = []
|
||||||
|
cur.append(r)
|
||||||
|
out.append(cur)
|
||||||
|
return [{"rows": c, "start": c[0]["tick"], "round": i + 1}
|
||||||
|
for i, c in enumerate(out)]
|
||||||
|
by_tick = {r["tick"]: r for r in rows}
|
||||||
|
out = []
|
||||||
|
for rn in rounds:
|
||||||
|
s = rn["startTick"]
|
||||||
|
out.append({"round": rn["round"], "start": s,
|
||||||
|
"rows": [by_tick[t] for t in range(s, s + rn["count"]) if t in by_tick]})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def response_series(rlist):
|
||||||
|
res = []
|
||||||
|
for rd in rlist:
|
||||||
|
R = rd["rows"]
|
||||||
|
n = len(R)
|
||||||
|
hc = [float("nan")] * n
|
||||||
|
rev = [float("nan")] * n
|
||||||
|
rad = [float("nan")] * n
|
||||||
|
for i in range(1, n):
|
||||||
|
hc[i] = abs(wrap180(R[i]["eh"] - R[i - 1]["eh"]))
|
||||||
|
v0, v1 = R[i - 1]["es"], R[i]["es"]
|
||||||
|
rev[i] = 1.0 if (abs(v0) > 0.5 and v0 * v1 < 0) else 0.0
|
||||||
|
theta = math.degrees(math.atan2(R[i]["sy"] - R[i]["ey"],
|
||||||
|
R[i]["sx"] - R[i]["ex"]))
|
||||||
|
mu = R[i]["eh"] if v1 >= 0 else R[i]["eh"] + 180.0
|
||||||
|
rad[i] = abs(math.cos(math.radians(mu - theta)))
|
||||||
|
res.append({"round": rd["round"], "start": rd["start"], "n": n,
|
||||||
|
"hc": hc, "rev": rev, "rad": rad})
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print(__doc__)
|
||||||
|
sys.exit(2)
|
||||||
|
fixture, events_path = sys.argv[1], sys.argv[2]
|
||||||
|
rows, rounds = load_fixture(fixture)
|
||||||
|
events = load_events(events_path)
|
||||||
|
rlist = build_rounds(rows, rounds)
|
||||||
|
arrays = response_series(rlist)
|
||||||
|
n_by_round = {a["round"]: a["n"] for a in arrays}
|
||||||
|
|
||||||
|
# ---- validate owner -> role mapping from muzzle positions ----
|
||||||
|
idx = {r["tick"]: i for i, r in enumerate(rows)}
|
||||||
|
start_of = {rn["round"]: rn["startTick"] for rn in rounds}
|
||||||
|
role_by_owner = {}
|
||||||
|
for e in events:
|
||||||
|
if e["type"] != "fire" or "x" not in e:
|
||||||
|
continue
|
||||||
|
start = start_of.get(e["round"])
|
||||||
|
if start is None:
|
||||||
|
continue
|
||||||
|
gi = start + (e["tick"] - 1)
|
||||||
|
if gi not in idx:
|
||||||
|
continue
|
||||||
|
r = rows[idx[gi]]
|
||||||
|
de = math.hypot(r["ex"] - e["x"], r["ey"] - e["y"])
|
||||||
|
ds = math.hypot(r["sx"] - e["x"], r["sy"] - e["y"])
|
||||||
|
role_by_owner.setdefault(e["owner"], []).append("e" if de <= ds else "s")
|
||||||
|
owner_role = {o: max(set(v), key=v.count) for o, v in role_by_owner.items()}
|
||||||
|
|
||||||
|
subj_owner = next((o for o, ro in owner_role.items() if ro == "e"), None)
|
||||||
|
adv_owner = next((o for o, ro in owner_role.items() if ro == "s"), None)
|
||||||
|
|
||||||
|
print(f"fixture : {fixture}")
|
||||||
|
print(f"events : {events_path}")
|
||||||
|
print(f"ticks : {len(rows)} rounds: {len(rlist)}")
|
||||||
|
for o, v in role_by_owner.items():
|
||||||
|
print(f"owner {o}: muzzle-position role votes e*={v.count('e')} s*={v.count('s')} "
|
||||||
|
f"-> {owner_role[o]}*")
|
||||||
|
print(f"subject (DrussGT, e*) = owner {subj_owner}; adversary (s*) = owner {adv_owner}")
|
||||||
|
|
||||||
|
fires_adv = [e for e in events if e["type"] == "fire" and e["owner"] == adv_owner]
|
||||||
|
fires_subj = [e for e in events if e["type"] == "fire" and e["owner"] == subj_owner]
|
||||||
|
hits_adv = [e for e in events if e["type"] == "hit" and e["owner"] == adv_owner]
|
||||||
|
hits_subj = [e for e in events if e["type"] == "hit" and e["owner"] == subj_owner]
|
||||||
|
bhb = [e for e in events if e["type"] == "hitbullet"]
|
||||||
|
print(f"fires : adversary={len(fires_adv)} subject(DrussGT)={len(fires_subj)}")
|
||||||
|
print(f"hits : adversary->DrussGT={len(hits_adv)} DrussGT->adversary={len(hits_subj)}"
|
||||||
|
f" bullet-bullet={len(bhb)}")
|
||||||
|
if fires_adv:
|
||||||
|
print(f"adversary hit rate = {len(hits_adv)/len(fires_adv):.3f}")
|
||||||
|
|
||||||
|
fire_ticks_by_round = {rd["round"]: [] for rd in rlist}
|
||||||
|
for e in fires_adv:
|
||||||
|
fire_ticks_by_round[e["round"]].append(e["tick"])
|
||||||
|
|
||||||
|
print("\n=== per-round (ticks, adversary fires, DrussGT hits taken, round death) ===")
|
||||||
|
per_round_hits = {}
|
||||||
|
for e in hits_adv:
|
||||||
|
per_round_hits[e["round"]] = per_round_hits.get(e["round"], 0) + 1
|
||||||
|
death_by_round = {}
|
||||||
|
for e in events:
|
||||||
|
if e["type"] == "death":
|
||||||
|
death_by_round.setdefault(e["round"], []).append(e["victim"])
|
||||||
|
for rd in rlist:
|
||||||
|
rn = rd["round"]
|
||||||
|
dth = death_by_round.get(rn)
|
||||||
|
if dth is None:
|
||||||
|
winner = "no death (timeout?)"
|
||||||
|
else:
|
||||||
|
# victimId == adversary owner -> adversary died -> DrussGT won
|
||||||
|
winner = (f"DrussGT (adversary victimId={dth[0]})" if dth[0] == adv_owner
|
||||||
|
else f"adversary (DrussGT victimId={dth[0]})")
|
||||||
|
print(f" round {rn:2d}: ticks={n_by_round[rn]:5d} advFires={len(fire_ticks_by_round[rn]):4d} "
|
||||||
|
f"DrussGT hits taken={per_round_hits.get(rn,0):2d} winner={winner}")
|
||||||
|
|
||||||
|
deltas = []
|
||||||
|
for rn, fts in fire_ticks_by_round.items():
|
||||||
|
fts = sorted(fts)
|
||||||
|
deltas += [b - a for a, b in zip(fts, fts[1:])]
|
||||||
|
if deltas:
|
||||||
|
s = sorted(deltas)
|
||||||
|
print(f"\nadversary fire interval (ticks): median={st.median(deltas):.1f} "
|
||||||
|
f"mean={st.mean(deltas):.1f} p10={s[len(s)//10]} p90={s[len(s)*9//10]} "
|
||||||
|
f"(min={s[0]}, max={s[-1]})")
|
||||||
|
|
||||||
|
# ---- event-locked PSTH around adversary fire tick ----
|
||||||
|
tau_lo, tau_hi = -5, 80
|
||||||
|
fire_by_round = {rd["round"]: [rd["start"] + (t - 1)
|
||||||
|
for t in sorted(fire_ticks_by_round[rd["round"]])]
|
||||||
|
for rd in rlist}
|
||||||
|
|
||||||
|
def psth_for(key, fires_map):
|
||||||
|
acc = {t: [] for t in range(tau_lo, tau_hi + 1)}
|
||||||
|
for arr in arrays:
|
||||||
|
lo, n = arr["start"], arr["n"]
|
||||||
|
for f in fires_map[arr["round"]]:
|
||||||
|
for tau in range(tau_lo, tau_hi + 1):
|
||||||
|
gi = f + tau
|
||||||
|
if lo <= gi < lo + n:
|
||||||
|
v = arr[key][gi - lo]
|
||||||
|
if v == v:
|
||||||
|
acc[tau].append(v)
|
||||||
|
return {t: (st.mean(acc[t]) if acc[t] else float("nan")) for t in acc}
|
||||||
|
|
||||||
|
random.seed(1234)
|
||||||
|
for key, label in [("hc", "|dHeading| deg"), ("rev", "reversal rate"), ("rad", "|cos| radial frac")]:
|
||||||
|
real = psth_for(key, fire_by_round)
|
||||||
|
nulls = {t: [] for t in range(tau_lo, tau_hi + 1)}
|
||||||
|
for _ in range(150):
|
||||||
|
shifted = {}
|
||||||
|
for arr in arrays:
|
||||||
|
lo, n = arr["start"], arr["n"]
|
||||||
|
base = fire_by_round[arr["round"]]
|
||||||
|
k = random.randint(0, max(1, n - 1))
|
||||||
|
shifted[arr["round"]] = [lo + ((x - lo + k) % n) for x in base]
|
||||||
|
acc = psth_for(key, shifted)
|
||||||
|
for t in nulls:
|
||||||
|
if acc[t] == acc[t]:
|
||||||
|
nulls[t].append(acc[t])
|
||||||
|
print(f"\n=== event-locked PSTH: {label} vs ticks-since-adversary-fire ===")
|
||||||
|
print(" tau : real null95-low null95-high flag")
|
||||||
|
for t in range(tau_lo, tau_hi + 1):
|
||||||
|
band = sorted(nulls[t])
|
||||||
|
lo_b = band[int(0.025 * len(band))] if band else float("nan")
|
||||||
|
hi_b = band[int(0.975 * len(band))] if band else float("nan")
|
||||||
|
flag = ""
|
||||||
|
if real[t] == real[t] and band and (real[t] < lo_b or real[t] > hi_b):
|
||||||
|
flag = "OUTSIDE-NULL"
|
||||||
|
if t <= 45:
|
||||||
|
print(f" {t:4d}: {real[t]:8.4f} {lo_b:9.4f} {hi_b:9.4f} {flag}")
|
||||||
|
|
||||||
|
# ---- control: is DrussGT's turning also locked to its OWN fire cadence? ----
|
||||||
|
own_fire_by_round = {rd["round"]: [] for rd in rlist}
|
||||||
|
_start = {rd["round"]: rd["start"] for rd in rlist}
|
||||||
|
for e in fires_subj:
|
||||||
|
own_fire_by_round[e["round"]].append(_start[e["round"]] + (e["tick"] - 1))
|
||||||
|
for rn in own_fire_by_round:
|
||||||
|
own_fire_by_round[rn].sort()
|
||||||
|
print("\n=== CONTROL PSTH (|dHeading|) locked to DrussGT's OWN fire times ===")
|
||||||
|
print(" tau : real null95-low null95-high flag")
|
||||||
|
real_own = psth_for("hc", own_fire_by_round)
|
||||||
|
for t in range(0, 41):
|
||||||
|
band = []
|
||||||
|
for _ in range(80):
|
||||||
|
shifted = {}
|
||||||
|
for arr in arrays:
|
||||||
|
lo, n = arr["start"], arr["n"]
|
||||||
|
k = random.randint(0, max(1, n - 1))
|
||||||
|
shifted[arr["round"]] = [lo + ((x - lo + k) % n) for x in own_fire_by_round[arr["round"]]]
|
||||||
|
band.append(psth_for("hc", shifted)[t])
|
||||||
|
band = sorted(v for v in band if v == v)
|
||||||
|
lo_b = band[int(0.025 * len(band))] if band else float("nan")
|
||||||
|
hi_b = band[int(0.975 * len(band))] if band else float("nan")
|
||||||
|
flag = "OUTSIDE-NULL" if (real_own[t] == real_own[t] and band and
|
||||||
|
(real_own[t] < lo_b or real_own[t] > hi_b)) else ""
|
||||||
|
print(f" {t:4d}: {real_own[t]:8.4f} {lo_b:9.4f} {hi_b:9.4f} {flag}")
|
||||||
|
|
||||||
|
# ---- range-keeping vs ticks since adversary fire ----
|
||||||
|
print("\n=== range response: mean range (px) vs ticks-since-adversary-fire ===")
|
||||||
|
for t in range(0, 41, 5):
|
||||||
|
vals = []
|
||||||
|
for rd in rlist:
|
||||||
|
R = rd["rows"]
|
||||||
|
lo = rd["start"]
|
||||||
|
for f in fire_by_round[rd["round"]]:
|
||||||
|
gi = f + t
|
||||||
|
if lo <= gi < lo + len(R):
|
||||||
|
r = R[gi - lo]
|
||||||
|
vals.append(math.hypot(r["ex"] - r["sx"], r["ey"] - r["sy"]))
|
||||||
|
print(f" tau={t:3d}: mean range = {st.mean(vals):7.1f} (n={len(vals)})")
|
||||||
|
|
||||||
|
# ---- cross-correlation: fire impulse train vs response ----
|
||||||
|
print("\n=== cross-correlation of adversary-fire impulse with response ===")
|
||||||
|
# Sparse computation: fires are rare (k ~ 1000 out of ~20000 ticks), so the
|
||||||
|
# only permutation-dependent term is sum_xy at each lag.
|
||||||
|
for key, label in [("hc", "|dHeading|"), ("rev", "reversal")]:
|
||||||
|
resp = [arr[key] for arr in arrays]
|
||||||
|
fire_rel = []
|
||||||
|
for arr in arrays:
|
||||||
|
lo, n = arr["start"], arr["n"]
|
||||||
|
fire_rel.append([gi - lo for gi in fire_by_round[arr["round"]] if lo <= gi < lo + n])
|
||||||
|
|
||||||
|
denom = {}
|
||||||
|
for lag in range(0, 41):
|
||||||
|
npar = 0
|
||||||
|
sy = sy2 = 0.0
|
||||||
|
for r in resp:
|
||||||
|
n = len(r)
|
||||||
|
start = 1 if lag == 0 else 0 # r[0] is NaN (round start)
|
||||||
|
for j in range(start + lag, n):
|
||||||
|
v = r[j]
|
||||||
|
if v == v:
|
||||||
|
npar += 1
|
||||||
|
sy += v
|
||||||
|
sy2 += v * v
|
||||||
|
denom[lag] = (npar, sy, sy2)
|
||||||
|
|
||||||
|
def sparse_corr(fr_lists):
|
||||||
|
out = {}
|
||||||
|
for lag in range(0, 41):
|
||||||
|
npar, sy, sy2 = denom[lag]
|
||||||
|
if npar < 10:
|
||||||
|
out[lag] = float("nan")
|
||||||
|
continue
|
||||||
|
k = 0
|
||||||
|
sxy = 0.0
|
||||||
|
for r, fl in zip(resp, fr_lists):
|
||||||
|
n = len(r)
|
||||||
|
start = 1 if lag == 0 else 0
|
||||||
|
for fi in fl:
|
||||||
|
if start <= fi < n - lag:
|
||||||
|
v = r[fi + lag]
|
||||||
|
if v == v:
|
||||||
|
k += 1
|
||||||
|
sxy += v
|
||||||
|
mux = k / npar
|
||||||
|
muy = sy / npar
|
||||||
|
cov = sxy / npar - mux * muy
|
||||||
|
varx = k / npar - mux * mux
|
||||||
|
vary = sy2 / npar - muy * muy
|
||||||
|
out[lag] = (cov / math.sqrt(varx * vary)
|
||||||
|
if varx > 0 and vary > 0 else float("nan"))
|
||||||
|
return out
|
||||||
|
|
||||||
|
real = sparse_corr(fire_rel)
|
||||||
|
best = max((v, k) for k, v in real.items() if v == v)
|
||||||
|
print(f" {label}: peak r={best[0]:+.4f} at lag={best[1]} ticks; "
|
||||||
|
f"r(0)={real[0]:+.4f} r(10)={real[10]:+.4f} r(20)={real[20]:+.4f} "
|
||||||
|
f"r(30)={real[30]:+.4f}")
|
||||||
|
|
||||||
|
null_peaks = []
|
||||||
|
for _ in range(200):
|
||||||
|
shifted = []
|
||||||
|
for arr, fl in zip(arrays, fire_rel):
|
||||||
|
n = arr["n"]
|
||||||
|
kk = random.randint(0, max(1, n - 1))
|
||||||
|
shifted.append([(x + kk) % n for x in fl])
|
||||||
|
null_peaks.append(max(v for v in sparse_corr(shifted).values() if v == v))
|
||||||
|
p = (1 + sum(1 for v in null_peaks if v >= best[0])) / (1 + len(null_peaks))
|
||||||
|
print(f" permutation p(peak >= real peak) = {p:.4f} "
|
||||||
|
f"(null peak mean={st.mean(null_peaks):+.4f}, max={max(null_peaks):+.4f})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,16 @@ export DRUSSGT_DATA="${DRUSSGT_DATA:-/tmp/drussgt_bridge_data}"
|
|||||||
BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
BOTDIR="${DRUSSGT_BOTDIR:-/tmp/tr_bots/DrussGT}"
|
||||||
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null
|
"$HERE/make_botdir.sh" "$BOTDIR" >/dev/null
|
||||||
|
|
||||||
|
EVENTS_ARGS=()
|
||||||
|
if [[ -n "${TR_EVENTS_OUT:-}" ]]; then
|
||||||
|
EVENTS_ARGS=(--events-out "$TR_EVENTS_OUT")
|
||||||
|
fi
|
||||||
|
|
||||||
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
|
java -cp "$HERE/out:$RUNNER" robocode_shim.TrBattleCapture \
|
||||||
--rounds "$ROUNDS" --out "$OUT_JSONL" \
|
--rounds "$ROUNDS" --out "$OUT_JSONL" \
|
||||||
--bot "$BOTDIR" --bot "$ADVERSARY" \
|
--bot "$BOTDIR" --bot "$ADVERSARY" \
|
||||||
--subject DrussGT --adversary "$(basename "$ADVERSARY")"
|
--subject DrussGT --adversary "$(basename "$ADVERSARY")" \
|
||||||
|
"${EVENTS_ARGS[@]}"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "=== movement statistics (TR) ==="
|
echo "=== movement statistics (TR) ==="
|
||||||
|
|||||||
@@ -42,18 +42,29 @@ public final class TrBattleCapture {
|
|||||||
private static int globalTick = 0;
|
private static int globalTick = 0;
|
||||||
private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
|
private static final Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
|
||||||
private static final List<double[]> pendingRows = new ArrayList<>();
|
private static final List<double[]> pendingRows = new ArrayList<>();
|
||||||
|
private static final List<String> roundResultLines = new ArrayList<>();
|
||||||
|
|
||||||
// battle evidence counters (subject = the bot under test)
|
// battle evidence counters (subject = the bot under test)
|
||||||
private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0;
|
private static int bulletsFired = 0, bulletHits = 0, bulletMisses = 0, bulletHitBullets = 0;
|
||||||
private static int scansSeen = 0, hitsTaken = 0;
|
private static int scansSeen = 0, hitsTaken = 0;
|
||||||
|
|
||||||
|
// optional raw-event sidecar (fire/hit/death events), used to measure
|
||||||
|
// closed-loop reactivity of the subject to the adversary's bullets
|
||||||
|
private static PrintWriter eventsWriter = null;
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
int rounds = 5;
|
int rounds = 5;
|
||||||
String outFile = "/tmp/tr_drussgt.jsonl";
|
String outFile = "/tmp/tr_drussgt.jsonl";
|
||||||
String adversary = "unknown";
|
String adversary = "unknown";
|
||||||
String note = "Tank Royale battle captured via TrBattleCapture (observer tick stream, perfect info)";
|
String note = "Tank Royale battle via the robocode_shim bridge: the real, unmodified DrussGT fights the "
|
||||||
|
+ "adversary in Tank Royale physics. Unlike the classic-robocode captures, the recorded trajectory "
|
||||||
|
+ "was produced while DrussGT was reacting to THIS adversary's live bullets (closed loop at capture "
|
||||||
|
+ "time). It is still a recorded trajectory, so replaying it against a different gun is open-loop; "
|
||||||
|
+ "and it is still perfect-information (observer gives true positions every tick, unlike the live "
|
||||||
|
+ "bot's stale between-scan WorldState).";
|
||||||
List<String> botDirs = new ArrayList<>();
|
List<String> botDirs = new ArrayList<>();
|
||||||
String subject = "DrussGT";
|
String subject = "DrussGT";
|
||||||
|
String eventsOut = null;
|
||||||
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
for (int i = 0; i < args.length; i++) {
|
||||||
switch (args[i]) {
|
switch (args[i]) {
|
||||||
@@ -63,9 +74,14 @@ public final class TrBattleCapture {
|
|||||||
case "--subject" -> subject = args[++i];
|
case "--subject" -> subject = args[++i];
|
||||||
case "--adversary" -> adversary = args[++i];
|
case "--adversary" -> adversary = args[++i];
|
||||||
case "--note" -> note = args[++i];
|
case "--note" -> note = args[++i];
|
||||||
|
case "--events-out" -> eventsOut = args[++i];
|
||||||
default -> { System.err.println("Unknown arg: " + args[i]); System.exit(2); }
|
default -> { System.err.println("Unknown arg: " + args[i]); System.exit(2); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (eventsOut != null) {
|
||||||
|
new File(eventsOut).getAbsoluteFile().getParentFile().mkdirs();
|
||||||
|
eventsWriter = new PrintWriter(new BufferedWriter(new FileWriter(eventsOut)));
|
||||||
|
}
|
||||||
if (botDirs.isEmpty()) {
|
if (botDirs.isEmpty()) {
|
||||||
System.err.println("--bot <dir> required (repeatable)");
|
System.err.println("--bot <dir> required (repeatable)");
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
@@ -88,9 +104,24 @@ public final class TrBattleCapture {
|
|||||||
System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern);
|
System.out.println("[capture] bots=" + botDirs + " rounds=" + rounds + " subject~=" + subjectPattern);
|
||||||
try (var handle = runner.startBattleAsync(setup, bots)) {
|
try (var handle = runner.startBattleAsync(setup, bots)) {
|
||||||
handle.getOnTickEvent().on(owner, event -> onTick(event, subjectPattern));
|
handle.getOnTickEvent().on(owner, event -> onTick(event, subjectPattern));
|
||||||
|
handle.getOnRoundEnded().on(owner, event -> {
|
||||||
|
StringBuilder sb = new StringBuilder("[capture] ROUND " + event.getRoundNumber() + " (turn "
|
||||||
|
+ event.getTurnNumber() + "): ");
|
||||||
|
for (var r : event.getResults()) {
|
||||||
|
sb.append(String.format(Locale.ROOT, "%s rank=%d score=%d | ",
|
||||||
|
r.getName(), r.getRank(), r.getTotalScore()));
|
||||||
|
}
|
||||||
|
String line = sb.toString();
|
||||||
|
System.out.println(line);
|
||||||
|
roundResultLines.add(line);
|
||||||
|
});
|
||||||
BattleResults results = handle.awaitResults();
|
BattleResults results = handle.awaitResults();
|
||||||
|
|
||||||
writeOutput(outFile, adversary, note, rounds);
|
writeOutput(outFile, adversary, note, rounds);
|
||||||
|
if (eventsWriter != null) {
|
||||||
|
eventsWriter.close();
|
||||||
|
System.out.println("[capture] events -> " + eventsOut);
|
||||||
|
}
|
||||||
System.out.println("[capture] rows=" + globalTick + " rounds=" + roundTicks + " -> " + outFile);
|
System.out.println("[capture] rows=" + globalTick + " rounds=" + roundTicks + " -> " + outFile);
|
||||||
System.out.println("[capture] subject event counts: scans=" + scansSeen
|
System.out.println("[capture] subject event counts: scans=" + scansSeen
|
||||||
+ " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits
|
+ " bulletsFired=" + bulletsFired + " bulletHits=" + bulletHits
|
||||||
@@ -146,9 +177,61 @@ public final class TrBattleCapture {
|
|||||||
&& sb.getScannedByBotId() == subjectId) {
|
&& sb.getScannedByBotId() == subjectId) {
|
||||||
scansSeen++;
|
scansSeen++;
|
||||||
}
|
}
|
||||||
|
logEvent(event.getRoundNumber(), event.getTurnNumber(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Raw event sidecar: bullet fires / hits / deaths for both bots. */
|
||||||
|
private static void logEvent(int round, int turn, dev.robocode.tankroyale.client.model.Event e) {
|
||||||
|
if (eventsWriter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long owner = null, victim = null, bullet = null;
|
||||||
|
Double power = null, x = null, y = null, dir = null, damage = null;
|
||||||
|
String type;
|
||||||
|
if (e instanceof dev.robocode.tankroyale.client.model.BulletFiredEvent bf) {
|
||||||
|
type = "fire";
|
||||||
|
bullet = (long) bf.getBullet().getBulletId();
|
||||||
|
owner = (long) bf.getBullet().getOwnerId();
|
||||||
|
power = bf.getBullet().getPower();
|
||||||
|
x = bf.getBullet().getX();
|
||||||
|
y = bf.getBullet().getY();
|
||||||
|
dir = bf.getBullet().getDirection();
|
||||||
|
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBotEvent bh) {
|
||||||
|
type = "hit";
|
||||||
|
bullet = (long) bh.getBullet().getBulletId();
|
||||||
|
owner = (long) bh.getBullet().getOwnerId();
|
||||||
|
victim = (long) bh.getVictimId();
|
||||||
|
damage = bh.getDamage();
|
||||||
|
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitBulletEvent bb) {
|
||||||
|
type = "hitbullet";
|
||||||
|
bullet = (long) bb.getBullet().getBulletId();
|
||||||
|
owner = (long) bb.getBullet().getOwnerId();
|
||||||
|
} else if (e instanceof dev.robocode.tankroyale.client.model.BulletHitWallEvent bw) {
|
||||||
|
type = "hitwall";
|
||||||
|
bullet = (long) bw.getBullet().getBulletId();
|
||||||
|
owner = (long) bw.getBullet().getOwnerId();
|
||||||
|
} else if (e instanceof dev.robocode.tankroyale.client.model.BotDeathEvent bd) {
|
||||||
|
type = "death";
|
||||||
|
victim = (long) bd.getVictimId();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("{\"round\":").append(round).append(",\"tick\":").append(turn)
|
||||||
|
.append(",\"type\":\"").append(type).append('\"');
|
||||||
|
if (owner != null) sb.append(",\"owner\":").append(owner);
|
||||||
|
if (victim != null) sb.append(",\"victim\":").append(victim);
|
||||||
|
if (bullet != null) sb.append(",\"bullet\":").append(bullet);
|
||||||
|
if (power != null) sb.append(",\"power\":").append(fmt(power));
|
||||||
|
if (x != null) sb.append(",\"x\":").append(fmt(x));
|
||||||
|
if (y != null) sb.append(",\"y\":").append(fmt(y));
|
||||||
|
if (dir != null) sb.append(",\"dir\":").append(fmt(dir));
|
||||||
|
if (damage != null) sb.append(",\"damage\":").append(fmt(damage));
|
||||||
|
sb.append('}');
|
||||||
|
eventsWriter.println(sb);
|
||||||
|
}
|
||||||
|
|
||||||
private static void writeOutput(String outFile, String adversary, String note, int rounds) throws Exception {
|
private static void writeOutput(String outFile, String adversary, String note, int rounds) throws Exception {
|
||||||
File f = new File(outFile);
|
File f = new File(outFile);
|
||||||
if (f.getParentFile() != null) {
|
if (f.getParentFile() != null) {
|
||||||
@@ -156,7 +239,7 @@ public final class TrBattleCapture {
|
|||||||
}
|
}
|
||||||
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
|
try (PrintWriter w = new PrintWriter(new BufferedWriter(new FileWriter(f)))) {
|
||||||
w.println("{\"meta\":{\"adversary\":\"" + esc(adversary)
|
w.println("{\"meta\":{\"adversary\":\"" + esc(adversary)
|
||||||
+ "\",\"source\":\"tank-royale\",\"perfect_info\":true,"
|
+ "\",\"source\":\"tr-bridge\",\"closed_loop\":true,\"perfect_info\":true,"
|
||||||
+ "\"arena\":{\"w\":800,\"h\":600},\"note\":\"" + esc(note) + "\"}}");
|
+ "\"arena\":{\"w\":800,\"h\":600},\"note\":\"" + esc(note) + "\"}}");
|
||||||
for (double[] r : pendingRows) {
|
for (double[] r : pendingRows) {
|
||||||
w.println("{\"tick\":" + (long) r[0]
|
w.println("{\"tick\":" + (long) r[0]
|
||||||
@@ -178,6 +261,14 @@ public final class TrBattleCapture {
|
|||||||
}
|
}
|
||||||
rs.println("]}");
|
rs.println("]}");
|
||||||
}
|
}
|
||||||
|
try (PrintWriter rr = new PrintWriter(new BufferedWriter(new FileWriter(outFile + ".results.json")))) {
|
||||||
|
rr.println("{\"roundResults\":[");
|
||||||
|
for (int i = 0; i < roundResultLines.size(); i++) {
|
||||||
|
rr.print(" \"" + esc(roundResultLines.get(i)) + "\"");
|
||||||
|
rr.println(i + 1 < roundResultLines.size() ? "," : "");
|
||||||
|
}
|
||||||
|
rr.println("]}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String fmt(double v) {
|
private static String fmt(double v) {
|
||||||
|
|||||||
Reference in New Issue
Block a user