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.
50 KiB
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 afile:linewitness 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).bestBinsandbinClearedarefloat[171](EnemyWave.java:20-21).DrussMoveGT.getDanger(...)returnsfloat(DrussMoveGT.java:1509);PlaceTime.dangerisfloat(PlaceTime.java:13) and the candidate sort orders by it (PlaceTime.java:16-17).- The KNN features are built as
floatand looked up againstfloat[]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)withvalue >= slices[index](BufferManager.java:513-517). BufferManagerholds 22static final float[]slice tables with 147 float literals (BufferManager.java:10-38), and eachStatBufferis a 9-dimensionalSingleBufferarray 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 mutableS/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),atan2built from it (FastTrig.java:155-180).asin = HALF_PI - acos(FastTrig.java:121);acosis a cubic sqrt approximation with hard-coded coefficientsa=1.570758334, b=-0.212875075, c=0.076897503, d=-0.020892330(FastTrig.java:126-131).sqrtjust forwards toMath.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, plusputBuffersInto:358) whose 3-Dfloat[][][]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 aStatBufferwith a 9-D array ofSingleBuffers (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 1is faithful. No<<,>>or^anywhere (the only>>is commented-out Quake inverse-sqrt atFastTrig.java:181).(int)casts: 137 [MEASURED], overwhelmingly(int)Math.round(...)on bin indices (e.g.DrussMoveGT.java:1531-1532,EnemyWavefactor-index rounding atDrussMoveGT.java:850,BufferManagerindices). JavaMath.roundreturnslong;(int)truncates. Nimint(round(x))matches for the ranges here.(long)casts: 6.(double)casts: 6.- No integer-overflow-dependent algorithm found (no hashing, no packing).
intis 32-bit in Java and 64-bit in Nim; the only place width matters ishits = (hits+1)%bins.length(BufferManager.java:535, tiny) andbins/hitsbookkeeping — 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 Nimand/or, but 4 sites carry a side effect in the right operand, where Nim's short-circuitandwould 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:100andPreciseMinMaxGFs.java:117,175use&&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 toseq[T]. NoHashMap/TreeMap/HashSet/TreeSet/LinkedListat all.Hashtable× 2 andVector× 1 only:MeleeRadar.java:13(Hashtable<String,EnemyInfo>) andDrussMoveGT.java:698(bot.getAllEvents()on death). - Sorting/comparators:
Collections.sort× 2 (DrussMoveGT.java:1169withTimeComparator,:1349onPlaceTime implements Comparable).Comparableimplementations:PlaceTime,GFRange,Indice,NormalDistribution(plus the duplicateGFRange.java/Indice.java).Comparator:DrussMoveGT.TimeComparator(DrussMoveGT.java:1152), which castsKDTree.SearchResultpayloads.Arrays.*: 6 (KDTreefill/copy,DrussGunDC:1044 Arrays.sort,EnergyDomeWorker:64 Arrays.toString). - Iterators:
Iterator× 12,Enumeration× 1 (MeleeRadar.java:45). All becomeforloops. - Anonymous classes / lambdas: ZERO.
grepfornew X(){,->,::found none (the only->is inside a comment atKDTree.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 plainobject. - Boxed generics: the movement's scan history uses untyped
ArrayListholdingInteger/Double(_surfDirections,_lateralVelocitys,_advancingVelocitys,_distances—DrussMoveGT.java:36-43, accessed with.intValue()/.doubleValue()and cast), andBulletPowerPredictorusesKDTree<Float>. These need concreteseq[int]/seq[float]/seq[float32]in Nim.KDTreeis generic (KDTree<T>); Nim generics cover it. java.awt.geom:Point2D× 268,Rectangle2D× 7. Movement usesPoint2D.Doubleas its universal 2-D point (DrussMoveGT.project,_myLocation,_enemyLocation,PlaceTime.place,PredictionStatus.endPoint, …) andRectangle2D.Doublefor_fieldRect.contains(DrussMoveGT.java:58,1510,1805). A NimVec2/(x,y: float)plus acontainshelper is a mechanical replacement.Graphics2D/Colorappear only inonPaint(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()writesgetDataFile((int)(Math.random()*100)+".error")viaRobocodeFileOutputStream(DrussGT.java:259), andDrussGunDChas aPrintStream/RobocodeFileOutputStreamlogger gated byfinal 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. NoSerializable.- Randomness:
Math.random()inInsulter(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 withstd/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. Nosynchronized. SkippedTurnEvent: overridden once (DrussMoveGT.java:717) and onlySystem.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
AdvancedRobotAPI. - 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 harnessgunheat_tracker.nimalready hard-codesGunCoolingRate = 0.1),bot.getOthers()(DrussMoveGT.java:149,680,789,809,…) andScannedRobotEventdelivery order. These are mappable toWorldState/ 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 ifgetGunInfo()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.Scanclass (84 lines, end ofDrussMoveGT.java) — dead underVCS=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:
- Enemy bullet power (
bpp.predictBulletPower,DrussMoveGT.java:281,307,556) — via its ownBulletPowerPredictor(KD-tree over[myEnergy, enemyEnergy, distance]), not the gun. - Enemy movement (
EnemyMoves.predict,DrussMoveGT.java:221,1287) — via its own KD-tree of observed motion chains. - 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.nimis 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). FullDrussGunDCis 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(→sortwith a closure orsystem.cmp), and object construction. - Conversely Nim removes: getters/setters, boxed generics,
Iteratorboilerplate,Classcasts.
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
FastTrig(193) — standalone, no deps.KDTree(617) — standalone generic; portManhattan/Euclidean,addPoint,nearestNeighbours.BufferManager(652) — pure data + index math; depends on nothing butArrayList.BulletPowerPredictor(50) — depends onKDTree.PreciseUtils+PreciseWave(161) — pure geometry.EnemyMoves(264) — depends onKDTree,FastTrig,PreciseMinMaxGFs.PreciseMinMaxGFs(266) — depends onFastTrig.- Structs:
PlaceTime,PredictionStatus,BulletTracker,DataToGun(68). EnemyWave(236) — depends onPreciseUtils,FastTrig.MovePredictor(228) — depends onPreciseUtils,FastTrig.DrussMoveGT(2283) — the big one; stitch it all, replaceAdvancedRobotcalls withWorldState, droponPaint, dropScan(VCS path).- Adapter module implementing
computeMove*(m: var DrussMove, ws: WorldState): MoveCommand, mapping DrussGT's chosen(heading, distance)to(speed, turnRate). - 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:
- Golden-value tests for
FastTrig(all functions on a grid, values captured from Java),normalRelativeAngle/normalAbsoluteAngle, andEnemyWave.getFactorIndex/maxEscapeAngle. - Golden-value tests for
BufferManager.getIndex/getIndexes/extractIntoBinsagainst captured Java outputs on a fixed input vector. - Golden-value tests for
DrussMoveGT.getDanger/getBinson capturedEnemyWavestate. - Replay test using the already-captured fixtures (
tools/fixtures/drussgt_*.jsonl): feed recorded per-tickWorldStateinto the port and compare the chosen movement direction and (if the gun is later ported) gun angle within tolerance. - Full battle: run the ported module in
ModularBotand 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'svirtual_bodies.nimis 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.shrun 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
ModularBotand compare win rate againstwave_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)".
- 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. - Secondary = decision replay on the existing fixtures.
tools/fixtures/drussgt_*.jsonlalready 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. - 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.
- 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.
- Immediate canary:
samplesentry/BorderGuard.java(716 lines). It is small, locally available, and exercises the collection / generics / engine-API-mapping traps end to end (LinkedHashMapwith access-order LRU,ArrayList,Iterator,Point2D,Math.sqrtquadratic,Utils.normalRelativeAngle). It deliberately does not cover float32. - 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 capturefloatarithmetic outputs (e.g. the exact Java result of(20-3*1.9)-style computations andFastTrigon 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. - 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 aComparable/Comparator; - consumes
ScannedRobotEvent+HitByBulletEventso 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.mdalready does for the jar).
Bottom line for the orchestrator
- Hazard counts: 193
floatoccurrences / 35 casts / 147 float literals / 60float[]in the movement dependency closure; 0%-on-negative sites (angle normalisation is already floor-based); ~68 static non-final fields (27 inDrussMoveGT); no infeasibility blockers (no reflection/threads/JNI/lambdas/anonymous classes/serialization). The real risks are float32 fidelity, coefficient-exactFastTrig, and ~240 hand-written buffer-table rows. - Movement scoping: mandatory 5007 Java LOC across 14 files (
DrussMoveGT2283 →BufferManager652 →KDTree617 →PreciseMinMaxGFs266 →EnemyMoves264 →EnemyWave236 →MovePredictor228 →FastTrig193 →PreciseUtils155 →BulletPowerPredictor50 → 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 isgetDistanceRemaining/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.