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:
2026-09-20 23:44:42 +02:00
parent 4f18c8ce07
commit 76ae6170f8
+720
View File
@@ -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.