; ============================================================================ ; $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 ..