; ============================================================================
; $EC00-$EFFF - solo trainer setup: picks the opening play and seeds the AI (track 29 sectors 0-3)
; ============================================================================
; Loaded by $7B24 from the map generator overlay, over the tail of the trainer module.

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

; ---- references to code/data outside this file ----
play4Grp12               := $0464
play4Grp32               := $0478
play4Col02               := $048C
play4Col22               := $04A0
loadSectors              := $0804
setLoadDest              := $085C
gameTypeOptions          := $0BA3
diagonalDirectionTable   := $1FB2
directionDeltaTable      := $20F0
headingToDirectionTable  := $7316
msgGetOpponentOnPhoneAnd := $8800
D_8832                   := $8832
D_8863                   := $8863
D_8864                   := $8864
D_8896                   := $8896
msgPickUpPhoneThenPressSpace := $8897
D_8898                   := $8898
D_88D2                   := $88D2
D_88DE                   := $88DE
D_88EA                   := $88EA
D_88F6                   := $88F6
D_8902                   := $8902
D_893E                   := $893E
D_8949                   := $8949
D_8952                   := $8952
D_8953                   := $8953
D_8954                   := $8954
D_8955                   := $8955
D_895D                   := $895D
D_8960                   := $8960
D_8964                   := $8964
D_8966                   := $8966
D_8967                   := $8967
D_8968                   := $8968
D_896B                   := $896B
D_896C                   := $896C
D_896E                   := $896E
D_896F                   := $896F
D_8970                   := $8970
D_8972                   := $8972
D_8974                   := $8974
D_8975                   := $8975
D_8976                   := $8976
D_8977                   := $8977
D_8978                   := $8978
D_8979                   := $8979
D_897A                   := $897A
D_897B                   := $897B
D_897C                   := $897C
gameClock                := $91CB
terrainPointsBySide      := $92AC
rxPacketLength           := $E01E
rxPacketBuffer           := $E020
rxPacketArg1             := $E021
rxPacketArg2             := $E022
rxPacketArg3             := $E023
serviceCommTick          := $E12F
computeTrainerGroupStats := $E531
rearmCarrierTimer        := $E5FF
cellDistance             := $E82A
mirrorBlockCornerForOpponent := $EA38
queueAiCommand           := $EA46
side1UnitColTable        := $F672
side1UnitRowTable        := $F6D6
side1ComcenFlags         := $F76B
side1UnitTypeTable       := $F79E
side1UnitDestColTable    := $F802
side1ComcenDestCol       := $F833
side1UnitDestRowTable    := $F866
side1ComcenDestRow       := $F897
side1ComcenDisplayFlags  := $FBB7
multiply8x8              := $FCCD
divideSigned16by8        := $FCEC
nextGameRandom           := $FD5D

; Contents
; --------
;    $EC0F  initTrainerAi                Builds the whole opening plan of the solo trainer (the computer opponent, side
;                                        1 =
;    $ED8E  rebuildAiGroupTables         Recomputes all of the trainer's per-group and per-unit bookkeeping from the
;    $EEC3  buildAiHeadingSearchOrder    Fills the 8-byte table at $894A with the order in which the resident AI
;    $EEED  orderAiGroupToPoint          Orders AI group Y to move so that its formation centre ends up on column X,
;    $EF00  shiftAiGroupDestinations     Moves a whole AI group without breaking its formation.
;    $EF27  staleDroneSteerTail          DEAD CODE in this overlay.
;    $EFAB  staleDroneTargetPickerTail   34 stale bytes, unreachable and unusable here.

        .org $EC00


; Three parallel 5-byte tables indexed by the AI plan number 0-4 that initTrainerAi derives from the
; game type and keeps in $8966.  They are read together at $ECB9-$ECD1.
mainGroupByPlanTable:
        .byte   $FF,$FF,$04,$04,$04     ; EC00  .....  plan 0-4 -> spearhead group id: none for the two 11-group plans, group 4 for the 5-group plans

; groupCountByPlanTable: byteTable, 5 bytes. Per AI plan 0-4: highest group index ($0A, $0A, 4, 4, 4)
; stored in $8953; $8954 gets this value + 1.
groupCountByPlanTable:
        .byte   $0A,$0A,$04,$04,$04     ; EC05  .....  plan 0-4 -> highest group index: 10 (11 groups) for plans 0/1, 4 (5 groups) for plans 2-4

; aiPlanParamTable: byteTable, 5 bytes. Per AI plan 0-4: drone score threshold ($FF, $FF, 8, $0C, $0C)
; copied to both $8978 and $8979.
aiPlanParamTable:
        .byte   $FF,$FF,$08,$0C,$0C     ; EC0A  .....  plan 0-4 -> drone detonation score threshold: $FF (unreachable score = never detonate) for plans 0/1, 8 or 12 for plans 2-4

; ----------------------------------------------------------------------
; initTrainerAi - Builds the whole opening plan of the solo trainer (the computer opponent, side 1 =
; units 50-99).  Clears the AI's scalar work area in the $8800 block, picks a first objective near the
; human comcen, turns the game type ($0BA3 bits 0-2) into a plan number 0-4, loads the five 150-byte
; opening plays from track 29 sectors 6-8 into $0200-$04FF, copies the chosen play's 50 group ids and
; 50 destination cells into $8832 and unitDestCol/unitDestRow of units 50-99, re-aims every group at a
; random point for the bigger plans, rebuilds the per-group tables and works out the round at which
; the AI changes gear ($896E).
; In:  $0BA3 game type bits 0-2; $91CB game clock (the game length in rounds, put there by
;      setGameClockForType at $7B1A); $92AC TERRAIN PTS setting of side 1; unit arrays unitFlagsTable
;      $F708, unitTypeTable $F76C, unitDestColTable $F7D0, unitDestRowTable $F834; the three plan
;      tables at $EC00/$EC05/$EC0A; the disk, through setLoadDest/loadSectors; nextGameRandom
; Out: $0200-$04FF overwritten with the playbook; the AI work area $8800-$8979 initialised; group ids
;      $8832-$8863 and unitDestCol/unitDestRow of units 50-99 set; unitFlagsTable[99]=$21 and
;      unitDisplayFlags[99]=$20 for plans 4 (FULL WAR / DEFENDER); unitDestCol[99] bits 6-7 cleared
; for
;      THE BOMB; $E01E=0; one $A5,00,$14 setup command queued for DEFENDER; $91CB left at $FF;
;      zp_18-zp_2B and zp_48-zp_4D clobbered
; Called from: $7B30 in game/mapGenerator6F00 - the only call, made once, right after $7B24 loads this
;      overlay (track 29 sectors 0-3) and immediately before $7B33 puts the modem-driver tail (track
; 34
;      sectors 12-15) back over it.  Everything in this file therefore runs exactly once, in a solo
; game
; Clear the trainer's scalar variables.  The whole $8800-$89FF page pair still holds the modem status
; strings loaded from track 29 s9-10, which are dead in a solo game, so every AI variable has to be
; written before it can be read.  X = 0 for the flags and counters, then X = $FF for the index
; variables whose 'none' value is $FF.
; ----------------------------------------------------------------------
initTrainerAi:
        ldx     #$00                    ; EC0F  X = 0: the value for every counter and flag cleared below
        stx     rxPacketLength          ; EC11  no fake incoming packet is pending yet (the comm module's rx packet length)  (also trainerAiE000:rxPacketLength)
L_EC14:
        stx     D_896C                  ; EC14  'human comcen has been spotted' flag = 0; randomiseCoordinate ($E14A) widens its aim while this is clear
        stx     D_8952                  ; EC17  AI command-queue length = 0: the queue at $88A0 is empty
        stx     D_8960                  ; EC1A  safest compass direction from the last threat sweep round the AI comcen = 0
        stx     D_896B                  ; EC1D  rounds left before the AI re-picks its rally point = 0, so it re-picks on the first round
        stx     D_8970                  ; EC20  missile-pass scratch of the resident $EC00 tail = 0
        stx     D_8968                  ; EC23  commands queued by the AI this round = 0
        stx     D_8977                  ; EC26  drone state = 0: no drone airborne and the $EE00 drone-flying overlay is not resident ($FF marks it resident)
        stx     D_897B                  ; EC29  previous-round 'AI comcen destroyed' latch = 0
        stx     D_897C                  ; EC2C  'AI comcen destroyed' flag tested by planTrainerRound = 0
        dex                             ; EC2F  X = $FF: the 'none' value for the index variables that follow
        stx     D_895D                  ; EC30  no spearhead group yet for the AI comcen to escort
        stx     D_8976                  ; EC33  no drone target unit picked
        stx     D_8972                  ; EC36  no player unit is being hunted for having shot at the AI comcen
        stx     D_8964                  ; EC39  no home-defence group yet; a real one is chosen at $EC9C unless the plan is QB SNEAK
L_EC3C:
        stx     D_8978                  ; EC3C  drone detonation score threshold = $FF (never); replaced from aiPlanParamTable at $ECCC
        stx     D_8979                  ; EC3F  and its decaying working copy = $FF
        lda     #$05                    ; EC42  comcen threat threshold
        sta     D_897A                  ; EC44  the resident AI only reacts to pressure on its own comcen once the measured threat reaches 5 ($E29A)
        jsr     serviceCommTick         ; EC47  really trainerAiE000:pickTrainerRallyPoint ($E12F) - the modem driver's name for this address is a collision; it stores a random cell near the human comcen in $8969/$896A as the AI's first objective  (also trainerAiE000:pickTrainerRallyPoint)

; ----------------------------------------------------------------------
; Turn the game type into an AI plan number 0-4 (kept in $8966):
;   0 SCRIMAGE -> plan 0, 1 QB SNEAK -> plan 1 (no home-defence group),
;   2 THE BOMB -> plan 0 with no playbook at all (only the two comcens exist),
;   3 FACE-OFF -> plan 2, 4 SLUGGERS -> plan 3, 5 FULL WAR -> plan 4,
;   6 DEFENDER -> plan 4 plus a queued point award for the defending computer.
; ----------------------------------------------------------------------
        lda     gameTypeOptions         ; EC4A  the game options byte written by the setup screens
        and     #$07                    ; EC4D  bits 0-2 = game type 0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB, 3 FACE-OFF, 4 SLUGGERS, 5 FULL WAR, 6 DEFENDER
        tay                             ; EC4F  Y = game type
        cpy     #$02                    ; EC50  is the game type below 2?
        bcc     L_EC93                  ; EC52  SCRIMAGE and QB SNEAK use the plan number that equals the game type
checkForTheBombGameType:
        bne     L_EC6C                  ; EC54  branch if the game type is 3 or higher; fall through only for game type 2 = THE BOMB

; ----------------------------------------------------------------------
; THE BOMB: both sides own nothing but their comcen, so there is no formation to deploy. Free the AI
; comcen to move and pretend it is the whole of group 0.
; ----------------------------------------------------------------------
        lda     side1ComcenDestCol      ; EC56  ordered destination column of the AI comcen, unitDestCol[99]
        and     #$3F                    ; EC59  keep the 0-39 column only, clearing the idle bit 7 so the engine treats the comcen as still moving
        sta     side1ComcenDestCol      ; EC5B  store it back
L_EC5E:
        ldy     #$00                    ; EC5E  group 0
        sty     D_8863                  ; EC60  group id of AI slot 49 = unit 99, the AI comcen: the only live AI unit in THE BOMB
        sty     D_8953                  ; EC63  highest group index in use = 0
        sty     D_8966                  ; EC66  AI plan number = 0
        jmp     L_ED6C                  ; EC69  skip the playbook load and the group scatter entirely and go straight to the table rebuild


; ----------------------------------------------------------------------
; Game types 3-6.  Y is biased down by one so that FACE-OFF and SLUGGERS map straight onto plans 2 and
; 3, while FULL WAR and DEFENDER both end up on plan 4.
; ----------------------------------------------------------------------
L_EC6C:
        dey                             ; EC6C  Y = gameType - 1: FACE-OFF -> 2, SLUGGERS -> 3, FULL WAR -> 4, DEFENDER -> 5
        cpy     #$04                    ; EC6D  is the biased type below 4?
L_EC6F:
        bcc     L_EC93                  ; EC6F  FACE-OFF -> plan 2, SLUGGERS -> plan 3
        beq     L_EC88                  ; EC71  FULL WAR (biased type 4) -> plan 4, without the extra point award

; ----------------------------------------------------------------------
; DEFENDER only: stage command $A5 (cmdSetupOption, $55A5) with kind 0 and value 20 and queue it as if
; the remote player had sent it, so the handler adds 20 x 10 = 200 purchase points to the AI's side
; score - the handicap that pays for the defending computer's extra force.
; ----------------------------------------------------------------------
        lda     #$A5                    ; EC73  command byte $A5 = cmdSetupOption
        sta     D_8896                  ; EC75  byte 0 of the AI's 3-byte command scratch
        lda     #$00                    ; EC78  argument 1 = kind 0: 'add points to the sender's side'
        sta     msgPickUpPhoneThenPressSpace; EC7A  byte 1 of the command scratch; the msg... label here is a $8800 message-string address collision
        lda     #$14                    ; EC7D  argument 2 = 20 points
        sta     D_8898                  ; EC7F  byte 2 of the command scratch
L_EC82:
        jsr     queueAiCommand          ; EC82  append the staged 3-byte command to the AI's outgoing queue at $88A0
        dec     D_8968                  ; EC85  undo the 'commands queued this round' bump queueAiCommand made: this is a setup option, not a battle order

; ----------------------------------------------------------------------
; FULL WAR and DEFENDER: reset the AI comcen's own flags.  unitDisplayFlags mirrors bits 2-6 of
; unitFlagsTable, so $21/$20 means facing 1, side bit 5 set (side 1), no group membership, and the
; idle bit 7 clear - i.e. the comcen counts as still moving (probably so it relocates during these two
; long games).
; ----------------------------------------------------------------------
L_EC88:
        ldy     #$21                    ; EC88  $21 = facing 1 (bits 0-1), group id 0 (bits 2-4), side 1 (bit 5), not in a group (bit 6 clear)
        sty     side1ComcenFlags        ; EC8A  unitFlagsTable[99] - the AI comcen
        dey                             ; EC8D  $20 = the same value with the facing bits stripped, which is what the engine mirrors into the display flags
        sty     side1ComcenDisplayFlags ; EC8E  unitDisplayFlags[99]: bit 7 (arrived/idle) clear, bit 6 (group order active) clear
usePlanFour:
        ldy     #$04                    ; EC91  FULL WAR and DEFENDER both use plan 4

; ----------------------------------------------------------------------
; Record the plan and choose the home-defence group: a random group 1, 2 or 3 that will be parked on
; the AI comcen instead of joining the attack.  QB SNEAK deliberately keeps none back.
; ----------------------------------------------------------------------
L_EC93:
        sty     D_8966                  ; EC93  remember the plan number 0-4 for the rest of the setup and for the resident AI
        lda     #$FF                    ; EC96  $FF = 'no home-defence group'
        cpy     #$01                    ; EC98  is this plan 1 (QB SNEAK)?
L_EC9A:
        beq     L_ECA3                  ; EC9A  QB SNEAK throws everything forward: leave the guard group as $FF
L_EC9C:
        jsr     nextGameRandom          ; EC9C  roll for a guard group
        and     #$03                    ; EC9F  keep the low two bits: 0-3
        beq     L_EC9C                  ; ECA1  reject 0 - group 0 is the one that follows the spearhead, so it is never the guard; re-roll
L_ECA3:
        sta     D_8964                  ; ECA3  home-defence group id 1-3

; ----------------------------------------------------------------------
; Load the AI playbook: track 29 sectors 6-8 -> $0200-$04FF, over the terrain tile rules (which the
; map generator has already finished with) and the copy-protection answer table.  The 768 bytes hold
; five 150-byte records, one per plan: 50 group ids, then 50 destination columns, then 50 destination
; rows, all indexed by AI slot 0-49 = unit 50-99.  The caller has already bumped diskLoadDepth
; ($7B17).
; ----------------------------------------------------------------------
L_ECA6:
        ldx     #$00                    ; ECA6  destination low byte = $00
        ldy     #$02                    ; ECA8  destination high byte = $02, i.e. load to $0200
        jsr     setLoadDest             ; ECAA  tell the fast loader where the next sectors go
        ldy     #$1D                    ; ECAD  track 29
        ldx     #$06                    ; ECAF  first sector 6
        lda     #$03                    ; ECB1  three sectors = 768 bytes = the five opening plays
        jsr     loadSectors             ; ECB3  pull them in over $0200-$04FF

; ----------------------------------------------------------------------
; Copy the plan's fixed parameters out of the three tables at the top of this overlay.
; ----------------------------------------------------------------------
        ldy     D_8966                  ; ECB6  Y = plan number, the index into all three 5-byte tables (and still in Y when multiply8x8 is called at $ECD4)
        lda     mainGroupByPlanTable,y  ; ECB9  spearhead group for this plan
        sta     D_895D                  ; ECBC  the group the AI comcen escorts and that group 0 is told to follow; $FF for plans 0/1
        ldx     groupCountByPlanTable,y ; ECBF  highest group index used by this plan
        stx     D_8953                  ; ECC2  store it
        inx                             ; ECC5  +1
        stx     D_8954                  ; ECC6  number of groups (11 or 5), the loop limit the resident AI uses at $E6A5/$E960/$EB3C
        lda     aiPlanParamTable,y      ; ECC9  drone detonation score threshold for this plan
        sta     D_8978                  ; ECCC  the fixed threshold
        sta     D_8979                  ; ECCF  and the working copy that the drone code decays to 3/4 as it gets desperate

; ----------------------------------------------------------------------
; Point three zero-page pointers at the three 50-byte arrays of the selected 150-byte plan record:
;   mapCellPtr ($48) -> group ids,  fillEndPtr ($4A) -> destination columns,  planRowPtr ($4C) ->
; rows.
; Base = $0200 + 150 * planNumber.
; ----------------------------------------------------------------------
        lda     #$96                    ; ECD2  150 = the size of one plan record
buildPlanRecordPointers:
        jsr     multiply8x8             ; ECD4  A x Y = 150 x plan number; returns the product low byte in A and the high byte in Y
        sta     mapCellPtr              ; ECD7  low byte of the record's offset into the playbook
        iny                             ; ECD9  add $100 ...
        iny                             ; ECDA  ... and another $100, because the playbook was loaded at $0200
        sty     mapCellPtrHi            ; ECDB  mapCellPtr now points at the record's 50 group ids
        clc                             ; ECDD  clear carry for the add below
L_ECDE:
        adc     #$32                    ; ECDE  +50 bytes: past the group ids
        bcc     L_ECE3                  ; ECE0  no page crossing
        iny                             ; ECE2  carry into the high byte
L_ECE3:
        sta     fillEndPtr              ; ECE3  fillEndPtr points at the record's 50 destination columns
        sty     fillEndPtrHi            ; ECE5  high byte of that pointer
L_ECE7:
        clc                             ; ECE7  clear carry again
        adc     #$32                    ; ECE8  +50 more bytes: past the destination columns
        bcc     L_ECED                  ; ECEA  no page crossing
        iny                             ; ECEC  carry into the high byte
L_ECED:
        sta     planRowPtr              ; ECED  planRowPtr points at the record's 50 destination rows
        sty     planRowPtrHi            ; ECEF  high byte of that pointer

; ----------------------------------------------------------------------
; Copy the plan into the live arrays, AI slot 49 down to 0 (units 99 down to 50).
; ----------------------------------------------------------------------
        ldy     #$31                    ; ECF1  49 = the last AI slot; the loop counts down to 0
copyPlanRecordLoop:
        lda     (mapCellPtr),y          ; ECF3  group id of this unit in the chosen opening play
        sta     D_8832,y                ; ECF5  $8832 + slot = the AI's own group-id array (the resident AI addresses it as $8800 + absolute unit index)
        lda     (fillEndPtr),y          ; ECF8  destination column from the play
        sta     side1UnitDestColTable,y ; ECFA  unitDestCol[50 + slot]
L_ECFD:
        lda     (planRowPtr),y          ; ECFD  destination row from the play
        sta     side1UnitDestRowTable,y ; ECFF  unitDestRow[50 + slot]
        dey                             ; ED02  previous slot
        bpl     copyPlanRecordLoop      ; ED03  loop until all 50 AI units have their group and destination
        lda     D_8966                  ; ED05  which plan is this?
        cmp     #$02                    ; ED08  plans 2, 3 and 4 re-aim every group; plans 0 and 1 keep the playbook's own destinations
L_ED0A:
        bcc     L_ED6C                  ; ED0A  SCRIMAGE / QB SNEAK: nothing more to place, go and rebuild the tables

; ----------------------------------------------------------------------
; Plans 2-4 only: scatter the groups.  Every group from the highest down to 1 is given a random target
; - any column 4-35, and a row in an 8-row band 7 rows above the group's own average ordered row - and
; the whole formation is translated there.  Group 0 is handled separately afterwards: it is sent to
; the same point as the spearhead group.
; ----------------------------------------------------------------------
        lda     D_8953                  ; ED0C  highest group index of this plan
        sta     D_8967                  ; ED0F  group loop counter, counting down to 0
L_ED12:
        ldy     D_8967                  ; ED12  Y = the group being placed
        jsr     computeTrainerGroupStats; ED15  measure group Y ($E531): member count, combat value, bounding box, centre and average ordered cell; C=1 if the group is empty (then zp_1F below is stale, but the group has no members to move either)
L_ED18:
        jsr     nextGameRandom          ; ED18  roll a column
        and     #$1F                    ; ED1B  0-31 ...
        clc                             ; ED1D  clear carry for the add below
        adc     #$04                    ; ED1E  ... + 4 = column 4-35, keeping the formation clear of both map edges
        sta     cellColA                ; ED20  zp_22 = wanted formation centre column
L_ED22:
        sta     D_88D2,y                ; ED22  and remember it as group Y's target column
L_ED25:
        lda     scratch1F               ; ED25  zp_1F = the group's average ordered row, from computeTrainerGroupStats
        cmp     #$10                    ; ED27  is that row above 16?
        bcs     L_ED2D                  ; ED29  no clamp needed
        lda     #$0F                    ; ED2B  floor it at 15 so the band computed below cannot go negative
L_ED2D:
        sec                             ; ED2D  prepare to subtract
        sbc     #$07                    ; ED2E  start the 8-row band 7 rows before the group's own row
L_ED30:
        sta     scratch18               ; ED30  base row of the band
rollGroupTargetRowJitter:
        jsr     nextGameRandom          ; ED32  roll a row offset
D_ED35:
        and     #$07                    ; ED35  0-7 rows of jitter inside the band
        clc                             ; ED37  clear carry for the add
        adc     scratch18               ; ED38  base + jitter
        sta     cellRowA                ; ED3A  zp_24 = wanted formation centre row
        sta     D_88DE,y                ; ED3C  and remember it as group Y's target row
        jsr     shiftAiGroupDestinations; ED3F  clip the point to the map and shift every member of the group by the same delta
L_ED42:
        dec     D_8967                  ; ED42  next group down
        bmi     L_ED59                  ; ED45  counter went negative: every group has been placed
        bne     L_ED12                  ; ED47  still above group 0: keep scattering

; ----------------------------------------------------------------------
; The counter has reached group 0.  Group 0 is the follow-up wave: instead of a random point it is
; ordered to the spearhead group's target, so the two arrive together.
; ----------------------------------------------------------------------
        ldy     D_895D                  ; ED49  spearhead group id for this plan
        bmi     L_ED12                  ; ED4C  $FF = no spearhead (only possible for plans 0/1, which never get here): fall back to a random point
        ldx     D_88D2,y                ; ED4E  the spearhead group's target column
        lda     D_88DE,y                ; ED51  and its target row
L_ED54:
        ldy     #$00                    ; ED54  group 0
        jsr     orderAiGroupToPoint     ; ED56  order group 0 to the same cell

; ----------------------------------------------------------------------
; Finally station the home-defence group on the AI comcen's own destination.
; ----------------------------------------------------------------------
L_ED59:
        ldy     D_8964                  ; ED59  home-defence group id
        bmi     L_ED6C                  ; ED5C  $FF: QB SNEAK and THE BOMB keep nobody back
        lda     side1ComcenDestCol      ; ED5E  AI comcen's ordered column, unitDestCol[99]
L_ED61:
        and     #$3F                    ; ED61  strip the idle bit 7 to leave a plain 0-39 column
L_ED63:
        tax                             ; ED63  X = target column, as orderAiGroupToPoint expects
        lda     side1ComcenDestRow      ; ED64  AI comcen's ordered row, unitDestRow[99]
        and     #$3F                    ; ED67  strip the idle bit
        jsr     orderAiGroupToPoint     ; ED69  order the guard group (still in Y) onto the comcen

; ----------------------------------------------------------------------
; Common tail for every plan: rebuild the per-group bookkeeping, then decide when the AI changes gear.
; gameClock counts DOWN from the game length, so subtracting 32-95 gives the clock value the game will
; reach after that many rounds; the resident AI compares $91CB against $896E at $E3E5 and
; $E765.  For plans 0/1 the value is the full game length, which makes those comparisons true for the
; whole game.
; ----------------------------------------------------------------------
L_ED6C:
        jsr     rebuildAiGroupTables    ; ED6C  build the group averages, the per-unit activation clocks and the heading search order
L_ED6F:
        jsr     nextGameRandom          ; ED6F  roll the delay before the AI changes gear
        and     #$3F                    ; ED72  0-63 ...
        ora     #$20                    ; ED74  ... with bit 5 forced on: 32-95 rounds
        sta     scratch18               ; ED76  keep it
L_ED78:
        lda     gameClock               ; ED78  the game length in rounds (setGameClockForType at $7B1A left it here for us: 63/63/31/127/127/254/254 by game type)
        ldy     D_8966                  ; ED7B  which plan?
        cpy     #$02                    ; ED7E  plans 0 and 1 ...
        bcc     L_ED85                  ; ED80  ... use the whole game length unchanged
        sec                             ; ED82  prepare to subtract
        sbc     scratch18               ; ED83  plans 2-4: the switch happens once 32-95 rounds have been played
L_ED85:
        sta     D_896E                  ; ED85  the clock value at which the AI's second phase begins
        lda     #$FF                    ; ED88  $FF = 'clock not running'
        sta     gameClock               ; ED8A  put the game clock back the way the setup screen left it at $7AF2; the real countdown is loaded again at $7B86 once play starts
L_ED8D:
        rts                             ; ED8D  back to $7B33, which immediately reloads the modem-driver tail over this overlay


; ----------------------------------------------------------------------
; rebuildAiGroupTables - Recomputes all of the trainer's per-group and per-unit bookkeeping from the
; current unit arrays.  Clears the twelve group slots and gives each a random 0-15 round start delay,
; then walks AI slots 49-0 accumulating each live unit's ordered destination and current cell into its
; group's 16-bit sums, stamping the unit's activation clock, flagging units that already stand on
; their destination, and dropping dead units out of their group.  Finally it divides the four sums by
; the member count so the group arrays hold averages: $88D2/$88DE = average ordered destination,
; $88EA/$88F6 = centre of the group as it stands now.
; In:  $8832 group id per AI slot, $8953 highest group, $8964 home-defence group, $8966 plan number,
;      $91CB game length, $92AC TERRAIN PTS of side 1, unitColTable+50 $F672, unitRowTable+50 $F6D6,
;      unitTypeTable+50 $F79E, unitDestColTable+50 $F802, unitDestRowTable+50 $F866, nextGameRandom
; Out: group arrays $88D2/$88DE/$88EA/$88F6 (averages), $8902 member count, $893E start delay and the
;      matching sum high bytes at $0464/$0478/$048C/$04A0, 12 entries each; $8800-$8831 activation
;      clock per AI slot; $8864-$8895 zone flag per AI slot; $8832 entries of dead units set to $FF;
;      bit 7 of unitDestCol set for units already at their destination; zp_18/1A/1B/1C/22/24/27/28
; used
; Called from: $ED6C in initTrainerAi (this file) - the only caller in this overlay
; Clear the twelve group slots.  The 16-bit sums are split: the low bytes live in the $8800 block next
; to the other group arrays, the high bytes in the $0400 page (which held the copy-protection answer
; table and the scan lists and is free once a game is running).  The D_0464/D_0478/D_048C/
; writeSectorToDisk labels are address collisions from the $0400 unit and mean nothing here.
; ----------------------------------------------------------------------
rebuildAiGroupTables:
        jsr     buildAiHeadingSearchOrder; ED8E  refresh the random left/right heading search order at $894A
        ldy     #$0B                    ; ED91  group slots 11 down to 0 (only 0..$8953 are really used, but all twelve are cleared)
L_ED93:
        lda     #$00                    ; ED93  the value written into all nine group arrays below
        sta     play4Grp12,y            ; ED95  high byte of the group's ordered-column sum
        sta     play4Grp32,y            ; ED98  high byte of the ordered-row sum
        sta     play4Col02,y            ; ED9B  high byte of the current-column sum
        sta     play4Col22,y            ; ED9E  $04A0: high byte of the current-row sum (the writeSectorToDisk label is a $0400-page collision)
        sta     D_8902,y                ; EDA1  member count
        sta     D_88D2,y                ; EDA4  low byte of the ordered-column sum, which becomes the average
        sta     D_88DE,y                ; EDA7  low byte of the ordered-row sum
        sta     D_88EA,y                ; EDAA  low byte of the current-column sum
        sta     D_88F6,y                ; EDAD  low byte of the current-row sum
        jsr     nextGameRandom          ; EDB0  roll this group's start delay
        and     #$0F                    ; EDB3  0-15 rounds
        sta     D_893E,y                ; EDB5  the group waits that many rounds before it starts obeying its orders
        dey                             ; EDB8  next group down
        bpl     L_ED93                  ; EDB9  loop over all twelve group slots

; ----------------------------------------------------------------------
; Walk the 50 AI slots (unit 50+slot), 49 down to 0.  Each live unit contributes to its group's four
; running sums and gets its own activation clock and idle flag; each dead one is unhooked from its
; group.
; ----------------------------------------------------------------------
        lda     #$31                    ; EDBB  49 = the last AI slot
        sta     scratch18               ; EDBD  zp_18 = the AI slot being processed
L_EDBF:
        ldy     scratch18               ; EDBF  Y = AI slot
        lda     terrainPointsBySide     ; EDC1  $92AC = the TERRAIN PTS setting of side 1 (the AI), 0-3, set by the setup screens
        bne     L_EDC8                  ; EDC4  terrain points are switched on
        lda     #$80                    ; EDC6  switched off: force the flag on for every unit
L_EDC8:
        and     #$80                    ; EDC8  with points on the value is 1-3, so bit 7 is clear and the flag ends up 0
L_EDCA:
        sta     D_8864,y                ; EDCA  per-unit zone flag; computeTrainerGroupStats ($E579) counts the members whose bit 7 is CLEAR, i.e. the ones that still have a zone bonus to go and earn
        lda     side1UnitTypeTable,y    ; EDCD  type byte of this AI unit
        bpl     L_EDDC                  ; EDD0  bit 7 = dead; branch for a live unit
L_EDD2:
        lda     #$FF                    ; EDD2  $FF = 'in no group'
        sta     D_8832,y                ; EDD4  a dead unit is unhooked from its group so no later pass counts it
        lda     #$00                    ; EDD7  leftover: A is never used, the jump target only decrements the slot counter
        jmp     L_EE6B                  ; EDD9  on to the next AI slot


; ----------------------------------------------------------------------
; Live unit: add its ordered destination and its current cell to its group's four 16-bit sums, and
; leave both cells in the zero page for the cellDistance test further down.
; ----------------------------------------------------------------------
L_EDDC:
        ldx     D_8832,y                ; EDDC  X = the unit's group id
        inc     D_8902,x                ; EDDF  one more member in that group
        lda     side1UnitDestColTable,y ; EDE2  ordered destination column
        and     #$3F                    ; EDE5  strip the idle bit 7 to leave a plain 0-39 column
        sta     cellColB                ; EDE7  zp_27 = point B for the cellDistance call at $EE5C
        clc                             ; EDE9  clear carry before the 16-bit add
        adc     D_88D2,x                ; EDEA  add into the group's ordered-column sum ...
        sta     D_88D2,x                ; EDED  ... low byte
        lda     #$00                    ; EDF0  A = 0: only the carry is being added on
        adc     play4Grp12,x            ; EDF2  propagate the carry ...
        sta     play4Grp12,x            ; EDF5  ... into the high byte at $0464
        lda     side1UnitDestRowTable,y ; EDF8  ordered destination row
        and     #$3F                    ; EDFB  strip the idle bit
        sta     cellRowB                ; EDFD  zp_28 = row of point B
        clc                             ; EDFF  clear carry before the 16-bit add
sub_EE00:
        adc     D_88DE,x                ; EE00  add into the group's ordered-row sum ...
        sta     D_88DE,x                ; EE03  ... low byte
        lda     #$00                    ; EE06  A = 0: only the carry is being added on
        adc     play4Grp32,x            ; EE08  propagate the carry ...
        sta     play4Grp32,x            ; EE0B  ... into the high byte at $0478
        lda     side1UnitColTable,y     ; EE0E  the unit's current column, unitCol[50 + slot]
        sta     cellColA                ; EE11  zp_22 = point A for cellDistance
        clc                             ; EE13  clear carry before the 16-bit add
L_EE14:
        adc     D_88EA,x                ; EE14  add into the group's current-column sum ...
        sta     D_88EA,x                ; EE17  ... low byte
        lda     play4Col02,x            ; EE1A  high byte at $048C ...
        adc     #$00                    ; EE1D  ... plus the carry
        sta     play4Col02,x            ; EE1F  store it back
        lda     side1UnitRowTable,y     ; EE22  the unit's current row, unitRow[50 + slot]
        sta     cellRowA                ; EE25  zp_24 = row of point A
        clc                             ; EE27  clear carry before the 16-bit add
L_EE28:
        adc     D_88F6,x                ; EE28  add into the group's current-row sum ...
        sta     D_88F6,x                ; EE2B  ... low byte
        lda     play4Col22,x            ; EE2E  high byte at $04A0 ...
        adc     #$00                    ; EE31  ... plus the carry
        sta     play4Col22,x            ; EE33  store it back

; ----------------------------------------------------------------------
; Stamp the unit's activation clock.  The clock counts down, so subtracting the group's 0-15 round
; delay produces the clock value at which this unit wakes up.  SCRIMAGE (plan 0) starts everybody at
; once, and so do group 0 and the home-defence group in every plan.
; ----------------------------------------------------------------------
        ldy     #$00                    ; EE36  Y = 0 = 'stagger the groups'
        lda     D_8966                  ; EE38  which plan?
        bne     L_EE3E                  ; EE3B  any plan but 0 staggers the start
        dey                             ; EE3D  plan 0 (SCRIMAGE): Y = $FF = 'no staggered start'
L_EE3E:
        sty     scratch1A               ; EE3E  zp_1A holds the marker; only its bit 7 is tested
        ldy     scratch18               ; EE40  Y = the AI slot again
        ldx     D_8832,y                ; EE42  X = its group id
        lda     gameClock               ; EE45  the full game length in rounds - the clock value at the moment play starts
        bit     scratch1A               ; EE48  test bit 7 of the plan-0 marker without disturbing A
checkGroupStartsAtOnce:
        bmi     L_EE59                  ; EE4A  plan 0: this unit is active from round one
        cpx     D_8964                  ; EE4C  is this the home-defence group?
        beq     L_EE59                  ; EE4F  the guard group never waits - it has to be in place from the start
        cpx     #$00                    ; EE51  is this group 0?
        beq     L_EE59                  ; EE53  group 0 follows the spearhead and never waits either
applyGroupStartDelay:
        sec                             ; EE55  prepare to subtract
        sbc     D_893E,x                ; EE56  otherwise subtract the group's 0-15 round delay from the clock
L_EE59:
        sta     msgGetOpponentOnPhoneAnd,y; EE59  $8800 + slot = the clock value at which this unit starts obeying orders (the msg... label is a $8800 message-string address collision)

; ----------------------------------------------------------------------
; A unit that is already standing on its ordered destination gets the idle bit set, so the engine will
; not spend movement points shuffling it.
; ----------------------------------------------------------------------
        jsr     cellDistance            ; EE5C  distance between the unit's cell (zp_22/zp_24) and its ordered destination (zp_27/zp_28)
        cmp     #$00                    ; EE5F  redundant - cellDistance already returns with the flags set from its own TXA
        bne     L_EE6B                  ; EE61  still has somewhere to go
        lda     side1UnitDestColTable,y ; EE63  its ordered column
        ora     #$80                    ; EE66  set bit 7 = 'arrived / idle'
        sta     side1UnitDestColTable,y ; EE68  store it back
L_EE6B:
        dec     scratch18               ; EE6B  next AI slot down
        bmi     L_EE72                  ; EE6D  the counter went negative: all 50 slots done
        jmp     L_EDBF                  ; EE6F  round again (too far for a branch)


; ----------------------------------------------------------------------
; Turn the four 16-bit sums of every group 0..$8953 into averages by dividing by the member count. A
; group with no members divides by zero here; the quotient is meaningless but the group has no units
; for anything to act on either.
; ----------------------------------------------------------------------
L_EE72:
        lda     D_8953                  ; EE72  highest group index in use
        sta     scratch1B               ; EE75  zp_1B = the group being averaged, counting down to 0
L_EE77:
        ldx     scratch1B               ; EE77  X = group index
        lda     D_8902,x                ; EE79  how many members it has
        sta     scratch1C               ; EE7C  zp_1C = the divisor, reloaded before each of the four divisions
        ldy     play4Grp12,x            ; EE7E  high byte of the ordered-column sum
        lda     D_88D2,x                ; EE81  low byte
        ldx     scratch1C               ; EE84  X = divisor
        jsr     divideSigned16by8       ; EE86  Y:A / X
        ldx     scratch1B               ; EE89  X = group index again
L_EE8B:
        sta     D_88D2,x                ; EE8B  the group's average ordered column
        ldy     play4Grp32,x            ; EE8E  high byte of the ordered-row sum
        lda     D_88DE,x                ; EE91  low byte
        ldx     scratch1C               ; EE94  X = divisor
        jsr     divideSigned16by8       ; EE96  Y:A / X
        ldx     scratch1B               ; EE99  X = group index
        sta     D_88DE,x                ; EE9B  the group's average ordered row
        ldy     play4Col02,x            ; EE9E  high byte of the current-column sum
L_EEA1:
        lda     D_88EA,x                ; EEA1  low byte
        ldx     scratch1C               ; EEA4  X = divisor
        jsr     divideSigned16by8       ; EEA6  Y:A / X
        ldx     scratch1B               ; EEA9  X = group index
        sta     D_88EA,x                ; EEAB  the group's current centre column
        ldy     play4Col22,x            ; EEAE  high byte of the current-row sum at $04A0
        lda     D_88F6,x                ; EEB1  low byte
        ldx     scratch1C               ; EEB4  X = divisor
L_EEB6:
        jsr     divideSigned16by8       ; EEB6  Y:A / X
storeGroupCentreRow:
        ldx     scratch1B               ; EEB9  X = group index
        sta     D_88F6,x                ; EEBB  the group's current centre row
L_EEBE:
        dec     scratch1B               ; EEBE  next group down
        bpl     L_EE77                  ; EEC0  loop until group 0 has been averaged
        rts                             ; EEC2  done


; ----------------------------------------------------------------------
; buildAiHeadingSearchOrder - Fills the 8-byte table at $894A with the order in which the resident AI
; tries headings: 0 first, then +/-1, +/-2, +/-3 and finally 4 (the opposite heading), every value
; folded into 0-7.  The sign of each pair is decided by a coin toss, so a given game has a consistent
; random left-or-right bias.  Because -4 == +4 mod 8 the last entry is always 4, and the first entry
; is always 0 (its 'negate' at $EED4 acts on a magnitude that is still zero).
; In:  nextGameRandom
; Out: $894A-$8951 = eight heading offsets; zp_18 scratch; A and Y clobbered
; Called from: $ED8E in rebuildAiGroupTables (this file) - the only caller in this overlay
; ----------------------------------------------------------------------
buildAiHeadingSearchOrder:
        lda     #$00                    ; EEC3  start from a zero heading offset
        sta     scratch18               ; EEC5  zp_18 = the running heading offset, starting at 0
        ldy     #$01                    ; EEC7  Y = 1 writes the first entry, $8949+1 = $894A; the loop runs Y = 1..8
L_EEC9:
        tya                             ; EEC9  A = the entry number
        lsr     a                       ; EECA  A = Y/2 = the magnitude of this pair, and C = 1 on the second (odd) entry of each pair
        bcs     L_EED4                  ; EECB  second entry of a pair: reuse the magnitude with the opposite sign
        sta     scratch18               ; EECD  first entry of a pair: adopt the new magnitude 0, 1, 2, 3 or 4
        jsr     nextGameRandom          ; EECF  toss a coin ...
        bpl     storeHeadingOffset      ; EED2  ... heads (bit 7 clear): take this pair positive first, tails: fall through and negate
L_EED4:
        lda     scratch18               ; EED4  negate the current magnitude ...
        eor     #$FF                    ; EED6  ... one's complement ...
        clc                             ; EED8  prepare the +1 of the two's complement negate
        adc     #$01                    ; EED9  ... plus one
        sta     scratch18               ; EEDB  keep the signed offset
storeHeadingOffset:
        lda     scratch18               ; EEDD  the signed offset, -4..+4
L_EEDF:
        clc                             ; EEDF  clear carry for the bias below
sub_EEE0:
        adc     #$08                    ; EEE0  bias by 8 so a negative offset stays positive ...
        and     #$07                    ; EEE2  ... and fold it into a heading 0-7
        sta     D_8949,y                ; EEE4  $8949+Y, i.e. $894A..$8951
        iny                             ; EEE7  next entry
        cpy     #$09                    ; EEE8  eight entries written?
        bcc     L_EEC9                  ; EEEA  keep going while Y is below 9
        rts                             ; EEEC  done


; ----------------------------------------------------------------------
; orderAiGroupToPoint - Orders AI group Y to move so that its formation centre ends up on column X,
; row A.  The two coordinates are stashed by patching the immediate operands of the two LDA # at
; $EEF8 and $EEFC, because computeTrainerGroupStats clobbers A, X and Y.  An empty group is left
; alone.  Falls into shiftAiGroupDestinations.
; In:  X = target column, A = target row, Y = group index; $8832 group ids, the unit arrays
; Out: self-modifies $EEF9 and $EEFD (the operand bytes); $8955 = Y; the group's target ($88D2/$88DE)
;      and the unitDestCol/unitDestRow of every member are updated; returns with C=1 and nothing
;      changed when the group has no live member
; Called from: $ED56 and $ED69 in initTrainerAi (this file)
; Self-modifying code: $EEED writes the target column into the operand byte $EEF9 (of the LDA # at
; $EEF8) and $EEF0 writes the target row into $EEFD (of the LDA # at $EEFC).  The $FF operands shown
; in the listing are just what the overlay was assembled with.
; ----------------------------------------------------------------------
orderAiGroupToPoint:
        stx     L_EEF8+1                ; EEED  park the target column in the operand byte of the LDA # at $EEF8
        sta     L_EEFC+1                ; EEF0  park the target row in the operand byte of the LDA # at $EEFC
        jsr     computeTrainerGroupStats; EEF3  measure group Y ($E531): member count, bounding box, centre and average ordered cell; clobbers A/X/Y
        bcs     L_EF26                  ; EEF6  C=1 means the group has no live member - nothing to order
L_EEF8:
        lda     #$FF                    ; EEF8  operand patched at $EEED = the target column
        sta     cellColA                ; EEFA  zp_22 = wanted formation centre column
L_EEFC:
        lda     #$FF                    ; EEFC  operand patched at $EEF0 = the target row
        sta     cellRowA                ; EEFE  zp_24 = wanted formation centre row

; ----------------------------------------------------------------------
; shiftAiGroupDestinations - Moves a whole AI group without breaking its formation.  The wanted centre
; in zp_22/zp_24 is first clipped so the group's bounding box still fits on the 40x40 map and stored
; as the group's target; the resulting shift is then added to the ordered destination of every unit
; whose group id matches $8955, so the formation is translated as a block.  The AND #$3F on the way
; also clears the idle bit 7, which restarts any member that had already arrived.
; In:  zp_22/zp_24 = wanted centre, $8955 = the group (set by computeTrainerGroupStats), $8832 group
;      ids, unitDestColTable+50 $F802, unitDestRowTable+50 $F866, the group's box
; $890E/$891A/$8926/$8932
; Out: $88D2/$88DE = the group's clipped target, unitDestCol/unitDestRow of every member shifted,
;      zp_2A/zp_2B = the shift applied, zp_22/zp_24 clipped, $8958
; Called from: $ED3F in initTrainerAi and by falling out of orderAiGroupToPoint (both in this file)
; ----------------------------------------------------------------------
shiftAiGroupDestinations:
        jsr     rearmCarrierTimer       ; EF00  really trainerAiE000:clampGroupTargetToMap ($E5FF) - the modem driver's name for this address is a collision.  It clips zp_22/zp_24 so the whole formation box stays on the map, stores the result as the group's target in $88D2/$88DE and returns the shift in zp_2A (columns) and zp_2B (rows)  (also trainerAiE000:clampGroupTargetToMap)
        ldy     #$31                    ; EF03  AI slots 49 down to 0
L_EF05:
        lda     D_8832,y                ; EF05  group id of this AI unit
        cmp     D_8955                  ; EF08  the group computeTrainerGroupStats last measured
        bne     L_EF23                  ; EF0B  not a member: skip it
        lda     side1UnitDestColTable,y ; EF0D  its ordered column
stripIdleBitFromDestCol:
        and     #$3F                    ; EF10  keep the 0-39 column, dropping the idle bit 7 so the unit is set moving again
        clc                             ; EF12  clear carry for the shift add
        adc     scratch2A               ; EF13  apply the group's column shift
        sta     side1UnitDestColTable,y ; EF15  store the new ordered column
        lda     side1UnitDestRowTable,y ; EF18  its ordered row
        and     #$3F                    ; EF1B  keep the 0-39 row only
        clc                             ; EF1D  clear carry for the shift add
        adc     scratch2B               ; EF1E  apply the group's row shift
L_EF20:
        sta     side1UnitDestRowTable,y ; EF20  store the new ordered row
L_EF23:
        dey                             ; EF23  next AI slot
        bpl     L_EF05                  ; EF24  loop over all 50
L_EF26:
        rts                             ; EF26  done (also the early exit for an empty group)


; ----------------------------------------------------------------------
; staleDroneSteerTail - DEAD CODE in this overlay.  From $EF27 to $EFFF the track 29 sector 0-3 image
; is byte for byte identical to the track 29 sector 4-5 drone-flying overlay (game/droneAiEE00), and
; these bytes are the tail of that overlay's steerDroneAndEmitMove, whose real entry point is $EF0F.
; Nothing in this file reaches $EF27 - the routine above ends with an RTS at $EF26 - and the whole
; page is thrown away again by $7B33 a few instructions after initTrainerAi returns. What the code
; does where it is live: turns the drone heading $896F by the step in X (C=1 forces one step
; anticlockwise), converts the new heading into a column/row delta through headingToDirectionTable
; ($7316), diagonalDirectionTable ($1FB2) and directionDeltaTable ($20F0), advances the drone by one
; cell, and then writes a 4-byte command $8D (cmdDroneMove: heading, column, row) straight into the
; comm module's INCOMING packet buffer at $E020 with $E01E = 4 - i.e. the trainer hands the engine a
; packet as though the remote player had sent it.  The coordinates are pre-mirrored so that the
; engine's mirror-and-decrement of a remote $8D lands them back on the right cell.
; In:  X = heading change, C = 1 forces -1, $896F drone heading, $8974/$8975 drone column/row
; Out: $896F, $8974/$8975 advanced one cell, $E020-$E023 = $8D + heading + mirrored column/row,
;      $E01E = 4
; Called from: nothing in this file (live at $EF0F-$EF9D in game/droneAiEE00)
; ----------------------------------------------------------------------
staleDroneSteerTail:
        bcc     L_EF2B                  ; EF27  carry clear: use the turn amount the caller put in X
        ldx     #$FF                    ; EF29  carry set: turn one step anticlockwise
L_EF2B:
        txa                             ; EF2B  A = the signed turn
L_EF2C:
        clc                             ; EF2C  clear carry for the heading add
        adc     D_896F                  ; EF2D  add it to the current drone heading
        and     #$07                    ; EF30  wrap into 0-7 (8 compass points)
        sta     D_896F                  ; EF32  the new heading
L_EF35:
        ldy     D_896F                  ; EF35  Y = heading 0-7
L_EF38:
        lda     headingToDirectionTable,y; EF38  $7316 in the $6F00 overlay: heading -> direction code (0-1 row only, 2-3 column only, 4-7 diagonal)  (also mapGenerator6F00:forestBlockCompareNe)
        ldy     #$00                    ; EF3B  Y = row delta, assume 0
        ldx     #$00                    ; EF3D  X = column delta, assume 0
        cmp     #$04                    ; EF3F  is the code a diagonal?
        bcs     L_EF57                  ; EF41  codes 4-7 move on both axes
        cmp     #$02                    ; EF43  is the code a pure-column move?
        bcs     L_EF4F                  ; EF45  codes 2-3 move along the column only
        tay                             ; EF47  codes 0-1: row only
L_EF48:
        lda     directionDeltaTable,y   ; EF48  $20F0: direction code -> signed delta
        tay                             ; EF4B  Y = row delta
        jmp     L_EF6D                  ; EF4C  on to the position update

L_EF4F:
        tax                             ; EF4F  codes 2-3
        lda     directionDeltaTable,x   ; EF50  $20F0: direction code -> signed delta
L_EF53:
        tax                             ; EF53  X = column delta
        jmp     L_EF6D                  ; EF54  on to the position update

L_EF57:
        tay                             ; EF57  codes 4-7
L_EF58:
        lda     diagonalDirectionTable,y; EF58  $1FB2: packs the two component codes of a diagonal into the two nibbles
        pha                             ; EF5B  keep the packed pair
        and     #$0F                    ; EF5C  low nibble = the row component
        tay                             ; EF5E  index the delta table with it
        lda     directionDeltaTable,y   ; EF5F  $20F0: -> signed delta
        tay                             ; EF62  Y = row delta
        pla                             ; EF63  get the packed pair back
        lsr     a                       ; EF64  shift the high nibble down ...
        lsr     a                       ; EF65  ...
        lsr     a                       ; EF66  ...
        lsr     a                       ; EF67  ... = the column component
        tax                             ; EF68  index the delta table with it
        lda     directionDeltaTable,x   ; EF69  $20F0: -> signed delta
        tax                             ; EF6C  X = column delta
L_EF6D:
        txa                             ; EF6D  A = column delta
        clc                             ; EF6E  clear carry for the position add
        adc     D_8974                  ; EF6F  current drone column
        sta     D_8974                  ; EF72  advance it one cell
        tax                             ; EF75  X = the new column, for mirrorBlockCornerForOpponent
        tya                             ; EF76  A = row delta
L_EF77:
        clc                             ; EF77  clear carry for the position add
        adc     D_8975                  ; EF78  current drone row
        sta     D_8975                  ; EF7B  advance it one cell
        tay                             ; EF7E  Y = the new row, for mirrorBlockCornerForOpponent
        lda     #$8D                    ; EF7F  command $8D = cmdDroneMove
        sta     rxPacketBuffer          ; EF81  byte 0 of the comm module's INCOMING packet buffer: the trainer forges the packet the engine is about to read  (also trainerAiE000:rxPacketBuffer)
        lda     D_896F                  ; EF84  the drone's heading
        clc                             ; EF87  clear carry for the add
        adc     #$04                    ; EF88  +4 ...
        and     #$07                    ; EF8A  ... modulo 8 = the same heading seen from the other end of the mirrored map
        sta     rxPacketArg1            ; EF8C  byte 1 of the packet  (also trainerAiE000:rxPacketArg1)
        jsr     mirrorBlockCornerForOpponent; EF8F  $EA38: X,Y = 39-(X+1), 39-(Y+1), pre-compensating the mirror-and-decrement the engine applies to a remote $8D
        stx     rxPacketArg2            ; EF92  byte 2 of the packet = mirrored column  (also trainerAiE000:rxPacketArg2)
        sty     rxPacketArg3            ; EF95  byte 3 of the packet = mirrored row  (also trainerAiE000:rxPacketArg3)
        lda     #$04                    ; EF98  packet length 4
L_EF9A:
        sta     rxPacketLength          ; EF9A  tell the game a 4-byte packet has arrived; mergeIncomingPacket ($4F05) will execute it  (also trainerAiE000:rxPacketLength)
        rts                             ; EF9D  done


; measureDroneDistanceToTarget - DEAD in this overlay (nothing here calls it), but a complete 13-byte
; routine: it copies the drone's cell $8974/$8975 into the reference point zp_27/zp_28 and tail-jumps
; to cellDistance ($E82A), which returns the integer distance 0-16 between the cell in zp_22/zp_24 and
; the drone.  Y is left untouched, which is what lets the live caller keep a unit index in Y across
; the call.  Rendered as .byte because no reachable code in this file references it; it disassembles
; as LDA $8974 / STA $27 / LDA $8975 / STA $28 / JMP $E82A and is the same routine as
; droneDistanceToCell at $EF9E in game/droneAiEE00.
; In:  zp_22/zp_24 = cell to measure against, $8974/$8975 = drone cell
; Out: A = X = distance 0-16, zp_27/zp_28 = drone cell, Y preserved
; Called from: nothing in this file ($EE26 and $EEFC in game/droneAiEE00)
measureDroneDistanceToTarget:
        .byte   $AD                     ; EF9E  AD 74 89 = LDA $8974 (drone column), 85 27 = STA zp_27, AD 75 89 = LDA $8975 (drone row)
        .byte   $74,$89,$85,$27,$AD,$75,$89; EF9F  t..'.u.
        .byte   $85                     ; EFA6  85 ...
        .byte   $28                     ; EFA7  ... 28 = STA zp_28, then 4C 2A E8 = JMP cellDistance ($E82A)
        .byte   $4C,$2A,$E8             ; EFA8  L*.

; ----------------------------------------------------------------------
; staleDroneTargetPickerTail - 34 stale bytes, unreachable and unusable here.  In the solo trainer's
; own $EC00 tail (track 35 sectors 12-15) and in the drone overlay (track 29 sectors 4-5) - both of
; which carry these exact bytes - they are the end of the drone's target picker: bump the candidate
; index zp_1F, loop back to $EF9B for the next of 15 candidate blocks, then adopt the best player unit
; found (zp_1D) as the drone's target $8976 and lower the detonation threshold $8979 to three quarters
; so the drone will settle for a weaker target next time round.  In THIS image the BCC at $EFB1
; branches to $EF9B, which is the middle of the STA $E01E at $EF9A, so the fragment could not run even
; if something jumped to it.  In the modem driver's tail (track 34 sectors 12-15) the same addresses
; hold the middle of the message 'INSERT DATA DISK AND PRESS SPACE.' instead.  The bytes only survive
; so that the shared strings from $EFCD on land at their fixed addresses.
; In:  zp_1D best candidate unit, zp_1F candidate index, $8979
; Out: $8976 drone target, $8979 reduced to 3/4, zp_18 scratch
; Called from: nothing - dead in this overlay
; ----------------------------------------------------------------------
staleDroneTargetPickerTail:
        inc     scratch1F               ; EFAB  next candidate block
        lda     scratch1F               ; EFAD  how many have been scored
        cmp     #$0F                    ; EFAF  15 candidate blocks per pass
        bcc     L_EF9A+1                ; EFB1  branches to $EF9B - a real instruction only in the images where this fragment is live; here it is the middle of the STA $E01E at $EF9A
L_EFB3:
        lda     scratch1D               ; EFB3  the best player unit found by the scoring pass
        bmi     L_EFCC                  ; EFB5  bit 7 set = no candidate at all
        sta     D_8976                  ; EFB7  adopt it as the drone's target unit
        lda     D_8979                  ; EFBA  the current detonation score threshold
        lsr     a                       ; EFBD  /2 ...
        lsr     a                       ; EFBE  ... /4
        sta     scratch18               ; EFBF  keep a quarter of it
        lda     D_8979                  ; EFC1  the threshold again
        sec                             ; EFC4  prepare to subtract
        sbc     scratch18               ; EFC5  threshold - threshold/4 = three quarters of it
        bcc     L_EFCC                  ; EFC7  underflow: leave the threshold alone
        sta     D_8979                  ; EFC9  lower the bar so the drone will accept a weaker target next time
L_EFCC:
        rts                             ; EFCC  done


; $EFCD-$EFFE is byte for byte identical in all three images that can occupy this page (the trainer
; setup overlay here, the solo trainer's own tail from track 35 and the modem driver's tail from track
; 34), and only the very last byte at $EFFF differs.  That is deliberate: the setup screens in overlay
; A print these strings by absolute address and must not care which image is resident.  Only the T34
; image, which $7B33 restores immediately after this overlay has run, also carries the 'INSERT xxxx
; DISK AND PRESS SPACE.' template that begins at $EFAF - here those bytes are the stale code above, so
; the three characters below are all that is left of that message.
msgSpaceTailFragment:
        .byte   "CE",'.'|$80            ; EFCD  tail of 'INSERT xxxx DISK AND PRESS SPACE.' - the terminator '.' has bit 7 set

; msgDataGameCustomOff: text, 18 bytes. 'DATAGAMECUSTOM OF'+'F'|$80; almost certainly addressed at two
; offsets ('DATAGAME' at $EFD0 and 'CUSTOM OFF' at $EFD8), only the final F carries the bit-7
; terminator.
msgDataDiskName:
        .byte   "DATA"                  ; EFD0  'DATA' - the 4-character disk name promptInsertDataDisk ($81B2) patches into the message template at $EFB6
msgGameDiskName:
        .byte   "GAME"                  ; EFD4  'GAME' - the disk name promptInsertGameDisk ($81A5) uses instead
msgCustomOff:
        .byte   "CUSTOM OF",'F'|$80     ; EFD8  'CUSTOM OFF' - installed as message slot $1E by overlay A; its ON/OFF patch points at the 'OFF' at $EFDF

; msgDestroyOff: text, 11 bytes. 'DESTROY OF'+'F'|$80.
msgDestroyOff:
        .byte   "DESTROY OF",'F'|$80    ; EFE2  'DESTROY OFF' - message slot $1E for the DESTROY toggle; the ON/OFF patch points at the 'OFF' at $EFEA

; msgAccepted: text, 8 bytes. 'ACCEPTE'+'D'|$80.
msgAccepted:
        .byte   "ACCEPTE",'D'|$80       ; EFED  'ACCEPTED' - printed centred on row 15 by overlay A when the fire button confirms an edited value

; overlayTailPadding_EFF5: byteTable, 11 bytes. $C0,$DF then four $00,$FF pairs and a trailing byte
; ($FF here, $00 in the T34 tail). Purpose unknown; the same bytes end every $EC00/$EE00 overlay
; variant.
overlayPageTailBytes:
        .byte   $C0,$DF,$00,$FF,$00,$FF,$00,$FF; EFF5  ........  unreferenced trailing bytes, the same in every image of this page
        .byte   $00                     ; EFFD  the last byte is $FF here and in the track 35 trainer tail, $00 in the track 34 modem tail
        .byte   $FF,$FF                 ; EFFE  ..
