{
 "_comment": "generated by tools/mergeSurvey.py from survey/*.json - do not edit by hand",
 "notes": {
  "E000": {
   "routine": [
    "commRequestEntry - Jump-table slot 0 of the communications module (JMP handleCommRequest). The game calls it every raster IRQ with X=0 ($1129), with X=1 to transmit the outgoing packet (sendOutgoingPacket $56AE), with X=2 to acknowledge a completed exchange ($4EFF/$526F) and with X=3 to cancel a pending send. In this build (T35 = SOLO TRAINER) there is no link at all: X=1 runs one turn of the computer opponent's AI and leaves its orders in the receive buffer.",
    "In:  X = request code 0..3",
    "Out: see handleCommRequest ($E043)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E003": {
   "routine": [
    "commSetLinkStateEntry - Jump-table slot 1 (JMP setCommLinkState). Called by initCommModule ($0F46, X=1) and by hangUpModemSetState ($1B56/$1B70, X=3) to bring the pretend link up (X=0) or down (X>=1).",
    "In:  X = 0 open, >=1 close",
    "Out: isLinkActive/commCarrierStatus/commLinkReady ($E03B/$E03C/$E03D)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E006": {
   "routine": [
    "commGetReceivedByteEntry - Jump-table slot 2 (JMP reportNoReceivedByte). Called by receiveChatCharacter ($1399) to fetch one byte of the opponent's chat line. In the trainer build it always returns C=1 (nothing received), so the chat receiver never produces text.",
    "In:  none",
    "Out: C=1 always (no byte)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E009": {
   "routine": [
    "commSendByteEntry - Jump-table slot 3 (JMP commNoOpClc). Jumped to from $1396 to transmit one chat character. In the trainer build the byte is discarded and C=0 (accepted) is returned.",
    "In:  A = byte to send (ignored)",
    "Out: C=0"
   ],
   "unit": "game/trainerAiE000"
  },
  "E00C": {
   "routine": [
    "commFrameTickEntry - Jump-table slot 4 (JMP commNoOpClc). Called once per raster IRQ from $112C; in the modem build this services the serial line, in the trainer build it does nothing.",
    "In:  none",
    "Out: C=0"
   ],
   "unit": "game/trainerAiE000"
  },
  "E012": {
   "routine": [
    "commKeyboardScanVector - JMP whose operand bytes $E013/$E014 are the module's keyboard-scan vector. initCommModule ($0F4B/$0F50) patches them to the game's scanKeyboard ($0DB7); until then the vector points at commNoOpClc so no key is ever reported.",
    "In:  none",
    "Out: whatever scanKeyboard returns (A=key, Y/X = shift/C= flags)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E015": {
   "routine": [
    "commReadKeyEntry - Jump-table slot 5 (JMP commKeyboardScanVector). Called from pollKeyboardEvent ($0E56) every frame; it is the only route by which the game reads the keyboard, so the comm module can steal the keyboard while it owns the CPU.",
    "In:  none",
    "Out: key code in A, shift/C= flags in Y/X"
   ],
   "unit": "game/trainerAiE000"
  },
  "E043": {
   "routine": [
    "handleCommRequest - Body of commRequestEntry. X=1 jumps to buildTrainerReplyPacket ($E082) which runs the computer opponent. X=3 clears outgoingPacketLength ($E01F). Every other value (X=0 per-frame poll, X=2 packet acknowledge) sets packetExchangeStatus ($E01D) = $C0 - 'own packet sent AND opponent packet received' - and copies commSuspendRequest ($E039) into commSuspendAck ($E03A). Because $E01D is always $C0 the lock-step exchange in processCommandExchange never waits, and suspend/resume around disk access ($0F5F/$0F71) succeeds immediately.",
    "In:  X = request code",
    "Out: $E01D = $C0, $E03A = $E039, or $E01F = 0"
   ],
   "unit": "game/trainerAiE000"
  },
  "E060": {
   "routine": [
    "setCommLinkState - Body of commSetLinkStateEntry. X=0 stores $FF into commLinkReady ($E03D), commCarrierStatus ($E03C) and isLinkActive ($E03B); X>=1 stores 0 in all three and also clears outgoingPacketLength ($E01F). Since $E03C is $FF the game's carrier tests (bit 6 in pollMenuSession, bit 7 in hangUpModemSetState) always see a healthy link.",
    "In:  X",
    "Out: $E03B, $E03C, $E03D, $E01F"
   ],
   "unit": "game/trainerAiE000"
  },
  "E075": {
   "routine": [
    "reportNoReceivedByte - SEC + RTS: 'no character available' answer of commGetReceivedByteEntry in the trainer build.",
    "In:  none",
    "Out: C=1"
   ],
   "unit": "game/trainerAiE000"
  },
  "E077": {
   "routine": [
    "commNoOpClc - CLC + RTS shared no-op used as the body of commSendByteEntry, commFrameTickEntry and the unpatched keyboard vector. $E078 is the bare RTS.",
    "In:  none",
    "Out: C=0"
   ],
   "unit": "game/trainerAiE000"
  },
  "E079": {
   "routine": [
    "runTrainerRoundHook - Once-per-round hook called from finishRound ($4654) after the game clock has been decremented. Unless the drone sub-overlay is resident (trainerDroneTimer $8977 bit 7 set) it runs planTrainerRound ($E170), the top-level 'think' routine of the computer opponent.",
    "In:  D_8977",
    "Out: see planTrainerRound"
   ],
   "unit": "game/trainerAiE000"
  },
  "E082": {
   "routine": [
    "buildTrainerReplyPacket - Reached with X=1 from sendOutgoingPacket, i.e. once per lock-step exchange right after the human's packet was handed over. Clears receivedPacketLength ($E01E) and returns at once during the setup phase (zp_B1), film playback ($0B9B bit 7), after the game ended ($0BA8) or while paused (zp_BE). Otherwise it manages the swappable $EE00 sub-overlay: while the opponent's drone is in the air (droneStateFlags $920D bit 0) it loads track 29 s4-5 to $EE00 (the drone-flight AI) and calls $EE00 with $8977 = $FF; when the drone is gone it reloads track 35 s14-15 to $EE00 (the native tail), sets $8976 = $FF and $8977 = 3. In the normal case it counts $8977 down and calls flushTrainerCommandQueue ($EA7B), which moves up to 7 bytes of queued AI orders into the received-packet buffer $E020.",
    "In:  zp_B1, $0B9B, $0BA8, zp_BE, $920D, $8977",
    "Out: $E01E/$E020 (the 'received' packet), $8976, $8977, $EE00-$EFFF possibly reloaded from disk"
   ],
   "unit": "game/trainerAiE000"
  },
  "E0E8": {
   "routine": [
    "trainerUnitStepHook - Per-unit hook called at the end of processUnitStep ($4526) for currentUnit ($90FA). Dead units are skipped. For one of the AI's own units (index >= 50) that has scored both territory zones (unitTypeTable bits 3 and 4) it sets bit 7 of trainerUnitPenetrated ($8864 + ordinal) so the planner stops counting it as 'still at home'; for the AI comcen (unit 99) it additionally samples bit 1 of unitLocalFlags[99] ($FBB7, set by recordUnitHit) into trainerComcenHit ($897B) and the one-round-sticky trainerComcenHitRecent ($897C). All other units fall through to trainerUnitSpottedHook ($EACF), which tracks what the AI has seen.",
    "In:  $90FA, $F76C, $FBB7, $8977",
    "Out: $8864+ordinal bit 7, $897B, $897C; otherwise calls $EACF",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E128": {
   "routine": [
    "setSubOverlayLoadDest - Helper: X=$00, Y=$EE, JMP setLoadDest ($085C) - points the fast loader at $EE00 before the two sub-overlay loads in buildTrainerReplyPacket.",
    "In:  none",
    "Out: loader destination = $EE00"
   ],
   "unit": "game/trainerAiE000"
  },
  "E12F": {
   "routine": [
    "pickTrainerRallyPoint - Chooses the AI's current objective: a random cell near the human comcen (unitCol[49] = $F671, unitRow[49] = $F6D5) via randomiseCoordinate, stored in trainerRallyCol/Row ($8969/$896A). Called every six rounds (or immediately once the human comcen has been spotted) from planTrainerRound, and once at game start by the trainer setup overlay ($EC47).",
    "In:  $F671, $F6D5, RNG",
    "Out: $8969, $896A"
   ],
   "unit": "game/trainerAiE000"
  },
  "E14A": {
   "routine": [
    "randomiseCoordinate - Returns A = base coordinate plus a random offset, clamped to 0..39. The base is stored into the immediate operand at $E163+1 and the offset comes from randomCoordinateOffsetTable ($E142) indexed by nextGameRandom AND 7. The offset is used raw when the human comcen is visible (bit 7 of unitEnergy[49] = $F9F5, the 'spotted' bit), doubled when trainerEnemyComcenSeen ($896C) bit 7 is set and quadrupled otherwise - so the AI aims accurately at a comcen it can see and wanders when it cannot. $E14D is the continuation that reuses the last base.",
    "In:  A = base coordinate, $F9F5, $896C, RNG",
    "Out: A = coordinate 0..39; self-modifies $E164"
   ],
   "unit": "game/trainerAiE000"
  },
  "E170": {
   "routine": [
    "planTrainerRound - Top-level planner of the solo trainer, run once per game round. Returns at once if the game is over ($0BA8). Resets trainerOrdersThisRound ($8968), refreshes the rally point every 6 rounds (or immediately when the human comcen is visible), sets trainerActionBudget ($8965) from the AI comcen's energy ($FA27 AND $3F) and trainerComcenDamage ($8971) from its damage nibble ($F9C3 AND $0F). With the DAMAGE option off ($0BA3 bit 4 clear) the budget is a flat 50; a fully damaged comcen ($0F) makes the AI do nothing this round; 1-7 damage costs 16 budget points. A budget below 20 diverts to the repair/energy routine ($EB67). Otherwise it evaluates the threat around its own comcen with scanEnemyThreat ($EBEF) into trainerBestDir/trainerEnemyProximity/trainerThreatScore ($8960/$8961/$8962), sets trainerDefendMode ($8963) to $FF (defend) when enemies are close and either no assault group exists or the comcen was just hit, else 0 (attack, and then considers launching a drone via $EEE0). It then runs scanTrainerUnitsForOrders ($E2BD), and while fewer than 5 orders have been issued picks/refreshes the assault group ($E83C) and finishes with pushTrainerComcenForward ($E21F, attack) or retreatTrainerComcen ($E266, defend).",
    "In:  $0BA8, $F9F5, $FA27, $F9C3, $0BA3, $91CB, RNG, group tables at $8800-$897C",
    "Out: $8965, $8968, $896B, $896C, $8960-$8963, orders queued at $88A0, plus everything the callees change",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E21F": {
   "routine": [
    "pushTrainerComcenForward - Attack-mode tail of planTrainerRound (branch target, not a JSR entry). If an assault group exists ($895D), the comcen is undamaged and its own replan clock ($8831) has come due, it queues a move order for the AI comcen (ordinal 49) one step further along trainerBestDir ($8960) from the assault group's average position ($895E/$895F), and re-arms $8831 to gameClock - 3. This is how the trainer walks its comcen up behind its attack.",
    "In:  $895D, $8971, $8831, $91CB, $8960, $8961, $895E, $895F",
    "Out: $8831, one $80/$81 order queued through queueTrainerMoveOrder ($E919)",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E266": {
   "routine": [
    "retreatTrainerComcen - Defend-mode tail of planTrainerRound (branch target). Records the current clock in trainerAlertClock ($896E), and unless the comcen is damaged queues a move order that walks the AI comcen away along trainerBestDir ($8960) by trainerEnemyProximity ($8961) cells. If the threat score ($8962) then still exceeds trainerRecallThreshold ($897A) - or the comcen was damaged - it recomputes the assault group's statistics and jumps to aimGroupAtOwnComcen ($E339) to pull the group back home.",
    "In:  $8971, $8960, $8961, $8962, $897A, $895D, $F6A3, $F707",
    "Out: $896E, $895A = 0, one comcen move order, possibly a full group recall",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E2BD": {
   "routine": [
    "scanTrainerUnitsForOrders - Walks the AI's 50 unit slots from ordinal 49 down to 0 using the embedded loop counter at $E2BC, stopping as soon as 4 orders have been queued this round ($8968). A dead slot ($F79E bit 7) gets its replan clock zeroed and its group set to $FF. A slot whose replan clock ($8800+ordinal) has been reached by the down-counting game clock ($91CB) is handed to decideTrainerGroupAction. The default replan urgency ($896D) is reloaded with 5 before each unit.",
    "In:  $8968, $91CB, $F79E, $8800, $8832",
    "Out: $E2BC, $8800, $8832, $896D; orders queued by the callee"
   ],
   "unit": "game/trainerAiE000"
  },
  "E2F1": {
   "routine": [
    "decideTrainerGroupAction - Decides what the AI group owning unit ordinal Y should do. Group id 0 is the comcen's own group: it is only handled when the comcen is undamaged and no assault group is selected, in which case the group target ($88D2/$88DE entry 0) is set to the comcen's own destination. For any group it caches the group flags ($893E) in the scratch byte $8966, masks them down to the low nibble, and calls computeTrainerGroupStats; an empty group aborts. It then branches into the four behaviours: head for the AI comcen (home group $8964, or the assault group when defending), advance toward the rally point ($8969/$896A) when the group has already penetrated, fall back to row 29 of the AI's half when it has not, or - for group 0 - run the free direction search from the comcen position.",
    "In:  Y = AI unit ordinal, $8832, $8971, $895D, $F833, $F897, $8964, $8966, zp_1D",
    "Out: $88D2/$88DE, $893E, $896D, $8955-$8963 group scratch; ends in one of the order-issuing tails",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E339": {
   "routine": [
    "aimGroupAtOwnComcen - Internal tail (JMP target from $E2B3 and fall-through from $E336): sets the line-stepper target zp_95/zp_94 to the AI comcen's cell ($F6A3/$F707), copies the group centre (zp_93/zp_92) into the search origin zp_27/zp_28 and the target into zp_22/zp_24, then falls into issueTrainerGroupOrders. Used to recall a group to defend the comcen.",
    "In:  $F6A3, $F707, zp_92/zp_93 (group centre from computeTrainerGroupStats)",
    "Out: zp_22/zp_24 target cell, zp_27/zp_28 origin"
   ],
   "unit": "game/trainerAiE000"
  },
  "E3D0": {
   "routine": [
    "aimGroupAtHomeLine - Internal branch target used when the group still has members that have not reached the enemy end zone (zp_1D non-zero) and the group's 'committed' flag (bit 6 of $893E) is clear: the target column stays where the group is and the target row becomes 29 plus the group's row extents, i.e. a defensive line inside the AI's own half. Once the alert clock ($896E) has been set, or the group is weak, the group's combat value ($8956/$8957) is capped at 9 before the search continues at $E400.",
    "In:  zp_93, $891A, $8932, $896E, $91CB, $8956/$8957",
    "Out: zp_94/zp_95 target, $8956/$8957 possibly capped",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E42A": {
   "routine": [
    "findTrainerAdvanceCell - Threat-aware path search. First builds the per-unit threat map (buildThreatWeights $E773) into the $0400/$0464 scratch pages. Then, for each of the 8 direction ranks in the preference list at $8896 (built by buildDirectionPreference $E7E3), it steps outward from (zp_27,zp_28) in steps of 2 cells, testing every candidate with evaluateCellSafety ($E69C); it keeps extending while the cell is safe, up to the desired advance distance ($895B) or the per-rank cap in maxAdvanceByRankTable ($E664), and moves to the next rank when a cell is rejected at distance 0. If nothing works the group stays put (direction 0, distance 0). The winning direction/distance are converted into the absolute target cell zp_22/zp_24 through directionColDeltaTable/directionRowDeltaTable ($E68A/$E693) and addStepsClamped ($EA1B).",
    "In:  zp_27/zp_28 origin, $8896 direction preference, $895B, $E664, $8956/$8957 group strength",
    "Out: zp_22/zp_24 = chosen target cell, zp_23 = distance, zp_26 = direction, $0400/$0464 threat map",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E49E": {
   "routine": [
    "issueTrainerGroupOrders - Issues the move orders for the current group ($8955). Marks every member's replan clock with the current game clock, clamps the target with clampGroupTargetToMap (which also yields the delta zp_2A/zp_2B from the group's average destination), derives the replan delay zp_29 from the distance class ($E82A, doubled when the group contains slow units - $895A) with a floor of 4 and a cap of the urgency $896D, and then walks all 50 slots: every member of the group gets a move order to its own destination plus the delta through queueTrainerMoveOrder ($E919) and a new replan clock of gameClock - zp_29. Members that also belong to a real in-game group (unitGroupByte bit 6) propagate that clock to all their in-game group mates so the AI does not re-order the same formation twice.",
    "In:  $8955, zp_22/zp_24 target, $91CB, $895A, $896D, $8832, $F802/$F866, $F73A",
    "Out: $8800 replan clocks, $88D2/$88DE group target, orders appended to the queue at $88A0, zp_29/zp_2A/zp_2B",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E531": {
   "routine": [
    "computeTrainerGroupStats - Gathers the statistics of AI group Y (stored in $8955). Scans the 50 AI slots: sums the per-type combat weights from unitCombatValueTable ($E2B7) into the 16-bit trainerGroupValue ($8956/$8957), sets trainerGroupHasSlowUnits ($895A) when a GRUNT (type 0) or BOOMER (type 2) is present, counts members that have not yet penetrated into zp_1D, and builds the bounding box of the members' ordered destinations in zp_22/zp_24 (columns) and zp_23/zp_25 (rows). Then averageGroupPosition ($E882) gives the member count and the average current and ordered positions, which are stored as group centre ($88EA/$88F6), group size ($8902) and the four box offsets ($890E left, $8926 right, $891A top, $8932 bottom). Returns C=1 for an empty group.",
    "In:  Y = group id, $8832, $F79E, $F802, $F866, $8864, $895D, $8895",
    "Out: $8955, $8956/$8957, $895A, $8902, $88EA/$88F6, $890E/$891A/$8926/$8932, zp_1D, zp_92/zp_93 = group centre, zp_1E/zp_1F = average destination; C=1 if empty"
   ],
   "unit": "game/trainerAiE000"
  },
  "E5FF": {
   "routine": [
    "clampGroupTargetToMap - Clamps the group target in zp_22/zp_24 so that the whole formation box ($890E/$8926 columns, $891A/$8932 rows) still fits inside the 40x40 map, stores the clamped target as the group's target cell ($88D2/$88DE) and returns the movement delta relative to the group's average ordered position (zp_1E/zp_1F) in zp_2A (columns, also copied to $8958) and zp_2B (rows). Also called by the trainer setup overlay at $EF00.",
    "In:  zp_22/zp_24 target, zp_1E/zp_1F average destination, $8955, box offsets",
    "Out: zp_22/zp_24 clamped, zp_2A/zp_2B delta, $8958, $88D2/$88DE"
   ],
   "unit": "game/trainerAiE000"
  },
  "E66C": {
   "routine": [
    "offsetPointInDirection - Computes the cell that lies Y steps in compass direction X from the point (zp_27 column, zp_28 row), using directionColDeltaTable/directionRowDeltaTable ($E68A/$E693) and addStepsClamped ($EA1B). Returns C=1 when the column leaves the map (the row is then not computed); otherwise zp_18 = column, zp_19 = row. Used by the path search ($E44D), by evaluateCellSafety ($E69C) and by the drone target scan ($EF9F). Self-modifies the LDX operand at $E67D.",
    "In:  X = direction 0..7 (0 = north, clockwise), Y = number of steps, zp_27/zp_28 = origin",
    "Out: zp_18/zp_19 = target cell, C=1 if off the map"
   ],
   "unit": "game/trainerAiE000"
  },
  "E00F": {
   "unit": "game/trainerAiE000",
   "block": [
    "commRtsRtiStubs: byteTable, 2 bytes. $60 (RTS) and $40 (RTI). $E010 is the module's default NMI handler, which is what the initial value $E010 of commNmiVector ($E031/$E032) points at."
   ]
  },
  "E018": {
   "unit": "game/trainerAiE000",
   "block": [
    "unusedJumpSlotE018: unknown, 5 bytes. Five $00 bytes. The modem build (T34) has a sixth jump-table entry here; in the trainer build nothing calls $E018 and executing it would BRK."
   ]
  },
  "E01D": {
   "unit": "game/trainerAiE000",
   "block": [
    "commInterfaceBlock: byteTable, 38 bytes. $E01D-$E042: the fixed interface block shared by both comm-module builds - packetExchangeStatus, receivedPacketLength/Buffer, outgoingPacketLength/Buffer, commBuildId, commNmiVector, cia2IcrMask, the suspend handshake and the link flags. See the variables list for the individual bytes; $E034-$E038 and $E03E-$E042 are only used by the modem build."
   ]
  },
  "E142": {
   "unit": "game/trainerAiE000",
   "block": [
    "randomCoordinateOffsetTable: byteTable, 8 bytes. $01,$FF,$01,$01,$FF,$FF,$02,$FE - signed cell offsets picked with nextGameRandom AND 7 by randomiseCoordinate; scaled by 1, 2 or 4 depending on whether the human comcen has been spotted."
   ]
  },
  "E2B7": {
   "unit": "game/trainerAiE000",
   "block": [
    "unitCombatValueTable: byteTable, 5 bytes. $02,$02,$03,$00,$00 - combat weight per unit type (0 GRUNT 2, 1 RIDER 2, 2 BOOMER 3, 3 SPY 0, 4 COMCEN 0). Summed into trainerGroupValue by computeTrainerGroupStats and copied into the per-unit weight page $0464 by buildThreatWeights ($E7AF)."
   ]
  },
  "E2BC": {
   "unit": "game/trainerAiE000",
   "block": [
    "trainerUnitScanIndex: byteTable, 1 bytes. Loop counter of scanTrainerUnitsForOrders, kept as a byte inside the code stream (written at $E2BF/$E2EB, read at $E2CE)."
   ]
  },
  "E664": {
   "unit": "game/trainerAiE000",
   "block": [
    "maxAdvanceByRankTable: byteTable, 8 bytes. $05,$05,$05,$03,$03,$01,$01,$01 - maximum number of cells the path search may advance for direction-preference rank 0..7: the three best headings may be followed up to 5 cells, the worst three only 1."
   ]
  },
  "E69C": {
   "routine": [
    "isGroupDestinationTooRisky - Risk test for a candidate destination of the AI group in currentAiGroup ($8955). Computes the cell 'distance Y in direction X' from (zp_27,zp_28) with computeCellInDirection, expands it by the group's bounding box (groupSpanLeft/Up/Right/Down) into a rectangle clamped to 0..39, then walks all 100 units and sums unitThreatWeight ($0464) of every unit whose square influence area (unitThreatRadius $0400) overlaps that rectangle: plus for the human player's units 0-49, minus for the AI's own units 50-99 (own group members were already zeroed by buildUnitThreatTables). The tail at $E743 rejects a negative or zero total, otherwise subtracts the group's own strength ($8956/$8957) and returns carry.",
    "In:  X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row, $8955 currentAiGroup, $8954 aiGroupCount, group span tables $890E/$891A/$8926/$8932, $0400/$0464 threat tables (built by buildUnitThreatTables), $8956/$8957 group strength, $896E, $91CB",
    "Out: C=1 -> reject (off map, or enemy threat >= group strength; exact ties broken with nextGameRandom); C=0 -> acceptable. zp_18/zp_19 = candidate column/row, zp_1A/zp_1B = signed 16-bit threat total, zp_1C clobbered; self-modifies the operands at $E704,$E70B,$E715,$E71C",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E773": {
   "routine": [
    "buildUnitThreatTables - Rebuilds the two 100-entry influence-map tables used by isGroupDestinationTooRisky. For each unit 99..0 it stores an influence radius in $0400 and a strength weight in $0464. Dead units, spies/comcens (type >= 3) and AI units belonging to the group currently being moved get radius 0 and weight 0; grunts/riders get base 3, boomers base 6, each plus terrainSightBonusTable of the terrain the unit stands on. The weight comes from unitTypeStrengthTable ($E2B7) = 2,2,3,0,0.",
    "In:  $8955 currentAiGroup, aiUnitGroup table ($8800+unit), unitType $F76C, unitUnderCell $FAF0, terrainClassTable $077E, terrainSightBonusTable $35BB, unitTypeStrengthTable $E2B7",
    "Out: $0400[0..99] = threat radius, $0464[0..99] = threat weight; zp_18, zp_19 clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "E7B9": {
   "routine": [
    "multiplyAbyY - 8-bit multiply by repeated addition: returns A = A * Y (mod 256), with A = 0 when Y = 0. The multiplicand is stashed in the self-modified operand at $E7CB and Y is restored from the operand at $E7D0 before returning. Only used by offsetCoordinateClamped, where A is a step delta of 0, +1 or $FF and Y a cell distance.",
    "In:  A = multiplicand, Y = count",
    "Out: A = A*Y; Y preserved; X, flags clobbered; self-modifies $E7CB and $E7D0"
   ],
   "unit": "game/trainerAiE000"
  },
  "E7D3": {
   "routine": [
    "buildDirectionFanTowardTarget - Works out the compass direction from the line start (zp_93,zp_92) to the line end (zp_95,zp_94) with getStepDirection, saves it in zp_19 and, when a direction was found, falls into buildDirectionFan to lay out the eight candidate move directions around it. Called from the group-movement code at $E416.",
    "In:  zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column",
    "Out: zp_19 = heading 0-7 ($FF and no fan built when start == target); $8896-$889D = candidate directions; clobbers A/X/Y, zp_18, $8C-$97 via initLineStepper/stepLine",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E7E3": {
   "routine": [
    "buildDirectionFan - Fills the eight-byte scratch buffer $8896-$889D with candidate move directions: entry i = (dirFanOffsetTable[i] + heading + 8) AND 7, where the offset is mirrored through dirMirrorTable ($E7DB, (8-x) AND 7) when bit 0 of the group's groupFlags is set, so left-handed and right-handed groups fan out the opposite way. The list is consumed by the move search at $E431-$E47E, whose per-slot minimum-distance thresholds are in $E664.",
    "In:  zp_19 = heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable",
    "Out: $8896-$889D = 8 candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E80E": {
   "routine": [
    "getStepDirection - Runs initLineStepper + one stepLine between (zp_93,zp_92) and (zp_95,zp_94) and converts the resulting per-step deltas ($90F5 column, $90F4 row) into one of the eight compass directions by searching dirToColDelta/dirToRowDelta backwards.",
    "In:  zp_92/zp_93 start row/col, zp_94/zp_95 end row/col",
    "Out: Y = direction 0-7, or $FF when the deltas match no entry (start == end); A = last compared delta; line-stepper state $8C-$97, $90F4/$90F5 updated"
   ],
   "unit": "game/trainerAiE000"
  },
  "E82A": {
   "routine": [
    "cellDistance - Integer distance between two map cells: calls distanceSquared ($3870) for |dcol|^2+|drow|^2 and converts it back with squaresTable, returning the largest n with n*n <= dsq. Saturated squared distances return 16.",
    "In:  zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row",
    "Out: A = X = distance 0-16"
   ],
   "unit": "game/trainerAiE000"
  },
  "E83C": {
   "routine": [
    "updateFocusGroupCentre - Keeps focusGroupId ($895D) and its centre ($895E/$895F) up to date; the AI comcen escorts this group. When the id has bit 7 set the centre falls back to the AI comcen's own cell and the group is re-enabled if its groupFlags bit 7 has cleared; an enabled group whose groupFlags bit 7 is set is disabled by ORing $80 into the id; otherwise the centroid of the group's members 50-95 is recomputed with computeGroupCentroid and the group is dropped ($895D = $FF) when it has no members left.",
    "In:  $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707",
    "Out: $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E882": {
   "routine": [
    "computeGroupCentroid - Scans AI units (index 50+Y down to 50) and averages the position and the ordered destination of every living member whose aiUnitGroup entry equals A. Four 16-bit sums are accumulated in zp_18-zp_21 and divided by the member count with divideSigned16by8.",
    "In:  A = group id (patched into the compare at $E89D), Y = highest AI-relative unit index to scan (usually $31 or $2D); $8832 aiUnitGroup, $F79E unitType, $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow",
    "Out: A = member count (Z set when none), X = average column, Y = average row, zp_1E = average destination column, zp_1F = average destination row, zp_1C = member count; zp_18-zp_21 clobbered; self-modifies $E89D, $E913, $E915"
   ],
   "unit": "game/trainerAiE000"
  },
  "E919": {
   "routine": [
    "orderAiUnitToCell - Central 'issue a move order' helper of the trainer AI. Returns immediately when the unit's ordered destination ($F802/$F866 masked to 6 bits) already is the requested cell; otherwise it mirrors both coordinates with mirrorMapCoordinate (because the engine treats them as an opponent packet and mirrors them back) and queues command $80 MOVE, or $81 MOVE GROUP when the unit's unitFlags bit 6 marks it as a group member.",
    "In:  X = target column, A = target row, Y = AI-relative unit index 0-49 (absolute unit = Y+50)",
    "Out: $8896 = $80/$81, $8897 = absolute unit index, $8898/$8899 = mirrored column/row, then queueAiCommand; nothing when the order is redundant"
   ],
   "unit": "game/trainerAiE000"
  },
  "E937": {
   "routine": [
    "queueMirroredMoveOrder - Second half of orderAiUnitToCell, only reached by falling through in this build (the XREF entries $E917/$EA01 belong to the T34 build): mirrors $8898/$8899, chooses command $80 or $81 from unitFlags bit 6, writes the unit index and tail-jumps into queueAiCommand.",
    "In:  $8898/$8899 = target column/row, Y = AI-relative unit index, $F73A unitFlags",
    "Out: command staged in $8896-$8899 and queued"
   ],
   "unit": "game/trainerAiE000"
  },
  "E95D": {
   "routine": [
    "isPathToPlayerComcenClear - Walks a Bresenham line from the current position (zp_93,zp_92) to the human player's comcen (unitCol[49]/unitRow[49]) and reports whether any living enemy combat unit (index < 50, type 0-2) stands on it. Used at $E3B1 to decide whether the group can make a straight run at the comcen. Both exits leave (zp_93,zp_92) restored to the group's centre through loadGroupCentreIntoLinePos.",
    "In:  zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player comcen position, map at $F000",
    "Out: C=1 = line reaches the comcen unobstructed, C=0 = blocked or invalid group; zp_92/zp_93 = group centre; zp_48/zp_49 and the line-stepper state clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E997": {
   "routine": [
    "loadGroupCentreIntoLinePos - Copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6) into the line-walker position zp_93/zp_92 and returns with C=1. Used as both a subroutine and the C=1 exit path of isPathToPlayerComcenClear.",
    "In:  $8955 currentAiGroup, $88EA/$88F6",
    "Out: zp_93 = column, zp_92 = row, C=1, A/Y clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "E9A6": {
   "routine": [
    "scoreTargetBlock - Scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment target: every unit standing in the four cells contributes targetValueByUnitType (grunt 1, rider 2, boomer 4, spy 1, comcen 4), positive for the human player's units and negative for the AI's own. A block with a negative total is rejected; otherwise, if the total beats the running best in zp_1C, the corner is stored in zp_95/zp_94 and the highest player unit index seen in the block (which favours the comcen, unit 49) becomes the chosen target in zp_1D. Called from the missile/drone target search at $EFA8.",
    "In:  X = column, Y = row of the top-left cell, zp_1C = best score so far (threshold), zp_1D = best unit so far, map at $F000, $F76C unitType",
    "Out: zp_1A = block score, zp_1B = highest player unit index in the block; on improvement zp_1C, zp_1D, zp_94, zp_95 updated; zp_48/zp_49 = map pointer; self-modifies $E9E4 and $E9E8",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E9F6": {
   "routine": [
    "addUnitTargetValue - Helper of scoreTargetBlock: takes a map cell byte holding $80|unitIndex, ignores dead units, and adds targetValueByUnitType[type] to zp_1A, negated for the AI's own units 50-99. For a player unit it also keeps the largest index seen in zp_1B.",
    "In:  A = map cell byte with bit 7 set, $F76C unitType, $E9F1 targetValueByUnitType",
    "Out: zp_1A += weight, zp_1B = max player unit index; X, Y clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EA1B": {
   "routine": [
    "offsetCoordinateClamped - Returns clamp(A + X*Y, 0..39) - the base coordinate A displaced by Y steps of the signed delta X - and reports through the carry whether the unclamped result left the 0..39 map range.",
    "In:  A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps",
    "Out: A = clamped coordinate 0-39, C=1 when the raw value was outside 0..39, Y preserved; self-modifies $EA27 and $EA36"
   ],
   "unit": "game/trainerAiE000"
  },
  "EA38": {
   "routine": [
    "mirrorBlockCornerForOpponent - Converts the top-left corner (X,Y) of a 2x2 object into the coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn back into (X,Y): X = 39-(X+1), Y = 39-(Y+1). Used when the AI writes a drone position into the fake incoming packet at $EEB1.",
    "In:  X = column, Y = row of the block's top-left cell",
    "Out: X, Y = mirrored coordinates; A clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EA46": {
   "routine": [
    "queueAiCommand - Appends the command staged in $8896.. to the AI's outgoing queue at $88A0. Bumps aiCommandsThisRound ($8968), looks the command length up with getCommandLength, records a high-water mark in aiCmdQueuePeak ($EA45) and, if the queue would grow to 50 bytes or more, drops the command and sets the border-colour shadow $9003 to 6 as a visible overflow warning instead.",
    "In:  $8896-$8899 = staged command, $8952 aiCmdQueueLength, commandLengthTable via getCommandLength",
    "Out: $88A0 queue extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the copy counter), $EA45 peak; on overflow $9003 = 6"
   ],
   "unit": "game/trainerAiE000"
  },
  "EA7B": {
   "routine": [
    "injectAiCommandsIntoPacket - The heart of the solo trainer: instead of receiving a packet over the modem, the AI copies whole commands out of its own queue at $88A0 straight into the comm module's received-packet buffer $E020/$E01E, so the lock-step engine executes them as if the opponent had sent them. It first calls sub_ED0B (drone/missile logic), stops if that set bit 7 of $8966, and then moves as many complete commands as fit while the packet stays below 8 bytes, shifting the remainder of the queue down each time. Called every exchange from $E0E4.",
    "In:  $8952 aiCmdQueueLength, $88A0 queue, $E01E current packet length, getCommandLength",
    "Out: $E020.. receive buffer filled, $E01E updated, queue compacted, $8952 reduced, $8966 = 0 (then possibly set by sub_ED0B), $92EE clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EACF": {
   "routine": [
    "updateAiUnitTracking - Per-unit hook run from sub_E0E8 after the engine steps each unit. For a human unit (index < 50) it latches the first grunt/rider/boomer whose chosen fire target ($91F0) is the AI comcen (unit 99) into comcenAttackerUnit ($8972). For an AI unit it flags the unit's group as having contact with the player's comcen (groupFlags bit 6, plus a game-clock stamp) when that comcen has been spotted and the unit is within 8 cells of it; finally, for the spy units 96-98 that have reached the opponent's end zone and have no group yet, it allocates a fresh group id ($8953) and seeds that group's target from the unit's own destination.",
    "In:  $90FA currentUnit, $F76C unitType, $91F0 chosenTargetUnit, $F9F5 player comcen energy/spotted bits, $F671/$F6D5 player comcen position, $F640/$F6A4 positions, $8800 aiUnitOrderRound, $8832 aiUnitGroup, $8864 aiUnitDeepFlag, $91CB roundsRemaining, $8954, $8964",
    "Out: $8972, $893E groupFlags, $8953 lastGroupId, aiUnitGroup entry, $87CE+unit round stamp (= aiUnitOrderRound), $88D2/$88DE group target",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EB67": {
   "routine": [
    "runAiComcenActions - Spends the AI comcen's remaining command budget for the round (3 minus aiCommandsThisRound). When the comcen is stunned it needs two slots: it halts the comcen and, with a 1-in-8 chance, queues command $9E (comcen repaired). Otherwise it halts the comcen once and then calls maybeQueueComcenEnergyRepair for every remaining slot. Entered from $E1CD and $E21B.",
    "In:  $8968 aiCommandsThisRound, $8971 comcenStunCounter",
    "Out: $8967 comcenActionBudget, commands queued through queueAiCommand; returns silently when the budget is exhausted",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EBA5": {
   "routine": [
    "maybeQueueComcenEnergyRepair - With 50 percent probability, and only while the AI comcen's energy is below 45 of 50, queues command $9A (ADD ENERGY) for unit 99 with an amount of 1 + (50-energy)/8, occasionally bumped by one so that the running total stays aligned to 4. This is how the solo trainer heals its own command centre; the projected energy is accumulated in $8965.",
    "In:  $FA27 = unitEnergy[99] (bits 0-5), $8965 projected comcen energy, nextGameRandom",
    "Out: $8896 = $9A, $8897 = $63, $8898 = amount, command queued; $8965 increased",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EBDD": {
   "routine": [
    "queueComcenCommand - Tail entry shared by runAiComcenActions and maybeQueueComcenEnergyRepair: stores A as the command byte in $8896 and jumps into queueAiCommand, leaving $8897/$8898 as previously prepared.",
    "In:  A = command byte, $8897/$8898 = parameters already staged",
    "Out: command appended to the AI queue"
   ],
   "unit": "game/trainerAiE000"
  },
  "EBE4": {
   "routine": [
    "haltAiComcen - Orders the AI comcen (unit 99) to move to the cell it already occupies, which orderAiUnitToCell turns into a MOVE command only when the comcen currently has a different destination - i.e. it stops the comcen so that it can repair or launch. Called before the repair and drone actions in runAiComcenActions.",
    "In:  $F6A3 unitCol[99], $F707 unitRow[99]",
    "Out: possibly a $80/$81 command queued; nothing when the comcen is already halted",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EBEF": {
   "routine": [
    "assessThreatAroundAiComcen - Builds a directional threat map around the AI comcen and picks an escape direction. For each of the human player's units 0-45 within 20 cells it adds a weight (1 for being close, 4 more when the comcen is inside the unit's weapon range of 5, or 9 for boomers, plus 1 when the comcen is standing still) into one of eight direction buckets in $8896-$889D. The tail (past the end of this chunk, $EC82-$ED0A) copies the buckets to $0400, smooths them circularly as (2*b[i]+b[i-1]+b[i+1])/4 and picks the centre of the least threatening three-direction window, breaking ties on the RNG state.",
    "In:  zp_27/zp_28 = AI comcen column/row, $F76C/$F640/$F6A4 player unit records, $897C comcenIsMoving, zp_57 RNG state",
    "Out: A = (total threat - threat count)/2, X = deepest weapon-range penetration (0 = nothing in range), Y = safest direction 0-7 or 8 when no threats; $8896-$889D and $0400-$0407 = direction buckets, zp_18-zp_23 clobbered. The caller at $E1DB stores them into $8962/$8961/$8960",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E68A": {
   "unit": "game/trainerAiE000",
   "block": [
    "dirToColDelta: byteTable, 9 bytes. Column delta of the 8 compass directions numbered clockwise from north: 0,+1,+1,+1,0,-1,-1,-1. The 9th entry ($E692) is 0 and pairs with dirToRowDelta[8] to form a harmless 'no direction' entry used when assessThreatAroundAiComcen finds no threats."
   ]
  },
  "E693": {
   "unit": "game/trainerAiE000",
   "block": [
    "dirToRowDelta: byteTable, 9 bytes. Row delta of the same 8 directions: -1,-1,0,+1,+1,+1,0,-1, plus the 9th zero entry. Direction 0 = north, 2 = east, 4 = south, 6 = west."
   ]
  },
  "E7DB": {
   "unit": "game/trainerAiE000",
   "block": [
    "dirMirrorTable: byteTable, 8 bytes. 00,07,06,05,04,03,02,01 = (8 - d) AND 7: mirrors a direction across the north-south axis. Used by buildDirectionFan to flip the candidate fan for groups whose groupFlags bit 0 is set. (The label L_E7E0 in the middle of the table is a T34 branch target, not a separate block.)"
   ]
  },
  "E9F1": {
   "unit": "game/trainerAiE000",
   "block": [
    "targetValueByUnitType: byteTable, 5 bytes. 01,02,04,01,04 - bombardment value of a unit by type (grunt 1, rider 2, boomer 4, spy 1, comcen 4), used by addUnitTargetValue."
   ]
  },
  "EA45": {
   "unit": "game/trainerAiE000",
   "block": [
    "aiCmdQueuePeak: byteTable, 1 bytes. One byte of data embedded in the code stream between mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI command queue length. Written and compared only inside queueAiCommand; no other code reads it."
   ]
  },
  "EC82": {
   "routine": [
    "rankThreatDirections - Second half of sub_EBEF (the routine starts at $EBEF, outside this chunk). sub_EBEF has just scanned the human player's mobile units 0-45 and accumulated a saturating 'enemy pressure' weight per compass direction into the 8 bytes $8896-$889D, the total into zp_1F, the number of threatening units into zp_20 and the largest weapon-range surplus into zp_21. This tail copies the 8-direction ring to the $0400 scratch page, smooths it (sub-loop at $EC91), searches the quietest 3-direction arc (sub-loop at $ECD4) and returns the summary. If zp_20 was 0 the caller path at $EC7C returns A=0, X=0, Y=8 instead (index 8 of the direction tables $E68A/$E693 is the null delta 0,0).",
    "In:  zp_1F total threat weight, zp_20 threatening unit count, zp_21 max range surplus, $8896-$889D per-direction weights, zp_57 (game RNG low byte) for the tie break",
    "Out: A = (zp_1F - zp_20) / 2 = overall threat level; Y = zp_18 = index 0-7 of the safest (lowest-pressure) compass direction, or 8 when nothing threatens; X = zp_21; $0400-$0407 and $8896-$889D overwritten; zp_18, zp_19, zp_1A, zp_23 clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EC91": {
   "routine": [
    "smoothThreatRing - Inner loop of rankThreatDirections (branch target of the DEC zp_23 / BPL at $ECCA, not a JSR target in this build). For each direction i = 7..0 it computes (2*ring[i] + ring[i-1] + ring[i+1]) / 4 with a 2-bit carry kept in zp_19, reading the unsmoothed copy from $0400,i and writing the result back to $8896,i. Indices wrap modulo 8, so it is a circular 1-2-1 blur of the 8-direction threat ring.",
    "In:  $0400-$0407 = unsmoothed direction weights, zp_23 = current direction index",
    "Out: $8896-$889D = smoothed weights; zp_19 carry scratch; zp_23 counts down to $FF"
   ],
   "unit": "game/trainerAiE000"
  },
  "ECD4": {
   "routine": [
    "findQuietestDirectionArc - Second inner loop of rankThreatDirections (branch target of $ECFE). For each y = 7..0 it sums the smoothed weights of the three adjacent directions y, y-1, y-2 (wrapping) and keeps the smallest sum in zp_19 with the centre direction (y-1) in zp_18, so the result is the middle of the calmest 135-degree arc. Ties are broken pseudo-randomly by BIT zp_57 / BMI at $ECF3 (zp_57 is the low byte of the lock-step game RNG state $57-$59). The three-term sum is 8-bit and can wrap if all weights are large.",
    "In:  $8896-$889D smoothed weights, zp_57",
    "Out: zp_18 = quietest direction 0-7, zp_19 = its arc sum; zp_1A scratch"
   ],
   "unit": "game/trainerAiE000"
  },
  "ED0B": {
   "routine": [
    "runAiMissileTurn - The computer opponent's missile/drone decision, called once per command exchange from sub_EA7B ($EA80), which is itself reached from the packet-transmit entry sub_E000 (X=1) at $E082. It first offers to launch the AI's own drone (sub_EE85), then, if the human player currently has a drone in the air ($920D bit 6) and the AI still has missiles ($92B1), decides whether to shoot at that drone: the drone cell is $92F6+6 / $92F7+5, the aim point goes into zp_22/zp_24 and a reluctance score is built in zp_23 from the range to the AI comcen, the comcen's stun state ($F9C3 & $0F), its energy bits and a missile-conservation test against the human's remaining drones ($92AE). A shot fires either as a clean kill (command $92 written at $E020, i.e. 'drone finished') or as a near miss whose impact point is scattered by +/-(zp_23/2+1) cells with sub_EE55. It then falls through into the code at $EDD5/$EE00 that fires at an ordinary spotted unit.",
    "In:  $8966 action budget, $920D droneStateFlags, $92B1 AI missiles left, $92AE human drones left, $92F6/$92F7 drone view origin, $F6A3/$F707 AI comcen col/row, $F9C3, $FA27, $8970 reload timer, $920B/$896F drone heading, game RNG",
    "Out: may append command $92 and/or $90 col,row to the fabricated incoming packet ($E020/$E01E), $8970 reload timer, $8966 decremented, $8968 += 2, zp_22/zp_23/zp_24/zp_29/zp_2B/zp_18 clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EDD5": {
   "routine": [
    "fireMissileAtSpottedUnit - Continuation of runAiMissileTurn (branch/JMP target only). When no player drone is being steered, the AI still has more missiles than twice the human's remaining drones, its reload timer $8970 has expired and its action budget $8966 is not spent, it fires a missile at the unit index held in $8972 (or, falling through at $EE00, at the human comcen unit $31 when that comcen is currently visible, $F9F5 bit 7). sub_EE6A checks that the target sits inside the AI comcen's 21x17 radar grid; the reload timer is then set to half the range and sub_EE2D queues the $90 strike. Ends by remembering the current drone heading in $896F and ticking $8970 down.",
    "In:  $920D, $92AE, $92B1, $8970, $8966, $8972 current missile target, $F76C unit type/alive flags, $F9F5, $920B",
    "Out: $8972 (set to $FF when no shot), $8970, $896F, packet bytes via sub_EE2D",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EE00": {
   "routine": [
    "targetEnemyComcenWithMissile - Fall-through label inside fireMissileAtSpottedUnit: if the human comcen (unit $31) is currently visible ($F9F5 bit 7) it becomes the missile target. IMPORTANT: although XREF lists 'sub_EE00 call:E0A5', the JSR at $E0A5 does NOT reach this code - $EE00-$EFFF is a swappable sub-overlay slot and $E0A5 is only executed while the track 29 s4-5 variant (disassembly/game/ovl_EE00_T29.s, the AI's drone-flying code) is loaded there. In this T35 build $EE00 is never entered from outside.",
    "In:  $F9F5 = energy/visibility byte of unit 49 (the human comcen)",
    "Out: X = $31 when the comcen is visible, otherwise branches away to $EE19"
   ],
   "unit": "game/trainerAiE000"
  },
  "EE2D": {
   "routine": [
    "queueMissileStrikeCommand - Appends a 3-byte command $90 col,row (missile strike, handler cmdMissileStrike $53C0) to the fabricated incoming packet at $E020, length $E01E. Both coordinates go through mirrorMapCoordinate ($3FA3, 39-v) because the packet is presented to the engine as if it had arrived from the remote player, and the command dispatcher un-mirrors remote parameters. Also spends one AI action ($8966--) and charges two units of packet cost ($8968 += 2).",
    "In:  zp_22 = strike column, zp_24 = strike row, $E01E current packet length",
    "Out: $E020+ packet bytes, $E01E advanced by 3, $8966 decremented, $8968 incremented twice, X clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EE55": {
   "routine": [
    "randomlySignValue - Returns A or -A with equal probability, using bit 0 of nextGameRandom ($FD5D). Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss the player's drone.",
    "In:  A = magnitude",
    "Out: A = zp_18 = +/-A (two's complement), game RNG advanced"
   ],
   "unit": "game/trainerAiE000"
  },
  "EE6A": {
   "routine": [
    "isUnitOnAiRadar - Loads unit X's map position into zp_22/zp_24 and tests whether that cell lies inside the 21x17 radar grid centred on the AI's comcen: it temporarily forces comcenUnit $9236 to $63 (unit 99 = side 1 comcen), calls overlay B's mapCellToRadarGrid ($7A13) and restores $9236 to $31 (unit 49 = the local player's comcen). The unconditional restore to $31 shows that this module always assumes the local player is side 0 and the computer opponent is side 1.",
    "In:  X = unit index, $F640/$F6A4 unit column/row arrays",
    "Out: zp_22 = column, zp_24 = row, C = 0 when the cell is on the AI's radar grid, C = 1 otherwise; $9236 left at $31; X/Y clobbered"
   ],
   "unit": "game/trainerAiE000"
  },
  "EE85": {
   "routine": [
    "launchAiDrone - Called first thing by runAiMissileTurn. When the launch delay $8977 has run out, a drone target unit has been chosen ($8976 >= 0) and no drone is airborne ($920D bit 7 clear), it fabricates the opponent's drone launch directly into the incoming packet: $E020 = $97 (launch), $E021 = $8D (drone move) with heading, column and row parameters, $E01E = 5. The launch point is the AI comcen's cell converted with sub_EA38 (v+1 then mirrored = 38-v, pre-compensating the mirror-and-decrement the $8D handler applies to remote commands). The heading byte is whatever sub_E80E leaves in A, which is the row component of the direction step (-1/0/+1); cmdDroneMove turns it into sprite shape (heading+4)&7 = 3/4/5, i.e. the drone is always drawn flying towards the player - possibly deliberate, possibly a slip for the direction index that sub_E80E returns in Y.",
    "In:  $8977 launch delay, $8976 target unit, $920D, $F6A3/$F707 AI comcen position, $F640/$F6A4 target position",
    "Out: $E020-$E024 = $97,$8D,heading,col,row; $E01E = 5; $8974/$8975 = launch column/row; $8973 = $64 (100 drone steps); $8977 = 3; $8966 decremented; zp_92-zp_95 line-stepper state clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EEE0": {
   "routine": [
    "chooseAiDroneTarget - Called once per AI round from $E1FF. Decides what the computer opponent should send its next drone at and stores the chosen unit index in $8976. Returns immediately if the AI has no drones left ($92AF = 0) or a target is already pending. In game type 2 (THE BOMB) the only target is the human comcen, once it is visible or on the AI's radar. Otherwise, if the human comcen is visible it sums the weights $8902[i] of every objective flagged $20 (doubled) or $40 in $893E[i]; when that sum reaches 8 the AI commits to an all-out attack: it sets bit 6 in every objective flag, re-stamps the order clock of all 46 mobile AI units (the 46-byte array at $8800, indexed relative to unit 50) with the game clock $91CB, sets the launch delay $8977 = $15 and targets the comcen. If the comcen is not visible it falls into pickDroneTargetAlongThreatAxis.",
    "In:  $92AF AI drones left, $8976, $0BA3 game type, $F9F5 comcen visibility, $8902/$893E objective tables, $8953/$8954 objective count, $91CB game clock",
    "Out: $8976 = target unit index ($31 for the comcen), $893E[] bit 6 set, $8800-$882D = $91CB, $8977 = $15; zp_18, zp_19 clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EF59": {
   "routine": [
    "pickDroneTargetAlongThreatAxis - Tail of chooseAiDroneTarget, reached when the human comcen is not a viable drone target. It takes the AI's current objective ($895D) - its map cell from $88EA/$88F6 and its threat threshold from $8902, or the AI comcen and threshold 3 when no objective is active - and runs the threat scan sub_EBEF around it. If the measured pressure reaches the threshold and at least one enemy unit is in weapon range, it walks a ray in the direction opposite the safest one ((Y+4) & 7, i.e. straight into the enemy) at radii 4..14 with sub_E66C and scores each 2x2 block of cells with sub_E9A6 (unit values 1/2/4/1/4 for GRUNT/RIDER/BOOMER/SPY/COMCEN). The best cluster's unit index becomes the drone target $8976, and the acceptance threshold $8979 is reduced to 75% of its value so the AI grows less picky over the game.",
    "In:  $895D current objective, $88EA/$88F6 objective column/row, $8902 objective weights, $8979 target value threshold, $F6A3/$F707 AI comcen position",
    "Out: $8976 target unit index, $8979 scaled down, $8956 threshold copy, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius, zp_22/zp_24/zp_27/zp_28 clobbered",
    "(confidence: medium)"
   ],
   "unit": "game/trainerAiE000"
  },
  "EFCD": {
   "unit": "game/trainerAiE000",
   "block": [
    "strayTextTail: text, 3 bytes. $43,$45,$AE = 'CE' plus '.'|$80. Stale tail of the string 'INSERT DATA DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the T34 build of this overlay slot; here the same bytes are preceded by code, so only the last three characters survive. Unreferenced."
   ]
  },
  "EFD0": {
   "unit": "game/trainerAiE000",
   "block": [
    "msgDatagamecustomOff2: text, 18 bytes. 'DATAGAMECUSTOM OFF' with bit 7 on the final F. Byte-identical to the same block in ovl_E000_T34, ovl_EC00_T29 and ovl_EE00_T29 - mastering-tool filler at the tail of every $EC00/$EE00 overlay build. No reference anywhere in the disassembly."
   ]
  },
  "EFE2": {
   "unit": "game/trainerAiE000",
   "block": [
    "msgDestroyOff2: text, 11 bytes. 'DESTROY OFF' with bit 7 on the final F. Same shared filler block; unreferenced."
   ]
  },
  "EFED": {
   "unit": "game/trainerAiE000",
   "block": [
    "msgAccepted2: text, 8 bytes. 'ACCEPTED' with bit 7 on the final D. Same shared filler block; unreferenced."
   ]
  },
  "EFF5": {
   "unit": "game/trainerAiE000",
   "block": [
    "overlayTailFiller: unknown, 11 bytes. $C0,$DF then $00,$FF repeated to the end of the page. Identical in all four builds of the $EC00/$EE00 overlay slot except for the very last byte ($FF here and in the track 29 variants, $00 in ovl_E000_T34). Looks like leftover buffer content from the mastering system; unreferenced."
   ]
  }
 }
}