Files
SirRoboGarage/docs/research/drussgt-port-audit.md
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

50 KiB
Raw Permalink Blame History

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
5d &/` operands with side effects (--, --`) 4 §3.5
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 SingleBuffers (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.printlns 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):

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):

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 PointChains).
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 HitByBulletEvents 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.