{
 "chunk": "game_main_0800_403B",
 "unit": "game/main_0800",
 "routines": [
  {
   "addr": "403B",
   "name": "markComcenRevealed",
   "summary": "Misclassified as data (35 bytes, see misclassified). Called from the radar ($53BC) and drone ($53F1) command handlers after a launch: selects the side (own side = playerSide when X=0, opponent side = playerSide EOR 1 when X<>0), sets comcenRevealTimer[side] = 6 rounds, sets the visibility bits 6-7 of that side's comcen in unitEnergyTable (comcenIndexBySide gives $31/$63), and for the opponent's action also queues the comcen for redraw in pendingRedrawUnit. Using radar or drones therefore exposes the launching comcen for 6 rounds.",
   "inputs": "X = 0 own side / nonzero opponent side; playerSide $0B9F; comcenIndexBySide $0C8A",
   "outputs": "comcenRevealTimer[side] = 6; unitEnergyTable[comcen] |= $C0; pendingRedrawUnit = comcen index when X<>0; A/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "406F",
   "name": "waitFramesOrInput",
   "summary": "Runs the game engine for up to A frames (one runGameFrames(1) per iteration) but returns early on a fresh fire-button press (sub_0EEA returns Z: button down after having been released, debounce expired) or the joystick leaves centre (joyDirection <> $0F), or when gameEndFlag becomes nonzero. Used by the overlay/menu code ($63C5, $6457, $64C5) for interruptible pauses while the battle keeps running. The frame count is kept in a self-modified operand at $4086.",
   "inputs": "A = number of frames; Y = value to return in Y (stored into the operand at $4092)",
   "outputs": "Y restored to the entry value; A/X clobbered; game state advanced by the ticks that ran",
   "confidence": "high"
  },
  {
   "addr": "4094",
   "name": "runGameFrames",
   "summary": "Advances the game by A frames: each iteration waits one frame (sub_C37F with A=1) and calls updateGameFrame. Stops early when gameEndFlag ($92C9) is nonzero. A=0 returns immediately. Called from many wait loops in the main program and both overlays so that the real-time battle continues while the UI is busy. Entry Y is preserved via the self-modified operand at $40B0.",
   "inputs": "A = frame count; Y preserved",
   "outputs": "Y = entry value; A/X clobbered",
   "confidence": "high"
  },
  {
   "addr": "40B2",
   "name": "updateGameFrame",
   "summary": "The per-frame game update, called from every main/UI loop. (1) sub_C86D leaves to the menu when gameEndFlag bit 7 is set. (2) Film playback: digit keys 1-9 set the step interval stepIntervalFrames = 9 - digit and queue message 7 'SPEED n'; otherwise during the comcen knock-out round ($922A bit 6) it only runs the chat handler and skips to the engine. (3) Status line update (sub_C4EF); on the battlefield screen the HOME key ($8C) is consumed and, in a film, cycles filmPlaybackFlag $80/$81/$82 = SHOW US/THEM/BOTH (names from $86FB copied into message 6 at $8726) and redraws the radar map and view. (4) Chat/pause keys (sub_C70A); expired timers switch off marker sprites 3 and 4 and redraw the view when viewRedrawTimer reached $80. (5) Pending single-unit (pendingRedrawUnit) and group (pendingRedrawGroup) redraws. (6) Engine: when stepCountdown ($90FC) is 0, reload it with stepIntervalFrames (0 in the setup phase), and every 10th unit (50th in setup) of the round apply the queued/received commands for time slice commandSlice = unitOrderIndex/10 via sub_4DB7, postponing the step while opponentWaitTimer is nonzero; then tick the hit-flash timers and process the next unit of the round order (processUnitStep at L_4213) or, after all 100 units, finish the round (finishRound at L_45E6).",
   "inputs": "$0B9B filmPlaybackFlag, $9248 lastKeyPressed, $90FB currentScreenId, $90FC stepCountdown, $9162 unitOrderIndex, $90FD unitProcessOrder, $92E3 stepIntervalFrames, zp_B1 gamePhase, timers $9234/$91C8/$923E, $923A/$923B redraw requests",
   "outputs": "advances one unit step or finishes a round; updates map/screen, sprites ($9022, $9010/$9018), message queue; may not return (menu exit via sub_C86D, knock-out round via L_0709)",
   "confidence": "high"
  },
  {
   "addr": "4213",
   "name": "processUnitStep",
   "summary": "Internal entry of updateGameFrame (reached by the unit-step branch, not by JSR) that processes one unit X of the round order: sets currentUnit $90FA; in battle shuffles two entries of unitBaseOrder with the lock-step RNG; skips dead units and everything while unitsFrozenFlags bit 7 is set; stunned units lose one stun count, get 0 movement points and are halted (sub_3FAC). Otherwise it refreshes the stationary flag (updateIdleFlag), turns the unit toward its destination (facing bits 0-1 of unitStateFlags via sub_3767), realises a unit whose path position is not its map position by walking back toward it to the first free cell (moveUnitToCell), then takes one step toward the destination with tryStepTowardTarget: blocked -> movement points = 15; a step onto a unit only updates the path position ($F898/$F8FC); a step onto free terrain moves the unit (moveUnitToCell); blitzing units pay energy for +25 points (sub_3EE2). It redraws the vacated cell, follows the tracked unit (followDirectedUnit), copies the stationary flag into unitOrderFlags bit 7, spots enemies in sight range (sub_35C3 X=0 -> visibility bits), chooses the nearest enemy in fire range (sub_35C3 X=$80, boomers via sub_3C85; comcens win ties; spies/comcens/cloaked units never fire), fires every other round ((roundsRemaining EOR unit) AND 1 = 0) through sub_38C2 and clears the reload bit on the same parity, then handles stationary units: blitz off, repair flag cleared, dig-in progress (+$20 per round in $F960 bits 5-7, fortified flag bit 6 of unitTypeTable when complete) or +1 energy per round with the repair flag set. Finally the comm-build-1 hook sub_E0E8 (probably the practice-mode AI) is called, the pinned bit is cleared and the unit is redrawn.",
   "inputs": "X = unit index, $9162 position in the round order; unit record arrays $F640-$FBB7; zp_B1 gamePhase; $0BA8 noGameInProgress",
   "outputs": "unit record updated (position, path position, movement points $FA28, flags, energy), map cells and screen updated, unitOrderIndex incremented at L_4537, comcenStunStatus refreshed from the own comcen's stun count",
   "confidence": "medium"
  },
  {
   "addr": "4559",
   "name": "tryStepTowardTarget",
   "summary": "Moves the line walker one cell from the current cell ($93,$92) toward the target ($95,$94) and decides whether currentUnit may enter that cell. Returns C=1 with $19 = 0 when the line has no steps (already there). Off-map cells (col/row >= 40) are refused (C=0, X=$FF). If the cell holds a unit, entering is allowed only for spies (type 3), for a cloaked mover when the occupant is cloaked too, for a same-side occupant (bit 5 of unitStateFlags = side) or when the occupant is a spy; otherwise C=0, X=$FF. Then the movement cost is looked up: terrain class = group class ($91F4 bits 3-5) for grouped units, else terrainClassTable[$077E][terrain under the unit] (0 for spies), cost = terrainMoveCostTable[class] (+8 for a diagonal step, zp_A9). With a negative movement-point balance or insufficient points it returns C=0 with X = unit index (wait). Otherwise C=1, A = remaining movement points.",
   "inputs": "$92/$93 current row/col, $94/$95 target row/col (from initLineStepper inputs), $90FA currentUnit, unit arrays $F76C/$F898/$F708/$FA28/$FAF0, $91F4 groupMoveTable",
   "outputs": "C=1: step accepted, A = movement points left, $19 = destination cell byte, $92/$93 advanced, zp_48 -> destination cell, X = unit; C=0: X=$FF blocked/off-map or X=unit not enough points; line state $8C-$97 modified",
   "confidence": "high"
  },
  {
   "addr": "4751",
   "name": "moveUnitToCell",
   "summary": "Moves currentUnit to the cell ($93,$92) whose byte is in $19: writes unit|$80 into the destination cell (zp_48 must still point at it), restores the saved terrain (unitSavedTerrain) into the old cell, updates unitCol/unitRow and saves the new terrain. Then scores territory: the column is mirrored for side-1 units (sub_3FA3) so that col < 10 is the far end zone (value 2) and col < 20 the near zone (value 1); a unit scoring a zone for the first time (bits 3/4 of unitTypeTable from zoneScoredBitTable) adds scoreValueBySide[side] points (x16 for a comcen, x2 for the far zone) to the 16-bit opponent score pointsAgainstSide[side EOR 1] and bumps scoreChangeCounter. Sets unitMovedFlag and unitActivityFlag.",
   "inputs": "$90FA currentUnit, $93/$92 destination col/row, $19 destination cell byte, zp_48 -> destination cell, $9228 currentUnitType, $92AC scoreValueBySide",
   "outputs": "map cells, $F640/$F6A4/$FAF0/$F76C of the unit, $92A2/$92A4 scores, $9208, $406E = 1, $92C2 = 1; zp_48 low byte clobbered",
   "confidence": "high"
  },
  {
   "addr": "47ED",
   "name": "shuffleUnitOrder",
   "summary": "Swaps two randomly chosen entries of unitBaseOrder ($9164, 100 unit indices) using randomUnitIndex, which draws from the lock-step scenario RNG so both machines shuffle identically. Called once per processed unit during battle and by overlay A at game start ($7BBC). The leading NOP is probably a disabled instruction byte.",
   "inputs": "$9164 unitBaseOrder, RNG state $5A-$5C",
   "outputs": "two entries of $9164 exchanged; A/X/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "4805",
   "name": "randomUnitIndex",
   "summary": "Returns a uniformly distributed unit index 0-99: takes nextScenarioRandom ($FD4A) AND $7F and retries while the value is >= 100.",
   "inputs": "RNG state $5A-$5C",
   "outputs": "A = 0..99; RNG advanced",
   "confidence": "high"
  },
  {
   "addr": "480F",
   "name": "followDirectedUnit",
   "summary": "While the battlefield screen is shown and trackedUnit ($91D7) equals currentUnit: computes the cursor cell (viewOriginCol+3, viewOriginRow+2) into $91DA/$91DB and, if the unit is not on it, places sprite 0 (radar cursor, $9010/$9018 via sub_3023), enables it (sub_31A3) and scrolls the view instantly to the unit (sub_1D4E with A=0). Keeps the view locked on the unit whose menu/targeting is open.",
   "inputs": "$90FB currentScreenId, $91D7 trackedUnit, $90FA currentUnit, zp_A5/zp_A6 view origin, $F640/$F6A4",
   "outputs": "$91DA/$91DB = cursor cell, sprite 0 position/enable, view scrolled ($92/$93, zp_A5/zp_A6)",
   "confidence": "medium"
  },
  {
   "addr": "4860",
   "name": "updateHitFlashTimers",
   "summary": "For every unit with a nonzero unitHitFlashTimer ($FA8C) decrements the low nibble (high nibble kept). When it reaches 0 the byte is cleared and the cell is redrawn: a living unit is drawn again (unit|$80), a dead one has its saved terrain written back into the map (sub_3EC2) and drawn. Called once per engine step.",
   "inputs": "$FA8C unitHitFlashTimer, $F76C unitTypeTable, $FAF0 unitSavedTerrain",
   "outputs": "$FA8C updated, map/screen cells redrawn; zp_18 used as loop index",
   "confidence": "high"
  },
  {
   "addr": "48A2",
   "name": "drawUnitAtPosition",
   "summary": "Draws unit Y on the map display: A = Y|$80 (the map byte of a unit) and falls into drawValueAtUnit.",
   "inputs": "Y = unit index",
   "outputs": "see drawValueAtUnit",
   "confidence": "high"
  },
  {
   "addr": "48A5",
   "name": "drawValueAtUnit",
   "summary": "Draws cell value A at unit Y's position: loads X = unitCol[Y], Y = unitRow[Y] and jumps to drawMapCell (sub_5C7C), which updates the battlefield view and the radar map.",
   "inputs": "A = cell byte to draw, Y = unit index",
   "outputs": "screen updated; A/X/Y clobbered",
   "confidence": "high"
  },
  {
   "addr": "48B1",
   "name": "updateIdleFlag",
   "summary": "Sets bit 7 of unitDestCol ($F7D0) for currentUnit when its destination ($F7D0/$F834 low 6 bits) equals its map position, clears it otherwise; the other bits are kept. Bit 7 = unit is stationary (no pending move). Returns the new byte in A and Y = currentUnit.",
   "inputs": "$90FA currentUnit, $F640/$F6A4/$F7D0/$F834",
   "outputs": "A = new $F7D0 byte (bit 7 = stationary), Y = currentUnit, $F7D0 updated",
   "confidence": "high"
  },
  {
   "addr": "48DD",
   "name": "rebuildGroupTables",
   "summary": "Recomputes the 16-entry groupMoveTable ($91F4) at the end of each round. Saves the old entries to $0430, clears per-group scratch (member count $0400, worst terrain cost $0410, best rate $0420 = $FF). For every living grouped unit (unitStateFlags bit 6; group = bits 2-5) it computes the speed class (type, or comcenSpeedClassOwn/Opponent for the comcens) and terrain class (0 for class 3 = spies, else terrainClassTable of the terrain under the unit): the group keeps the highest terrain cost (class in bits 3-5) and the lowest rate (class in bits 0-2), i.e. a group moves like its slowest member on the worst ground. Groups with members get bits 6-7 of their previous entry back.",
   "inputs": "unit arrays $F76C/$F708/$FAF0, $92B4/$92B5 comcen speed classes, $9236 ownComcenUnit, tables $405E/$4066/$077E",
   "outputs": "$91F4-$9203 rewritten; $0400-$043F scratch; zp_18/zp_19/zp_1A clobbered",
   "confidence": "high"
  },
  {
   "addr": "45E6",
   "name": "finishRound",
   "summary": "Internal label reached by JMP from updateGameFrame once all 100 units were processed. In a running game it advances the game clock: roundsRemaining ($91CB) is decremented (in the setup phase only when unitActivityFlag was set); at 0 the game ends on time (noGameInProgress = 8, sub_4027 decides the winner on points); when it equals lastQuarterRound message $1D 'LAST QTR WARNING!' is queued and $92FA = $80 (setup phase: wrap to $FF). It then runs the per-side comcenRevealTimer (keeps an exposed comcen visible), and in battle calls the comm-build-1 hook sub_E079, checkRecyclerSpotted (sub_3F57), recycleDeadUnits (sub_3E23), rebuildGroupTables and the visibility shift sub_3576, gives every unit its movement points (stationary/dead units: 15; moving units: + speedClassRateTable[class] where class = type, the group's class, or the comcen classes $92B4/$92B5, saturating at 127) and clears the $0400 scratch. Finally it builds the next round's processing order unitProcessOrder: units are taken from unitBaseOrder, but a unit whose next step lands on a not-yet-ordered unit is pushed on a stack ($0464) so that the blocking unit is ordered first ($0400 marks: 0 = unordered, 1 = on the stack, 5 = ordered), then stepCountdown and unitOrderIndex are reset to 0.",
   "inputs": "$91CB, $92EF, $92C2, zp_B1, $0BA8, $92B2/$92B3, unit arrays, $9164 unitBaseOrder",
   "outputs": "$90FD unitProcessOrder rebuilt, $9162 = 0, $90FC = 0, $FA28 movement points, $91F4, visibility bits, messages; zp_1A/zp_1B used as stack/list indices",
   "confidence": "high"
  }
 ],
 "variables": [
  {
   "addr": "18",
   "scope": "zp",
   "name": "tempByte18",
   "meaning": "scratch: loop index (unit), stun count, remaining-points temp, speed class",
   "confidence": "high"
  },
  {
   "addr": "19",
   "scope": "zp",
   "name": "tempByte19",
   "meaning": "scratch: destination cell byte returned by tryStepTowardTarget (bit 7 = a unit, low 7 bits = its index), terrain class, flags",
   "confidence": "high"
  },
  {
   "addr": "1A",
   "scope": "zp",
   "name": "tempByte1A",
   "meaning": "scratch: unit loop index in rebuildGroupTables; stack pointer into $0464 while building the processing order",
   "confidence": "high"
  },
  {
   "addr": "1B",
   "scope": "zp",
   "name": "tempByte1B",
   "meaning": "scratch: direction of the chosen fire target; list index into unitBaseOrder while building the processing order",
   "confidence": "high"
  },
  {
   "addr": "2F",
   "scope": "zp",
   "name": "scanResultCount",
   "meaning": "count-1 of units found by the area scan sub_35C3/sub_37AA (negative = none); the hits are in $0400 (unit), $0432 (distance), $0464 (direction)",
   "confidence": "high"
  },
  {
   "addr": "48",
   "scope": "zp",
   "name": "mapCellPtr",
   "meaning": "pointer (lo, $49 hi) to the map cell computed by sub_25FA; written through with Y=0 to place or clear a unit",
   "confidence": "high"
  },
  {
   "addr": "5D",
   "scope": "zp",
   "name": "joyDirection",
   "meaning": "joystick direction bits, $0F = centred (waitFramesOrInput exits when it changes)",
   "confidence": "high"
  },
  {
   "addr": "6B",
   "scope": "zp",
   "name": "currentUnitFacing",
   "meaning": "facing/heading 0-3 of the unit being processed; stored into bits 0-1 of unitStateFlags",
   "confidence": "medium"
  },
  {
   "addr": "91",
   "scope": "zp",
   "name": "lineStepsLeft",
   "meaning": "steps left on the current line (0 = destination reached with the last step)",
   "confidence": "high"
  },
  {
   "addr": "92",
   "scope": "zp",
   "name": "lineCurRow",
   "meaning": "line walker current row = the unit's path row during a step",
   "confidence": "high"
  },
  {
   "addr": "93",
   "scope": "zp",
   "name": "lineCurCol",
   "meaning": "line walker current column = the unit's path column during a step",
   "confidence": "high"
  },
  {
   "addr": "94",
   "scope": "zp",
   "name": "lineEndRow",
   "meaning": "line target row (unit destination row)",
   "confidence": "high"
  },
  {
   "addr": "95",
   "scope": "zp",
   "name": "lineEndCol",
   "meaning": "line target column (unit destination column)",
   "confidence": "high"
  },
  {
   "addr": "A5",
   "scope": "zp",
   "name": "viewOriginCol",
   "meaning": "map column of the left edge of the battlefield view",
   "confidence": "high"
  },
  {
   "addr": "A6",
   "scope": "zp",
   "name": "viewOriginRow",
   "meaning": "map row of the top edge of the battlefield view",
   "confidence": "high"
  },
  {
   "addr": "A9",
   "scope": "zp",
   "name": "isDiagonalStep",
   "meaning": "1 when the last stepLine moved diagonally: costs 8 extra movement points",
   "confidence": "high"
  },
  {
   "addr": "B1",
   "scope": "zp",
   "name": "gamePhase",
   "meaning": "0 = battle; nonzero = setup/menu phase (movement points fixed at 80, no combat, 50-unit command slices)",
   "confidence": "high"
  },
  {
   "addr": "0400",
   "scope": "abs",
   "name": "scratchList0400",
   "meaning": "100-byte scratch: per-group member counts (rebuildGroupTables), unit list of the area scan, and ordering marks while building the processing order (0 unordered, 1 stacked, 5 ordered)",
   "confidence": "high"
  },
  {
   "addr": "0410",
   "scope": "abs",
   "name": "groupWorstTerrainCost",
   "meaning": "16-byte scratch: highest terrain cost among a group's members",
   "confidence": "high"
  },
  {
   "addr": "0420",
   "scope": "abs",
   "name": "groupBestRate",
   "meaning": "16-byte scratch: lowest movement rate among a group's members",
   "confidence": "high"
  },
  {
   "addr": "0430",
   "scope": "abs",
   "name": "groupMoveTableOld",
   "meaning": "16-byte copy of groupMoveTable from the previous round (bits 6-7 are carried over)",
   "confidence": "high"
  },
  {
   "addr": "0432",
   "scope": "abs",
   "name": "scanResultDistance",
   "meaning": "distance of each unit found by the area scan (parallel to $0400)",
   "confidence": "high"
  },
  {
   "addr": "0464",
   "scope": "abs",
   "name": "scanResultDirection",
   "meaning": "direction toward each unit found by the area scan (parallel to $0400); also the pending-unit stack while building the processing order",
   "confidence": "high"
  },
  {
   "addr": "077E",
   "scope": "abs",
   "name": "terrainClassTable",
   "meaning": "terrain byte -> movement/terrain class (0-8) used to index terrainMoveCostTable, $38B3 and $35BB; $40 (blank) -> 0, $41-$54 -> 5, $55-$5C -> 1, $5D-$60 -> 2, $61-$64 -> 4, $67 -> 7",
   "confidence": "high"
  },
  {
   "addr": "0B7D",
   "scope": "abs",
   "name": "inputLockoutTimer",
   "meaning": "frames during which keys are ignored; set to 60 after the HOME key",
   "confidence": "high"
  },
  {
   "addr": "0B9B",
   "scope": "abs",
   "name": "filmPlaybackFlag",
   "meaning": "bit 7 = film playback; bits 0-1 = film view mode 0 US, 1 THEM, 2 BOTH (cycled by HOME)",
   "confidence": "high"
  },
  {
   "addr": "0B9F",
   "scope": "abs",
   "name": "playerSide",
   "meaning": "this player's side 0/1",
   "confidence": "high"
  },
  {
   "addr": "0BA3",
   "scope": "abs",
   "name": "gameTypeOptions",
   "meaning": "game option bits; bit 3 tested by the halt routine sub_3FAC",
   "confidence": "medium"
  },
  {
   "addr": "0BA5",
   "scope": "abs",
   "name": "commBuildFlag",
   "meaning": "bit 7 = comm build 1 (T35) resident: its hooks $E0E8 (per unit) and $E079 (per round) are called by the engine",
   "confidence": "high"
  },
  {
   "addr": "0BA8",
   "scope": "abs",
   "name": "noGameInProgress",
   "meaning": "0 while a game runs; set to 8 when the clock runs out",
   "confidence": "high"
  },
  {
   "addr": "0C8A",
   "scope": "abs",
   "name": "comcenIndexBySide",
   "meaning": "unit index of each side's COMCEN: $31 (side 0), $63 (side 1)",
   "confidence": "high"
  },
  {
   "addr": "406E",
   "scope": "abs",
   "name": "unitMovedFlag",
   "meaning": "set by moveUnitToCell when the current unit changed cells; L_436F then redraws the vacated cell",
   "confidence": "high"
  },
  {
   "addr": "67CA",
   "scope": "abs",
   "name": "currentSoundId",
   "meaning": "id of the sound effect currently playing (compared with $14/$16 before starting the comcen sounds)",
   "confidence": "medium"
  },
  {
   "addr": "8726",
   "scope": "abs",
   "name": "msgShowModeText",
   "meaning": "4-character slot in message 6 'SHOW ****' that receives US/THEM/BOTH",
   "confidence": "high"
  },
  {
   "addr": "8730",
   "scope": "abs",
   "name": "msgSpeedDigit",
   "meaning": "digit slot of message 7 'SPEED *'",
   "confidence": "high"
  },
  {
   "addr": "86FB",
   "scope": "abs",
   "name": "filmViewModeNames",
   "meaning": "3 x 4-byte names US/THEM/BOTH (bit-7 terminated) in overlay B",
   "confidence": "high"
  },
  {
   "addr": "9010",
   "scope": "abs",
   "name": "spriteXShadow",
   "meaning": "sprite X shadows; sprite 0 = radar cursor",
   "confidence": "high"
  },
  {
   "addr": "9018",
   "scope": "abs",
   "name": "spriteYShadow",
   "meaning": "sprite Y shadows",
   "confidence": "high"
  },
  {
   "addr": "9022",
   "scope": "abs",
   "name": "spriteEnableShadow",
   "meaning": "sprite enable shadow; bits 3/4 cleared when marker timers $91C8/$923E expire",
   "confidence": "high"
  },
  {
   "addr": "90FA",
   "scope": "abs",
   "name": "currentUnit",
   "meaning": "index of the unit being processed by the engine",
   "confidence": "high"
  },
  {
   "addr": "90FB",
   "scope": "abs",
   "name": "currentScreenId",
   "meaning": "0 = battlefield screen",
   "confidence": "high"
  },
  {
   "addr": "90FC",
   "scope": "abs",
   "name": "stepCountdown",
   "meaning": "frames until the next engine step (decremented by the IRQ); reloaded from stepIntervalFrames",
   "confidence": "high"
  },
  {
   "addr": "90FD",
   "scope": "abs",
   "name": "unitProcessOrder",
   "meaning": "100-byte list of unit indices in this round's processing order (rebuilt by finishRound)",
   "confidence": "high"
  },
  {
   "addr": "9162",
   "scope": "abs",
   "name": "unitOrderIndex",
   "meaning": "position in unitProcessOrder of the next unit to process (0-99, 100 = round complete)",
   "confidence": "high"
  },
  {
   "addr": "9164",
   "scope": "abs",
   "name": "unitBaseOrder",
   "meaning": "100-byte base order of unit indices, randomly shuffled two entries at a time (lock-step RNG)",
   "confidence": "high"
  },
  {
   "addr": "91C8",
   "scope": "abs",
   "name": "marker3Timer",
   "meaning": "countdown; sprite 3 is switched off when it reaches 0",
   "confidence": "medium"
  },
  {
   "addr": "91CB",
   "scope": "abs",
   "name": "roundsRemaining",
   "meaning": "game clock in rounds, counts down to 0 (game over on points); its parity against the unit index decides which units fire/reload this round",
   "confidence": "high"
  },
  {
   "addr": "91D7",
   "scope": "abs",
   "name": "trackedUnit",
   "meaning": "unit the view follows while its menu/targeting is open ($FF = none); set from selectedUnit before updateGameFrame",
   "confidence": "medium"
  },
  {
   "addr": "91DA",
   "scope": "abs",
   "name": "currentUnitCol",
   "meaning": "column of the current unit before its step (cell to redraw); also the cursor column in followDirectedUnit",
   "confidence": "high"
  },
  {
   "addr": "91DB",
   "scope": "abs",
   "name": "currentUnitRow",
   "meaning": "row of the current unit before its step",
   "confidence": "high"
  },
  {
   "addr": "91DC",
   "scope": "abs",
   "name": "boomerTargetTable",
   "meaning": "per-boomer assigned target entries read/cleared through sub_3C74",
   "confidence": "low"
  },
  {
   "addr": "91F0",
   "scope": "abs",
   "name": "fireTargetUnit",
   "meaning": "unit chosen as fire target for the current unit ($FF = none)",
   "confidence": "high"
  },
  {
   "addr": "91F1",
   "scope": "abs",
   "name": "fireTargetDistance",
   "meaning": "distance of the chosen fire target (minimum search)",
   "confidence": "high"
  },
  {
   "addr": "91F4",
   "scope": "abs",
   "name": "groupMoveTable",
   "meaning": "16 entries (group 0-7 per side, bit 3 = side): bits 0-2 slowest speed class, bits 3-5 worst terrain class, bits 6-7 group flags",
   "confidence": "high"
  },
  {
   "addr": "9208",
   "scope": "abs",
   "name": "scoreChangeCounter",
   "meaning": "incremented whenever a score changes (status display refresh)",
   "confidence": "medium"
  },
  {
   "addr": "920E",
   "scope": "abs",
   "name": "statsNeedRefresh",
   "meaning": "set to $80 after a command slice was applied",
   "confidence": "high"
  },
  {
   "addr": "9223",
   "scope": "abs",
   "name": "opponentWaitTimer",
   "meaning": "nonzero while waiting for the opponent's data for the current command slice ($80 + elapsed/8 frames); postpones the engine step",
   "confidence": "high"
  },
  {
   "addr": "9227",
   "scope": "abs",
   "name": "currentUnitTerrain",
   "meaning": "terrain byte saved under the current unit (used to redraw its vacated cell)",
   "confidence": "high"
  },
  {
   "addr": "9228",
   "scope": "abs",
   "name": "currentUnitType",
   "meaning": "type 0-7 of the current unit (0 grunt?, 1 rider?, 2 boomer/artillery, 3 spy, 4 comcen)",
   "confidence": "medium"
  },
  {
   "addr": "922A",
   "scope": "abs",
   "name": "comcenStunStatus",
   "meaning": "own comcen stun count (bits 0-3) with bit 7 when stunned; $FF triggers the knock-out round L_0709, bit 6 set during it",
   "confidence": "high"
  },
  {
   "addr": "922C",
   "scope": "abs",
   "name": "currentUnitCloakBits",
   "meaning": "bits 6-7 (cloak flags) of the current unit's $F898 entry, re-applied when the path column is rewritten",
   "confidence": "high"
  },
  {
   "addr": "9234",
   "scope": "abs",
   "name": "viewRedrawTimer",
   "meaning": "countdown; when it reads $80 (expired) the battlefield view is redrawn and it is cleared",
   "confidence": "medium"
  },
  {
   "addr": "9236",
   "scope": "abs",
   "name": "ownComcenUnit",
   "meaning": "index of the player's comcen",
   "confidence": "high"
  },
  {
   "addr": "923A",
   "scope": "abs",
   "name": "pendingRedrawGroup",
   "meaning": "group id whose members must be redrawn (bit 6 added for the compare with $F708 & $7C); $FF = none",
   "confidence": "high"
  },
  {
   "addr": "923B",
   "scope": "abs",
   "name": "pendingRedrawUnit",
   "meaning": "unit to redraw next frame ($FF = none)",
   "confidence": "high"
  },
  {
   "addr": "923C",
   "scope": "abs",
   "name": "commandSlice",
   "meaning": "index of the current 10-unit (setup: 50-unit) command time slice within the round = unitOrderIndex / 10",
   "confidence": "high"
  },
  {
   "addr": "923E",
   "scope": "abs",
   "name": "marker4Timer",
   "meaning": "countdown; sprite 4 is switched off when it reaches 0",
   "confidence": "medium"
  },
  {
   "addr": "923F",
   "scope": "abs",
   "name": "unitsFrozenFlags",
   "meaning": "bit 7 = units are not processed (set with $C0 at game end)",
   "confidence": "medium"
  },
  {
   "addr": "9248",
   "scope": "abs",
   "name": "lastKeyPressed",
   "meaning": "last typed key code: $B1-$B9 digits, $8C = CLR/HOME (keyboard table $0D67), $FF none",
   "confidence": "high"
  },
  {
   "addr": "92A2",
   "scope": "abs",
   "name": "pointsAgainstSide",
   "meaning": "two 16-bit scores (lo bytes, hi at $92A4) indexed by side: index i holds the points scored by side i EOR 1 (zone entries, damage)",
   "confidence": "medium"
  },
  {
   "addr": "92A4",
   "scope": "abs",
   "name": "pointsAgainstSideHi",
   "meaning": "high bytes of pointsAgainstSide",
   "confidence": "medium"
  },
  {
   "addr": "92A8",
   "scope": "abs",
   "name": "recyclerCol",
   "meaning": "per-side column of the RECYCLER (unit respawn point), bit 7 = none",
   "confidence": "medium"
  },
  {
   "addr": "92AA",
   "scope": "abs",
   "name": "recyclerRow",
   "meaning": "per-side row of the RECYCLER",
   "confidence": "medium"
  },
  {
   "addr": "92AC",
   "scope": "abs",
   "name": "scoreValueBySide",
   "meaning": "per-side points awarded when one of that side's units enters the enemy zone (0 = no territory scoring)",
   "confidence": "medium"
  },
  {
   "addr": "92B2",
   "scope": "abs",
   "name": "comcenRevealTimer",
   "meaning": "per-side rounds (6) during which a comcen that launched radar/drone stays visible",
   "confidence": "high"
  },
  {
   "addr": "92B4",
   "scope": "abs",
   "name": "comcenSpeedClassOwn",
   "meaning": "speed class (index into speedClassRateTable) of the own comcen",
   "confidence": "medium"
  },
  {
   "addr": "92B5",
   "scope": "abs",
   "name": "comcenSpeedClassOpponent",
   "meaning": "speed class of the opponent's comcen",
   "confidence": "medium"
  },
  {
   "addr": "92C2",
   "scope": "abs",
   "name": "unitActivityFlag",
   "meaning": "set when a unit changed its path position or dug in this round; in the setup phase the round counter only advances when set",
   "confidence": "medium"
  },
  {
   "addr": "92C8",
   "scope": "abs",
   "name": "spriteScrollActive",
   "meaning": "nonzero: apply the fine-scroll shake (sub_29C1) once per step and decrement",
   "confidence": "high"
  },
  {
   "addr": "92C9",
   "scope": "abs",
   "name": "gameEndFlag",
   "meaning": "nonzero once the game is over; aborts runGameFrames/waitFramesOrInput",
   "confidence": "high"
  },
  {
   "addr": "92E3",
   "scope": "abs",
   "name": "stepIntervalFrames",
   "meaning": "frames between engine steps (4 at game start; film speed keys set 9 - digit)",
   "confidence": "high"
  },
  {
   "addr": "92EF",
   "scope": "abs",
   "name": "lastQuarterRound",
   "meaning": "roundsRemaining value at which 'LAST QTR WARNING!' is issued (total/4)",
   "confidence": "high"
  },
  {
   "addr": "92FA",
   "scope": "abs",
   "name": "lastQuarterFlag",
   "meaning": "$80 after the last-quarter warning",
   "confidence": "medium"
  },
  {
   "addr": "F640",
   "scope": "abs",
   "name": "unitCol",
   "meaning": "per-unit map column (100 entries)",
   "confidence": "high"
  },
  {
   "addr": "F6A4",
   "scope": "abs",
   "name": "unitRow",
   "meaning": "per-unit map row",
   "confidence": "high"
  },
  {
   "addr": "F688",
   "scope": "abs",
   "name": "unitStateFlagsAlias",
   "meaning": "unitStateFlags ($F708) addressed with a map byte (unit|$80) as index: $F688 + ($80+i) = $F708 + i",
   "confidence": "high"
  },
  {
   "addr": "F6EC",
   "scope": "abs",
   "name": "unitTypeTableAlias",
   "meaning": "unitTypeTable ($F76C) indexed by map byte (unit|$80)",
   "confidence": "high"
  },
  {
   "addr": "F708",
   "scope": "abs",
   "name": "unitStateFlags",
   "meaning": "bits 0-1 facing 0-3, bits 2-5 group id (bit 5 = side), bit 6 = member of a group, bit 7 = blitz charge (energy spent this round, boosts the next shot)",
   "confidence": "high"
  },
  {
   "addr": "F76C",
   "scope": "abs",
   "name": "unitTypeTable",
   "meaning": "bits 0-2 unit type (2 = boomer/artillery, 3 = spy, 4 = comcen), bit 3/4 = scored the near/far enemy zone, bit 6 = fortified (dug in), bit 7 = dead ($FF = slot unused)",
   "confidence": "high"
  },
  {
   "addr": "F7D0",
   "scope": "abs",
   "name": "unitDestCol",
   "meaning": "bits 0-5 destination column, bit 7 = stationary (destination reached)",
   "confidence": "high"
  },
  {
   "addr": "F818",
   "scope": "abs",
   "name": "unitPathColAlias",
   "meaning": "unitPathCol ($F898) indexed by map byte (unit|$80)",
   "confidence": "high"
  },
  {
   "addr": "F834",
   "scope": "abs",
   "name": "unitDestRow",
   "meaning": "bits 0-5 destination row, bit 7 = repairing (gained energy while stationary)",
   "confidence": "high"
  },
  {
   "addr": "F898",
   "scope": "abs",
   "name": "unitPathCol",
   "meaning": "bits 0-5 logical (path) column, which may lie on another unit's cell; bits 6-7 = cloaked",
   "confidence": "high"
  },
  {
   "addr": "F8FC",
   "scope": "abs",
   "name": "unitPathRow",
   "meaning": "bits 0-5 logical (path) row; bits 6-7 = blitz mode (cleared when the unit becomes stationary)",
   "confidence": "high"
  },
  {
   "addr": "F960",
   "scope": "abs",
   "name": "unitStunTable",
   "meaning": "bits 0-3 stun counter (decremented once per processed step), bits 5-7 dig-in progress (+$20 per stationary round, bit 7 = dug in)",
   "confidence": "high"
  },
  {
   "addr": "F9C4",
   "scope": "abs",
   "name": "unitEnergyTable",
   "meaning": "bits 0-5 energy 0-50, bit 6 = spotted this round, bit 7 = visible/drawn (sub_3576 shifts 6 -> 7 each round)",
   "confidence": "high"
  },
  {
   "addr": "FA28",
   "scope": "abs",
   "name": "unitMovePoints",
   "meaning": "signed movement-point balance: +rate per round, -terrain cost per step, 15 when blocked/stationary, 100 on reaching a unit, 80 in setup",
   "confidence": "high"
  },
  {
   "addr": "FA70",
   "scope": "abs",
   "name": "unitSavedTerrainAlias",
   "meaning": "unitSavedTerrain ($FAF0) indexed by map byte (unit|$80)",
   "confidence": "high"
  },
  {
   "addr": "FA8C",
   "scope": "abs",
   "name": "unitHitFlashTimer",
   "meaning": "nonzero while a hit/explosion is shown on the unit's cell: low nibble counts down per step, then the cell is redrawn",
   "confidence": "high"
  },
  {
   "addr": "FAF0",
   "scope": "abs",
   "name": "unitSavedTerrain",
   "meaning": "terrain byte under the unit, written back when it leaves; negative ($FF) = unit not on the map",
   "confidence": "high"
  },
  {
   "addr": "FB54",
   "scope": "abs",
   "name": "unitOrderFlags",
   "meaning": "bit 0 = reloading (fired, cleared on the unit's parity round), bit 1 = pinned/under fire (cleared each step), bits 2-5 group id, bit 6 = group membership ordered, bit 7 = stationary (copy of $F7D0 bit 7; enables DIG IN)",
   "confidence": "high"
  }
 ],
 "dataBlocks": [
  {
   "addr": "405E",
   "length": 8,
   "type": "byteTable",
   "name": "terrainMoveCostTable",
   "description": "movement points charged per step by terrain class 0-7: 20,65,30,40,20,40,20,20. Class 8 (terrain $68-$6B/$6F-$72 via $077E) reads into speedClassRateTable[0] = 12."
  },
  {
   "addr": "4066",
   "length": 5,
   "type": "byteTable",
   "name": "speedClassRateTable",
   "description": "movement points gained per round by speed class 0-4: 12,25,12,25,0 (class 4 = immobile comcen; comcen classes come from $92B4/$92B5)"
  },
  {
   "addr": "406B",
   "length": 3,
   "type": "byteTable",
   "name": "zoneScoredBitTable",
   "description": "bit masks ORed into unitTypeTable when a unit scores a zone: [1] = $08 near zone (col 10-19 mirrored), [2] = $10 far zone (col 0-9); entry 0 ($28) unused"
  },
  {
   "addr": "406E",
   "length": 1,
   "type": "byteTable",
   "name": "unitMovedFlag",
   "description": "1-byte variable (not a table): set when moveUnitToCell moved the current unit"
  }
 ],
 "misclassified": [
  {
   "addr": "403B",
   "length": 35,
   "actual": "code",
   "evidence": "Bytes AD 9F 0B E0 00 F0 02 49 01 A8 A9 06 99 B2 92 B9 8A 0C A8 B9 C4 F9 09 C0 99 C4 F9 E0 00 F0 03 8C 3B 92 60 decode to LDA $0B9F / CPX #0 / BEQ / EOR #1 / TAY / LDA #6 / STA $92B2,Y / LDA $0C8A,Y / TAY / LDA $F9C4,Y / ORA #$C0 / STA $F9C4,Y / CPX #0 / BEQ / STY $923B / RTS and the routine is called by JSR $403B embedded in the (also misclassified) command handlers at $53BC and $53F1."
  }
 ],
 "insights": [
  "Line/address mismatch: the chunk spec said lines 7048-7882 but those lines hold $3DF5-$4480; this survey covers the address range $403B-$4986 (file lines 7360-8542), which is what the chunk name refers to.",
  "Game engine structure: updateGameFrame ($40B2) is the per-frame heartbeat called from every UI loop (directly or via runGameFrames $4094 / waitFramesOrInput $406F). Every stepIntervalFrames ($92E3, default 4, film speed keys 1-9 give 8..0) it processes ONE unit from the round order $90FD; after all 100 units finishRound (L_45E6) ticks the game clock roundsRemaining ($91CB), hands out movement points and rebuilds the order. Commands are applied in lock step every 10 units (commandSlice $923C = unitOrderIndex/10, 50 in the setup phase) via sub_4DB7, and the step is postponed while opponentWaitTimer ($9223) is nonzero; the random shuffle of the base order uses the lock-step scenario RNG ($FD4A) so both machines stay in sync.",
  "Unit records are 14 parallel 100-byte arrays starting at $F640 with stride $64: $F640 col, $F6A4 row, $F708 state flags (facing/group/side/blitz charge), $F76C type+dead+zone bits, $F7D0 dest col (+bit 7 stationary), $F834 dest row (+bit 7 repairing), $F898 path col (+cloak bits 6-7), $F8FC path row (+blitz bits 6-7), $F960 stun/dig-in, $F9C4 energy+visibility, $FA28 movement points, $FA8C hit-flash timer, $FAF0 terrain under the unit, $FB54 order flags (reload/pinned/group/stationary). Units 0-49 are side 0 (comcen $31), 50-99 side 1 (comcen $63); a map cell holding a unit contains unit|$80, so code also indexes the arrays with the map byte through aliases $80 lower ($F688 = $F708, $F6EC = $F76C, $F818 = $F898, $FA70 = $FAF0).",
  "Movement model: each step walks one Bresenham cell from the unit's PATH position ($F898/$F8FC) toward its destination ($F7D0/$F834) and charges terrainMoveCostTable[$405E][class] (+8 diagonal) from the signed balance $FA28, which refills every round by speedClassRateTable[$4066][class] (12 or 25; a group uses its slowest member and worst terrain via groupMoveTable $91F4). Spies (type 3) ignore terrain and may enter occupied cells; other units may step onto same-side units or enemy spies, and cloaked units onto cloaked ones - in that case only the path position moves and the map position is realised later by walking back to the first free cell. A unit reaching a unit-occupied destination gets 100 points. Blitz (menu SET BLITZ, $F8FC bits 6-7) spends energy via sub_3EE2 for +25 points per step and a boosted shot ($F708 bit 7); it switches off automatically when the unit becomes stationary. Cloak ($F898 bits 6-7) and blitz are mutually exclusive and unavailable to spies.",
  "Combat/visibility per step: spot scan (sub_35C3 X=0) sets visibility bits 6/7 of $F9C4 on enemies in sight and draws newly seen ones; fire scan (X=$80, boomers through sub_3C85) picks the nearest enemy (comcens preferred); a unit fires (sub_38C2) only on rounds where (roundsRemaining EOR unitIndex) AND 1 = 0 and sets the reload bit, which is cleared on the same parity two rounds later. Spies, comcens and cloaked units never fire. Stationary unstunned units either dig in ($F960 += $20 per round, fortified when bit 7 sets, also mirrored as $F76C bit 6) or regain 1 energy per round (max 50) with $F834 bit 7 = REPAIR shown in the unit status.",
  "Territory scoring (moveUnitToCell): columns are mirrored for side-1 units (sub_3FA3 = 39 - col, also used to mirror opponent commands), col 0-9 is the far zone (2x) and 10-19 the near zone; a unit scores once per zone (bits 3/4 of $F76C) scoreValueBySide[$92AC] points (comcen x16) into the 16-bit scores $92A2/$92A4, which are indexed by the LOSING side (index i = points scored by side i^1; $28F1 compares them for WE WON/LOST ON POINTS).",
  "Per-side scenario data at $92A6-$92B5: $92A6 recycler unit type, $92A8/$92AA recycler position (sub_3E23 respawns dead units there, 'UNIT RECYCLED.'; sub_3F57 reveals the enemy recycler with terrain byte $67 when an own unit comes within 3 cells, 'SPOTTED RECYCLER!'), $92AC zone score value, $92B2 comcen reveal timer; relative (0 = us, 1 = them): $92AE radar uses left, $92B0 drones left, $92B4 comcen speed class. $403B (misclassified as data) is called after a radar/drone launch and keeps the launching comcen visible for 6 rounds.",
  "The comm-module build 1 (T35, $0BA5 bit 7) is not only a link variant: the engine calls its hooks sub_E0E8 after every unit step and sub_E079 every round (only in battle, not in films); $E0E8 inspects the side-1 unit records, which looks like the computer opponent / practice-mode AI (medium confidence).",
  "Film playback controls inside updateGameFrame: keys 1-9 set the replay speed (message 7 'SPEED n' via the digit slot at $8730), CLR/HOME ($8C in the key table at $0D67, row 6 column 3) cycles $0B9B bits 0-1 through SHOW US/THEM/BOTH using the 4-byte names at $86FB copied into message 6 at $8726.",
  "Large misclassified code regions adjacent to this chunk: $4987 onward (next chunk) is code (LDA #0 / STA $18 ... LDY #$63 / LDA $F76C,Y), and the command handlers $5011-$523D plus $5336-$54xx are rendered as .byte although the dispatch table at $4D2A (lo/hi pairs, command $80+n) points into them ($80->$5011 move, $82->$5023 cloak on, $84->$504B cloak off, $93->$5070 blitz on, $95->$5062 blitz off, $8E->$5336, $90->$53C0 radar, $92->$538B, $97->$53A3 drone?)."
 ]
}