feat(tools): capture real DrussGT movement from classic Robocode as fixtures
There are no genuinely competitive adversaries for Tank Royale, and the in-repo ones were broken until recently. Classic Robocode 1.9.5.5 is obtainable (SourceForge, 20.4 MB) and its programmatic control API (robocode.control.RobocodeEngine + BattleAdaptor.onTurnEnded) can run real battles headless and expose per-turn robot state. So a legacy leader bot's MOVEMENT can be captured and used as a gun-testing fixture with no port. Captured unmodified DrussGT 3.1.4159 vs spinbot/ramfire/crazy/corners and a mirror match: 28,797 ticks, plus two trivial-bot contrasts. Conversion to the Tank Royale convention is validated to 0.000-0.001 deg by recomputing the direction implied by (heading, speed) and comparing it against the recorded per-tick displacement -- i.e. the data is proven to be genuine recorded motion rather than a mangled export. (A first attempt treated the snapshot API's headings as degrees; they are radians, ~95 deg off.) The statistics confirm it is really a wave surfer: perpendicular to the opponent 65-96% of ticks, radial ~0.001, 41-72% of ticks at full speed, reversing on 42-46% of ticks, holding range at a 283-526 px median. The straight-line contrast is radial-dominant (0.75) with ZERO reversals. Discovery: DrussGT detects predictable guns and switches to a bullet-shield stand-still mode, so captures against sample.Walls/TrackFire had to be rejected as non-movement. CAVEATS, recorded in DRUSSGT_FIXTURES.md: these are open-loop (replayed DrussGT never dodges OUR bullets) and perfect-information (the observer gives true positions every tick, unlike our stale live WorldState). Both make our guns look better than in live play, so use them for RELATIVE gun ranking, not absolute hit rates. Jars stay out of git; capture tooling is reproducible via capture.sh.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# Classic-Robocode leader movement fixtures — DrussGT
|
||||
|
||||
Captured **real, unmodified DrussGT 3.1.4159** movement from classic Robocode
|
||||
1.9.5.5 and converted it to the project's shared JSONL fixture format.
|
||||
|
||||
These are **movement-pattern fixtures for gun testing**, not runnable opponents.
|
||||
|
||||
## How the data was obtained
|
||||
|
||||
| Artifact | Source | Result |
|
||||
|---|---|---|
|
||||
| Classic Robocode 1.9.5.5 (full install, 20,431,686 bytes) | `https://sourceforge.net/projects/robocode/files/robocode/1.9.5.5/robocode-1.9.5.5-setup.jar/download` | HTTP 200 |
|
||||
| `robocode-1.9.5.5.zip` | same SourceForge dir | HTTP **404** (does not exist) |
|
||||
| `net.sf.robocode:*` 1.9.5.5 (api/core/battle/host/samples/repository/ui/sound/roborumble) | `https://repo1.maven.org/maven2/net/sf/robocode/` | HTTP 200 (available; not needed once the full installer was fetched) |
|
||||
| DrussGT 3.1.4159 jar (159,289 bytes, md5 `5cd6015dcc6d6da8a7e6aeecb1fec211`) | `http://robocode-archive.strangeautomata.com/robots/jk.mega.DrussGT_3.1.4159.jar` | HTTP 200 |
|
||||
|
||||
The Robocode "setup jar" is a plain ZIP of the full installation
|
||||
(`libs/`, `robots/`, `battles/`, javadoc). Extracting it is enough to run the
|
||||
programmatic control API headlessly. On Java 21 the engine needs
|
||||
`-Djava.security.manager=allow` and four `--add-opens` flags (see
|
||||
`tools/robocode_fixture_capture/README.md`); Java 17/11 also work.
|
||||
|
||||
**The DrussGT jar and classic Robocode are third-party redistributables and are
|
||||
kept OUT of the repository** — they live only under `/tmp/robocode/`.
|
||||
`tools/robocode_fixture_capture/capture.sh` re-downloads them on demand.
|
||||
|
||||
## Capture mechanism
|
||||
|
||||
`tools/robocode_fixture_capture/Capture.java` uses the classic Robocode control
|
||||
API — no GUI:
|
||||
|
||||
- `robocode.control.RobocodeEngine` + `BattleSpecification` (800×600, 20 rounds,
|
||||
gun-cooling 0.1, inactivity 450) run the battle headlessly.
|
||||
- A `BattleAdaptor` receives `onTurnEnded(TurnEndedEvent)` every turn.
|
||||
- `event.getTurnSnapshot().getRobots()` yields `IRobotSnapshot[]` with `getX()`,
|
||||
`getY()`, `getBodyHeading()`, `getVelocity()`, `getEnergy()` for every robot,
|
||||
every turn — true positions, no fog of war.
|
||||
- The fallback "spy robot" approach was **not needed**; the observer API worked.
|
||||
- Robots captured: `e* = jk.mega.DrussGT`, `s* = the opponent that fought it`.
|
||||
|
||||
## Coordinate conversion (exact)
|
||||
|
||||
Tank Royale and classic Robocode share the **same positional axes** (origin
|
||||
bottom-left, +x right, +y up), so `x`/`y` are copied unchanged. Only the angle
|
||||
system differs:
|
||||
|
||||
- classic: `0° = North (+y)`, positive rotation **clockwise**
|
||||
- Tank Royale: `0° = East (+x)`, positive rotation **counter-clockwise**
|
||||
|
||||
The control-snapshot API returns headings in **radians** (per the official
|
||||
javadoc: *"Returns the body heading of the robot in radians"*), in the classic
|
||||
convention. Conversion used:
|
||||
|
||||
```java
|
||||
double classicDeg = Math.toDegrees(rawBodyHeadingRadians); // 0=north, clockwise
|
||||
double tankDeg = (90.0 - classicDeg) % 360.0; // 0=east, counter-clockwise
|
||||
if (tankDeg < 0) tankDeg += 360.0;
|
||||
// x, y: unchanged
|
||||
```
|
||||
|
||||
Equivalently, in radians: `tankRad = PI/2 - classicRad`.
|
||||
|
||||
**Validation:** for every consecutive tick within a round, the measured
|
||||
displacement direction of DrussGT was compared with the direction implied by the
|
||||
converted heading + signed velocity. Mean angular error is **0.000°–0.001°** on
|
||||
every fixture — the conversion is exact, and it doubles as proof the fixtures are
|
||||
real recorded motion rather than fabricated data.
|
||||
|
||||
## Fields
|
||||
|
||||
Following the mandated shared format exactly:
|
||||
|
||||
```
|
||||
{"meta":{"adversary":"...","source":"classic-robocode","perfect_info":true,"arena":{"w":800,"h":600},"note":"..."}}
|
||||
{"tick":0,"ex":...,"ey":...,"eh":...,"es":...,"ee":...,"sx":...,"sy":...,"sh":...,"ss":...,"se":...}
|
||||
```
|
||||
|
||||
- `eh`/`sh`: **Tank Royale** heading in degrees (0=east, CCW).
|
||||
- `es`/`ss`: **signed** velocity in px/tick (`getVelocity()`; negative = moving
|
||||
backwards relative to body heading).
|
||||
- `ee`/`se`: energy (0–100).
|
||||
- `tick`: **continuous global counter across all rounds** in the file; positions
|
||||
teleport at round boundaries. Round boundaries are listed in
|
||||
`drussgt_meta/<file>.jsonl.rounds.json` (`startTick`, `count`).
|
||||
|
||||
## Files
|
||||
|
||||
| File | ticks | rounds | bytes | enemy (`e*`) | shooter (`s*`) |
|
||||
|---|---:|---:|---:|---|---|
|
||||
| `drussgt_vs_spinbot.jsonl` | 5002 | 20 | 741,720 | DrussGT | sample.SpinBot |
|
||||
| `drussgt_vs_ramfire.jsonl` | 3240 | 20 | 481,176 | DrussGT | sample.RamFire (rammer) |
|
||||
| `drussgt_vs_crazy.jsonl` | 9025 | 20 | 1,335,519 | DrussGT | sample.Crazy |
|
||||
| `drussgt_vs_corners.jsonl` | 4975 | 20 | 732,806 | DrussGT | sample.Corners (wall-hugger) |
|
||||
| `drussgt_vs_drussgt.jsonl` | 6555 | 2 | 965,834 | DrussGT | DrussGT (mirror) |
|
||||
| `contrast_straightline.jsonl` | 1451 | 1 | 213,049 | trivial.StraightLine | sample.SittingDuck |
|
||||
| `contrast_stationary_sittingduck.jsonl` | 1451 | 1 | 213,117 | sample.SittingDuck | sample.SittingDuck |
|
||||
|
||||
`drussgt_meta/` holds the round-boundary sidecars; it is deliberately outside the
|
||||
`*.jsonl` namespace.
|
||||
|
||||
## Statistics — evidence this is real DrussGT movement
|
||||
|
||||
All numbers measured from the fixtures above.
|
||||
|
||||
| fixture | mean speed | %ticks speed≥7.5 | %ticks speed<0.5 | %neg. velocity | mean |Δheading| | %|Δh|>5° | median dist | perp frac | radial frac | mean\|sin\| |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| drussgt_vs_drussgt | 5.26 | 41.1% | 6.6% | 45.7% | 1.29° | 8.2% | 526 | 0.951 | 0.001 | 0.972 |
|
||||
| drussgt_vs_spinbot | 6.59 | 70.1% | 6.7% | 46.4% | 1.81° | 9.4% | 402 | 0.962 | 0.001 | 0.971 |
|
||||
| drussgt_vs_ramfire | 6.65 | 72.3% | 4.6% | 41.7% | 3.00° | 14.9% | 283 | 0.651 | 0.013 | 0.898 |
|
||||
| drussgt_vs_crazy | 6.16 | 66.8% | 13.7% | 43.9% | 2.01° | 8.3% | 378 | 0.829 | 0.010 | 0.934 |
|
||||
| drussgt_vs_corners | 6.01 | 58.6% | 9.2% | 41.9% | 2.09° | 12.5% | 504 | 0.920 | 0.004 | 0.964 |
|
||||
| **contrast straight-line** | 6.28 | 75.3% | 18.4% | **0.0%** | 1.66° | 16.6%* | 239 | **0.086** | **0.752** | 0.373 |
|
||||
| **contrast stationary** | **0.00** | 0.0% | **100%** | 0.0% | **0.00°** | 0.0% | 210 | n/a | n/a | n/a |
|
||||
|
||||
`perp frac` = fraction of moving ticks whose movement is within 30° of
|
||||
perpendicular to the enemy→shooter line (`|sin δ| > 0.866`); `radial frac` = within
|
||||
30° of that line (`|cos δ| > 0.866`).
|
||||
|
||||
\* the straight-line bot only ever turns 10°/tick in place at a wall, giving the
|
||||
bimodal heading-change distribution below — it never arcs.
|
||||
|
||||
**Speed distribution** (fraction of all ticks per px/tick bin):
|
||||
|
||||
| bin | 0 | 0.5-1 | 1-2 | 2-3 | 3-4 | 4-5 | 5-6 | 6-7 | 7-7.5 | 7.5-8 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| DrussGT vs SpinBot | .067 | .006 | .024 | .033 | .024 | .042 | .030 | .052 | .020 | **.701** |
|
||||
| straight-line (contrast) | .184 | .000 | .009 | .009 | .009 | .009 | .009 | .009 | .009 | **.753** |
|
||||
| stationary (contrast) | **1.000** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
**Heading-change-per-tick distribution** (fraction, degrees):
|
||||
|
||||
| bin | 0-0.5 | 0.5-1 | 1-2 | 2-3 | 3-5 | 5-7 | 7-10 | 10-11 |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| DrussGT vs SpinBot | .439 | .113 | .122 | .057 | .175 | .035 | .044 | .015 |
|
||||
| DrussGT vs Crazy | .389 | .096 | .117 | .075 | .240 | .033 | .041 | .010 |
|
||||
| straight-line (contrast) | **.833** | 0 | 0 | .001 | 0 | 0 | 0 | **.166** |
|
||||
| stationary (contrast) | **1.000** | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Interpretation: DrussGT spends most of its time at full speed, frequently
|
||||
**reverses** (≈42–46% of ticks have negative velocity — stop-and-go surfing), and
|
||||
its movement is strongly **perpendicular** to the opponent (mean |sin| ≈ 0.90–0.97,
|
||||
radial fraction ≈ 0.001–0.013) while actively controlling range (median distance
|
||||
283–526 px, varying by opponent). The contrast bots are stationary or move
|
||||
radially at constant heading with no reversals. The difference is in the numbers,
|
||||
not asserted.
|
||||
|
||||
## Caveats (read before using these as an opponent)
|
||||
|
||||
1. **OPEN-LOOP.** This is recorded trajectory data replayed into a gun test.
|
||||
Replayed DrussGT does not react to bullets fired by *our* bot, so our gun's
|
||||
score against these fixtures is **optimistic**. This is a *movement-pattern
|
||||
benchmark*, not an opponent. It tests whether our aiming math can lead and
|
||||
predict leader-class motion; it does not test whether we can out-duel DrussGT.
|
||||
2. **PERFECT INFORMATION.** Classic Robocode's battle observer reports true
|
||||
positions every turn. Our live bot's `WorldState` is stale between radar scans.
|
||||
Fixtures from this source are therefore optimistic relative to live play in
|
||||
*two* ways (open-loop + no sensor latency/fog). A gun that performs well here
|
||||
may perform worse live.
|
||||
3. **Bullet-shielding is excluded.** Against predictable guns (`sample.Walls`,
|
||||
`sample.TrackFire`) DrussGT's EnergyDome shield engages and it **stands still**
|
||||
— a real DrussGT behaviour but not movement. Those captures were rejected;
|
||||
only opponents that make DrussGT surf (SpinBot/RamFire/Crazy/Corners and a
|
||||
DrussGT mirror) are included. DrussGT's source was **not modified** — the
|
||||
shipped jar is used as-is.
|
||||
4. **Rounds are concatenated.** `tick` is continuous, so positions jump at round
|
||||
boundaries. Split on the boundaries in `drussgt_meta/*.rounds.json` (or on
|
||||
impossible displacements) before computing anything displacement-based.
|
||||
5. **Signed velocity.** `es`/`ss` may be negative. Reconstruct motion as
|
||||
`x += es*cos(radians(eh)); y += es*sin(radians(eh))` — do **not** use `abs(es)`
|
||||
with `eh` unless you also flip `eh` by 180° for negative ticks.
|
||||
|
||||
Reproduce with `tools/robocode_fixture_capture/capture.sh`; validate/analyse with
|
||||
`tools/robocode_fixture_capture/analyze.py`.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":1451}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":1451}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":480},{"round":1,"startTick":480,"count":287},{"round":2,"startTick":767,"count":214},{"round":3,"startTick":981,"count":185},{"round":4,"startTick":1166,"count":280},{"round":5,"startTick":1446,"count":225},{"round":6,"startTick":1671,"count":344},{"round":7,"startTick":2015,"count":292},{"round":8,"startTick":2307,"count":248},{"round":9,"startTick":2555,"count":206},{"round":10,"startTick":2761,"count":210},{"round":11,"startTick":2971,"count":216},{"round":12,"startTick":3187,"count":275},{"round":13,"startTick":3462,"count":271},{"round":14,"startTick":3733,"count":164},{"round":15,"startTick":3897,"count":228},{"round":16,"startTick":4125,"count":188},{"round":17,"startTick":4313,"count":226},{"round":18,"startTick":4539,"count":208},{"round":19,"startTick":4747,"count":228}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":1825},{"round":1,"startTick":1825,"count":473},{"round":2,"startTick":2298,"count":417},{"round":3,"startTick":2715,"count":617},{"round":4,"startTick":3332,"count":483},{"round":5,"startTick":3815,"count":394},{"round":6,"startTick":4209,"count":300},{"round":7,"startTick":4509,"count":372},{"round":8,"startTick":4881,"count":334},{"round":9,"startTick":5215,"count":509},{"round":10,"startTick":5724,"count":341},{"round":11,"startTick":6065,"count":389},{"round":12,"startTick":6454,"count":475},{"round":13,"startTick":6929,"count":268},{"round":14,"startTick":7197,"count":333},{"round":15,"startTick":7530,"count":250},{"round":16,"startTick":7780,"count":187},{"round":17,"startTick":7967,"count":257},{"round":18,"startTick":8224,"count":414},{"round":19,"startTick":8638,"count":387}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":3506},{"round":1,"startTick":3506,"count":3049}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":189},{"round":1,"startTick":189,"count":182},{"round":2,"startTick":371,"count":161},{"round":3,"startTick":532,"count":159},{"round":4,"startTick":691,"count":177},{"round":5,"startTick":868,"count":149},{"round":6,"startTick":1017,"count":161},{"round":7,"startTick":1178,"count":172},{"round":8,"startTick":1350,"count":175},{"round":9,"startTick":1525,"count":160},{"round":10,"startTick":1685,"count":158},{"round":11,"startTick":1843,"count":164},{"round":12,"startTick":2007,"count":140},{"round":13,"startTick":2147,"count":166},{"round":14,"startTick":2313,"count":161},{"round":15,"startTick":2474,"count":168},{"round":16,"startTick":2642,"count":165},{"round":17,"startTick":2807,"count":151},{"round":18,"startTick":2958,"count":142},{"round":19,"startTick":3100,"count":140}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"rounds":[{"round":0,"startTick":0,"count":623},{"round":1,"startTick":623,"count":352},{"round":2,"startTick":975,"count":192},{"round":3,"startTick":1167,"count":276},{"round":4,"startTick":1443,"count":293},{"round":5,"startTick":1736,"count":154},{"round":6,"startTick":1890,"count":289},{"round":7,"startTick":2179,"count":164},{"round":8,"startTick":2343,"count":260},{"round":9,"startTick":2603,"count":166},{"round":10,"startTick":2769,"count":263},{"round":11,"startTick":3032,"count":239},{"round":12,"startTick":3271,"count":287},{"round":13,"startTick":3558,"count":192},{"round":14,"startTick":3750,"count":162},{"round":15,"startTick":3912,"count":197},{"round":16,"startTick":4109,"count":176},{"round":17,"startTick":4285,"count":231},{"round":18,"startTick":4516,"count":292},{"round":19,"startTick":4808,"count":194}]}
|
||||
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
@@ -0,0 +1,226 @@
|
||||
import robocode.control.*;
|
||||
import robocode.control.events.*;
|
||||
import robocode.control.snapshot.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Headless classic-Robocode battle runner that dumps per-turn robot state to JSONL.
|
||||
*
|
||||
* Usage: Capture <enemyNamePattern> <shooterNamePattern> <rounds> <outFile> <adversaryLabel> <note>
|
||||
*
|
||||
* The robot whose name matches enemyNamePattern is written as e* (the movement we want),
|
||||
* the other as s* (the shooter / bearing reference).
|
||||
*
|
||||
* Heading conversion (classic Robocode -> Tank Royale):
|
||||
* classic heading: 0 = North (+y), positive = clockwise
|
||||
* tank royale heading: 0 = East (+x), positive = counter-clockwise
|
||||
* => headingTank = (90 - headingClassic) mod 360
|
||||
* Positions are identical (both origin bottom-left, x right, y up).
|
||||
*/
|
||||
public class Capture {
|
||||
|
||||
public static class Row {
|
||||
int tick; // global, continuous across rounds
|
||||
double ex, ey, eh, es, ee;
|
||||
double sx, sy, sh, ss, se;
|
||||
}
|
||||
|
||||
static List<Row> rows = new ArrayList<>();
|
||||
static Map<Integer, String> idxToName = new HashMap<>();
|
||||
static int enemyIdx = -1, shooterIdx = -1;
|
||||
static int globalTick = 0;
|
||||
static Map<Integer, Integer> roundTicks = new LinkedHashMap<>();
|
||||
static List<String> errors = new ArrayList<>();
|
||||
|
||||
static double tankHeading(double classicRad) {
|
||||
// IRobotSnapshot.getBodyHeading() returns RADIANS in the classic Robocode
|
||||
// convention (0 = north, positive = clockwise).
|
||||
double classicDeg = Math.toDegrees(classicRad);
|
||||
double t = (90.0 - classicDeg) % 360.0;
|
||||
if (t < 0) t += 360.0;
|
||||
return t;
|
||||
}
|
||||
|
||||
static String f(double v) {
|
||||
return String.format(Locale.ROOT, "%.4f", v);
|
||||
}
|
||||
|
||||
static boolean printedIndexMap = false;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.setProperty("java.awt.headless", "true");
|
||||
if (args.length < 6) {
|
||||
System.err.println("usage: Capture <enemyPattern> <shooterPattern> <rounds> <out> <label> <note>");
|
||||
System.exit(2);
|
||||
}
|
||||
final String enemyPattern = args[0];
|
||||
final String shooterPattern = args[1];
|
||||
final int rounds = Integer.parseInt(args[2]);
|
||||
final String outFile = args[3];
|
||||
final String label = args[4];
|
||||
final String note = args[5];
|
||||
|
||||
File home = new File(System.getProperty("robocode.home", "/tmp/robocode/install"));
|
||||
System.out.println("[capture] home=" + home + " enemy=" + enemyPattern + " shooter=" + shooterPattern
|
||||
+ " rounds=" + rounds);
|
||||
|
||||
RobocodeEngine engine = new RobocodeEngine(home);
|
||||
engine.setVisible(false);
|
||||
engine.addBattleListener(new BattleAdaptor() {
|
||||
@Override
|
||||
public void onTurnEnded(TurnEndedEvent event) {
|
||||
try {
|
||||
ITurnSnapshot snap = event.getTurnSnapshot();
|
||||
IRobotSnapshot[] bots = snap.getRobots();
|
||||
if (!printedIndexMap) {
|
||||
printedIndexMap = true;
|
||||
for (IRobotSnapshot b : bots) {
|
||||
System.out.println("[capture] index=" + b.getRobotIndex() + " name=" + b.getName());
|
||||
}
|
||||
}
|
||||
IRobotSnapshot en = null, sh = null;
|
||||
for (IRobotSnapshot b : bots) {
|
||||
String n = b.getName();
|
||||
if (enemyIdx >= 0 && b.getRobotIndex() == enemyIdx) en = b;
|
||||
if (shooterIdx >= 0 && b.getRobotIndex() == shooterIdx) sh = b;
|
||||
}
|
||||
if (en == null || sh == null) return;
|
||||
if (en.getState() == null || !en.getState().isAlive()) return;
|
||||
if (sh.getState() == null || !sh.getState().isAlive()) return;
|
||||
|
||||
Row r = new Row();
|
||||
r.tick = globalTick++;
|
||||
r.ex = en.getX(); r.ey = en.getY();
|
||||
r.eh = tankHeading(en.getBodyHeading());
|
||||
r.es = en.getVelocity(); r.ee = en.getEnergy();
|
||||
r.sx = sh.getX(); r.sy = sh.getY();
|
||||
r.sh = tankHeading(sh.getBodyHeading());
|
||||
r.ss = sh.getVelocity(); r.se = sh.getEnergy();
|
||||
rows.add(r);
|
||||
roundTicks.merge(snap.getRound(), 1, Integer::sum);
|
||||
} catch (Throwable t) {
|
||||
errors.add("onTurnEnded: " + t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoundStarted(RoundStartedEvent e) {
|
||||
// nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBattleError(BattleErrorEvent e) {
|
||||
errors.add("battleError: " + e.getError());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRoundEnded(RoundEndedEvent e) {
|
||||
System.out.println("[capture] round " + e.getRound() + " ended, turns=" + e.getTurns());
|
||||
}
|
||||
});
|
||||
|
||||
List<RobotSpecification> picked = new ArrayList<>();
|
||||
RobotSpecification enSpec = null, shSpec = null;
|
||||
String query = enemyPattern.equals(shooterPattern) ? enemyPattern : enemyPattern + "," + shooterPattern;
|
||||
RobotSpecification[] repo = engine.getLocalRepository(query);
|
||||
if (repo == null || repo.length < 1) {
|
||||
System.err.println("[capture] FAILED to resolve robots; got " + (repo == null ? 0 : repo.length));
|
||||
System.exit(3);
|
||||
}
|
||||
for (RobotSpecification rs : repo) {
|
||||
System.out.println("[capture] repo entry name=" + rs.getName() + " class=" + rs.getClassName());
|
||||
if (enSpec == null && rs.getClassName().equals(enemyPattern)) enSpec = rs;
|
||||
if (shSpec == null && rs.getClassName().equals(shooterPattern)) shSpec = rs;
|
||||
}
|
||||
if (shSpec == null && enemyPattern.equals(shooterPattern)) shSpec = enSpec;
|
||||
if (enSpec == null || shSpec == null) {
|
||||
System.err.println("[capture] could not match specs; enemy=" + enSpec + " shooter=" + shSpec);
|
||||
System.exit(3);
|
||||
}
|
||||
System.out.println("[capture] enemy spec = " + enSpec.getName());
|
||||
System.out.println("[capture] shooter spec = " + shSpec.getName());
|
||||
|
||||
// Determine indices from the repository order passed to the battle.
|
||||
RobotSpecification[] chosen = new RobotSpecification[]{enSpec, shSpec};
|
||||
// The engine assigns robot index by order in the robots array (0-based).
|
||||
enemyIdx = 0;
|
||||
shooterIdx = 1;
|
||||
idxToName.put(0, enSpec.getName());
|
||||
idxToName.put(1, shSpec.getName());
|
||||
|
||||
BattleSpecification spec = new BattleSpecification(
|
||||
rounds, // numRounds
|
||||
450L, // inactivity time (turns)
|
||||
0.1, // gun cooling rate
|
||||
new BattlefieldSpecification(800, 600),
|
||||
chosen);
|
||||
|
||||
long t0 = System.currentTimeMillis();
|
||||
engine.runBattle(spec, true);
|
||||
engine.waitTillBattleOver();
|
||||
engine.close();
|
||||
long dt = System.currentTimeMillis() - t0;
|
||||
System.out.println("[capture] battle done in " + dt + " ms, rows=" + rows.size());
|
||||
|
||||
// robust: verify index mapping by re-reading robot names once more is not possible; rely on order.
|
||||
|
||||
// Write JSONL
|
||||
File of = new File(outFile);
|
||||
of.getParentFile().mkdirs();
|
||||
try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(of)))) {
|
||||
StringBuilder meta = new StringBuilder();
|
||||
meta.append("{\"meta\":{");
|
||||
meta.append("\"adversary\":\"").append(label).append("\",");
|
||||
meta.append("\"source\":\"classic-robocode\",");
|
||||
meta.append("\"perfect_info\":true,");
|
||||
meta.append("\"arena\":{\"w\":800,\"h\":600},");
|
||||
meta.append("\"note\":\"").append(escape(note)).append("\"");
|
||||
meta.append("}}");
|
||||
out.println(meta);
|
||||
for (Row r : rows) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"tick\":").append(r.tick);
|
||||
sb.append(",\"ex\":").append(f(r.ex));
|
||||
sb.append(",\"ey\":").append(f(r.ey));
|
||||
sb.append(",\"eh\":").append(f(r.eh));
|
||||
sb.append(",\"es\":").append(f(r.es));
|
||||
sb.append(",\"ee\":").append(f(r.ee));
|
||||
sb.append(",\"sx\":").append(f(r.sx));
|
||||
sb.append(",\"sy\":").append(f(r.sy));
|
||||
sb.append(",\"sh\":").append(f(r.sh));
|
||||
sb.append(",\"ss\":").append(f(r.ss));
|
||||
sb.append(",\"se\":").append(f(r.se));
|
||||
sb.append("}");
|
||||
out.println(sb);
|
||||
}
|
||||
}
|
||||
System.out.println("[capture] wrote " + of.length() + " bytes to " + of.getAbsolutePath());
|
||||
|
||||
// Sidecar with round boundaries (for statistics; not part of the shared fixture format).
|
||||
try (PrintWriter rs = new PrintWriter(new BufferedWriter(new FileWriter(outFile + ".rounds.json")))) {
|
||||
rs.print("{\"rounds\":[");
|
||||
int start = 0;
|
||||
boolean first = true;
|
||||
for (Map.Entry<Integer, Integer> e : roundTicks.entrySet()) {
|
||||
if (!first) rs.print(",");
|
||||
first = false;
|
||||
rs.print("{\"round\":" + e.getKey() + ",\"startTick\":" + start + ",\"count\":" + e.getValue() + "}");
|
||||
start += e.getValue();
|
||||
}
|
||||
rs.println("]}");
|
||||
}
|
||||
System.out.println("[capture] rounds ticks: " + roundTicks);
|
||||
if (!errors.isEmpty()) {
|
||||
System.out.println("[capture] errors (" + errors.size() + "):");
|
||||
for (String e : errors) System.out.println(" " + e);
|
||||
}
|
||||
// exit code 0 only if we captured something
|
||||
System.exit(rows.isEmpty() ? 4 : 0);
|
||||
}
|
||||
|
||||
static String escape(String s) {
|
||||
return s.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# robocode_fixture_capture
|
||||
|
||||
Reproduces the classic-Robocode **DrussGT movement fixtures** in
|
||||
`tools/fixtures/` (see `tools/fixtures/DRUSSGT_FIXTURES.md` for the data,
|
||||
conversion and caveats).
|
||||
|
||||
## Run it
|
||||
|
||||
```sh
|
||||
tools/robocode_fixture_capture/capture.sh # writes to tools/fixtures
|
||||
tools/robocode_fixture_capture/capture.sh /tmp/out # or another directory
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
1. downloads classic Robocode 1.9.5.5 from SourceForge into `/tmp/robocode/`
|
||||
(its "setup jar" is a plain zip of the full install),
|
||||
2. downloads the third-party `DrussGT.jar` from the Robocode archive,
|
||||
3. builds the trivial `trivial.StraightLine` contrast bot as a jar,
|
||||
4. compiles `Capture.java` and runs the headless battles,
|
||||
5. writes `*.jsonl` fixtures plus `drussgt_meta/*.rounds.json` sidecars.
|
||||
|
||||
**The Robocode distribution and the DrussGT jar are third-party
|
||||
redistributables and are intentionally kept out of the repository** — they live
|
||||
only under `/tmp/robocode/`. Nothing under `/tmp` is committed.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `capture.sh` | download + build + run everything |
|
||||
| `Capture.java` | headless `RobocodeEngine` observer that dumps per-turn state to JSONL |
|
||||
| `StraightLine.java` / `StraightLine.properties` | trivial constant-speed contrast bot |
|
||||
| `analyze.py` | conversion validation + movement statistics |
|
||||
|
||||
## Running the engine headlessly
|
||||
|
||||
Robocode 1.9.5.5 installs a `SecurityManager` and reflects into
|
||||
`sun.net.www.protocol.jar`. On Java 21 both are blocked by default, so the JVM
|
||||
needs (this is what `capture.sh` uses):
|
||||
|
||||
```
|
||||
-Djava.security.manager=allow
|
||||
--add-opens=java.base/sun.net.www.protocol.jar=ALL-UNNAMED
|
||||
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED
|
||||
--add-opens=java.desktop/sun.awt=ALL-UNNAMED
|
||||
```
|
||||
|
||||
Java 17/11 work with fewer flags (the `--add-opens` for `sun.net` is still
|
||||
required on 17+).
|
||||
|
||||
## Key API gotchas found while building this
|
||||
|
||||
- `RobocodeEngine.getLocalRepository(name)` requires the **full class name**
|
||||
(`jk.mega.DrussGT`, not `DrussGT`). A robot added as loose `.class` files is
|
||||
indexed but marked unusable (`trivial.StraightLine*`) and cannot be selected;
|
||||
packaging it as a **jar** with a `robots:` manifest entry fixes it.
|
||||
Delete `robots/robot.database` to force a repository rescan.
|
||||
- `IRobotSnapshot.getBodyHeading()` / `getGunHeading()` / `getRadarHeading()`
|
||||
return **radians** (classic convention: 0=north, clockwise) — the rest of the
|
||||
classic robot API uses degrees. `getVelocity()` is signed px/turn.
|
||||
See the conversion in `tools/fixtures/DRUSSGT_FIXTURES.md`.
|
||||
|
||||
## Validate / analyse
|
||||
|
||||
```sh
|
||||
python3 tools/robocode_fixture_capture/analyze.py \
|
||||
tools/fixtures/drussgt_vs_*.jsonl tools/fixtures/contrast_*.jsonl
|
||||
```
|
||||
|
||||
`analyze.py` reports the coordinate-conversion check (mean angular error between
|
||||
recorded displacement and the direction implied by `eh`+`es`; expect 0.000°),
|
||||
speed / heading-change distributions, range statistics and the
|
||||
perpendicular-vs-radial split.
|
||||
@@ -0,0 +1,19 @@
|
||||
package trivial;
|
||||
|
||||
import robocode.Robot;
|
||||
|
||||
/**
|
||||
* Deliberately trivial reference opponent for fixture contrast.
|
||||
* Moves in straight legs at constant speed; turns 180 degrees in place at walls.
|
||||
* Never fires, never scans deliberately.
|
||||
*/
|
||||
public class StraightLine extends Robot {
|
||||
public void run() {
|
||||
// normalise body heading to east (classic Robocode heading 90 == east)
|
||||
turnRight(90 - getHeading());
|
||||
while (true) {
|
||||
ahead(100000); // straight leg until blocked by a wall
|
||||
turnRight(180); // reverse in place
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
robot.classname=trivial.StraightLine
|
||||
robot.name=StraightLine
|
||||
robot.version=1.0
|
||||
robot.author.name=fixture-contrast
|
||||
robot.description=Trivial straight-line contrast bot
|
||||
robocode.version=1.9.5.5
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Analyze classic-Robocode fixture JSONL + validate the heading conversion.
|
||||
|
||||
Conversion claimed: tank_heading = (90 - classic_heading) mod 360
|
||||
Since classic Robocode x/y axes match Tank Royale (origin bottom-left, x right,
|
||||
y up), position deltas between ticks should align with the reported heading.
|
||||
"""
|
||||
import json, sys, math, statistics as st
|
||||
|
||||
def load(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 = []
|
||||
import os
|
||||
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 = None
|
||||
return rows, rounds
|
||||
|
||||
def wrap180(a):
|
||||
return ((a + 180.0) % 360.0) - 180.0
|
||||
|
||||
def unit(deg):
|
||||
r = math.radians(deg)
|
||||
return math.cos(r), math.sin(r)
|
||||
|
||||
def analyze(path):
|
||||
rows, rounds = load(path)
|
||||
# boundary ticks (first tick of a round)
|
||||
starts = set()
|
||||
if rounds:
|
||||
for r in rounds:
|
||||
starts.add(r["startTick"])
|
||||
else:
|
||||
# fallback heuristic: big jump
|
||||
for i in range(1, len(rows)):
|
||||
dx = rows[i]["ex"] - rows[i-1]["ex"]
|
||||
dy = rows[i]["ey"] - rows[i-1]["ey"]
|
||||
if math.hypot(dx, dy) > 20:
|
||||
starts.add(rows[i]["tick"])
|
||||
|
||||
n = len(rows)
|
||||
speeds = [abs(r["es"]) for r in rows]
|
||||
signed = [r["es"] for r in rows]
|
||||
heading_changes = []
|
||||
dists = []
|
||||
perp_sin = []
|
||||
radial_cos = []
|
||||
disp_err = [] # angular error between displacement dir and reported movement dir
|
||||
disp_err_next = []
|
||||
moving_ticks = 0
|
||||
|
||||
for i, r in enumerate(rows):
|
||||
d = math.hypot(r["ex"] - r["sx"], r["ey"] - r["sy"])
|
||||
dists.append(d)
|
||||
if i == 0 or rows[i]["tick"] in starts:
|
||||
continue
|
||||
p = rows[i-1]
|
||||
dh = wrap180(r["eh"] - p["eh"])
|
||||
heading_changes.append(abs(dh))
|
||||
dx = r["ex"] - p["ex"]
|
||||
dy = r["ey"] - p["ey"]
|
||||
step = math.hypot(dx, dy)
|
||||
v = r["es"]
|
||||
if abs(v) < 0.5 or step < 0.2:
|
||||
continue
|
||||
moving_ticks += 1
|
||||
# movement direction implied by heading + velocity sign (tank convention)
|
||||
mu = r["eh"] if v >= 0 else r["eh"] + 180.0
|
||||
# radial direction from enemy to shooter
|
||||
theta = math.degrees(math.atan2(r["sy"] - r["ey"], r["sx"] - r["ex"]))
|
||||
delta = math.radians(mu - theta)
|
||||
perp_sin.append(abs(math.sin(delta)))
|
||||
radial_cos.append(abs(math.cos(delta)))
|
||||
# conversion validation: displacement direction vs movement dir
|
||||
phi = math.degrees(math.atan2(dy, dx))
|
||||
disp_err.append(abs(wrap180(phi - mu)))
|
||||
phi_prev = math.degrees(math.atan2(dy, dx))
|
||||
mu_prev = p["eh"] if p["es"] >= 0 else p["eh"] + 180.0
|
||||
disp_err_next.append(abs(wrap180(phi_prev - mu_prev)))
|
||||
|
||||
def pct(x, p):
|
||||
if not x:
|
||||
return float("nan")
|
||||
s = sorted(x)
|
||||
k = min(len(s)-1, int(round((p/100.0)*(len(s)-1))))
|
||||
return s[k]
|
||||
|
||||
def hist(x, edges):
|
||||
tot = len(x)
|
||||
out = []
|
||||
for lo, hi in zip(edges[:-1], edges[1:]):
|
||||
c = sum(1 for v in x if lo <= v < hi)
|
||||
out.append((f"[{lo},{hi})", c/tot if tot else 0.0))
|
||||
return out
|
||||
|
||||
res = {
|
||||
"path": path, "n": n,
|
||||
"speed_hist": hist(speeds, [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 7.5, 8.01]),
|
||||
"speed_mean": st.mean(speeds), "speed_median": st.median(speeds),
|
||||
"speed_std": (st.pstdev(speeds) if n > 1 else 0.0),
|
||||
"speed_p90": pct(speeds, 90),
|
||||
"frac_rest": sum(1 for v in speeds if v < 0.5)/n,
|
||||
"frac_full": sum(1 for v in speeds if v >= 7.5)/n,
|
||||
"frac_neg": sum(1 for v in signed if v < -0.5)/n,
|
||||
"hc_mean": st.mean(heading_changes) if heading_changes else float("nan"),
|
||||
"hc_median": st.median(heading_changes) if heading_changes else float("nan"),
|
||||
"hc_p90": pct(heading_changes, 90), "hc_p99": pct(heading_changes, 99),
|
||||
"hc_max": max(heading_changes) if heading_changes else float("nan"),
|
||||
"frac_hc_lt1": (sum(1 for v in heading_changes if v < 1.0)/len(heading_changes)) if heading_changes else float("nan"),
|
||||
"frac_hc_gt5": (sum(1 for v in heading_changes if v > 5.0)/len(heading_changes)) if heading_changes else float("nan"),
|
||||
"hc_hist": hist(heading_changes, [0, 0.5, 1, 2, 3, 5, 7, 10, 11, 180.1]),
|
||||
"dist_mean": st.mean(dists), "dist_median": st.median(dists),
|
||||
"dist_p10": pct(dists, 10), "dist_p90": pct(dists, 90),
|
||||
"moving_ticks": moving_ticks,
|
||||
"perp_frac": (sum(1 for v in perp_sin if v > 0.866)/len(perp_sin)) if perp_sin else float("nan"),
|
||||
"radial_frac": (sum(1 for v in radial_cos if v > 0.866)/len(radial_cos)) if radial_cos else float("nan"),
|
||||
"mean_abs_sin": st.mean(perp_sin) if perp_sin else float("nan"),
|
||||
"mean_abs_cos": st.mean(radial_cos) if radial_cos else float("nan"),
|
||||
"conv_err_same_tick_mean": st.mean(disp_err) if disp_err else float("nan"),
|
||||
"conv_err_prev_tick_mean": st.mean(disp_err_next) if disp_err_next else float("nan"),
|
||||
"conv_err_same_tick_median": st.median(disp_err) if disp_err else float("nan"),
|
||||
}
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
for path in sys.argv[1:]:
|
||||
r = analyze(path)
|
||||
print("="*100)
|
||||
print(r["path"], "ticks=", r["n"], "movingTicks=", r["moving_ticks"])
|
||||
print(f" conversion check: mean |angle(displacement) - movementdir| = {r['conv_err_same_tick_mean']:.3f} deg "
|
||||
f"(median {r['conv_err_same_tick_median']:.3f}); using prev-tick heading {r['conv_err_prev_tick_mean']:.3f}")
|
||||
print(f" SPEED: mean={r['speed_mean']:.3f} median={r['speed_median']:.3f} std={r['speed_std']:.3f} p90={r['speed_p90']:.2f} "
|
||||
f"rest(<0.5)={r['frac_rest']:.3f} full(>=7.5)={r['frac_full']:.3f} neg={r['frac_neg']:.3f}")
|
||||
print(" speed hist:", " ".join(f"{k}:{v:.3f}" for k,v in r["speed_hist"]))
|
||||
print(f" HEADING CHANGE: mean={r['hc_mean']:.3f} median={r['hc_median']:.3f} p90={r['hc_p90']:.2f} p99={r['hc_p99']:.2f} max={r['hc_max']:.2f} "
|
||||
f"|d|<1={r['frac_hc_lt1']:.3f} |d|>5={r['frac_hc_gt5']:.3f}")
|
||||
print(" heading-change hist:", " ".join(f"{k}:{v:.3f}" for k,v in r["hc_hist"]))
|
||||
print(f" DISTANCE: mean={r['dist_mean']:.1f} median={r['dist_median']:.1f} p10={r['dist_p10']:.1f} p90={r['dist_p90']:.1f}")
|
||||
print(f" LATERAL/RADIAL: perpFrac={r['perp_frac']:.3f} radialFrac={r['radial_frac']:.3f} "
|
||||
f"mean|sin|={r['mean_abs_sin']:.3f} mean|cos|={r['mean_abs_cos']:.3f}")
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Reproduce the classic-Robocode DrussGT movement fixtures in tools/fixtures/.
|
||||
#
|
||||
# This script downloads classic Robocode 1.9.5.5 and the third-party DrussGT jar
|
||||
# into /tmp (NOT into the repository), runs headless battles with the Capture
|
||||
# observer, and writes JSONL fixtures + round-boundary sidecars.
|
||||
#
|
||||
# Requirements: bash, curl, unzip, javac/java (Java 11+; tested on Java 21 with
|
||||
# -Djava.security.manager=allow and --add-opens flags below).
|
||||
#
|
||||
# Usage: tools/robocode_fixture_capture/capture.sh [output_dir]
|
||||
# (default output_dir = tools/fixtures)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO="$(cd "$HERE/../.." && pwd)"
|
||||
OUT="${1:-$REPO/tools/fixtures}"
|
||||
WORK=/tmp/robocode
|
||||
INSTALL="$WORK/install"
|
||||
|
||||
ROBOCODE_VER=1.9.5.5
|
||||
ROBOCODE_SETUP_URL="https://sourceforge.net/projects/robocode/files/robocode/$ROBOCODE_VER/robocode-$ROBOCODE_VER-setup.jar/download"
|
||||
DRUSSGT_URL="http://robocode-archive.strangeautomata.com/robots/jk.mega.DrussGT_3.1.4159.jar"
|
||||
|
||||
mkdir -p "$WORK/dl" "$WORK/work" "$OUT"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Classic Robocode (installer jar is a plain zip with the full install)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ ! -d "$INSTALL/libs" ]; then
|
||||
echo "[capture] downloading classic Robocode $ROBOCODE_VER from SourceForge"
|
||||
curl -sSL -o "$WORK/dl/robocode-$ROBOCODE_VER-setup.jar" "$ROBOCODE_SETUP_URL"
|
||||
unzip -q -o "$WORK/dl/robocode-$ROBOCODE_VER-setup.jar" -d "$INSTALL"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. DrussGT (third-party, keep OUT of git)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ ! -f "$WORK/dl/DrussGT.jar" ]; then
|
||||
echo "[capture] downloading DrussGT 3.1.4159"
|
||||
curl -sSL -o "$WORK/dl/DrussGT.jar" "$DRUSSGT_URL"
|
||||
fi
|
||||
cp -f "$WORK/dl/DrussGT.jar" "$INSTALL/robots/jk.mega.DrussGT_3.1.4159.jar"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Trivial contrast bot, packaged as a jar so the repository indexes it
|
||||
# ---------------------------------------------------------------------------
|
||||
rm -rf "$WORK/work/trivialjar"
|
||||
mkdir -p "$WORK/work/trivialjar/trivial"
|
||||
javac -cp "$INSTALL/libs/robocode.jar" -d "$WORK/work/trivialjar" "$HERE/StraightLine.java"
|
||||
cp "$HERE/StraightLine.properties" "$WORK/work/trivialjar/trivial/StraightLine.properties"
|
||||
printf 'Manifest-Version: 1.0\nrobots: trivial.StraightLine\n\n' > "$WORK/work/trivialjar/MANIFEST.MF"
|
||||
( cd "$WORK/work/trivialjar" && jar cfm "$INSTALL/robots/trivial.StraightLine.jar" \
|
||||
MANIFEST.MF trivial/StraightLine.class trivial/StraightLine.properties )
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Capture program
|
||||
# ---------------------------------------------------------------------------
|
||||
javac -cp "$INSTALL/libs/*" -d "$WORK/work" "$HERE/Capture.java"
|
||||
rm -f "$INSTALL/robots/robot.database" # force repository re-scan
|
||||
|
||||
run_capture() { # enemy shooter rounds out label note
|
||||
echo "[capture] $1 vs $2 ($3 rounds) -> $4"
|
||||
( cd "$INSTALL" && java -Djava.security.manager=allow \
|
||||
--add-opens=java.base/sun.net.www.protocol.jar=ALL-UNNAMED \
|
||||
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
|
||||
--add-opens=java.desktop/sun.awt=ALL-UNNAMED \
|
||||
-cp "$WORK/work:$INSTALL/libs/*" -Xmx512M \
|
||||
Capture "$1" "$2" "$3" "$4" "$5" "$6" ) | grep -E 'rows=|rounds ticks|errors' || true
|
||||
}
|
||||
|
||||
NOTE_D="e*=DrussGT 3.1.4159 (jk.mega.DrussGT) on classic Robocode 1.9.5.5; s*=opponent. tick is global/continuous across all rounds, so positions jump at round boundaries (see drussgt_meta/*.rounds.json sidecar). es/ss are SIGNED velocity (classic getVelocity, px/tick; negative = reversing). Heading conversion: classic snapshot headings are radians (0=north, clockwise) -> deg=toDegrees(raw); tank=(90-deg) mod 360 (0=east, CCW). Positions x/y are unchanged (same axes). Captured headless via RobocodeEngine TurnEndedEvent (perfect info, no fog)."
|
||||
|
||||
run_capture jk.mega.DrussGT sample.SpinBot 20 "$OUT/drussgt_vs_spinbot.jsonl" DrussGT "$NOTE_D"
|
||||
run_capture jk.mega.DrussGT sample.RamFire 20 "$OUT/drussgt_vs_ramfire.jsonl" DrussGT "$NOTE_D"
|
||||
run_capture jk.mega.DrussGT sample.Crazy 20 "$OUT/drussgt_vs_crazy.jsonl" DrussGT "$NOTE_D"
|
||||
run_capture jk.mega.DrussGT sample.Corners 20 "$OUT/drussgt_vs_corners.jsonl" DrussGT "$NOTE_D"
|
||||
run_capture jk.mega.DrussGT jk.mega.DrussGT 2 "$OUT/drussgt_vs_drussgt.jsonl" DrussGT "$NOTE_D"
|
||||
run_capture sample.SittingDuck sample.SittingDuck 1 "$OUT/contrast_stationary_sittingduck.jsonl" SittingDuck \
|
||||
"e*=sample.SittingDuck (stationary). s*=sample.SittingDuck. Contrast fixture: zero movement by construction."
|
||||
run_capture trivial.StraightLine sample.SittingDuck 1 "$OUT/contrast_straightline.jsonl" StraightLine \
|
||||
"e*=trivial.StraightLine (constant-speed straight legs, 180-degree turns at walls). s*=sample.SittingDuck. Contrast fixture."
|
||||
|
||||
# Move sidecars out of the *.jsonl namespace
|
||||
mkdir -p "$OUT/drussgt_meta"
|
||||
for f in "$OUT"/*.jsonl.rounds.json; do
|
||||
[ -e "$f" ] && mv -f "$f" "$OUT/drussgt_meta/"
|
||||
done
|
||||
|
||||
echo "[capture] done. Validate with:"
|
||||
echo " python3 $HERE/analyze.py $OUT/drussgt_vs_*.jsonl $OUT/contrast_*.jsonl"
|
||||
Reference in New Issue
Block a user