579 lines
43 KiB
ArmAsm
579 lines
43 KiB
ArmAsm
; ============================================================================
|
|
; $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 ..
|