Files
SirRoboGarage/docs/research/drussgt-port-audit.md
T
SirStone 76ae6170f8 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.
2026-09-20 23:44:42 +02:00

721 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.