Hazard one
Two determinism traps that don't exist on the JVM
Swift's Dictionary iteration order is randomly seeded per process.
Java's HashMap order is arbitrary but stable for a given insertion
sequence, so ROTP has been quietly relying on it for years. Port HashMap to
Dictionary and the same seed produces a different game every launch — on the
same machine, with no error and nothing to catch it.
Swift's sort() is not stable. Java's Collections.sort is
guaranteed stable (TimSort). Every tie — fleet ordering, target selection, tech
lists, incident processing — resolves differently, again silently.
69 files use HashMap (334 sites) · 38 files under model/ + ai/ declare HashMap or HashSet fields
~87 keySet/values/entrySet iteration sites inside rotp/model
160 Collections.sort calls across 63 files · 174 Comparator references
Fix it once, up front
Write an insertion-ordered dictionary and a stable sort, and make them the blanket
substitution during transliteration — never a case-by-case judgement about whether this
particular map "matters". Adding a tiebreak on entity ID to every comparator is a
reasonable belt-and-braces second layer. Cheap now; effectively unfindable later.
Hazard two
Integer overflow traps instead of wrapping
The RNG is a serializable copy of JDK SplittableRandom, and its mixing
functions are pure wrapping 64-bit arithmetic — z = (z ^ (z >>> 30)) *
0xbf58476d1ce4e5b9. In Swift that traps and crashes on the first call. Every
operation in there needs &*, &+ and an explicit
unsigned type.
The good news is the blast radius: unsigned right shift appears in just
two files, 22 sites, essentially all of it inside the RNG. This is a
contained, one-afternoon problem — as long as you know about it before you write the
class rather than while debugging turn one.
Hazard three
Swift 6 strict concurrency vs. an unguarded shared graph
The Java shares its entire mutable object graph between the turn thread and the Swing
thread with essentially no synchronisation — synchronized appears in 5 files,
volatile in exactly one. Swift 6's language mode makes that shape a compile
error, and the obvious fix (isolate the model in a GameActor) is the wrong
one: the renderer needs synchronous access to thousands of live objects every frame, so
actor isolation forces a snapshot layer you don't want and the Java never needed.
Recommendation
@MainActor-first. Model and turn loop both on the main actor; the
awaits at the suspension points are exactly where the run loop wants to
breathe anyway. Offload only the AI planning pass, with a Task.yield()
between empires so a long Xilmi turn doesn't freeze the window. If that proves noisy,
port in Swift 5 language mode and adopt strict concurrency as a separate pass.
Hazard four
Optionality, and float parity you can't fully have
247,000 lines of implicit Java nullability have to become explicit Swift Optionals. It's
mechanical but it's everywhere, and it's the main reason this is a translation rather than
a transliteration. Budget for it as a constant tax rather than a milestone.
Separately: bit-exact parity against the Java build is not fully achievable.
sqrt is IEEE-exact, but pow, sin and friends are
not specified bit-for-bit, and ROTP calls them from game logic — 130 Math.pow
sites across 51 files, 185 Math.sqrt across 48. StrictMath,
which would be reproducible, is used in exactly one file.
Consequence for the parity harness
Diff with a tolerance rather than on equality, and treat divergence as a signal to
investigate rather than a test failure. Where a specific game outcome hinges on a
transcendental, port that one call site against StrictMath's algorithm.
This applies to the web port too — it just matters more here, because Swift is otherwise
close enough that exact parity feels within reach.