Port Original Tactics hex math to typed Jerboa

ober

a416ccea7067d334f5fde941638ece4514a163a5

diff --git a/.build.yml b/.build.yml
index 272aa6f..71b9a6c 100644
--- a/.build.yml
+++ b/.build.yml
@@ -5,7 +5,7 @@ packages:
   - make=4.4.1-r4
 sources:
   # Build dependency: full immutable commit, mirrored in dependencies.lock.json.
-  - "https://git.sr.ht/~lisp/jerboa#3a1e213c984f40f16404f78007bbbccc925ec94a"
+  - "https://git.sr.ht/~lisp/jerboa#46dfd30d2ab66b59ef02336f5a0c7981b838bf84"
   # The second source is the build subject selected by the SourceHut submitter.
   - https://git.sr.ht/~lisp/jerboa-android
 tasks:
@@ -14,6 +14,6 @@ tasks:
       test "$(apk info -v chez-scheme)" = chez-scheme-10.3.0-r2
       test "$(apk info -v git)" = git-2.54.0-r0
       test "$(apk info -v make)" = make-4.4.1-r4
-      test "$(git -C ../jerboa rev-parse HEAD)" = 3a1e213c984f40f16404f78007bbbccc925ec94a
-      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = d1c285186e9618c88d0a89d61003816b126a3ef0
+      test "$(git -C ../jerboa rev-parse HEAD)" = 46dfd30d2ab66b59ef02336f5a0c7981b838bf84
+      test "$(git -C ../jerboa rev-parse 'HEAD^{tree}')" = 960136c5cd1e5a3c496f3398e2ef0cb8561cffc3
       JERBOA="chez --libdirs .:../jerboa/lib --script" make test
diff --git a/dependencies.lock.json b/dependencies.lock.json
index c0c2f1c..7f802a4 100644
--- a/dependencies.lock.json
+++ b/dependencies.lock.json
@@ -11,8 +11,8 @@
   "generator_runtime": {
     "name": "jerboa",
     "repository": "https://git.sr.ht/~lisp/jerboa",
-    "commit": "3a1e213c984f40f16404f78007bbbccc925ec94a",
-    "tree": "d1c285186e9618c88d0a89d61003816b126a3ef0"
+    "commit": "46dfd30d2ab66b59ef02336f5a0c7981b838bf84",
+    "tree": "960136c5cd1e5a3c496f3398e2ef0cb8561cffc3"
   },
   "assurance_tools": {
     "osv_scanner": {
diff --git a/scripts/verify-supply-chain.sh b/scripts/verify-supply-chain.sh
index 89325ad..893b52e 100755
--- a/scripts/verify-supply-chain.sh
+++ b/scripts/verify-supply-chain.sh
@@ -3,8 +3,8 @@ set -eu
 
 repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd -P)
 lock="$repo/dependencies.lock.json"
-jerboa_commit=3a1e213c984f40f16404f78007bbbccc925ec94a # gitsafe:ignore
-jerboa_tree=d1c285186e9618c88d0a89d61003816b126a3ef0 # gitsafe:ignore
+jerboa_commit=46dfd30d2ab66b59ef02336f5a0c7981b838bf84 # gitsafe:ignore
+jerboa_tree=960136c5cd1e5a3c496f3398e2ef0cb8561cffc3 # gitsafe:ignore
 gradle_sha=20f1b1176237254a6fc204d8434196fa11a4cfb387567519c61556e8710aed78
 jdk_macos_sha=8fa1eff40bb637a33613b2ccb8b12c70dc3661cc22cf8e784943715769a05336
 jdk_linux_sha=d8afc263758141a66e0e3aafc321e783f7016696f4eaea067d340a269037d331
diff --git a/templates/original-tactics.ss b/templates/original-tactics.ss
index d871fdc..fd90f77 100644
--- a/templates/original-tactics.ss
+++ b/templates/original-tactics.ss
@@ -2636,270 +2636,572 @@
        "        )"
        "    }"
        "}"))
-    (kotlin-file-lines "com/jerboa/originaltactics/HexMath.kt"
-      ("package com.jerboa.originaltactics"
-       ""
-       "import org.json.JSONArray"
-       "import org.json.JSONObject"
-       "import kotlin.math.abs"
-       ""
-       "data class ProjectedHelmPose("
-       "    val q: Int,"
-       "    val r: Int,"
-       "    val facing: Int,"
-       "    val speed: Int,"
-       "    val turnMode: String,"
-       "    val distanceSinceTurn: Int"
-       ")"
-       ""
-       "data class ProjectedHelmMove("
-       "    val impulse: Int,"
-       "    val maneuver: String?,"
-       "    val pose: ProjectedHelmPose"
-       ")"
-       ""
-       "data class ProjectedRangePoint("
-       "    val impulse: Int,"
-       "    val range: Int"
-       ")"
-       ""
-       "fun hexRangeFromPositions(alpha: JSONObject?, beta: JSONObject?): Int? {"
-       "    if (alpha == null || beta == null) return null"
-       "    return hexRange(alpha.optInt(\"q\"), alpha.optInt(\"r\"), beta.optInt(\"q\"), beta.optInt(\"r\"))"
-       "}"
-       ""
-       "fun hexRange(q1: Int, r1: Int, q2: Int, r2: Int): Int {"
-       "    val dq = q2 - q1"
-       "    val dr = r2 - r1"
-       "    val ds = -dq - dr"
-       "    return (abs(dq) + abs(dr) + abs(ds)) / 2"
-       "}"
-       ""
-       "fun currentBattleRange(battle: JSONObject?): String {"
-       "    val computed = hexRangeFromPositions("
-       "        battle?.optJSONObject(\"alpha-position\"),"
-       "        battle?.optJSONObject(\"beta-position\")"
-       "    )"
-       "    if (computed != null) return computed.toString()"
-       "    return (battle?.opt(\"range\") ?: \"-\").toString()"
-       "}"
-       ""
-       "fun movementImpulsesForSpeed(speed: Int): List<Int> {"
-       "    val moves = speed.coerceIn(0, 32)"
-       "    if (moves <= 0) return emptyList()"
-       "    return (1..moves).map { step -> (step * 32 + moves - 1) / moves }"
-       "}"
-       ""
-       "fun nextMoveImpulse(position: JSONObject?, currentImpulse: Int): Int? {"
-       "    val speed = position?.optInt(\"speed\", 0)?.coerceIn(0, 32) ?: 0"
-       "    return movementImpulsesForSpeed(speed).firstOrNull { it > currentImpulse }"
-       "}"
-       ""
-       "fun maneuverMapFromJson(raw: Any?): Map<Int, String> {"
-       "    val out = linkedMapOf<Int, String>()"
-       "    when (raw) {"
-       "        is JSONArray -> {"
-       "            for (index in 0 until raw.length()) {"
-       "                val item = raw.opt(index)"
-       "                when (item) {"
-       "                    is JSONObject -> {"
-       "                        val pair = item.optJSONArray(\"\\$pair\")"
-       "                        if (pair != null) {"
-       "                            val impulse = pair.optInt(0, -1)"
-       "                            val maneuver = pair.optString(1, \"\")"
-       "                            if (impulse in 1..32 && maneuver.isNotBlank()) out[impulse] = maneuver"
-       "                        }"
-       "                    }"
-       "                    is JSONArray -> {"
-       "                        val impulse = item.optInt(0, -1)"
-       "                        val maneuver = item.optString(1, \"\")"
-       "                        if (impulse in 1..32 && maneuver.isNotBlank()) out[impulse] = maneuver"
-       "                    }"
-       "                }"
-       "            }"
-       "        }"
-       "        is JSONObject -> {"
-       "            val keys = raw.keys()"
-       "            while (keys.hasNext()) {"
-       "                val key = keys.next()"
-       "                val impulse = key.toIntOrNull() ?: continue"
-       "                val maneuver = raw.optString(key, \"\")"
-       "                if (impulse in 1..32 && maneuver.isNotBlank()) out[impulse] = maneuver"
-       "            }"
-       "        }"
-       "    }"
-       "    return out"
-       "}"
-       ""
-       "fun projectedHelmMoves("
-       "    position: JSONObject?,"
-       "    maneuvers: Map<Int, String>,"
-       "    currentImpulse: Int,"
-       "    maxMoves: Int"
-       "): List<ProjectedHelmMove> {"
-       "    if (position == null) return emptyList()"
-       "    val speed = position.optInt(\"speed\", 0).coerceIn(0, 32)"
-       "    if (speed <= 0) return emptyList()"
-       "    val movementImpulses = movementImpulsesForSpeed(speed).filter { it > currentImpulse }"
-       "    if (movementImpulses.isEmpty()) return emptyList()"
-       ""
-       "    val out = mutableListOf<ProjectedHelmMove>()"
-       "    var pose = startingProjectedHelmPose(position)"
-       "    for (impulse in movementImpulses) {"
-       "        val maneuver = maneuvers[impulse]"
-       "        pose = applyProjectedHelmMove(pose, maneuver)"
-       "        out.add(ProjectedHelmMove(impulse, maneuver, pose))"
-       "        if (out.size >= maxMoves.coerceAtLeast(1)) break"
-       "    }"
-       "    return out"
-       "}"
-       ""
-       "fun projectedHelmPoseAt("
-       "    position: JSONObject?,"
-       "    projected: List<ProjectedHelmMove>,"
-       "    impulse: Int"
-       "): ProjectedHelmPose? ="
-       "    projected.lastOrNull { it.impulse <= impulse }?.pose ?: position?.let { startingProjectedHelmPose(it) }"
-       ""
-       "fun projectedRangeForecast("
-       "    alphaPosition: JSONObject?,"
-       "    betaPosition: JSONObject?,"
-       "    alphaManeuversRaw: Any?,"
-       "    betaManeuversRaw: Any?,"
-       "    currentImpulse: Int,"
-       "    maxMoves: Int = 8,"
-       "    labelLimit: Int = 5"
-       "): List<ProjectedRangePoint> {"
-       "    if (alphaPosition == null || betaPosition == null) return emptyList()"
-       "    val alphaProjected = projectedHelmMoves("
-       "        alphaPosition,"
-       "        maneuverMapFromJson(alphaManeuversRaw),"
-       "        currentImpulse,"
-       "        maxMoves"
-       "    )"
-       "    val betaProjected = projectedHelmMoves("
-       "        betaPosition,"
-       "        maneuverMapFromJson(betaManeuversRaw),"
-       "        currentImpulse,"
-       "        maxMoves"
-       "    )"
-       "    val impulses = (alphaProjected.map { it.impulse } + betaProjected.map { it.impulse })"
-       "        .distinct()"
-       "        .sorted()"
-       "        .take(labelLimit.coerceAtLeast(1))"
-       "    if (impulses.isEmpty()) return emptyList()"
-       ""
-       "    val out = mutableListOf<ProjectedRangePoint>()"
-       "    var previousRange: Int? = null"
-       "    impulses.forEachIndexed { index, impulse ->"
-       "        val alphaPose = projectedHelmPoseAt(alphaPosition, alphaProjected, impulse) ?: return@forEachIndexed"
-       "        val betaPose = projectedHelmPoseAt(betaPosition, betaProjected, impulse) ?: return@forEachIndexed"
-       "        val range = hexRange(alphaPose.q, alphaPose.r, betaPose.q, betaPose.r)"
-       "        val important = range <= 2 || previousRange == null || range != previousRange || index == impulses.lastIndex"
-       "        previousRange = range"
-       "        if (important) out.add(ProjectedRangePoint(impulse, range))"
-       "    }"
-       "    return out"
-       "}"
-       ""
-       "fun maneuverAbbreviation(maneuver: String): String ="
-       "    when (maneuver) {"
-       "        \"left\" -> \"L\""
-       "        \"right\" -> \"R\""
-       "        \"sideslip-left\" -> \"SL\""
-       "        \"sideslip-right\" -> \"SR\""
-       "        \"het\" -> \"HET\""
-       "        else -> maneuver.take(3).uppercase()"
-       "    }"
-       ""
-       "private fun startingProjectedHelmPose(position: JSONObject): ProjectedHelmPose ="
-       "    ProjectedHelmPose("
-       "        position.optInt(\"q\", 0),"
-       "        position.optInt(\"r\", 0),"
-       "        normalizedHelmFacing(position.optInt(\"facing\", 0)),"
-       "        position.optInt(\"speed\", 0).coerceIn(0, 32),"
-       "        position.optString(\"turn-mode\", \"c\"),"
-       "        position.optInt(\"distance-since-turn\", 0)"
-       "    )"
-       ""
-       "private fun applyProjectedHelmMove(pose: ProjectedHelmPose, maneuver: String?): ProjectedHelmPose ="
-       "    when (maneuver) {"
-       "        \"left\" -> if (canProjectedTurn(pose)) {"
-       "            moveForward(pose.copy(facing = normalizedHelmFacing(pose.facing - 1), distanceSinceTurn = 0))"
-       "        } else {"
-       "            pose"
-       "        }"
-       "        \"right\" -> if (canProjectedTurn(pose)) {"
-       "            moveForward(pose.copy(facing = normalizedHelmFacing(pose.facing + 1), distanceSinceTurn = 0))"
-       "        } else {"
-       "            pose"
-       "        }"
-       "        \"sideslip-left\" -> moveInDirection(pose, normalizedHelmFacing(pose.facing - 1))"
-       "        \"sideslip-right\" -> moveInDirection(pose, normalizedHelmFacing(pose.facing + 1))"
-       "        \"het\" -> moveForward(pose.copy(facing = normalizedHelmFacing(pose.facing + 3), distanceSinceTurn = 0))"
-       "        else -> moveForward(pose)"
-       "    }"
-       ""
-       "private fun moveForward(pose: ProjectedHelmPose): ProjectedHelmPose ="
-       "    moveInDirection(pose, pose.facing)"
-       ""
-       "private fun moveInDirection(pose: ProjectedHelmPose, direction: Int): ProjectedHelmPose {"
-       "    val delta = FACING_DELTAS[normalizedHelmFacing(direction)]"
-       "    return pose.copy("
-       "        q = pose.q + delta.first,"
-       "        r = pose.r + delta.second,"
-       "        distanceSinceTurn = pose.distanceSinceTurn + 1"
-       "    )"
-       "}"
-       ""
-       "private fun canProjectedTurn(pose: ProjectedHelmPose): Boolean ="
-       "    pose.distanceSinceTurn >= turnModeRequiredDistance(pose.turnMode, pose.speed)"
-       ""
-       "private fun turnModeRequiredDistance(turnMode: String, speed: Int): Int {"
-       "    val bands = TURN_MODE_BANDS[turnMode.lowercase()] ?: TURN_MODE_BANDS[\"c\"].orEmpty()"
-       "    val distances = TURN_MODE_DISTANCES[turnMode.lowercase()] ?: TURN_MODE_DISTANCES[\"c\"].orEmpty()"
-       "    for (index in bands.indices) {"
-       "        val band = bands[index]"
-       "        if (speed in band.first..band.second) return distances.getOrElse(index) { 0 }"
-       "    }"
-       "    return 0"
-       "}"
-       ""
-       "private fun normalizedHelmFacing(facing: Int): Int ="
-       "    ((facing % 6) + 6) % 6"
-       ""
-       "private val FACING_DELTAS = arrayOf("
-       "    1 to 0,"
-       "    1 to -1,"
-       "    0 to -1,"
-       "    -1 to 0,"
-       "    -1 to 1,"
-       "    0 to 1"
-       ")"
-       ""
-       "private val TURN_MODE_BANDS = mapOf("
-       "    \"seeking-weapon\" to listOf(1 to 32),"
-       "    \"shuttle\" to listOf(1 to 11, 12 to 23, 24 to 999),"
-       "    \"aa\" to listOf(2 to 8, 9 to 16, 17 to 24, 25 to 999),"
-       "    \"a\" to listOf(2 to 6, 7 to 12, 13 to 19, 20 to 26, 27 to 999),"
-       "    \"b\" to listOf(2 to 5, 6 to 10, 11 to 15, 16 to 21, 22 to 28, 29 to 999),"
-       "    \"c\" to listOf(2 to 4, 5 to 9, 10 to 14, 15 to 20, 21 to 27, 28 to 999),"
-       "    \"d\" to listOf(2 to 4, 5 to 8, 9 to 12, 13 to 17, 18 to 24, 25 to 999),"
-       "    \"e\" to listOf(2 to 3, 4 to 6, 7 to 10, 11 to 14, 15 to 20, 21 to 29, 30 to 999),"
-       "    \"f\" to listOf(2 to 3, 4 to 5, 6 to 9, 10 to 13, 14 to 17, 18 to 23, 24 to 29, 30 to 999)"
-       ")"
-       ""
-       "private val TURN_MODE_DISTANCES = mapOf("
-       "    \"seeking-weapon\" to listOf(1),"
-       "    \"shuttle\" to listOf(1, 2, 3),"
-       "    \"aa\" to listOf(1, 2, 3, 4),"
-       "    \"a\" to listOf(1, 2, 3, 4, 5),"
-       "    \"b\" to listOf(1, 2, 3, 4, 5, 6),"
-       "    \"c\" to listOf(1, 2, 3, 4, 5, 6),"
-       "    \"d\" to listOf(1, 2, 3, 4, 5, 6),"
-       "    \"e\" to listOf(1, 2, 3, 4, 5, 6, 7),"
-       "    \"f\" to listOf(1, 2, 3, 4, 5, 6, 7, 8)"
-       ")"))
+    (typed-kotlin-file "com/jerboa/originaltactics/HexMath.kt"
+      (kotlin-imports (org json JSONArray) (org json JSONObject))
+      (typed-library (com jerboa originaltactics)
+        (export make-ProjectedHelmPose ProjectedHelmPose?
+                make-ProjectedHelmMove ProjectedHelmMove?
+                make-ProjectedRangePoint ProjectedRangePoint?
+                hexRangeFromPositions hexRange currentBattleRange
+                movementImpulsesForSpeed nextMoveImpulse maneuverMapFromJson
+                projectedHelmMoves projectedHelmPoseAt projectedRangeForecast
+                maneuverAbbreviation)
+        (type Any)
+        (type Int32)
+        (type JSONArray)
+        (type JSONObject)
+        (record ProjectedHelmPose
+          ((q : Int32)
+           (r : Int32)
+           (facing : Int32)
+           (speed : Int32)
+           (turnMode : String)
+           (distanceSinceTurn : Int32)))
+        (record ProjectedHelmMove
+          ((impulse : Int32)
+           (maneuver : (Nullable String))
+           (pose : ProjectedHelmPose)))
+        (record ProjectedRangePoint
+          ((impulse : Int32)
+           (range : Int32)))
+        (extern (asJsonArray (value : Any)) : (Nullable JSONArray)
+          (kotlin-safe-cast JSONArray))
+        (extern (asJsonObject (value : Any)) : (Nullable JSONObject)
+          (kotlin-safe-cast JSONObject))
+        (extern (anyToString (value : Any)) : String
+          (kotlin-member-call toString))
+        (extern (jsonObjectKeys (json : JSONObject)) : (Iterator String)
+          (kotlin-member-call keys))
+        (extern (iteratorHasNext (items : (Iterator String))) : Bool
+          (kotlin-member-call hasNext))
+        (extern (iteratorNext (items : (Iterator String))) : String
+          (kotlin-member-call next))
+        (def (int32Abs (value : Int32)) : Int32
+          (if (< value (int32 0)) (- (int32 0) value) value))
+        (def (clampImpulse (value : Int32)) : Int32
+          (if (< value (int32 0))
+            (int32 0)
+            (if (> value (int32 32)) (int32 32) value)))
+        (def (atLeastOne (value : Int32)) : Int32
+          (if (< value (int32 1)) (int32 1) value))
+        (def (betweenInt32?
+               (value : Int32)
+               (low : Int32)
+               (high : Int32)) : Bool
+          (and (>= value low) (<= value high)))
+        (def (hexRangeFromPositions
+               (alpha : (Nullable JSONObject))
+               (beta : (Nullable JSONObject))) : (Nullable Int32)
+          (if (or (nullable-null? alpha) (nullable-null? beta))
+            (nullable-none Int32)
+            (nullable-some
+              (hexRange
+                (json-object-opt-int32-default (nullable-get alpha) "q" (int32 0))
+                (json-object-opt-int32-default (nullable-get alpha) "r" (int32 0))
+                (json-object-opt-int32-default (nullable-get beta) "q" (int32 0))
+                (json-object-opt-int32-default (nullable-get beta) "r" (int32 0))))))
+        (def (hexRange
+               (q1 : Int32)
+               (r1 : Int32)
+               (q2 : Int32)
+               (r2 : Int32)) : Int32
+          (let ((dq (- q2 q1))
+                (dr (- r2 r1)))
+            (let ((ds (- (int32 0) (+ dq dr))))
+              (/ (+ (+ (int32Abs dq) (int32Abs dr)) (int32Abs ds))
+                 (int32 2)))))
+        (def (nullableAnyToString
+               (value : (Nullable Any))
+               (fallback : String)) : String
+          (if (nullable-null? value)
+            fallback
+            (anyToString (nullable-get value))))
+        (def (currentBattleRange
+               (battle : (Nullable JSONObject))) : String
+          (if (nullable-null? battle)
+            "-"
+            (let ((computed
+                    (hexRangeFromPositions
+                      (json-object-opt-json-object
+                        (nullable-get battle)
+                        "alpha-position")
+                      (json-object-opt-json-object
+                        (nullable-get battle)
+                        "beta-position"))))
+              (if (nullable-null? computed)
+                (nullableAnyToString
+                  (json-object-opt-any (nullable-get battle) "range")
+                  "-")
+                (int32->string (nullable-get computed))))))
+        (def (movementImpulsesForSpeed (speed : Int32)) : (MutableList Int32)
+          (let ((moves (clampImpulse speed))
+                (out (mutable-list-empty Int32)))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((step (in-range (int32 1) (+ moves (int32 1)))))
+                (begin
+                  (mutable-list-add!
+                    out
+                    (/ (+ (- (* step (int32 32)) (int32 1)) moves)
+                       moves))
+                  ignored))
+              out)))
+        (def (nextMoveImpulse
+               (position : (Nullable JSONObject))
+               (currentImpulse : Int32)) : (Nullable Int32)
+          (if (nullable-null? position)
+            (nullable-none Int32)
+            (let ((moves
+                    (movementImpulsesForSpeed
+                      (json-object-opt-int32-default
+                        (nullable-get position)
+                        "speed"
+                        (int32 0)))))
+              (for/fold ((found (nullable-none Int32)))
+                        ((index (in-range (int32 0) (list-size moves))))
+                (if (nullable-null? found)
+                  (let ((candidate (list-ref moves index)))
+                    (if (> candidate currentImpulse)
+                      (nullable-some candidate)
+                      found))
+                  found)))))
+        (def (validManeuverPair?
+               (impulse : Int32)
+               (maneuver : String)) : Bool
+          (and (betweenInt32? impulse (int32 1) (int32 32))
+               (not (string-blank? maneuver))))
+        (def (addManeuverPair!
+               (out : (MutableMap Int32 String))
+               (impulse : Int32)
+               (maneuver : String)) : Unit
+          (if (validManeuverPair? impulse maneuver)
+            (mutable-map-put! out impulse maneuver)
+            (begin)))
+        (def (addManeuverArray!
+               (out : (MutableMap Int32 String))
+               (pair : JSONArray)) : Unit
+          (addManeuverPair!
+            out
+            (json-array-opt-int32 pair (int32 0) (int32 -1))
+            (json-array-opt-string pair (int32 1))))
+        (def (addManeuverObjectPair!
+               (out : (MutableMap Int32 String))
+               (item : JSONObject)) : Unit
+          (let ((pair (json-object-opt-json-array item "$pair")))
+            (if (nullable-null? pair)
+              (begin)
+              (addManeuverArray! out (nullable-get pair)))))
+        (def (addManeuverRawItem!
+               (out : (MutableMap Int32 String))
+               (item : (Nullable Any))) : Unit
+          (if (nullable-null? item)
+            (begin)
+            (let ((asArray (asJsonArray (nullable-get item)))
+                  (asObject (asJsonObject (nullable-get item))))
+              (if (nullable-null? asArray)
+                (if (nullable-null? asObject)
+                  (begin)
+                  (addManeuverObjectPair! out (nullable-get asObject)))
+                (addManeuverArray! out (nullable-get asArray))))))
+        (def (addManeuverArrayItems!
+               (out : (MutableMap Int32 String))
+               (raw : JSONArray)) : Unit
+          (begin
+            (for/fold ((ignored (int32 0)))
+                      ((index (in-range (int32 0) (json-array-length raw))))
+              (begin
+                (addManeuverRawItem!
+                  out
+                  (json-array-opt-any raw index))
+                ignored))
+            (begin)))
+        (def (addManeuverObjectItems!
+               (out : (MutableMap Int32 String))
+               (raw : JSONObject)) : Unit
+          (let ((keys (jsonObjectKeys raw)))
+            (while (iteratorHasNext keys)
+              (let ((key (iteratorNext keys)))
+                (let ((impulse (string->int32-or-null key)))
+                  (if (nullable-null? impulse)
+                    (begin)
+                    (addManeuverPair!
+                      out
+                      (nullable-get impulse)
+                      (json-object-opt-string-default raw key ""))))))))
+        (def (maneuverMapFromJson
+               (raw : (Nullable Any))) : (MutableMap Int32 String)
+          (let ((out (mutable-map-empty Int32 String)))
+            (begin
+              (if (nullable-null? raw)
+                (begin)
+                (let ((asArray (asJsonArray (nullable-get raw)))
+                      (asObject (asJsonObject (nullable-get raw))))
+                  (if (nullable-null? asArray)
+                    (if (nullable-null? asObject)
+                      (begin)
+                      (addManeuverObjectItems! out (nullable-get asObject)))
+                    (addManeuverArrayItems! out (nullable-get asArray)))))
+              out)))
+        (def (startingProjectedHelmPose
+               (position : JSONObject)) : ProjectedHelmPose
+          (make-ProjectedHelmPose
+            (json-object-opt-int32-default position "q" (int32 0))
+            (json-object-opt-int32-default position "r" (int32 0))
+            (normalizedHelmFacing
+              (json-object-opt-int32-default position "facing" (int32 0)))
+            (clampImpulse
+              (json-object-opt-int32-default position "speed" (int32 0)))
+            (json-object-opt-string-default position "turn-mode" "c")
+            (json-object-opt-int32-default
+              position
+              "distance-since-turn"
+              (int32 0))))
+        (def (projectedHelmMoves
+               (position : (Nullable JSONObject))
+               (maneuvers : (Map Int32 String))
+               (currentImpulse : Int32)
+               (maxMoves : Int32)) : (MutableList ProjectedHelmMove)
+          (let ((out (mutable-list-empty ProjectedHelmMove)))
+            (if (nullable-null? position)
+              out
+              (let ((movementImpulses
+                      (movementImpulsesForSpeed
+                        (json-object-opt-int32-default
+                          (nullable-get position)
+                          "speed"
+                          (int32 0)))))
+                (var ((pose (startingProjectedHelmPose (nullable-get position))))
+                  (begin
+                    (for/fold ((ignored (int32 0)))
+                              ((index (in-range (int32 0)
+                                                (list-size movementImpulses))))
+                      (let ((impulse (list-ref movementImpulses index)))
+                        (if (and (> impulse currentImpulse)
+                                 (< (list-size out)
+                                    (atLeastOne maxMoves)))
+                          (let ((maneuver (map-ref-or-null maneuvers impulse)))
+                            (begin
+                              (set! pose
+                                (applyProjectedHelmMove pose maneuver))
+                              (mutable-list-add!
+                                out
+                                (make-ProjectedHelmMove
+                                  impulse
+                                  maneuver
+                                  pose))
+                              ignored))
+                          ignored)))
+                    out))))))
+        (def (projectedHelmPoseAt
+               (position : (Nullable JSONObject))
+               (projected : (List ProjectedHelmMove))
+               (impulse : Int32)) : (Nullable ProjectedHelmPose)
+          (let ((found
+                  (for/fold ((candidate (nullable-none ProjectedHelmPose)))
+                            ((index (in-range (int32 0) (list-size projected))))
+                    (let ((move (list-ref projected index)))
+                      (if (<= (ProjectedHelmMove-impulse move) impulse)
+                        (nullable-some (ProjectedHelmMove-pose move))
+                        candidate)))))
+            (if (nullable-null? found)
+              (if (nullable-null? position)
+                (nullable-none ProjectedHelmPose)
+                (nullable-some
+                  (startingProjectedHelmPose (nullable-get position))))
+              found)))
+        (def (hasProjectedImpulse?
+               (projected : (List ProjectedHelmMove))
+               (impulse : Int32)) : Bool
+          (for/fold ((found #f))
+                    ((index (in-range (int32 0) (list-size projected))))
+            (or found
+                (= (ProjectedHelmMove-impulse
+                     (list-ref projected index))
+                   impulse))))
+        (def (projectedRangeForecast
+               (alphaPosition : (Nullable JSONObject))
+               (betaPosition : (Nullable JSONObject))
+               (alphaManeuversRaw : (Nullable Any))
+               (betaManeuversRaw : (Nullable Any))
+               (currentImpulse : Int32)
+               (maxMoves : Int32 (default (int32 8)))
+               (labelLimit : Int32 (default (int32 5)))) : (MutableList ProjectedRangePoint)
+          (let ((out (mutable-list-empty ProjectedRangePoint)))
+            (if (or (nullable-null? alphaPosition)
+                    (nullable-null? betaPosition))
+              out
+              (let ((alphaProjected
+                      (projectedHelmMoves
+                        alphaPosition
+                        (maneuverMapFromJson alphaManeuversRaw)
+                        currentImpulse
+                        maxMoves))
+                    (betaProjected
+                      (projectedHelmMoves
+                        betaPosition
+                        (maneuverMapFromJson betaManeuversRaw)
+                        currentImpulse
+                        maxMoves)))
+                (var ((previousRange (nullable-none Int32)))
+                  (begin
+                    (for/fold ((ignored (int32 0)))
+                              ((impulse (in-range (int32 1) (int32 33))))
+                      (if (and (< (list-size out) (atLeastOne labelLimit))
+                               (or (hasProjectedImpulse? alphaProjected impulse)
+                                   (hasProjectedImpulse? betaProjected impulse)))
+                        (let ((alphaPose
+                                (projectedHelmPoseAt
+                                  alphaPosition
+                                  alphaProjected
+                                  impulse))
+                              (betaPose
+                                (projectedHelmPoseAt
+                                  betaPosition
+                                  betaProjected
+                                  impulse)))
+                          (if (or (nullable-null? alphaPose)
+                                  (nullable-null? betaPose))
+                            ignored
+                            (let ((range
+                                    (hexRange
+                                      (ProjectedHelmPose-q
+                                        (nullable-get alphaPose))
+                                      (ProjectedHelmPose-r
+                                        (nullable-get alphaPose))
+                                      (ProjectedHelmPose-q
+                                        (nullable-get betaPose))
+                                      (ProjectedHelmPose-r
+                                        (nullable-get betaPose)))))
+                              (let ((important
+                                      (or (<= range (int32 2))
+                                          (or (nullable-null? previousRange)
+                                              (not
+                                                (= range
+                                                   (nullable-get
+                                                     previousRange)))))))
+                                (begin
+                                  (set! previousRange
+                                    (nullable-some range))
+                                  (if important
+                                    (mutable-list-add!
+                                      out
+                                      (make-ProjectedRangePoint
+                                        impulse
+                                        range))
+                                    (begin))
+                                  ignored)))))
+                        ignored))
+                    out))))))
+        (def (maneuverAbbreviation (maneuver : String)) : String
+          (if (equal? maneuver "left")
+            "L"
+            (if (equal? maneuver "right")
+              "R"
+              (if (equal? maneuver "sideslip-left")
+                "SL"
+                (if (equal? maneuver "sideslip-right")
+                  "SR"
+                  (if (equal? maneuver "het")
+                    "HET"
+                    (string-uppercase
+                      (string-take maneuver (int32 3)))))))))
+        (def (applyProjectedHelmMove
+               (pose : ProjectedHelmPose)
+               (maneuver : (Nullable String))) : ProjectedHelmPose
+          (if (nullable-null? maneuver)
+            (moveForward pose)
+            (let ((name (nullable-get maneuver)))
+              (if (equal? name "left")
+                (if (canProjectedTurn? pose)
+                  (moveForward
+                    (make-ProjectedHelmPose
+                      (ProjectedHelmPose-q pose)
+                      (ProjectedHelmPose-r pose)
+                      (normalizedHelmFacing
+                        (- (ProjectedHelmPose-facing pose) (int32 1)))
+                      (ProjectedHelmPose-speed pose)
+                      (ProjectedHelmPose-turnMode pose)
+                      (int32 0)))
+                  pose)
+                (if (equal? name "right")
+                  (if (canProjectedTurn? pose)
+                    (moveForward
+                      (make-ProjectedHelmPose
+                        (ProjectedHelmPose-q pose)
+                        (ProjectedHelmPose-r pose)
+                        (normalizedHelmFacing
+                          (+ (ProjectedHelmPose-facing pose) (int32 1)))
+                        (ProjectedHelmPose-speed pose)
+                        (ProjectedHelmPose-turnMode pose)
+                        (int32 0)))
+                    pose)
+                  (if (equal? name "sideslip-left")
+                    (moveInDirection
+                      pose
+                      (normalizedHelmFacing
+                        (- (ProjectedHelmPose-facing pose) (int32 1))))
+                    (if (equal? name "sideslip-right")
+                      (moveInDirection
+                        pose
+                        (normalizedHelmFacing
+                          (+ (ProjectedHelmPose-facing pose) (int32 1))))
+                      (if (equal? name "het")
+                        (moveForward
+                          (make-ProjectedHelmPose
+                            (ProjectedHelmPose-q pose)
+                            (ProjectedHelmPose-r pose)
+                            (normalizedHelmFacing
+                              (+ (ProjectedHelmPose-facing pose) (int32 3)))
+                            (ProjectedHelmPose-speed pose)
+                            (ProjectedHelmPose-turnMode pose)
+                            (int32 0)))
+                        (moveForward pose)))))))))
+        (def (moveForward (pose : ProjectedHelmPose)) : ProjectedHelmPose
+          (moveInDirection pose (ProjectedHelmPose-facing pose)))
+        (def (directionDeltaQ (direction : Int32)) : Int32
+          (let ((facing (normalizedHelmFacing direction)))
+            (if (or (= facing (int32 0)) (= facing (int32 1)))
+              (int32 1)
+              (if (or (= facing (int32 3)) (= facing (int32 4)))
+                (int32 -1)
+                (int32 0)))))
+        (def (directionDeltaR (direction : Int32)) : Int32
+          (let ((facing (normalizedHelmFacing direction)))
+            (if (or (= facing (int32 1)) (= facing (int32 2)))
+              (int32 -1)
+              (if (or (= facing (int32 4)) (= facing (int32 5)))
+                (int32 1)
+                (int32 0)))))
+        (def (moveInDirection
+               (pose : ProjectedHelmPose)
+               (direction : Int32)) : ProjectedHelmPose
+          (make-ProjectedHelmPose
+            (+ (ProjectedHelmPose-q pose) (directionDeltaQ direction))
+            (+ (ProjectedHelmPose-r pose) (directionDeltaR direction))
+            (ProjectedHelmPose-facing pose)
+            (ProjectedHelmPose-speed pose)
+            (ProjectedHelmPose-turnMode pose)
+            (+ (ProjectedHelmPose-distanceSinceTurn pose) (int32 1))))
+        (def (canProjectedTurn? (pose : ProjectedHelmPose)) : Bool
+          (>= (ProjectedHelmPose-distanceSinceTurn pose)
+              (turnModeRequiredDistance
+                (ProjectedHelmPose-turnMode pose)
+                (ProjectedHelmPose-speed pose))))
+        (def (turnModeRequiredDistance
+               (turnMode : String)
+               (speed : Int32)) : Int32
+          (let ((mode (string-lowercase turnMode)))
+            (if (equal? mode "seeking-weapon")
+              (if (betweenInt32? speed (int32 1) (int32 32))
+                (int32 1)
+                (int32 0))
+              (if (equal? mode "shuttle")
+                (turnModeShuttleDistance speed)
+                (if (equal? mode "aa")
+                  (turnModeAaDistance speed)
+                  (if (equal? mode "a")
+                    (turnModeADistance speed)
+                    (if (equal? mode "b")
+                      (turnModeBDistance speed)
+                      (if (equal? mode "d")
+                        (turnModeDDistance speed)
+                        (if (equal? mode "e")
+                          (turnModeEDistance speed)
+                          (if (equal? mode "f")
+                            (turnModeFDistance speed)
+                            (turnModeCDistance speed)))))))))))
+        (def (turnModeShuttleDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 1) (int32 11))
+            (int32 1)
+            (if (betweenInt32? speed (int32 12) (int32 23))
+              (int32 2)
+              (if (>= speed (int32 24)) (int32 3) (int32 0)))))
+        (def (turnModeAaDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 8))
+            (int32 1)
+            (if (betweenInt32? speed (int32 9) (int32 16))
+              (int32 2)
+              (if (betweenInt32? speed (int32 17) (int32 24))
+                (int32 3)
+                (if (>= speed (int32 25)) (int32 4) (int32 0))))))
+        (def (turnModeADistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 6))
+            (int32 1)
+            (if (betweenInt32? speed (int32 7) (int32 12))
+              (int32 2)
+              (if (betweenInt32? speed (int32 13) (int32 19))
+                (int32 3)
+                (if (betweenInt32? speed (int32 20) (int32 26))
+                  (int32 4)
+                  (if (>= speed (int32 27)) (int32 5) (int32 0)))))))
+        (def (turnModeBDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 5))
+            (int32 1)
+            (if (betweenInt32? speed (int32 6) (int32 10))
+              (int32 2)
+              (if (betweenInt32? speed (int32 11) (int32 15))
+                (int32 3)
+                (if (betweenInt32? speed (int32 16) (int32 21))
+                  (int32 4)
+                  (if (betweenInt32? speed (int32 22) (int32 28))
+                    (int32 5)
+                    (if (>= speed (int32 29)) (int32 6) (int32 0))))))))
+        (def (turnModeCDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 4))
+            (int32 1)
+            (if (betweenInt32? speed (int32 5) (int32 9))
+              (int32 2)
+              (if (betweenInt32? speed (int32 10) (int32 14))
+                (int32 3)
+                (if (betweenInt32? speed (int32 15) (int32 20))
+                  (int32 4)
+                  (if (betweenInt32? speed (int32 21) (int32 27))
+                    (int32 5)
+                    (if (>= speed (int32 28)) (int32 6) (int32 0))))))))
+        (def (turnModeDDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 4))
+            (int32 1)
+            (if (betweenInt32? speed (int32 5) (int32 8))
+              (int32 2)
+              (if (betweenInt32? speed (int32 9) (int32 12))
+                (int32 3)
+                (if (betweenInt32? speed (int32 13) (int32 17))
+                  (int32 4)
+                  (if (betweenInt32? speed (int32 18) (int32 24))
+                    (int32 5)
+                    (if (>= speed (int32 25)) (int32 6) (int32 0))))))))
+        (def (turnModeEDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 3))
+            (int32 1)
+            (if (betweenInt32? speed (int32 4) (int32 6))
+              (int32 2)
+              (if (betweenInt32? speed (int32 7) (int32 10))
+                (int32 3)
+                (if (betweenInt32? speed (int32 11) (int32 14))
+                  (int32 4)
+                  (if (betweenInt32? speed (int32 15) (int32 20))
+                    (int32 5)
+                    (if (betweenInt32? speed (int32 21) (int32 29))
+                      (int32 6)
+                      (if (>= speed (int32 30)) (int32 7) (int32 0)))))))))
+        (def (turnModeFDistance (speed : Int32)) : Int32
+          (if (betweenInt32? speed (int32 2) (int32 3))
+            (int32 1)
+            (if (betweenInt32? speed (int32 4) (int32 5))
+              (int32 2)
+              (if (betweenInt32? speed (int32 6) (int32 9))
+                (int32 3)
+                (if (betweenInt32? speed (int32 10) (int32 13))
+                  (int32 4)
+                  (if (betweenInt32? speed (int32 14) (int32 17))
+                    (int32 5)
+                    (if (betweenInt32? speed (int32 18) (int32 23))
+                      (int32 6)
+                      (if (betweenInt32? speed (int32 24) (int32 29))
+                        (int32 7)
+                        (if (>= speed (int32 30)) (int32 8) (int32 0))))))))))
+        (def (normalizedHelmFacing (facing : Int32)) : Int32
+          (mod (+ (mod facing (int32 6)) (int32 6)) (int32 6)))))
     (kotlin-file-lines "com/jerboa/originaltactics/MainActivity.kt"
       ("package com.jerboa.originaltactics"
        ""