docs(research): portability audit of DrussGT 3.1.4159
Measured hazard counts for a Java->Nim port, with the correction that only 22 of 28 top-level classes ship source (6 do not: 5 in the gun package plus dMove/Scan), so a full port would need a decompiler while a shim would not care at all. Real traps: 193 float / 35 casts / 147 literals / 60 float[] in the movement closure (the danger histogram is float[171] -- porting 32-bit Java floats to Nim's default float64 diverges silently); ~68 non-final statics; and 4 Java single-& sites with side effects, which break under Nim's short-circuiting 'and'. Non-issues, correcting earlier assumptions: 0 sites of %-on-negative (angle normalisation is floor-based) and FastTrig has no lookup tables, it is 7 coefficient-exact polynomials. Movement scoping: 5007 LOC across 14 files, ~4.8-6.1k Nim LOC, 4-8 focused agent-days to first-compiles. It can be ported WITHOUT the gun (data flows movement->gun only), but the harness never routes HitByBullet into movement modules, so the danger bins would never train -- that plumbing is the real blocker, not the translation.
This commit is contained in:
@@ -0,0 +1,720 @@
|
|||||||
|
# DrussGT 3.1.4159 → Nim portability audit
|
||||||
|
|
||||||
|
**Issue:** N/A (read-only portability audit requested by orchestrator)
|
||||||
|
**Date:** 2026-09-20
|
||||||
|
**Subject:** DrussGT 3.1.4159 (Java, classic Robocode), `public class DrussGT extends AdvancedRobot`
|
||||||
|
**Source analysed:** `/tmp/drussgt/src` → copied to `/tmp/portaudit/src` before analysis (so the
|
||||||
|
analysis could not collide with any other agent). 22 `.java` files, **7372 lines** total.
|
||||||
|
Entry point `jk/mega/DrussGT.java`.
|
||||||
|
|
||||||
|
**Method / legend.**
|
||||||
|
- `[MEASURED]` = a count I actually took in the source (grep/script over `/tmp/portaudit/src`); every
|
||||||
|
such row carries a `file:line` witness or names the exact command.
|
||||||
|
- `[ESTIMATE]` = engineering judgment, explicitly labelled. Never presented as measured.
|
||||||
|
- `[EXTERNAL]` = depends on classic-Robocode library semantics that are **not** in this source tree
|
||||||
|
and cannot be verified offline from the supplied input (e.g. `robocode.util.Utils`).
|
||||||
|
- Per the task constraints I did **not** build, run nim/nimble, or download anything. The
|
||||||
|
Robocode/DrussGT jars already present in `/tmp/robocode/` were only *listed*.
|
||||||
|
|
||||||
|
Where this report quotes DrussGT, it quotes `/tmp/portaudit/src/jk/...`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Verdict in one paragraph
|
||||||
|
|
||||||
|
A **behaviourally exact** port is impossible for the reasons already established (different engine
|
||||||
|
tick ordering and physics: classic Robocode is `setAhead`-distance-based, Tank Royale's harness is
|
||||||
|
`speed`/`turnRate`-based; see §A.3). An **algorithmically faithful** port of the movement subsystem
|
||||||
|
is achievable and is the right first target, because the movement is self-contained: it consumes
|
||||||
|
engine state + hit events and exports future-position data *to* the gun, never the reverse
|
||||||
|
(§A.4). It is **large** — the mandatory movement file set is **5007 lines of Java** [MEASURED]
|
||||||
|
(§A.1), of which ~2100 lines are the `DrussMoveGT` core. The dominant fidelity risk is not `%`
|
||||||
|
or reflection; it is **`float` (32-bit) state** (193 `float` occurrences in movement, 35 `(float)`
|
||||||
|
casts there; 195/37 tree-wide) and
|
||||||
|
**table-exact data** (`BufferManager`: ~240 hand-written buffer-set rows / 22 slice tables [MEASURED])
|
||||||
|
plus a **~5000-line KNN/wave-danger engine** that must be ported rather than replaced (stylistically
|
||||||
|
similar to the existing `common_libs/guns/knn_gun.nim`, which already reimplements a reduced version
|
||||||
|
of the DC gun). No reflection, threads, lambdas, anonymous classes, JNI or serialization stand in the
|
||||||
|
way. Recommended validation is a **hybrid**: golden-value unit tests on the trap-prone pure functions
|
||||||
|
(cheapest) **plus** replay of the already-captured classic-Robocode DrussGT fixtures
|
||||||
|
(`tools/fixtures/`) to compare decisions, not trajectories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Source inventory `[MEASURED]`
|
||||||
|
|
||||||
|
`find /tmp/portaudit/src -name '*.java' -exec wc -l {} +` and per-file `wc -l`:
|
||||||
|
|
||||||
|
| File | Lines | Role |
|
||||||
|
|---|---:|---|
|
||||||
|
| `jk/mega/dMove/DrussMoveGT.java` | 2283 | **movement core** (wave surfer, KNN buffer orchestration) |
|
||||||
|
| `jk/mega/dGun/DrussGunDC.java` | 1352 | gun |
|
||||||
|
| `jk/mega/BufferManager.java` | 652 | movement stat/flattener buffers + feature slice tables |
|
||||||
|
| `jk/tree/KDTree.java` | 617 | KD-tree (used by gun **and** movement enemy model) |
|
||||||
|
| `jk/precise/EnergyDomeWorker.java` | 462 | "EnergyDome" anti-gun shield (optional subsystem) |
|
||||||
|
| `jk/mega/dGun/PreciseMinMaxGFs.java` | 266 | precise MEA geometry — **used by movement** |
|
||||||
|
| `jk/mega/EnemyMoves.java` | 264 | enemy-movement predictor (KD-tree of motion chains) |
|
||||||
|
| `jk/mega/dMove/EnemyWave.java` | 236 | enemy bullet wave, shadows, GF factor index |
|
||||||
|
| `jk/mega/dMove/MovePredictor.java` | 228 | our own tick-by-tick movement simulator |
|
||||||
|
| `jk/math/FastTrig.java` | 193 | polynomial trig (no tables) |
|
||||||
|
| `jk/mega/Insulter.java` | 162 | random insults (cosmetic) |
|
||||||
|
| `jk/precise/util/PreciseUtils.java` | 155 | circle/rect intersection for precise waves |
|
||||||
|
| `jk/melee/MeleeRadar.java` | 94 | radar |
|
||||||
|
| `jk/mega/BulletPowerPredictor.java` | 50 | KD-tree bullet-power predictor |
|
||||||
|
| `jk/mega/dMove/PredictionStatus.java` | 27 | prediction result struct |
|
||||||
|
| `jk/mega/dMove/PlaceTime.java` | 18 | (place,time,danger) struct |
|
||||||
|
| `jk/mega/dMove/BulletTracker.java` | 12 | our-bullet tracking for shadows |
|
||||||
|
| `jk/mega/DataToGun.java` | 11 | movement→gun struct |
|
||||||
|
| `jk/mega/dGun/GFRange.java` | 8 | gun helper (duplicated in `DrussGunDC.java:729`) |
|
||||||
|
| `jk/mega/dGun/Indice.java` | 9 | gun helper (duplicated in `DrussGunDC.java:1207`) |
|
||||||
|
| `jk/precise/util/PreciseWave.java` | 6 | precise wave struct |
|
||||||
|
| `jk/mega/DrussGT.java` | 267 | bot entry / event dispatch |
|
||||||
|
| **Total** | **7372** | |
|
||||||
|
|
||||||
|
Comment/blank lines: **1718 / 7372 (23%)** [MEASURED]; `DrussMoveGT.java` alone is **564 comment/blank
|
||||||
|
of 2283 (25%)** [MEASURED].
|
||||||
|
|
||||||
|
**Decompilation artefact / incomplete-gun warning `[MEASURED]`.** The tree also contains `.class`
|
||||||
|
files. `DrussMoveGT.java` ends with a second top-level class `Scan` (line 2187) and `DrussGunDC.java`
|
||||||
|
declares `DCRobotState`/`GFRange`/`StoreScan`/`DCWave`/`Indice`/`NormalDistribution`/`JKDCUtils`
|
||||||
|
inline (lines 587/729/737/745/1207/1216/1222). Consequently `GFRange.java` and `Indice.java` are
|
||||||
|
**duplicate class definitions** — the tree does **not** compile as-is. This affects the gun only:
|
||||||
|
movement's required files all exist (movement references only `DataToGun`, `PreciseMinMaxGFs` and
|
||||||
|
never `JKDCUtils`/`DCWave`/`StoreScan`/`DCRobotState`, per
|
||||||
|
`grep -rnE 'JKDCUtils|DCRobotState|DCWave|StoreScan|NormalDistribution' --include='*.java' | grep -v DrussGunDC` → no hits outside `DrussGunDC.java`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Hazard counts table (the summary the task asked for)
|
||||||
|
|
||||||
|
| # | Hazard | Count `[MEASURED]` | Where | Severity for DrussGT |
|
||||||
|
|---|---|---|---:|---|
|
||||||
|
| 1 | `float` occurrences (movement only) | **193** (115 `DrussMoveGT`, 67 `BufferManager`, 9 `EnemyWave`, 1 `PlaceTime`, 1 `BulletPowerPredictor`) | see §3.1 | **HIGH (fatal for exactness)** |
|
||||||
|
| 1b | `(float)` casts (movement) | **35** (31 `DrussMoveGT`, 4 `EnemyWave`) | §3.1 | HIGH |
|
||||||
|
| 1c | `float[]` / `float[][]` / `float[][][]` decls | **60 / 10 / 9** | §3.1 | HIGH |
|
||||||
|
| 1d | `float` literals (whole tree) | **147** (133 `BufferManager`, 13 `DrussMoveGT`, 1 `FastTrig`) | §3.1 | HIGH |
|
||||||
|
| 1e | `double` occurrences (whole tree) | 685 lines | — | baseline: >3× float |
|
||||||
|
| 2 | `%` modulo total | **7** (4 arithmetic, 3 string-format) | §3.2 | **LOW** |
|
||||||
|
| 2b | `%` on possibly-negative operands | **0** | §3.2 | none |
|
||||||
|
| 2c | `Math.floor`-based angle normalisers | 2 functions (`FastTrig.normalRelativeAngle`/`normalAbsoluteAngle`, many call sites) | §3.2 | LOW — already floor-mod semantics |
|
||||||
|
| 3 | `static` non-`final` fields | **~68** total; **27** in `DrussMoveGT` | §3.3 | MEDIUM |
|
||||||
|
| 3b | `static final` primitives/constants | many (harmless) | §3.3 | none |
|
||||||
|
| 3c | `static final` **arrays with mutable contents** | 2 (`narrowprofile`, `wideprofile`) | §3.3 | LOW-MED |
|
||||||
|
| 4 | Table-driven math (`FastTrig`) | **0 tables** — 7 polynomial/rational approximations (sin/cos/tan/asin/acos/atan + Chebyshev) | §3.4 | **HIGH** (must be coefficient-exact) |
|
||||||
|
| 4b | Hand-written data tables (`BufferManager`) | 22 slice tables / ~240 buffer-set rows | §3.4 | HIGH (must be data-exact) |
|
||||||
|
| 5 | `>>>` unsigned shift | **1** (`KDTree.java:545`, non-negative operands) | §3.5 | LOW |
|
||||||
|
| 5b | `<<`, `>>`, `^` | 0 / 0 / 0 | §3.5 | none |
|
||||||
|
| 5c | single `&` / single `|` (non-short-circuit boolean) | **34 / 11** | §3.5 | MEDIUM (see side-effect sites) |
|
||||||
|
| 5d | `&`/`|` operands with side effects (`--`, `--`) | **4** | §3.5 | **MEDIUM-HIGH trap** |
|
||||||
|
| 5e | `(int)` casts | **137** | §3.5 | LOW-MED |
|
||||||
|
| 6 | `java.util`: `ArrayList` occurrences | 136 | §3.6 | LOW (→ `seq`) |
|
||||||
|
| 6b | `Hashtable` / `Vector` | 2 / 1 (`MeleeRadar`, `onDeath`) | §3.6 | LOW |
|
||||||
|
| 6c | `HashMap`/`TreeMap`/`HashSet`/`TreeSet`/`LinkedList` | **0** | §3.6 | none |
|
||||||
|
| 6d | `Collections.sort` / `Arrays.*` | 2 / 6 | §3.6 | LOW |
|
||||||
|
| 6e | `Comparable` / `Comparator` impls | 5 / 1 | §3.6 | LOW |
|
||||||
|
| 6f | anonymous classes / lambdas | **0 / 0** | §3.6 | none |
|
||||||
|
| 6g | `java.awt.geom.Point2D` / `Rectangle2D` | 268 / 7 | §3.6 | LOW |
|
||||||
|
| 6h | `java.io` active persistence | 2 sites, both best-effort error logging (`logData=false`) | §3.6 | LOW |
|
||||||
|
| 7 | reflection / JNI / `System.load` / serialization / dynamic class loading | **0** | §3.7 | none |
|
||||||
|
| 7b | threads / `synchronized` | 1 (`DrussGT.java:79 Thread.sleep(0,1000)`) | §3.7 | LOW |
|
||||||
|
| 7c | `SkippedTurnEvent` reliance | 1 method, prints only | §3.7 | LOW |
|
||||||
|
| 7d | engine-timing semantics (`getTime`, gun-cooling rate, `getOthers`) | pervasive | §3.7 | MEDIUM |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Hazard-by-hazard detail
|
||||||
|
|
||||||
|
### 3.1 `float` vs `double` — **the single biggest fidelity risk** `[MEASURED]`
|
||||||
|
|
||||||
|
Counts (occurrences, not lines), from `grep -oE '\bfloat\b' | wc -l` per file:
|
||||||
|
|
||||||
|
| File | `float` tokens | `(float)` casts | `float` literals |
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
| `jk/mega/dMove/DrussMoveGT.java` | 115 | 31 | 13 |
|
||||||
|
| `jk/mega/BufferManager.java` | 67 | 0 | 133 |
|
||||||
|
| `jk/mega/dMove/EnemyWave.java` | 9 | 4 | 0 |
|
||||||
|
| `jk/mega/BulletPowerPredictor.java` | 1 | 0 | 0 |
|
||||||
|
| `jk/mega/dMove/PlaceTime.java` | 1 | 0 | 0 |
|
||||||
|
| `jk/mega/dGun/DrussGunDC.java` | 2 | 2 | 0 |
|
||||||
|
| `jk/math/FastTrig.java` | 0 | 0 | 1 |
|
||||||
|
| **Total** | **195** | **37** | **147** |
|
||||||
|
|
||||||
|
Array declarations: `float[]` × 60, `float[][]` × 10, `float[][][]` × 9 [MEASURED].
|
||||||
|
|
||||||
|
Why this is potentially fatal for a wave surfer: the **entire danger histogram and the KNN feature
|
||||||
|
vector are 32-bit**.
|
||||||
|
|
||||||
|
- `EnemyWave.BINS = 171`, `MIDDLE_BIN = 85` (`EnemyWave.java:41-42`). `bestBins` and `binCleared`
|
||||||
|
are `float[171]` (`EnemyWave.java:20-21`).
|
||||||
|
- `DrussMoveGT.getDanger(...)` returns `float` (`DrussMoveGT.java:1509`); `PlaceTime.danger` is
|
||||||
|
`float` (`PlaceTime.java:13`) and the candidate sort orders by it (`PlaceTime.java:16-17`).
|
||||||
|
- The KNN features are built as `float` and looked up against `float[]` slice boundaries:
|
||||||
|
`lastLatVel`, `advVel`, `BFT`, `tsdirchange`, `accel`, `tsvchange`, `dl10`, `forwardWall`,
|
||||||
|
`reverseWall` (`DrussMoveGT.java:320-357`, duplicated for the imaginary/flattener waves at
|
||||||
|
453-490 and 574-611) → `BufferManager.getIndexes(...)` (`BufferManager.java:452`) →
|
||||||
|
`getIndex(float[] slices, float value)` with `value >= slices[index]` (`BufferManager.java:513-517`).
|
||||||
|
- `BufferManager` holds 22 `static final float[]` slice tables with 147 float literals
|
||||||
|
(`BufferManager.java:10-38`), and each `StatBuffer` is a 9-dimensional `SingleBuffer` array built
|
||||||
|
from those slices (`BufferManager.java:541-600`).
|
||||||
|
|
||||||
|
Consequence: a value that lands *exactly* on a slice boundary (e.g. `BFT == 20f`) can fall into a
|
||||||
|
different bucket if computed in binary64. Over thousands of ticks this silently reshapes the danger
|
||||||
|
histogram. Nim defaults to `float64`; the port must use `float32` (`float32`/`array[... , float32]`)
|
||||||
|
for every one of these fields, casts and literals to preserve rounding — or accept documented
|
||||||
|
divergence. **Do not port `float` → Nim `float` (64-bit) silently.**
|
||||||
|
|
||||||
|
Full site list for the casts is in §A.1; the `(float)` casts are concentrated at
|
||||||
|
`DrussMoveGT.java:320-357, 432, 453-490, 574-611, 1506, 1558` and
|
||||||
|
`EnemyWave.java:108,110,188,190`.
|
||||||
|
|
||||||
|
### 3.2 `%` remainder on angles / negatives — **LOW, and mostly a non-issue** `[MEASURED]`
|
||||||
|
|
||||||
|
All `%` sites (`grep -rnE '%' --include='*.java'` minus comments):
|
||||||
|
|
||||||
|
| Site | Operand | Negative possible? | Correct Nim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `KDTree.java:103` `nodeSize % _bucketSize == 0` | `nodeSize` = point count | No | `mod` |
|
||||||
|
| `BufferManager.java:535` `hits = (hits+1)%bins.length` | counter | No | `mod` |
|
||||||
|
| `EnergyDomeWorker.java:154` `bullets%2` | counter | No | `mod` |
|
||||||
|
| `EnergyDomeWorker.java:158` `maxIndex%(OPTIONS/3)` | index | No | `mod` |
|
||||||
|
| `DrussGunDC.java:44`, `DrussMoveGT.java:101,658` | string `"%"` format | n/a | n/a |
|
||||||
|
|
||||||
|
There are **zero `%`-on-negative** sites in the source. Angle normalisation never uses `%`: it uses
|
||||||
|
`FastTrig.normalRelativeAngle` / `normalAbsoluteAngle`, which are **floor-based**
|
||||||
|
(`FastTrig.java:183-193`: `i = Math.floor(d*(1/(2π))); d -= i*(2π)`), i.e. exactly Nim's `floorMod`
|
||||||
|
behaviour, not Java `%`. The correct Nim construct already exists — reimplement these two functions
|
||||||
|
verbatim. The classic `((x+180) % 360) - 180` idiom does **not** appear.
|
||||||
|
|
||||||
|
The one caveat is `[EXTERNAL]` `robocode.util.Utils.normalRelativeAngle` / `normalAbsoluteAngle`,
|
||||||
|
called at `MeleeRadar.java:55,60` and `EnemyMoves.java:58,123`. Those are the engine's functions,
|
||||||
|
not DrussGT's; their exact semantics are not in this tree and should be pinned against Robocode
|
||||||
|
source before porting (they are used by the enemy-movement feature, so they affect movement).
|
||||||
|
|
||||||
|
### 3.3 `static` mutable state — **~68 non-final static fields; 27 in movement** `[MEASURED]`
|
||||||
|
|
||||||
|
`static final` primitives (e.g. `DrussMoveGT.VCS`, `DrussMoveGT.WALKING_STICK`, `WALL_MARGIN`,
|
||||||
|
`WALL_STICK`, `EnemyWave.BINS`, `EnergyDomeWorker.OPTIONS`) are harmless constants. The hazard is the
|
||||||
|
**non-final static** fields, which are shared across instances and have initialisation-order issues:
|
||||||
|
|
||||||
|
`DrussMoveGT.java` (27 fields):
|
||||||
|
- 4 buffer lists: `statBuffers:19`, `flattenerBuffers:20`, `ABSBuffers:21`, `flattenerTickBuffers:22`
|
||||||
|
- 4 KD-trees: `surfBufferTree:25`, `flatBufferTree:26`, `ABSBufferTree:27`, `tickBufferTree:28`
|
||||||
|
- 6 scan-history lists: `_distances:36`, `_lateralVelocitys:37`, `_advancingVelocitys:38`,
|
||||||
|
`_flattenerTickWaves:41`, `_surfDirections:42`, `_surfAbsBearings:43`
|
||||||
|
- scalars: `BULLET_POWER:45`, `lateralDirection:47`, `_fieldRect:58`, `WALL_STICK:62`,
|
||||||
|
`totalEnemyDamage:64`, `weightedEnemyFirerate`+`weightedEnemyHitrate:65`, `totalMyDamage:66`,
|
||||||
|
`bestDistance:70`, `flattenerEnabled`+`flattenerStarted:71`, `waveCounter:86`
|
||||||
|
|
||||||
|
Other files:
|
||||||
|
- `DrussGunDC.java`: `dataToLog:16`, `myLocation`+`myNextLocation:19`, `BULLET_POWER:20`,
|
||||||
|
`waveList`+`removeList:24`, `bulletsShot:27`, `bulletsPassed:28`, `bulletsHit:29`,
|
||||||
|
`currentGF:31`, `enemyName:34`, `paintPoints:746`, `DCHits/actualHits/DCASHits:748`,
|
||||||
|
`randomHits:749`, `targetLocation:750`, `targetHeading:751`, `GUN:752`, `heapTree/ASTree:766`,
|
||||||
|
`currentTime:769` (plus mutable `S/W/N/E/HALF_PI/WALL_MARGIN:1294-1299`).
|
||||||
|
- `PreciseMinMaxGFs.java`: `MARGIN:11`, `WIDTH:12`, `HEIGHT:13` — note this is **used by movement's
|
||||||
|
enemy model** and hard-codes 800×600 (see §A.3).
|
||||||
|
- `EnergyDomeWorker.java`: `optionScores:13`, `offsetCounts:14`, `offsets:15`, `dirOffsets:16`,
|
||||||
|
`unmatchedEnemyDamage:30`, `enemyDamage:31`, `myDamage:32`.
|
||||||
|
- `DrussGT.java`: `bulletPowerPredictor:18`, `shieldEnabled:19`.
|
||||||
|
- `EnemyMoves.java`: `tree:46`.
|
||||||
|
|
||||||
|
Plus **2 `static final` arrays with mutable contents** filled in a static initializer:
|
||||||
|
`narrowprofile`/`wideprofile` (`DrussMoveGT.java:1250-1256`). Porting hazard: Nim has no mutable
|
||||||
|
global state shared the same way across module instances unless declared `var` at module scope;
|
||||||
|
the natural Nim translation is object fields, but then the cross-round persistence that the Java
|
||||||
|
relies on (e.g. `statBuffers` populated only when `getRoundNum()==0`, `DrussMoveGT.java:105-129`;
|
||||||
|
`statBuffers.clear()` at end of match, `DrussMoveGT.java:660-663`) must be explicitly modelled.
|
||||||
|
Initialisation order matters: `surfBufferTree`/`flatBufferTree` etc. are initialised at class-load
|
||||||
|
time *before* the constructor runs (`DrussMoveGT.java:25-28`).
|
||||||
|
|
||||||
|
### 3.4 Lookup-table / approximate math — **`FastTrig` is NOT a table; `BufferManager` is** `[MEASURED]`
|
||||||
|
|
||||||
|
`jk/math/FastTrig.java` (193 lines) contains **no arrays and no lookup tables**. It is pure
|
||||||
|
polynomial approximation:
|
||||||
|
- `sin`/`sinInBounds`: degree-13 odd polynomial, coefficients `-2.0534...e-08 … 9.99999707...e-01`
|
||||||
|
(`FastTrig.java:30-46`, `64-73`).
|
||||||
|
- `cos`/`cosInBounds`: degree-12 even polynomial (`FastTrig.java:48-62`, `75-87`).
|
||||||
|
- `tan`: Chebyshev rational (`FastTrig.java:89-110`).
|
||||||
|
- `atan`: Chebyshev on `[0,1]` (`chebyshev_atan`, `FastTrig.java:147-153`), `atan2` built from it
|
||||||
|
(`FastTrig.java:155-180`).
|
||||||
|
- `asin = HALF_PI - acos` (`FastTrig.java:121`); `acos` is a cubic sqrt approximation with
|
||||||
|
hard-coded coefficients `a=1.570758334, b=-0.212875075, c=0.076897503, d=-0.020892330`
|
||||||
|
(`FastTrig.java:126-131`).
|
||||||
|
- `sqrt` just forwards to `Math.sqrt` (`FastTrig.java:171-173`).
|
||||||
|
|
||||||
|
What must be ported **coefficient-exact**: all of the above. Replacing `FastTrig.sin/cos/atan2/asin`
|
||||||
|
with `std.math` `sin/cos/arctan2/arcsin` will change the last 6–7 decimal digits and, through the
|
||||||
|
`float` bin boundaries of §3.1, occasionally flip a bucket. The tables size is therefore **zero
|
||||||
|
arrays**; the "table" is ~30 scalar coefficients. `[EXTERNAL]` `asin` uses the approximated `acos`
|
||||||
|
(not libm), so `EnemyWave.maxEscapeAngle()` (`EnemyWave.java:217`) and `smoothWest`/`distanceWest`
|
||||||
|
(`DrussMoveGT.java:1954-1986`) inherit that approximation.
|
||||||
|
|
||||||
|
The genuine **table-driven** data is in `BufferManager.java`:
|
||||||
|
- 22 `static final float[]` slice tables (`BufferManager.java:10-38`), **128 scalar entries**, 147
|
||||||
|
float literals.
|
||||||
|
- 4 set-builders (`getStatBuffers:40`, `getFlattenerBuffers:218`, `getABSBuffers:278`,
|
||||||
|
`getFlattenerTickBuffers:317`, plus `putBuffersInto:358`) whose 3-D `float[][][]` literals encode
|
||||||
|
~**130 + 50 + 30 + 30 = ~240 buffer-set rows** (measured by counting rows starting with `{` in
|
||||||
|
each builder's line range) [MEASURED]. Each row is a 9-slice feature selection; each becomes a
|
||||||
|
`StatBuffer` with a 9-D array of `SingleBuffer`s (`BufferManager.java:541-600`). This is not
|
||||||
|
"approximate math" but it is **hand-tuned data that must be reproduced exactly**, and it is the
|
||||||
|
largest single body of raw transcription in the port (§A.1).
|
||||||
|
|
||||||
|
Also table-like: `Scan.location()` / `Scan.ASLocation()` (`DrussMoveGT.java:2252-2281`) and
|
||||||
|
`EnemyMoves.Locator.getLocation()` (`EnemyMoves.java:238-253`) contain 10–16 hard-coded feature
|
||||||
|
weights; and `narrowprofile`/`wideprofile` are generated (`DrussMoveGT.java:1250-1256`).
|
||||||
|
|
||||||
|
### 3.5 Integer arithmetic / bit manipulation — **LOW** `[MEASURED]`
|
||||||
|
|
||||||
|
- `>>>` appears exactly **once**: `KDTree.java:545 int index = (i+j) >>> 1;` in the binary search.
|
||||||
|
Operands are non-negative stack indices; `(i+j) shr 1` is faithful. No `<<`, `>>` or `^` anywhere
|
||||||
|
(the only `>>` is commented-out Quake inverse-sqrt at `FastTrig.java:181`).
|
||||||
|
- `(int)` casts: **137** [MEASURED], overwhelmingly `(int)Math.round(...)` on bin indices
|
||||||
|
(e.g. `DrussMoveGT.java:1531-1532`, `EnemyWave` factor-index rounding at `DrussMoveGT.java:850`,
|
||||||
|
`BufferManager` indices). Java `Math.round` returns `long`; `(int)` truncates. Nim
|
||||||
|
`int(round(x))` matches for the ranges here. `(long)` casts: 6. `(double)` casts: 6.
|
||||||
|
- No integer-overflow-dependent algorithm found (no hashing, no packing). `int` is 32-bit in Java
|
||||||
|
and 64-bit in Nim; the only place width matters is `hits = (hits+1)%bins.length`
|
||||||
|
(`BufferManager.java:535`, tiny) and `bins/hits` bookkeeping — safe.
|
||||||
|
- **Non-short-circuit boolean operators**: Java uses single `&` (34 sites) and `|` (11 sites)
|
||||||
|
instead of `&&` (82) / `||` (37). On pure operands these are behaviourally equivalent to Nim
|
||||||
|
`and`/`or`, but **4 sites carry a side effect in the right operand**, where Nim's short-circuit
|
||||||
|
`and` would change behaviour:
|
||||||
|
- `MovePredictor.java:175` `… & --counter != 0;`
|
||||||
|
- `MovePredictor.java:179` `… & --counter != 0;`
|
||||||
|
- `DrussMoveGT.java:1807` `… & count-- > 0);`
|
||||||
|
- `DrussMoveGT.java:1813` `… & count-- > 0);`
|
||||||
|
These must be rewritten (evaluate the decrement unconditionally, then combine) — a classic
|
||||||
|
Java→Nim trap. (`MovePredictor.java:100` and `PreciseMinMaxGFs.java:117,175` use `&&` with a
|
||||||
|
side effect and are therefore *correctly* short-circuit; do not "fix" those.)
|
||||||
|
|
||||||
|
### 3.6 Java library dependencies requiring real work `[MEASURED]`
|
||||||
|
|
||||||
|
- **Collections**: `ArrayList` × 136 occurrences — by far the main one, maps to `seq[T]`. No
|
||||||
|
`HashMap`/`TreeMap`/`HashSet`/`TreeSet`/`LinkedList` at all. `Hashtable` × 2 and `Vector` × 1
|
||||||
|
only: `MeleeRadar.java:13` (`Hashtable<String,EnemyInfo>`) and `DrussMoveGT.java:698`
|
||||||
|
(`bot.getAllEvents()` on death).
|
||||||
|
- **Sorting/comparators**: `Collections.sort` × 2 (`DrussMoveGT.java:1169` with `TimeComparator`,
|
||||||
|
`:1349` on `PlaceTime implements Comparable`). `Comparable` implementations: `PlaceTime`,
|
||||||
|
`GFRange`, `Indice`, `NormalDistribution` (plus the duplicate `GFRange.java`/`Indice.java`).
|
||||||
|
`Comparator`: `DrussMoveGT.TimeComparator` (`DrussMoveGT.java:1152`), which casts
|
||||||
|
`KDTree.SearchResult` payloads. `Arrays.*`: 6 (`KDTree` fill/copy, `DrussGunDC:1044 Arrays.sort`,
|
||||||
|
`EnergyDomeWorker:64 Arrays.toString`).
|
||||||
|
- **Iterators**: `Iterator` × 12, `Enumeration` × 1 (`MeleeRadar.java:45`). All become `for` loops.
|
||||||
|
- **Anonymous classes / lambdas: ZERO.** `grep` for `new X(){`, `->`, `::` found none (the only
|
||||||
|
`->` is inside a comment at `KDTree.java:527`). This removes the usual Nim closure/`proc`-object
|
||||||
|
rewrite burden. The only inner types are named static/non-static classes
|
||||||
|
(`TimeComparator`, `Scan`, `PointChain`, `Locator`, `EnemyInfo`, `DetectWave`,
|
||||||
|
`SingleBuffer`, `StatBuffer`, `SearchResult`, `Node`, `PrioQueue`, etc.), each a plain `object`.
|
||||||
|
- **Boxed generics**: the movement's scan history uses untyped `ArrayList` holding
|
||||||
|
`Integer`/`Double` (`_surfDirections`, `_lateralVelocitys`, `_advancingVelocitys`,
|
||||||
|
`_distances` — `DrussMoveGT.java:36-43`, accessed with `.intValue()`/`.doubleValue()` and cast),
|
||||||
|
and `BulletPowerPredictor` uses `KDTree<Float>`. These need concrete `seq[int]`/`seq[float]`/
|
||||||
|
`seq[float32]` in Nim. `KDTree` is generic (`KDTree<T>`); Nim generics cover it.
|
||||||
|
- **`java.awt.geom`**: `Point2D` × 268, `Rectangle2D` × 7. Movement uses `Point2D.Double` as its
|
||||||
|
universal 2-D point (`DrussMoveGT.project`, `_myLocation`, `_enemyLocation`, `PlaceTime.place`,
|
||||||
|
`PredictionStatus.endPoint`, …) and `Rectangle2D.Double` for `_fieldRect.contains`
|
||||||
|
(`DrussMoveGT.java:58`, `1510`, `1805`). A Nim `Vec2`/`(x,y: float)` plus a `contains` helper is
|
||||||
|
a mechanical replacement. `Graphics2D`/`Color` appear only in `onPaint` (`DrussMoveGT.java:2030`,
|
||||||
|
157 lines; `DrussGunDC.java:477`; `MeleeRadar`), which can be dropped for a headless port.
|
||||||
|
- **`java.io`**: only two sites, both best-effort diagnostics — `DrussGT.contain()` writes
|
||||||
|
`getDataFile((int)(Math.random()*100)+".error")` via `RobocodeFileOutputStream`
|
||||||
|
(`DrussGT.java:259`), and `DrussGunDC` has a `PrintStream`/`RobocodeFileOutputStream` logger
|
||||||
|
gated by `final static boolean logData = false` (`DrussGunDC.java:15, 565-567`). **No learned
|
||||||
|
data is persisted between battles** — the bot relearns every match. Persistence is effectively a
|
||||||
|
no-op for a port; skip it. No `Serializable`.
|
||||||
|
- **Randomness**: `Math.random()` in `Insulter` (3×) and the error-file name
|
||||||
|
(`DrussGT.java:259`); `Math.random()` in the gun's random shot (`DrussGunDC.java:1148`). Movement
|
||||||
|
has no RNG. Port with `std/random`.
|
||||||
|
|
||||||
|
### 3.7 Infeasibility blockers — **none found** `[MEASURED]`
|
||||||
|
|
||||||
|
- **Reflection / dynamic class loading / JNI / `System.load` / serialization: none.**
|
||||||
|
`grep -rnE 'Class.forName|getDeclared|getMethod|reflect|\bnative\b|System\.load|Runtime\.'` → 0.
|
||||||
|
- **Threads/concurrency: none real.** The only match is `Thread.sleep(0,1000)` in the main loop
|
||||||
|
(`DrussGT.java:79`) — a classic-Robocode pacing/busy-wait idiom to let the engine deliver events,
|
||||||
|
not a worker thread. No `synchronized`.
|
||||||
|
- **`SkippedTurnEvent`**: overridden once (`DrussMoveGT.java:717`) and only `System.out.println`s
|
||||||
|
timing. It is **not** used to make decisions. Engine no longer emits it in modern Robocode anyway.
|
||||||
|
- **Undocumented engine internals: none.** Movement only uses the documented `AdvancedRobot` API.
|
||||||
|
- **Engine-timing semantics that DO matter** (MEDIUM): `bot.getTime()` as an absolute tick
|
||||||
|
(`DrussMoveGT.java:698,717,757,1291,1321…`), `bot.getGunCoolingRate()` (used to track enemy gun
|
||||||
|
heat: `DrussMoveGT.java:156-157,203-204,304,309,426,680`; classic value is 0.1 — the harness
|
||||||
|
`gunheat_tracker.nim` already hard-codes `GunCoolingRate = 0.1`), `bot.getOthers()`
|
||||||
|
(`DrussMoveGT.java:149,680,789,809,…`) and `ScannedRobotEvent` delivery order. These are
|
||||||
|
mappable to `WorldState` / a hard-coded cooling rate, but they are the reason a *bit-exact*
|
||||||
|
trajectory match is impossible (see §C).
|
||||||
|
|
||||||
|
### 3.8 The 10 largest methods `[MEASURED]`
|
||||||
|
|
||||||
|
Measured with a brace-matching script (`/tmp/portaudit/methods.py`), sorted by body line count:
|
||||||
|
|
||||||
|
| # | Lines | file:line | Method | What it is |
|
||||||
|
|---:|---:|---|---|---|
|
||||||
|
| 1 | 234 | `DrussMoveGT.java:1258` | `getBestPoint(EnemyWave,EnemyWave,EnemyWave)` | **Algorithmic core**: builds danger bins, predicts safe points for up to 3 waves, scores/sorts them, picks the safest. |
|
||||||
|
| 2 | 224 | `EnergyDomeWorker.java:85` | `onScannedRobot(ScannedRobotEvent)` | Shield subsystem (optional, not movement). |
|
||||||
|
| 3 | 178 | `BufferManager.java:40` | `getStatBuffers()` | **Data table** — ~130 hand-written 9-slice buffer sets. |
|
||||||
|
| 4 | 155 | `DrussMoveGT.java:2030` | `onPaint(Graphics2D)` | Debug drawing — **droppable**. |
|
||||||
|
| 5 | 132 | `DrussGunDC.java:787` | `test(…)` | Gun wave scoring / virtual-bullet resolution. |
|
||||||
|
| 6 | 130 | `DrussMoveGT.java:420` | `addWave(double)` | Build wave from energy drop; compute the 9 KNN features; fetch stats. |
|
||||||
|
| 7 | 116 | `DrussMoveGT.java:303` | `addImaginaryWave()` | Same features for a predicted enemy shot. |
|
||||||
|
| 8 | 103 | `DrussMoveGT.java:550` | `addFlattenerTickWave()` | Same features for the "flattener" anti-pattern-matcher. |
|
||||||
|
| 9 | 98 | `DrussGunDC.java:968` | `getBearing(KDTree,…)` | Gun KNN density peak (gun core). |
|
||||||
|
| 10 | 94 | `BufferManager.java:358` | `putBuffersInto(float[][][],ArrayList)` | Data-table → `StatBuffer` construction. |
|
||||||
|
|
||||||
|
**The algorithmic core is `DrussMoveGT.getBestPoint` + the wave/feature pipeline
|
||||||
|
(`addWave`/`addImaginaryWave`/`addFlattenerTickWave`) + `getBins`/`getDanger`/`predictPositions`.**
|
||||||
|
Note `addWave`, `addImaginaryWave` and `addFlattenerTickWave` are near-duplicates of the same ~100
|
||||||
|
lines of feature extraction (a candidate for de-duplication in the port, but de-duplication must
|
||||||
|
preserve the tiny differences: `addWave` indexes history at offset 2, `addImaginaryWave` at offset 0,
|
||||||
|
`addFlattenerTickWave` at offset 2 with `flattenerStarted` gating).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverable A — the movement subsystem, precisely scoped
|
||||||
|
|
||||||
|
### A.0 The ACTUAL harness interface `[MEASURED]`
|
||||||
|
|
||||||
|
**`common_libs/movement_harness/movement_interface.nim:8-20`** (quoted verbatim):
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type
|
||||||
|
MoveCommand* = tuple[speed: float, turnRate: float]
|
||||||
|
## speed: target speed in px/tick, clamped to ±8 by bot API
|
||||||
|
## turnRate: body turn rate in degrees/tick
|
||||||
|
|
||||||
|
## MovementModule concept — any type T implementing computeMove is valid.
|
||||||
|
template isMovementModule*(T: typedesc): bool =
|
||||||
|
compiles(
|
||||||
|
block:
|
||||||
|
var m: T
|
||||||
|
let ws = WorldState()
|
||||||
|
let cmd: MoveCommand = m.computeMove(ws)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
`WorldState` is defined in **`common_libs/gun_harness/gun_interface.nim:12-31`** (the movement
|
||||||
|
harness re-exports it: `export gun_interface.WorldState`):
|
||||||
|
|
||||||
|
```nim
|
||||||
|
type
|
||||||
|
EnemyInfo* = object
|
||||||
|
id*: int
|
||||||
|
x*, y*: float
|
||||||
|
heading*, speed*, energy*: float
|
||||||
|
|
||||||
|
WorldState* = object
|
||||||
|
enemyX*, enemyY*: float
|
||||||
|
enemySpeed*, enemyHeading*: float
|
||||||
|
selfX*, selfY*: float
|
||||||
|
selfSpeed*, selfHeading*: float
|
||||||
|
selfRadarHeading*: float
|
||||||
|
selfEnergy*: float
|
||||||
|
enemyEnergy*: float
|
||||||
|
arenaWidth*, arenaHeight*: float
|
||||||
|
tick*: int
|
||||||
|
enemies*: seq[EnemyInfo]
|
||||||
|
```
|
||||||
|
|
||||||
|
A movement module is therefore a type with `proc computeMove*(m: var T, ws: WorldState): MoveCommand`.
|
||||||
|
By convention the real modules also expose `initX*()`, `resetRound*(m)`, `clearGraphics*(m)`, and
|
||||||
|
`removeBulletNear*(m, x, y)` (e.g. `common_libs/movements/the_floor_is_lava.nim:76-105`,
|
||||||
|
`phantom_meteor.nim:194-197`), and `ModularBot_garage/src/ModularBot.nim:545-548` calls
|
||||||
|
`computeMove` every tick and routes `onHitByBullet` into a separate `VirtualBodyTracker`
|
||||||
|
(`ModularBot.nim:296-309`), **not** into the movement module. Note the harness's own physics
|
||||||
|
(`common_libs/movement_harness/virtual_bodies.nim:75-90`) uses Tank Royale
|
||||||
|
`maxTurn = 10 - 0.75*|speed|`, speed ramp ±1, clamp to arena.
|
||||||
|
|
||||||
|
### A.1 (a) Exact file list for a movement-only port
|
||||||
|
|
||||||
|
Mandatory set (transitive closure of `DrussMoveGT`'s dependencies), measured LOC:
|
||||||
|
|
||||||
|
| File | Lines | Difficulty | Why it is needed |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `jk/mega/dMove/DrussMoveGT.java` | 2283 (2126 excl. `onPaint`) | **Very hard** | The movement core. |
|
||||||
|
| `jk/mega/BufferManager.java` | 652 | Medium (mechanical) + hard index logic | Stat/flattener/ABS/tick buffers + slice tables. |
|
||||||
|
| `jk/tree/KDTree.java` | 617 | Hard | Used by `EnemyMoves` **and** `BulletPowerPredictor`. Generic; exact split/tie behaviour. |
|
||||||
|
| `jk/mega/dGun/PreciseMinMaxGFs.java` | 266 | Medium-hard | **In a gun package but required by movement**: `EnemyMoves` calls `getPreciseMEAs` (`EnemyMoves.java:94`). Three mini-simulators. |
|
||||||
|
| `jk/mega/EnemyMoves.java` | 264 | Hard | Enemy movement predictor (KD-tree of `PointChain`s). |
|
||||||
|
| `jk/mega/dMove/EnemyWave.java` | 236 | Medium-hard | Wave + `binCleared` shadows + `getFactorIndex`. |
|
||||||
|
| `jk/mega/dMove/MovePredictor.java` | 228 | Medium-hard | Our own tick simulator + classic `getNewVelocity`. |
|
||||||
|
| `jk/math/FastTrig.java` | 193 | Easy (must be exact) | All trig for movement. |
|
||||||
|
| `jk/precise/util/PreciseUtils.java` | 155 | Medium | Circle/line intersection used by `EnemyWave.logShadow` / `MovePredictor`. |
|
||||||
|
| `jk/mega/BulletPowerPredictor.java` | 50 | Easy-medium | Predict enemy bullet power (KD-tree KNN). |
|
||||||
|
| `jk/mega/dMove/PredictionStatus.java` | 27 | Trivial | Result struct. |
|
||||||
|
| `jk/mega/dMove/PlaceTime.java` | 18 | Trivial | (place,time,danger) struct, `Comparable`. |
|
||||||
|
| `jk/mega/dMove/BulletTracker.java` | 12 | Trivial | Our-bullet tracking. |
|
||||||
|
| `jk/precise/util/PreciseWave.java` | 6 | Trivial | Struct. |
|
||||||
|
| **Mandatory total** | **5007** | | |
|
||||||
|
|
||||||
|
Optional / droppable:
|
||||||
|
- `jk/mega/DataToGun.java` (11) — only needed if `getGunInfo()` is kept (it exports to the gun).
|
||||||
|
- `jk/mega/Insulter.java` (162) — cosmetic taunt at end of round.
|
||||||
|
- `DrussMoveGT.onPaint` (157 lines) — headless port can drop it.
|
||||||
|
- `Scan` class (84 lines, end of `DrussMoveGT.java`) — **dead under `VCS=true`** (`DrussMoveGT.java:17`).
|
||||||
|
|
||||||
|
`static final boolean VCS = true` (`DrussMoveGT.java:17`) means the **entire KD-tree surf path is
|
||||||
|
dead**: `getBins` uses `extractIntoBins(wave.allStats, …)` (`DrussMoveGT.java:1199-1201`) and never
|
||||||
|
`surfBufferTree`. But `KDTree` is still required because `EnemyMoves` (`EnemyMoves.java:46`) and
|
||||||
|
`BulletPowerPredictor` (`BulletPowerPredictor.java:6`) use it. A port could substitute a brute-force
|
||||||
|
KNN for `BulletPowerPredictor` (50 lines, tiny tree) but **should not** for `EnemyMoves`, whose
|
||||||
|
prediction quality depends on nearest-neighbour selection over motion chains.
|
||||||
|
|
||||||
|
### A.2 (b) Gap analysis — what the harness does NOT provide
|
||||||
|
|
||||||
|
DrussGT's movement is written against `AdvancedRobot` and consumes data the `WorldState`/`MoveCommand`
|
||||||
|
interface does not carry. Concretely:
|
||||||
|
|
||||||
|
| DrussGT expects | Source witness | Harness status | Impact / workaround |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `setAhead(distance)` + `setTurnRightRadians(angle)` | `DrussMoveGT.java:1821` (`doSurfing` fallback), `1872-1873` (`goTo`) | Harness wants `(speed, turnRate)` | **Physics-model mismatch.** Must convert desired heading/distance to speed+turn (and the port's own predictor uses classic accel=1/decel=2, `DrussMoveGT.java:1075`; harness `virtual_bodies` ramps ±1). |
|
||||||
|
| `bot.getDistanceRemaining()` | `DrussMoveGT.java:1291` (`now.distanceRemaining = …`) and `goTo` terminal logic `1836-1852` | **Absent** | Cannot faithfully seed `PredictionStatus.distanceRemaining` from tick-driven `WorldState`. Either track remaining ourselves from `MoveCommand` or approximate. |
|
||||||
|
| `bot.getGunCoolingRate()` / `getGunHeat()` | `DrussMoveGT.java:156-157,186,203-204,309,426` | **Absent** | Hard-code 0.1 (matches harness `gunheat_tracker.GunCoolingRate`). `getGunHeat()` only used to init on first scan (`:186`); fake with `3.0`. |
|
||||||
|
| `bot.getOthers()` | `DrussMoveGT.java:149,680,789,809,758` | `ws.enemies.len` | Direct. |
|
||||||
|
| `bot.getRoundNum()` / `getNumRounds()` | `DrussMoveGT.java:98,105,223,229,237,654` | **Absent** | Used for flattener gating + round-0 buffer init. Add `round`/`numRounds` to `WorldState` or a module-level counter. |
|
||||||
|
| `ScannedRobotEvent` (bearing, distance, heading, velocity, energy, name) | `DrussMoveGT.onScannedRobot`, `EnemyMoves.onScannedRobot` | `WorldState` has the fields but **event-driven scan cadence is gone** | DrussGT creates waves/features *per scan*; the harness calls `computeMove` *per tick*. The port must decide when a "new scan" happened (e.g. on enemy state change). |
|
||||||
|
| `HitByBulletEvent`, `BulletHitBulletEvent`, `BulletHitEvent`, `HitRobotEvent`, `RobotDeathEvent` | `DrussMoveGT.java:667-722`, `935-1030` | **Not passed to `computeMove` at all** | **Biggest gap.** Danger bins only improve when `logHit`/`logFlattener`/`logABS` fire (`DrussMoveGT.java:860-928`). Without hit-event routing the KNN never learns and the surfer degenerates to its priors. Requires harness extension (route hit events into the module) or a new optional `onHit*` on the module concept. |
|
||||||
|
| our fired `Bullet` objects (`getX/Y/HeadingRadians/Velocity/isActive`) | `DrussMoveGT.java:287-300, 731-745, 859-905` | `movement_harness/bullet_shadows.nim` exists but is **harness-internal** (`ShadowTracker`), not given to modules | Bullet-shadow ("flattener"/ABS) features need our bullet trajectories. `flattenerStarted` only turns on once enemy hit-rate >8-9% (`DrussMoveGT.java:229-239`), so **a first port can run with `flattenerStarted=false` and skip shadows**. |
|
||||||
|
| `bot.getAllEvents()` | `DrussMoveGT.java:698` (`onDeath`) | Absent | Cosmetic (replays `HitByBulletEvent`s into `onHitByBullet`). Can call the same handler directly. |
|
||||||
|
| `bot.getBattleFieldWidth/Height()` | `DrussMoveGT.java:128-129` | `ws.arenaWidth/Height` | Direct. |
|
||||||
|
| `e.getBearingRadians()/getHeadingRadians()` | `DrussMoveGT.java:190-214` | `ws.enemy*` gives absolute x/y/heading | Derivable, but **bearing sign conventions differ** (classic 0=N clockwise vs Tank Royale 0=E CCW) — the capture tooling already documents the exact conversion (`tools/fixtures/DRUSSGT_FIXTURES.md`). |
|
||||||
|
| enemy gun-heat model + `BulletPowerPredictor` | `DrussMoveGT.java:303-361` | none | Must be ported (it is inside movement). |
|
||||||
|
|
||||||
|
Additional **hard-coded-arena** hazard inside the would-be movement dependency: `PreciseMinMaxGFs`
|
||||||
|
uses `static double MARGIN=18, WIDTH=800, HEIGHT=600` (`PreciseMinMaxGFs.java:11-13`) and never
|
||||||
|
updates them, while `DrussMoveGT._fieldRect` is rebuilt for the real field size
|
||||||
|
(`DrussMoveGT.java:128`). For a Tank Royale port the 800×600 assumption must be parameterised.
|
||||||
|
|
||||||
|
### A.3 (c) Can the movement work WITHOUT porting the gun? — **YES** `[MEASURED]`
|
||||||
|
|
||||||
|
Direction of data flow is one-way: `DrussMoveGT.getGunInfo()` (`DrussMoveGT.java:249-286`) builds a
|
||||||
|
`DataToGun` (our safest future point/heading/velocity/time) and hands it to `DrussGunDC`. The gun
|
||||||
|
*consumes* movement output; movement never consumes gun output. Verified: the only `dGun` references
|
||||||
|
inside movement are `DataToGun` (a plain struct, `DataToGun.java`) and `PreciseMinMaxGFs`
|
||||||
|
(geometry), per the grep in §1.
|
||||||
|
|
||||||
|
The movement **does** predict:
|
||||||
|
1. **Enemy bullet power** (`bpp.predictBulletPower`, `DrussMoveGT.java:281,307,556`) — via its own
|
||||||
|
`BulletPowerPredictor` (KD-tree over `[myEnergy, enemyEnergy, distance]`), not the gun.
|
||||||
|
2. **Enemy movement** (`EnemyMoves.predict`, `DrussMoveGT.java:221,1287`) — via its own KD-tree of
|
||||||
|
observed motion chains.
|
||||||
|
3. **Enemy bullet danger (GF histogram)** — learned from our own hit events, not from the gun.
|
||||||
|
|
||||||
|
It does **not** predict the enemy's gun bearing directly; that is the gun's job and is irrelevant
|
||||||
|
to evasion. So a standalone movement module is coherent **provided hit events and (optionally) our
|
||||||
|
bullet tracks are routed to it** (§A.2). A first, reduced version can run with flattener/ABS waves
|
||||||
|
disabled (`flattenerStarted=false`) and still be a genuine wave surfer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverable B — defensible effort estimate
|
||||||
|
|
||||||
|
**Estimation basis.** All "L" figures are **measured Java LOC** [MEASURED] from §1. The "Nim LOC to
|
||||||
|
write" and durations are `[ESTIMATE]` derived from the Java LOC with an explicit, stated
|
||||||
|
Nim-expansion factor. Basis for the factor:
|
||||||
|
- The repo already contains a *reduced* KNN port of the DC gun: `common_libs/guns/knn_gun.nim` is
|
||||||
|
**305 lines** and its header explicitly says it is "inspired by DrussGT's DC gun"
|
||||||
|
(`knn_gun.nim:1`) with "Inverse-distance weights, Gaussian (same as DrussGT getBearingGaussian)"
|
||||||
|
(`knn_gun.nim:242`). Full `DrussGunDC` is 1352 Java lines, i.e. the existing partial port is
|
||||||
|
≈23% of a full one. This is direct evidence that 1 Java line does **not** shrink dramatically in
|
||||||
|
Nim, and that feature/table transcription dominates.
|
||||||
|
- Nim is generally *more* verbose than Java for: generic containers, `Comparable`/`Comparator`
|
||||||
|
(→ `sort` with a closure or `system.cmp`), and object construction.
|
||||||
|
- Conversely Nim removes: getters/setters, boxed generics, `Iterator` boilerplate, `Class` casts.
|
||||||
|
|
||||||
|
I use a **Nim LOC ≈ 0.85–1.15 × Java LOC** for the algorithmic code, and note that the
|
||||||
|
`BufferManager` data tables transcribe ~1:1 (they are data). Therefore Nim LOC ranges below are
|
||||||
|
roughly the Java LOC; the effort is dominated by *fidelity review*, not typing.
|
||||||
|
|
||||||
|
Subsystems (measured Java LOC → `[ESTIMATE]` Nim LOC / effort):
|
||||||
|
|
||||||
|
| Subsystem | Measured Java LOC | `[ESTIMATE]` Nim LOC | Kind of work | Risk |
|
||||||
|
|---|---:|---:|---|---|
|
||||||
|
| **Utils** (`FastTrig`, `KDTree`, `PreciseUtils`, `PreciseWave`) | 971 | 900–1100 | Coefficient-exact trig; generic KD-tree; circle/line intersection. Pure, unit-testable. | High (exactness) |
|
||||||
|
| **Movement** (`DrussMoveGT` core + `EnemyWave` + `MovePredictor` + structs) | 2804 (2647 excl. paint) | 2600–3200 | The heart: wave pipeline, danger scoring, wall-smoothing, 3-wave lookahead, classic velocity rules. | Very high |
|
||||||
|
| **Enemy-modeling** (`EnemyMoves`, `BufferManager`, `BulletPowerPredictor`) | 966 | 1000–1300 | KD-tree motion chains; ~240 hand-copied buffer sets × 9 slices; power KNN. Mostly transcription + index correctness. | High |
|
||||||
|
| **Movement package extras** (`PreciseMinMaxGFs`) | 266 | 250–350 | Three mini-simulators for precise MEA; used by enemy model. | Medium |
|
||||||
|
| **Gun** (`DrussGunDC` + inline `DCWave`/`StoreScan`/`JKDCUtils`/`NormDist`) | 1352 (+ ~5 classes that only exist as `.class`) | 1400–2000 | KD-tree of scans, MEA geometry, virtual-bullet scoring, DC/DCAS/random selection. Movement can be ported first. | Very high (and source-incomplete) |
|
||||||
|
| **Radar** (`MeleeRadar`) | 94 | 90–140 | Trivial state machine + melee scan. | Low |
|
||||||
|
| **Persistence** (error log + gun logger) | ~10 active | 0–40 | Skip (best-effort diagnostics; `logData=false`). | None |
|
||||||
|
| **Shield** (`EnergyDomeWorker`) | 462 | 500–700 | Optional subsystem; not needed for the movement target. | Medium |
|
||||||
|
| **Cosmetic** (`Insulter`, `DataToGun`) | 173 | 100–160 | Optional. | None |
|
||||||
|
|
||||||
|
**Movement-only first milestone: `[ESTIMATE]` ≈ 4,800–6,100 Nim LOC** across the 14 mandatory
|
||||||
|
files, plus the harness extension for hit-event routing. **`[ESTIMATE]` elapsed: 4–8 focused
|
||||||
|
agent-days** for "compiles and runs a battle", of which at least half is fidelity review of the
|
||||||
|
`float`/table/index logic (not typing).
|
||||||
|
|
||||||
|
### Shortest path to a **first compiling version**
|
||||||
|
|
||||||
|
1. **`FastTrig`** (193) — standalone, no deps.
|
||||||
|
2. **`KDTree`** (617) — standalone generic; port `Manhattan`/`Euclidean`, `addPoint`,
|
||||||
|
`nearestNeighbours`.
|
||||||
|
3. **`BufferManager`** (652) — pure data + index math; depends on nothing but `ArrayList`.
|
||||||
|
4. **`BulletPowerPredictor`** (50) — depends on `KDTree`.
|
||||||
|
5. **`PreciseUtils` + `PreciseWave`** (161) — pure geometry.
|
||||||
|
6. **`EnemyMoves`** (264) — depends on `KDTree`, `FastTrig`, `PreciseMinMaxGFs`.
|
||||||
|
7. **`PreciseMinMaxGFs`** (266) — depends on `FastTrig`.
|
||||||
|
8. **Structs**: `PlaceTime`, `PredictionStatus`, `BulletTracker`, `DataToGun` (68).
|
||||||
|
9. **`EnemyWave`** (236) — depends on `PreciseUtils`, `FastTrig`.
|
||||||
|
10. **`MovePredictor`** (228) — depends on `PreciseUtils`, `FastTrig`.
|
||||||
|
11. **`DrussMoveGT`** (2283) — the big one; stitch it all, replace `AdvancedRobot` calls with
|
||||||
|
`WorldState`, drop `onPaint`, drop `Scan` (VCS path).
|
||||||
|
12. **Adapter module** implementing `computeMove*(m: var DrussMove, ws: WorldState): MoveCommand`,
|
||||||
|
mapping DrussGT's chosen `(heading, distance)` to `(speed, turnRate)`.
|
||||||
|
13. **Harness extension**: feed `HitByBullet` (and optionally our-bullet) events to the module.
|
||||||
|
|
||||||
|
At step 13 this *compiles and moves*, but the danger bins are untrained until events are routed.
|
||||||
|
|
||||||
|
### Shortest path to a **first behaviourally-debugged version**
|
||||||
|
|
||||||
|
After the above, in order of value:
|
||||||
|
1. **Golden-value tests** for `FastTrig` (all functions on a grid, values captured from Java),
|
||||||
|
`normalRelativeAngle`/`normalAbsoluteAngle`, and `EnemyWave.getFactorIndex`/`maxEscapeAngle`.
|
||||||
|
2. **Golden-value tests** for `BufferManager.getIndex`/`getIndexes`/`extractIntoBins` against
|
||||||
|
captured Java outputs on a fixed input vector.
|
||||||
|
3. **Golden-value tests** for `DrussMoveGT.getDanger`/`getBins` on captured `EnemyWave` state.
|
||||||
|
4. **Replay test** using the already-captured fixtures (`tools/fixtures/drussgt_*.jsonl`): feed
|
||||||
|
recorded per-tick `WorldState` into the port and compare the chosen movement direction and
|
||||||
|
(if the gun is later ported) gun angle within tolerance.
|
||||||
|
5. **Full battle**: run the ported module in `ModularBot` and compare win rate / hit-rate against
|
||||||
|
the baseline surfer (`wave_surfer.nim`), accepting that trajectories diverge.
|
||||||
|
|
||||||
|
Steps 1–3 are cheap and are where the porting traps are caught; step 4 is the strongest feasible
|
||||||
|
validation; step 5 only sanity-checks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverable C — validation strategy
|
||||||
|
|
||||||
|
**The fundamental problem is real and cannot be engineered away:** classic Robocode and Tank Royale
|
||||||
|
run different physics and tick pipelines, so once the port is dropped into a Tank Royale battle its
|
||||||
|
trajectory diverges from the original after the first decision, no matter how faithful the code.
|
||||||
|
Comparing *trajectories* is therefore invalid; only comparing *decisions given identical inputs*
|
||||||
|
(or comparing pure-function outputs) is meaningful. Evaluating the three options:
|
||||||
|
|
||||||
|
### (i) Faithful classic-Robocode physics/replay simulator in Nim
|
||||||
|
Build a simulator that reproduces classic Robocode turning/acceleration and feeds the port the same
|
||||||
|
per-tick state as the original. Compare decisions (heading, dodge direction, gun angle within X°).
|
||||||
|
- **Cost: HIGH.** Classic physics is *distance-based* (`setAhead`), with decel=2 / accel=1
|
||||||
|
(`DrussMoveGT.java:1075`), and the predictor already encodes it; but building a simulator that
|
||||||
|
matches the engine's event ordering (scan cadence, bullet resolution, winner tie-breaks) is a
|
||||||
|
large project in itself. The repo's `virtual_bodies.nim` is **Tank Royale** physics, not classic,
|
||||||
|
so it cannot be reused as-is.
|
||||||
|
- **Trustworthiness: HIGHEST** *if* the simulator is itself correct; otherwise it launders
|
||||||
|
simulator bugs as port bugs.
|
||||||
|
|
||||||
|
### (ii) Unit-test individual pure functions against captured Java outputs
|
||||||
|
Capture outputs from a running, instrumented Java DrussGT for: `FastTrig.*`,
|
||||||
|
`normalRelativeAngle`/`normalAbsoluteAngle`, `BufferManager.getIndex(es)`,
|
||||||
|
`extractIntoBins`, `EnemyWave.getFactorIndex`/`maxEscapeAngle`/`getDanger`, `DrussMoveGT.getBins`,
|
||||||
|
and the `MovePredictor` tick loop. Freeze them as golden values; assert the Nim port matches exactly
|
||||||
|
(or within a documented epsilon for float32).
|
||||||
|
- **Cost: LOW.** The hard part — running classic Robocode headlessly — **is already solved in this
|
||||||
|
repo**: `tools/robocode_fixture_capture/Capture.java` + `capture.sh` run classic Robocode 1.9.5.5
|
||||||
|
headlessly and dump per-turn JSONL (`tools/fixtures/DRUSSGT_FIXTURES.md`). An instrumented
|
||||||
|
DrussGT can be compiled against the same install to emit intermediate values.
|
||||||
|
- **Trustworthiness: HIGH for the traps** (float32 rounding, bin boundaries, polynomial coeffs,
|
||||||
|
angle normalisation), **LOW for end-to-end behaviour** (it cannot tell you whether the *surfing
|
||||||
|
policy* is right, only that the sub-computations are right).
|
||||||
|
|
||||||
|
### (iii) Accept no validation; judge only by win rate
|
||||||
|
- **Cost: LOWEST to start.** Run the ported module in `ModularBot` and compare win rate against
|
||||||
|
`wave_surfer.nim`/`the_floor_is_lava.nim`.
|
||||||
|
- **Trustworthiness: LOWEST.** A win-rate result cannot distinguish "faithful port" from "different
|
||||||
|
bot that happens to surf", and Tank Royale opponents differ from classic ones. It is a
|
||||||
|
*product* metric, not a *fidelity* metric.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
**Hybrid, in this order: (ii) first, then a decision-replay harness that is "half of (i)".**
|
||||||
|
|
||||||
|
1. **Primary = option (ii), golden-value tests.** It is the cheapest, directly targets the
|
||||||
|
measured fidelity traps (`float32`, table indices, polynomial coefficients, floor-based angle
|
||||||
|
normalisation), and the Java-side capture infrastructure already exists. Requires compiling an
|
||||||
|
*instrumented copy* of DrussGT (print/trap the intermediate values) against `/tmp/robocode/install`.
|
||||||
|
2. **Secondary = decision replay on the existing fixtures.** `tools/fixtures/drussgt_*.jsonl`
|
||||||
|
already record true per-tick state (position/heading/speed/energy) for real DrussGT vs
|
||||||
|
SpinBot/RamFire/Crazy/Corners/DrussGT. Extend the capture to also log **DrussGT's own decisions**
|
||||||
|
(chosen move direction / ahead distance) and **enemy bullet fire + hit events**, then drive the
|
||||||
|
Nim port tick-by-tick from the recorded state and compare decisions. This exercises the whole
|
||||||
|
policy without needing a physics simulator, because positions are recorded. This is the most
|
||||||
|
trustworthy feasible end-to-end check and is far cheaper than a full simulator.
|
||||||
|
3. **Do not build full option (i) unless a physics bug is suspected** — it is the only path to
|
||||||
|
*trajectory* matching, but the stated premise (different engines) means trajectory matching is
|
||||||
|
not the goal.
|
||||||
|
4. **Use option (iii) only as a final smoke test**, never as evidence of fidelity.
|
||||||
|
|
||||||
|
**Cheapest: (iii) < (ii) < (i). Most trustworthy: (i) > (ii) > (iii). Recommended: (ii) + fixture
|
||||||
|
replay, ≈80–90% of (i)'s trustworthiness at a small fraction of its cost.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deliverable D — a smaller first target
|
||||||
|
|
||||||
|
The goal is to shake out the Java→Nim traps (float32, `%`-on-negatives, float-vs-int, collections)
|
||||||
|
before touching DrussGT's 5007-line movement.
|
||||||
|
|
||||||
|
**Availability note.** I could not verify new downloads (no network per task constraints). However,
|
||||||
|
the *locally present* classic Robocode install (`/tmp/robocode/install/robots/`, listed read-only)
|
||||||
|
contains the full official sample-bot source set. Measured:
|
||||||
|
|
||||||
|
| Candidate (locally available, `[MEASURED]` LOC) | float | `%` | collections | Exercises |
|
||||||
|
|---|---:|---:|---|---|
|
||||||
|
| `samplesentry/BorderGuard.java` (716) | 0 | 4† | `LinkedHashMap`, `ArrayList`, `Iterator`, `Point2D`, `Math.sqrt` quadratic | collections, generics, `Utils.normalRelativeAngle`, engine API mapping |
|
||||||
|
| `sample/Walls.java` (94) | 0 | 1 (`getHeading()%90`) | 0 | `%` on degrees (non-negative), wall geometry |
|
||||||
|
| `sample/Corners.java` (140) | 0 | 0 | 0 | `Utils.normalRelativeAngleDegrees` |
|
||||||
|
| `sample/Crazy.java` (103), `RamFire.java` (86), `SpinBot.java` (70) | 0 | 0 | 0 | movement patterns / physics |
|
||||||
|
|
||||||
|
† BorderGuard's four `%` matches are `Color(0x00,0xFF,…)` alpha bytes, not modulo; only
|
||||||
|
`Walls.java:50` is a real modulo. **None of the locally available samples uses `float` at all**, so
|
||||||
|
none exercises the single biggest DrussGT trap.
|
||||||
|
|
||||||
|
**Recommendation.**
|
||||||
|
1. **Immediate canary: `samplesentry/BorderGuard.java` (716 lines).** It is small, locally
|
||||||
|
available, and exercises the *collection / generics / engine-API-mapping* traps end to end
|
||||||
|
(`LinkedHashMap` with access-order LRU, `ArrayList`, `Iterator`, `Point2D`, `Math.sqrt`
|
||||||
|
quadratic, `Utils.normalRelativeAngle`). It deliberately does **not** cover float32.
|
||||||
|
2. **For float32 specifically, do not rely on a found bot — build the fixture first.** Use the
|
||||||
|
existing `tools/robocode_fixture_capture/` infrastructure to instrument a tiny Java class and
|
||||||
|
capture `float` arithmetic outputs (e.g. the exact Java result of
|
||||||
|
`(20-3*1.9)`-style computations and `FastTrig` on a grid), then port the *pure functions*
|
||||||
|
(`FastTrig`, angle normalisation, `BufferManager.getIndex`) and assert equality. This converts
|
||||||
|
the float32 risk into a mechanical, testable diff and is cheaper than acquiring a third-party
|
||||||
|
wave-surfer.
|
||||||
|
3. **If a genuine small wave-surfer is to be used as the canary**, it must meet these criteria
|
||||||
|
(I cannot verify availability of a specific one offline):
|
||||||
|
- Single-threaded, plain Java, **no reflection / JNI / dynamic loading**;
|
||||||
|
- ≤ ~1500 LOC and ≤ ~10 source files;
|
||||||
|
- **uses `float`** (KNN/neural/buffer bots do; simple pattern-matchers do not);
|
||||||
|
- contains `%` on a value that can be **negative** (angle normalisation) — this is the trap the
|
||||||
|
samples miss entirely;
|
||||||
|
- uses at least one non-trivial collection (`ArrayList`/`HashMap`) and a `Comparable`/`Comparator`;
|
||||||
|
- consumes `ScannedRobotEvent` + `HitByBulletEvent` so the event-routing gap is surfaced early;
|
||||||
|
- source obtainable under a redistributable licence (the Robocode archive bots are third-party —
|
||||||
|
keep the source out of the repo as `tools/fixtures/DRUSSGT_FIXTURES.md` already does for the jar).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bottom line for the orchestrator
|
||||||
|
|
||||||
|
- **Hazard counts:** 193 `float` occurrences / 35 casts / 147 float literals / 60 `float[]` in the
|
||||||
|
movement dependency closure; **0 `%`-on-negative** sites (angle normalisation is already
|
||||||
|
floor-based); **~68 static non-final fields** (27 in `DrussMoveGT`); **no infeasibility blockers**
|
||||||
|
(no reflection/threads/JNI/lambdas/anonymous classes/serialization). The real risks are float32
|
||||||
|
fidelity, coefficient-exact `FastTrig`, and ~240 hand-written buffer-table rows.
|
||||||
|
- **Movement scoping:** mandatory **5007 Java LOC** across 14 files (`DrussMoveGT` 2283 →
|
||||||
|
`BufferManager` 652 → `KDTree` 617 → `PreciseMinMaxGFs` 266 → `EnemyMoves` 264 → `EnemyWave` 236
|
||||||
|
→ `MovePredictor` 228 → `FastTrig` 193 → `PreciseUtils` 155 → `BulletPowerPredictor` 50 → four
|
||||||
|
structs), interface = `computeMove(m: var T, ws: WorldState): MoveCommand`. **Movement can be
|
||||||
|
ported without the gun** (data flows movement→gun only). The critical missing harness piece is
|
||||||
|
**hit-event routing into the module**; the missing state is `getDistanceRemaining`/`getGunHeat`/
|
||||||
|
`getRoundNum`, all approximable.
|
||||||
|
- **Effort:** `[ESTIMATE]` ≈ 4.8k–6.1k Nim LOC for movement-only, 4–8 focused agent-days to
|
||||||
|
first-compiles; gun is a further ≈1.4k–2k LOC and its source is **incomplete** (5 classes exist
|
||||||
|
only as `.class`).
|
||||||
|
- **Validation:** cheapest = pure-function golden tests (option ii); most trustworthy = full
|
||||||
|
classic simulator (option i) but cost-prohibitive; **recommended = (ii) golden tests on the traps
|
||||||
|
plus decision-replay of the already-captured `tools/fixtures/` data**.
|
||||||
|
- **First bot:** use the locally-available `samplesentry/BorderGuard.java` (716 lines) to shake out
|
||||||
|
collections/engine-mapping, and build float32 fixture tests from the existing capture tooling
|
||||||
|
rather than hunting a small wave-surfer.
|
||||||
Reference in New Issue
Block a user