; ============================================================================
; $E000-$EFFF - solo trainer: the computer opponent (track 35 sectors 0-15)
; ============================================================================
; Loaded by $0F1A when the solo trainer was chosen ($0BA5 bit 7 set). It presents the same jump table as
; the modem driver but fakes the link: instead of sending and receiving over a wire it plans the moves of
; side 1 and injects them straight into the received-packet buffer. $EE00-$EFFF is swapped for the drone
; AI (track 29 sectors 4-5) while its drone is airborne.

        .setcpu "6502"
        .include "c64.inc"
        .include "zeropage.inc"

; ---- references to code/data outside this file ----
checksumXorConstant      := $0400
play4Grp12               := $0464
D_077E                   := $077E
loadSectors              := $0804
setLoadDest              := $085C
filmPlaybackMode         := $0B9B
gameTypeOptions          := $0BA3
gameEndReason            := $0BA8
D_2009                   := $2009
readMapCell              := $25FA
terrainSightBonusTable   := $35BB
squaresTable             := $3860
distanceSquared          := $3870
D_3E99                   := $3E99
mirrorMapCoordinate      := $3FA3
D_434C                   := $434C
D_4CE0                   := $4CE0
getCommandLength         := $4D9F
D_60E0                   := $60E0
L_754C                   := $754C
D_774C                   := $774C
mapCellToRadarGrid       := $7A13
msgWeWonByAKnockout      := $87CE
msgGetOpponentOnPhoneAnd := $8800
D_8831                   := $8831
D_8832                   := $8832
D_8864                   := $8864
D_8895                   := $8895
D_8896                   := $8896
msgPickUpPhoneThenPressSpace := $8897
D_8898                   := $8898
D_8899                   := $8899
D_88A0                   := $88A0
D_88D2                   := $88D2
D_88DE                   := $88DE
D_88EA                   := $88EA
D_88F6                   := $88F6
D_8902                   := $8902
D_890E                   := $890E
D_891A                   := $891A
D_8926                   := $8926
D_8932                   := $8932
D_893E                   := $893E
D_894A                   := $894A
D_8952                   := $8952
D_8953                   := $8953
D_8954                   := $8954
D_8955                   := $8955
D_8956                   := $8956
D_8957                   := $8957
D_8958                   := $8958
D_895A                   := $895A
D_895B                   := $895B
D_895D                   := $895D
msgOpponentUsingNonStandardGame := $895E
D_895F                   := $895F
D_8960                   := $8960
D_8961                   := $8961
D_8962                   := $8962
D_8963                   := $8963
D_8964                   := $8964
D_8965                   := $8965
D_8966                   := $8966
D_8967                   := $8967
D_8968                   := $8968
D_8969                   := $8969
D_896A                   := $896A
D_896B                   := $896B
D_896C                   := $896C
D_896D                   := $896D
D_896E                   := $896E
D_896F                   := $896F
D_8970                   := $8970
D_8971                   := $8971
D_8972                   := $8972
D_8973                   := $8973
D_8974                   := $8974
D_8975                   := $8975
D_8976                   := $8976
D_8977                   := $8977
D_8979                   := $8979
D_897A                   := $897A
D_897B                   := $897B
D_897C                   := $897C
D_8D03                   := $8D03
vicBorderShadow          := $9003
lastStepRowDelta         := $90F4
lastStepColDelta         := $90F5
currentUnit              := $90FA
gameClock                := $91CB
fireTargetUnit           := $91F0
droneHeading             := $920B
droneControlFlags        := $920D
comcenUnit               := $9236
dronesLeft               := $92AE
opponentDronesLeft       := $92AF
opponentMissilesLeft     := $92B1
currentCommandLength     := $92EE
droneViewOriginCol       := $92F6
droneViewOriginRow       := $92F7
D_A989                   := $A989
D_B908                   := $B908
unitColTable             := $F640
side0ComcenCol           := $F671
side1UnitColTable        := $F672
side1ComcenCol           := $F6A3
unitRowTable             := $F6A4
side0ComcenRow           := $F6D5
side1UnitRowTable        := $F6D6
side1ComcenRow           := $F707
side1UnitFlagsTable      := $F73A
unitTypeTable            := $F76C
side1UnitTypeTable       := $F79E
unitDestColTable         := $F7D0
side1UnitDestColTable    := $F802
side1ComcenDestCol       := $F833
unitDestRowTable         := $F834
side1UnitDestRowTable    := $F866
side1ComcenDestRow       := $F897
side1ComcenStun          := $F9C3
side0ComcenEnergy        := $F9F5
side1ComcenEnergy        := $FA27
unitTerrainUnderTable    := $FAF0
side1ComcenDisplayFlags  := $FBB7
initLineStepper          := $FBB8
stepLine                 := $FBFA
divideSigned16by8        := $FCEC
nextGameRandom           := $FD5D

; Contents
; --------
;    $E000  commRequestEntry             Jump-table slot 0 of the opponent module (JMP handleCommRequest).
;    $E003  commSetLinkStateEntry        Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or down.
;    $E006  commGetReceivedByteEntry     Jump-table slot 2 (JMP reportNoReceivedByte).
;    $E009  commSendByteEntry            Jump-table slot 3 (JMP commNoOpClc).
;    $E00C  commFrameTickEntry           Jump-table slot 4 (JMP commNoOpClc).
;    $E012  commKeyboardScanVector       A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan
;                                        vector.
;    $E015  commReadKeyEntry             Jump-table slot 5 (JMP commKeyboardScanVector).
;    $E043  handleCommRequest            Body of commRequestEntry.
;    $E060  setCommLinkState             Body of commSetLinkStateEntry.
;    $E075  reportNoReceivedByte         SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in
;                                        the trainer build.
;    $E077  commNoOpClc                  CLC + RTS: the shared no-op used as the body of commSendByteEntry,
;                                        commFrameTickEntry and the unpatched keyboard vector.
;    $E079  runTrainerRoundHook          Once-per-round hook, called from finishRound ($4654) right after the game clock
;                                        has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded).
;    $E082  buildTrainerReplyPacket      Entered with X=1 from sendOutgoingPacket, i.e.
;    $E0E8  trainerUnitStepHook          Per-unit hook called at the end of processUnitStep ($4526) for currentUnit,
;                                        i.e.
;    $E128  setSubOverlayLoadDest        Points the fast loader at $EE00 (X = destination low byte, Y = high byte) and
;                                        tail-jumps into setLoadDest.
;    $E12F  pickTrainerRallyPoint        Chooses the AI's current objective: a cell scattered at random around the human
;                                        comcen (unit 49), stored as the rally column/row that every attacking group heads for.
;    $E14A  randomiseCoordinate          Returns A = base coordinate plus a random offset, clamped to 0..39.
;    $E170  planTrainerRound             Top-level planner of the solo trainer, run once per game round.
;    $E21F  pushTrainerComcenForward     Attack-mode tail of planTrainerRound (a branch target, not a JSR entry).
;    $E266  retreatTrainerComcen         Defend-mode tail of planTrainerRound (a branch target).
;    $E2BD  scanTrainerUnitsForOrders    Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down to 0,
;                                        stopping as soon as four commands have been queued this round.
;    $E2F1  decideTrainerGroupAction     Decides what the AI group owning unit ordinal Y should do.
;    $E339  aimGroupAtOwnComcen          Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from
;                                        $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343 that copies the group
;                                        centre into the search origin and the line target into the wanted destination, and jumps to issueTrainerGroupOrders.
;    $E3D0  aimGroupAtHomeLine           Internal branch target used when the group still has members that have not
;                                        reached the player's end zones and has no contact with his comcen: the target column stays where the group already is
;                                        and the target row becomes 29 plus the group's own vertical extents, i.e.
;    $E42A  findTrainerAdvanceCell       Threat-aware path search.
;    $E49E  issueTrainerGroupOrders      Issues the move orders for the group in $8955.
;    $E531  computeTrainerGroupStats     Gathers the statistics of AI group Y.
;    $E5FF  clampGroupTargetToMap        Clips the wanted group centre in zp_22/zp_24 so that the whole formation box
;                                        (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores the clipped cell
;                                        as the group's target and returns the movement delta relative to the group's average ordered destination in zp_2A
;                                        (columns, also kept in $8958) and zp_2B (rows).
;    $E66C  offsetPointInDirection       Computes the cell that lies Y steps in compass direction X from the reference
;                                        point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and offsetCoordinateClamped.
;    $E69C  isGroupDestinationTooRisky   influence-map test of one candidate destination for the AI group in
;                                        currentAiGroup ($8955).
;    $E773  buildUnitThreatTables        rebuilds the two 100-entry influence-map tables that isGroupDestinationTooRisky
;                                        reads.
;    $E7B9  multiplyAbyY                 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y =
;                                        0.
;    $E7D3  buildDirectionFanTowardTarget finds the compass heading from the line start (lineCurCol,lineCurRow) to the
;                                         line end
;    $E7E3  buildDirectionFan            fills the eight-byte scratch buffer $8896-$889D with the move directions the
;                                        path search should try,
;    $E80E  getStepDirection             sets a Bresenham walker up between (lineCurCol,lineCurRow) and
;                                        (lineEndCol,lineEndRow), takes exactly one
;    $E82A  cellDistance                 integer distance between two map cells.
;    $E83C  updateFocusGroupCentre       keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row)
;                                        current.
;    $E882  computeGroupCentroid         averages the position and the ordered destination of every living member of one
;                                        AI group.
;    $E919  orderAiUnitToCell            the trainer's central 'issue a move order' helper.
;    $E937  queueMirroredMoveOrder       second half of orderAiUnitToCell.
;    $E95D  isPathToPlayerComcenClear    walks a Bresenham line from the current line position (which the caller has set
;                                        to the group
;    $E997  loadGroupCentreIntoLinePos   copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6)
;                                        into the line
;    $E9A6  scoreTargetBlock             scores the 2x2 block of map cells whose top-left corner is (X,Y) as a
;                                        bombardment target.
;    $E9F6  addUnitTargetValue           helper of scoreTargetBlock.
;    $EA1B  offsetCoordinateClamped      returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps of
;                                        the signed step X, held
;    $EA38  mirrorBlockCornerForOpponent converts the top-left corner of a 2x2 object (the drone) into the coordinates
;                                        that the engine's
;    $EA46  queueAiCommand               appends the command staged in $8896..$8899 to the AI's 50-byte order queue at
;                                        $88A0.
;    $EA7B  injectAiCommandsIntoPacket   the heart of the solo trainer.
;    $EACF  updateAiUnitTracking         per-unit hook, run from trainerUnitStepHook after the engine has stepped each
;                                        unit.
;    $EB67  runAiComcenActions           spends whatever is left of the AI comcen's three command slots for this round.
;    $EBA5  maybeQueueComcenEnergyRepair the trainer's version of the human console's energy repair.
;    $EBDD  queueComcenCommand           shared tail of the comcen actions: stores A as the command byte and jumps into
;                                        queueAiCommand, leaving
;    $EBE4  haltAiComcen                 orders the AI comcen to move to the cell it is already standing on.
;    $EBEF  assessThreatAroundAiComcen   builds a directional pressure map around the AI comcen and picks the direction
;                                        to run.
;    $EC82  rankThreatDirections         Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just turned
;                                        every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one of the eight compass
;                                        buckets $8896-$889D.
;    $EC91  smoothThreatRing             Circular 1-2-1 blur of the eight-direction threat ring.
;    $ECD4  findQuietestDirectionArc     For every leading direction y = 7..0 it adds the blurred pressures of y, y-1
;                                        and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle direction y-1.
;    $ED0B  runAiMissileTurn             The computer opponent's one 'weapon' decision per lock-step exchange.
;    $EDD5  fireMissileAtSpottedUnit     Second half of the missile turn, reached whenever no anti-drone shot was taken.
;    $EE00  targetEnemyComcenWithMissile Fall-through tail of fireMissileAtSpottedUnit: when the AI has no remembered
;                                        attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49) instead, but only while that
;                                        comcen is currently visible.
;    $EE2D  queueMissileStrikeCommand    Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler
;                                        cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to have received from
;                                        the opponent.
;    $EE55  randomlySignValue            Returns A or -A with equal probability, using bit 0 of the lock-step game RNG.
;    $EE6A  isUnitOnAiRadar              Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17
;                                        radar grid centred on the computer opponent's own COMCEN.
;    $EE85  launchAiDrone                Sends the computer opponent's drone up when one is due.
;    $EEE0  chooseAiDroneTarget          Picks what the computer opponent should send its next drone at and leaves the
;                                        unit index in $8976.
;    $EF59  pickDroneTargetAlongThreatAxis Tail of chooseAiDroneTarget for the case where the human comcen is no use as
;                                          a drone target.

        .org $E000


; ----------------------------------------------------------------------
; commRequestEntry - Jump-table slot 0 of the opponent module (JMP handleCommRequest). The game calls
; it every raster IRQ with X=0 ($1129), with X=1 from sendOutgoingPacket ($56AE) to hand over the
; outgoing packet, with X=2 to acknowledge a completed exchange ($4EFF/$526F) and with X=3 to cancel a
; pending send. In this build (track 35 = SOLO TRAINER) there is no link at all: X=1 runs one step of
; the computer opponent and leaves its orders in the receive buffer.
; In:  X = request code 0..3
; Out: see handleCommRequest ($E043)
; Called from: mainProgram0800 $1129 (raster IRQ, X=0), $4EFF and $526F (X=2), $56AE (X=1),
;     $570F/$5727/$573B/$5767/$5776 (menu-level exchange)
; ----------------------------------------------------------------------
commRequestEntry:
        jmp     handleCommRequest       ; E000  slot 0: per-frame poll / packet request, X selects the function


; ----------------------------------------------------------------------
; commSetLinkStateEntry - Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or
; down.
; In:  X = 0 open, >= 1 close
; Out: isLinkActive / linkStatus / linkStatusSample ($E03B/$E03C/$E03D), and $E01F when closing
; Called from: initCommModule $0F46 (X=1), hangUpModemSetState $1B56 and $1B70 (X=3)
; ----------------------------------------------------------------------
commSetLinkStateEntry:
        jmp     setCommLinkState        ; E003  slot 1: open (X=0) or close (X>0) the link flags


; ----------------------------------------------------------------------
; commGetReceivedByteEntry - Jump-table slot 2 (JMP reportNoReceivedByte). In the modem build this
; pops the UART receive ring; the trainer has no wire, so it always answers 'nothing received' and the
; opponent chat line stays empty in a solo game.
; In:  none
; Out: C=1 always (no byte)
; Called from: receiveChatCharacter $1399
; ----------------------------------------------------------------------
commGetReceivedByteEntry:
        jmp     reportNoReceivedByte    ; E006  slot 2: fetch a received chat byte - the trainer never has one


; ----------------------------------------------------------------------
; commSendByteEntry - Jump-table slot 3 (JMP commNoOpClc). The chat character handed over is simply
; dropped and C=0 ('accepted') is returned.
; In:  A = byte to send (ignored)
; Out: C=0
; Called from: handleChatSend $1396 (JMP, not JSR)
; ----------------------------------------------------------------------
commSendByteEntry:
        jmp     commNoOpClc             ; E009  slot 3: send a chat byte - the trainer throws it away


; ----------------------------------------------------------------------
; commFrameTickEntry - Jump-table slot 4 (JMP commNoOpClc). In the modem build this services the
; software UART once per frame; with no serial line there is nothing to do.
; In:  none
; Out: C=0
; Called from: rasterIrqHandler $112C
; ----------------------------------------------------------------------
commFrameTickEntry:
        jmp     commNoOpClc             ; E00C  slot 4: per-IRQ service tick - nothing to service without a UART


; ----------------------------------------------------------------------
; Two one-instruction stubs the module can point a vector at.  $E00F is a plain RTS and $E010 an RTI;
; $E010 is the initial value of nmiChainVector ($E031/$E032), so an NMI arriving before the game
; patches
; that vector simply returns.
; ----------------------------------------------------------------------
commRtsRtiStubs:
        rts                             ; E00F  $60 = RTS: harmless body for any vector pointed here

        .byte   $40                     ; E010  $40 = RTI: the module's default NMI handler, target of the initial $E031/$E032 = $E010
isOriginateMode:
        .byte   $00                     ; E011  answer/originate flag: waitForAnswerOriginateKey ($0CDE) stores 0 for 'A' and 1 for 'O'. Modem-only; nothing in the trainer build reads it

; ----------------------------------------------------------------------
; commKeyboardScanVector - A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan
; vector. initCommModule patches them to the game's scanKeyboard ($0DB7) with the two stores at
; $0F4B/$0F50; until then the vector points at commNoOpClc, so no key is ever reported.
; In:  none
; Out: whatever scanKeyboard returns (A = key code, X/Y = shift / Commodore-key flags)
; Called from: commReadKeyEntry $E015 (JMP). The XREF also lists a call from $E2EC, but that address
;     is mid-instruction here - it belongs to the modem build that shares this address range.
; ----------------------------------------------------------------------
commKeyboardScanVector:
        jmp     commNoOpClc             ; E012  operand bytes $E013/$E014 are the keyboard vector, patched to scanKeyboard $0DB7 by $0F4B/$0F50


; ----------------------------------------------------------------------
; commReadKeyEntry - Jump-table slot 5 (JMP commKeyboardScanVector). This is the only route by which
; the game reads the keyboard, so a comm module that owns the CPU can steal keys before the game sees
; them.
; In:  none
; Out: key code in A, shift / Commodore-key flags in X and Y
; Called from: pollKeyboardEvent $0E56 (every frame)
; ----------------------------------------------------------------------
commReadKeyEntry:
        jmp     commKeyboardScanVector  ; E015  slot 5: read one key through the patchable vector above


; Dead space.  The modem build (track 34) puts a sixth jump-table entry at $E018; the trainer build
; leaves five $00 bytes here.  Nothing in this file or in the main program calls $E018, and executing
; it would BRK.
unusedJumpSlotE018:
        .byte   $00                     ; E018  five unused $00 bytes where the modem build has its sixth jump-table slot
        .byte   $00,$00,$00,$00         ; E019  ....

; $E01D-$E042 is the fixed interface block that both comm-module builds present to the game at exactly
; the
; same addresses: the packet-exchange flags, the received and outgoing packet lengths and their two
; 7-byte buffers, the build id, the NMI chain vector, the CIA2 ICR shadow, the disk-access suspend
; handshake and the three link flags.  In the trainer build most of it is only ever written by
; handleCommRequest and setCommLinkState; $E034-$E038 and $E03E-$E042 are modem-build variables that
; stay zero for ever.
commInterfaceBlock:
        .byte   $00                     ; E01D  exchange flags: bit 7 = opponent packet received, bit 6 = exchange complete, bits 0/5 = send requested / in progress. handleCommRequest pins it at $C0
rxPacketLength:
        .byte   $00                     ; E01E  length 0-7 of the 'received' packet the trainer fabricates in $E020
txPacketLength:
        .byte   $00                     ; E01F  length of the outgoing packet the game built in $E027; the trainer never reads the bytes
rxPacketBuffer:
        .byte   $00                     ; E020  received-packet buffer $E020-$E026, byte 0 = the command byte the engine will execute
rxPacketArg1:
        .byte   $00                     ; E021  received packet byte 1 (first command argument)
rxPacketArg2:
        .byte   $00                     ; E022  received packet byte 2
rxPacketArg3:
        .byte   $00                     ; E023  received packet byte 3
rxPacketArg4:
        .byte   $00                     ; E024  received packet bytes 4-6, the tail of the 7-byte buffer
        .byte   $00,$00                 ; E025  ..
txPacketBuffer:
        .byte   $00                     ; E027  outgoing-packet buffer $E027-$E02D, filled by sendOutgoingPacket ($568C); discarded here
txPacketArg1:
        .byte   $00                     ; E028  outgoing packet byte 1
txPacketArg2:
        .byte   $00                     ; E029  outgoing packet byte 2
txPacketArg3:
        .byte   $00                     ; E02A  outgoing packet bytes 3-6
        .byte   $00,$00,$00             ; E02B  ...
hostOutCount:
        .byte   $00                     ; E02E  chat transmit-queue depth; handleChatSend ($1357/$1376) only opens typing while it is 0, so in a solo game the editor opens and the text goes nowhere
peerCommBuildId:
        .byte   $00                     ; E02F  build id reported by the peer; never filled in without a link
commBuildId:
        .byte   $FF                     ; E030  comm build id: $FF = build 1 = the solo trainer (track 35). The game compares it with linkTypeFlag $0BA5 at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D to see whether the right module is resident
nmiChainVector:
        .byte   $10                     ; E031  NMI chain vector low byte, initially $10 -> $E010 (the RTI above); the game patches it to $1298 at $0BF3
nmiChainVectorHi:
        .byte   $E0                     ; E032  NMI chain vector high byte ($E0 here, patched to $12)
ciaIcrShadow:
        .byte   $7F                     ; E033  CIA2 ICR mask shadow; $7F means 'clear every CIA2 interrupt source'. readFilmByte ($5889/$58A8/$58CD) writes this back to $DD0D after banking the film RAM in - the trainer wants no CIA2 NMI at all
L_E034:
        .byte   $00                     ; E034  $E034-$E037: modem-build link variables (command phase, retry state); never touched here
        .byte   $00,$00,$00             ; E035  ...
D_E038:
        .byte   $00                     ; E038  modem-build link variable; unused in the trainer build
nmiSuspendRequest:
        .byte   $00                     ; E039  disk-access suspend request: suspendCommModule stores $C0 here at $0F67, resumeCommModule 0 at $0F78
nmiSuspendAck:
        .byte   $00                     ; E03A  suspend acknowledge; handleCommRequest copies $E039 into it so the wait at $0F6A/$0F7B always succeeds at once
isLinkActive:
        .byte   $00                     ; E03B  link-active flag, $FF after setCommLinkState(X=0); read all over the main program ($0F60, $1331, $1A40, $4E46, ...)
linkStatus:
        .byte   $00                     ; E03C  carrier / link status; kept at $FF so the bit-6 test in pollMenuSession ($56B1) and the bit-7 test in hangUpModemSetState ($1B4B) always pass
linkStatusSample:
        .byte   $00                     ; E03D  $E03D = link-ready sample read at $1BAB; $E03E is a modem-build byte and stays zero
        .byte   $00                     ; E03E  .
D_E03F:
        .byte   $00                     ; E03F  modem-build reply timer; unused here
D_E040:
        .byte   $00                     ; E040  modem-build connection phase; unused here
D_E041:
        .byte   $00                     ; E041  modem-build 'skip init string' flag; unused here
D_E042:
        .byte   $00                     ; E042  modem-build UART restart request; unused here

; ----------------------------------------------------------------------
; handleCommRequest - Body of commRequestEntry. X=1 jumps to buildTrainerReplyPacket, which is where
; the computer opponent actually runs. X=3 forgets the outgoing packet. Every other code (X=0
; per-frame poll, X=2 acknowledge) simply declares the exchange finished by storing $C0 in the
; exchange flags and mirroring the suspend request into the suspend acknowledge.
; In:  X = request code 0..3
; Out: $E01D = $C0 and $E03A = $E039, or $E01F = 0 for X=3, or a whole AI turn for X=1
; Called from: commRequestEntry $E000
; ----------------------------------------------------------------------
handleCommRequest:
        cpx     #$01                    ; E043  request 1 = 'transmit the outgoing packet'?
D_E045:
        bne     L_E04A                  ; E045  no - test the remaining request codes
D_E047:
        jmp     buildTrainerReplyPacket ; E047  yes: instead of transmitting, run one step of the computer opponent

L_E04A:
        cpx     #$03                    ; E04A  request 3 = 'cancel the pending send'?
D_E04C:
        bne     L_E054                  ; E04C  no - fall through to the poll / acknowledge case
D_E04E:
        lda     #$00                    ; E04E  cancel: drop whatever was queued
        sta     txPacketLength          ; E050  outgoing packet length = 0
        rts                             ; E053  done


; ----------------------------------------------------------------------
; Poll (X=0) and acknowledge (X=2) share this tail.  Because the flags are unconditionally set to 'own
; packet sent AND opponent packet received', processCommandExchange ($4DB7) never waits for a peer and
; the suspend/resume handshake around every disk access completes on the first poll.
; ----------------------------------------------------------------------
L_E054:
        lda     #$C0                    ; E054  $C0 = bit 7 'opponent packet received' + bit 6 'exchange complete'
D_E056:
        sta     commInterfaceBlock      ; E056  report the exchange as finished the instant it is asked about
D_E059:
        lda     nmiSuspendRequest       ; E059  take the disk-access suspend request...
        sta     nmiSuspendAck           ; E05C  ...and answer it immediately, so $0F5F/$0F71 never block
D_E05F:
        rts                             ; E05F  done


; ----------------------------------------------------------------------
; setCommLinkState - Body of commSetLinkStateEntry. X=0 stores $FF into the three link flags
; $E03D/$E03C/$E03B, X>=1 stores 0 in all three and also clears the outgoing packet length. Since the
; carrier byte $E03C is $FF whenever the link is 'open', every carrier test in the main program sees a
; healthy connection.
; In:  X = 0 open, >= 1 close
; Out: $E03B, $E03C, $E03D and (on close) $E01F
; Called from: commSetLinkStateEntry $E003
; ----------------------------------------------------------------------
setCommLinkState:
        lda     #$FF                    ; E060  $FF = link up
        cpx     #$01                    ; E062  X=0 means open, anything else means close
        bcc     L_E06B                  ; E064  open - keep $FF
        lda     #$00                    ; E066  close - the three flags become 0
        sta     txPacketLength          ; E068  and the outgoing packet is discarded
L_E06B:
        sta     linkStatusSample        ; E06B  link-ready sample
        sta     linkStatus              ; E06E  carrier status - $FF makes every carrier test in the game succeed
L_E071:
        sta     isLinkActive            ; E071  link-active flag the main program polls everywhere
        rts                             ; E074  done


; ----------------------------------------------------------------------
; reportNoReceivedByte - SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in
; the trainer build.
; In:  none
; Out: C=1
; Called from: commGetReceivedByteEntry $E006 (JMP)
; ----------------------------------------------------------------------
reportNoReceivedByte:
        sec                             ; E075  C=1 = 'no character available'
        rts                             ; E076  the trainer never delivers opponent chat text


; ----------------------------------------------------------------------
; commNoOpClc - CLC + RTS: the shared no-op used as the body of commSendByteEntry, commFrameTickEntry
; and the unpatched keyboard vector. $E078 is the bare RTS on its own.
; In:  none
; Out: C=0
; Called from: commSendByteEntry $E009, commFrameTickEntry $E00C, commKeyboardScanVector $E012 (all
;     JMP)
; ----------------------------------------------------------------------
commNoOpClc:
        clc                             ; E077  C=0 = 'accepted' / 'nothing to report'
commNoOpRts:
        rts                             ; E078  shared bare RTS


; ----------------------------------------------------------------------
; runTrainerRoundHook - Once-per-round hook, called from finishRound ($4654) right after the game
; clock has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded). Unless
; the drone-flight sub-overlay is currently sitting at $EE00 it runs planTrainerRound, the computer
; opponent's 'think' routine.
; In:  $8977 drone timer (bit 7 = drone sub-overlay resident)
; Out: see planTrainerRound ($E170)
; Called from: finishRound $4654
; ----------------------------------------------------------------------
runTrainerRoundHook:
        lda     D_8977                  ; E079  drone timer; bit 7 marks the drone-flight overlay as resident at $EE00
        bmi     L_E081                  ; E07C  the drone-flight AI occupies $EE00-$EFFF, so planTrainerRound must not run (it would call chooseAiDroneTarget at $EEE0)
D_E07E:
        jsr     planTrainerRound        ; E07E  run the once-per-round planner
L_E081:
        rts                             ; E081  back to finishRound


; ----------------------------------------------------------------------
; buildTrainerReplyPacket - Entered with X=1 from sendOutgoingPacket, i.e. once per lock-step
; exchange, immediately after the human's packet was handed over. It clears the received-packet length
; and gives up at once during the setup phase, during film playback, after the game has ended or while
; the game is paused. Otherwise it manages the swappable $EE00-$EFFF quarter of the module: while a
; drone is airborne it loads track 29 sectors 4-5 there (the drone-flight AI) and calls $EE00 every
; exchange; when the drone is gone it reloads track 35 sectors 14-15 (its own missile/target tail). In
; the normal case it ticks the launch countdown down and calls injectAiCommandsIntoPacket, which moves
; whole queued AI commands into the received-packet buffer $E020.
; In:  gamePhase zp_B1, filmPlaybackMode $0B9B, gameEndReason $0BA8, pauseState zp_BE,
;     droneControlFlags $920D, drone timer $8977
; Out: $E01E/$E020.. = the fabricated 'received' packet, $8976 drone target, $8977 timer, and
;     $EE00-$EFFF possibly reloaded from disk
; Called from: handleCommRequest $E047 (JMP, request code 1)
; Guards first: the AI only produces orders while a real battle is running.
; ----------------------------------------------------------------------
buildTrainerReplyPacket:
        lda     #$00                    ; E082  start with an empty incoming packet
D_E084:
        sta     rxPacketLength          ; E084  received packet length = 0
        lda     gamePhase               ; E087  gamePhase is 0 only while the battle proper runs
        bne     L_E0E7                  ; E089  side selection / unit setup / menus - stay silent
        lda     filmPlaybackMode        ; E08B  film playback flag
D_E08E:
        bmi     L_E0E7                  ; E08E  bit 7 = replaying a film; the recorded stream supplies the orders
D_E090:
        lda     gameEndReason           ; E090  non-zero once the game has ended
        bne     L_E0E7                  ; E093  game over - nothing left to order
        lda     pauseState              ; E095  pause / disconnect state
        bne     L_E0E7                  ; E097  paused - freeze the opponent as well

; ----------------------------------------------------------------------
; $EE00-$EFFF is a third overlay layer inside this module.  $8977 bit 7 says the drone-flight AI
; (track 29 sectors 4-5) is resident there; otherwise this module's own missile/target tail (track 35
; sectors 14-15) is.  The two loads below swap between them as the AI's drone takes off and lands.
; ----------------------------------------------------------------------
        lda     D_8977                  ; E099  drone timer: bit 7 = the drone-flight overlay is resident at $EE00
        bpl     L_E0C1                  ; E09C  our own tail is resident - take the normal path
        lda     droneControlFlags       ; E09E  drone control flags
D_E0A1:
        and     #$01                    ; E0A1  bit 0 = a drone-move command is in force, i.e. a drone is airborne
D_E0A3:
        beq     L_E0AB                  ; E0A3  drone gone - swap our own tail back in
L_E0A5:
        jsr     targetEnemyComcenWithMissile; E0A5  still flying: call the drone-flight AI now loaded at $EE00 (track 29 s4-5, NOT the code assembled at $EE00 in this file)
D_E0A8:
        jmp     L_E0E7                  ; E0A8  done for this exchange

L_E0AB:
        jsr     setSubOverlayLoadDest   ; E0AB  point the fast loader at $EE00
        ldy     #$23                    ; E0AE  Y = track $23 = 35, this module's own track
        lda     #$02                    ; E0B0  A = 2 sectors
        ldx     #$0E                    ; E0B2  X = first sector 14: the native $EE00-$EFFF tail
        jsr     loadSectors             ; E0B4  reload it over the drone AI
        lda     #$FF                    ; E0B7  $FF = no drone target chosen
        sta     D_8976                  ; E0B9  let chooseAiDroneTarget pick a fresh one
        lda     #$03                    ; E0BC  3 exchanges of cool-down before the next launch
        sta     D_8977                  ; E0BE  storing 3 also clears bit 7, marking the drone overlay as gone
L_E0C1:
        lda     droneControlFlags       ; E0C1  drone control flags
        and     #$01                    ; E0C4  bit 0 = a drone is airborne
        beq     L_E0DC                  ; E0C6  no drone - just run the normal turn
        lda     #$FF                    ; E0C8  $FF marks the drone overlay resident and blocks planning
        sta     D_8977                  ; E0CA  store it in the drone timer
        jsr     setSubOverlayLoadDest   ; E0CD  point the fast loader at $EE00
        ldy     #$1D                    ; E0D0  Y = track $1D = 29
        ldx     #$04                    ; E0D2  X = first sector 4
        lda     #$02                    ; E0D4  A = 2 sectors: the drone-flight AI
        jsr     loadSectors             ; E0D6  load it over our own tail
        jmp     L_E0A5                  ; E0D9  and fly the drone straight away


; ----------------------------------------------------------------------
; Normal exchange: tick the drone launch countdown, then move as many complete AI commands as fit out
; of the queue at $88A0 into the received-packet buffer $E020.
; ----------------------------------------------------------------------
L_E0DC:
        lda     D_8977                  ; E0DC  launch countdown
        beq     L_E0E4                  ; E0DF  already 0 - launchAiDrone may fire this exchange
        dec     D_8977                  ; E0E1  otherwise tick it down by one exchange
L_E0E4:
        jsr     injectAiCommandsIntoPacket; E0E4  copy queued AI orders into the fabricated received packet
L_E0E7:
        rts                             ; E0E7  return to sendOutgoingPacket


; ----------------------------------------------------------------------
; trainerUnitStepHook - Per-unit hook called at the end of processUnitStep ($4526) for currentUnit,
; i.e. once for every living unit of both sides every round. For one of the AI's own units (index >=
; 50) that has now scored both end zones (bits 3 and 4 of the type byte) it sets bit 7 of that unit's
; 'has penetrated' flag so the planner stops counting it as still at home; for the AI comcen (unit 99)
; it also samples the 'was hit' bit of its display flags into the current and the one-round-sticky
; comcen-hit flags. Everything else is passed to updateAiUnitTracking, the AI's observer.
; In:  currentUnit $90FA, unitTypeTable $F76C, comcen display flags $FBB7, drone timer $8977
; Out: bit 7 of $8864+ordinal, $897B comcen hit this round, $897C comcen hit this or last round;
;     otherwise calls $EACF
; Called from: processUnitStep $4526
; ----------------------------------------------------------------------
trainerUnitStepHook:
        ldy     currentUnit             ; E0E8  the unit the engine has just stepped
        lda     unitTypeTable,y         ; E0EB  type byte: bit 7 = dead, bits 3/4 = end zones already scored, bits 0-2 = type
        bmi     L_E127                  ; E0EE  dead slot - nothing to observe
        cpy     #$32                    ; E0F0  $32 = 50: units 0-49 are the human player's
        bcc     L_E11F                  ; E0F2  human unit - hand it to the observer
        and     #$18                    ; E0F4  keep only the two zone-scored bits
L_E0F6:
        cmp     #$18                    ; E0F6  both the near zone (bit 3, columns 10-19) and the far zone (bit 4, columns 0-9) scored?
        bne     L_E11F                  ; E0F8  not deep in the player's half yet
        lda     D_8832,y                ; E0FA  $8832+unit is $8864+ordinal: this AI unit's 'has penetrated' flag
        ora     #$80                    ; E0FD  mark it penetrated...
        sta     D_8832,y                ; E0FF  ...so computeTrainerGroupStats stops counting it as still at home
        cpy     #$63                    ; E102  $63 = 99 = the AI's own comcen
        bne     L_E11F                  ; E104  any other AI unit - hand it to the observer
        lda     D_897B                  ; E106  this round's 'comcen was hit' flag...
        sta     D_897C                  ; E109  ...becomes the one-round-old sticky flag
        ldx     #$00                    ; E10C  assume 'not hit' this round
        lda     side1ComcenDisplayFlags ; E10E  display flags of unit 99 ($FB54 array)
L_E111:
        and     #$02                    ; E111  bit 1 is set by recordUnitHit ($3C1A) whenever the unit takes damage
        beq     L_E119                  ; E113  not hit
        dex                             ; E115  X = $FF = hit
        stx     D_897C                  ; E116  a fresh hit also re-arms the sticky flag
L_E119:
        stx     D_897B                  ; E119  record whether the comcen was hit this round
L_E11C:
        jmp     L_E127                  ; E11C  done with the comcen

L_E11F:
        lda     D_8977                  ; E11F  drone timer, bit 7 = the drone-flight overlay is resident, i.e. the AI's drone is airborne
        bmi     L_E127                  ; E122  the AI suspends its unit observation for as long as its drone is in the air
        jsr     updateAiUnitTracking    ; E124  track what this unit has seen and what it is shooting at
L_E127:
        rts                             ; E127  back to processUnitStep


; ----------------------------------------------------------------------
; setSubOverlayLoadDest - Points the fast loader at $EE00 (X = destination low byte, Y = high byte)
; and tail-jumps into setLoadDest. Used before each of the two sub-overlay loads in
; buildTrainerReplyPacket.
; In:  none
; Out: fast-loader destination = $EE00
; Called from: $E0AB and $E0CD
; ----------------------------------------------------------------------
setSubOverlayLoadDest:
        ldx     #$00                    ; E128  destination low byte = $00
        ldy     #$EE                    ; E12A  destination high byte = $EE
        jmp     setLoadDest             ; E12C  hand $EE00 to the fast loader's setLoadDest


; ----------------------------------------------------------------------
; pickTrainerRallyPoint - Chooses the AI's current objective: a cell scattered at random around the
; human comcen (unit 49), stored as the rally column/row that every attacking group heads for.
; Refreshed every six rounds, and immediately whenever the human comcen becomes visible.
; In:  side0ComcenCol $F671, side0ComcenRow $F6D5, the game RNG
; Out: $8969 rally column, $896A rally row
; Called from: planTrainerRound $E188; trainerSetupEC00 initTrainerAi $EC47 (once at game start)
; ----------------------------------------------------------------------
pickTrainerRallyPoint:
        lda     side0ComcenCol          ; E12F  column of the human comcen (unit 49)
        jsr     randomiseCoordinate     ; E132  scatter it by a random offset
        sta     D_8969                  ; E135  rally column the AI's groups head for
        lda     side0ComcenRow          ; E138  row of the human comcen
        jsr     randomiseCoordinate     ; E13B  scatter that as well
        sta     D_896A                  ; E13E  rally row
        rts                             ; E141  done


; randomCoordinateOffsetTable - eight signed cell offsets, picked with nextGameRandom AND 7 by
; randomiseCoordinate and then scaled by 1, 2 or 4 according to how well the AI knows where the human
; comcen is.
randomCoordinateOffsetTable:
        .byte   $01                     ; E142  offsets +1,-1,+1,+1,-1,-1,+2 (entries 0-6)
        .byte   $FF,$01,$01,$FF,$FF,$02 ; E143  ......
L_E149:
        .byte   $FE                     ; E149  entry 7: -2 cells

; ----------------------------------------------------------------------
; randomiseCoordinate - Returns A = base coordinate plus a random offset, clamped to 0..39. The base
; is written into the ADC immediate at $E164 and the offset comes from randomCoordinateOffsetTable
; indexed by nextGameRandom AND 7. The offset is used raw while the human comcen is actually visible
; (bit 7 of its energy byte), doubled once it has ever been seen ($896C bit 7) and quadrupled
; otherwise - so the AI aims accurately at a comcen it can see and wanders when it cannot. $E14D is an
; alternate entry that reuses the base already patched in.
; In:  A = base coordinate 0-39, side0ComcenEnergy $F9F5, $896C 'comcen has been seen', the game RNG
; Out: A = coordinate 0..39; self-modifies the operand byte at $E164 (L_E163+1)
; Called from: pickTrainerRallyPoint $E132 and $E13B
; ----------------------------------------------------------------------
randomiseCoordinate:
        sta     L_E163+1                ; E14A  patch the base coordinate into the ADC immediate at $E164
randomiseCoordinateSameBase:
        jsr     nextGameRandom          ; E14D  alternate entry that reuses the base already patched in (no caller in this build)
        and     #$07                    ; E150  keep 3 bits: offset table index 0-7
        tay                             ; E152  use it as the table index
        lda     randomCoordinateOffsetTable,y; E153  signed cell offset -2..+2
        bit     side0ComcenEnergy       ; E156  energy byte of the human comcen; bit 7 = 'currently spotted'
        bmi     L_E162                  ; E159  visible right now - aim with the raw offset
        asl     a                       ; E15B  not visible: double the scatter
        bit     D_896C                  ; E15C  sticky 'the comcen has been seen at some point' flag
        bmi     L_E162                  ; E15F  seen before - x2 scatter is enough
        asl     a                       ; E161  never seen at all - x4, so the AI just roams
L_E162:
        clc                             ; E162  prepare the add
L_E163:
        adc     #$FF                    ; E163  operand byte patched at $E14A with the base coordinate
        bpl     L_E169                  ; E165  result still positive - keep it
        lda     #$00                    ; E167  clamp to the left/top edge of the map
L_E169:
        cmp     #$28                    ; E169  $28 = 40 columns/rows on the battlefield
L_E16B:
        bcc     L_E16F                  ; E16B  inside the map
        lda     #$27                    ; E16D  clamp to the last cell, 39
L_E16F:
        rts                             ; E16F  A = coordinate 0-39


; ----------------------------------------------------------------------
; planTrainerRound - Top-level planner of the solo trainer, run once per game round. It resets the
; per-round order counter, refreshes the rally point, works out this round's action budget from the AI
; comcen's energy and stun state, and then either spends the round on comcen repair (budget below 20)
; or evaluates the threat around its own comcen and decides between attack mode ($8963 = 0, plus a
; drone decision) and defend mode ($8963 = $FF). It then re-plans every AI unit whose replan clock has
; come due and finishes by walking the comcen forward behind the assault group or retreating it.
; In:  gameEndReason $0BA8, comcen records $F9F5/$FA27/$F9C3, gameTypeOptions $0BA3, gameClock $91CB,
;     the RNG and the whole $8800-$897C AI work block
; Out: $8965 budget, $8968 order count, $896B rally timer, $896C comcen-seen flag, $8960-$8963 threat
;     result, commands appended to the queue at $88A0
; Called from: runTrainerRoundHook $E07E
; ----------------------------------------------------------------------
planTrainerRound:
        lda     gameEndReason           ; E170  non-zero once the game has ended
        beq     L_E176                  ; E173  still playing
        rts                             ; E175  game over - no plan

L_E176:
        sta     D_8968                  ; E176  A is 0 here: clear the count of commands queued this round

; ----------------------------------------------------------------------
; The rally point (a cell near the human comcen) is refreshed every six rounds, or at once on the
; round the human comcen becomes visible.
; ----------------------------------------------------------------------
        lda     side0ComcenEnergy       ; E179  human comcen energy byte, bit 7 = currently spotted
        bpl     L_E183                  ; E17C  not spotted this round
        sta     D_896C                  ; E17E  spotted: bit 7 sticks in $896C, so randomiseCoordinate scatters less from now on...
        bmi     L_E188                  ; E181  ...and the rally point is refreshed immediately
L_E183:
        dec     D_896B                  ; E183  otherwise count the rally-point refresh timer down
        bpl     L_E190                  ; E186  not due yet
L_E188:
        jsr     pickTrainerRallyPoint   ; E188  pick a new random cell near the human comcen
        lda     #$05                    ; E18B  5 = next refresh in six rounds
        sta     D_896B                  ; E18D  reload the timer

; ----------------------------------------------------------------------
; This round's action budget.  Normally it is the AI comcen's own energy (0-50); with the COMCEN
; DAMAGE option on ($0BA3 bit 4) the comcen's stun counter modifies it.  A budget under 20 means the
; AI can only afford to repair and refuel its comcen.
; ----------------------------------------------------------------------
L_E190:
        lda     side1ComcenEnergy       ; E190  AI comcen energy (unit 99)
        and     #$3F                    ; E193  bits 0-5 hold the 0-50 energy points
        sta     D_8965                  ; E195  that is this round's action budget
        lda     side1ComcenStun         ; E198  AI comcen stun byte (unit 99 of the $F960 array)
        and     #$0F                    ; E19B  low nibble = the STUN'D counter 0-15 shown on the console
        sta     D_8971                  ; E19D  remember how badly the comcen is stunned
        lda     gameTypeOptions         ; E1A0  game options byte
        and     #$10                    ; E1A3  bit 4 = the COMCEN DAMAGE option
        beq     L_E1C1                  ; E1A5  damage rules off - use a flat budget
        lda     D_8971                  ; E1A7  stun counter
        beq     L_E1C6                  ; E1AA  not stunned - keep the energy budget
        cmp     #$0F                    ; E1AC  $0F = maximum stun
D_E1AE:
        beq     L_E1D0                  ; E1AE  completely out - the AI does nothing at all this round
        cmp     #$08                    ; E1B0  stun 8-14
        bcs     L_E1C1                  ; E1B2  heavy stun (8-14) takes the flat 50 budget while a light stun (1-7) is penalised 16 points below - counter-intuitive, possibly a slip for a branch to $E1CD
        lda     D_8965                  ; E1B4  stun 1-7: take the energy budget...
        sec                             ; E1B7  prepare the subtraction
        sbc     #$10                    ; E1B8  ...and charge a 16 point penalty
        bcc     L_E1CD                  ; E1BA  budget went negative - only repair the comcen
        sta     D_8965                  ; E1BC  store the reduced budget
        bcs     L_E1C6                  ; E1BF  always taken (the SBC above left C=1)
L_E1C1:
        lda     #$32                    ; E1C1  $32 = 50 = a full budget
        sta     D_8965                  ; E1C3  store it
L_E1C6:
        lda     D_8965                  ; E1C6  this round's budget
        cmp     #$14                    ; E1C9  $14 = 20 points
L_E1CB:
        bcs     L_E1D1                  ; E1CB  enough left to manoeuvre
L_E1CD:
        jsr     runAiComcenActions      ; E1CD  too little - spend the round on comcen repair and energy
L_E1D0:
        rts                             ; E1D0  done


; ----------------------------------------------------------------------
; Threat assessment around the AI's own comcen.  assessThreatAroundAiComcen buckets the human units by
; compass direction and returns the overall threat, the deepest weapon-range penetration and the
; safest direction; those three become $8962, $8961 and $8960.  The AI defends when an enemy weapon
; actually covers the comcen and it has either no assault group to rely on or has just been hit.
; ----------------------------------------------------------------------
L_E1D1:
        lda     side1ComcenCol          ; E1D1  AI comcen column
        sta     cellColB                ; E1D4  reference point B of the threat scan
        lda     side1ComcenRow          ; E1D6  AI comcen row
        sta     cellRowB                ; E1D9  reference point B of the threat scan
        jsr     assessThreatAroundAiComcen; E1DB  bucket the human units around it into 8 compass directions
        sta     D_8962                  ; E1DE  A = overall threat score
        sty     D_8960                  ; E1E1  Y = safest direction 0-7 (8 = nothing anywhere)
        stx     D_8961                  ; E1E4  X = deepest weapon-range penetration, 0 = no enemy weapon covers the comcen
        ldy     #$00                    ; E1E7  assume attack mode (0)
L_E1E9:
        cpx     #$00                    ; E1E9  is any enemy weapon covering the comcen?
        beq     L_E1F9                  ; E1EB  no threat at all - attack
        dey                             ; E1ED  Y = $FF = defend
        bit     D_895D                  ; E1EE  is an assault group selected? bit 7 = none
        bmi     L_E1F9                  ; E1F1  no group to rely on - defend
        bit     D_897C                  ; E1F3  was the comcen hit this round or last?
        bmi     L_E1F9                  ; E1F6  yes - defend
        iny                             ; E1F8  threatened but coping - attack after all
L_E1F9:
        sty     D_8963                  ; E1F9  0 = attack, $FF = defend
        tya                             ; E1FC  A = the mode just chosen
        bne     L_E202                  ; E1FD  defending - do not spend a drone
        jsr     chooseAiDroneTarget     ; E1FF  attacking - decide what the next drone should be sent at
L_E202:
        jsr     scanTrainerUnitsForOrders; E202  issue orders to every AI unit whose replan clock has come due
        lda     D_8968                  ; E205  commands queued so far this round
        cmp     #$05                    ; E208  5 is the per-round ceiling
L_E20A:
        bcs     L_E21E                  ; E20A  budget spent - stop here
        jsr     updateFocusGroupCentre  ; E20C  refresh the assault group's id and centre
        ldx     D_8963                  ; E20F  attack (0) or defend ($FF)?
        bmi     retreatTrainerComcen    ; E212  defend - pull the comcen back and recall the group
        lda     D_8965                  ; E214  action budget
        cmp     #$20                    ; E217  $20 = 32 points
        bcs     pushTrainerComcenForward; E219  plenty of energy - walk the comcen up behind the attack
        jsr     runAiComcenActions      ; E21B  low budget - repair the comcen instead
L_E21E:
        rts                             ; E21E  done


; ----------------------------------------------------------------------
; pushTrainerComcenForward - Attack-mode tail of planTrainerRound (a branch target, not a JSR entry).
; If an assault group exists, the comcen is not stunned and its own replan clock has come due, it
; queues a MOVE for the AI comcen to a cell one step beyond the enemy's reach from the assault group's
; centre, and re-arms the comcen's replan clock to gameClock - 3. This is how the trainer walks its
; command centre up behind its attack.
; In:  $895D assault group id, $8971 comcen stun, $8831 comcen replan clock, gameClock $91CB, $8960
;     safest direction, $8961 enemy reach, $895E/$895F assault group centre
; Out: $8831 re-armed, one $80/$81 MOVE queued through orderAiUnitToCell
; Called from: planTrainerRound $E219 (branch)
; ----------------------------------------------------------------------
pushTrainerComcenForward:
        ldy     D_895D                  ; E21F  assault group id, bit 7 = none selected
L_E222:
        bmi     L_E265                  ; E222  no group to follow
        lda     D_8971                  ; E224  comcen stun counter
        bne     L_E265                  ; E227  stunned - it cannot move
        lda     D_8831                  ; E229  the comcen's own replan clock
        cmp     gameClock               ; E22C  the clock counts down, so 'stamp >= clock' means the round has arrived
        bcc     L_E265                  ; E22F  not due yet
        lda     gameClock               ; E231  current round
        sec                             ; E234  prepare the subtraction
        sbc     #$03                    ; E235  next comcen replan in 3 rounds
L_E237:
        bcs     L_E23B                  ; E237  no underflow
        lda     #$00                    ; E239  clamp at round 0
L_E23B:
        sta     D_8831                  ; E23B  re-arm the comcen replan clock
        ldx     D_8960                  ; E23E  safest direction from the threat scan
        lda     dirToColDelta,x         ; E241  column step -1/0/+1 for that direction
        tax                             ; E244  X = column step for offsetCoordinateClamped
        ldy     D_8961                  ; E245  how deep the enemy weapons reach
        iny                             ; E248  one cell beyond that
        lda     msgOpponentUsingNonStandardGame; E249  $895E = assault group centre column (the label is a stale message-string name)
        jsr     offsetCoordinateClamped ; E24C  column = centre + step * distance, clamped to 0..39
        sta     scratch18               ; E24F  stash the target column
        ldx     D_8960                  ; E251  same direction...
        lda     dirToRowDelta,x         ; E254  ...row step -1/0/+1
        tax                             ; E257  X = row step
L_E258:
        lda     D_895F                  ; E258  assault group centre row
        jsr     offsetCoordinateClamped ; E25B  row = centre + step * distance, clamped
        ldx     scratch18               ; E25E  X = column, A = row
L_E260:
        ldy     #$31                    ; E260  $31 = AI ordinal 49 = unit 99, the AI comcen
        jsr     orderAiUnitToCell       ; E262  queue the MOVE
L_E265:
        rts                             ; E265  done


; ----------------------------------------------------------------------
; retreatTrainerComcen - Defend-mode tail of planTrainerRound (a branch target). It stamps the commit
; clock $896E with the current round - from then on groups are allowed to use their full combat value
; - and, unless the comcen is stunned, queues a MOVE that walks it away along the safest direction by
; as many cells as the enemy reaches. If the threat score still exceeds the recall threshold $897A, or
; the comcen was stunned, it recomputes the assault group's statistics and jumps to
; aimGroupAtOwnComcen to pull the whole group home.
; In:  $8971 comcen stun, $8960 safest direction, $8961 enemy reach, $8962 threat score, $897A recall
;     threshold, $895D assault group, comcen position $F6A3/$F707
; Out: $896E commit clock, $895A cleared, one comcen MOVE and possibly a full group recall
; Called from: planTrainerRound $E212 (branch)
; ----------------------------------------------------------------------
retreatTrainerComcen:
        lda     gameClock               ; E266  current round
        sta     D_896E                  ; E269  stamp the commit clock: groups may now use their full strength
        lda     D_8971                  ; E26C  comcen stun counter
        bne     L_E29F                  ; E26F  stunned - it cannot run, go straight to recalling the group
        ldx     D_8960                  ; E271  safest direction
        lda     dirToColDelta,x         ; E274  its column step
        tax                             ; E277  X = column step
        ldy     D_8961                  ; E278  run as far as the enemy reaches
        lda     side1ComcenCol          ; E27B  AI comcen column
        jsr     offsetCoordinateClamped ; E27E  clamped destination column
        sta     scratch18               ; E281  stash it
L_E283:
        ldx     D_8960                  ; E283  safest direction again
        lda     dirToRowDelta,x         ; E286  its row step
        tax                             ; E289  X = row step
        lda     side1ComcenRow          ; E28A  AI comcen row
sub_E28D:
        jsr     offsetCoordinateClamped ; E28D  clamped destination row
        ldx     scratch18               ; E290  X = column
        ldy     #$31                    ; E292  $31 = AI ordinal 49 = unit 99
        jsr     orderAiUnitToCell       ; E294  queue the comcen MOVE
        lda     D_8962                  ; E297  threat score from the scan
L_E29A:
        cmp     D_897A                  ; E29A  recall threshold, set to 5 by the setup overlay at $EC44
        bcc     L_E2B6                  ; E29D  manageable - leave the assault group where it is
L_E29F:
        lda     D_895D                  ; E29F  assault group id
        cmp     #$FF                    ; E2A2  $FF = no group at all
        beq     L_E2B6                  ; E2A4  nothing to recall
        and     #$0F                    ; E2A6  strip the disable bit, leaving the group index 0-11
        tay                             ; E2A8  Y = the group index for computeTrainerGroupStats
        jsr     computeTrainerGroupStats; E2A9  recompute its members, strength, centre and box
        bcs     L_E2B6                  ; E2AC  empty group - nothing to recall
        lda     #$00                    ; E2AE  pretend the group has no slow units...
        sta     D_895A                  ; E2B0  ...so issueTrainerGroupOrders does not double the replan delay - this is an urgent recall
        jmp     aimGroupAtOwnComcen     ; E2B3  march the whole group home to the comcen

L_E2B6:
        rts                             ; E2B6  done


; unitCombatValueTable - combat weight per unit type, indexed by the type bits 0-2 of $F76C.  Summed
; into the 16-bit group value by computeTrainerGroupStats and also copied into the per-unit influence
; weight page $0464 by buildUnitThreatTables ($E7AF).
unitCombatValueTable:
        .byte   $02                     ; E2B7  GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0
        .byte   $02,$03,$00,$00         ; E2B8  ....

; trainerUnitScanIndex - the loop counter of scanTrainerUnitsForOrders, kept as a byte inside the code
; stream rather than in the $8800 work block.  Written at $E2BF and $E2EB, read at $E2CE.
trainerUnitScanIndex:
        .byte   $00                     ; E2BC  current AI ordinal being scanned, 49 down to 0

; ----------------------------------------------------------------------
; scanTrainerUnitsForOrders - Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down
; to 0, stopping as soon as four commands have been queued this round. A dead slot has its replan
; clock zeroed and is removed from every group; a slot whose replan clock has been reached by the
; down-counting game clock is handed to decideTrainerGroupAction, which re-plans the whole group that
; unit belongs to. The default replan urgency is reloaded with 5 before every unit.
; In:  $8968 orders this round, gameClock $91CB, side1UnitTypeTable $F79E, replan clocks $8800, group
;     ids $8832
; Out: $E2BC, $8800 and $8832 entries, $896D urgency; orders queued by the callee
; Called from: planTrainerRound $E202
; ----------------------------------------------------------------------
scanTrainerUnitsForOrders:
        lda     #$31                    ; E2BD  $31 = 49, the highest AI ordinal (unit 99)
        sta     trainerUnitScanIndex    ; E2BF  start the walk at the comcen and count down

; ----------------------------------------------------------------------
; Loop over AI ordinals 49..0.  It maintains the embedded counter at $E2BC and stops early once four
; commands have been queued.
; ----------------------------------------------------------------------
L_E2C2:
        lda     D_8968                  ; E2C2  commands queued so far this round
        cmp     #$04                    ; E2C5  4 is the ceiling for unit orders
        bcs     L_E2F0                  ; E2C7  budget spent
        lda     #$05                    ; E2C9  default replan urgency: re-plan again in at most 5 rounds
        sta     D_896D                  ; E2CB  reload the default urgency before every unit
        ldy     trainerUnitScanIndex    ; E2CE  current AI ordinal 49..0
        lda     side1UnitTypeTable,y    ; E2D1  type byte of unit ordinal+50
        bpl     L_E2E0                  ; E2D4  still alive
        lda     #$00                    ; E2D6  dead: clear its replan clock...
        sta     msgGetOpponentOnPhoneAnd,y; E2D8  ...at $8800+ordinal
        lda     #$FF                    ; E2DB  ...and take it out of every group
        sta     D_8832,y                ; E2DD  $FF = belongs to no group
L_E2E0:
        lda     msgGetOpponentOnPhoneAnd,y; E2E0  this unit's replan clock
        cmp     gameClock               ; E2E3  the clock counts down; stamp >= clock means the planned round has come
L_E2E6:
        bcc     L_E2EB                  ; E2E6  not due yet
        jsr     decideTrainerGroupAction; E2E8  re-plan the whole group this unit belongs to
L_E2EB:
        dec     trainerUnitScanIndex    ; E2EB  next ordinal down
        bpl     L_E2C2                  ; E2EE  loop until ordinal 0 has been done
L_E2F0:
        rts                             ; E2F0  done


; ----------------------------------------------------------------------
; decideTrainerGroupAction - Decides what the AI group owning unit ordinal Y should do. Group 0 is the
; comcen's own escort and is only re-planned while the comcen is unstunned and no assault group is
; selected, in which case group 0 simply inherits the comcen's own ordered destination. For any group
; it caches the group flags, trims them to the low nibble and calls computeTrainerGroupStats; an empty
; group aborts. It then picks one of four behaviours: march home to the AI comcen (the home-defence
; group, or any group during a recall), charge the rally point when the group has already penetrated
; and made contact, form a defensive line on row 29 when it has not, or - for group 0 - run a free
; direction search out from the comcen.
; In:  Y = AI unit ordinal, $8832 group ids, $8971 comcen stun, $895D assault group, comcen
;     destination $F833/$F897, $8964 home group, $8966 flag cache, zp_1D members still at home
; Out: $88D2/$88DE group target, $893E group flags, $896D urgency, $8955-$8963 group scratch; ends in
;     one of the order-issuing tails
; Called from: scanTrainerUnitsForOrders $E2E8
; ----------------------------------------------------------------------
decideTrainerGroupAction:
        lda     D_8832,y                ; E2F1  group id of this AI unit
        bne     L_E31A                  ; E2F4  groups 1..11 are ordinary objectives

; ----------------------------------------------------------------------
; Group 0 is the comcen's own escort.  It is only worth re-planning while the comcen can still move
; and no assault group has been picked; then it just adopts the comcen's own destination as its
; target.
; ----------------------------------------------------------------------
        lda     D_8971                  ; E2F6  group 0: is the comcen stunned?
        bne     L_E319                  ; E2F9  stunned - leave the escort alone
        ldx     #$03                    ; E2FB  urgency 3 for the comcen's escort
        stx     D_896D                  ; E2FD  urgency for group 0
        ldx     D_895D                  ; E300  is an assault group already selected?
        bpl     L_E319                  ; E303  yes - group 0 is not the one to re-plan
        lda     side1ComcenDestCol      ; E305  AI comcen ordered destination column
        and     #$3F                    ; E308  strip the flag bits, keep the 0-39 cell
        sta     D_88D2                  ; E30A  group 0 target column = wherever the comcen is heading
        lda     side1ComcenDestRow      ; E30D  AI comcen ordered destination row
        and     #$3F                    ; E310  strip the flag bits, keep the 0-39 cell
        sta     D_88DE                  ; E312  group 0 target row
        lda     #$00                    ; E315  A = group id 0
        beq     L_E31A                  ; E317  always taken
L_E319:
        rts                             ; E319  nothing to do for this group

L_E31A:
        tay                             ; E31A  Y = group id
L_E31B:
        lda     D_893E,y                ; E31B  flags of that group
        sta     D_8966                  ; E31E  keep a copy for the tests below (bit 6 = has made contact with the player's comcen)
        and     #$0F                    ; E321  clear bits 4-7, leaving the start-delay nibble
        sta     D_893E,y                ; E323  write the trimmed flags back
        jsr     computeTrainerGroupStats; E326  members, strength, centre and bounding box of the group
        bcc     L_E32C                  ; E329  the group has members
        rts                             ; E32B  empty group - nothing to order

L_E32C:
        ldy     D_8955                  ; E32C  group id again
        cpy     D_8964                  ; E32F  the home-defence group chosen at setup ($EC39/$ECA3)
L_E332:
        bne     L_E356                  ; E332  not the home group - decide between charging and digging in
        lda     #$02                    ; E334  urgency 2 - re-plan the home guard often
        sta     D_896D                  ; E336  urgency for the home guard

; ----------------------------------------------------------------------
; aimGroupAtOwnComcen - Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from
; $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343
; that copies the group centre into the search origin and the line target into the wanted destination,
; and jumps to issueTrainerGroupOrders. Used to recall a group to defend the comcen.
; In:  comcen position $F6A3/$F707, group centre zp_93/zp_92 (left there by computeTrainerGroupStats)
; Out: zp_27/zp_28 = search origin, zp_22/zp_24 = wanted group destination; tail-jumps to $E49E
; Called from: $E2B3 (JMP) and by falling through from decideTrainerGroupAction $E336
; ----------------------------------------------------------------------
aimGroupAtOwnComcen:
        lda     side1ComcenCol          ; E339  AI comcen column
        sta     lineEndCol              ; E33C  line target column
        lda     side1ComcenRow          ; E33E  AI comcen row
        sta     lineEndRow              ; E341  line target row

; ----------------------------------------------------------------------
; sendGroupStraightToTarget - shared tail (also reached from $E3AF and $E3CD): move the group in a
; straight line to (lineEndCol, lineEndRow) with no threat search at all.
; ----------------------------------------------------------------------
sendGroupStraightToTarget:
        lda     lineCurCol              ; E343  group centre column (set by computeTrainerGroupStats)
        sta     cellColB                ; E345  search origin / reference column
        lda     lineCurRow              ; E347  group centre row
        sta     cellRowB                ; E349  search origin / reference row
        lda     lineEndCol              ; E34B  line target column
        sta     cellColA                ; E34D  wanted group destination column
        lda     lineEndRow              ; E34F  line target row
        sta     cellRowA                ; E351  wanted group destination row
sub_E353:
        jmp     issueTrainerGroupOrders ; E353  issue the move orders for the whole group


; ----------------------------------------------------------------------
; Not the home guard.  A group that still has members which have not scored both end zones, and which
; has not yet made contact with the player's comcen (flags bit 6), falls back to a defensive line;
; anything else pushes on towards the rally point.
; ----------------------------------------------------------------------
L_E356:
        bit     D_8966                  ; E356  cached group flags
        bvs     L_E35F                  ; E359  bit 6 set: the group has already made contact with the player's comcen
        lda     scratch1D               ; E35B  count of members that have not scored both end zones
        bne     aimGroupAtHomeLine      ; E35D  some are still at home - form a defensive line instead
L_E35F:
        ldy     D_8955                  ; E35F  group id
L_E362:
        bne     L_E37A                  ; E362  groups 1..11 head for the rally point
        lda     D_8963                  ; E364  group 0 (the comcen escort): attack or defend?
        bmi     L_E379                  ; E367  defending - the escort stays where it is
        lda     D_8960                  ; E369  safest direction around the comcen
        sta     scratch19               ; E36C  use it as the fan heading
        jsr     buildDirectionFan       ; E36E  lay out the 8 candidate directions in $8896-$889D
        lda     #$05                    ; E371  try to advance 5 cells
        sta     D_895B                  ; E373  wanted advance distance for the search
        jmp     findTrainerAdvanceCell  ; E376  run the threat-aware path search

L_E379:
        rts                             ; E379  nothing to do


; ----------------------------------------------------------------------
; Groups 1..11: head for the rally point.  How boldly depends on whether the human comcen has been
; found, how much of the game is left and whether this group has already made contact with it.
; ----------------------------------------------------------------------
L_E37A:
        lda     D_8969                  ; E37A  rally column near the human comcen
        sta     lineEndCol              ; E37D  line target column
        lda     D_896A                  ; E37F  rally row
        sta     lineEndRow              ; E382  line target row
L_E384:
        lda     side0ComcenEnergy       ; E384  human comcen energy byte, bit 7 = spotted
        bmi     L_E392                  ; E387  spotted
        lda     gameClock               ; E389  rounds left in the game
        cmp     #$28                    ; E38C  $28 = 40 rounds
        bcs     searchGroupPathToTarget ; E38E  plenty of time left - just search for a safe advance
        bcc     L_E39F                  ; E390  late in the game - be twice as bold
L_E392:
        bit     D_8966                  ; E392  cached group flags
        bvc     searchGroupPathToTarget ; E395  no contact with the comcen yet - just search
        lda     D_893E,y                ; E397  group flags
        ora     #$80                    ; E39A  bit 7 = this group has the spotted comcen in its sights
        sta     D_893E,y                ; E39C  store the flags back
L_E39F:
        asl     D_8956                  ; E39F  double the group's combat value, low byte...
D_E3A2:
        rol     D_8957                  ; E3A2  ...and high byte, so the risk test accepts twice as much opposition
        bit     D_8966                  ; E3A5  cached group flags
        bvc     searchGroupPathToTarget ; E3A8  no contact - fall back to the cautious search
        lda     D_8976                  ; E3AA  the unit the next drone is aimed at
        cmp     #$31                    ; E3AD  $31 = 49 = the human comcen
        beq     sendGroupStraightToTarget; E3AF  the drone is already going for the comcen - the group beelines to the rally point too
        jsr     isPathToPlayerComcenClear; E3B1  is the straight line to the human comcen free of living enemy combat units?
        bcc     searchGroupPathToTarget ; E3B4  blocked - fall back to the cautious search
sub_E3B6:
        ldy     D_8955                  ; E3B6  group id
sub_E3B9:
        lda     D_8902,y                ; E3B9  number of members
sub_E3BC:
        cmp     #$04                    ; E3BC  4 or more
        bcc     L_E3C8                  ; E3BE  too small to commit
        lda     D_893E,y                ; E3C0  group flags
        ora     #$20                    ; E3C3  bit 5 = committed to the all-out attack (chooseAiDroneTarget weighs this doubled)
        sta     D_893E,y                ; E3C5  store the committed flag back
L_E3C8:
        lda     #$03                    ; E3C8  urgency 3
        sta     D_896D                  ; E3CA  re-plan this group again in 3 rounds at the latest
        jmp     sendGroupStraightToTarget; E3CD  charge straight at the rally point


; ----------------------------------------------------------------------
; aimGroupAtHomeLine - Internal branch target used when the group still has members that have not
; reached the player's end zones and has no contact with his comcen: the target column stays where the
; group already is and the target row becomes 29 plus the group's own vertical extents, i.e. a
; defensive line inside the AI's own half. While the commit clock $896E has not been passed the
; group's combat value is also capped at 9, which makes the risk test reject anything but a very quiet
; cell.
; In:  group centre zp_93, group spans $891A/$8932, commit clock $896E, gameClock $91CB, group value
;     $8956/$8957
; Out: zp_94/zp_95 target cell, $8956/$8957 possibly capped at 9; falls into the path search at $E400
; Called from: decideTrainerGroupAction $E35D (branch)
; ----------------------------------------------------------------------
aimGroupAtHomeLine:
        lda     lineCurCol              ; E3D0  group centre column
        sta     lineEndCol              ; E3D2  hold the column, only the row changes
        lda     #$1D                    ; E3D4  $1D = row 29
        ldy     D_8955                  ; E3D6  group id
        clc                             ; E3D9  prepare the two adds
        adc     D_891A,y                ; E3DA  plus the group's reach above its centre
        adc     D_8932,y                ; E3DD  plus its reach below, so the whole formation box sits inside the AI's own half
        sta     lineEndRow              ; E3E0  defensive line row
        lda     D_896E                  ; E3E2  commit clock, set at setup ($ED85) and re-stamped by every comcen retreat
        cmp     gameClock               ; E3E5  the clock counts down: stamp >= clock means the commit round has passed
        bcs     searchGroupPathToTarget ; E3E8  committed - let the group use its real strength
L_E3EA:
        lda     D_8957                  ; E3EA  group combat value, high byte
        bne     L_E3F6                  ; E3ED  over 255 - definitely cap it
        lda     D_8956                  ; E3EF  low byte
L_E3F2:
        cmp     #$09                    ; E3F2  9 is the cautious ceiling
        bcc     searchGroupPathToTarget ; E3F4  already weak enough
L_E3F6:
        lda     #$00                    ; E3F6  cap the value at 9 so the risk test only accepts very quiet cells
        sta     D_8957                  ; E3F8  high byte of the capped value = 0
        lda     #$09                    ; E3FB  9 = the cautious ceiling
        sta     D_8956                  ; E3FD  low byte of the capped value

; ----------------------------------------------------------------------
; searchGroupPathToTarget - shared entry (branch target from $E38E, $E395, $E3A8, $E3B4, $E3E8 and
; $E3F4): measure how far the target is, build the eight-way candidate fan around the heading towards
; it and run the threat-aware search.  When the group already stands on its target there is no
; heading, so the search is skipped and the members are merely re-stamped with a two-round replan
; delay.
; ----------------------------------------------------------------------
searchGroupPathToTarget:
        lda     lineCurCol              ; E400  group centre column
        sta     cellColB                ; E402  search origin column
        lda     lineCurRow              ; E404  group centre row
        sta     cellRowB                ; E406  search origin row
        lda     lineEndCol              ; E408  target column
        sta     cellColA                ; E40A  point A of the distance measure
        lda     lineEndRow              ; E40C  target row
        sta     cellRowA                ; E40E  point A row
        jsr     cellDistance            ; E410  distance from the group to its target, 0-16 cells
        sta     D_895B                  ; E413  that is how far the group would like to advance
D_E416:
        jsr     buildDirectionFanTowardTarget; E416  heading towards the target plus the 8 candidate directions in $8896-$889D
        lda     scratch19               ; E419  heading 0-7, or $FF when the group already stands on the target
        bpl     findTrainerAdvanceCell  ; E41B  valid heading - search for a safe cell along it
        lda     #$00                    ; E41D  already there: zero movement delta
        sta     scratch2A               ; E41F  no column shift
        sta     scratch2B               ; E421  no row shift
        lda     #$02                    ; E423  replan again in 2 rounds
        sta     scanListIndex           ; E425  zp_29 = replan delay in rounds
        jmp     L_E4C7                  ; E427  skip the search and just re-stamp the members


; ----------------------------------------------------------------------
; findTrainerAdvanceCell - Threat-aware path search. It first builds the per-unit influence map
; (radius in $0400, weight in $0464) with buildUnitThreatTables. Then, for each of the eight direction
; ranks in the fan at $8896, it steps outward from the search origin in jumps of two cells, testing
; every candidate with isGroupDestinationTooRisky; it keeps extending while the cell is acceptable, up
; to the wanted advance distance $895B or the per-rank cap in maxAdvanceByRankTable, and moves on to
; the next rank as soon as a candidate is rejected before either limit is reached. If no rank works
; the group stays put (direction 0, distance 0). The winning direction and distance are finally turned
; into the absolute target cell zp_22/zp_24 and the code falls into issueTrainerGroupOrders.
; In:  zp_27/zp_28 search origin, $8896-$889D direction fan, $895B wanted distance, $8956/$8957 group
;     strength
; Out: zp_22/zp_24 = chosen target cell, zp_23 = distance, zp_26 = direction, $0400/$0464 influence
;     map; falls into $E49E
; Called from: decideTrainerGroupAction $E376 (JMP) and $E41B (branch)
; ----------------------------------------------------------------------
findTrainerAdvanceCell:
        jsr     buildUnitThreatTables   ; E42A  build the per-unit influence map in the $0400/$0464 scratch pages
D_E42D:
        lda     #$00                    ; E42D  start with direction rank 0, the best one in the fan
D_E42F:
        sta     cellColA                ; E42F  zp_22 doubles as the rank index for the length of the search

; ----------------------------------------------------------------------
; Outer loop over the eight direction ranks; inner loop at $E435 pushes two cells further out at a
; time along the current rank while the destination stays acceptable.
; ----------------------------------------------------------------------
L_E431:
        lda     #$00                    ; E431  distance reached along this rank = 0
        sta     scratch23               ; E433  zp_23 = distance reached along this rank
L_E435:
        ldy     cellColA                ; E435  current rank
        ldx     D_8896,y                ; E437  the direction the fan put in that rank
        stx     scanRowCount            ; E43A  keep it as the candidate heading
        lda     scratch23               ; E43C  distance reached so far
        clc                             ; E43E  prepare the add
        adc     #$02                    ; E43F  probe two cells further out
        sta     scratch2A               ; E441  candidate distance
        tay                             ; E443  Y = distance for the risk test
D_E444:
        jsr     isGroupDestinationTooRisky; E444  too much enemy influence on the formation box at that spot?
D_E447:
        bcs     L_E464                  ; E447  rejected - stop extending this rank
        ldx     scanRowCount            ; E449  candidate heading
        ldy     scratch2A               ; E44B  candidate distance
        jsr     offsetPointInDirection  ; E44D  does the cell itself stay on the 40x40 map?
        bcs     L_E464                  ; E450  off the map - stop extending
        lda     scratch2A               ; E452  accept the new distance
        sta     scratch23               ; E454  remember it as the distance reached on this rank
        cmp     D_895B                  ; E456  as far as the group wanted to go?
D_E459:
        bcs     L_E480                  ; E459  yes - take it
D_E45B:
        ldy     cellColA                ; E45B  rank
        cmp     maxAdvanceByRankTable,y ; E45D  per-rank distance cap: 5,5,5,3,3,1,1,1
        bcs     L_E480                  ; E460  cap reached - take it
        bcc     L_E435                  ; E462  keep pushing outward along this rank
L_E464:
        lda     scratch23               ; E464  distance already accepted on this rank
        cmp     D_895B                  ; E466  far enough anyway?
        bcs     L_E480                  ; E469  yes - take it
        ldy     cellColA                ; E46B  rank
        cmp     maxAdvanceByRankTable,y ; E46D  per-rank cap
        bcs     L_E480                  ; E470  at the cap - take it
        inc     cellColA                ; E472  try the next-best direction
        lda     cellColA                ; E474  the new rank index
        cmp     #$08                    ; E476  the fan holds 8 candidate directions
        bcc     L_E431                  ; E478  still ranks left to try
        lda     #$00                    ; E47A  nothing worked at all: stay put
        sta     scratch23               ; E47C  distance 0...
L_E47E:
        sta     scanRowCount            ; E47E  ...and direction 0

; ----------------------------------------------------------------------
; Turn the chosen (direction, distance) pair back into an absolute cell and fall into the order
; issuer.
; ----------------------------------------------------------------------
L_E480:
        ldx     scanRowCount            ; E480  chosen heading
        lda     dirToColDelta,x         ; E482  its column step -1/0/+1
        tax                             ; E485  X = column step
L_E486:
        ldy     scratch23               ; E486  chosen distance
        lda     cellColB                ; E488  search origin column
        jsr     offsetCoordinateClamped ; E48A  target column = origin + step * distance, clamped to 0..39
        sta     cellColA                ; E48D  chosen target column
        ldx     scanRowCount            ; E48F  chosen heading
        lda     dirToRowDelta,x         ; E491  its row step -1/0/+1
        tax                             ; E494  X = row step
        ldy     scratch23               ; E495  chosen distance
        lda     cellRowB                ; E497  search origin row
        jsr     offsetCoordinateClamped ; E499  target row, clamped
        sta     cellRowA                ; E49C  chosen target row - fall into the order issuer

; ----------------------------------------------------------------------
; issueTrainerGroupOrders - Issues the move orders for the group in $8955. It first stamps every
; member's replan clock with the current round so the scan loop cannot pick the group again, clips the
; wanted target with clampGroupTargetToMap (which also yields the movement delta zp_2A/zp_2B relative
; to the group's average ordered destination), derives a replan delay from the distance to the target
; (doubled when the group contains GRUNTs or BOOMERs, floored at 4 rounds and capped by the urgency
; $896D), and then walks all 50 AI slots giving every member a MOVE to its own destination shifted by
; the group delta - so the whole formation translates without changing shape. A member that also
; belongs to a real in-game group propagates its new replan clock to all its formation mates.
; In:  $8955 current group, zp_22/zp_24 wanted target, gameClock $91CB, $895A slow-unit flag, $896D
;     urgency, $8832 group ids, destinations $F802/$F866, unit flags $F73A
; Out: replan clocks $8800, group target $88D2/$88DE, commands appended to the queue at $88A0, zp_29
;     delay, zp_2A/zp_2B delta
; Called from: aimGroupAtOwnComcen $E353 (JMP) and by falling through from findTrainerAdvanceCell
;     $E49C
; Stamp every member of the group with the current round, so that scanTrainerUnitsForOrders will not
; re-plan the same group again later in this pass.
; ----------------------------------------------------------------------
issueTrainerGroupOrders:
        ldy     #$31                    ; E49E  walk AI ordinals 49..0
L_E4A0:
        lda     D_8832,y                ; E4A0  group id of this unit
        cmp     D_8955                  ; E4A3  the group being ordered
        bne     L_E4AE                  ; E4A6  not a member
        lda     gameClock               ; E4A8  current round
        sta     msgGetOpponentOnPhoneAnd,y; E4AB  stamp the member's replan clock ($8800+ordinal)
L_E4AE:
        dey                             ; E4AE  next ordinal
        bpl     L_E4A0                  ; E4AF  loop over all 50 slots

; ----------------------------------------------------------------------
; Work out where the formation may actually stand and how long it should be left alone afterwards.
; ----------------------------------------------------------------------
        jsr     clampGroupTargetToMap   ; E4B1  clip the target so the whole formation box stays on the map; leaves the delta in zp_2A/zp_2B
        jsr     cellDistance            ; E4B4  distance from the group centre to the clipped target
        bit     D_895A                  ; E4B7  does the group contain GRUNTs or BOOMERs?
        bpl     L_E4BD                  ; E4BA  all fast - use the raw distance
        asl     a                       ; E4BC  slow group - leave it alone twice as long
L_E4BD:
        sta     scanListIndex           ; E4BD  replan delay in rounds
        cmp     #$04                    ; E4BF  minimum 4 rounds
        bcs     L_E4C7                  ; E4C1  already at least 4
        lda     #$04                    ; E4C3  floor the delay at 4
        sta     scanListIndex           ; E4C5  store the floored delay
L_E4C7:
        lda     D_896D                  ; E4C7  urgency of this group (2 home guard, 3 comcen escort / committed attack, 5 default)
        cmp     scanListIndex           ; E4CA  is the delay longer than the urgency allows?
        bcs     L_E4D0                  ; E4CC  no
        sta     scanListIndex           ; E4CE  cap the delay at the urgency

; ----------------------------------------------------------------------
; Second pass over all 50 AI slots: every member gets a MOVE to its own ordered destination plus the
; group delta, and a new replan clock of gameClock - delay. zp_22 is reused here as the loop counter.
; ----------------------------------------------------------------------
L_E4D0:
        lda     #$31                    ; E4D0  walk AI ordinals 49..0 again
sub_E4D2:
        sta     cellColA                ; E4D2  zp_22 is the loop counter for this pass
L_E4D4:
        ldy     cellColA                ; E4D4  current ordinal
        lda     D_8832,y                ; E4D6  its group id
        cmp     D_8955                  ; E4D9  the group being ordered
        bne     L_E529                  ; E4DC  not a member
        lda     msgGetOpponentOnPhoneAnd,y; E4DE  its replan clock
        cmp     gameClock               ; E4E1  must still carry the stamp written above
        bcc     L_E529                  ; E4E4  a formation mate already re-planned it
        lda     side1UnitDestColTable,y ; E4E6  the unit's own ordered destination column
        and     #$3F                    ; E4E9  drop the flag bits, keep the 0-39 cell
        clc                             ; E4EB  prepare the add
        adc     scratch2A               ; E4EC  shift it by the group's column delta
        tax                             ; E4EE  X = target column
        lda     side1UnitDestRowTable,y ; E4EF  its ordered destination row
        and     #$3F                    ; E4F2  drop the flag bits, keep the 0-39 cell
        clc                             ; E4F4  prepare the add
        adc     scratch2B               ; E4F5  shift it by the group's row delta
        jsr     orderAiUnitToCell       ; E4F7  queue $80 MOVE (or $81 MOVE GROUP) for this unit
L_E4FA:
        ldy     cellColA                ; E4FA  ordinal again
        lda     gameClock               ; E4FC  current round
        sec                             ; E4FF  prepare the subtraction
        sbc     scanListIndex           ; E500  next replan in 'delay' rounds
sub_E502:
        bcs     L_E506                  ; E502  no underflow
        lda     #$00                    ; E504  clamp at round 0
L_E506:
        sta     msgGetOpponentOnPhoneAnd,y; E506  new replan clock for this unit
        sta     scratch18               ; E509  keep a copy for the formation mates
        lda     side1UnitFlagsTable,y   ; E50B  the unit's authoritative flags byte
        and     #$7C                    ; E50E  bits 2-6: in-game group id (2-4), side (5) and the 'in a group' bit (6)
        cmp     #$40                    ; E510  bit 6 clear means the unit is not in a player-style group
        bcc     L_E529                  ; E512  no formation mates to update
L_E514:
        sta     scratch19               ; E514  the group key to match
        ldx     #$31                    ; E516  scan all 50 AI ordinals
L_E518:
        lda     side1UnitFlagsTable,x   ; E518  flags of that unit
sub_E51B:
        and     #$7C                    ; E51B  same mask
        cmp     scratch19               ; E51D  same in-game group?
        bne     L_E526                  ; E51F  no
        lda     scratch18               ; E521  the replan clock just computed
        sta     msgGetOpponentOnPhoneAnd,x; E523  give the formation mate the same clock, so the group is not ordered twice
L_E526:
        dex                             ; E526  next ordinal
        bpl     L_E518                  ; E527  loop over all 50 slots
L_E529:
        dec     cellColA                ; E529  next ordinal
        bmi     L_E530                  ; E52B  all 50 slots done
L_E52D:
        jmp     L_E4D4                  ; E52D  keep going (out of branch range)

L_E530:
        rts                             ; E530  done


; ----------------------------------------------------------------------
; computeTrainerGroupStats - Gathers the statistics of AI group Y. It scans the 50 AI slots and, for
; every living member, sums the per-type combat weights into the 16-bit group value, notes whether the
; group contains slow units (GRUNT or BOOMER), counts the members that have not yet scored both end
; zones, and tracks the minimum and maximum of the members' ordered destinations. computeGroupCentroid
; then supplies the member count and the average current and ordered positions; those become the group
; centre, the group size and the four formation-box offsets (reach left, right, up, down). Returns C=1
; for an empty group.
; In:  Y = group id, $8832 group ids, types $F79E, destinations $F802/$F866, penetration flags $8864,
;     $895D assault group, $8895 comcen penetration flag
; Out: $8955 current group, $8956/$8957 value, $895A slow flag, $8902 size, $88EA/$88F6 centre,
;     $890E/$8926/$891A/$8932 box, zp_1D still-at-home count, zp_93/zp_92 = centre, zp_1E/zp_1F =
;     average destination; C=1 if the group is empty
; Called from: retreatTrainerComcen $E2A9 and decideTrainerGroupAction $E326
; ----------------------------------------------------------------------
computeTrainerGroupStats:
        sty     D_8955                  ; E531  remember which group is being measured
sub_E534:
        ldx     #$00                    ; E534  0 for the accumulators
        stx     scratch1D               ; E536  members that have not scored both end zones
        stx     D_8956                  ; E538  combat value low byte
        stx     D_8957                  ; E53B  combat value high byte
        stx     cellRowA                ; E53E  zp_24 = highest destination column seen (max tracker starts at 0)
sub_E540:
        stx     scanColCount            ; E540  zp_25 = highest destination row seen
        stx     D_895A                  ; E542  no slow units found yet
        dex                             ; E545  $FF for the minimum trackers
        stx     cellColA                ; E546  zp_22 = lowest destination column seen
        stx     scratch23               ; E548  zp_23 = lowest destination row seen

; ----------------------------------------------------------------------
; Loop over AI ordinals 49..0 accumulating strength, the slow-unit flag, the still-at-home count and
; the bounding box of the members' ordered destinations.
; ----------------------------------------------------------------------
        ldy     #$31                    ; E54A  walk AI ordinals 49..0
L_E54C:
        lda     D_8832,y                ; E54C  group id of this unit
        cmp     D_8955                  ; E54F  the group being measured
        bne     L_E5A2                  ; E552  not a member
        lda     side1UnitTypeTable,y    ; E554  type byte
        bmi     L_E5A2                  ; E557  dead
        and     #$07                    ; E559  bits 0-2 = unit type 0-4
        tax                             ; E55B  X = unit type, used as the index into unitCombatValueTable
        beq     L_E562                  ; E55C  type 0 GRUNT is slow
        cpx     #$02                    ; E55E  type 2 BOOMER is slow too
        bne     L_E567                  ; E560  RIDER, SPY and COMCEN are not
L_E562:
        lda     #$FF                    ; E562  $FF = the group contains slow units
        sta     D_895A                  ; E564  flag the group as slow
L_E567:
        lda     unitCombatValueTable,x  ; E567  combat weight of this type (2,2,3,0,0)
        clc                             ; E56A  prepare the add
L_E56B:
        adc     D_8956                  ; E56B  add it into the 16-bit group value
        sta     D_8956                  ; E56E  new low byte
        lda     D_8957                  ; E571  high byte
sub_E574:
        adc     #$00                    ; E574  carry into the high byte
        sta     D_8957                  ; E576  new high byte
        lda     D_8864,y                ; E579  this unit's 'has scored both end zones' flag
        bmi     L_E580                  ; E57C  already deep in the player's half
        inc     scratch1D               ; E57E  still at home - count it
L_E580:
        lda     side1UnitDestColTable,y ; E580  its ordered destination column
        and     #$3F                    ; E583  strip the flag bits
        cmp     cellColA                ; E585  below the running minimum?
        bcs     L_E58B                  ; E587  no
        sta     cellColA                ; E589  new leftmost destination
L_E58B:
        cmp     cellRowA                ; E58B  above the running maximum?
        bcc     L_E591                  ; E58D  no
        sta     cellRowA                ; E58F  new rightmost destination
L_E591:
        lda     side1UnitDestRowTable,y ; E591  its ordered destination row
        and     #$3F                    ; E594  strip the flag bits
        cmp     scratch23               ; E596  below the running minimum?
        bcs     L_E59C                  ; E598  no
        sta     scratch23               ; E59A  new topmost destination
L_E59C:
        cmp     scanColCount            ; E59C  above the running maximum?
        bcc     L_E5A2                  ; E59E  no
        sta     scanColCount            ; E5A0  new bottom-most destination
L_E5A2:
        dey                             ; E5A2  next ordinal
        bpl     L_E54C                  ; E5A3  loop over all 50 slots
        ldy     D_8955                  ; E5A5  group id
        cpy     D_895D                  ; E5A8  is this the assault group?
        bne     L_E5B4                  ; E5AB  no
        lda     D_8895                  ; E5AD  $8895 = $8864+49 = the AI comcen's own 'has penetrated' flag
        bmi     L_E5B4                  ; E5B0  the comcen is already deep in enemy ground
        inc     scratch1D               ; E5B2  otherwise the assault group counts as one more unit still at home

; ----------------------------------------------------------------------
; Average the members' positions and destinations, then store the centre, the size and the four
; formation-box offsets that the risk test and the target clipper work with.
; ----------------------------------------------------------------------
L_E5B4:
        ldy     #$31                    ; E5B4  scan ordinals 49..0
sub_E5B6:
        lda     D_8955                  ; E5B6  group id to match
        jsr     computeGroupCentroid    ; E5B9  average position and average ordered destination of the members
D_E5BC:
        sty     scratch18               ; E5BC  Y = average row
D_E5BE:
        ldy     D_8955                  ; E5BE  group id
L_E5C1:
        lda     scratch1C               ; E5C1  member count
sub_E5C3:
        sta     D_8902,y                ; E5C3  group size
        bne     L_E5CA                  ; E5C6  the group has members
        sec                             ; E5C8  C=1 = empty group
        rts                             ; E5C9  nothing more to do

L_E5CA:
        txa                             ; E5CA  X = average column
        sta     D_88EA,y                ; E5CB  group centre column
        sta     lineCurCol              ; E5CE  and the line walker's current column, for the callers
        lda     scratch18               ; E5D0  average row
        sta     D_88F6,y                ; E5D2  group centre row
L_E5D5:
        sta     lineCurRow              ; E5D5  and the line walker's current row
        lda     scratch1E               ; E5D7  average ordered destination column
        sec                             ; E5D9  prepare the subtraction
        sbc     cellColA                ; E5DA  minus the leftmost destination
        sta     D_890E,y                ; E5DC  = how far the formation reaches left of its centre
        lda     cellRowA                ; E5DF  rightmost destination
        sec                             ; E5E1  prepare the subtraction
        sbc     scratch1E               ; E5E2  minus the average
L_E5E4:
        clc                             ; E5E4  clear carry for the +1 below
        adc     #$01                    ; E5E5  +1 so the box includes the edge cell itself
        sta     D_8926,y                ; E5E7  = how far the formation reaches right
        lda     scratch1F               ; E5EA  average ordered destination row
        sec                             ; E5EC  prepare the subtraction
        sbc     scratch23               ; E5ED  minus the topmost destination
        sta     D_891A,y                ; E5EF  = reach above the centre
        lda     scanColCount            ; E5F2  bottom-most destination
        sec                             ; E5F4  prepare the subtraction
        sbc     scratch1F               ; E5F5  minus the average
        clc                             ; E5F7  clear carry for the +1 below
L_E5F8:
        adc     #$01                    ; E5F8  +1 for the edge cell
        sta     D_8932,y                ; E5FA  = reach below the centre
        clc                             ; E5FD  C=0 = the group exists
        rts                             ; E5FE  done


; ----------------------------------------------------------------------
; clampGroupTargetToMap - Clips the wanted group centre in zp_22/zp_24 so that the whole formation box
; (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores
; the clipped cell as the group's target and returns the movement delta relative to the group's
; average ordered destination in zp_2A (columns, also kept in $8958) and zp_2B (rows).
; In:  zp_22/zp_24 wanted centre, zp_1E/zp_1F average ordered destination, $8955 current group, the
;     four box offsets
; Out: zp_22/zp_24 clipped, zp_2A/zp_2B delta, $8958, $88D2/$88DE group target
; Called from: issueTrainerGroupOrders $E4B1; trainerSetupEC00 shiftAiGroupDestinations $EF00
; ----------------------------------------------------------------------
clampGroupTargetToMap:
        lda     cellColA                ; E5FF  wanted target column
        sec                             ; E601  prepare the subtraction
        ldy     D_8955                  ; E602  group id
        sbc     D_890E,y                ; E605  minus the formation's reach to the left
        bpl     L_E60F                  ; E608  left edge still >= column 0
        lda     D_890E,y                ; E60A  too far left: the leftmost the centre may sit at
        sta     cellColA                ; E60D  push the centre right so the box fits
L_E60F:
        lda     cellColA                ; E60F  target column
        clc                             ; E611  prepare the add
        adc     D_8926,y                ; E612  plus the reach to the right
        cmp     #$28                    ; E615  $28 = 40 columns
        bcc     L_E620                  ; E617  right edge fits
        lda     #$27                    ; E619  $27 = column 39; the CMP above left C=1, so this SBC is exact without a SEC
        sbc     D_8926,y                ; E61B  minus the reach right
        sta     cellColA                ; E61E  clipped centre column
L_E620:
        lda     cellColA                ; E620  clipped column
        sec                             ; E622  prepare the subtraction
        ldy     D_8955                  ; E623  group id (Y already holds it - a redundant reload)
        sbc     scratch1E               ; E626  minus the group's average ordered column
L_E628:
        sta     D_8958                  ; E628  column delta, also kept here for the setup overlay
        sta     scratch2A               ; E62B  column delta the caller adds to every member
        lda     cellRowA                ; E62D  wanted target row
        sec                             ; E62F  prepare the subtraction
        ldy     D_8955                  ; E630  group id (redundant reload)
        sbc     D_891A,y                ; E633  minus the reach upward
        bpl     L_E63D                  ; E636  top edge still >= row 0
        lda     D_891A,y                ; E638  too high: the topmost the centre may sit at
        sta     cellRowA                ; E63B  push the centre down so the box fits
L_E63D:
        lda     cellRowA                ; E63D  target row
        clc                             ; E63F  prepare the add
        adc     D_8932,y                ; E640  plus the reach downward
        cmp     #$28                    ; E643  $28 = 40 rows
        bcc     L_E64F                  ; E645  bottom edge fits
        lda     #$27                    ; E647  $27 = row 39
        sec                             ; E649  explicit SEC here, unlike the column path above which relies on the CMP carry
        sbc     D_8932,y                ; E64A  minus the reach down
        sta     cellRowA                ; E64D  clipped centre row
L_E64F:
        lda     cellRowA                ; E64F  clipped row
        sec                             ; E651  prepare the subtraction
        ldy     D_8955                  ; E652  group id (redundant reload)
        sbc     scratch1F               ; E655  minus the group's average ordered row
sub_E657:
        sta     scratch2B               ; E657  row delta the caller adds to every member
        lda     cellColA                ; E659  clipped column
        sta     D_88D2,y                ; E65B  remember it as this group's target cell
        lda     cellRowA                ; E65E  clipped row
        sta     D_88DE,y                ; E660  and its target row
L_E663:
        rts                             ; E663  done


; maxAdvanceByRankTable - how many cells the path search may advance for direction-preference rank
; 0..7. The three best headings may be followed up to 5 cells, the next two 3 cells and the worst
; three only 1, so a group that has to detour a long way round also moves a shorter distance.
maxAdvanceByRankTable:
        .byte   $05                     ; E664  5,5,5,3,3,1,1,1 cells per rank
        .byte   $05,$05,$03,$03,$01,$01,$01; E665  .......

; ----------------------------------------------------------------------
; offsetPointInDirection - Computes the cell that lies Y steps in compass direction X from the
; reference point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and
; offsetCoordinateClamped. The result is left in zp_18 (column) and zp_19 (row); carry set means the
; raw coordinate left the 40x40 map, and when the column already left it the row is not computed at
; all.
; In:  X = direction 0..7 (0 = north, clockwise), Y = number of steps, zp_27/zp_28 = origin column/row
; Out: zp_18/zp_19 = target cell, C=1 when the point fell off the map; self-modifies the LDX immediate
;     at $E67D (L_E67C+1)
; Called from: findTrainerAdvanceCell $E44D, isGroupDestinationTooRisky $E69C, the drone target scan
;     $EF9F
; ----------------------------------------------------------------------
offsetPointInDirection:
        stx     L_E67C+1                ; E66C  patch the direction into the LDX immediate at $E67D so it can be reused for the row table
        lda     dirToColDelta,x         ; E66F  column delta -1/0/+1 of that direction
        tax                             ; E672  X = column step for offsetCoordinateClamped
L_E673:
        lda     cellColB                ; E673  origin column
        jsr     offsetCoordinateClamped ; E675  column + step * distance, clamped to 0..39
        bcs     L_E689                  ; E678  left the map - report failure, the row is not computed
        sta     scratch18               ; E67A  candidate column
L_E67C:
        ldx     #$FF                    ; E67C  operand patched at $E66C with the direction index
        lda     dirToRowDelta,x         ; E67E  row delta -1/0/+1 of that direction
        tax                             ; E681  X = row step
        lda     cellRowB                ; E682  origin row
        jsr     offsetCoordinateClamped ; E684  row + step * distance, clamped to 0..39
        sta     scratch19               ; E687  candidate row
L_E689:
        rts                             ; E689  C=1 when either coordinate had to be clamped back onto the 40x40 map


; Compass direction tables shared by the whole trainer AI.  Directions are numbered 0-7 clockwise from
; north: 0 N, 1 NE, 2 E, 3 SE, 4 S, 5 SW, 6 W, 7 NW.  A ninth entry (index 8) holds the delta pair 0,0
; so that 'no direction at all' can be expressed as an ordinary index - assessThreatAroundAiComcen
; returns 8 when nothing threatens the AI comcen.  Read by $E241/$E274 (comcen push/retreat),
; $E482 (path search), offsetPointInDirection ($E66F/$E67E) and getStepDirection ($E816/$E81E).
dirToColDelta:
        .byte   $00,$01,$01,$01,$00,$FF,$FF,$FF; E68A  ........  column step of directions 0-7: N 0, NE +1, E +1, SE +1, S 0, SW -1, W -1, NW -1
        .byte   $00                     ; E692  index 8: the null direction, column step 0

; 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.
dirToRowDelta:
        .byte   $FF,$FF,$00,$01,$01,$01,$00,$FF; E693  ........  row step of directions 0-7: N -1, NE -1, E 0, SE +1, S +1, SW +1, W 0, NW -1
        .byte   $00                     ; E69B  index 8: the null direction, row step 0

; ----------------------------------------------------------------------
; isGroupDestinationTooRisky - influence-map test of one candidate destination for the AI group in
; currentAiGroup ($8955). Steps Y cells in direction X away from (cellColB,cellRowB) with
; offsetPointInDirection, grows that cell into the rectangle the whole formation would cover (the four
; span bytes $890E left / $891A up / $8926 right / $8932 down, clamped to 0..39) and plants the four
; edges into the four CMP operands of its own scan loop.  It then walks all 100 units and sums a
; signed 16-bit score: every unit whose square influence area (side 2*unitThreatRadius $0400) overlaps
; the rectangle adds unitThreatWeight ($0464) if it is one of the human player's units 0-49 and
; subtracts it if it is one of the AI's own 50-99 (units of the moving group itself were already given
; radius 0 by buildUnitThreatTables).  The verdict compares the enemy surplus with the group's own
; combat value $8956/$8957.
; In:  X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row; $8955
;     currentAiGroup, $8954 aiGroupCount,
;      span tables $890E/$891A/$8926/$8932, threat tables $0400/$0464 (built by
; buildUnitThreatTables), group value $8956/$8957,
;      $896E aggression-phase clock, $91CB game clock
; Out: C=1 = reject (off the map, or enemy threat >= own combat value); C=0 = acceptable.  zp_18/zp_19
;     = the candidate cell,
;      zp_1A/zp_1B = the signed 16-bit threat surplus, zp_1C = the radius of the unit being weighed.
;      Self-modifies the CMP operands at $E704, $E70B, $E715 and $E71C.
; Called from: findTrainerAdvanceCell ($E444) only.
; ----------------------------------------------------------------------
isGroupDestinationTooRisky:
        jsr     offsetPointInDirection  ; E69C  candidate cell = origin (cellColB,cellRowB) moved Y cells in direction X
L_E69F:
        bcc     L_E6A2                  ; E69F  the candidate is on the map, so weigh it
        rts                             ; E6A1  the step ran off the edge: return C=1 = reject

L_E6A2:
        ldy     D_8955                  ; E6A2  Y = the group being moved
        cpy     D_8954                  ; E6A5  is it one of the aiGroupCount live groups?
        bcc     L_E6AC                  ; E6A8  yes: measure the threat over its formation footprint
        clc                             ; E6AA  no such group, so there is nothing to be afraid of
        rts                             ; E6AB  return C=0 = destination acceptable


; ----------------------------------------------------------------------
; Grow the candidate cell into the rectangle the whole formation would stand on, clamp it to the 40x40
; map, and write its four edges straight into the four CMP instructions of the unit scan below.
; ----------------------------------------------------------------------
L_E6AC:
        lda     scratch18               ; E6AC  candidate column
        sec                             ; E6AE  prepare the subtraction
        sbc     D_890E,y                ; E6AF  minus the group's span to the left = left edge of the formation box
        bpl     L_E6B6                  ; E6B2  still on the map
        lda     #$00                    ; E6B4  hangs off the left edge: clamp the box to column 0
L_E6B6:
        sta     L_E714+1                ; E6B6  self-modify: the CMP at $E714 now tests against the box's left column
        lda     scratch18               ; E6B9  candidate column again
        clc                             ; E6BB  prepare the addition
        adc     D_8926,y                ; E6BC  plus the group's span to the right = right edge of the box
        cmp     #$28                    ; E6BF  past the last column (40)?
        bcc     L_E6C5                  ; E6C1  no, keep it
L_E6C3:
        lda     #$27                    ; E6C3  yes: clamp the box to column 39
L_E6C5:
        sta     L_E71B+1                ; E6C5  self-modify: the CMP at $E71B now tests against the box's right column
L_E6C8:
        lda     scratch19               ; E6C8  candidate row
        sec                             ; E6CA  prepare the subtraction
        sbc     D_891A,y                ; E6CB  minus the group's span upwards = top edge of the box
        bpl     L_E6D2                  ; E6CE  still on the map
        lda     #$00                    ; E6D0  hangs off the top: clamp the box to row 0
L_E6D2:
        sta     L_E703+1                ; E6D2  self-modify: the CMP at $E703 now tests against the box's top row
        lda     scratch19               ; E6D5  candidate row again
        clc                             ; E6D7  prepare the addition
        adc     D_8932,y                ; E6D8  plus the group's span downwards = bottom edge of the box
        cmp     #$28                    ; E6DB  past the last row (40)?
        bcc     L_E6E1                  ; E6DD  no, keep it
        lda     #$27                    ; E6DF  yes: clamp the box to row 39
L_E6E1:
        sta     L_E70A+1                ; E6E1  self-modify: the CMP at $E70A now tests against the box's bottom row

; ----------------------------------------------------------------------
; Walk all 100 units, unit 99 down to unit 0, and accumulate a signed 16-bit 'how dangerous is this
; square' score in zp_1A/zp_1B: enemy influence adds, friendly influence subtracts.  A unit only
; counts when its influence square (centre = its cell, half-width = unitThreatRadius) overlaps the
; formation box just computed.
; ----------------------------------------------------------------------
        lda     #$00                    ; E6E4  clear the running score
        sta     scratch1A               ; E6E6  threat surplus, low byte
        sta     scratch1B               ; E6E8  threat surplus, high byte
        ldy     #$63                    ; E6EA  start at unit 99
L_E6EC:
        lda     checksumXorConstant,y   ; E6EC  unitThreatRadius[unit] from the $0400 influence page  (also trainerPlaybook0200:play3Col12)
        beq     L_E740                  ; E6EF  radius 0 (destroyed, spy/comcen, or a member of the moving group): the unit does not count
L_E6F1:
        sta     scratch1C               ; E6F1  keep this unit's influence radius
        cpy     #$32                    ; E6F3  unit 50-99 = one of the AI's own?
        bcc     L_E6FC                  ; E6F5  no, it is one of the human player's units 0-49
        lda     D_8955                  ; E6F7  own unit: is the group being moved group 0, the AI comcen's own group?
        beq     L_E740                  ; E6FA  yes: friendly cover is not credited to the comcen group, so skip own units entirely
L_E6FC:
        lda     unitRowTable,y          ; E6FC  row of this unit
L_E6FF:
        tax                             ; E6FF  keep the row for the second test
        clc                             ; E700  prepare the addition
        adc     scratch1C               ; E701  row + radius = bottom edge of its influence square
L_E703:
        cmp     #$FF                    ; E703  operand patched at $E6D2 with the box's top row
        bcc     L_E740                  ; E705  its influence stops above the box: no overlap, next unit
        txa                             ; E707  the row again (the CMP above left C=1 for the subtraction)
        sbc     scratch1C               ; E708  row - radius = top edge of its influence square
L_E70A:
        cmp     #$FF                    ; E70A  operand patched at $E6E1 with the box's bottom row
        bcs     L_E740                  ; E70C  its influence starts below the box: no overlap
        lda     unitColTable,y          ; E70E  column of this unit
        tax                             ; E711  keep the column for the second test
L_E712:
        adc     scratch1C               ; E712  column + radius = right edge of its influence square (the CMP above left C=0)
L_E714:
        cmp     #$FF                    ; E714  operand patched at $E6B6 with the box's left column
        bcc     L_E740                  ; E716  its influence stops left of the box: no overlap
        txa                             ; E718  the column again (the CMP above left C=1 for the subtraction)
        sbc     scratch1C               ; E719  column - radius = left edge of its influence square
L_E71B:
        cmp     #$FF                    ; E71B  operand patched at $E6C5 with the box's right column
        bcs     L_E740                  ; E71D  its influence starts right of the box: no overlap
        cpy     #$32                    ; E71F  the squares overlap, so whose unit is it?
        bcc     addEnemyThreatWeight    ; E721  human player's unit 0-49: its weight adds to the danger
        lda     scratch1A               ; E723  one of the AI's own: subtract its weight instead (friendly cover)
        sbc     play4Grp12,y            ; E725  unitThreatWeight[unit] from the $0464 influence page (the CPY above left C=1)
        sta     scratch1A               ; E728  store the surplus low byte
        lda     scratch1B               ; E72A  surplus high byte
        sbc     #$00                    ; E72C  propagate the borrow
        sta     scratch1B               ; E72E  store it back
        jmp     L_E740                  ; E730  on to the next unit

addEnemyThreatWeight:
        lda     scratch1A               ; E733  surplus low byte
        adc     play4Grp12,y            ; E735  add unitThreatWeight[unit] (the CPY at $E71F left C=0)
        sta     scratch1A               ; E738  store it back
        lda     scratch1B               ; E73A  surplus high byte
sub_E73C:
        adc     #$00                    ; E73C  propagate the carry
        sta     scratch1B               ; E73E  store it back
L_E740:
        dey                             ; E740  next unit
        bpl     L_E6EC                  ; E741  until unit 0 has been weighed

; ----------------------------------------------------------------------
; Verdict.  A negative or zero surplus means the AI already dominates the square.  Otherwise the enemy
; surplus is compared with this group's own combat value, and an exact draw is settled by the game
; clock and the RNG so that both machines would reach the same answer.
; ----------------------------------------------------------------------
judgeGroupThreatTotal:
        lda     scratch1B               ; E743  sign of the 16-bit surplus
        bpl     L_E749                  ; E745  not negative: weigh it against the group's own strength
L_E747:
        clc                             ; E747  friendly influence outweighs the enemy there
        rts                             ; E748  C=0 = destination acceptable

L_E749:
        lda     scratch1A               ; E749  surplus low byte
        ora     scratch1B               ; E74B  is the whole 16-bit surplus exactly zero?
        beq     L_E747                  ; E74D  no enemy influence at all: accept
        lda     scratch1A               ; E74F  surplus low byte
        sec                             ; E751  prepare the subtraction
L_E752:
        sbc     D_8956                  ; E752  minus the group's own combat value, low byte
L_E755:
        sta     scratch1A               ; E755  keep the difference
        lda     scratch1B               ; E757  surplus high byte
        sbc     D_8957                  ; E759  minus the group's combat value, high byte
        sta     scratch1B               ; E75C  keep it
        ora     scratch1A               ; E75E  is the difference exactly zero?
        bne     L_E772                  ; E760  no: return the carry of the subtraction (C=1 = threat >= own strength = reject)
        lda     D_896E                  ; E762  dead heat: consult the aggression-phase clock set at setup ($EF00 area) or by retreatTrainerComcen
L_E765:
        cmp     gameClock               ; E765  has the game reached that round?  the game clock counts down, so 'phase clock >= clock' means yes
        bcs     L_E76D                  ; E768  yes, late enough in the game: toss a coin
        sec                             ; E76A  still early: play safe
L_E76B:
        bcs     L_E772                  ; E76B  always taken (the BCS above fell through with C=1): return C=1 = reject
L_E76D:
        jsr     nextGameRandom          ; E76D  step the lock-step game RNG so both machines stay in sync
        cmp     #$80                    ; E770  C=1 for half of the random values, i.e. reject a tie 50 percent of the time
L_E772:
        rts                             ; E772  carry tells the caller whether the destination is too dangerous


; ----------------------------------------------------------------------
; buildUnitThreatTables - rebuilds the two 100-entry influence-map tables that
; isGroupDestinationTooRisky reads. For each unit 99..0 it writes an influence radius into $0400 and a
; combat weight into $0464.  Destroyed units, spies and comcens (type 3 and 4) and every AI unit
; belonging to the group that is about to be moved get radius 0 and weight 0. A grunt or rider gets
; base radius 3, a boomer 6, both plus the terrain sight bonus of the tile the unit is standing on, so
; units on high ground project further.  The weight comes from unitCombatValueTable ($E2B7) =
; 2,2,3,0,0.
; In:  $8955 currentAiGroup, aiUnitGroup ($8800+absolute unit index), unitTypeTable $F76C,
;     unitTerrainUnderTable $FAF0,
;      terrainClassTable $077E, terrainSightBonusTable $35BB, unitCombatValueTable $E2B7
; Out: $0400[0..99] = influence radius, $0464[0..99] = combat weight; zp_18 and zp_19 clobbered
; Called from: findTrainerAdvanceCell ($E42A), immediately before its search loop.
; One entry per unit, walking backwards from unit 99 to unit 0.
; ----------------------------------------------------------------------
buildUnitThreatTables:
        ldy     #$63                    ; E773  start at unit 99
L_E775:
        cpy     #$32                    ; E775  is this one of the AI's own units 50-99?
        bcc     L_E781                  ; E777  no, it is a human player unit: no group test needed
        lda     msgGetOpponentOnPhoneAnd,y; E779  aiUnitGroup[unit] - the group table addressed by absolute index ($8800+unit = $8832+ordinal)
        cmp     D_8955                  ; E77C  does it belong to the very group we are about to move?
        beq     L_E786                  ; E77F  yes: a group must not weigh its own members
L_E781:
        lda     unitTypeTable,y         ; E781  unit type byte, bit 7 = destroyed
        bpl     L_E78A                  ; E784  alive: work out its influence radius
L_E786:
        lda     #$00                    ; E786  destroyed, or a member of the moving group: no influence
        beq     L_E7A7                  ; E788  always taken (A = 0): store radius 0
L_E78A:
        ldx     #$03                    ; E78A  default influence radius 3 cells
L_E78C:
        and     #$07                    ; E78C  unit type 0-4 without the status bits
        sta     scratch19               ; E78E  keep the type for the weight look-up
        cmp     #$02                    ; E790  GRUNT (0) or RIDER (1)?
        bcc     L_E798                  ; E792  yes: keep radius 3
        bne     L_E786                  ; E794  type 3 SPY or 4 COMCEN: they project no threat at all
        ldx     #$06                    ; E796  BOOMER (2): its guns reach twice as far, radius 6
L_E798:
        stx     scratch18               ; E798  stash the base radius
        ldx     unitTerrainUnderTable,y ; E79A  terrain code of the tile hidden under the unit
        lda     D_077E,x                ; E79D  terrain class 0-8 of that tile
        tax                             ; E7A0  class becomes the index
        lda     terrainSightBonusTable,x; E7A1  extra range the terrain grants: 0, 1 or 2 cells
        clc                             ; E7A4  prepare the addition
        adc     scratch18               ; E7A5  radius = base radius + terrain bonus
L_E7A7:
        sta     checksumXorConstant,y   ; E7A7  unitThreatRadius[unit] into the $0400 scratch page  (also trainerPlaybook0200:play3Col12)
        tax                             ; E7AA  is the radius zero?
        beq     L_E7B2                  ; E7AB  yes: A is already 0, so the weight becomes 0 too
        ldx     scratch19               ; E7AD  unit type
        lda     unitCombatValueTable,x  ; E7AF  combat weight: GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0
L_E7B2:
        sta     play4Grp12,y            ; E7B2  unitThreatWeight[unit] into the $0464 scratch page
L_E7B5:
        dey                             ; E7B5  next unit
        bpl     L_E775                  ; E7B6  until unit 0 has an entry
        rts                             ; E7B8  both influence tables are current


; ----------------------------------------------------------------------
; multiplyAbyY - 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y = 0. The
; multiplicand is copied into its own ADC operand and Y is restored from a second patched operand
; before returning, so the caller's step count survives the call.
; In:  A = multiplicand, Y = count
; Out: A = A*Y; Y unchanged; X untouched, flags clobbered; self-modifies the operands at $E7CB and
;     $E7D0
; Called from: offsetCoordinateClamped ($EA22) only, where A is a step of 0, +1 or $FF and Y a cell
;     distance.
; ----------------------------------------------------------------------
multiplyAbyY:
        sty     L_E7CF+1                ; E7B9  self-modify: stash the count in the LDY at $E7CF so Y can be handed back
        sta     L_E7CA+1                ; E7BC  self-modify: the multiplicand becomes the operand of the ADC at $E7CA
        cpy     #$00                    ; E7BF  multiply by zero?
        bne     L_E7C6                  ; E7C1  no: run the addition loop
        tya                             ; E7C3  Y is 0, so A = 0
        beq     L_E7CE                  ; E7C4  always taken: return zero
L_E7C6:
        dey                             ; E7C6  one addition per unit of the count
        beq     L_E7CE                  ; E7C7  count used up: A holds the product
L_E7C9:
        clc                             ; E7C9  clear the carry before each addition
L_E7CA:
        adc     #$FF                    ; E7CA  operand patched at $E7BC: add the multiplicand once more
        bne     L_E7C6                  ; E7CC  keep adding; a running total of exactly 0 would end the loop early, which the callers' 0/+1/-1 steps never produce
L_E7CE:
        pha                             ; E7CE  hold the product across the Y restore
L_E7CF:
        ldy     #$FF                    ; E7CF  operand patched at $E7B9: give the caller its count back
        pla                             ; E7D1  product back into A
        rts                             ; E7D2  A = A*Y


; ----------------------------------------------------------------------
; buildDirectionFanTowardTarget - finds the compass heading from the line start
; (lineCurCol,lineCurRow) to the line end (lineEndCol,lineEndRow) with getStepDirection, saves it in
; zp_19 and falls into buildDirectionFan so the eight candidate move directions are laid out around
; that heading.  Nothing is built when start and target are the same cell.
; In:  zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column, $8955 currentAiGroup
; Out: zp_19 = heading 0-7 ($FF and no fan when start = target); $8896-$889D = the eight candidate
;     directions;
;      zp_18 and the line-stepper state $8C-$97 clobbered
; Called from: the group-movement code at $E416.
; ----------------------------------------------------------------------
buildDirectionFanTowardTarget:
        jsr     getStepDirection        ; E7D3  heading of the first Bresenham step from the group centre toward its target
        sty     scratch19               ; E7D6  remember the heading for buildDirectionFan
        bpl     buildDirectionFan       ; E7D8  a real heading 0-7: lay the eight candidates out around it
        rts                             ; E7DA  Y = $FF, the group is already on its target cell: no fan to build


; dirMirrorTable - eight bytes, entry d = (8 - d) AND 7, i.e. the mirror of a heading offset about the
; north-south axis (0 stays 0, +1 becomes -1 and so on).  buildDirectionFan runs the offsets of a
; 'left-handed' group through it so that half the groups sweep clockwise and half anticlockwise. The
; label in the middle of the table is a branch target of the modem-driver build at the same address,
; not a separate block.
dirMirrorTable:
        .byte   $00,$07,$06,$05,$04     ; E7DB  .....  mirror of heading offsets 0-4: 0, -1, -2, -3, -4 expressed modulo 8
L_E7E0:
        .byte   $03                     ; E7E0  mirror of heading offsets 5-7: +3, +2, +1
        .byte   $02,$01                 ; E7E1  ..

; ----------------------------------------------------------------------
; buildDirectionFan - fills the eight-byte scratch buffer $8896-$889D with the move directions the
; path search should try, in order of preference: entry i = (dirFanOffsetTable[i] + heading + 8) AND
; 7.  dirFanOffsetTable ($894A) is built once per game by the setup overlay's
; buildAiHeadingSearchOrder ($EEC3) as 0, +/-1, +/-1, +/-2, +/-2, +/-3, +/-3, +/-4 with a random
; left/right bias, so the search always tries straight ahead first and fans outwards.  When bit 0 of
; the group's groupFlags is set every offset is mirrored through dirMirrorTable, giving that group the
; opposite handedness.
; In:  zp_19 = wanted heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable
; Out: $8896-$889D = eight candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered
; Called from: decideTrainerGroupAction ($E36E) and by falling through from
;     buildDirectionFanTowardTarget ($E7D8).
; The list is consumed by the move search at $E431-$E47E, whose per-rank distance caps are in
; maxAdvanceByRankTable ($E664).
; ----------------------------------------------------------------------
buildDirectionFan:
        ldx     #$00                    ; E7E3  assume the fan is not mirrored
        ldy     D_8955                  ; E7E5  the group being moved
        lda     D_893E,y                ; E7E8  its groupFlags; the low nibble is the random start delay handed out at setup
        and     #$01                    ; E7EB  bit 0 of that delay serves as the group's fixed handedness
        beq     L_E7F0                  ; E7ED  clear: fan out the normal way
        dex                             ; E7EF  set: X = $FF, mirror every offset
L_E7F0:
        stx     scratch18               ; E7F0  mirror flag for the loop below
        ldy     #$07                    ; E7F2  fill the eight preference slots, 7 down to 0
L_E7F4:
        lda     D_894A,y                ; E7F4  heading offset of preference rank Y
L_E7F7:
        bit     scratch18               ; E7F7  is this group mirrored?  (BIT leaves A alone)
        bpl     L_E7FF                  ; E7F9  no: use the offset as it is
        tax                             ; E7FB  offset becomes the index
        lda     dirMirrorTable,x        ; E7FC  yes: swap left for right
L_E7FF:
        clc                             ; E7FF  clear the carry before adding
        adc     scratch19               ; E800  candidate = wanted heading + offset
        clc                             ; E802  the addition above may have carried
        adc     #$08                    ; E803  bias by a whole turn so a negative offset stays positive
L_E805:
        and     #$07                    ; E805  wrap back into a compass direction 0-7
        sta     D_8896,y                ; E807  store the candidate for preference rank Y
L_E80A:
        dey                             ; E80A  next rank
        bpl     L_E7F4                  ; E80B  until all eight ranks are filled
        rts                             ; E80D  the fan is ready for the path search


; ----------------------------------------------------------------------
; getStepDirection - sets a Bresenham walker up between (lineCurCol,lineCurRow) and
; (lineEndCol,lineEndRow), takes exactly one step, and converts the deltas that step applied ($90F5
; column, $90F4 row) back into one of the eight compass directions by searching
; dirToColDelta/dirToRowDelta from index 7 downwards.  stepLine zeroes both deltas before it moves, so
; a zero-length line leaves 0,0 - which matches no entry 0-7 and comes out as Y = $FF.
; In:  zp_92/zp_93 = start row/column, zp_94/zp_95 = end row/column
; Out: Y = direction 0-7, or $FF when start and end are the same cell; A = the last delta compared;
;      line-stepper state $8C-$97 and $90F4/$90F5 updated
; Called from: buildDirectionFanTowardTarget ($E7D3), assessThreatAroundAiComcen ($EC35), the drone
;     code ($EEC7)
; and the $EE00 drone-flight overlay ($EEB1).
; ----------------------------------------------------------------------
getStepDirection:
        jsr     initLineStepper         ; E80E  set the Bresenham walker up between the two cells
        jsr     stepLine                ; E811  take one step; it records the deltas it applied in $90F5 (column) and $90F4 (row), zeroing them first
        ldy     #$07                    ; E814  search the direction tables backwards from 7
L_E816:
        lda     dirToColDelta,y         ; E816  column step of direction Y
        cmp     lastStepColDelta        ; E819  does it match the step just taken?
        bne     L_E826                  ; E81C  no: try the next direction
        lda     dirToRowDelta,y         ; E81E  row step of direction Y
        cmp     lastStepRowDelta        ; E821  does that match too?
        beq     L_E829                  ; E824  both match: Y is the heading
L_E826:
        dey                             ; E826  next direction down
        bpl     L_E816                  ; E827  index 8 (the null delta) is deliberately never tested, so Y = $FF means 'the line has no step'
L_E829:
        rts                             ; E829  Y = compass direction, or $FF


; ----------------------------------------------------------------------
; cellDistance - integer distance between two map cells.  distanceSquared ($3870) gives |dcol|^2 +
; |drow|^2 (each delta capped at 15, the sum saturating at $FF with C=1), and this routine converts it
; back through squaresTable ($3860) into the largest n with n*n <= dsq.  A saturated squared distance
; simply reports 16.
; In:  zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row
; Out: A = X = distance 0-16; Y untouched
; Called from: $E410, $E4B4, assessThreatAroundAiComcen ($EC24), the missile code ($ED43/$EE0C/$EE5C)
;     and the drone overlay.
; ----------------------------------------------------------------------
cellDistance:
        jsr     distanceSquared         ; E82A  A = squared distance between (cellColA,cellRowA) and (cellColB,cellRowB), C=1 if it saturated at 255
        ldx     #$10                    ; E82D  16 is the largest distance this routine reports
        bcs     L_E83A                  ; E82F  saturated: everything beyond 15 cells is simply 'far'
        dex                             ; E831  otherwise start the search at 15
L_E832:
        cmp     squaresTable,x          ; E832  is the squared distance at least X*X?
        bcs     L_E83A                  ; E835  yes: X is the integer distance
        dex                             ; E837  try the next smaller radius
        bne     L_E832                  ; E838  X = 0 falls out of the loop as distance 0
L_E83A:
        txa                             ; E83A  hand the distance back in A as well as X
        rts                             ; E83B  A = X = distance in cells


; ----------------------------------------------------------------------
; updateFocusGroupCentre - keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row)
; current.  The focus group is the assault group the AI comcen walks along behind.  When the id has
; bit 7 set the centre falls back on the AI comcen's own cell, and a suspended group is taken back
; into service as soon as bit 7 of its groupFlags has cleared.  An active group whose groupFlags bit 7
; is set (it has reached its objective) is suspended by ORing $80 into the id.  Otherwise the centroid
; of the group's combat members is recomputed, and a group that has lost every member is dropped
; ($895D = $FF).
; In:  $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707, $8832 aiUnitGroup
; Out: $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered
; Called from: planTrainerRound ($E20C), once per round just before the comcen push/retreat tail.
; ----------------------------------------------------------------------
updateFocusGroupCentre:
        ldy     D_895D                  ; E83C  focusGroupId: $FF = no group, bit 7 set = the group is suspended
        bpl     L_E860                  ; E83F  an active group id 0-15: go and refresh its centre
        lda     side1ComcenCol          ; E841  no usable group: fall back on the AI comcen's own cell (unit 99)
L_E844:
        sta     msgOpponentUsingNonStandardGame; E844  focus centre column ($895E)
        lda     side1ComcenRow          ; E847  AI comcen row
        sta     D_895F                  ; E84A  focus centre row
        cpy     #$FF                    ; E84D  was the focus group dropped altogether?
        beq     L_E85F                  ; E84F  yes: nothing left to revive
        tya                             ; E851  suspended group: recover its id
        and     #$0F                    ; E852  strip the suspend bit
        tay                             ; E854  index the group tables with it
        lda     D_893E,y                ; E855  its groupFlags
L_E858:
        bmi     L_E85F                  ; E858  bit 7 still set (the group is at its objective): leave it suspended
        sty     D_895D                  ; E85A  the flag cleared: put the group back in service
        bpl     L_E860                  ; E85D  always taken (an id 0-15 is positive): recompute its centre
L_E85F:
        rts                             ; E85F  nothing to update this round

L_E860:
        lda     D_893E,y                ; E860  groupFlags of the active focus group
        bpl     L_E86C                  ; E863  still on its way: recompute the centre
        tya                             ; E865  bit 7 set: the group has arrived
        ora     #$80                    ; E866  mark the id as suspended
        sta     D_895D                  ; E868  store it back; the centre keeps the comcen fallback from the next call
        rts                             ; E86B  done for this round

L_E86C:
        lda     D_895D                  ; E86C  group id for the centroid scan
        ldy     #$2D                    ; E86F  scan AI ordinals 45 down to 0 - the combat units only, since 46-48 are the spies and 49 the comcen
        jsr     computeGroupCentroid    ; E871  A = member count, X = average column, Y = average row
        bne     L_E87B                  ; E874  the group still has members
        lda     #$FF                    ; E876  no member left
        sta     D_895D                  ; E878  drop the focus group; X/Y are 0/$FF here, so the centre is stale for the one round until the fallback runs
L_E87B:
        stx     msgOpponentUsingNonStandardGame; E87B  focus centre column
        sty     D_895F                  ; E87E  focus centre row
        rts                             ; E881  the comcen now has something to escort


; ----------------------------------------------------------------------
; computeGroupCentroid - averages the position and the ordered destination of every living member of
; one AI group.  It scans AI ordinals Y down to 0 (absolute units 50+Y down to 50), accumulates four
; 16-bit sums and divides each by the member count with divideSigned16by8.  The wanted group id is
; patched into its own CMP, and the two averaged coordinates are handed back through two patched
; immediate operands so that A can carry the member count.
; In:  A = group id, Y = highest AI ordinal to scan (usually $31 = 49 or $2D = 45); $8832 aiUnitGroup,
;     $F79E unitType,
;      $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow (all indexed by AI ordinal)
; Out: A = member count (Z set when the group is empty), 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 the operands at $E89D, $E913 and $E915
; Called from: computeTrainerGroupStats ($E5B9) and updateFocusGroupCentre ($E871).
; ----------------------------------------------------------------------
computeGroupCentroid:
        sta     L_E89C+1                ; E882  self-modify: the wanted group id becomes the operand of the CMP at $E89C
        lda     #$00                    ; E885  clear all nine accumulator bytes
        sta     scratch18               ; E887  column sum, low
        sta     scratch19               ; E889  column sum, high
        sta     scratch1A               ; E88B  row sum, low
        sta     scratch1B               ; E88D  row sum, high
        sta     scratch1C               ; E88F  member count
        sta     scratch1E               ; E891  ordered-destination column sum, low
L_E893:
        sta     scratch1F               ; E893  ordered-destination column sum, high
        sta     scratch20               ; E895  ordered-destination row sum, low
        sta     scratch21               ; E897  ordered-destination row sum, high

; ----------------------------------------------------------------------
; One pass per AI ordinal, from the caller's starting ordinal down to 0.
; ----------------------------------------------------------------------
L_E899:
        lda     D_8832,y                ; E899  aiUnitGroup[ordinal] - which group this AI unit belongs to
L_E89C:
        cmp     #$FF                    ; E89C  operand patched at $E882: is it the group we are averaging?
L_E89E:
        bne     L_E8DF                  ; E89E  no: next ordinal
        lda     side1UnitTypeTable,y    ; E8A0  type byte of AI unit 50+ordinal
        bmi     L_E8DF                  ; E8A3  bit 7 set: destroyed, so it does not count
        inc     scratch1C               ; E8A5  one more live member
        lda     side1UnitColTable,y     ; E8A7  its current column
        clc                             ; E8AA  clear the carry
        adc     scratch18               ; E8AB  into the 16-bit column sum
        sta     scratch18               ; E8AD  store the low byte
        lda     scratch19               ; E8AF  column sum, high byte
        adc     #$00                    ; E8B1  propagate the carry
        sta     scratch19               ; E8B3  store it back
        lda     side1UnitRowTable,y     ; E8B5  its current row
        clc                             ; E8B8  clear the carry
        adc     scratch1A               ; E8B9  into the 16-bit row sum
        sta     scratch1A               ; E8BB  store the low byte
        lda     scratch1B               ; E8BD  row sum, high byte
        adc     #$00                    ; E8BF  propagate the carry
        sta     scratch1B               ; E8C1  store it back
        lda     side1UnitDestColTable,y ; E8C3  the column it has been ordered to
        and     #$3F                    ; E8C6  the coordinate is in bits 0-5; bits 6-7 are order flags
        clc                             ; E8C8  clear the carry
        adc     scratch1E               ; E8C9  into the destination-column sum
        sta     scratch1E               ; E8CB  store the low byte
        bcc     L_E8D1                  ; E8CD  no carry
        inc     scratch1F               ; E8CF  carry into the high byte
L_E8D1:
        lda     side1UnitDestRowTable,y ; E8D1  the row it has been ordered to
        and     #$3F                    ; E8D4  coordinate bits only
        clc                             ; E8D6  clear the carry
        adc     scratch20               ; E8D7  into the destination-row sum
L_E8D9:
        sta     scratch20               ; E8D9  store the low byte
        bcc     L_E8DF                  ; E8DB  no carry
        inc     scratch21               ; E8DD  carry into the high byte
L_E8DF:
        dey                             ; E8DF  next ordinal
        bpl     L_E899                  ; E8E0  down to AI ordinal 0

; ----------------------------------------------------------------------
; Divide the four sums by the member count.  The two positional averages are returned in X and Y
; through the two immediate operands patched below.
; ----------------------------------------------------------------------
        ldx     scratch1C               ; E8E2  member count is the divisor
        beq     L_E916                  ; E8E4  no members: skip the divisions and report zero
        lda     scratch18               ; E8E6  column sum, low
        ldy     scratch19               ; E8E8  column sum, high
        jsr     divideSigned16by8       ; E8EA  average column
        sta     L_E912+1                ; E8ED  self-modify: it becomes the operand of the LDX at $E912
        lda     scratch1A               ; E8F0  row sum, low
L_E8F2:
        ldy     scratch1B               ; E8F2  row sum, high
        ldx     scratch1C               ; E8F4  divisor again (divideSigned16by8 clobbers X)
        jsr     divideSigned16by8       ; E8F6  average row
        sta     L_E914+1                ; E8F9  self-modify: it becomes the operand of the LDY at $E914
        ldx     scratch1C               ; E8FC  divisor again
        lda     scratch1E               ; E8FE  destination-column sum, low
        ldy     scratch1F               ; E900  destination-column sum, high
        jsr     divideSigned16by8       ; E902  average ordered destination column
        sta     scratch1E               ; E905  returned in zp_1E
        ldx     scratch1C               ; E907  divisor again
sub_E909:
        lda     scratch20               ; E909  destination-row sum, low
        ldy     scratch21               ; E90B  destination-row sum, high
        jsr     divideSigned16by8       ; E90D  average ordered destination row
        sta     scratch1F               ; E910  returned in zp_1F, overwriting the sum byte that has just been consumed
L_E912:
        ldx     #$FF                    ; E912  operand patched at $E8ED: X = average column
L_E914:
        ldy     #$FF                    ; E914  operand patched at $E8F9: Y = average row
L_E916:
        lda     scratch1C               ; E916  A = member count, Z set when the group is empty
        rts                             ; E918  caller gets count in A, centre in X/Y, average destination in zp_1E/zp_1F


; ----------------------------------------------------------------------
; orderAiUnitToCell - the trainer's central 'issue a move order' helper.  It does nothing when the
; unit's ordered destination already is the requested cell, so redundant orders never take up room in
; the queue or in the packet.  Otherwise it falls into queueMirroredMoveOrder, which mirrors both
; coordinates (the engine will mirror them back because it believes the command arrived from a remote
; opponent) and queues $80 MOVE UNIT or $81 MOVE GROUP.
; In:  X = target column, A = target row, Y = AI ordinal 0-49 (absolute unit = Y+50)
; Out: nothing when the order is redundant; otherwise $8896 = $80/$81, $8897 = absolute unit index,
;      $8898/$8899 = mirrored column/row, and the command appended to the AI queue at $88A0
; Called from: retreatTrainerComcen ($E262), pushTrainerComcenForward ($E294), issueTrainerGroupOrders
;     ($E4F7)
; and haltAiComcen ($EBEC).
; ----------------------------------------------------------------------
orderAiUnitToCell:
        stx     D_8898                  ; E919  stage the target column as the command's first coordinate
        sta     D_8899                  ; E91C  and the target row as the second
        lda     side1UnitDestColTable,y ; E91F  the column AI unit 50+Y has already been ordered to
        and     #$3F                    ; E922  coordinate bits only; bits 6-7 are order flags
L_E924:
        cmp     D_8898                  ; E924  same column as the new order?
        bne     L_E934                  ; E927  no: the order is worth sending
        lda     side1UnitDestRowTable,y ; E929  the row it has already been ordered to
        and     #$3F                    ; E92C  coordinate bits only
        cmp     D_8899                  ; E92E  same row too?
        bne     L_E934                  ; E931  no: send the order
        rts                             ; E933  the unit is already under exactly this order: queue nothing

L_E934:
        lda     D_8898                  ; E934  target column, ready for the mirror below

; ----------------------------------------------------------------------
; queueMirroredMoveOrder - second half of orderAiUnitToCell.  Mirrors the staged coordinates with
; mirrorMapCoordinate (39 - v), picks command $80 cmdMoveUnit or $81 cmdMoveGroup according to bit 6
; of the unit's flags, converts the AI ordinal into the absolute unit index and tail-jumps into
; queueAiCommand.  Both players see their own side at the bottom of the map, so an incoming command's
; coordinates are always mirrored by the engine; the trainer pre-mirrors so the round trip comes out
; right.
; In:  $8898/$8899 = target column/row, Y = AI ordinal 0-49, $F73A unitFlags indexed by ordinal
; Out: the four bytes $8896-$8899 staged and queued
; Called from: reached only by falling through from orderAiUnitToCell ($E934); the XREF entries
;     $E917/$EA01 belong to the
; modem-driver build that occupies the same addresses.
; ----------------------------------------------------------------------
queueMirroredMoveOrder:
        jsr     mirrorMapCoordinate     ; E937  39 - column: the engine mirrors it back when it 'receives' the packet
        sta     D_8898                  ; E93A  store the mirrored column
        lda     D_8899                  ; E93D  target row
        jsr     mirrorMapCoordinate     ; E940  39 - row
        sta     D_8899                  ; E943  store the mirrored row
        ldx     #$80                    ; E946  command $80 = cmdMoveUnit
        lda     side1UnitFlagsTable,y   ; E948  unitFlags of AI unit 50+Y
        and     #$40                    ; E94B  bit 6 = the unit is a member of an in-game group
        beq     L_E950                  ; E94D  not grouped: keep $80
        inx                             ; E94F  grouped: use $81 = cmdMoveGroup so its formation follows
L_E950:
        stx     D_8896                  ; E950  command byte
        tya                             ; E953  AI ordinal 0-49
        clc                             ; E954  clear the carry
        adc     #$32                    ; E955  plus 50: the AI plays side 1, so its units are 50-99
        sta     msgPickUpPhoneThenPressSpace; E957  first parameter of the command ($8897)
L_E95A:
        jmp     queueAiCommand          ; E95A  append the staged command to the AI order queue


; ----------------------------------------------------------------------
; isPathToPlayerComcenClear - walks a Bresenham line from the current line position (which the caller
; has set to the group centre) to the human player's comcen (unit 49) and reports whether any living
; enemy fighting unit stands on it.  Only human grunts, riders and boomers (types 0-2) block; the AI's
; own units, destroyed units, spies and the comcen do not.  Both exits put the group centre back into
; the line position through loadGroupCentreIntoLinePos.
; In:  zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player
;     comcen cell, map at $F000
; Out: C=1 = the lane to the comcen is open, C=0 = blocked or the group id was invalid; zp_92/zp_93 =
;     the group centre;
;      zp_48/zp_49 (map pointer) and the line-stepper state $8C-$97 clobbered
; Called from: decideTrainerGroupAction ($E3B1), to decide whether the group may make a straight run
;     at the comcen.
; ----------------------------------------------------------------------
isPathToPlayerComcenClear:
        ldy     D_8955                  ; E95D  the group asking for a straight run
L_E960:
        cpy     D_8954                  ; E960  is it one of the live groups?
        bcs     L_E995                  ; E963  no: report 'blocked' and do nothing else
        lda     side0ComcenCol          ; E965  column of the human player's comcen (unit 49)
        sta     lineEndCol              ; E968  line target column
        lda     side0ComcenRow          ; E96A  its row
sub_E96D:
        sta     lineEndRow              ; E96D  line target row
        jsr     initLineStepper         ; E96F  set the walker up from the current position to the comcen

; ----------------------------------------------------------------------
; Walk the lane one cell at a time, stopping at the first live enemy fighting unit.
; ----------------------------------------------------------------------
L_E972:
        jsr     stepLine                ; E972  advance one cell along the lane
sub_E975:
        beq     loadGroupCentreIntoLinePos; E975  the line ran out without a blocker: restore the centre and return C=1
        ldx     lineCurCol              ; E977  column the walker is standing on
        ldy     lineCurRow              ; E979  and its row
L_E97B:
        jsr     readMapCell             ; E97B  map byte of that cell; bit 7 set means a unit occupies it
        bpl     L_E972                  ; E97E  bare terrain: keep walking
        and     #$7F                    ; E980  strip the occupancy bit to get the unit index
L_E982:
        cmp     #$32                    ; E982  one of the AI's own units 50-99?
        bcs     L_E972                  ; E984  yes: a unit never blocks its own side's lane
L_E986:
        tay                             ; E986  human player's unit: index it
        lda     unitTypeTable,y         ; E987  its type byte
        bmi     L_E972                  ; E98A  destroyed: not an obstacle
        and     #$07                    ; E98C  unit type 0-4
        cmp     #$03                    ; E98E  type 3 SPY or 4 COMCEN?
        bcs     L_E972                  ; E990  yes: neither blocks a charge
        jsr     loadGroupCentreIntoLinePos; E992  a live grunt, rider or boomer sits in the lane: put the group centre back
L_E995:
        clc                             ; E995  C=0 = the lane is blocked (also the exit for an invalid group id)
        rts                             ; E996  carry tells the caller whether it may charge


; ----------------------------------------------------------------------
; loadGroupCentreIntoLinePos - copies the current group's centre (groupCentreCol $88EA /
; groupCentreRow $88F6) into the line walker's position and returns with C=1.  It doubles as the 'path
; is clear' exit of isPathToPlayerComcenClear, which is why it ends with SEC.
; In:  $8955 currentAiGroup, $88EA/$88F6 per-group centre tables
; Out: zp_93 = centre column, zp_92 = centre row, C=1; A and Y clobbered
; Called from: isPathToPlayerComcenClear ($E992) and as its C=1 exit ($E975).
; ----------------------------------------------------------------------
loadGroupCentreIntoLinePos:
        ldy     D_8955                  ; E997  the current group
        lda     D_88EA,y                ; E99A  its centre column
        sta     lineCurCol              ; E99D  line walker column
        lda     D_88F6,y                ; E99F  its centre row
L_E9A2:
        sta     lineCurRow              ; E9A2  line walker row
        sec                             ; E9A4  C=1: this is also the 'lane is clear' answer of isPathToPlayerComcenClear
        rts                             ; E9A5  line position restored


; ----------------------------------------------------------------------
; scoreTargetBlock - scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment
; target.  A missile and a drone both cover 2x2 cells, so all four are added up: every unit standing
; in them contributes targetValueByUnitType, positive for the human player's units and negative for
; the AI's own.  A block with a negative total is never chosen; otherwise, when the total beats the
; running best in zp_1C, the corner is remembered in zp_95/zp_94 and the highest human unit index
; found in the block becomes the nominal target in zp_1D (which favours the comcen, unit 49).
; In:  X = column, Y = row of the top-left cell; zp_1C = best score so far (may be negative to accept
;     anything),
;      zp_1D = best target so far; map at $F000, unitTypeTable $F76C
; Out: zp_1A = the block's score, zp_1B = highest human unit index in it; on an improvement zp_1C,
;     zp_1D, zp_94 and zp_95 are
;      updated; zp_48/zp_49 = map pointer; self-modifies the operands at $E9E4 and $E9E8
; Called from: the missile/drone target scan at $EFA8, and from the $EE00 drone-flight overlay.
; ----------------------------------------------------------------------
scoreTargetBlock:
        stx     L_E9E3+1                ; E9A6  self-modify: keep the block's column in the LDA at $E9E3
        sty     L_E9E7+1                ; E9A9  self-modify: keep the block's row in the LDA at $E9E7
        lda     #$00                    ; E9AC  clear the accumulators
        sta     scratch1A               ; E9AE  score of this 2x2 block
        sta     scratch1B               ; E9B0  highest human unit index seen in it
        jsr     readMapCell             ; E9B2  top-left cell; readMapCell also leaves mapCellPtr pointing at it
        bpl     L_E9BA                  ; E9B5  bare terrain
        jsr     addUnitTargetValue      ; E9B7  a unit stands here: score it

; ----------------------------------------------------------------------
; The other three cells of the block are reached straight through mapCellPtr: +1 is the cell to the
; right, +40 the cell below (the map is 40 cells wide) and +41 the diagonal.
; ----------------------------------------------------------------------
L_E9BA:
        ldy     #$01                    ; E9BA  offset +1 = the cell to the right
        lda     (mapCellPtr),y          ; E9BC  read it
        bpl     L_E9C3                  ; E9BE  bare terrain
        jsr     addUnitTargetValue      ; E9C0  score the unit standing there
L_E9C3:
        ldy     #$28                    ; E9C3  offset +40 = one map row down
        lda     (mapCellPtr),y          ; E9C5  read it
        bpl     L_E9CC                  ; E9C7  bare terrain
        jsr     addUnitTargetValue      ; E9C9  score it
L_E9CC:
        ldy     #$29                    ; E9CC  offset +41 = down and to the right
        lda     (mapCellPtr),y          ; E9CE  read it
        bpl     L_E9D5                  ; E9D0  bare terrain
        jsr     addUnitTargetValue      ; E9D2  score it
L_E9D5:
        lda     scratch1A               ; E9D5  total value of the block
        bmi     L_E9F0                  ; E9D7  negative: the AI's own units dominate it, so never bombard it
L_E9D9:
        cmp     scratch1C               ; E9D9  better than the best block found so far?
        bcs     L_E9E1                  ; E9DB  yes: take it
        bit     scratch1C               ; E9DD  no, but is the running best itself still negative (nothing chosen yet)?
        bpl     L_E9F0                  ; E9DF  no: keep the previous best
L_E9E1:
        sta     scratch1C               ; E9E1  new best score
L_E9E3:
        lda     #$FF                    ; E9E3  operand patched at $E9A6: the block's column
        sta     lineEndCol              ; E9E5  remember it as the aim point
L_E9E7:
        lda     #$FF                    ; E9E7  operand patched at $E9A9: the block's row
L_E9E9:
        sta     lineEndRow              ; E9E9  aim point row
        lda     scratch1B               ; E9EB  the biggest human unit index in the block
        sta     scratch1D               ; E9ED  becomes the nominal target unit (49 = the comcen wins any tie)
        rts                             ; E9EF  a new best target has been recorded

L_E9F0:
        rts                             ; E9F0  rejected: the running best is left alone


; targetValueByUnitType - five bytes indexed by unit type, the bombardment value of a unit standing in
; a candidate target block.  Boomers and the comcen are worth four times a grunt.
targetValueByUnitType:
        .byte   $01,$02,$04,$01,$04     ; E9F1  .....  GRUNT 1, RIDER 2, BOOMER 4, SPY 1, COMCEN 4

; ----------------------------------------------------------------------
; addUnitTargetValue - helper of scoreTargetBlock.  Takes a map cell byte that holds $80 plus a unit
; index, ignores destroyed units, and adds targetValueByUnitType[type] to the block score in zp_1A -
; negated when the unit belongs to the AI itself, so the trainer will not shell its own troops.  For a
; human unit it also keeps the largest index seen, which is how the human comcen (unit 49) ends up
; being preferred as the nominal target.
; In:  A = map cell byte with bit 7 set, unitTypeTable $F76C, targetValueByUnitType $E9F1, zp_1A/zp_1B
;     accumulators
; Out: zp_1A += the (possibly negated) value, zp_1B = highest human unit index seen; X and Y clobbered
; Called from: scoreTargetBlock, once per cell of the 2x2 block ($E9B7, $E9C0, $E9C9, $E9D2).
; ----------------------------------------------------------------------
addUnitTargetValue:
        and     #$7F                    ; E9F6  drop the 'a unit is here' bit: what is left is the unit index
        tax                             ; E9F8  index the unit tables with it
        lda     unitTypeTable,x         ; E9F9  its type byte
        bmi     L_EA1A                  ; E9FC  destroyed: worth nothing
        and     #$07                    ; E9FE  unit type 0-4
        tay                             ; EA00  index the value table
        lda     targetValueByUnitType,y ; EA01  bombardment value of that type
        cpx     #$32                    ; EA04  unit 50-99 = one of the AI's own?
        bcs     L_EA10                  ; EA06  yes: its value counts against the block
        cpx     scratch1B               ; EA08  human unit: is it the highest index seen in this block so far?
        bcc     L_EA15                  ; EA0A  no: just add its value
        stx     scratch1B               ; EA0C  yes: it becomes the block's nominal target
        bcs     L_EA15                  ; EA0E  always taken (the BCS above fell through with C=1)
L_EA10:
        eor     #$FF                    ; EA10  negate the value ...
        clc                             ; EA12  clear the carry ...
        adc     #$01                    ; EA13  ... two's complement, so friendly units subtract
L_EA15:
        clc                             ; EA15  clear the carry before accumulating
        adc     scratch1A               ; EA16  add into the block score
        sta     scratch1A               ; EA18  store it back
L_EA1A:
        rts                             ; EA1A  cell scored


; ----------------------------------------------------------------------
; offsetCoordinateClamped - returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps
; of the signed step X, held inside the 40-cell battlefield, with the carry reporting whether the
; unclamped value had left the map.  The base coordinate and the caller's step count are both parked
; in immediate operands so nothing but A is disturbed.
; 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 negative or past 39, Y unchanged;
;      self-modifies the operands at $EA27 and $EA36
; Called from: pushTrainerComcenForward/retreatTrainerComcen ($E24C/$E25B/$E27E/$E28D), the path
;     search ($E48A/$E499)
; and offsetPointInDirection ($E675/$E684).
; ----------------------------------------------------------------------
offsetCoordinateClamped:
        sty     L_EA35+1                ; EA1B  self-modify: save the step count in the LDY at $EA35 so Y survives the multiply
        sta     L_EA26+1                ; EA1E  self-modify: the base coordinate becomes the operand of the ADC at $EA26
        txa                             ; EA21  A = the signed step, 0, +1 or $FF
        jsr     multiplyAbyY            ; EA22  A = step * number of steps
        clc                             ; EA25  clear the carry
L_EA26:
        adc     #$FF                    ; EA26  operand patched at $EA1E: add the base coordinate
        tay                             ; EA28  keep the raw, unclamped result for the range test
L_EA29:
        bpl     L_EA2D                  ; EA29  positive: no low clamp needed
        lda     #$00                    ; EA2B  walked off the top or left edge: clamp to 0
L_EA2D:
        cmp     #$28                    ; EA2D  past the last cell (40)?
        bcc     L_EA33                  ; EA2F  no, keep it
        lda     #$27                    ; EA31  yes: clamp to 39
L_EA33:
        cpy     #$28                    ; EA33  C=1 when the raw value was 40 or more, which also covers a negative value (>= $80)
L_EA35:
        ldy     #$FF                    ; EA35  operand patched at $EA1B: hand the caller its step count back
        rts                             ; EA37  A = clamped coordinate, C=1 if it had to be clamped


; ----------------------------------------------------------------------
; mirrorBlockCornerForOpponent - converts the top-left corner of a 2x2 object (the drone) into the
; coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn
; back into that same corner: X = 39-(X+1), Y = 39-(Y+1). Mirroring alone would give the bottom-right
; corner of the mirrored block, hence the pre-increment.
; In:  X = column, Y = row of the block's top-left cell
; Out: X, Y = the mirrored corner; A clobbered
; Called from: the fake drone-position packet at $EEB1 and $EF8F, and from the drone-flight overlay.
; ----------------------------------------------------------------------
mirrorBlockCornerForOpponent:
        inx                             ; EA38  the engine subtracts 1 after mirroring, so add it here first
        txa                             ; EA39  column+1 into A
        jsr     mirrorMapCoordinate     ; EA3A  39 - (column+1)
L_EA3D:
        tax                             ; EA3D  mirrored column back into X
        iny                             ; EA3E  same for the row
        tya                             ; EA3F  row+1 into A
        jsr     mirrorMapCoordinate     ; EA40  39 - (row+1)
        tay                             ; EA43  mirrored row back into Y
        rts                             ; EA44  corner is now expressed the way a remote opponent would have sent it


; aiCmdQueuePeak - a single byte of data sitting in the code stream between
; mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI's order queue.  It is
; written and compared only inside queueAiCommand and read nowhere else, so it is a development
; leftover.
aiCmdQueuePeak:
        .byte   $00                     ; EA45  .  longest the AI order queue has ever grown this game; nothing ever reads it back

; ----------------------------------------------------------------------
; queueAiCommand - appends the command staged in $8896..$8899 to the AI's 50-byte order queue at
; $88A0.  It looks the total command length up with getCommandLength, keeps a high-water mark in
; aiCmdQueuePeak, and when the queue would reach 50 bytes it drops the command and turns the border
; blue instead, as a visible warning that the AI is producing orders faster than the lock-step packets
; can carry them away.
; In:  $8896-$8899 = the staged command, $8952 aiCmdQueueLength, commandLengthTable through
;     getCommandLength ($4D9F)
; Out: queue at $88A0 extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the
;     copy counter),
;      $EA45 peak updated; on overflow $9003 (the border-colour shadow) = 6 and the command is thrown
; away
; Called from: queueMirroredMoveOrder ($E95A) and queueComcenCommand ($EBE0); the trainer setup
;     overlay has its own
; call at its own $EC82.
; ----------------------------------------------------------------------
queueAiCommand:
        inc     D_8968                  ; EA46  count one more command issued this round
        lda     D_8896                  ; EA49  the staged command byte
        jsr     getCommandLength        ; EA4C  currentCommandLength ($92EE) = 1 command byte plus its parameters
        clc                             ; EA4F  clear the carry
        adc     D_8952                  ; EA50  how long the queue would be afterwards
        cmp     aiCmdQueuePeak          ; EA53  beyond the previous high-water mark?
        bcc     L_EA5B                  ; EA56  no
        sta     aiCmdQueuePeak          ; EA58  record the new peak
L_EA5B:
        cmp     #$32                    ; EA5B  the queue at $88A0 holds 50 bytes
        bcc     L_EA65                  ; EA5D  it still fits
        lda     #$06                    ; EA5F  colour 6 = blue
        sta     vicBorderShadow         ; EA61  overflow: recolour the border through the VIC shadow and drop the command
        rts                             ; EA64  command lost (the round counter at $8968 was already bumped)

L_EA65:
        ldx     #$00                    ; EA65  copy from the first staging byte
        ldy     D_8952                  ; EA67  append at the current end of the queue
L_EA6A:
        lda     D_8896,x                ; EA6A  next staged byte
        sta     D_88A0,y                ; EA6D  into the queue
        inx                             ; EA70  advance the staging index
        iny                             ; EA71  advance the queue index
        inc     D_8952                  ; EA72  the queue is one byte longer
        dec     currentCommandLength    ; EA75  one byte of this command done
        bne     L_EA6A                  ; EA78  until the whole command has been copied
        rts                             ; EA7A  the order is now waiting to be handed to the engine


; ----------------------------------------------------------------------
; injectAiCommandsIntoPacket - the heart of the solo trainer.  Where the modem build would decode a
; packet that arrived over the wire, this build fabricates one: it copies whole commands out of the
; AI's own queue at $88A0 straight into the comm module's received-packet buffer ($E020, length
; $E01E), so the lock-step engine executes the computer opponent's orders through exactly the same
; command interpreter a human opponent's orders would use.  It first runs the missile/drone decision,
; which may claim the whole packet for itself, and then moves as many complete commands as fit while
; the packet stays under 8 bytes, shifting the remainder of the queue down after each one.
; In:  $8952 aiCmdQueueLength, the queue at $88A0, $E01E current packet length, getCommandLength
;     ($4D9F)
; Out: $E020.. filled, $E01E updated, the queue compacted and $8952 reduced; $8966 cleared then
;     possibly set by runAiMissileTurn;
;      $92EE clobbered
; Called from: buildTrainerReplyPacket ($E0E4), once per lock-step exchange.
; ----------------------------------------------------------------------
injectAiCommandsIntoPacket:
        lda     #$00                    ; EA7B  clear the flag first
        sta     D_8966                  ; EA7D  $8966 bit 7 will mean 'the missile/drone code owns this packet'
        jsr     runAiMissileTurn        ; EA80  offer the AI a missile shot or a drone launch; it writes straight into the packet
        lda     D_8966                  ; EA83  did it claim the packet?
        bmi     L_EACE                  ; EA86  yes: leave the order queue for the next exchange
        lda     D_8952                  ; EA88  is anything queued at all?
        beq     L_EACE                  ; EA8B  no: send an empty packet

; ----------------------------------------------------------------------
; One iteration per queued command: measure it, check that it still fits in the packet, copy it into
; the packet, then slide the rest of the queue down over it.
; ----------------------------------------------------------------------
L_EA8D:
        ldy     #$00                    ; EA8D  read from the front of the queue
        lda     D_88A0,y                ; EA8F  its command byte
        jsr     getCommandLength        ; EA92  currentCommandLength = its total length
        lda     rxPacketLength          ; EA95  bytes already in the fabricated 'received' packet
        clc                             ; EA98  clear the carry
        adc     currentCommandLength    ; EA99  plus this command
        cmp     #$08                    ; EA9C  a packet carries at most 7 command bytes
        bcs     L_EACE                  ; EA9E  it would not fit: the rest waits for the next exchange
D_EAA0:
        ldx     rxPacketLength          ; EAA0  append at the current end of the packet buffer
L_EAA3:
        lda     D_88A0,y                ; EAA3  next byte of the command
        sta     rxPacketBuffer,x        ; EAA6  into the received-packet buffer at $E020
        inx                             ; EAA9  advance the packet index
        iny                             ; EAAA  advance the queue index
        cpy     currentCommandLength    ; EAAB  whole command copied?
        bcc     L_EAA3                  ; EAAE  no: keep copying
        stx     rxPacketLength          ; EAB0  new packet length
        ldx     #$00                    ; EAB3  now compact the queue from its start
L_EAB5:
        lda     D_88A0,y                ; EAB5  byte just past the command that was taken
D_EAB8:
        sta     D_88A0,x                ; EAB8  slide it down to the front
D_EABB:
        inx                             ; EABB  advance the write index
D_EABC:
        iny                             ; EABC  advance the read index
D_EABD:
        cpy     D_8952                  ; EABD  until the old end of the queue
D_EAC0:
        bcc     L_EAB5                  ; EAC0  keep shifting
D_EAC2:
        lda     D_8952                  ; EAC2  old queue length
D_EAC5:
        sec                             ; EAC5  prepare the subtraction
D_EAC6:
        sbc     currentCommandLength    ; EAC6  minus the command just handed over
        sta     D_8952                  ; EAC9  new queue length
        bne     L_EA8D                  ; EACC  more orders waiting: try to fit another into the same packet
L_EACE:
        rts                             ; EACE  the packet the engine is about to 'receive' is ready


; ----------------------------------------------------------------------
; updateAiUnitTracking - per-unit hook, run from trainerUnitStepHook after the engine has stepped each
; unit.  It maintains three pieces of AI awareness.  For a human unit it latches the first
; grunt/rider/boomer that has picked the AI comcen as its fire target into comcenAttackerUnit ($8972).
; For one of its own units it marks that unit's group as having the human comcen in reach (groupFlags
; bit 6) and stamps the unit for an immediate replan, once the comcen has been spotted and the unit is
; within 8 cells of it.  Finally, an AI spy (units 96-98) that has reached the opponent's end zone
; while still inside an ordinary group is split off into a brand new group of its own, seeded with its
; current destination, so it can operate alone.
; In:  $90FA currentUnit, $F76C unitTypeTable, $91F0 fireTargetUnit, $F9F5 human comcen energy byte
;     (bit 7 = spotted),
;      $F671/$F6D5 human comcen cell, $F640/$F6A4 unit cells, aiUnitOrderRound ($87CE+unit),
; aiUnitGroup ($8800+unit),
;      aiUnitDeepFlag ($8832+unit), $91CB game clock, $8954 aiGroupCount, $8964 home group id
; Out: $8972 comcenAttackerUnit, groupFlags $893E bit 6, $8953 last allocated group id, the unit's
;     group and round stamp,
;      and the new group's target cell $88D2/$88DE
; Called from: trainerUnitStepHook ($E124), for every unit the engine steps.
; ----------------------------------------------------------------------
updateAiUnitTracking:
        ldy     currentUnit             ; EACF  the unit the engine has just stepped
        cpy     #$32                    ; EAD2  unit 0-49 or 50-99?
        bcs     L_EAF1                  ; EAD4  one of the AI's own: run the contact tracking
        ldx     D_8972                  ; EAD6  comcenAttackerUnit, $FF while nobody is known to be shooting at the AI comcen
        bpl     L_EAEE                  ; EAD9  already latched: nothing to learn
        lda     fireTargetUnit          ; EADB  the unit this human unit has picked as its fire target
        cmp     #$63                    ; EADE  unit 99 = the AI comcen
        bne     L_EAEE                  ; EAE0  aiming at something else
        lda     unitTypeTable,y         ; EAE2  its type byte
        and     #$07                    ; EAE5  unit type 0-4
        cmp     #$03                    ; EAE7  type 3 SPY or 4 COMCEN?
        bcs     L_EAEE                  ; EAE9  yes: they cannot shoot, so they are not a threat to record
D_EAEB:
        sty     D_8972                  ; EAEB  remember the first unit that is aiming at the AI comcen
L_EAEE:
        jmp     L_EB66                  ; EAEE  human unit handled


; ----------------------------------------------------------------------
; AI unit: has it got close enough to the human comcen to count as contact?  The two coordinate tests
; accept a difference in -8..+7 by comparing the signed byte against $F8 and then against 8.
; ----------------------------------------------------------------------
L_EAF1:
        lda     side0ComcenEnergy       ; EAF1  energy byte of the human comcen; bit 7 doubles as 'the AI has spotted it'
        bpl     L_EB2C                  ; EAF4  not spotted yet: there is no contact to record
        lda     side0ComcenCol          ; EAF6  column of the human comcen
        sec                             ; EAF9  prepare the subtraction
        sbc     unitColTable,y          ; EAFA  minus this AI unit's column
        cmp     #$F8                    ; EAFD  is the difference at least -8?
D_EAFF:
        bcs     L_EB05                  ; EAFF  yes, within 8 columns to one side
D_EB01:
        cmp     #$08                    ; EB01  is it less than +8?
sub_EB03:
        bcs     L_EB2C                  ; EB03  no: more than 8 columns away
L_EB05:
        lda     side0ComcenRow          ; EB05  row of the human comcen
        sec                             ; EB08  prepare the subtraction
        sbc     unitRowTable,y          ; EB09  minus this AI unit's row
        cmp     #$F8                    ; EB0C  is the difference at least -8?
        bcs     L_EB14                  ; EB0E  yes
        cmp     #$08                    ; EB10  is it less than +8?
        bcs     L_EB2C                  ; EB12  no: more than 8 rows away
L_EB14:
        ldx     msgGetOpponentOnPhoneAnd,y; EB14  aiUnitGroup[unit], the group table addressed by absolute index
        cpx     D_8964                  ; EB17  is it the home/guard group?
        beq     L_EB66                  ; EB1A  yes: that group is not supposed to chase the comcen
        lda     D_893E,x                ; EB1C  its groupFlags
        ora     #$40                    ; EB1F  bit 6 = this group has the human comcen within reach
        sta     D_893E,x                ; EB21  store the flags back
        bmi     L_EB2C                  ; EB24  bit 7 set (the group is already suspended): do not restamp its clock
        lda     gameClock               ; EB26  the down-counting round clock
        sta     msgWeWonByAKnockout,y   ; EB29  aiUnitOrderRound[unit]: stamping the current clock makes the planner replan this unit at once  (also mapGenerator6F00:msgWeWonByAKnockout)

; ----------------------------------------------------------------------
; An AI spy (units 96-98) that has scored both end zones is promoted out of its group into a group of
; its own so it can act independently behind the human lines.
; ----------------------------------------------------------------------
L_EB2C:
        cpy     #$63                    ; EB2C  the AI comcen itself (unit 99)?
        bcs     L_EB66                  ; EB2E  yes: nothing else to do for it
        cpy     #$60                    ; EB30  units 96-98 are the AI's three SPYs
        bcc     L_EB66                  ; EB32  any other unit: done
        lda     D_8832,y                ; EB34  aiUnitDeepFlag[unit], bit 7 = the unit has scored both territory zones
        bpl     L_EB66                  ; EB37  this spy has not got that far yet
        lda     msgGetOpponentOnPhoneAnd,y; EB39  its current group id
        cmp     D_8954                  ; EB3C  does it still belong to one of the ordinary groups?
        bcs     L_EB66                  ; EB3F  no (already split off, or $FF): leave it alone
        cmp     D_8964                  ; EB41  is it in the home/guard group?
        beq     L_EB66                  ; EB44  yes: that one stays together
        inc     D_8953                  ; EB46  allocate the next group id; note that aiGroupCount $8954 is not bumped with it, and that ids past 11 would run off the end of the twelve-entry per-group tables
        lda     D_8953                  ; EB49  the new id
        tax                             ; EB4C  index the per-group tables with it
        sta     msgGetOpponentOnPhoneAnd,y; EB4D  the spy becomes a group of its own
        lda     gameClock               ; EB50  the current round
        sta     msgWeWonByAKnockout,y   ; EB53  stamp it so the planner picks the new group up on the next scan  (also mapGenerator6F00:msgWeWonByAKnockout)
        lda     unitDestColTable,y      ; EB56  the destination the spy is already heading for
        and     #$3F                    ; EB59  coordinate bits only
        sta     D_88D2,x                ; EB5B  becomes the new group's target column
        lda     unitDestRowTable,y      ; EB5E  and its destination row
        and     #$3F                    ; EB61  coordinate bits only
        sta     D_88DE,x                ; EB63  becomes the new group's target row
L_EB66:
        rts                             ; EB66  unit tracked


; ----------------------------------------------------------------------
; runAiComcenActions - spends whatever is left of the AI comcen's three command slots for this round.
; A stunned comcen needs two of them: one to halt itself and one for the 1-in-8 repair attempt that
; queues command $9E, exactly the odds the human REPAIR tab uses.  An unstunned comcen is halted once
; and then spends every remaining slot on maybeQueueComcenEnergyRepair.
; In:  $8968 commands already issued this round, $8971 the comcen's stun counter (low nibble of
;     unitStunTable[99])
; Out: $8967 = the comcen's remaining slot budget, commands queued through queueAiCommand; returns
;     silently when there is
;      no budget left
; Called from: planTrainerRound at $E1CD (comcen too weak to attack) and $E21B (defend mode).
; ----------------------------------------------------------------------
runAiComcenActions:
        lda     #$03                    ; EB67  the comcen may spend at most 3 command slots in a round
        sec                             ; EB69  prepare the subtraction
        sbc     D_8968                  ; EB6A  minus the orders the planner has already queued
        bcc     L_EBE3                  ; EB6D  already over budget: do nothing at all
        sta     D_8967                  ; EB6F  slots left for the comcen
        tax                             ; EB72  keep the budget in X as well
        lda     D_8971                  ; EB73  the comcen's stun counter
        beq     L_EB96                  ; EB76  not stunned: go straight to the energy repairs
        cpx     #$02                    ; EB78  a stun repair attempt costs two slots
        bcc     L_EBE3                  ; EB7A  not enough left
        dex                             ; EB7C  spend one ...
        dex                             ; EB7D  ... and the second
        stx     D_8967                  ; EB7E  the reduced budget
        jsr     haltAiComcen            ; EB81  halt the comcen; a moving comcen cannot be worked on
        jsr     nextGameRandom          ; EB84  step the lock-step RNG
        and     #$07                    ; EB87  one chance in eight
        bne     L_EBE3                  ; EB89  the repair attempt failed this round
        lda     #$9E                    ; EB8B  command $9E = cmdComcenRepairedOrSetupReady, which clears the comcen stun counter on both machines
        jsr     queueComcenCommand      ; EB8D  queue it ($8897/$8898 are ignored, the command is one byte long)
        inc     D_8967                  ; EB90  cancel the DEC that the shared loop tail below is about to do
        jmp     L_EB9F                  ; EB93  join the energy-repair loop with the remaining budget

L_EB96:
        txa                             ; EB96  slots left
        beq     L_EBE3                  ; EB97  none: nothing to do
        jsr     haltAiComcen            ; EB99  halt the comcen so it can repair
L_EB9C:
        jsr     maybeQueueComcenEnergyRepair; EB9C  spend one slot on an energy repair
L_EB9F:
        dec     D_8967                  ; EB9F  one slot used
        bne     L_EB9C                  ; EBA2  keep spending until the budget is exhausted
        rts                             ; EBA4  the comcen has done what it can this round


; ----------------------------------------------------------------------
; maybeQueueComcenEnergyRepair - the trainer's version of the human console's energy repair.  With 50
; percent probability, and only while the AI comcen's energy is below 45 of 50, it queues command $9A
; cmdAddEnergy for unit 99 with the same amount the human repair uses, (50 - energy) / 8 + 1.  The
; projected energy is accumulated in $8965 (which planTrainerRound seeded from the comcen's real
; energy and uses as its action budget), and the amount is nudged by one whenever that projection
; would land on a multiple of four.
; In:  $FA27 = unitEnergyTable[99] (energy in bits 0-5), $8965 projected comcen energy, nextGameRandom
; Out: $8896 = $9A, $8897 = $63 (unit 99), $8898 = the amount, command queued; $8965 raised by the
;     same amount
; Called from: runAiComcenActions ($EB9C), once per remaining comcen command slot.
; ----------------------------------------------------------------------
maybeQueueComcenEnergyRepair:
        jsr     nextGameRandom          ; EBA5  step the lock-step RNG
        and     #$01                    ; EBA8  an energy repair succeeds one time in two, as on the human console
        beq     L_EBE3                  ; EBAA  failed this time: spend the slot on nothing
        lda     side1ComcenEnergy       ; EBAC  energy byte of AI comcen unit 99
        and     #$3F                    ; EBAF  energy 0-50 lives in bits 0-5; bits 6-7 are visibility flags
        cmp     #$2D                    ; EBB1  45 out of 50 or better?
        bcs     L_EBE3                  ; EBB3  healthy enough: do not waste a command on it
        sta     scratch18               ; EBB5  keep the current energy
        lda     #$32                    ; EBB7  50 = a full comcen
        sec                             ; EBB9  prepare the subtraction
        sbc     scratch18               ; EBBA  the shortfall
        lsr     a                       ; EBBC  divide by 8 ...
        lsr     a                       ; EBBD  ...
        lsr     a                       ; EBBE  ... so repairs get smaller as the comcen recovers
        clc                             ; EBBF  clear the carry
        adc     #$01                    ; EBC0  amount = shortfall/8 + 1
        sta     D_8898                  ; EBC2  second parameter of the command
        clc                             ; EBC5  clear the carry
        adc     D_8965                  ; EBC6  add it to the energy the comcen is projected to end up with
        sta     D_8965                  ; EBC9  keep the projection
        and     #$03                    ; EBCC  did the projection land on a multiple of four?
        bne     L_EBD6                  ; EBCE  no: the amount stands
        inc     D_8898                  ; EBD0  yes: repair one point more so it does not
        inc     D_8965                  ; EBD3  and keep the projection in step with it
L_EBD6:
        lda     #$63                    ; EBD6  unit 99 = the AI comcen
        sta     msgPickUpPhoneThenPressSpace; EBD8  first parameter of the command ($8897)
        lda     #$9A                    ; EBDB  command $9A = cmdAddEnergy (unit, amount); falls straight into queueComcenCommand

; ----------------------------------------------------------------------
; queueComcenCommand - shared tail of the comcen actions: stores A as the command byte and jumps into
; queueAiCommand, leaving
; $8897 and $8898 as the caller staged them.
; In:  A = command byte, $8897/$8898 = parameters already staged
; Out: the command appended to the AI order queue
; Called from: runAiComcenActions ($EB8D) and by falling through from maybeQueueComcenEnergyRepair
;     ($EBDB).
; ----------------------------------------------------------------------
queueComcenCommand:
        sta     D_8896                  ; EBDD  command byte into the staging area
        jmp     queueAiCommand          ; EBE0  append the staged bytes to the queue

L_EBE3:
        rts                             ; EBE3  shared 'do nothing this slot' exit of the comcen action code


; ----------------------------------------------------------------------
; haltAiComcen - orders the AI comcen to move to the cell it is already standing on.
; orderAiUnitToCell turns that into a real
; $80/$81 command only when the comcen currently has some other destination, so this stops a moving
; comcen and costs nothing when
; it is already still.  A comcen has to be stationary before it can repair or launch.
; In:  $F6A3 unitCol[99], $F707 unitRow[99]
; Out: possibly one $80/$81 command queued; nothing at all when the comcen is already halted
; Called from: runAiComcenActions ($EB81 stunned path, $EB99 repair path).
; ----------------------------------------------------------------------
haltAiComcen:
        ldx     side1ComcenCol          ; EBE4  the column the AI comcen is standing on
        lda     side1ComcenRow          ; EBE7  and its row
        ldy     #$31                    ; EBEA  AI ordinal 49 = absolute unit 99, the AI comcen
        jmp     orderAiUnitToCell       ; EBEC  ordering it to its own cell halts it; a redundant order is dropped


; ----------------------------------------------------------------------
; assessThreatAroundAiComcen - builds a directional pressure map around the AI comcen and picks the
; direction to run.  Every living human combat unit (ordinals 0-45: the 28 grunts, 12 riders and 6
; boomers) within 20 cells adds a weight into one of eight compass buckets at $8896-$889D: 1 for
; merely being nearby, +1 within 10 cells, and +4 more when the comcen is inside that unit's weapon
; range (5 cells, 9 for a boomer).  The base 1 is dropped once the comcen has actually been hit
; ($897C), so only units that can reach it steer the escape.  The tail rankThreatDirections ($EC82,
; just past this chunk) smooths the ring and returns the centre of the quietest three-direction arc.
; In:  zp_27/zp_28 = AI comcen column/row (set by the caller at $E1D1),
;     unitTypeTable/unitColTable/unitRowTable,
;      $897C = 'the comcen was hit this round or last', zp_57 game RNG low byte for the tie break
; Out: A = (total pressure - number of threats) / 2 = overall threat level, X = deepest weapon-range
;     surplus (0 = nothing in
;      range), Y = safest compass direction 0-7, or 8 (the null delta entry) when nothing threatens;
;      $8896-$889D and $0400-$0407 hold the direction buckets, zp_18-zp_23 clobbered
; Called from: planTrainerRound ($E1DB), which files the results into $8962/$8960/$8961, and the drone
;     code ($EF7B).
; ----------------------------------------------------------------------
assessThreatAroundAiComcen:
        lda     #$00                    ; EBEF  clear the accumulators
        sta     scratch1F               ; EBF1  total enemy pressure
        sta     scratch20               ; EBF3  number of units that count as a threat
        sta     scratch21               ; EBF5  deepest weapon-range surplus seen
        sta     scratch1E               ; EBF7  cleared but never read again in either half of the routine: a leftover
        ldy     #$07                    ; EBF9  clear the eight compass buckets, 7 down to 0
L_EBFB:
        sta     D_8896,y                ; EBFB  bucket for direction Y
D_EBFE:
        dey                             ; EBFE  next direction
        bpl     L_EBFB                  ; EBFF  until all eight are clear

; ----------------------------------------------------------------------
; One pass per human combat unit, ordinal 45 down to 0 (the roster is 28 grunts, 12 riders, 6 boomers,
; 3 spies and the comcen, so 0-45 is exactly the units that can shoot).
; ----------------------------------------------------------------------
        lda     #$2D                    ; EC01  scan the human player's units 45 down to 0
        sta     scratch18               ; EC03  loop counter
L_EC05:
        ldx     scratch18               ; EC05  the unit being weighed
        lda     unitTypeTable,x         ; EC07  its type byte
D_EC0A:
        bmi     L_EC74                  ; EC0A  destroyed: skip it
        ldy     #$05                    ; EC0C  weapon range 5 cells for a grunt or rider
        and     #$02                    ; EC0E  bit 1 of the type marks a BOOMER (type 2); a SPY would match too, but the scan never reaches one
        beq     L_EC14                  ; EC10  not a boomer: keep range 5
L_EC12:
        ldy     #$09                    ; EC12  a boomer reaches 9 cells
L_EC14:
        sty     scratch19               ; EC14  weapon range of this unit
        lda     unitColTable,x          ; EC16  its column
        sta     cellColA                ; EC19  point A of the distance test
        sta     lineEndCol              ; EC1B  and the target cell of the direction test
        lda     unitRowTable,x          ; EC1D  its row
        sta     cellRowA                ; EC20  point A row
        sta     lineEndRow              ; EC22  target row
        jsr     cellDistance            ; EC24  distance from the AI comcen, which the caller left in cellColB/cellRowB
        cmp     #$14                    ; EC27  20 cells
        bcs     L_EC74                  ; EC29  too far away to matter
sub_EC2B:
        sta     scratch1A               ; EC2B  keep the distance
        lda     cellColB                ; EC2D  the comcen's own cell ...
        sta     lineCurCol              ; EC2F  ... is where the direction is measured from
        lda     cellRowB                ; EC31  comcen row
        sta     lineCurRow              ; EC33  line walker row
        jsr     getStepDirection        ; EC35  Y = compass direction from the comcen toward this unit
        bpl     L_EC3C                  ; EC38  a real direction 0-7
        ldy     #$00                    ; EC3A  the unit is standing on the comcen's own cell: charge it to direction 0

; ----------------------------------------------------------------------
; Weigh this unit: a base 1 for being nearby (suppressed once the comcen is actually under fire), +1
; within 10 cells and +4 more when the comcen sits inside its weapon range.
; ----------------------------------------------------------------------
L_EC3C:
        inc     scratch20               ; EC3C  one more threatening unit
        ldx     #$00                    ; EC3E  the weight this unit will contribute
        bit     D_897C                  ; EC40  was the comcen hit in this round or the previous one?
        bmi     L_EC46                  ; EC43  yes: only units that can actually reach it get any weight
        inx                             ; EC45  no: every nearby enemy is worth 1
L_EC46:
        lda     scratch1A               ; EC46  the distance again
        cmp     #$0A                    ; EC48  closer than 10 cells?
        bcs     L_EC5E                  ; EC4A  no: it only counts as a distant presence
        inx                             ; EC4C  +1 for being close
        lda     scratch19               ; EC4D  its weapon range
        sec                             ; EC4F  prepare the subtraction
        sbc     scratch1A               ; EC50  range minus distance: how deep the comcen sits inside its envelope
        bcc     L_EC5E                  ; EC52  the comcen is out of range of this unit
sub_EC54:
        inx                             ; EC54  +1 ...
        inx                             ; EC55  +2 ...
        inx                             ; EC56  +3 ...
        inx                             ; EC57  +4 more: the comcen is inside this unit's weapon range
sub_EC58:
        cmp     scratch21               ; EC58  is this the deepest penetration so far?
L_EC5A:
        bcc     L_EC5E                  ; EC5A  no
        sta     scratch21               ; EC5C  remember the worst range surplus, returned in X

; ----------------------------------------------------------------------
; Add the weight to its compass bucket and to the running total, both saturating at 255.
; ----------------------------------------------------------------------
L_EC5E:
        txa                             ; EC5E  the weight of this unit
        clc                             ; EC5F  clear the carry
        adc     D_8896,y                ; EC60  into the bucket of its compass direction
        bcc     L_EC67                  ; EC63  no overflow
        lda     #$FF                    ; EC65  saturate the bucket at 255
L_EC67:
        sta     D_8896,y                ; EC67  store the bucket back
        txa                             ; EC6A  the weight again
        clc                             ; EC6B  clear the carry
L_EC6C:
        adc     scratch1F               ; EC6C  into the overall pressure total
        bcc     L_EC72                  ; EC6E  no overflow
        lda     #$FF                    ; EC70  saturate the total at 255
L_EC72:
        sta     scratch1F               ; EC72  store it back
L_EC74:
        dec     scratch18               ; EC74  next human unit
        bpl     L_EC05                  ; EC76  until unit 0 has been weighed

; ----------------------------------------------------------------------
; Hand the ring over to rankThreatDirections, or - when nothing at all threatened the comcen - report
; the null direction 8, whose entry in dirToColDelta/dirToRowDelta is the zero step.
; ----------------------------------------------------------------------
        lda     scratch20               ; EC78  did anything threaten the comcen?
        bne     rankThreatDirections    ; EC7A  yes: smooth the ring and pick the quietest arc
        tax                             ; EC7C  no threats: A = X = 0
        ldy     #$08                    ; EC7D  direction 8 = the null entry of the direction tables, i.e. 'no need to move'
        jmp     threatScanDone          ; EC7F  share the RTS at the end of rankThreatDirections


; ----------------------------------------------------------------------
; rankThreatDirections - Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just
; turned every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one
; of the eight compass buckets $8896-$889D. This half blurs that ring circularly, finds the centre of
; the quietest three-direction arc and returns the summary the planner stores in $8960-$8962.
; In:  $8896-$889D = raw per-direction pressure, zp_1F = total pressure, zp_20 = number of units that
;     contributed, zp_21 = deepest weapon-range penetration, zp_57 = low byte of the lock-step game
;     RNG state (tie break)
; Out: A = (zp_1F - zp_20) / 2 = overall threat level, X = zp_21, Y = zp_18 = the quietest compass
;     direction 0-7; $8896-$889D holds the blurred ring and $0400-$0407 the raw copy; zp_18, zp_19,
;     zp_1A and zp_23 clobbered
; Called from: branch at $EC7A only (the tail of $EBEF); when no unit threatened at all the path at
;     $EC7C returns A=0, X=0, Y=8 instead - index 8 is the deliberate null entry (0,0) of
;     dirToColDelta/dirToRowDelta
; Take an unsmoothed copy of the eight direction buckets into the $0400 scratch page, because the blur
; below overwrites $8896-$889D while it still needs each direction's original neighbours.
; 
; A warning for readers of the rest of this chunk: $EC00-$EFFF is shared by four different overlay
; images (this trainer tail, the modem driver, the track 29 trainer setup and the track 29 drone AI),
; and XREF merges their cross references.  Many of the L_/D_/sub_ labels below are branch targets of
; one of the other builds that happen to land on an instruction boundary here; in this file they sit
; in the middle of a routine and nothing reaches them.
; ----------------------------------------------------------------------
rankThreatDirections:
        ldy     #$07                    ; EC82  start at direction 7 (north-west) and walk the ring backwards
copyThreatRingLoop:
        lda     D_8896,y                ; EC84  raw pressure of direction Y from the 8-byte ring at $8896
        sta     checksumXorConstant,y   ; EC87  keep the untouched copy in the $0400 scratch page (the same page finishRound uses for scan lists)  (also trainerPlaybook0200:play3Col12)
        dey                             ; EC8A  next direction down
        bpl     copyThreatRingLoop      ; EC8B  until all 8 directions have been copied
        lda     #$07                    ; EC8D  the blur pass also starts at direction 7
        sta     scratch23               ; EC8F  zp_23 = the direction currently being smoothed

; ----------------------------------------------------------------------
; smoothThreatRing - Circular 1-2-1 blur of the eight-direction threat ring. For i = 7 down to 0 it
; computes (2*raw[i] + raw[i-1] + raw[i+1]) / 4, reading the untouched copy from $0400 and writing the
; result back over $8896+i. Indices wrap modulo 8, so north's neighbours are north-west and
; north-east.
; In:  $0400-$0407 = raw direction weights, zp_23 = direction index counter (7 on entry)
; Out: $8896-$889D = blurred weights; zp_19 = the sum's high bits; zp_23 ends at $FF; A/X/Y clobbered
; Called from: nothing - this is the loop head of rankThreatDirections, branched to from the DEC/BPL
;     at $ECCA. (XREF shows a JSR from $7AE9, but that call belongs to the other overlay that shares
;     these addresses.)
; ----------------------------------------------------------------------
smoothThreatRing:
        lda     scratch23               ; EC91  i = the direction being smoothed
L_EC93:
        tax                             ; EC93  X indexes the centre sample
        tay                             ; EC94  Y will index the neighbour on the counter-clockwise side ...
        dey                             ; EC95  ... i - 1
        bpl     blurSumStart            ; EC96  no wrap needed while i-1 is still 0..7
        ldy     #$07                    ; EC98  i was 0, so its counter-clockwise neighbour is direction 7
blurSumStart:
        lda     #$00                    ; EC9A  clear the two extra bits the weighted sum can produce
L_EC9C:
        sta     scratch19               ; EC9C  zp_19 = bits 8-9 of the running sum
        lda     checksumXorConstant,x   ; EC9E  raw pressure of the centre direction  (also trainerPlaybook0200:play3Col12)
        asl     a                       ; ECA1  double it - the centre direction carries weight 2
        bcc     addCounterClockwiseNeighbour; ECA2  no carry out of the doubling
        inc     scratch19               ; ECA4  carry the 9th bit into zp_19
addCounterClockwiseNeighbour:
        clc                             ; ECA6  clear carry before adding a neighbour
        adc     checksumXorConstant,y   ; ECA7  + the counter-clockwise neighbour, weight 1  (also trainerPlaybook0200:play3Col12)
        bcc     stepToClockwiseNeighbour; ECAA  no carry out of the add
        inc     scratch19               ; ECAC  carry into the high bits again
stepToClockwiseNeighbour:
        inx                             ; ECAE  step X to the clockwise neighbour, i + 1
        cpx     #$08                    ; ECAF  past the last direction?
        bcc     addClockwiseNeighbour   ; ECB1  no, i+1 is still inside the ring
        ldx     #$00                    ; ECB3  wrap i+1 round to direction 0
addClockwiseNeighbour:
        clc                             ; ECB5  clear carry before the last add
        adc     checksumXorConstant,x   ; ECB6  + the clockwise neighbour, weight 1  (also trainerPlaybook0200:play3Col12)
        bcc     finishBlurredValue      ; ECB9  no carry
        inc     scratch19               ; ECBB  carry into the high bits
finishBlurredValue:
        lsr     scratch19               ; ECBD  divide the 10-bit sum by 4: high bits down ...
        ror     a                       ; ECBF  ... into A (first halving)
        lsr     scratch19               ; ECC0  and again ...
        ror     a                       ; ECC2  ... A = (2*raw[i] + raw[i-1] + raw[i+1]) / 4
        ldy     scratch23               ; ECC3  Y = i again
        sta     D_8896,y                ; ECC5  write the blurred pressure back into the live ring at $8896+i
        dec     scratch23               ; ECC8  next direction down
        bpl     smoothThreatRing        ; ECCA  repeat for directions 7..0

; ----------------------------------------------------------------------
; Now look for the calmest 135-degree arc: three adjacent directions whose blurred pressures add up to
; the smallest total.  The centre of that arc is the direction the comcen will run towards.
; ----------------------------------------------------------------------
        lda     #$FF                    ; ECCC  $FF = no arc chosen yet, and as an arc sum 255 is worse than anything real
        sta     scratch18               ; ECCE  zp_18 = best (quietest) centre direction so far
        sta     scratch19               ; ECD0  zp_19 = the best arc sum so far
        ldy     #$07                    ; ECD2  test the eight arcs starting with the one led by direction 7

; ----------------------------------------------------------------------
; findQuietestDirectionArc - For every leading direction y = 7..0 it adds the blurred pressures of y,
; y-1 and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle
; direction y-1. The result is the centre of the calmest 135-degree wedge around the reference point.
; Exact ties are decided by bit 7 of the game RNG state, so the trainer does not always break them the
; same way.
; In:  $8896-$889D = blurred direction weights, zp_57 = game RNG state byte 0, zp_18/zp_19 preset to
;     $FF
; Out: zp_18 = quietest centre direction 0-7, zp_19 = its arc sum; zp_1A = scratch for the candidate
;     centre; A/X/Y clobbered
; Called from: nothing - loop head of rankThreatDirections, branched to from the DEY/BPL at $ECFE.
;     (The XREF calls from $EC74/$EC7E belong to the modem-driver build that shares this address.)
; ----------------------------------------------------------------------
findQuietestDirectionArc:
        tya                             ; ECD4  A = the arc's leading direction
        tax                             ; ECD5  X indexes the first of the three samples
        lda     D_8896,x                ; ECD6  blurred pressure of direction y
        dex                             ; ECD9  step to y - 1, the middle of the arc
        bpl     addArcMiddleDirection   ; ECDA  no wrap needed
        ldx     #$07                    ; ECDC  y was 0, so the middle direction is 7
addArcMiddleDirection:
        stx     scratch1A               ; ECDE  remember the middle direction - that is what the caller receives
        clc                             ; ECE0  clear carry before the add
        adc     D_8896,x                ; ECE1  + pressure of the middle direction
        dex                             ; ECE4  step to y - 2, the trailing direction
        bpl     addArcTrailingDirection ; ECE5  no wrap needed
L_ECE7:
        ldx     #$07                    ; ECE7  wrap y-2 round to direction 7
addArcTrailingDirection:
        clc                             ; ECE9  clear carry before the last add
        adc     D_8896,x                ; ECEA  + pressure of the trailing direction; A = the arc total (8-bit, and it can wrap if the whole ring is saturated at $FF)
L_ECED:
        cmp     scratch19               ; ECED  compare with the quietest arc found so far
        bcc     rememberQuietestArc     ; ECEF  strictly quieter - take this arc
        bne     nextArcCandidate        ; ECF1  louder - keep the previous best
L_ECF3:
        bit     gameRngState0           ; ECF3  a tie: toss a coin on bit 7 of the game RNG state. zp_57 is not stepped here, so every tie inside one call falls the same way
        bmi     nextArcCandidate        ; ECF5  coin says keep the arc that was found first
rememberQuietestArc:
        sta     scratch19               ; ECF7  remember the new best arc sum ...
        ldx     scratch1A               ; ECF9  ... and its middle direction ...
        stx     scratch18               ; ECFB  ... as the escape direction to return
nextArcCandidate:
        dey                             ; ECFD  next leading direction
        bpl     findQuietestDirectionArc; ECFE  eight arcs in all

; ----------------------------------------------------------------------
; Return the summary: A = threat level, X = deepest weapon-range penetration, Y = escape direction.
; Subtracting the unit count removes the +1 base weight each threatening unit contributed, so what is
; left is the 'extra' pressure from being close or inside somebody's weapon range.
; ----------------------------------------------------------------------
        lda     scratch1F               ; ED00  total pressure accumulated over all threatening units
        sec                             ; ED02  no borrow
        sbc     scratch20               ; ED03  minus one per threatening unit: strip the base weight each one added
        lsr     a                       ; ED05  halve it - A is the threat level the planner keeps in $8962
        ldy     scratch18               ; ED06  Y = the quietest direction; callers add 4 mod 8 to get the direction the enemy is in
        ldx     scratch21               ; ED08  X = the largest weapon-range surplus seen, 0 = nothing has this point in range ($8961)
threatScanDone:
        rts                             ; ED0A  shared exit; the 'nothing threatens at all' path at $EC7F jumps here with A=0, X=0, Y=8


; ----------------------------------------------------------------------
; runAiMissileTurn - The computer opponent's one 'weapon' decision per lock-step exchange. It first
; offers to launch the AI's own drone (launchAiDrone), then, if the human is steering a drone of his
; own and the AI still has missiles and is reloaded, it decides whether to shoot that drone down: the
; aim point is the drone's cell, a reluctance score is built from the range to the AI comcen, the
; comcen's stun state and energy and a missile-conservation rule, and the shot either kills the drone
; (command $92 plus the $90 burst) or lands scattered around it. Whatever happens it falls through
; into fireMissileAtSpottedUnit.
; In:  $8966 action budget for this exchange (0 = free), $920D drone state flags, $92B1 AI missiles
;     left, $92AE human drones left, $8970 reload countdown, $92F6/$92F7 drone camera window origin,
;     $F6A3/$F707 AI comcen cell, $F9C3 comcen stun, $FA27 comcen energy, $920B/$896F drone heading,
;     game RNG
; Out: may put command $92 and/or $90 col,row into the fabricated 'received' packet ($E020, length
;     $E01E); $8970 reload timer, $8966 spent, $8968 += 2;
;     zp_22/zp_23/zp_24/zp_27/zp_28/zp_29/zp_2B/zp_18 clobbered
; Called from: injectAiCommandsIntoPacket ($EA80), which itself runs once per exchange from the $E000
;     jump-table entry with X=1
; The AI is allowed exactly one drone launch or missile shot per exchange: injectAiCommandsIntoPacket
; clears $8966 to 0 before calling, and each action decrements it to $FF.
; ----------------------------------------------------------------------
runAiMissileTurn:
        jsr     launchAiDrone           ; ED0B  first chance of the exchange: send a drone up if one is due
        lda     D_8966                  ; ED0E  $8966 = this exchange's action budget
        bmi     skipAntiDroneShot       ; ED11  the drone launch already used it - go straight to the ground-target path
        lda     side1ComcenCol          ; ED13  column of the AI's own comcen (unit 99)
        sta     cellColB                ; ED16  zp_27/zp_28 = point B of every cellDistance below: the comcen is what the AI defends
L_ED18:
        lda     side1ComcenRow          ; ED18  row of the AI comcen
        sta     cellRowB                ; ED1B  zp_28 = reference row
        bit     droneControlFlags       ; ED1D  $920D bit 6 = the human is steering his own drone right now
        bvs     considerAntiDroneShot   ; ED20  he is - think about shooting it down
skipAntiDroneShot:
        jmp     fireMissileAtSpottedUnit; ED22  no drone to shoot at, so look for an ordinary target

considerAntiDroneShot:
        lda     opponentMissilesLeft    ; ED25  $92B1 = missiles the computer opponent has left
        beq     skipAntiDroneShot       ; ED28  out of missiles
        lda     D_8970                  ; ED2A  $8970 = reload countdown; $FF (negative) means ready to fire
L_ED2D:
        bpl     skipAntiDroneShot       ; ED2D  still reloading

; ----------------------------------------------------------------------
; Aim at the human's drone.  $92F6/$92F7 are the top-left corner of his 14x12 drone camera window and
; the drone always sits in the middle of it, hence the +6/+5.
; ----------------------------------------------------------------------
        lda     #$00                    ; ED2F  0 = 'the comcen is undamaged' until proved otherwise
L_ED31:
        sta     scratch2B               ; ED31  zp_2B = comcen-damaged flag
        lda     droneViewOriginCol      ; ED33  left edge of the drone camera window
        clc                             ; ED36  clear carry for the add
        adc     #$06                    ; ED37  the drone sits 6 columns into the window
        sta     cellColA                ; ED39  zp_22 = aim column (point A)
        lda     droneViewOriginRow      ; ED3B  top edge of the drone camera window
        clc                             ; ED3E  clear carry
        adc     #$05                    ; ED3F  the drone sits 5 rows into the window
        sta     cellRowA                ; ED41  zp_24 = aim row
L_ED43:
        jsr     cellDistance            ; ED43  A = range in cells (0-16) from the AI comcen to the drone
        sta     scratch23               ; ED46  zp_23 = reluctance to fire, starting at that range
        lsr     a                       ; ED48  half the range ...
        sta     scanListIndex           ; ED49  ... is what the reload timer will be set to if the shot goes off
        lda     side1ComcenStun         ; ED4B  stun byte of the AI comcen (unit 99)
        and     #$0F                    ; ED4E  bits 0-3 = the stun counter
        beq     checkComcenEnergy       ; ED50  not stunned - check the energy instead
        dec     scratch2B               ; ED52  zp_2B = $FF: remember that the comcen is stunned - it costs another point of reluctance at $ED69
L_ED54:
        cmp     #$0E                    ; ED54  stun counter 14 or 15 = the comcen is effectively out of action
        bcs     skipAntiDroneShot       ; ED56  too badly stunned to shoot at a drone
        bcc     bumpReluctance          ; ED58  always taken (the CMP above set carry only for >= $0E): skip the energy test
checkComcenEnergy:
        lda     side1ComcenEnergy       ; ED5A  energy byte of the AI comcen
        and     #$03                    ; ED5D  low two bits of the 0-50 energy value
        bne     applyStunPenalty        ; ED5F  not a multiple of 4 - no extra hesitation
bumpReluctance:
        inc     scratch23               ; ED61  energy divisible by 4: +1 reluctance, a cheap energy-driven hesitation
applyStunPenalty:
        lda     scratch23               ; ED63  A = the reluctance BEFORE the stun bump below
        bit     scratch2B               ; ED65  was the comcen stunned? (zp_2B = $FF if so)
        bpl     testCloseShot           ; ED67  no
        inc     scratch23               ; ED69  stunned: +1 reluctance. A keeps the old value, so the test at $ED6B still uses the unbumped score
testCloseShot:
        cmp     #$03                    ; ED6B  reluctance under 3 - a close shot from a healthy comcen: fire without the conservation test
        bcc     fireAntiDroneMissile    ; ED6D  take the shot
testReluctanceLimit:
        lda     scratch23               ; ED6F  reload the (possibly bumped) reluctance
        cmp     #$05                    ; ED71  5 or more = far away and/or a hurt comcen ...
        bcs     skipAntiDroneShot       ; ED73  ... not worth a missile
        lda     dronesLeft              ; ED75  $92AE = drones the human still has in stock
L_ED78:
        asl     a                       ; ED78  x2 ...
        asl     a                       ; ED79  ... x4: four missiles are reserved for every future drone
        sta     scratch18               ; ED7A  zp_18 = the reserve
        lda     opponentMissilesLeft    ; ED7C  missiles the AI owns
        sec                             ; ED7F  no borrow wanted
        sbc     scratch18               ; ED80  missiles above the reserve
        bcs     clampMissileSurplus     ; ED82  positive surplus
        lda     #$00                    ; ED84  below the reserve - treat the surplus as 0
clampMissileSurplus:
        clc                             ; ED86  clear carry
        adc     #$01                    ; ED87  surplus + 1
        cmp     scratch23               ; ED89  compare with the reluctance
        bcc     fireMissileAtSpottedUnit; ED8B  too few spare missiles for a shot this reluctant - save it for later

; ----------------------------------------------------------------------
; The shot goes off.  Whether it actually hits is decided by one random byte against a threshold that
; grows with the reluctance and jumps by $40 when the drone has changed heading since the last shot,
; so a jinking drone is much harder to hit.
; ----------------------------------------------------------------------
fireAntiDroneMissile:
        lda     scanListIndex           ; ED8D  half the range, computed at $ED48
        sta     D_8970                  ; ED8F  start the reload countdown
        inc     scratch23               ; ED92  reluctance + 1 so that even a perfect shot can miss
        lda     scratch23               ; ED94  A = reluctance + 1
        asl     a                       ; ED96  x2 ...
        asl     a                       ; ED97  x4 ...
        asl     a                       ; ED98  x8 ...
        asl     a                       ; ED99  x16 ...
        asl     a                       ; ED9A  x32: the miss threshold is (reluctance+1) * 32
        ldx     droneHeading            ; ED9B  $920B = the heading of the drone right now
        cpx     D_896F                  ; ED9E  $896F = the heading it had at the last shot
        beq     storeMissThreshold      ; EDA1  unchanged - the drone is flying straight
        clc                             ; EDA3  clear carry
        adc     #$40                    ; EDA4  it jinked: +$40 makes a hit far less likely
storeMissThreshold:
        sta     scratch18               ; EDA6  zp_18 = miss threshold
        jsr     nextGameRandom          ; EDA8  step the lock-step game RNG (both machines would get the same byte)
        cmp     scratch18               ; EDAB  random byte against the threshold
        bcs     droneShotDownCleanly    ; EDAD  random >= threshold: a clean hit on the drone
        lsr     scratch23               ; EDAF  a miss: scatter radius = reluctance / 2 ...
        inc     scratch23               ; EDB1  ... + 1, so it is at least one cell off
        lda     scratch23               ; EDB3  A = scatter radius
        jsr     randomlySignValue       ; EDB5  randomly plus or minus
        clc                             ; EDB8  clear carry
        adc     cellColA                ; EDB9  shift the aim column
        sta     cellColA                ; EDBB  zp_22 = the missed column
        lda     scratch23               ; EDBD  A = scatter radius again
L_EDBF:
        jsr     randomlySignValue       ; EDBF  randomly plus or minus
        clc                             ; EDC2  clear carry
        adc     cellRowA                ; EDC3  shift the aim row
        sta     cellRowA                ; EDC5  zp_24 = the missed row
        jmp     queueScatteredStrike    ; EDC7  queue the strike at the scattered cell

droneShotDownCleanly:
        lda     #$92                    ; EDCA  command $92 = DRONE SHOT DOWN (handler cmdDroneShotDown $538B, clears $920D bits 6-7)
        sta     rxPacketBuffer          ; EDCC  it is the first byte of the packet - $E01E is still 0 whenever this path is reached
        inc     rxPacketLength          ; EDCF  packet is now 1 byte long
queueScatteredStrike:
        jsr     queueMissileStrikeCommand; EDD2  append the $90 strike so the human also sees the missile burst on the drone's cell

; ----------------------------------------------------------------------
; fireMissileAtSpottedUnit - Second half of the missile turn, reached whenever no anti-drone shot was
; taken. It fires a comcen missile at the unit stored in $8972 (the last human unit seen shooting at
; the AI comcen) or, when there is none or it is dead, at the human comcen itself while that comcen is
; visible. The target must sit on the AI comcen's 21x17 radar grid; the reload timer is then set to
; half the range and the $90 strike is queued. The tail remembers the human drone's heading for the
; jink test and ticks the reload timer down.
; In:  $920D bit 6, $92AE human drones left, $92B1 AI missiles, $8970 reload countdown, $8966 action
;     budget, $8972 current missile target, $F76C unit type/dead bits, $F9F5 human comcen
;     energy/visibility, $920B drone heading, zp_27/zp_28 = AI comcen cell
; Out: $8972 ($FF when the target was dropped), $8970, $896F, and via queueMissileStrikeCommand the
;     bytes $90 col,row in the fabricated packet
; Called from: JMP at $ED22 and BCC at $ED8B in runAiMissileTurn, plus the fall-through from $EDD2
; Missile conservation: while the human still owns drones the AI keeps two missiles in hand for each
; of them, so it will not empty the rack on ground targets and be defenceless against the next drone.
; ----------------------------------------------------------------------
fireMissileAtSpottedUnit:
        bit     droneControlFlags       ; EDD5  $920D bit 6 = the human is flying a drone
        bvs     forgetMissileTarget     ; EDD8  he is - the anti-drone code above owns this exchange, so no ground shot
        lda     dronesLeft              ; EDDA  drones the human still has
        asl     a                       ; EDDD  two missiles reserved per drone
        sta     scratch18               ; EDDE  zp_18 = the reserve
        lda     opponentMissilesLeft    ; EDE0  missiles the AI owns
        beq     forgetMissileTarget     ; EDE3  none at all
        sec                             ; EDE5  no borrow wanted
        sbc     scratch18               ; EDE6  missiles minus the reserve
        bcc     forgetMissileTarget     ; EDE8  fewer than the reserve - hold fire
        beq     forgetMissileTarget     ; EDEA  exactly the reserve - hold fire as well
        lda     D_8970                  ; EDEC  reload countdown
        bpl     missileTurnTail         ; EDEF  still reloading (>= 0): skip to the tail, which ticks it down
        lda     D_8966                  ; EDF1  this exchange's action budget
        bmi     forgetMissileTarget     ; EDF4  already spent on the drone launch - drop the target and leave
        ldx     D_8972                  ; EDF6  $8972 = human unit last seen shooting at the AI comcen, $FF = none known
        bmi     targetEnemyComcenWithMissile; EDF9  none - try the human comcen instead
        lda     unitTypeTable,x         ; EDFB  type byte of that unit
        bpl     checkTargetOnRadar      ; EDFE  still alive (bit 7 clear) - shoot at it

; ----------------------------------------------------------------------
; targetEnemyComcenWithMissile - Fall-through tail of fireMissileAtSpottedUnit: when the AI has no
; remembered attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49)
; instead, but only while that comcen is currently visible.
; In:  $F9F5 = energy/visibility byte of unit 49 (bit 7 = visible right now)
; Out: X = $31 when the comcen may be shot at, otherwise branches to $EE19 which clears the target
; Called from: BMI at $EDF9 and fall-through from $EDFE. XREF lists 'call:E0A5', but that JSR only
;     ever runs while the track 29 drone-flight overlay occupies $EE00-$EFFF, so it never enters this
;     code
; ----------------------------------------------------------------------
targetEnemyComcenWithMissile:
        lda     side0ComcenEnergy       ; EE00  energy/visibility byte of the human comcen
        bpl     forgetMissileTarget     ; EE03  bit 7 clear = not visible; nothing to shoot at
        ldx     #$31                    ; EE05  $31 = 49 = the human player's COMCEN
checkTargetOnRadar:
        jsr     isUnitOnAiRadar         ; EE07  put the target cell in zp_22/zp_24 and test it against the AI comcen's radar grid
        bcs     forgetMissileTarget     ; EE0A  outside the 21x17 grid - the AI cannot see it, so no shot
        jsr     cellDistance            ; EE0C  range from the AI comcen (zp_27/zp_28) to the target
L_EE0F:
        lsr     a                       ; EE0F  half of it ...
        sta     D_8970                  ; EE10  ... becomes the reload countdown: long shots take longer to repeat
        jsr     queueMissileStrikeCommand; EE13  append the $90 col,row strike to the fabricated packet
        jmp     missileTurnTail         ; EE16  keep $8972 so the same unit stays the target next exchange

forgetMissileTarget:
        lda     #$FF                    ; EE19  $FF = no target
        sta     D_8972                  ; EE1B  forget the remembered attacker

; ----------------------------------------------------------------------
; Shared tail: remember the human drone's heading so the next anti-drone shot can tell whether it
; jinked, and tick the reload countdown down towards $FF = ready.
; ----------------------------------------------------------------------
missileTurnTail:
        lda     droneHeading            ; EE1E  current heading of the human's drone
        sta     D_896F                  ; EE21  $896F is compared against it at $ED9E next time
        lda     D_8970                  ; EE24  reload countdown
        bmi     missileTurnDone         ; EE27  already at $FF: the launcher is ready, leave it alone
        dec     D_8970                  ; EE29  one exchange closer to being reloaded
missileTurnDone:
        rts                             ; EE2C  back to injectAiCommandsIntoPacket


; ----------------------------------------------------------------------
; queueMissileStrikeCommand - Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler
; cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to
; have received from the opponent. Both coordinates are pre-mirrored with mirrorMapCoordinate (39-v)
; because the command dispatcher un-mirrors every parameter that arrives from a remote player.
; In:  zp_22 = strike column, zp_24 = strike row, $E01E = current packet length
; Out: three bytes appended at $E020+, $E01E advanced by 3, $8966 spent (decremented to $FF), $8968
;     charged two of the round's command slots; X clobbered
; Called from: $EDD2 (anti-drone shot) and $EE13 (shot at a spotted unit)
; ----------------------------------------------------------------------
queueMissileStrikeCommand:
        ldx     rxPacketLength          ; EE2D  X = current length of the fabricated incoming packet
        lda     #$90                    ; EE30  $90 = MISSILE STRIKE
        sta     rxPacketBuffer,x        ; EE32  command byte into the received-packet buffer
        inx                             ; EE35  next byte
        lda     cellColA                ; EE36  strike column
        jsr     mirrorMapCoordinate     ; EE38  $3FA3: 39 - column, undoing the mirror the engine will apply to a remote command
        sta     rxPacketBuffer,x        ; EE3B  first parameter
L_EE3E:
        inx                             ; EE3E  next byte
        lda     cellRowA                ; EE3F  strike row
L_EE41:
        jsr     mirrorMapCoordinate     ; EE41  mirror it as well
        sta     rxPacketBuffer,x        ; EE44  second parameter
        inx                             ; EE47  past the last byte
        stx     rxPacketLength          ; EE48  packet is three bytes longer
        dec     D_8966                  ; EE4B  this exchange's single action is now spent
        inc     D_8968                  ; EE4E  charge the first of the two command slots a missile costs
        inc     D_8968                  ; EE51  and the second: a missile counts double against the four commands a round allows
L_EE54:
        rts                             ; EE54  done


; ----------------------------------------------------------------------
; randomlySignValue - Returns A or -A with equal probability, using bit 0 of the lock-step game RNG.
; Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss
; the human's drone.
; In:  A = magnitude
; Out: A = zp_18 = +A or -A (two's complement); the game RNG has been stepped
; Called from: $EDB5 (column offset) and $EDBF (row offset)
; ----------------------------------------------------------------------
randomlySignValue:
        sta     scratch18               ; EE55  keep the magnitude
        jsr     nextGameRandom          ; EE57  one random byte from the lock-step RNG
        and     #$01                    ; EE5A  just its lowest bit
        beq     returnSignedValue       ; EE5C  0 - leave the magnitude positive
        lda     scratch18               ; EE5E  1 - negate it: two's complement ...
        eor     #$FF                    ; EE60  ... invert ...
        clc                             ; EE62  clear carry for the +1
        adc     #$01                    ; EE63  ... and add one
        sta     scratch18               ; EE65  zp_18 = -magnitude
returnSignedValue:
        lda     scratch18               ; EE67  return the signed value
        rts                             ; EE69  back to the missile scatter


; ----------------------------------------------------------------------
; isUnitOnAiRadar - Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17
; radar grid centred on the computer opponent's own COMCEN. Because mapCellToRadarGrid centres the
; grid on comcenUnit ($9236), this routine swaps that variable to $63 (unit 99, side 1's comcen) for
; the test and puts it back to $31 (unit 49) afterwards - the unconditional $31 is proof that this
; module always assumes the human is side 0.
; In:  X = unit index, $F640/$F6A4 = unit column/row arrays
; Out: zp_22 = column, zp_24 = row of that unit (point A for the next cellDistance), C = 0 when the
;     cell is on the AI's radar grid and C = 1 when it is off it; $9236 left at $31; X and Y clobbered
; Called from: $EE07 (missile targeting) and $EEFA (drone targeting in THE BOMB). Note it calls into
;     the $6F00 comcen-screens overlay, so overlay B must be the resident one
; ----------------------------------------------------------------------
isUnitOnAiRadar:
        lda     unitColTable,x          ; EE6A  map column of unit X
        sta     cellColA                ; EE6D  zp_22 = point A column, also the strike coordinate if a missile follows
        lda     unitRowTable,x          ; EE6F  map row of unit X
L_EE72:
        sta     cellRowA                ; EE72  zp_24 = point A row
        tay                             ; EE74  Y = row, as mapCellToRadarGrid wants it
        ldx     cellColA                ; EE75  X = column
L_EE77:
        lda     #$63                    ; EE77  $63 = 99 = side 1's COMCEN
        sta     comcenUnit              ; EE79  pretend the local comcen is the AI's, so the radar grid is centred on it
        jsr     mapCellToRadarGrid      ; EE7C  $7A13 in the comcen-screens overlay: C=0 when the cell is inside the 21 x 17 grid
        lda     #$31                    ; EE7F  $31 = 49 = the human player's COMCEN
        sta     comcenUnit              ; EE81  restore comcenUnit - the rest of the game expects the local player's comcen there
        rts                             ; EE84  carry from the grid test survives both stores


; ----------------------------------------------------------------------
; launchAiDrone - Sends the computer opponent's drone up when one is due. It writes the launch
; straight into the packet the game is about to 'receive': $97 DRONE LAUNCH followed by a $8D DRONE
; MOVE that places the drone on the AI comcen and points it at the chosen target unit. The AI keeps
; its own copy of the drone position in $8974/$8975 and a 100-step fuel counter in $8973; $8977 = 3
; holds the next launch off for three exchanges.
; In:  $8977 launch delay (must be 0), $8976 target unit (>= 0), $920D bit 7 (no drone of the human's
;     in the air), $F6A3/$F707 AI comcen cell, $F640/$F6A4 target cell
; Out: $E020 = $97, $E021-$E024 = $8D heading,col,row, $E01E = 5; $8974/$8975 launch cell, $8973 = $64
;     fuel, $8977 = 3, $896F heading, $8966 spent; zp_92-zp_95 line-stepper state clobbered
; Called from: $ED0B, the first thing runAiMissileTurn does each exchange
; Three gates before a launch: the delay must have run out, a target must have been picked by
; chooseAiDroneTarget, and no drone may be in the air already - the engine tracks only one at a time.
; ----------------------------------------------------------------------
launchAiDrone:
        lda     D_8977                  ; EE85  $8977 = exchanges to wait before the next launch ($FF also means the drone-flight overlay is resident)
        bne     noDroneLaunch           ; EE88  not due yet
        lda     D_8976                  ; EE8A  $8976 = the unit the drone should be sent at
        bmi     noDroneLaunch           ; EE8D  bit 7 set = no target chosen
        lda     droneControlFlags       ; EE8F  $920D drone state flags
        bmi     noDroneLaunch           ; EE92  bit 7 = a drone flight is already in progress - wait for it to end
        dec     D_8966                  ; EE94  spend this exchange's single action
        lda     #$97                    ; EE97  $97 = DRONE LAUNCH (handler cmdDroneLaunch $53A3: 'DRONE ALERT!' and one drone off the opponent's stock)
        sta     rxPacketBuffer          ; EE99  first byte of the fabricated packet
        lda     #$8D                    ; EE9C  $8D = DRONE MOVE, 4 bytes: heading, column, row
        sta     rxPacketArg1            ; EE9E  second byte
L_EEA1:
        ldx     side1ComcenCol          ; EEA1  the drone starts over the AI's own comcen
        stx     D_8974                  ; EEA4  $8974 = the AI's private copy of the drone column
        stx     lineCurCol              ; EEA7  zp_93 = line-stepper start column
        ldy     side1ComcenRow          ; EEA9  comcen row
        sty     D_8975                  ; EEAC  $8975 = drone row
L_EEAF:
        sty     lineCurRow              ; EEAF  zp_92 = line-stepper start row
        jsr     mirrorBlockCornerForOpponent; EEB1  add one to each coordinate and mirror: exactly undoes the mirror-then-decrement the engine applies to a remote $8D (the drone is a 2x2 object)
        stx     rxPacketArg3            ; EEB4  column parameter of the $8D
        sty     rxPacketArg4            ; EEB7  row parameter
        ldy     D_8976                  ; EEBA  the chosen target unit
        lda     unitColTable,y          ; EEBD  its column ...
        sta     lineEndCol              ; EEC0  ... becomes the line-stepper target column
        lda     unitRowTable,y          ; EEC2  its row ...
        sta     lineEndRow              ; EEC5  ... the target row
        jsr     getStepDirection        ; EEC7  one Bresenham step towards the target: Y = compass direction 0-7, A = that direction's row delta
        sta     D_896F                  ; EECA  remember the heading for the AI's own bookkeeping
        sta     rxPacketArg2            ; EECD  heading parameter of the $8D - but this is A, the row delta (-1/0/+1), not the direction index in Y. cmdDroneMove draws sprite shape (heading+4) AND 7 = 3, 4 or 5, so the AI's drone is always drawn flying south towards the player. Probably a slip for TYA
L_EED0:
        lda     #$05                    ; EED0  5 bytes: one $97 plus four for the $8D
        sta     rxPacketLength          ; EED2  hand the packet to the engine as if the opponent had sent it
        lda     #$64                    ; EED5  $64 = 100 steps of fuel, one per exchange, matching the human's drone tank
        sta     D_8973                  ; EED7  $8973 = the AI's own fuel counter
        lda     #$03                    ; EEDA  wait three exchanges before considering another launch
        sta     D_8977                  ; EEDC  $8977 also stays non-negative, marking the native $EE00 tail as the resident one
noDroneLaunch:
        rts                             ; EEDF  nothing to launch


; ----------------------------------------------------------------------
; chooseAiDroneTarget - Picks what the computer opponent should send its next drone at and leaves the
; unit index in $8976. It bails out when the AI has no drones left or a target is still pending. In
; game type 2 (THE BOMB) the only worthwhile target is the human COMCEN, as soon as it is visible or
; shows on the AI's radar. Otherwise, once the comcen is visible, it adds up the strength of every
; group that has reached it (double for a group with a clear run at it) and, at a total of 8, commits
; to an all-out attack: every group is flagged committed, all 46 combat units are made due for new
; orders, the drone is held back 21 exchanges and then aimed at the comcen. If the comcen is not
; visible it falls into pickDroneTargetAlongThreatAxis.
; In:  $92AF drones the AI has left, $8976, $0BA3 bits 0-2 = game type, $F9F5 human comcen visibility,
;     $8902 group strengths, $893E group flags, $8953 highest group id, $8954 group count, $91CB game
;     clock
; Out: $8976 = target unit ($31 = the human comcen), $893E bit 6 set for every group, $8800-$882D
;     stamped with the clock, $8977 = $15; zp_18, zp_19 clobbered
; Called from: planTrainerRound ($E1FF), once per round and only while the AI is in attack mode ($8963
;     = 0)
; ----------------------------------------------------------------------
chooseAiDroneTarget:
        lda     opponentDronesLeft      ; EEE0  $92AF = drones the computer opponent still owns
        beq     droneTargetDone         ; EEE3  none left - it will never launch again
        lda     D_8976                  ; EEE5  $8976 = pending drone target
        bpl     droneTargetDone         ; EEE8  bit 7 clear = a target is already chosen, so leave it alone
        lda     gameTypeOptions         ; EEEA  game options byte
L_EEED:
        and     #$07                    ; EEED  bits 0-2 = the game type
        cmp     #$02                    ; EEEF  type 2 = THE BOMB, where only the comcen counts
        bne     chooseTargetByGroupPressure; EEF1  any other game type
        lda     side0ComcenEnergy       ; EEF3  energy/visibility of the human comcen
        bmi     targetHumanComcen       ; EEF6  bit 7 = visible right now: send the drone at it
        ldx     #$31                    ; EEF8  $31 = 49 = the human comcen
        jsr     isUnitOnAiRadar         ; EEFA  is it at least on the AI comcen's radar grid?
L_EEFD:
        bcs     droneTargetDone         ; EEFD  not on the radar either - no drone target this round
        bcc     targetHumanComcen       ; EEFF  on the radar - target it

; ----------------------------------------------------------------------
; Every other game type: measure how much of the AI's army has actually converged on the human comcen.
; $893E bit 5 = this group has a clear line to the comcen (set at $E3C3), bit 6 = one of its members
; has
; come within 8 cells of it (set at $EB1F).
; ----------------------------------------------------------------------
chooseTargetByGroupPressure:
        lda     side0ComcenEnergy       ; EF01  comcen visibility again
        bpl     pickDroneTargetAlongThreatAxis; EF04  not visible - go and look for a worthwhile cluster of units instead
        ldy     D_8954                  ; EF06  $8954 = number of AI groups ...
        dey                             ; EF09  ... so the last group index is one less
        lda     #$00                    ; EF0A  zp_18 = 0
        sta     scratch18               ; EF0C  running total of converging strength
sumConvergingGroupsLoop:
        lda     D_8902,y                ; EF0E  $8902 = number of units in group Y
        sta     scratch19               ; EF11  zp_19 = this group's weight
        ldx     D_893E,y                ; EF13  group flags
        txa                             ; EF16  test them ...
        and     #$20                    ; EF17  ... bit 5 = the group has a clear run at the human comcen
        beq     testGroupContactFlag    ; EF19  no clear run - try the contact bit
        asl     scratch19               ; EF1B  a clear run counts double
        jmp     addGroupWeight          ; EF1D  add it in

testGroupContactFlag:
        txa                             ; EF20  flags again
        and     #$40                    ; EF21  bit 6 = a member of this group has reached the comcen
L_EF23:
        beq     nextGroupInSum          ; EF23  neither flag - this group does not count
addGroupWeight:
        lda     scratch19               ; EF25  the group's weight
        clc                             ; EF27  clear carry
L_EF28:
        adc     scratch18               ; EF28  add to the running total
        sta     scratch18               ; EF2A  keep the total
nextGroupInSum:
        dey                             ; EF2C  next group down
        bpl     sumConvergingGroupsLoop ; EF2D  until group 0 has been counted
        lda     scratch18               ; EF2F  converging strength
        cmp     #$08                    ; EF31  eight units' worth is the trigger for the all-out attack
        bcc     pickDroneTargetAlongThreatAxis; EF33  not enough yet - go back to hunting clusters

; ----------------------------------------------------------------------
; All-out attack: flag every group as committed, then stamp the order clock of all 46 mobile AI units
; with the current game clock.  scanTrainerUnitsForOrders re-plans a unit whose stamp has caught up
; with the down-counting clock ($E2E3), so this makes the whole army replan on the next scan.
; ----------------------------------------------------------------------
commitAllGroups:
        ldy     D_8953                  ; EF35  $8953 = highest group id in use
commitGroupLoop:
        lda     D_893E,y                ; EF38  flags of group Y
        ora     #$40                    ; EF3B  bit 6 = committed to the attack
        sta     D_893E,y                ; EF3D  store them back
        dey                             ; EF40  next group down
        bpl     commitGroupLoop         ; EF41  until group 0 is done
        ldy     #$2D                    ; EF43  $2D = 45: units 50..95, the AI's 28 grunts, 12 riders and 6 boomers (its 3 spies and comcen are left alone)
        lda     gameClock               ; EF45  $91CB = the down-counting game clock
stampOrderClockLoop:
        sta     msgGetOpponentOnPhoneAnd,y; EF48  $8800+ordinal = the round this unit may next be given orders
        dey                             ; EF4B  next AI unit
        bpl     stampOrderClockLoop     ; EF4C  all 46 of them
        lda     #$15                    ; EF4E  $15 = 21 exchanges ...
        sta     D_8977                  ; EF50  ... before the drone follows the ground attack in
targetHumanComcen:
        lda     #$31                    ; EF53  $31 = 49 = the human COMCEN
        sta     D_8976                  ; EF55  that is the drone's target
droneTargetDone:
        rts                             ; EF58  no target this round


; ----------------------------------------------------------------------
; pickDroneTargetAlongThreatAxis - Tail of chooseAiDroneTarget for the case where the human comcen is
; no use as a drone target. It scans the threat around the AI's current objective (the escorted
; group's centre, or the AI comcen when no group is being escorted), and if the pressure reaches that
; group's own strength and something really has the point in weapon range, it walks a ray in the
; direction the enemy is coming from ((quietest+4) AND 7) at radii 4 to 14, scoring each 2x2 block of
; cells with scoreTargetBlock. The highest-numbered human unit in the best-scoring block becomes the
; drone target, and the acceptance threshold $8979 is cut to three quarters so the AI grows less
; choosy as the game goes on.
; In:  $895D escorted group ($FF = none), $88EA/$88F6 group centre, $8902 group strength, $8979
;     block-score threshold (set per opening play by the trainer setup overlay: $FF, $FF, 8, $0C,
;     $0C), $F6A3/$F707 AI comcen cell
; Out: $8976 = drone target unit (untouched when nothing scored), $8979 scaled by 3/4, $8956 = the
;     threshold used, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius,
;     zp_18/zp_19/zp_22/zp_24/zp_27/zp_28 and zp_94/zp_95 clobbered
; Called from: BPL at $EF04 and BCC at $EF33 inside chooseAiDroneTarget - it is that routine's
;     fall-through tail, not a separate entry
; Choose the point to scan around and the pressure it takes to make the AI bother: the group the
; comcen escorts and its member count, or the AI comcen itself with a fixed threshold of 3.
; ----------------------------------------------------------------------
pickDroneTargetAlongThreatAxis:
        ldx     side1ComcenCol          ; EF59  default scan point: the AI's own comcen column
        ldy     side1ComcenRow          ; EF5C  and row
        lda     #$03                    ; EF5F  default threat threshold
        sta     D_8956                  ; EF61  $8956 doubles as the trainer's 'how much pressure is worth reacting to' value
        lda     D_895D                  ; EF64  $895D = the group the AI comcen escorts
        bmi     storeScanReferencePoint ; EF67  bit 7 = no group is being escorted - keep the comcen defaults
        tay                             ; EF69  Y = that group
        lda     D_8902,y                ; EF6A  $8902 = its member count
L_EF6D:
        sta     D_8956                  ; EF6D  a big group needs more pressure before its drone is called in
        ldx     D_88EA,y                ; EF70  the group's centre column ...
        lda     D_88F6,y                ; EF73  ... and centre row ...
        tay                             ; EF76  ... become the scan point
storeScanReferencePoint:
        stx     cellColB                ; EF77  zp_27 = reference column for the threat scan
        sty     cellRowB                ; EF79  zp_28 = reference row (the scan routine is written for the comcen but works on any point)
        jsr     assessThreatAroundAiComcen; EF7B  A = threat level, X = deepest weapon-range penetration, Y = quietest compass direction
        cmp     D_8956                  ; EF7E  compare the measured pressure with the threshold
        bcc     pickDroneTargetDone     ; EF81  too quiet around the objective - no drone target
        txa                             ; EF83  X = the range surplus
        beq     pickDroneTargetDone     ; EF84  nothing actually has the objective in weapon range - not worth a drone either

; ----------------------------------------------------------------------
; Walk a ray from the objective straight into the enemy - the direction opposite the quietest one -
; and score every 2x2 block of cells along it from 4 to 14 cells out.  scoreTargetBlock keeps the best
; score in zp_1C and the strongest human unit of that block in zp_1D.
; ----------------------------------------------------------------------
        tya                             ; EF86  A = the quietest direction
        clc                             ; EF87  clear carry
        adc     #$04                    ; EF88  +4 of the 8 compass points = the exact opposite ...
        and     #$07                    ; EF8A  ... modulo 8: the direction the enemy is pressing from
        sta     scratch1E               ; EF8C  zp_1E = the ray's direction
        lda     #$04                    ; EF8E  start 4 cells out from the objective
        sta     scratch1F               ; EF90  zp_1F = the current radius
        lda     D_8979                  ; EF92  $8979 = the score a block has to beat
        sta     scratch1C               ; EF95  zp_1C = the score to beat. A value with bit 7 set (the $FF that plays 0 and 1 start with) makes scoreTargetBlock accept any block whose score is not negative
        lda     #$FF                    ; EF97  $FF = no unit picked yet
        sta     scratch1D               ; EF99  zp_1D = the unit the best block yields, none yet
scanRayLoop:
        ldx     scratch1E               ; EF9B  direction of the ray
        ldy     scratch1F               ; EF9D  how far out
        jsr     offsetPointInDirection  ; EF9F  zp_18/zp_19 = the cell zp_1F cells along the ray, clamped to the map (offsetPointInDirection patches its own LDX operand at $E67C+1 with the direction index); C=1 means it fell off the 40x40 map
        bcs     pickBestScoredBlock     ; EFA2  the ray ran off the 40x40 map - stop scanning
        ldx     scratch18               ; EFA4  X = candidate column
        ldy     scratch19               ; EFA6  Y = candidate row
        jsr     scoreTargetBlock        ; EFA8  score the 2x2 block at that corner - grunt 1, rider 2, boomer 4, spy 1, comcen 4, negative for the AI's own units. scoreTargetBlock patches $E9E3+1/$E9E7+1 with X and Y so a winning block can drop its corner into zp_95/zp_94; nothing here uses that copy
        inc     scratch1F               ; EFAB  one cell further out
        lda     scratch1F               ; EFAD  current radius
        cmp     #$0F                    ; EFAF  stop after 14 cells
        bcc     scanRayLoop             ; EFB1  keep walking the ray
pickBestScoredBlock:
        lda     scratch1D               ; EFB3  zp_1D = highest-numbered human unit standing in the best block. Since the roster runs 0-27 grunts, 28-39 riders, 40-45 boomers, 46-48 spies, 49 comcen, the highest index is also the most valuable unit there
        bmi     pickDroneTargetDone     ; EFB5  still $FF - no block beat the threshold, so no drone target this round
        sta     D_8976                  ; EFB7  it becomes the drone's target ($8976); launchAiDrone aims at it on a later exchange

; ----------------------------------------------------------------------
; Having spent a target, drop the acceptance threshold to three quarters, so later drones settle for
; smaller clusters.  Plays 0 and 1 start at $FF, which counts as 'accept anything'; after three
; targets the threshold lands on $6C = 108, which no 2x2 block (maximum 16) can ever reach again.
; ----------------------------------------------------------------------
        lda     D_8979                  ; EFBA  the current threshold
        lsr     a                       ; EFBD  /2 ...
        lsr     a                       ; EFBE  ... /4
        sta     scratch18               ; EFBF  zp_18 = a quarter of it
        lda     D_8979                  ; EFC1  the threshold again
        sec                             ; EFC4  no borrow wanted
        sbc     scratch18               ; EFC5  threshold minus a quarter = 3/4 of it
        bcc     pickDroneTargetDone     ; EFC7  never taken: A - A/4 cannot borrow, so this is a redundant guard
        sta     D_8979                  ; EFC9  store the reduced threshold
pickDroneTargetDone:
        rts                             ; EFCC  back to the planner


; strayTextTail - The last three characters, 'CE' and '.' with bit 7 set, of the string 'INSERT DATA
; DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the modem-driver build of this overlay slot.
; Here the trainer's code runs up to $EFCC, so only this fragment survives.
; In:  -
; Out: -
; Called from: nothing; unreferenced
; $EFCD-$EFFF is not code or data of the trainer: it is the tail of the disk sector, and every build
; of
; the $EC00/$EE00 overlay slot carries the same mastering-tool leftovers here.  Nothing in the
; disassembly reads any of it.
strayTextTail:
        .byte   "CE",'.'|$80            ; EFCD  'CE' plus '.' with bit 7 set - the tail of a string that belongs to another build

; 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.
msgDatagamecustomOff2:
        .byte   "DATA"                  ; EFD0  'DATA' - start of the shared filler block found at the end of every $EC00/$EE00 overlay
        .byte   "GAME"                  ; EFD4  'GAME'
        .byte   "CUSTOM OF",'F'|$80     ; EFD8  'CUSTOM OFF', bit 7 set on the last F to terminate the string

; msgDestroyOff2: text, 11 bytes. 'DESTROY OFF' with bit 7 on the final F. Same shared filler block;
; unreferenced.
msgDestroyOff2:
        .byte   "DESTROY OF",'F'|$80    ; EFE2  'DESTROY OFF', bit 7 on the final F; same filler block, never referenced

; msgAccepted2: text, 8 bytes. 'ACCEPTED' with bit 7 on the final D. Same shared filler block;
; unreferenced.
msgAccepted2:
        .byte   "ACCEPTE",'D'|$80       ; EFED  'ACCEPTED', bit 7 on the final D; same filler block, never referenced

; Sector padding: $C0,$DF then $00,$FF to the end of the page.  Identical in all four builds of this
; overlay slot apart from the very last byte, and read by nothing.
overlayTailFiller:
        .byte   $C0,$DF,$00,$FF,$00,$FF,$00,$FF; EFF5  ........  $C0,$DF then alternating $00,$FF padding to the end of the page
        .byte   $00,$FF,$FF             ; EFFD  ...  last three padding bytes; $EFFF is $FF here and in the track 29 variants, $00 in the modem driver
