# 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`) 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`. These need concrete `seq[int]`/`seq[float]`/ `seq[float32]` in Nim. `KDTree` is generic (`KDTree`); 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.