; ============================================================================
; $EE00-$EFFF - solo trainer: drone flight AI (track 29 sectors 4-5)
; ============================================================================
; Swapped in by the trainer module ($E0D6) while its drone is in the air and swapped back out afterwards.

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

; ---- references to code/data outside this file ----
diagonalDirectionTable   := $1FB2
directionDeltaTable      := $20F0
headingToDirectionTable  := $7316
mapCellToRadarGrid       := $7A13
D_896F                   := $896F
D_8973                   := $8973
D_8974                   := $8974
D_8975                   := $8975
D_8976                   := $8976
D_8978                   := $8978
D_8979                   := $8979
currentScreen            := $90FB
missilesLeft             := $92B0
droneArrivalFlags        := $92F9
rxPacketLength           := $E01E
rxPacketBuffer           := $E020
rxPacketArg1             := $E021
rxPacketArg2             := $E022
rxPacketArg3             := $E023
getStepDirection         := $E80E
cellDistance             := $E82A
scoreTargetBlock         := $E9A6
mirrorBlockCornerForOpponent := $EA38
unitColTable             := $F640
unitRowTable             := $F6A4
unitStunTable            := $F960
nextGameRandom           := $FD5D

; Contents
; --------
;    $EE00  flyTrainerDroneStep          one flight step of the solo trainer's drone, run once per lock-step command
;                                        exchange for as long as this track-29 s4-5 page is the resident $EE00 image.
;    $EE15  pickDroneAimPointAndDecide   decides between blowing the drone up where it stands and flying it one more
;                                        cell.
;    $EE2D  midCheckDroneOnMapEE2D       NOT an entry point in this image.
;    $EE55  midStoreDetonateThresholdEE55 NOT an entry point in this image.
;    $EE85  midScanDiagonalBlockEE85     NOT an entry point in this image.
;    $EEB6  emitDroneDetonateCommand     writes the 3-byte command $8E (detonate) plus the drone's current cell into the
;                                        fabricated received packet.
;    $EED0  maybeJinkDroneHeading        takes the heading getStepDirection worked out and, under a narrow set of
;                                        conditions, throws it away for a random one.
;    $EEED  midCheckFuelForJinkEEED      NOT an entry point in this image.
;    $EF0F  steerDroneAndEmitMove        turns the drone at most one 45-degree step towards the desired heading, moves
;                                        it one cell that way and reports the move as command $8D.
;    $EF9E  droneDistanceToCell          measures the distance from the cell in zp_22/zp_24 to the drone.

        .org $EE00


; ----------------------------------------------------------------------
; flyTrainerDroneStep - one flight step of the solo trainer's drone, run once per lock-step command
; exchange for as long as this track-29 s4-5 page is the resident $EE00 image.  The whole page has
; exactly one product: a fake 'packet received from the opponent' in rxPacketBuffer $E020 with its
; length in rxPacketLength $E01E, holding either $8F (drone released, 1 byte), $8E col row (detonate,
; 3 bytes) or $8D heading col row (fly one cell, 4 bytes).  Everything else in the game - the command
; interpreter, the game film, the screens - then treats the AI's drone exactly like a modem
; opponent's.  This entry only disposes of the two trivial cases (the flight is already over, or the
; fuel has run out) and falls through into pickDroneAimPointAndDecide for the real decision.
; In:  droneArrivalFlags $92F9 (bit 7 = a $92 'drone finished' command has already gone through a
;     packet merger, i.e. the drone was shot down or has arrived), aiDroneFuel $8973 (steps left, $64
;     = 100 at launch), drone cell $8974/$8975, drone target unit $8976, aiDroneDetonateThreshold
;     $8978, unit arrays $F640/$F6A4/$F960 and the map at $F000
; Out: $E01E and $E020.. = the fabricated opponent packet; $8973 decremented; on the flight path also
;     $896F (heading) and $8974/$8975 (cell); zp_18, zp_19, zp_1C, zp_1D, zp_22, zp_24, zp_27, zp_28
;     and the line-stepper state zp_8C-zp_97 clobbered
; Called from: the trainer module's buildTrainerReplyPacket - the 'jsr $EE00' at $E0A5, reached only
;     while droneOverlayState $8977 has bit 7 set (this page loaded).  Note that in the trainer's own
;     $EE00 image the same address is just a fall-through label of fireMissileAtSpottedUnit, so that
;     one JSR means two entirely different things depending on which page is resident.
; Is there still a drone to fly, and does it still have fuel?  A drone flight costs two disk loads:
; $E0C1-$E0D9 of the trainer loads this page (track 29 s4-5) as soon as droneStateFlags $920D bit 0
; says a
; drone is airborne, and $E0AB-$E0BE loads the trainer's own tail (track 35 s14-15) back when that bit
; clears.  While this page is resident the trainer issues no other orders at all, because all of its
; other think routines are in the page that was swapped out.
; ----------------------------------------------------------------------
flyTrainerDroneStep:
        lda     droneArrivalFlags       ; EE00  droneArrivalFlags: bit 7 goes up the moment command $92 (drone finished) passes through mergeIncomingPacket ($4F42) or mergeOutgoingPacket ($4FA8)
        bmi     droneStepDone           ; EE03  branch if the flight is already over (shot down by the player's missile, or arrived) - emit nothing this exchange
        dec     D_8973                  ; EE05  spend one step of drone fuel; launchAiDrone loaded aiDroneFuel with $64 = 100 steps
        bpl     pickDroneAimPointAndDecide; EE08  still 0 or more steps left: go and decide where to fly
        lda     #$8F                    ; EE0A  out of fuel: command $8F = cmdDroneRelease ($537B), which for a remote command clears $920D bits 0-5 - that is what makes the trainer swap this page back out next exchange
        sta     rxPacketBuffer          ; EE0C  the fabricated packet's only command byte  (also trainerAiE000:rxPacketBuffer)
setRxPacketLengthOne:
        lda     #$01                    ; EE0F  $8F carries no arguments
        sta     rxPacketLength          ; EE11  one byte of 'received' packet is waiting for mergeIncomingPacket ($4F05)  (also trainerAiE000:rxPacketLength)
droneStepDone:
        rts                             ; EE14  back to $E0A8 in the trainer module


; ----------------------------------------------------------------------
; pickDroneAimPointAndDecide - decides between blowing the drone up where it stands and flying it one
; more cell.  Within 6 cells of its target unit it scores the 2x2 block the drone covers with
; scoreTargetBlock ($E9A6) and detonates as soon as that score reaches aiDroneDetonateThreshold $8978
; - or only three quarters of it when the target is stunned and so cannot walk out from under the
; blast.  Otherwise it scores the four 2x2 blocks that touch the target's cell, keeps the best corner
; as the aim point in zp_95/zp_94, points the line stepper from the drone's cell at it and asks
; getStepDirection ($E80E) for a compass heading; 'no direction' means the drone is already standing
; on its aim cell, which detonates as well.
; In:  $8976 drone target unit, unit position arrays $F640/$F6A4, drone cell $8974/$8975,
;     aiDroneDetonateThreshold $8978, unitStunTable $F960, the map through scoreTargetBlock
; Out: zp_1C = best block score, zp_1D = highest-numbered player unit in it, zp_95/zp_94 = aim
;     column/row, zp_93/zp_92 = line start column/row, Y = heading 0-7 with N clear when a direction
;     was found; continues in emitDroneDetonateCommand or maybeJinkDroneHeading
; Called from: nothing - $EE08 falls into it.  It is a block of flyTrainerDroneStep, not a subroutine.
; Phase 1 - how far is the drone from the unit it was launched at, and is the cluster underneath it
; worth spending the drone on?  zp_1C is the running best/threshold that scoreTargetBlock compares
; against, so clearing it here means 'only a block worth 0 or more counts'.
; ----------------------------------------------------------------------
pickDroneAimPointAndDecide:
        lda     #$00                    ; EE15  start the scoring with a best-so-far of 0...
        sta     scratch1C               ; EE17  ...scoreTargetBlock ignores any 2x2 block that scores below zp_1C
loadTargetUnitCell:
        ldy     D_8976                  ; EE19  Y = the unit the drone was sent at, picked once per round by chooseAiDroneTarget ($EEE0 of the trainer's own page)
        lda     unitColTable,y          ; EE1C  target's map column - unitColTable is the first of the 100-entry unit arrays at $F640, each $64 apart
        sta     cellColA                ; EE1F  point A of cellDistance = the target's cell
        lda     unitRowTable,y          ; EE21  target's map row (unitRowTable = $F640 + $64)
        sta     cellRowA                ; EE24  point A row
measureRangeToTarget:
        jsr     droneDistanceToCell     ; EE26  A = X = distance in cells from the drone to the target; Y survives the call, which is why the target index can stay there
        cmp     #$06                    ; EE29  6 cells is the radius inside which detonating is worth considering
        bcs     aimAtBlockNearTarget    ; EE2B  branch if the drone is still far out - just pick an aim cell and fly, Y still holds the target unit

; ----------------------------------------------------------------------
; midCheckDroneOnMapEE2D - NOT an entry point in this image.  $EE2D is the 'ldx $8974 / bmi' on-map
; test in the middle of pickDroneAimPointAndDecide; the label exists only because the modem driver
; (track 34) has a real routine at this address and XREF merges all five images of $EE00-$EFFF.
; In:  -
; Out: -
; Called from: nothing in this image; the two 'call:EDD2,EE13' references in XREF.txt belong to the
;     trainer's own page.
; ----------------------------------------------------------------------
midCheckDroneOnMapEE2D:
        ldx     D_8974                  ; EE2D  drone column; it is a signed value and can go negative, because the flight code never clamps it to 0..39
        bmi     aimAtBlockNearTarget    ; EE30  branch if the drone has drifted off the left/top edge of the map - it cannot be standing over anything
        ldy     D_8975                  ; EE32  drone row for the block score.  NOTE: this also destroys the target unit index in Y, which is a bug on the branch below
        bmi     aimAtBlockNearTarget    ; EE35  branch if the drone row is off the map: aimAtBlockNearTarget then indexes the unit arrays with the drone row instead of the target unit index (probable bug, only reachable once the drone has walked off the map)
        jsr     scoreTargetBlock        ; EE37  score the 2x2 block whose top-left corner is (X,Y) = the drone's cell: the sum of targetValueByUnitType ($E9F1 = grunt 1, rider 2, boomer 4, spy 1, comcen 4) over the units in its four cells, counted positive for the player's units 0-49 and negative for the AI's own 50-99
        lda     scratch1C               ; EE3A  the score of the block under the drone (still 0 if the block scored negative)
        cmp     D_8978                  ; EE3C  aiDroneDetonateThreshold, copied at trainer setup ($ECC9) out of aiPlanParamTable ($EC0A = $FF,$FF,$08,$0C,$0C), indexed by the opening play the trainer drew ($8966, 0-4); $FF for plays 0 and 1 means 'never detonate early', so those drones only ever blow up when they reach their aim cell
        bcs     emitDroneDetonateCommand; EE3F  branch if the cluster under the drone is worth the threshold - blow up here

; ----------------------------------------------------------------------
; A stunned target cannot walk out of the blast, so accept a cheaper cluster: three quarters of the
; threshold.  unitStunTable $F960 holds the stun/pinned countdown in its low nibble (bits 5-7 are the
; dig-in state, bit 7 = dug in).
; ----------------------------------------------------------------------
checkTargetStunned:
        ldy     D_8976                  ; EE41  Y = the target unit index again (it was overwritten by the drone row at $EE32)
        lda     unitStunTable,y         ; EE44  the target's stun byte
        and     #$0F                    ; EE47  low nibble = stun/pinned countdown; nonzero is what makes the status panel print STUN'D
        beq     aimAtBlockNearTarget    ; EE49  branch if the target is not stunned - keep the full threshold and fly on
        lda     D_8978                  ; EE4B  aiDroneDetonateThreshold T
        lsr     a                       ; EE4E  T/2
        sta     scratch18               ; EE4F  park T/2
        lsr     a                       ; EE51  T/4
        clc                             ; EE52  clear carry before the add (the LSRs left it holding a shifted-out bit)
        adc     scratch18               ; EE53  T/4 + T/2 = 3T/4

; ----------------------------------------------------------------------
; midStoreDetonateThresholdEE55 - NOT an entry point in this image.  $EE55 is the 'sta zp_18' that
; parks the relaxed (3/4) detonate threshold used against a stunned target, inside
; pickDroneAimPointAndDecide.  The label is imported from the other images of this page.
; In:  -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midStoreDetonateThresholdEE55:
        sta     scratch18               ; EE55  zp_18 = the relaxed threshold
        lda     scratch1C               ; EE57  score of the block under the drone
compareScoreWithThreshold:
        cmp     scratch18               ; EE59  test it against three quarters of the threshold
        bcs     emitDroneDetonateCommand; EE5B  good enough over a stunned target - detonate

; ----------------------------------------------------------------------
; Phase 2 - choose an aim cell.  The drone is a 2x2 object addressed by its top-left corner, so the
; four corners that put the target's own cell inside the drone footprint are (col,row), (col-1,row),
; (col-1,row-1) and (col,row-1).  Each is handed to scoreTargetBlock, which keeps the best score in
; zp_1C, the highest player unit index in it in zp_1D and writes the winning corner straight into
; lineEndCol/lineEndRow (zp_95/zp_94) - exactly the two bytes the line stepper wants as its end point.
; Y = the target unit index on entry, except on the buggy path from $EE35.
; ----------------------------------------------------------------------
aimAtBlockNearTarget:
        lda     unitColTable,y          ; EE5D  target's column (Y is the target unit index; see the note at $EE32 for the one path where it is not)
        sta     scratch18               ; EE60  zp_18 = target column, kept because X gets reloaded for each of the four corners
        sta     lineEndCol              ; EE62  seed the stepper's end column with the target cell itself, in case all four blocks score negative and none of them is recorded
        lda     unitRowTable,y          ; EE64  target's row
storeAimRow:
        sta     scratch19               ; EE67  zp_19 = target row
        sta     lineEndRow              ; EE69  seed the end row the same way
scoreFourBlocksAroundTarget:
        lda     #$FF                    ; EE6B  $FF = 'no best block yet': scoreTargetBlock's 'bit zp_1C / bpl' test accepts any score when the running best is negative
        sta     scratch1C               ; EE6D  arm the scan
        ldx     scratch18               ; EE6F  corner 1: X = the target's own column...
        ldy     scratch19               ; EE71  ...Y = its row, i.e. the block covering (col,row)..(col+1,row+1)
        jsr     scoreTargetBlock        ; EE73  score it; on an improvement the corner lands in zp_95/zp_94 and the best player unit in zp_1D
        ldx     scratch18               ; EE76  corner 2: target column...
        dex                             ; EE78  ...minus one
        bmi     scoreBlockNorthOfTarget ; EE79  skip both left-hand corners when the target stands in column 0
        ldy     scratch19               ; EE7B  same row
        jsr     scoreTargetBlock        ; EE7D  score the block at (col-1,row)
        ldx     scratch18               ; EE80  corner 3: column...
        dex                             ; EE82  ...minus one again
        ldy     scratch19               ; EE83  target row...

; ----------------------------------------------------------------------
; midScanDiagonalBlockEE85 - NOT an entry point in this image.  $EE85 is the 'dey' that turns corner
; (col-1,row) into the diagonal corner (col-1,row-1) inside the four-block scan of
; pickDroneAimPointAndDecide.  The label is an artifact of the T34/T35 images of this page (in the
; trainer's own page $EE85 is launchAiDrone).
; In:  -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midScanDiagonalBlockEE85:
        dey                             ; EE85  ...minus one: the diagonal corner (col-1,row-1)
        bmi     setLineStepperEndpoints ; EE86  skip it when the target stands in row 0
        jsr     scoreTargetBlock        ; EE88  score the block at (col-1,row-1)
scoreBlockNorthOfTarget:
        ldx     scratch18               ; EE8B  corner 4: the target's own column
        ldy     scratch19               ; EE8D  target row...
        dey                             ; EE8F  ...minus one
        bmi     setLineStepperEndpoints ; EE90  skip when the target stands in row 0
        jsr     scoreTargetBlock        ; EE92  score the block at (col,row-1)

; ----------------------------------------------------------------------
; Phase 3 - take one Bresenham step from the drone towards the chosen corner.  initLineStepper ($FBB8)
; wants the start cell in zp_93/zp_92 (column/row) and the end cell in zp_95/zp_94, and
; getStepDirection ($E80E) runs it once and turns the resulting per-step deltas into a compass
; direction 0-7 (clockwise from north) in Y.  A drone coordinate that has gone negative is clamped to
; 0 with the end point shifted by the same amount - except that the subtraction is written the wrong
; way round, so the shifted end point comes out negated (see the line comments).
; ----------------------------------------------------------------------
setLineStepperEndpoints:
        lda     D_8974                  ; EE95  drone column
        bpl     storeLineStartColumn    ; EE98  branch if it is still a sane 0..39 value - use it as the line start
        sec                             ; EE9A  prepare the borrow-free subtract
        sbc     lineEndCol              ; EE9B  A = droneCol - aimCol.  To clamp the start to 0 and keep the vector, the end point needs aimCol - droneCol; this computes the negative of that (probable bug - reachable only after the drone has walked off the left edge)
        sta     lineEndCol              ; EE9D  store the shifted end column
        lda     #$00                    ; EE9F  and clamp the start column to the left edge
storeLineStartColumn:
        sta     lineCurCol              ; EEA1  zp_93 = line-stepper start column
        lda     D_8975                  ; EEA3  drone row, same treatment
        bpl     storeLineStartRow       ; EEA6  branch if the row is on the map
        sec                             ; EEA8  prepare the subtract
        sbc     lineEndRow              ; EEA9  A = droneRow - aimRow (the same sign slip as the column above)
        sta     lineEndRow              ; EEAB  shifted end row
        lda     #$00                    ; EEAD  clamp the start row to the top edge
storeLineStartRow:
        sta     lineCurRow              ; EEAF  zp_92 = line-stepper start row
        jsr     getStepDirection        ; EEB1  one line step from the drone towards the aim cell, converted to a heading: Y = 0-7 with N clear when the step deltas matched dirToColDelta/dirToRowDelta ($E68A/$E693), or Y = $FF with N set when nothing matched
        bpl     maybeJinkDroneHeading   ; EEB4  branch with a usable heading.  Falling through means the stepper did not move at all, i.e. the drone is already standing on its aim cell - so detonate

; ----------------------------------------------------------------------
; emitDroneDetonateCommand - writes the 3-byte command $8E (detonate) plus the drone's current cell
; into the fabricated received packet.  Both coordinates go through mirrorBlockCornerForOpponent
; ($EA38, v -> 39-(v+1) = 38-v) because the engine treats everything in that buffer as having arrived
; from the remote player and runs mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) over the
; parameters, which turns them back into the real cell.  The handler cmdDroneDetonate ($5336) then
; plays the blast and clears $920D bits 0-5, ending the flight.
; In:  drone cell $8974/$8975 (top-left corner of the 2x2 drone)
; Out: $E020 = $8E, $E021/$E022 = pre-mirrored column/row, $E01E = 3; A, X, Y clobbered
; Called from: $EE3F (the cluster under the drone is worth the threshold), $EE5B (relaxed threshold
;     over a stunned target) and $EEB4 (the drone is already on its aim cell).  All three are
;     branches, not calls.
; ----------------------------------------------------------------------
emitDroneDetonateCommand:
        lda     #$8E                    ; EEB6  command $8E = cmdDroneDetonate ($5336)
        sta     rxPacketBuffer          ; EEB8  the packet's command byte  (also trainerAiE000:rxPacketBuffer)
        ldx     D_8974                  ; EEBB  X = drone column
mirrorDroneCellIntoPacket:
        ldy     D_8975                  ; EEBE  Y = drone row
        jsr     mirrorBlockCornerForOpponent; EEC1  pre-compensate the engine's remote-coordinate mirror: X = 38-X, Y = 38-Y
        stx     rxPacketArg1            ; EEC4  argument 1 = column as the player's machine will read it  (also trainerAiE000:rxPacketArg1)
        sty     rxPacketArg2            ; EEC7  argument 2 = row  (also trainerAiE000:rxPacketArg2)
        lda     #$03                    ; EECA  command byte plus two arguments
        sta     rxPacketLength          ; EECC  hand the 3-byte packet to the engine  (also trainerAiE000:rxPacketLength)
        rts                             ; EECF  back to $E0A8


; ----------------------------------------------------------------------
; maybeJinkDroneHeading - takes the heading getStepDirection worked out and, under a narrow set of
; conditions, throws it away for a random one.  The drone only weaves when the human could actually
; shoot it down: he still has missile strikes, the drone's cell is inside the 21x17 radar window
; centred on his comcen, that radar/missile screen is the one currently on show, the drone has at
; least 12 steps of fuel left and it is at least 3 cells from its aim point.  Then two exchanges in
; three (nextGameRandom >= $56) the heading becomes a random 0-7.  Since steerDroneAndEmitMove turns
; at most one 45-degree step per exchange the result is a wobble, not a jump.  Purpose inferred;
; medium confidence.
; In:  Y = heading 0-7 from getStepDirection, missilesLeft $92B0 (the LOCAL player's missiles), drone
;     cell $8974/$8975, comcenUnit $9236, currentScreen $90FB, aiDroneFuel $8973, aim cell zp_95/zp_94
; Out: zp_19 = the heading steerDroneAndEmitMove should turn towards; zp_22/zp_24/zp_27/zp_28
;     clobbered; falls into steerDroneAndEmitMove
; Called from: nothing - $EEB4 branches into it.
; Evasive weave.  Every one of the five tests below asks the same question from a different angle: can
; the human see this drone and do something about it?  If not, the drone flies the straight line.
; ----------------------------------------------------------------------
maybeJinkDroneHeading:
        sty     scratch19               ; EED0  remember the heading the line stepper asked for
        lda     missilesLeft            ; EED2  missile strikes the LOCAL (human) player has left - $9236/$92B0 are all the local player's own state
        beq     steerDroneAndEmitMove   ; EED5  he cannot shoot at all - fly straight in
        ldx     D_8974                  ; EED7  X = drone column for the radar-window test
        stx     cellColB                ; EEDA  redundant store: droneDistanceToCell ($EF9E) writes the same two bytes again before anything reads them
        ldy     D_8975                  ; EEDC  Y = drone row
testDroneOnPlayerRadar:
        sty     cellRowB                ; EEDF  redundant in the same way
        jsr     mapCellToRadarGrid      ; EEE1  overlay B helper: X = col + 10 - comcenCol, Y = row + 8 - comcenRow, C = 1 when the cell falls outside the 21x17 radar grid.  $9236 is deliberately left alone, so this is the HUMAN player's comcen window - unlike isUnitOnAiRadar ($EE6A of the trainer's own page), which forces $9236 to $63 (unit 99) first
        bcs     steerDroneAndEmitMove   ; EEE4  off the player's radar - nothing to hide from
        lda     currentScreen           ; EEE6  which game screen is on show
        cmp     #$01                    ; EEE9  screen 1 = the comcen RADAR / MISSILE screen, the only place a missile strike can be aimed
requireRadarScreenShown:
        bne     steerDroneAndEmitMove   ; EEEB  the player is not watching - fly straight

; ----------------------------------------------------------------------
; midCheckFuelForJinkEEED - NOT an entry point in this image.  $EEED is the 'lda $8973 / cmp #$0C'
; fuel test inside maybeJinkDroneHeading.  The label is an artifact of the other images of this page.
; In:  -
; Out: -
; Called from: nothing in this image.
; ----------------------------------------------------------------------
midCheckFuelForJinkEEED:
        lda     D_8973                  ; EEED  steps of fuel left
        cmp     #$0C                    ; EEF0  12 steps
        bcc     steerDroneAndEmitMove   ; EEF2  nearly out of fuel - stop wasting steps and go straight in
        lda     lineEndCol              ; EEF4  aim column chosen by the four-block scan
        sta     cellColA                ; EEF6  point A of cellDistance = the aim cell
        lda     lineEndRow              ; EEF8  aim row
        sta     cellRowA                ; EEFA  point A row
        jsr     droneDistanceToCell     ; EEFC  distance from the drone to its aim point
        cmp     #$03                    ; EEFF  3 cells
requireRoomToWeave:
        bcc     steerDroneAndEmitMove   ; EF01  almost there - do not risk missing the target for the sake of dodging
        tax                             ; EF03  dead instruction: the distance is stashed in X but nothing reads it, and steerDroneAndEmitMove reloads X a few instructions later
        jsr     nextGameRandom          ; EF04  the deterministic game RNG on $57-$59; it preserves X and Y
        cmp     #$56                    ; EF07  $56 = 86, so 170 of the 256 possible bytes pass: weave about two exchanges in three
        bcc     steerDroneAndEmitMove   ; EF09  no weave this exchange
        and     #$07                    ; EF0B  a random compass heading 0-7
        sta     scratch19               ; EF0D  replace the sensible heading with it

; ----------------------------------------------------------------------
; steerDroneAndEmitMove - turns the drone at most one 45-degree step towards the desired heading,
; moves it one cell that way and reports the move as command $8D.  Instruction for instruction this is
; overlay B's steerDrone ($731E-$7391), the routine that flies the human player's drone, with the AI's
; variables substituted ($896F/$8974/$8975 for droneHeading/droneCol/droneRow $920B/$92F6/$92F7), with
; the desired heading read from zp_19 instead of looked up from the joystick, and with the turn sound
; ($10) removed.  That duplication is useful evidence: the heading encoding and the three lookup
; tables are shared between the player's drone and the AI's.  Neither coordinate is clamped, so a
; drone that keeps missing can walk clean off the 40x40 map.
; In:  zp_19 = desired heading 0-7 (clockwise from north), $896F = the drone's current heading,
;     $8974/$8975 = drone cell
; Out: $896F turned by at most one step, $8974/$8975 advanced by one cell, $E020 = $8D, $E021 =
;     (heading + 4) & 7, $E022/$E023 = pre-mirrored column/row, $E01E = 4
; Called from: nothing - it is the fall-through of maybeJinkDroneHeading and the target of all six 'do
;     not weave' branches at $EED5, $EEE4, $EEEB, $EEF2, $EF01 and $EF09.
; Turn towards the desired heading the short way round.  Headings are 0-7 clockwise from north, so the
; difference is read modulo 8: a positive difference of 1..3 turns clockwise (+1) and one of 4..7 is
; shorter anticlockwise (-1); a negative difference of -1..-4 turns anticlockwise and -5..-7
; clockwise.
; ----------------------------------------------------------------------
steerDroneAndEmitMove:
        lda     scratch19               ; EF0F  desired heading
        sec                             ; EF11  prepare the subtract
        sbc     D_896F                  ; EF12  difference from the drone's current heading
        beq     convertHeadingToStep    ; EF15  already pointing the right way - no turn at all
        bcc     desiredHeadingBelowCurrent; EF17  branch when the desired heading is the lower number (difference came out negative)
        ldx     #$FF                    ; EF19  assume one step anticlockwise
        cmp     #$04                    ; EF1B  half a turn: a positive difference of 4 or more is shorter going the other way
        bcs     applyHeadingStep        ; EF1D  difference 4..7 - keep the -1 step
        ldx     #$01                    ; EF1F  difference 1..3: one step clockwise
        bne     applyHeadingStep        ; EF21  always taken (X = 1, so Z is clear)
desiredHeadingBelowCurrent:
        ldx     #$01                    ; EF23  negative difference: assume one step clockwise
pickShorterTurnBackwards:
        cmp     #$FC                    ; EF25  $FC = -4: differences of -1..-4 are shorter anticlockwise
        bcc     applyHeadingStep        ; EF27  difference -5..-7 - keep the +1 step
        ldx     #$FF                    ; EF29  one step anticlockwise
applyHeadingStep:
        txa                             ; EF2B  A = the step, +1 or -1
addStepToHeading:
        clc                             ; EF2C  clear carry for the add
        adc     D_896F                  ; EF2D  apply it to the current heading
        and     #$07                    ; EF30  headings wrap inside 0-7
        sta     D_896F                  ; EF32  the drone's new heading

; ----------------------------------------------------------------------
; Turn the heading into a one-cell step.  headingToDirectionTable ($7316) maps heading 0-7
; (N,NE,E,SE,S,SW,W,NW) to the game's direction code (0 N, 1 S, 2 E, 3 W, 4 NW, 5 NE, 6 SE, 7 SW).
; Codes 0-1 are pure row moves and 2-3 pure column moves, both indexing directionDeltaTable ($20F0 =
; -1,+1,+1,-1) directly; codes 4-7 index diagonalDirectionTable ($1FB2+4 = $30,$20,$21,$31), whose
; high
; nibble is the column index and low nibble the row index into that same delta table.
; ----------------------------------------------------------------------
convertHeadingToStep:
        ldy     D_896F                  ; EF35  the drone's heading
lookUpDirectionCode:
        lda     headingToDirectionTable,y; EF38  direction code 0-7  (also mapGenerator6F00:forestBlockCompareNe)
        ldy     #$00                    ; EF3B  row delta defaults to 'no move'
        ldx     #$00                    ; EF3D  column delta defaults to 'no move'
        cmp     #$04                    ; EF3F  codes 4-7 are the diagonals
        bcs     readDiagonalDeltaPair   ; EF41  branch to the packed-pair path
        cmp     #$02                    ; EF43  codes 2-3 are east/west
        bcs     readColDeltaEastWest    ; EF45  branch to the column-only path
        tay                             ; EF47  codes 0-1 index the delta table directly: entry 0 = -1 (north), entry 1 = +1 (south)
readRowDeltaNorthSouth:
        lda     directionDeltaTable,y   ; EF48  the row delta
        tay                             ; EF4B  keep it in Y; the column delta stays 0
        jmp     advanceDroneCell        ; EF4C  join the common move code

readColDeltaEastWest:
        tax                             ; EF4F  codes 2-3: entry 2 = +1 (east), entry 3 = -1 (west)
        lda     directionDeltaTable,x   ; EF50  the column delta
storeColDelta:
        tax                             ; EF53  keep it in X; the row delta stays 0
        jmp     advanceDroneCell        ; EF54  join the common move code

readDiagonalDeltaPair:
        tay                             ; EF57  codes 4-7 index the packed diagonal table
loadPackedDiagonalDelta:
        lda     diagonalDirectionTable,y; EF58  $30 = NW, $20 = NE, $21 = SE, $31 = SW, as (columnIndex << 4) | rowIndex
        pha                             ; EF5B  save the packed byte for the second nibble
        and     #$0F                    ; EF5C  low nibble = index of the row delta
        tay                             ; EF5E  index the delta table with it
        lda     directionDeltaTable,y   ; EF5F  row delta
        tay                             ; EF62  Y = row delta
        pla                             ; EF63  the packed byte back
        lsr     a                       ; EF64  shift the high nibble down...
        lsr     a                       ; EF65  ...four places...
        lsr     a                       ; EF66  ...to get the column...
        lsr     a                       ; EF67  ...delta index
        tax                             ; EF68  index the delta table with it
        lda     directionDeltaTable,x   ; EF69  column delta
        tax                             ; EF6C  X = column delta

; ----------------------------------------------------------------------
; Move the drone one cell and report it.  Nothing here clamps the result to 0..39 - the
; negative-coordinate patch-up back at $EE95 is the only thing that copes with a drone that has left
; the map.
; ----------------------------------------------------------------------
advanceDroneCell:
        txa                             ; EF6D  column delta
        clc                             ; EF6E  clear carry for the add
        adc     D_8974                  ; EF6F  drone column
        sta     D_8974                  ; EF72  the drone's new column
        tax                             ; EF75  X = new column, ready for the mirror call
        tya                             ; EF76  row delta
addRowDeltaToDroneRow:
        clc                             ; EF77  clear carry for the add
        adc     D_8975                  ; EF78  drone row
        sta     D_8975                  ; EF7B  the drone's new row
        tay                             ; EF7E  Y = new row
        lda     #$8D                    ; EF7F  command $8D = drone move; the packet mergers execute it on the spot ($4F23) instead of queueing it
        sta     rxPacketBuffer          ; EF81  the packet's command byte  (also trainerAiE000:rxPacketBuffer)
        lda     D_896F                  ; EF84  the drone's heading
        clc                             ; EF87  clear carry for the add
        adc     #$04                    ; EF88  +4 headings = 180 degrees
        and     #$07                    ; EF8A  wrap into 0-7: the player sees the battlefield mirrored, so a drone flying south on the AI's map is flying north on his, and cmdDroneMove picks the sprite shape straight from this byte
        sta     rxPacketArg1            ; EF8C  argument 1 = the heading  (also trainerAiE000:rxPacketArg1)
        jsr     mirrorBlockCornerForOpponent; EF8F  pre-mirror the new cell: X = 38-X, Y = 38-Y
        stx     rxPacketArg2            ; EF92  argument 2 = column  (also trainerAiE000:rxPacketArg2)
        sty     rxPacketArg3            ; EF95  argument 3 = row  (also trainerAiE000:rxPacketArg3)
        lda     #$04                    ; EF98  command byte plus three arguments
storeRxPacketLength:
        sta     rxPacketLength          ; EF9A  the exchange's fabricated 'received' packet is complete  (also trainerAiE000:rxPacketLength)
        rts                             ; EF9D  back to $E0A8


; ----------------------------------------------------------------------
; droneDistanceToCell - measures the distance from the cell in zp_22/zp_24 to the drone.  It fills the
; drone's cell in as point B of cellDistance ($E82A) and tail-jumps into it, so the caller gets back
; the integer distance (the largest n with n*n <= dcol^2 + drow^2, saturating at 16) in A and X.
; Nothing along the way touches Y, which is what lets pickDroneAimPointAndDecide keep the target unit
; index there across the call.
; In:  zp_22/zp_24 = the cell to measure from, drone cell $8974/$8975
; Out: A = X = distance 0-16, zp_27/zp_28 = the drone's cell, Y preserved
; Called from: $EE26 (range to the target unit) and $EEFC (range to the aim point) - the only two JSRs
;     inside this page.
; ----------------------------------------------------------------------
droneDistanceToCell:
        lda     D_8974                  ; EF9E  drone column
        sta     cellColB                ; EFA1  point B of cellDistance = the drone
        lda     D_8975                  ; EFA3  drone row
        sta     cellRowB                ; EFA6  point B row
        jmp     cellDistance            ; EFA8  tail call: returns A = X = distance between (zp_22,zp_24) and the drone


; ----------------------------------------------------------------------
; staleCommTailBytes ($EFAB-$EFCF, 37 bytes) - dead in this image. Everything from here to $EFF4 is
; byte-for-byte identical in all four programs that are ever loaded at
; $EE00-$EFFF (the trainer's own tail from T35 s14-15, the modem driver from T34, the track-29 s0-3
; $EC00
; sub-overlay and this drone AI), because the map-generator overlay addresses the strings below by
; absolute address and they therefore may not move.  These 37 bytes are simply what happened to
; precede them in the original source. In the trainer's own page the same bytes are the tail of
; pickDroneTargetAlongThreatAxis: they bump the ray radius and, on the way out, store the chosen unit
; in $8976 and scale the acceptance threshold $8979 down to 75%.  In the modem driver they are the
; middle of the message 'INSERT DATA DISK AND PRESS SPACE.' starting at $EFAF.  The line comments
; below describe that trainer-page meaning; nothing here runs in this image. Latent bug: overlay A's
; promptInsertDiskAndWaitSpace ($81B6) unconditionally installs $EFAF as message slot $1E and copies
; four characters into $EFB6-$EFB9.  With this page (or the trainer's own, or the
; $EC00 sub-overlay) resident, $EFAF is the $C9 of 'cmp #$0F' - a complete one-character string 'I' -
; and
; the four-byte patch lands on top of code.
; ----------------------------------------------------------------------
staleCommTailBytes:
        inc     scratch1F               ; EFAB  trainer page: step the search ray outwards (zp_1F = radius)
        lda     scratch1F               ; EFAD  trainer page: the new radius
        cmp     #$0F                    ; EFAF  trainer page: stop after radius 14.  Modem page: this $C9 opcode byte is the 'I' that starts 'INSERT DATA DISK...', and is a whole terminated string on its own here
        bcc     storeRxPacketLength+1   ; EFB1  trainer page: keep walking the ray.  Here the target, $EF9B, is the operand byte of the 'sta $E01E' at storeRxPacketLength - a branch into the middle of an instruction, which is the clearest proof that this block is not code in this image
staleTailStoreTargetUnit:
        lda     scratch1D               ; EFB3  trainer page: the best player unit scoreTargetBlock found, $FF when none
        bmi     staleTailReturn         ; EFB5  trainer page: nothing worth sending a drone at - leave $8976 alone
        sta     D_8976                  ; EFB7  trainer page: that unit becomes the AI's next drone target
        lda     D_8979                  ; EFBA  trainer page: the target-value threshold
        lsr     a                       ; EFBD  trainer page: /2
        lsr     a                       ; EFBE  trainer page: /4
        sta     scratch18               ; EFBF  trainer page: park threshold/4
        lda     D_8979                  ; EFC1  trainer page: the threshold again
        sec                             ; EFC4  trainer page: prepare the subtract
        sbc     scratch18               ; EFC5  trainer page: 3/4 of the threshold - the AI grows steadily less picky about drone targets
        bcc     staleTailReturn         ; EFC7  trainer page: never let it go negative
        sta     D_8979                  ; EFC9  trainer page: store the reduced threshold
staleTailReturn:
        rts                             ; EFCC  trainer page: return to chooseAiDroneTarget


; msgInsertDiskTail ($EFCD, 3 bytes) - 'CE' plus '.' with bit 7 set as the string terminator: the last
; three characters of the modem driver's message 'INSERT DATA DISK AND PRESS SPACE.'.  Meaningless in
; this image, but it has to stay put so that the strings below keep their addresses.
msgInsertDiskTail:
        .byte   "CE",'.'|$80            ; EFCD  end of '...PRESS SPACE.' in the modem-driver image of this page

; Shared, position-fixed strings ($EFD0-$EFF4).  These belong to the map-generator/setup overlay
; (overlay A at $6F00-$87FF), not to the $E000 module at all: overlay A refers to them by hard-coded
; address, so every image of this page has to carry them at exactly these addresses. msgDataDiskName
; ($EFD0, 4 bytes, no terminator) - the literal 'DATA', which promptInsertDataDisk ($81B2) copies into
; the INSERT-DISK message before printing it.
msgDataDiskName:
        .byte   "DATA"                  ; EFD0  disk name patched into the INSERT-DISK prompt

; msgGameDiskName ($EFD4, 4 bytes, no terminator) - 'GAME', the other disk name, used by
; promptInsertGameDisk ($81A5).
msgGameDiskName:
        .byte   "GAME"                  ; EFD4  the other disk name

; msgCustomOff ($EFD8, 10 bytes) - 'CUSTOM OF' plus 'F' with bit 7 set (the string terminator).
; Overlay A installs it as message slot $1E for the CUSTOM rules switch and patches the word at $EFDF
; between 'OFF' and 'ON '.
msgCustomOff:
        .byte   "CUSTOM OF",'F'|$80     ; EFD8  message slot $1E for the CUSTOM switch; the ON/OFF word starts at $EFDF

; msgDestroyOff ($EFE2, 11 bytes) - 'DESTROY OF' plus the terminating 'F'.  Message slot $1E for the
; DESTROY switch; the ON/OFF word overlay A patches sits at $EFEA.
msgDestroyOff:
        .byte   "DESTROY OF",'F'|$80    ; EFE2  message slot $1E for the DESTROY switch; the ON/OFF word starts at $EFEA

; msgAccepted ($EFED, 8 bytes) - 'ACCEPTE' plus the terminating 'D'.  Overlay A prints it centred on
; row 15 when the fire button confirms a value in the custom-game editor.
msgAccepted:
        .byte   "ACCEPTE",'D'|$80       ; EFED  printed centred on row 15 when a custom-game value is confirmed

; trailingPadEFF5 ($EFF5-$EFFF, 11 bytes) - $C0,$DF,$00,$FF,$00,$FF,$00,$FF,$00,$FF,$FF.  Unreferenced
; by anything, and identical in every image of this page except the very last byte ($FF here and in
; the trainer's own tail, $00 in the modem driver).  Almost certainly padding left by the mastering
; system rather than data the game ever reads.
trailingPadEFF5:
        .byte   $C0                     ; EFF5  unreferenced trailing pad to the end of the page
        .byte   $DF,$00,$FF,$00,$FF,$00,$FF; EFF6  .......
        .byte   $00                     ; EFFD  last three pad bytes; the final $FF is $00 in the modem-driver image of this page
        .byte   $FF,$FF                 ; EFFE  ..
