; ============================================================================ ; $E000-$EFFF - solo trainer: the computer opponent (track 35 sectors 0-15) ; ============================================================================ ; Loaded by $0F1A when the solo trainer was chosen ($0BA5 bit 7 set). It presents the same jump table as ; the modem driver but fakes the link: instead of sending and receiving over a wire it plans the moves of ; side 1 and injects them straight into the received-packet buffer. $EE00-$EFFF is swapped for the drone ; AI (track 29 sectors 4-5) while its drone is airborne. .setcpu "6502" .include "c64.inc" .include "zeropage.inc" ; ---- references to code/data outside this file ---- checksumXorConstant := $0400 play4Grp12 := $0464 D_077E := $077E loadSectors := $0804 setLoadDest := $085C filmPlaybackMode := $0B9B gameTypeOptions := $0BA3 gameEndReason := $0BA8 D_2009 := $2009 readMapCell := $25FA terrainSightBonusTable := $35BB squaresTable := $3860 distanceSquared := $3870 D_3E99 := $3E99 mirrorMapCoordinate := $3FA3 D_434C := $434C D_4CE0 := $4CE0 getCommandLength := $4D9F D_60E0 := $60E0 L_754C := $754C D_774C := $774C mapCellToRadarGrid := $7A13 msgWeWonByAKnockout := $87CE msgGetOpponentOnPhoneAnd := $8800 D_8831 := $8831 D_8832 := $8832 D_8864 := $8864 D_8895 := $8895 D_8896 := $8896 msgPickUpPhoneThenPressSpace := $8897 D_8898 := $8898 D_8899 := $8899 D_88A0 := $88A0 D_88D2 := $88D2 D_88DE := $88DE D_88EA := $88EA D_88F6 := $88F6 D_8902 := $8902 D_890E := $890E D_891A := $891A D_8926 := $8926 D_8932 := $8932 D_893E := $893E D_894A := $894A D_8952 := $8952 D_8953 := $8953 D_8954 := $8954 D_8955 := $8955 D_8956 := $8956 D_8957 := $8957 D_8958 := $8958 D_895A := $895A D_895B := $895B D_895D := $895D msgOpponentUsingNonStandardGame := $895E D_895F := $895F D_8960 := $8960 D_8961 := $8961 D_8962 := $8962 D_8963 := $8963 D_8964 := $8964 D_8965 := $8965 D_8966 := $8966 D_8967 := $8967 D_8968 := $8968 D_8969 := $8969 D_896A := $896A D_896B := $896B D_896C := $896C D_896D := $896D D_896E := $896E D_896F := $896F D_8970 := $8970 D_8971 := $8971 D_8972 := $8972 D_8973 := $8973 D_8974 := $8974 D_8975 := $8975 D_8976 := $8976 D_8977 := $8977 D_8979 := $8979 D_897A := $897A D_897B := $897B D_897C := $897C D_8D03 := $8D03 vicBorderShadow := $9003 lastStepRowDelta := $90F4 lastStepColDelta := $90F5 currentUnit := $90FA gameClock := $91CB fireTargetUnit := $91F0 droneHeading := $920B droneControlFlags := $920D comcenUnit := $9236 dronesLeft := $92AE opponentDronesLeft := $92AF opponentMissilesLeft := $92B1 currentCommandLength := $92EE droneViewOriginCol := $92F6 droneViewOriginRow := $92F7 D_A989 := $A989 D_B908 := $B908 unitColTable := $F640 side0ComcenCol := $F671 side1UnitColTable := $F672 side1ComcenCol := $F6A3 unitRowTable := $F6A4 side0ComcenRow := $F6D5 side1UnitRowTable := $F6D6 side1ComcenRow := $F707 side1UnitFlagsTable := $F73A unitTypeTable := $F76C side1UnitTypeTable := $F79E unitDestColTable := $F7D0 side1UnitDestColTable := $F802 side1ComcenDestCol := $F833 unitDestRowTable := $F834 side1UnitDestRowTable := $F866 side1ComcenDestRow := $F897 side1ComcenStun := $F9C3 side0ComcenEnergy := $F9F5 side1ComcenEnergy := $FA27 unitTerrainUnderTable := $FAF0 side1ComcenDisplayFlags := $FBB7 initLineStepper := $FBB8 stepLine := $FBFA divideSigned16by8 := $FCEC nextGameRandom := $FD5D ; Contents ; -------- ; $E000 commRequestEntry Jump-table slot 0 of the opponent module (JMP handleCommRequest). ; $E003 commSetLinkStateEntry Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or down. ; $E006 commGetReceivedByteEntry Jump-table slot 2 (JMP reportNoReceivedByte). ; $E009 commSendByteEntry Jump-table slot 3 (JMP commNoOpClc). ; $E00C commFrameTickEntry Jump-table slot 4 (JMP commNoOpClc). ; $E012 commKeyboardScanVector A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan ; vector. ; $E015 commReadKeyEntry Jump-table slot 5 (JMP commKeyboardScanVector). ; $E043 handleCommRequest Body of commRequestEntry. ; $E060 setCommLinkState Body of commSetLinkStateEntry. ; $E075 reportNoReceivedByte SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in ; the trainer build. ; $E077 commNoOpClc CLC + RTS: the shared no-op used as the body of commSendByteEntry, ; commFrameTickEntry and the unpatched keyboard vector. ; $E079 runTrainerRoundHook Once-per-round hook, called from finishRound ($4654) right after the game clock ; has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded). ; $E082 buildTrainerReplyPacket Entered with X=1 from sendOutgoingPacket, i.e. ; $E0E8 trainerUnitStepHook Per-unit hook called at the end of processUnitStep ($4526) for currentUnit, ; i.e. ; $E128 setSubOverlayLoadDest Points the fast loader at $EE00 (X = destination low byte, Y = high byte) and ; tail-jumps into setLoadDest. ; $E12F pickTrainerRallyPoint Chooses the AI's current objective: a cell scattered at random around the human ; comcen (unit 49), stored as the rally column/row that every attacking group heads for. ; $E14A randomiseCoordinate Returns A = base coordinate plus a random offset, clamped to 0..39. ; $E170 planTrainerRound Top-level planner of the solo trainer, run once per game round. ; $E21F pushTrainerComcenForward Attack-mode tail of planTrainerRound (a branch target, not a JSR entry). ; $E266 retreatTrainerComcen Defend-mode tail of planTrainerRound (a branch target). ; $E2BD scanTrainerUnitsForOrders Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down to 0, ; stopping as soon as four commands have been queued this round. ; $E2F1 decideTrainerGroupAction Decides what the AI group owning unit ordinal Y should do. ; $E339 aimGroupAtOwnComcen Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from ; $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343 that copies the group ; centre into the search origin and the line target into the wanted destination, and jumps to issueTrainerGroupOrders. ; $E3D0 aimGroupAtHomeLine Internal branch target used when the group still has members that have not ; reached the player's end zones and has no contact with his comcen: the target column stays where the group already is ; and the target row becomes 29 plus the group's own vertical extents, i.e. ; $E42A findTrainerAdvanceCell Threat-aware path search. ; $E49E issueTrainerGroupOrders Issues the move orders for the group in $8955. ; $E531 computeTrainerGroupStats Gathers the statistics of AI group Y. ; $E5FF clampGroupTargetToMap Clips the wanted group centre in zp_22/zp_24 so that the whole formation box ; (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores the clipped cell ; as the group's target and returns the movement delta relative to the group's average ordered destination in zp_2A ; (columns, also kept in $8958) and zp_2B (rows). ; $E66C offsetPointInDirection Computes the cell that lies Y steps in compass direction X from the reference ; point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and offsetCoordinateClamped. ; $E69C isGroupDestinationTooRisky influence-map test of one candidate destination for the AI group in ; currentAiGroup ($8955). ; $E773 buildUnitThreatTables rebuilds the two 100-entry influence-map tables that isGroupDestinationTooRisky ; reads. ; $E7B9 multiplyAbyY 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y = ; 0. ; $E7D3 buildDirectionFanTowardTarget finds the compass heading from the line start (lineCurCol,lineCurRow) to the ; line end ; $E7E3 buildDirectionFan fills the eight-byte scratch buffer $8896-$889D with the move directions the ; path search should try, ; $E80E getStepDirection sets a Bresenham walker up between (lineCurCol,lineCurRow) and ; (lineEndCol,lineEndRow), takes exactly one ; $E82A cellDistance integer distance between two map cells. ; $E83C updateFocusGroupCentre keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row) ; current. ; $E882 computeGroupCentroid averages the position and the ordered destination of every living member of one ; AI group. ; $E919 orderAiUnitToCell the trainer's central 'issue a move order' helper. ; $E937 queueMirroredMoveOrder second half of orderAiUnitToCell. ; $E95D isPathToPlayerComcenClear walks a Bresenham line from the current line position (which the caller has set ; to the group ; $E997 loadGroupCentreIntoLinePos copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6) ; into the line ; $E9A6 scoreTargetBlock scores the 2x2 block of map cells whose top-left corner is (X,Y) as a ; bombardment target. ; $E9F6 addUnitTargetValue helper of scoreTargetBlock. ; $EA1B offsetCoordinateClamped returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps of ; the signed step X, held ; $EA38 mirrorBlockCornerForOpponent converts the top-left corner of a 2x2 object (the drone) into the coordinates ; that the engine's ; $EA46 queueAiCommand appends the command staged in $8896..$8899 to the AI's 50-byte order queue at ; $88A0. ; $EA7B injectAiCommandsIntoPacket the heart of the solo trainer. ; $EACF updateAiUnitTracking per-unit hook, run from trainerUnitStepHook after the engine has stepped each ; unit. ; $EB67 runAiComcenActions spends whatever is left of the AI comcen's three command slots for this round. ; $EBA5 maybeQueueComcenEnergyRepair the trainer's version of the human console's energy repair. ; $EBDD queueComcenCommand shared tail of the comcen actions: stores A as the command byte and jumps into ; queueAiCommand, leaving ; $EBE4 haltAiComcen orders the AI comcen to move to the cell it is already standing on. ; $EBEF assessThreatAroundAiComcen builds a directional pressure map around the AI comcen and picks the direction ; to run. ; $EC82 rankThreatDirections Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just turned ; every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one of the eight compass ; buckets $8896-$889D. ; $EC91 smoothThreatRing Circular 1-2-1 blur of the eight-direction threat ring. ; $ECD4 findQuietestDirectionArc For every leading direction y = 7..0 it adds the blurred pressures of y, y-1 ; and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle direction y-1. ; $ED0B runAiMissileTurn The computer opponent's one 'weapon' decision per lock-step exchange. ; $EDD5 fireMissileAtSpottedUnit Second half of the missile turn, reached whenever no anti-drone shot was taken. ; $EE00 targetEnemyComcenWithMissile Fall-through tail of fireMissileAtSpottedUnit: when the AI has no remembered ; attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49) instead, but only while that ; comcen is currently visible. ; $EE2D queueMissileStrikeCommand Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler ; cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to have received from ; the opponent. ; $EE55 randomlySignValue Returns A or -A with equal probability, using bit 0 of the lock-step game RNG. ; $EE6A isUnitOnAiRadar Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17 ; radar grid centred on the computer opponent's own COMCEN. ; $EE85 launchAiDrone Sends the computer opponent's drone up when one is due. ; $EEE0 chooseAiDroneTarget Picks what the computer opponent should send its next drone at and leaves the ; unit index in $8976. ; $EF59 pickDroneTargetAlongThreatAxis Tail of chooseAiDroneTarget for the case where the human comcen is no use as ; a drone target. .org $E000 ; ---------------------------------------------------------------------- ; commRequestEntry - Jump-table slot 0 of the opponent module (JMP handleCommRequest). The game calls ; it every raster IRQ with X=0 ($1129), with X=1 from sendOutgoingPacket ($56AE) to hand over the ; outgoing packet, with X=2 to acknowledge a completed exchange ($4EFF/$526F) and with X=3 to cancel a ; pending send. In this build (track 35 = SOLO TRAINER) there is no link at all: X=1 runs one step of ; the computer opponent and leaves its orders in the receive buffer. ; In: X = request code 0..3 ; Out: see handleCommRequest ($E043) ; Called from: mainProgram0800 $1129 (raster IRQ, X=0), $4EFF and $526F (X=2), $56AE (X=1), ; $570F/$5727/$573B/$5767/$5776 (menu-level exchange) ; ---------------------------------------------------------------------- commRequestEntry: jmp handleCommRequest ; E000 slot 0: per-frame poll / packet request, X selects the function ; ---------------------------------------------------------------------- ; commSetLinkStateEntry - Jump-table slot 1 (JMP setCommLinkState): brings the pretend link up or ; down. ; In: X = 0 open, >= 1 close ; Out: isLinkActive / linkStatus / linkStatusSample ($E03B/$E03C/$E03D), and $E01F when closing ; Called from: initCommModule $0F46 (X=1), hangUpModemSetState $1B56 and $1B70 (X=3) ; ---------------------------------------------------------------------- commSetLinkStateEntry: jmp setCommLinkState ; E003 slot 1: open (X=0) or close (X>0) the link flags ; ---------------------------------------------------------------------- ; commGetReceivedByteEntry - Jump-table slot 2 (JMP reportNoReceivedByte). In the modem build this ; pops the UART receive ring; the trainer has no wire, so it always answers 'nothing received' and the ; opponent chat line stays empty in a solo game. ; In: none ; Out: C=1 always (no byte) ; Called from: receiveChatCharacter $1399 ; ---------------------------------------------------------------------- commGetReceivedByteEntry: jmp reportNoReceivedByte ; E006 slot 2: fetch a received chat byte - the trainer never has one ; ---------------------------------------------------------------------- ; commSendByteEntry - Jump-table slot 3 (JMP commNoOpClc). The chat character handed over is simply ; dropped and C=0 ('accepted') is returned. ; In: A = byte to send (ignored) ; Out: C=0 ; Called from: handleChatSend $1396 (JMP, not JSR) ; ---------------------------------------------------------------------- commSendByteEntry: jmp commNoOpClc ; E009 slot 3: send a chat byte - the trainer throws it away ; ---------------------------------------------------------------------- ; commFrameTickEntry - Jump-table slot 4 (JMP commNoOpClc). In the modem build this services the ; software UART once per frame; with no serial line there is nothing to do. ; In: none ; Out: C=0 ; Called from: rasterIrqHandler $112C ; ---------------------------------------------------------------------- commFrameTickEntry: jmp commNoOpClc ; E00C slot 4: per-IRQ service tick - nothing to service without a UART ; ---------------------------------------------------------------------- ; Two one-instruction stubs the module can point a vector at. $E00F is a plain RTS and $E010 an RTI; ; $E010 is the initial value of nmiChainVector ($E031/$E032), so an NMI arriving before the game ; patches ; that vector simply returns. ; ---------------------------------------------------------------------- commRtsRtiStubs: rts ; E00F $60 = RTS: harmless body for any vector pointed here .byte $40 ; E010 $40 = RTI: the module's default NMI handler, target of the initial $E031/$E032 = $E010 isOriginateMode: .byte $00 ; E011 answer/originate flag: waitForAnswerOriginateKey ($0CDE) stores 0 for 'A' and 1 for 'O'. Modem-only; nothing in the trainer build reads it ; ---------------------------------------------------------------------- ; commKeyboardScanVector - A JMP whose two operand bytes $E013/$E014 are the module's keyboard-scan ; vector. initCommModule patches them to the game's scanKeyboard ($0DB7) with the two stores at ; $0F4B/$0F50; until then the vector points at commNoOpClc, so no key is ever reported. ; In: none ; Out: whatever scanKeyboard returns (A = key code, X/Y = shift / Commodore-key flags) ; Called from: commReadKeyEntry $E015 (JMP). The XREF also lists a call from $E2EC, but that address ; is mid-instruction here - it belongs to the modem build that shares this address range. ; ---------------------------------------------------------------------- commKeyboardScanVector: jmp commNoOpClc ; E012 operand bytes $E013/$E014 are the keyboard vector, patched to scanKeyboard $0DB7 by $0F4B/$0F50 ; ---------------------------------------------------------------------- ; commReadKeyEntry - Jump-table slot 5 (JMP commKeyboardScanVector). This is the only route by which ; the game reads the keyboard, so a comm module that owns the CPU can steal keys before the game sees ; them. ; In: none ; Out: key code in A, shift / Commodore-key flags in X and Y ; Called from: pollKeyboardEvent $0E56 (every frame) ; ---------------------------------------------------------------------- commReadKeyEntry: jmp commKeyboardScanVector ; E015 slot 5: read one key through the patchable vector above ; Dead space. The modem build (track 34) puts a sixth jump-table entry at $E018; the trainer build ; leaves five $00 bytes here. Nothing in this file or in the main program calls $E018, and executing ; it would BRK. unusedJumpSlotE018: .byte $00 ; E018 five unused $00 bytes where the modem build has its sixth jump-table slot .byte $00,$00,$00,$00 ; E019 .... ; $E01D-$E042 is the fixed interface block that both comm-module builds present to the game at exactly ; the ; same addresses: the packet-exchange flags, the received and outgoing packet lengths and their two ; 7-byte buffers, the build id, the NMI chain vector, the CIA2 ICR shadow, the disk-access suspend ; handshake and the three link flags. In the trainer build most of it is only ever written by ; handleCommRequest and setCommLinkState; $E034-$E038 and $E03E-$E042 are modem-build variables that ; stay zero for ever. commInterfaceBlock: .byte $00 ; E01D exchange flags: bit 7 = opponent packet received, bit 6 = exchange complete, bits 0/5 = send requested / in progress. handleCommRequest pins it at $C0 rxPacketLength: .byte $00 ; E01E length 0-7 of the 'received' packet the trainer fabricates in $E020 txPacketLength: .byte $00 ; E01F length of the outgoing packet the game built in $E027; the trainer never reads the bytes rxPacketBuffer: .byte $00 ; E020 received-packet buffer $E020-$E026, byte 0 = the command byte the engine will execute rxPacketArg1: .byte $00 ; E021 received packet byte 1 (first command argument) rxPacketArg2: .byte $00 ; E022 received packet byte 2 rxPacketArg3: .byte $00 ; E023 received packet byte 3 rxPacketArg4: .byte $00 ; E024 received packet bytes 4-6, the tail of the 7-byte buffer .byte $00,$00 ; E025 .. txPacketBuffer: .byte $00 ; E027 outgoing-packet buffer $E027-$E02D, filled by sendOutgoingPacket ($568C); discarded here txPacketArg1: .byte $00 ; E028 outgoing packet byte 1 txPacketArg2: .byte $00 ; E029 outgoing packet byte 2 txPacketArg3: .byte $00 ; E02A outgoing packet bytes 3-6 .byte $00,$00,$00 ; E02B ... hostOutCount: .byte $00 ; E02E chat transmit-queue depth; handleChatSend ($1357/$1376) only opens typing while it is 0, so in a solo game the editor opens and the text goes nowhere peerCommBuildId: .byte $00 ; E02F build id reported by the peer; never filled in without a link commBuildId: .byte $FF ; E030 comm build id: $FF = build 1 = the solo trainer (track 35). The game compares it with linkTypeFlag $0BA5 at $0AEA/$0B2C/$1A9F/$1AFA/$1B3D to see whether the right module is resident nmiChainVector: .byte $10 ; E031 NMI chain vector low byte, initially $10 -> $E010 (the RTI above); the game patches it to $1298 at $0BF3 nmiChainVectorHi: .byte $E0 ; E032 NMI chain vector high byte ($E0 here, patched to $12) ciaIcrShadow: .byte $7F ; E033 CIA2 ICR mask shadow; $7F means 'clear every CIA2 interrupt source'. readFilmByte ($5889/$58A8/$58CD) writes this back to $DD0D after banking the film RAM in - the trainer wants no CIA2 NMI at all L_E034: .byte $00 ; E034 $E034-$E037: modem-build link variables (command phase, retry state); never touched here .byte $00,$00,$00 ; E035 ... D_E038: .byte $00 ; E038 modem-build link variable; unused in the trainer build nmiSuspendRequest: .byte $00 ; E039 disk-access suspend request: suspendCommModule stores $C0 here at $0F67, resumeCommModule 0 at $0F78 nmiSuspendAck: .byte $00 ; E03A suspend acknowledge; handleCommRequest copies $E039 into it so the wait at $0F6A/$0F7B always succeeds at once isLinkActive: .byte $00 ; E03B link-active flag, $FF after setCommLinkState(X=0); read all over the main program ($0F60, $1331, $1A40, $4E46, ...) linkStatus: .byte $00 ; E03C carrier / link status; kept at $FF so the bit-6 test in pollMenuSession ($56B1) and the bit-7 test in hangUpModemSetState ($1B4B) always pass linkStatusSample: .byte $00 ; E03D $E03D = link-ready sample read at $1BAB; $E03E is a modem-build byte and stays zero .byte $00 ; E03E . D_E03F: .byte $00 ; E03F modem-build reply timer; unused here D_E040: .byte $00 ; E040 modem-build connection phase; unused here D_E041: .byte $00 ; E041 modem-build 'skip init string' flag; unused here D_E042: .byte $00 ; E042 modem-build UART restart request; unused here ; ---------------------------------------------------------------------- ; handleCommRequest - Body of commRequestEntry. X=1 jumps to buildTrainerReplyPacket, which is where ; the computer opponent actually runs. X=3 forgets the outgoing packet. Every other code (X=0 ; per-frame poll, X=2 acknowledge) simply declares the exchange finished by storing $C0 in the ; exchange flags and mirroring the suspend request into the suspend acknowledge. ; In: X = request code 0..3 ; Out: $E01D = $C0 and $E03A = $E039, or $E01F = 0 for X=3, or a whole AI turn for X=1 ; Called from: commRequestEntry $E000 ; ---------------------------------------------------------------------- handleCommRequest: cpx #$01 ; E043 request 1 = 'transmit the outgoing packet'? D_E045: bne L_E04A ; E045 no - test the remaining request codes D_E047: jmp buildTrainerReplyPacket ; E047 yes: instead of transmitting, run one step of the computer opponent L_E04A: cpx #$03 ; E04A request 3 = 'cancel the pending send'? D_E04C: bne L_E054 ; E04C no - fall through to the poll / acknowledge case D_E04E: lda #$00 ; E04E cancel: drop whatever was queued sta txPacketLength ; E050 outgoing packet length = 0 rts ; E053 done ; ---------------------------------------------------------------------- ; Poll (X=0) and acknowledge (X=2) share this tail. Because the flags are unconditionally set to 'own ; packet sent AND opponent packet received', processCommandExchange ($4DB7) never waits for a peer and ; the suspend/resume handshake around every disk access completes on the first poll. ; ---------------------------------------------------------------------- L_E054: lda #$C0 ; E054 $C0 = bit 7 'opponent packet received' + bit 6 'exchange complete' D_E056: sta commInterfaceBlock ; E056 report the exchange as finished the instant it is asked about D_E059: lda nmiSuspendRequest ; E059 take the disk-access suspend request... sta nmiSuspendAck ; E05C ...and answer it immediately, so $0F5F/$0F71 never block D_E05F: rts ; E05F done ; ---------------------------------------------------------------------- ; setCommLinkState - Body of commSetLinkStateEntry. X=0 stores $FF into the three link flags ; $E03D/$E03C/$E03B, X>=1 stores 0 in all three and also clears the outgoing packet length. Since the ; carrier byte $E03C is $FF whenever the link is 'open', every carrier test in the main program sees a ; healthy connection. ; In: X = 0 open, >= 1 close ; Out: $E03B, $E03C, $E03D and (on close) $E01F ; Called from: commSetLinkStateEntry $E003 ; ---------------------------------------------------------------------- setCommLinkState: lda #$FF ; E060 $FF = link up cpx #$01 ; E062 X=0 means open, anything else means close bcc L_E06B ; E064 open - keep $FF lda #$00 ; E066 close - the three flags become 0 sta txPacketLength ; E068 and the outgoing packet is discarded L_E06B: sta linkStatusSample ; E06B link-ready sample sta linkStatus ; E06E carrier status - $FF makes every carrier test in the game succeed L_E071: sta isLinkActive ; E071 link-active flag the main program polls everywhere rts ; E074 done ; ---------------------------------------------------------------------- ; reportNoReceivedByte - SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in ; the trainer build. ; In: none ; Out: C=1 ; Called from: commGetReceivedByteEntry $E006 (JMP) ; ---------------------------------------------------------------------- reportNoReceivedByte: sec ; E075 C=1 = 'no character available' rts ; E076 the trainer never delivers opponent chat text ; ---------------------------------------------------------------------- ; commNoOpClc - CLC + RTS: the shared no-op used as the body of commSendByteEntry, commFrameTickEntry ; and the unpatched keyboard vector. $E078 is the bare RTS on its own. ; In: none ; Out: C=0 ; Called from: commSendByteEntry $E009, commFrameTickEntry $E00C, commKeyboardScanVector $E012 (all ; JMP) ; ---------------------------------------------------------------------- commNoOpClc: clc ; E077 C=0 = 'accepted' / 'nothing to report' commNoOpRts: rts ; E078 shared bare RTS ; ---------------------------------------------------------------------- ; runTrainerRoundHook - Once-per-round hook, called from finishRound ($4654) right after the game ; clock has been decremented and only in a solo game ($0BA5 bit 7 set, film playback excluded). Unless ; the drone-flight sub-overlay is currently sitting at $EE00 it runs planTrainerRound, the computer ; opponent's 'think' routine. ; In: $8977 drone timer (bit 7 = drone sub-overlay resident) ; Out: see planTrainerRound ($E170) ; Called from: finishRound $4654 ; ---------------------------------------------------------------------- runTrainerRoundHook: lda D_8977 ; E079 drone timer; bit 7 marks the drone-flight overlay as resident at $EE00 bmi L_E081 ; E07C the drone-flight AI occupies $EE00-$EFFF, so planTrainerRound must not run (it would call chooseAiDroneTarget at $EEE0) D_E07E: jsr planTrainerRound ; E07E run the once-per-round planner L_E081: rts ; E081 back to finishRound ; ---------------------------------------------------------------------- ; buildTrainerReplyPacket - Entered with X=1 from sendOutgoingPacket, i.e. once per lock-step ; exchange, immediately after the human's packet was handed over. It clears the received-packet length ; and gives up at once during the setup phase, during film playback, after the game has ended or while ; the game is paused. Otherwise it manages the swappable $EE00-$EFFF quarter of the module: while a ; drone is airborne it loads track 29 sectors 4-5 there (the drone-flight AI) and calls $EE00 every ; exchange; when the drone is gone it reloads track 35 sectors 14-15 (its own missile/target tail). In ; the normal case it ticks the launch countdown down and calls injectAiCommandsIntoPacket, which moves ; whole queued AI commands into the received-packet buffer $E020. ; In: gamePhase zp_B1, filmPlaybackMode $0B9B, gameEndReason $0BA8, pauseState zp_BE, ; droneControlFlags $920D, drone timer $8977 ; Out: $E01E/$E020.. = the fabricated 'received' packet, $8976 drone target, $8977 timer, and ; $EE00-$EFFF possibly reloaded from disk ; Called from: handleCommRequest $E047 (JMP, request code 1) ; Guards first: the AI only produces orders while a real battle is running. ; ---------------------------------------------------------------------- buildTrainerReplyPacket: lda #$00 ; E082 start with an empty incoming packet D_E084: sta rxPacketLength ; E084 received packet length = 0 lda gamePhase ; E087 gamePhase is 0 only while the battle proper runs bne L_E0E7 ; E089 side selection / unit setup / menus - stay silent lda filmPlaybackMode ; E08B film playback flag D_E08E: bmi L_E0E7 ; E08E bit 7 = replaying a film; the recorded stream supplies the orders D_E090: lda gameEndReason ; E090 non-zero once the game has ended bne L_E0E7 ; E093 game over - nothing left to order lda pauseState ; E095 pause / disconnect state bne L_E0E7 ; E097 paused - freeze the opponent as well ; ---------------------------------------------------------------------- ; $EE00-$EFFF is a third overlay layer inside this module. $8977 bit 7 says the drone-flight AI ; (track 29 sectors 4-5) is resident there; otherwise this module's own missile/target tail (track 35 ; sectors 14-15) is. The two loads below swap between them as the AI's drone takes off and lands. ; ---------------------------------------------------------------------- lda D_8977 ; E099 drone timer: bit 7 = the drone-flight overlay is resident at $EE00 bpl L_E0C1 ; E09C our own tail is resident - take the normal path lda droneControlFlags ; E09E drone control flags D_E0A1: and #$01 ; E0A1 bit 0 = a drone-move command is in force, i.e. a drone is airborne D_E0A3: beq L_E0AB ; E0A3 drone gone - swap our own tail back in L_E0A5: jsr targetEnemyComcenWithMissile; E0A5 still flying: call the drone-flight AI now loaded at $EE00 (track 29 s4-5, NOT the code assembled at $EE00 in this file) D_E0A8: jmp L_E0E7 ; E0A8 done for this exchange L_E0AB: jsr setSubOverlayLoadDest ; E0AB point the fast loader at $EE00 ldy #$23 ; E0AE Y = track $23 = 35, this module's own track lda #$02 ; E0B0 A = 2 sectors ldx #$0E ; E0B2 X = first sector 14: the native $EE00-$EFFF tail jsr loadSectors ; E0B4 reload it over the drone AI lda #$FF ; E0B7 $FF = no drone target chosen sta D_8976 ; E0B9 let chooseAiDroneTarget pick a fresh one lda #$03 ; E0BC 3 exchanges of cool-down before the next launch sta D_8977 ; E0BE storing 3 also clears bit 7, marking the drone overlay as gone L_E0C1: lda droneControlFlags ; E0C1 drone control flags and #$01 ; E0C4 bit 0 = a drone is airborne beq L_E0DC ; E0C6 no drone - just run the normal turn lda #$FF ; E0C8 $FF marks the drone overlay resident and blocks planning sta D_8977 ; E0CA store it in the drone timer jsr setSubOverlayLoadDest ; E0CD point the fast loader at $EE00 ldy #$1D ; E0D0 Y = track $1D = 29 ldx #$04 ; E0D2 X = first sector 4 lda #$02 ; E0D4 A = 2 sectors: the drone-flight AI jsr loadSectors ; E0D6 load it over our own tail jmp L_E0A5 ; E0D9 and fly the drone straight away ; ---------------------------------------------------------------------- ; Normal exchange: tick the drone launch countdown, then move as many complete AI commands as fit out ; of the queue at $88A0 into the received-packet buffer $E020. ; ---------------------------------------------------------------------- L_E0DC: lda D_8977 ; E0DC launch countdown beq L_E0E4 ; E0DF already 0 - launchAiDrone may fire this exchange dec D_8977 ; E0E1 otherwise tick it down by one exchange L_E0E4: jsr injectAiCommandsIntoPacket; E0E4 copy queued AI orders into the fabricated received packet L_E0E7: rts ; E0E7 return to sendOutgoingPacket ; ---------------------------------------------------------------------- ; trainerUnitStepHook - Per-unit hook called at the end of processUnitStep ($4526) for currentUnit, ; i.e. once for every living unit of both sides every round. For one of the AI's own units (index >= ; 50) that has now scored both end zones (bits 3 and 4 of the type byte) it sets bit 7 of that unit's ; 'has penetrated' flag so the planner stops counting it as still at home; for the AI comcen (unit 99) ; it also samples the 'was hit' bit of its display flags into the current and the one-round-sticky ; comcen-hit flags. Everything else is passed to updateAiUnitTracking, the AI's observer. ; In: currentUnit $90FA, unitTypeTable $F76C, comcen display flags $FBB7, drone timer $8977 ; Out: bit 7 of $8864+ordinal, $897B comcen hit this round, $897C comcen hit this or last round; ; otherwise calls $EACF ; Called from: processUnitStep $4526 ; ---------------------------------------------------------------------- trainerUnitStepHook: ldy currentUnit ; E0E8 the unit the engine has just stepped lda unitTypeTable,y ; E0EB type byte: bit 7 = dead, bits 3/4 = end zones already scored, bits 0-2 = type bmi L_E127 ; E0EE dead slot - nothing to observe cpy #$32 ; E0F0 $32 = 50: units 0-49 are the human player's bcc L_E11F ; E0F2 human unit - hand it to the observer and #$18 ; E0F4 keep only the two zone-scored bits L_E0F6: cmp #$18 ; E0F6 both the near zone (bit 3, columns 10-19) and the far zone (bit 4, columns 0-9) scored? bne L_E11F ; E0F8 not deep in the player's half yet lda D_8832,y ; E0FA $8832+unit is $8864+ordinal: this AI unit's 'has penetrated' flag ora #$80 ; E0FD mark it penetrated... sta D_8832,y ; E0FF ...so computeTrainerGroupStats stops counting it as still at home cpy #$63 ; E102 $63 = 99 = the AI's own comcen bne L_E11F ; E104 any other AI unit - hand it to the observer lda D_897B ; E106 this round's 'comcen was hit' flag... sta D_897C ; E109 ...becomes the one-round-old sticky flag ldx #$00 ; E10C assume 'not hit' this round lda side1ComcenDisplayFlags ; E10E display flags of unit 99 ($FB54 array) L_E111: and #$02 ; E111 bit 1 is set by recordUnitHit ($3C1A) whenever the unit takes damage beq L_E119 ; E113 not hit dex ; E115 X = $FF = hit stx D_897C ; E116 a fresh hit also re-arms the sticky flag L_E119: stx D_897B ; E119 record whether the comcen was hit this round L_E11C: jmp L_E127 ; E11C done with the comcen L_E11F: lda D_8977 ; E11F drone timer, bit 7 = the drone-flight overlay is resident, i.e. the AI's drone is airborne bmi L_E127 ; E122 the AI suspends its unit observation for as long as its drone is in the air jsr updateAiUnitTracking ; E124 track what this unit has seen and what it is shooting at L_E127: rts ; E127 back to processUnitStep ; ---------------------------------------------------------------------- ; setSubOverlayLoadDest - Points the fast loader at $EE00 (X = destination low byte, Y = high byte) ; and tail-jumps into setLoadDest. Used before each of the two sub-overlay loads in ; buildTrainerReplyPacket. ; In: none ; Out: fast-loader destination = $EE00 ; Called from: $E0AB and $E0CD ; ---------------------------------------------------------------------- setSubOverlayLoadDest: ldx #$00 ; E128 destination low byte = $00 ldy #$EE ; E12A destination high byte = $EE jmp setLoadDest ; E12C hand $EE00 to the fast loader's setLoadDest ; ---------------------------------------------------------------------- ; pickTrainerRallyPoint - Chooses the AI's current objective: a cell scattered at random around the ; human comcen (unit 49), stored as the rally column/row that every attacking group heads for. ; Refreshed every six rounds, and immediately whenever the human comcen becomes visible. ; In: side0ComcenCol $F671, side0ComcenRow $F6D5, the game RNG ; Out: $8969 rally column, $896A rally row ; Called from: planTrainerRound $E188; trainerSetupEC00 initTrainerAi $EC47 (once at game start) ; ---------------------------------------------------------------------- pickTrainerRallyPoint: lda side0ComcenCol ; E12F column of the human comcen (unit 49) jsr randomiseCoordinate ; E132 scatter it by a random offset sta D_8969 ; E135 rally column the AI's groups head for lda side0ComcenRow ; E138 row of the human comcen jsr randomiseCoordinate ; E13B scatter that as well sta D_896A ; E13E rally row rts ; E141 done ; randomCoordinateOffsetTable - eight signed cell offsets, picked with nextGameRandom AND 7 by ; randomiseCoordinate and then scaled by 1, 2 or 4 according to how well the AI knows where the human ; comcen is. randomCoordinateOffsetTable: .byte $01 ; E142 offsets +1,-1,+1,+1,-1,-1,+2 (entries 0-6) .byte $FF,$01,$01,$FF,$FF,$02 ; E143 ...... L_E149: .byte $FE ; E149 entry 7: -2 cells ; ---------------------------------------------------------------------- ; randomiseCoordinate - Returns A = base coordinate plus a random offset, clamped to 0..39. The base ; is written into the ADC immediate at $E164 and the offset comes from randomCoordinateOffsetTable ; indexed by nextGameRandom AND 7. The offset is used raw while the human comcen is actually visible ; (bit 7 of its energy byte), doubled once it has ever been seen ($896C bit 7) and quadrupled ; otherwise - so the AI aims accurately at a comcen it can see and wanders when it cannot. $E14D is an ; alternate entry that reuses the base already patched in. ; In: A = base coordinate 0-39, side0ComcenEnergy $F9F5, $896C 'comcen has been seen', the game RNG ; Out: A = coordinate 0..39; self-modifies the operand byte at $E164 (L_E163+1) ; Called from: pickTrainerRallyPoint $E132 and $E13B ; ---------------------------------------------------------------------- randomiseCoordinate: sta L_E163+1 ; E14A patch the base coordinate into the ADC immediate at $E164 randomiseCoordinateSameBase: jsr nextGameRandom ; E14D alternate entry that reuses the base already patched in (no caller in this build) and #$07 ; E150 keep 3 bits: offset table index 0-7 tay ; E152 use it as the table index lda randomCoordinateOffsetTable,y; E153 signed cell offset -2..+2 bit side0ComcenEnergy ; E156 energy byte of the human comcen; bit 7 = 'currently spotted' bmi L_E162 ; E159 visible right now - aim with the raw offset asl a ; E15B not visible: double the scatter bit D_896C ; E15C sticky 'the comcen has been seen at some point' flag bmi L_E162 ; E15F seen before - x2 scatter is enough asl a ; E161 never seen at all - x4, so the AI just roams L_E162: clc ; E162 prepare the add L_E163: adc #$FF ; E163 operand byte patched at $E14A with the base coordinate bpl L_E169 ; E165 result still positive - keep it lda #$00 ; E167 clamp to the left/top edge of the map L_E169: cmp #$28 ; E169 $28 = 40 columns/rows on the battlefield L_E16B: bcc L_E16F ; E16B inside the map lda #$27 ; E16D clamp to the last cell, 39 L_E16F: rts ; E16F A = coordinate 0-39 ; ---------------------------------------------------------------------- ; planTrainerRound - Top-level planner of the solo trainer, run once per game round. It resets the ; per-round order counter, refreshes the rally point, works out this round's action budget from the AI ; comcen's energy and stun state, and then either spends the round on comcen repair (budget below 20) ; or evaluates the threat around its own comcen and decides between attack mode ($8963 = 0, plus a ; drone decision) and defend mode ($8963 = $FF). It then re-plans every AI unit whose replan clock has ; come due and finishes by walking the comcen forward behind the assault group or retreating it. ; In: gameEndReason $0BA8, comcen records $F9F5/$FA27/$F9C3, gameTypeOptions $0BA3, gameClock $91CB, ; the RNG and the whole $8800-$897C AI work block ; Out: $8965 budget, $8968 order count, $896B rally timer, $896C comcen-seen flag, $8960-$8963 threat ; result, commands appended to the queue at $88A0 ; Called from: runTrainerRoundHook $E07E ; ---------------------------------------------------------------------- planTrainerRound: lda gameEndReason ; E170 non-zero once the game has ended beq L_E176 ; E173 still playing rts ; E175 game over - no plan L_E176: sta D_8968 ; E176 A is 0 here: clear the count of commands queued this round ; ---------------------------------------------------------------------- ; The rally point (a cell near the human comcen) is refreshed every six rounds, or at once on the ; round the human comcen becomes visible. ; ---------------------------------------------------------------------- lda side0ComcenEnergy ; E179 human comcen energy byte, bit 7 = currently spotted bpl L_E183 ; E17C not spotted this round sta D_896C ; E17E spotted: bit 7 sticks in $896C, so randomiseCoordinate scatters less from now on... bmi L_E188 ; E181 ...and the rally point is refreshed immediately L_E183: dec D_896B ; E183 otherwise count the rally-point refresh timer down bpl L_E190 ; E186 not due yet L_E188: jsr pickTrainerRallyPoint ; E188 pick a new random cell near the human comcen lda #$05 ; E18B 5 = next refresh in six rounds sta D_896B ; E18D reload the timer ; ---------------------------------------------------------------------- ; This round's action budget. Normally it is the AI comcen's own energy (0-50); with the COMCEN ; DAMAGE option on ($0BA3 bit 4) the comcen's stun counter modifies it. A budget under 20 means the ; AI can only afford to repair and refuel its comcen. ; ---------------------------------------------------------------------- L_E190: lda side1ComcenEnergy ; E190 AI comcen energy (unit 99) and #$3F ; E193 bits 0-5 hold the 0-50 energy points sta D_8965 ; E195 that is this round's action budget lda side1ComcenStun ; E198 AI comcen stun byte (unit 99 of the $F960 array) and #$0F ; E19B low nibble = the STUN'D counter 0-15 shown on the console sta D_8971 ; E19D remember how badly the comcen is stunned lda gameTypeOptions ; E1A0 game options byte and #$10 ; E1A3 bit 4 = the COMCEN DAMAGE option beq L_E1C1 ; E1A5 damage rules off - use a flat budget lda D_8971 ; E1A7 stun counter beq L_E1C6 ; E1AA not stunned - keep the energy budget cmp #$0F ; E1AC $0F = maximum stun D_E1AE: beq L_E1D0 ; E1AE completely out - the AI does nothing at all this round cmp #$08 ; E1B0 stun 8-14 bcs L_E1C1 ; E1B2 heavy stun (8-14) takes the flat 50 budget while a light stun (1-7) is penalised 16 points below - counter-intuitive, possibly a slip for a branch to $E1CD lda D_8965 ; E1B4 stun 1-7: take the energy budget... sec ; E1B7 prepare the subtraction sbc #$10 ; E1B8 ...and charge a 16 point penalty bcc L_E1CD ; E1BA budget went negative - only repair the comcen sta D_8965 ; E1BC store the reduced budget bcs L_E1C6 ; E1BF always taken (the SBC above left C=1) L_E1C1: lda #$32 ; E1C1 $32 = 50 = a full budget sta D_8965 ; E1C3 store it L_E1C6: lda D_8965 ; E1C6 this round's budget cmp #$14 ; E1C9 $14 = 20 points L_E1CB: bcs L_E1D1 ; E1CB enough left to manoeuvre L_E1CD: jsr runAiComcenActions ; E1CD too little - spend the round on comcen repair and energy L_E1D0: rts ; E1D0 done ; ---------------------------------------------------------------------- ; Threat assessment around the AI's own comcen. assessThreatAroundAiComcen buckets the human units by ; compass direction and returns the overall threat, the deepest weapon-range penetration and the ; safest direction; those three become $8962, $8961 and $8960. The AI defends when an enemy weapon ; actually covers the comcen and it has either no assault group to rely on or has just been hit. ; ---------------------------------------------------------------------- L_E1D1: lda side1ComcenCol ; E1D1 AI comcen column sta cellColB ; E1D4 reference point B of the threat scan lda side1ComcenRow ; E1D6 AI comcen row sta cellRowB ; E1D9 reference point B of the threat scan jsr assessThreatAroundAiComcen; E1DB bucket the human units around it into 8 compass directions sta D_8962 ; E1DE A = overall threat score sty D_8960 ; E1E1 Y = safest direction 0-7 (8 = nothing anywhere) stx D_8961 ; E1E4 X = deepest weapon-range penetration, 0 = no enemy weapon covers the comcen ldy #$00 ; E1E7 assume attack mode (0) L_E1E9: cpx #$00 ; E1E9 is any enemy weapon covering the comcen? beq L_E1F9 ; E1EB no threat at all - attack dey ; E1ED Y = $FF = defend bit D_895D ; E1EE is an assault group selected? bit 7 = none bmi L_E1F9 ; E1F1 no group to rely on - defend bit D_897C ; E1F3 was the comcen hit this round or last? bmi L_E1F9 ; E1F6 yes - defend iny ; E1F8 threatened but coping - attack after all L_E1F9: sty D_8963 ; E1F9 0 = attack, $FF = defend tya ; E1FC A = the mode just chosen bne L_E202 ; E1FD defending - do not spend a drone jsr chooseAiDroneTarget ; E1FF attacking - decide what the next drone should be sent at L_E202: jsr scanTrainerUnitsForOrders; E202 issue orders to every AI unit whose replan clock has come due lda D_8968 ; E205 commands queued so far this round cmp #$05 ; E208 5 is the per-round ceiling L_E20A: bcs L_E21E ; E20A budget spent - stop here jsr updateFocusGroupCentre ; E20C refresh the assault group's id and centre ldx D_8963 ; E20F attack (0) or defend ($FF)? bmi retreatTrainerComcen ; E212 defend - pull the comcen back and recall the group lda D_8965 ; E214 action budget cmp #$20 ; E217 $20 = 32 points bcs pushTrainerComcenForward; E219 plenty of energy - walk the comcen up behind the attack jsr runAiComcenActions ; E21B low budget - repair the comcen instead L_E21E: rts ; E21E done ; ---------------------------------------------------------------------- ; pushTrainerComcenForward - Attack-mode tail of planTrainerRound (a branch target, not a JSR entry). ; If an assault group exists, the comcen is not stunned and its own replan clock has come due, it ; queues a MOVE for the AI comcen to a cell one step beyond the enemy's reach from the assault group's ; centre, and re-arms the comcen's replan clock to gameClock - 3. This is how the trainer walks its ; command centre up behind its attack. ; In: $895D assault group id, $8971 comcen stun, $8831 comcen replan clock, gameClock $91CB, $8960 ; safest direction, $8961 enemy reach, $895E/$895F assault group centre ; Out: $8831 re-armed, one $80/$81 MOVE queued through orderAiUnitToCell ; Called from: planTrainerRound $E219 (branch) ; ---------------------------------------------------------------------- pushTrainerComcenForward: ldy D_895D ; E21F assault group id, bit 7 = none selected L_E222: bmi L_E265 ; E222 no group to follow lda D_8971 ; E224 comcen stun counter bne L_E265 ; E227 stunned - it cannot move lda D_8831 ; E229 the comcen's own replan clock cmp gameClock ; E22C the clock counts down, so 'stamp >= clock' means the round has arrived bcc L_E265 ; E22F not due yet lda gameClock ; E231 current round sec ; E234 prepare the subtraction sbc #$03 ; E235 next comcen replan in 3 rounds L_E237: bcs L_E23B ; E237 no underflow lda #$00 ; E239 clamp at round 0 L_E23B: sta D_8831 ; E23B re-arm the comcen replan clock ldx D_8960 ; E23E safest direction from the threat scan lda dirToColDelta,x ; E241 column step -1/0/+1 for that direction tax ; E244 X = column step for offsetCoordinateClamped ldy D_8961 ; E245 how deep the enemy weapons reach iny ; E248 one cell beyond that lda msgOpponentUsingNonStandardGame; E249 $895E = assault group centre column (the label is a stale message-string name) jsr offsetCoordinateClamped ; E24C column = centre + step * distance, clamped to 0..39 sta scratch18 ; E24F stash the target column ldx D_8960 ; E251 same direction... lda dirToRowDelta,x ; E254 ...row step -1/0/+1 tax ; E257 X = row step L_E258: lda D_895F ; E258 assault group centre row jsr offsetCoordinateClamped ; E25B row = centre + step * distance, clamped ldx scratch18 ; E25E X = column, A = row L_E260: ldy #$31 ; E260 $31 = AI ordinal 49 = unit 99, the AI comcen jsr orderAiUnitToCell ; E262 queue the MOVE L_E265: rts ; E265 done ; ---------------------------------------------------------------------- ; retreatTrainerComcen - Defend-mode tail of planTrainerRound (a branch target). It stamps the commit ; clock $896E with the current round - from then on groups are allowed to use their full combat value ; - and, unless the comcen is stunned, queues a MOVE that walks it away along the safest direction by ; as many cells as the enemy reaches. If the threat score still exceeds the recall threshold $897A, or ; the comcen was stunned, it recomputes the assault group's statistics and jumps to ; aimGroupAtOwnComcen to pull the whole group home. ; In: $8971 comcen stun, $8960 safest direction, $8961 enemy reach, $8962 threat score, $897A recall ; threshold, $895D assault group, comcen position $F6A3/$F707 ; Out: $896E commit clock, $895A cleared, one comcen MOVE and possibly a full group recall ; Called from: planTrainerRound $E212 (branch) ; ---------------------------------------------------------------------- retreatTrainerComcen: lda gameClock ; E266 current round sta D_896E ; E269 stamp the commit clock: groups may now use their full strength lda D_8971 ; E26C comcen stun counter bne L_E29F ; E26F stunned - it cannot run, go straight to recalling the group ldx D_8960 ; E271 safest direction lda dirToColDelta,x ; E274 its column step tax ; E277 X = column step ldy D_8961 ; E278 run as far as the enemy reaches lda side1ComcenCol ; E27B AI comcen column jsr offsetCoordinateClamped ; E27E clamped destination column sta scratch18 ; E281 stash it L_E283: ldx D_8960 ; E283 safest direction again lda dirToRowDelta,x ; E286 its row step tax ; E289 X = row step lda side1ComcenRow ; E28A AI comcen row sub_E28D: jsr offsetCoordinateClamped ; E28D clamped destination row ldx scratch18 ; E290 X = column ldy #$31 ; E292 $31 = AI ordinal 49 = unit 99 jsr orderAiUnitToCell ; E294 queue the comcen MOVE lda D_8962 ; E297 threat score from the scan L_E29A: cmp D_897A ; E29A recall threshold, set to 5 by the setup overlay at $EC44 bcc L_E2B6 ; E29D manageable - leave the assault group where it is L_E29F: lda D_895D ; E29F assault group id cmp #$FF ; E2A2 $FF = no group at all beq L_E2B6 ; E2A4 nothing to recall and #$0F ; E2A6 strip the disable bit, leaving the group index 0-11 tay ; E2A8 Y = the group index for computeTrainerGroupStats jsr computeTrainerGroupStats; E2A9 recompute its members, strength, centre and box bcs L_E2B6 ; E2AC empty group - nothing to recall lda #$00 ; E2AE pretend the group has no slow units... sta D_895A ; E2B0 ...so issueTrainerGroupOrders does not double the replan delay - this is an urgent recall jmp aimGroupAtOwnComcen ; E2B3 march the whole group home to the comcen L_E2B6: rts ; E2B6 done ; unitCombatValueTable - combat weight per unit type, indexed by the type bits 0-2 of $F76C. Summed ; into the 16-bit group value by computeTrainerGroupStats and also copied into the per-unit influence ; weight page $0464 by buildUnitThreatTables ($E7AF). unitCombatValueTable: .byte $02 ; E2B7 GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0 .byte $02,$03,$00,$00 ; E2B8 .... ; trainerUnitScanIndex - the loop counter of scanTrainerUnitsForOrders, kept as a byte inside the code ; stream rather than in the $8800 work block. Written at $E2BF and $E2EB, read at $E2CE. trainerUnitScanIndex: .byte $00 ; E2BC current AI ordinal being scanned, 49 down to 0 ; ---------------------------------------------------------------------- ; scanTrainerUnitsForOrders - Walks the AI's 50 unit slots from ordinal 49 (unit 99, the comcen) down ; to 0, stopping as soon as four commands have been queued this round. A dead slot has its replan ; clock zeroed and is removed from every group; a slot whose replan clock has been reached by the ; down-counting game clock is handed to decideTrainerGroupAction, which re-plans the whole group that ; unit belongs to. The default replan urgency is reloaded with 5 before every unit. ; In: $8968 orders this round, gameClock $91CB, side1UnitTypeTable $F79E, replan clocks $8800, group ; ids $8832 ; Out: $E2BC, $8800 and $8832 entries, $896D urgency; orders queued by the callee ; Called from: planTrainerRound $E202 ; ---------------------------------------------------------------------- scanTrainerUnitsForOrders: lda #$31 ; E2BD $31 = 49, the highest AI ordinal (unit 99) sta trainerUnitScanIndex ; E2BF start the walk at the comcen and count down ; ---------------------------------------------------------------------- ; Loop over AI ordinals 49..0. It maintains the embedded counter at $E2BC and stops early once four ; commands have been queued. ; ---------------------------------------------------------------------- L_E2C2: lda D_8968 ; E2C2 commands queued so far this round cmp #$04 ; E2C5 4 is the ceiling for unit orders bcs L_E2F0 ; E2C7 budget spent lda #$05 ; E2C9 default replan urgency: re-plan again in at most 5 rounds sta D_896D ; E2CB reload the default urgency before every unit ldy trainerUnitScanIndex ; E2CE current AI ordinal 49..0 lda side1UnitTypeTable,y ; E2D1 type byte of unit ordinal+50 bpl L_E2E0 ; E2D4 still alive lda #$00 ; E2D6 dead: clear its replan clock... sta msgGetOpponentOnPhoneAnd,y; E2D8 ...at $8800+ordinal lda #$FF ; E2DB ...and take it out of every group sta D_8832,y ; E2DD $FF = belongs to no group L_E2E0: lda msgGetOpponentOnPhoneAnd,y; E2E0 this unit's replan clock cmp gameClock ; E2E3 the clock counts down; stamp >= clock means the planned round has come L_E2E6: bcc L_E2EB ; E2E6 not due yet jsr decideTrainerGroupAction; E2E8 re-plan the whole group this unit belongs to L_E2EB: dec trainerUnitScanIndex ; E2EB next ordinal down bpl L_E2C2 ; E2EE loop until ordinal 0 has been done L_E2F0: rts ; E2F0 done ; ---------------------------------------------------------------------- ; decideTrainerGroupAction - Decides what the AI group owning unit ordinal Y should do. Group 0 is the ; comcen's own escort and is only re-planned while the comcen is unstunned and no assault group is ; selected, in which case group 0 simply inherits the comcen's own ordered destination. For any group ; it caches the group flags, trims them to the low nibble and calls computeTrainerGroupStats; an empty ; group aborts. It then picks one of four behaviours: march home to the AI comcen (the home-defence ; group, or any group during a recall), charge the rally point when the group has already penetrated ; and made contact, form a defensive line on row 29 when it has not, or - for group 0 - run a free ; direction search out from the comcen. ; In: Y = AI unit ordinal, $8832 group ids, $8971 comcen stun, $895D assault group, comcen ; destination $F833/$F897, $8964 home group, $8966 flag cache, zp_1D members still at home ; Out: $88D2/$88DE group target, $893E group flags, $896D urgency, $8955-$8963 group scratch; ends in ; one of the order-issuing tails ; Called from: scanTrainerUnitsForOrders $E2E8 ; ---------------------------------------------------------------------- decideTrainerGroupAction: lda D_8832,y ; E2F1 group id of this AI unit bne L_E31A ; E2F4 groups 1..11 are ordinary objectives ; ---------------------------------------------------------------------- ; Group 0 is the comcen's own escort. It is only worth re-planning while the comcen can still move ; and no assault group has been picked; then it just adopts the comcen's own destination as its ; target. ; ---------------------------------------------------------------------- lda D_8971 ; E2F6 group 0: is the comcen stunned? bne L_E319 ; E2F9 stunned - leave the escort alone ldx #$03 ; E2FB urgency 3 for the comcen's escort stx D_896D ; E2FD urgency for group 0 ldx D_895D ; E300 is an assault group already selected? bpl L_E319 ; E303 yes - group 0 is not the one to re-plan lda side1ComcenDestCol ; E305 AI comcen ordered destination column and #$3F ; E308 strip the flag bits, keep the 0-39 cell sta D_88D2 ; E30A group 0 target column = wherever the comcen is heading lda side1ComcenDestRow ; E30D AI comcen ordered destination row and #$3F ; E310 strip the flag bits, keep the 0-39 cell sta D_88DE ; E312 group 0 target row lda #$00 ; E315 A = group id 0 beq L_E31A ; E317 always taken L_E319: rts ; E319 nothing to do for this group L_E31A: tay ; E31A Y = group id L_E31B: lda D_893E,y ; E31B flags of that group sta D_8966 ; E31E keep a copy for the tests below (bit 6 = has made contact with the player's comcen) and #$0F ; E321 clear bits 4-7, leaving the start-delay nibble sta D_893E,y ; E323 write the trimmed flags back jsr computeTrainerGroupStats; E326 members, strength, centre and bounding box of the group bcc L_E32C ; E329 the group has members rts ; E32B empty group - nothing to order L_E32C: ldy D_8955 ; E32C group id again cpy D_8964 ; E32F the home-defence group chosen at setup ($EC39/$ECA3) L_E332: bne L_E356 ; E332 not the home group - decide between charging and digging in lda #$02 ; E334 urgency 2 - re-plan the home guard often sta D_896D ; E336 urgency for the home guard ; ---------------------------------------------------------------------- ; aimGroupAtOwnComcen - Internal tail (JMP target from retreatTrainerComcen $E2B3, fall-through from ; $E336): points the line target at the AI comcen's cell, then falls into the shared code at $E343 ; that copies the group centre into the search origin and the line target into the wanted destination, ; and jumps to issueTrainerGroupOrders. Used to recall a group to defend the comcen. ; In: comcen position $F6A3/$F707, group centre zp_93/zp_92 (left there by computeTrainerGroupStats) ; Out: zp_27/zp_28 = search origin, zp_22/zp_24 = wanted group destination; tail-jumps to $E49E ; Called from: $E2B3 (JMP) and by falling through from decideTrainerGroupAction $E336 ; ---------------------------------------------------------------------- aimGroupAtOwnComcen: lda side1ComcenCol ; E339 AI comcen column sta lineEndCol ; E33C line target column lda side1ComcenRow ; E33E AI comcen row sta lineEndRow ; E341 line target row ; ---------------------------------------------------------------------- ; sendGroupStraightToTarget - shared tail (also reached from $E3AF and $E3CD): move the group in a ; straight line to (lineEndCol, lineEndRow) with no threat search at all. ; ---------------------------------------------------------------------- sendGroupStraightToTarget: lda lineCurCol ; E343 group centre column (set by computeTrainerGroupStats) sta cellColB ; E345 search origin / reference column lda lineCurRow ; E347 group centre row sta cellRowB ; E349 search origin / reference row lda lineEndCol ; E34B line target column sta cellColA ; E34D wanted group destination column lda lineEndRow ; E34F line target row sta cellRowA ; E351 wanted group destination row sub_E353: jmp issueTrainerGroupOrders ; E353 issue the move orders for the whole group ; ---------------------------------------------------------------------- ; Not the home guard. A group that still has members which have not scored both end zones, and which ; has not yet made contact with the player's comcen (flags bit 6), falls back to a defensive line; ; anything else pushes on towards the rally point. ; ---------------------------------------------------------------------- L_E356: bit D_8966 ; E356 cached group flags bvs L_E35F ; E359 bit 6 set: the group has already made contact with the player's comcen lda scratch1D ; E35B count of members that have not scored both end zones bne aimGroupAtHomeLine ; E35D some are still at home - form a defensive line instead L_E35F: ldy D_8955 ; E35F group id L_E362: bne L_E37A ; E362 groups 1..11 head for the rally point lda D_8963 ; E364 group 0 (the comcen escort): attack or defend? bmi L_E379 ; E367 defending - the escort stays where it is lda D_8960 ; E369 safest direction around the comcen sta scratch19 ; E36C use it as the fan heading jsr buildDirectionFan ; E36E lay out the 8 candidate directions in $8896-$889D lda #$05 ; E371 try to advance 5 cells sta D_895B ; E373 wanted advance distance for the search jmp findTrainerAdvanceCell ; E376 run the threat-aware path search L_E379: rts ; E379 nothing to do ; ---------------------------------------------------------------------- ; Groups 1..11: head for the rally point. How boldly depends on whether the human comcen has been ; found, how much of the game is left and whether this group has already made contact with it. ; ---------------------------------------------------------------------- L_E37A: lda D_8969 ; E37A rally column near the human comcen sta lineEndCol ; E37D line target column lda D_896A ; E37F rally row sta lineEndRow ; E382 line target row L_E384: lda side0ComcenEnergy ; E384 human comcen energy byte, bit 7 = spotted bmi L_E392 ; E387 spotted lda gameClock ; E389 rounds left in the game cmp #$28 ; E38C $28 = 40 rounds bcs searchGroupPathToTarget ; E38E plenty of time left - just search for a safe advance bcc L_E39F ; E390 late in the game - be twice as bold L_E392: bit D_8966 ; E392 cached group flags bvc searchGroupPathToTarget ; E395 no contact with the comcen yet - just search lda D_893E,y ; E397 group flags ora #$80 ; E39A bit 7 = this group has the spotted comcen in its sights sta D_893E,y ; E39C store the flags back L_E39F: asl D_8956 ; E39F double the group's combat value, low byte... D_E3A2: rol D_8957 ; E3A2 ...and high byte, so the risk test accepts twice as much opposition bit D_8966 ; E3A5 cached group flags bvc searchGroupPathToTarget ; E3A8 no contact - fall back to the cautious search lda D_8976 ; E3AA the unit the next drone is aimed at cmp #$31 ; E3AD $31 = 49 = the human comcen beq sendGroupStraightToTarget; E3AF the drone is already going for the comcen - the group beelines to the rally point too jsr isPathToPlayerComcenClear; E3B1 is the straight line to the human comcen free of living enemy combat units? bcc searchGroupPathToTarget ; E3B4 blocked - fall back to the cautious search sub_E3B6: ldy D_8955 ; E3B6 group id sub_E3B9: lda D_8902,y ; E3B9 number of members sub_E3BC: cmp #$04 ; E3BC 4 or more bcc L_E3C8 ; E3BE too small to commit lda D_893E,y ; E3C0 group flags ora #$20 ; E3C3 bit 5 = committed to the all-out attack (chooseAiDroneTarget weighs this doubled) sta D_893E,y ; E3C5 store the committed flag back L_E3C8: lda #$03 ; E3C8 urgency 3 sta D_896D ; E3CA re-plan this group again in 3 rounds at the latest jmp sendGroupStraightToTarget; E3CD charge straight at the rally point ; ---------------------------------------------------------------------- ; aimGroupAtHomeLine - Internal branch target used when the group still has members that have not ; reached the player's end zones and has no contact with his comcen: the target column stays where the ; group already is and the target row becomes 29 plus the group's own vertical extents, i.e. a ; defensive line inside the AI's own half. While the commit clock $896E has not been passed the ; group's combat value is also capped at 9, which makes the risk test reject anything but a very quiet ; cell. ; In: group centre zp_93, group spans $891A/$8932, commit clock $896E, gameClock $91CB, group value ; $8956/$8957 ; Out: zp_94/zp_95 target cell, $8956/$8957 possibly capped at 9; falls into the path search at $E400 ; Called from: decideTrainerGroupAction $E35D (branch) ; ---------------------------------------------------------------------- aimGroupAtHomeLine: lda lineCurCol ; E3D0 group centre column sta lineEndCol ; E3D2 hold the column, only the row changes lda #$1D ; E3D4 $1D = row 29 ldy D_8955 ; E3D6 group id clc ; E3D9 prepare the two adds adc D_891A,y ; E3DA plus the group's reach above its centre adc D_8932,y ; E3DD plus its reach below, so the whole formation box sits inside the AI's own half sta lineEndRow ; E3E0 defensive line row lda D_896E ; E3E2 commit clock, set at setup ($ED85) and re-stamped by every comcen retreat cmp gameClock ; E3E5 the clock counts down: stamp >= clock means the commit round has passed bcs searchGroupPathToTarget ; E3E8 committed - let the group use its real strength L_E3EA: lda D_8957 ; E3EA group combat value, high byte bne L_E3F6 ; E3ED over 255 - definitely cap it lda D_8956 ; E3EF low byte L_E3F2: cmp #$09 ; E3F2 9 is the cautious ceiling bcc searchGroupPathToTarget ; E3F4 already weak enough L_E3F6: lda #$00 ; E3F6 cap the value at 9 so the risk test only accepts very quiet cells sta D_8957 ; E3F8 high byte of the capped value = 0 lda #$09 ; E3FB 9 = the cautious ceiling sta D_8956 ; E3FD low byte of the capped value ; ---------------------------------------------------------------------- ; searchGroupPathToTarget - shared entry (branch target from $E38E, $E395, $E3A8, $E3B4, $E3E8 and ; $E3F4): measure how far the target is, build the eight-way candidate fan around the heading towards ; it and run the threat-aware search. When the group already stands on its target there is no ; heading, so the search is skipped and the members are merely re-stamped with a two-round replan ; delay. ; ---------------------------------------------------------------------- searchGroupPathToTarget: lda lineCurCol ; E400 group centre column sta cellColB ; E402 search origin column lda lineCurRow ; E404 group centre row sta cellRowB ; E406 search origin row lda lineEndCol ; E408 target column sta cellColA ; E40A point A of the distance measure lda lineEndRow ; E40C target row sta cellRowA ; E40E point A row jsr cellDistance ; E410 distance from the group to its target, 0-16 cells sta D_895B ; E413 that is how far the group would like to advance D_E416: jsr buildDirectionFanTowardTarget; E416 heading towards the target plus the 8 candidate directions in $8896-$889D lda scratch19 ; E419 heading 0-7, or $FF when the group already stands on the target bpl findTrainerAdvanceCell ; E41B valid heading - search for a safe cell along it lda #$00 ; E41D already there: zero movement delta sta scratch2A ; E41F no column shift sta scratch2B ; E421 no row shift lda #$02 ; E423 replan again in 2 rounds sta scanListIndex ; E425 zp_29 = replan delay in rounds jmp L_E4C7 ; E427 skip the search and just re-stamp the members ; ---------------------------------------------------------------------- ; findTrainerAdvanceCell - Threat-aware path search. It first builds the per-unit influence map ; (radius in $0400, weight in $0464) with buildUnitThreatTables. Then, for each of the eight direction ; ranks in the fan at $8896, it steps outward from the search origin in jumps of two cells, testing ; every candidate with isGroupDestinationTooRisky; it keeps extending while the cell is acceptable, up ; to the wanted advance distance $895B or the per-rank cap in maxAdvanceByRankTable, and moves on to ; the next rank as soon as a candidate is rejected before either limit is reached. If no rank works ; the group stays put (direction 0, distance 0). The winning direction and distance are finally turned ; into the absolute target cell zp_22/zp_24 and the code falls into issueTrainerGroupOrders. ; In: zp_27/zp_28 search origin, $8896-$889D direction fan, $895B wanted distance, $8956/$8957 group ; strength ; Out: zp_22/zp_24 = chosen target cell, zp_23 = distance, zp_26 = direction, $0400/$0464 influence ; map; falls into $E49E ; Called from: decideTrainerGroupAction $E376 (JMP) and $E41B (branch) ; ---------------------------------------------------------------------- findTrainerAdvanceCell: jsr buildUnitThreatTables ; E42A build the per-unit influence map in the $0400/$0464 scratch pages D_E42D: lda #$00 ; E42D start with direction rank 0, the best one in the fan D_E42F: sta cellColA ; E42F zp_22 doubles as the rank index for the length of the search ; ---------------------------------------------------------------------- ; Outer loop over the eight direction ranks; inner loop at $E435 pushes two cells further out at a ; time along the current rank while the destination stays acceptable. ; ---------------------------------------------------------------------- L_E431: lda #$00 ; E431 distance reached along this rank = 0 sta scratch23 ; E433 zp_23 = distance reached along this rank L_E435: ldy cellColA ; E435 current rank ldx D_8896,y ; E437 the direction the fan put in that rank stx scanRowCount ; E43A keep it as the candidate heading lda scratch23 ; E43C distance reached so far clc ; E43E prepare the add adc #$02 ; E43F probe two cells further out sta scratch2A ; E441 candidate distance tay ; E443 Y = distance for the risk test D_E444: jsr isGroupDestinationTooRisky; E444 too much enemy influence on the formation box at that spot? D_E447: bcs L_E464 ; E447 rejected - stop extending this rank ldx scanRowCount ; E449 candidate heading ldy scratch2A ; E44B candidate distance jsr offsetPointInDirection ; E44D does the cell itself stay on the 40x40 map? bcs L_E464 ; E450 off the map - stop extending lda scratch2A ; E452 accept the new distance sta scratch23 ; E454 remember it as the distance reached on this rank cmp D_895B ; E456 as far as the group wanted to go? D_E459: bcs L_E480 ; E459 yes - take it D_E45B: ldy cellColA ; E45B rank cmp maxAdvanceByRankTable,y ; E45D per-rank distance cap: 5,5,5,3,3,1,1,1 bcs L_E480 ; E460 cap reached - take it bcc L_E435 ; E462 keep pushing outward along this rank L_E464: lda scratch23 ; E464 distance already accepted on this rank cmp D_895B ; E466 far enough anyway? bcs L_E480 ; E469 yes - take it ldy cellColA ; E46B rank cmp maxAdvanceByRankTable,y ; E46D per-rank cap bcs L_E480 ; E470 at the cap - take it inc cellColA ; E472 try the next-best direction lda cellColA ; E474 the new rank index cmp #$08 ; E476 the fan holds 8 candidate directions bcc L_E431 ; E478 still ranks left to try lda #$00 ; E47A nothing worked at all: stay put sta scratch23 ; E47C distance 0... L_E47E: sta scanRowCount ; E47E ...and direction 0 ; ---------------------------------------------------------------------- ; Turn the chosen (direction, distance) pair back into an absolute cell and fall into the order ; issuer. ; ---------------------------------------------------------------------- L_E480: ldx scanRowCount ; E480 chosen heading lda dirToColDelta,x ; E482 its column step -1/0/+1 tax ; E485 X = column step L_E486: ldy scratch23 ; E486 chosen distance lda cellColB ; E488 search origin column jsr offsetCoordinateClamped ; E48A target column = origin + step * distance, clamped to 0..39 sta cellColA ; E48D chosen target column ldx scanRowCount ; E48F chosen heading lda dirToRowDelta,x ; E491 its row step -1/0/+1 tax ; E494 X = row step ldy scratch23 ; E495 chosen distance lda cellRowB ; E497 search origin row jsr offsetCoordinateClamped ; E499 target row, clamped sta cellRowA ; E49C chosen target row - fall into the order issuer ; ---------------------------------------------------------------------- ; issueTrainerGroupOrders - Issues the move orders for the group in $8955. It first stamps every ; member's replan clock with the current round so the scan loop cannot pick the group again, clips the ; wanted target with clampGroupTargetToMap (which also yields the movement delta zp_2A/zp_2B relative ; to the group's average ordered destination), derives a replan delay from the distance to the target ; (doubled when the group contains GRUNTs or BOOMERs, floored at 4 rounds and capped by the urgency ; $896D), and then walks all 50 AI slots giving every member a MOVE to its own destination shifted by ; the group delta - so the whole formation translates without changing shape. A member that also ; belongs to a real in-game group propagates its new replan clock to all its formation mates. ; In: $8955 current group, zp_22/zp_24 wanted target, gameClock $91CB, $895A slow-unit flag, $896D ; urgency, $8832 group ids, destinations $F802/$F866, unit flags $F73A ; Out: replan clocks $8800, group target $88D2/$88DE, commands appended to the queue at $88A0, zp_29 ; delay, zp_2A/zp_2B delta ; Called from: aimGroupAtOwnComcen $E353 (JMP) and by falling through from findTrainerAdvanceCell ; $E49C ; Stamp every member of the group with the current round, so that scanTrainerUnitsForOrders will not ; re-plan the same group again later in this pass. ; ---------------------------------------------------------------------- issueTrainerGroupOrders: ldy #$31 ; E49E walk AI ordinals 49..0 L_E4A0: lda D_8832,y ; E4A0 group id of this unit cmp D_8955 ; E4A3 the group being ordered bne L_E4AE ; E4A6 not a member lda gameClock ; E4A8 current round sta msgGetOpponentOnPhoneAnd,y; E4AB stamp the member's replan clock ($8800+ordinal) L_E4AE: dey ; E4AE next ordinal bpl L_E4A0 ; E4AF loop over all 50 slots ; ---------------------------------------------------------------------- ; Work out where the formation may actually stand and how long it should be left alone afterwards. ; ---------------------------------------------------------------------- jsr clampGroupTargetToMap ; E4B1 clip the target so the whole formation box stays on the map; leaves the delta in zp_2A/zp_2B jsr cellDistance ; E4B4 distance from the group centre to the clipped target bit D_895A ; E4B7 does the group contain GRUNTs or BOOMERs? bpl L_E4BD ; E4BA all fast - use the raw distance asl a ; E4BC slow group - leave it alone twice as long L_E4BD: sta scanListIndex ; E4BD replan delay in rounds cmp #$04 ; E4BF minimum 4 rounds bcs L_E4C7 ; E4C1 already at least 4 lda #$04 ; E4C3 floor the delay at 4 sta scanListIndex ; E4C5 store the floored delay L_E4C7: lda D_896D ; E4C7 urgency of this group (2 home guard, 3 comcen escort / committed attack, 5 default) cmp scanListIndex ; E4CA is the delay longer than the urgency allows? bcs L_E4D0 ; E4CC no sta scanListIndex ; E4CE cap the delay at the urgency ; ---------------------------------------------------------------------- ; Second pass over all 50 AI slots: every member gets a MOVE to its own ordered destination plus the ; group delta, and a new replan clock of gameClock - delay. zp_22 is reused here as the loop counter. ; ---------------------------------------------------------------------- L_E4D0: lda #$31 ; E4D0 walk AI ordinals 49..0 again sub_E4D2: sta cellColA ; E4D2 zp_22 is the loop counter for this pass L_E4D4: ldy cellColA ; E4D4 current ordinal lda D_8832,y ; E4D6 its group id cmp D_8955 ; E4D9 the group being ordered bne L_E529 ; E4DC not a member lda msgGetOpponentOnPhoneAnd,y; E4DE its replan clock cmp gameClock ; E4E1 must still carry the stamp written above bcc L_E529 ; E4E4 a formation mate already re-planned it lda side1UnitDestColTable,y ; E4E6 the unit's own ordered destination column and #$3F ; E4E9 drop the flag bits, keep the 0-39 cell clc ; E4EB prepare the add adc scratch2A ; E4EC shift it by the group's column delta tax ; E4EE X = target column lda side1UnitDestRowTable,y ; E4EF its ordered destination row and #$3F ; E4F2 drop the flag bits, keep the 0-39 cell clc ; E4F4 prepare the add adc scratch2B ; E4F5 shift it by the group's row delta jsr orderAiUnitToCell ; E4F7 queue $80 MOVE (or $81 MOVE GROUP) for this unit L_E4FA: ldy cellColA ; E4FA ordinal again lda gameClock ; E4FC current round sec ; E4FF prepare the subtraction sbc scanListIndex ; E500 next replan in 'delay' rounds sub_E502: bcs L_E506 ; E502 no underflow lda #$00 ; E504 clamp at round 0 L_E506: sta msgGetOpponentOnPhoneAnd,y; E506 new replan clock for this unit sta scratch18 ; E509 keep a copy for the formation mates lda side1UnitFlagsTable,y ; E50B the unit's authoritative flags byte and #$7C ; E50E bits 2-6: in-game group id (2-4), side (5) and the 'in a group' bit (6) cmp #$40 ; E510 bit 6 clear means the unit is not in a player-style group bcc L_E529 ; E512 no formation mates to update L_E514: sta scratch19 ; E514 the group key to match ldx #$31 ; E516 scan all 50 AI ordinals L_E518: lda side1UnitFlagsTable,x ; E518 flags of that unit sub_E51B: and #$7C ; E51B same mask cmp scratch19 ; E51D same in-game group? bne L_E526 ; E51F no lda scratch18 ; E521 the replan clock just computed sta msgGetOpponentOnPhoneAnd,x; E523 give the formation mate the same clock, so the group is not ordered twice L_E526: dex ; E526 next ordinal bpl L_E518 ; E527 loop over all 50 slots L_E529: dec cellColA ; E529 next ordinal bmi L_E530 ; E52B all 50 slots done L_E52D: jmp L_E4D4 ; E52D keep going (out of branch range) L_E530: rts ; E530 done ; ---------------------------------------------------------------------- ; computeTrainerGroupStats - Gathers the statistics of AI group Y. It scans the 50 AI slots and, for ; every living member, sums the per-type combat weights into the 16-bit group value, notes whether the ; group contains slow units (GRUNT or BOOMER), counts the members that have not yet scored both end ; zones, and tracks the minimum and maximum of the members' ordered destinations. computeGroupCentroid ; then supplies the member count and the average current and ordered positions; those become the group ; centre, the group size and the four formation-box offsets (reach left, right, up, down). Returns C=1 ; for an empty group. ; In: Y = group id, $8832 group ids, types $F79E, destinations $F802/$F866, penetration flags $8864, ; $895D assault group, $8895 comcen penetration flag ; Out: $8955 current group, $8956/$8957 value, $895A slow flag, $8902 size, $88EA/$88F6 centre, ; $890E/$8926/$891A/$8932 box, zp_1D still-at-home count, zp_93/zp_92 = centre, zp_1E/zp_1F = ; average destination; C=1 if the group is empty ; Called from: retreatTrainerComcen $E2A9 and decideTrainerGroupAction $E326 ; ---------------------------------------------------------------------- computeTrainerGroupStats: sty D_8955 ; E531 remember which group is being measured sub_E534: ldx #$00 ; E534 0 for the accumulators stx scratch1D ; E536 members that have not scored both end zones stx D_8956 ; E538 combat value low byte stx D_8957 ; E53B combat value high byte stx cellRowA ; E53E zp_24 = highest destination column seen (max tracker starts at 0) sub_E540: stx scanColCount ; E540 zp_25 = highest destination row seen stx D_895A ; E542 no slow units found yet dex ; E545 $FF for the minimum trackers stx cellColA ; E546 zp_22 = lowest destination column seen stx scratch23 ; E548 zp_23 = lowest destination row seen ; ---------------------------------------------------------------------- ; Loop over AI ordinals 49..0 accumulating strength, the slow-unit flag, the still-at-home count and ; the bounding box of the members' ordered destinations. ; ---------------------------------------------------------------------- ldy #$31 ; E54A walk AI ordinals 49..0 L_E54C: lda D_8832,y ; E54C group id of this unit cmp D_8955 ; E54F the group being measured bne L_E5A2 ; E552 not a member lda side1UnitTypeTable,y ; E554 type byte bmi L_E5A2 ; E557 dead and #$07 ; E559 bits 0-2 = unit type 0-4 tax ; E55B X = unit type, used as the index into unitCombatValueTable beq L_E562 ; E55C type 0 GRUNT is slow cpx #$02 ; E55E type 2 BOOMER is slow too bne L_E567 ; E560 RIDER, SPY and COMCEN are not L_E562: lda #$FF ; E562 $FF = the group contains slow units sta D_895A ; E564 flag the group as slow L_E567: lda unitCombatValueTable,x ; E567 combat weight of this type (2,2,3,0,0) clc ; E56A prepare the add L_E56B: adc D_8956 ; E56B add it into the 16-bit group value sta D_8956 ; E56E new low byte lda D_8957 ; E571 high byte sub_E574: adc #$00 ; E574 carry into the high byte sta D_8957 ; E576 new high byte lda D_8864,y ; E579 this unit's 'has scored both end zones' flag bmi L_E580 ; E57C already deep in the player's half inc scratch1D ; E57E still at home - count it L_E580: lda side1UnitDestColTable,y ; E580 its ordered destination column and #$3F ; E583 strip the flag bits cmp cellColA ; E585 below the running minimum? bcs L_E58B ; E587 no sta cellColA ; E589 new leftmost destination L_E58B: cmp cellRowA ; E58B above the running maximum? bcc L_E591 ; E58D no sta cellRowA ; E58F new rightmost destination L_E591: lda side1UnitDestRowTable,y ; E591 its ordered destination row and #$3F ; E594 strip the flag bits cmp scratch23 ; E596 below the running minimum? bcs L_E59C ; E598 no sta scratch23 ; E59A new topmost destination L_E59C: cmp scanColCount ; E59C above the running maximum? bcc L_E5A2 ; E59E no sta scanColCount ; E5A0 new bottom-most destination L_E5A2: dey ; E5A2 next ordinal bpl L_E54C ; E5A3 loop over all 50 slots ldy D_8955 ; E5A5 group id cpy D_895D ; E5A8 is this the assault group? bne L_E5B4 ; E5AB no lda D_8895 ; E5AD $8895 = $8864+49 = the AI comcen's own 'has penetrated' flag bmi L_E5B4 ; E5B0 the comcen is already deep in enemy ground inc scratch1D ; E5B2 otherwise the assault group counts as one more unit still at home ; ---------------------------------------------------------------------- ; Average the members' positions and destinations, then store the centre, the size and the four ; formation-box offsets that the risk test and the target clipper work with. ; ---------------------------------------------------------------------- L_E5B4: ldy #$31 ; E5B4 scan ordinals 49..0 sub_E5B6: lda D_8955 ; E5B6 group id to match jsr computeGroupCentroid ; E5B9 average position and average ordered destination of the members D_E5BC: sty scratch18 ; E5BC Y = average row D_E5BE: ldy D_8955 ; E5BE group id L_E5C1: lda scratch1C ; E5C1 member count sub_E5C3: sta D_8902,y ; E5C3 group size bne L_E5CA ; E5C6 the group has members sec ; E5C8 C=1 = empty group rts ; E5C9 nothing more to do L_E5CA: txa ; E5CA X = average column sta D_88EA,y ; E5CB group centre column sta lineCurCol ; E5CE and the line walker's current column, for the callers lda scratch18 ; E5D0 average row sta D_88F6,y ; E5D2 group centre row L_E5D5: sta lineCurRow ; E5D5 and the line walker's current row lda scratch1E ; E5D7 average ordered destination column sec ; E5D9 prepare the subtraction sbc cellColA ; E5DA minus the leftmost destination sta D_890E,y ; E5DC = how far the formation reaches left of its centre lda cellRowA ; E5DF rightmost destination sec ; E5E1 prepare the subtraction sbc scratch1E ; E5E2 minus the average L_E5E4: clc ; E5E4 clear carry for the +1 below adc #$01 ; E5E5 +1 so the box includes the edge cell itself sta D_8926,y ; E5E7 = how far the formation reaches right lda scratch1F ; E5EA average ordered destination row sec ; E5EC prepare the subtraction sbc scratch23 ; E5ED minus the topmost destination sta D_891A,y ; E5EF = reach above the centre lda scanColCount ; E5F2 bottom-most destination sec ; E5F4 prepare the subtraction sbc scratch1F ; E5F5 minus the average clc ; E5F7 clear carry for the +1 below L_E5F8: adc #$01 ; E5F8 +1 for the edge cell sta D_8932,y ; E5FA = reach below the centre clc ; E5FD C=0 = the group exists rts ; E5FE done ; ---------------------------------------------------------------------- ; clampGroupTargetToMap - Clips the wanted group centre in zp_22/zp_24 so that the whole formation box ; (reach left/right in $890E/$8926 and up/down in $891A/$8932) still fits inside the 40x40 map, stores ; the clipped cell as the group's target and returns the movement delta relative to the group's ; average ordered destination in zp_2A (columns, also kept in $8958) and zp_2B (rows). ; In: zp_22/zp_24 wanted centre, zp_1E/zp_1F average ordered destination, $8955 current group, the ; four box offsets ; Out: zp_22/zp_24 clipped, zp_2A/zp_2B delta, $8958, $88D2/$88DE group target ; Called from: issueTrainerGroupOrders $E4B1; trainerSetupEC00 shiftAiGroupDestinations $EF00 ; ---------------------------------------------------------------------- clampGroupTargetToMap: lda cellColA ; E5FF wanted target column sec ; E601 prepare the subtraction ldy D_8955 ; E602 group id sbc D_890E,y ; E605 minus the formation's reach to the left bpl L_E60F ; E608 left edge still >= column 0 lda D_890E,y ; E60A too far left: the leftmost the centre may sit at sta cellColA ; E60D push the centre right so the box fits L_E60F: lda cellColA ; E60F target column clc ; E611 prepare the add adc D_8926,y ; E612 plus the reach to the right cmp #$28 ; E615 $28 = 40 columns bcc L_E620 ; E617 right edge fits lda #$27 ; E619 $27 = column 39; the CMP above left C=1, so this SBC is exact without a SEC sbc D_8926,y ; E61B minus the reach right sta cellColA ; E61E clipped centre column L_E620: lda cellColA ; E620 clipped column sec ; E622 prepare the subtraction ldy D_8955 ; E623 group id (Y already holds it - a redundant reload) sbc scratch1E ; E626 minus the group's average ordered column L_E628: sta D_8958 ; E628 column delta, also kept here for the setup overlay sta scratch2A ; E62B column delta the caller adds to every member lda cellRowA ; E62D wanted target row sec ; E62F prepare the subtraction ldy D_8955 ; E630 group id (redundant reload) sbc D_891A,y ; E633 minus the reach upward bpl L_E63D ; E636 top edge still >= row 0 lda D_891A,y ; E638 too high: the topmost the centre may sit at sta cellRowA ; E63B push the centre down so the box fits L_E63D: lda cellRowA ; E63D target row clc ; E63F prepare the add adc D_8932,y ; E640 plus the reach downward cmp #$28 ; E643 $28 = 40 rows bcc L_E64F ; E645 bottom edge fits lda #$27 ; E647 $27 = row 39 sec ; E649 explicit SEC here, unlike the column path above which relies on the CMP carry sbc D_8932,y ; E64A minus the reach down sta cellRowA ; E64D clipped centre row L_E64F: lda cellRowA ; E64F clipped row sec ; E651 prepare the subtraction ldy D_8955 ; E652 group id (redundant reload) sbc scratch1F ; E655 minus the group's average ordered row sub_E657: sta scratch2B ; E657 row delta the caller adds to every member lda cellColA ; E659 clipped column sta D_88D2,y ; E65B remember it as this group's target cell lda cellRowA ; E65E clipped row sta D_88DE,y ; E660 and its target row L_E663: rts ; E663 done ; maxAdvanceByRankTable - how many cells the path search may advance for direction-preference rank ; 0..7. The three best headings may be followed up to 5 cells, the next two 3 cells and the worst ; three only 1, so a group that has to detour a long way round also moves a shorter distance. maxAdvanceByRankTable: .byte $05 ; E664 5,5,5,3,3,1,1,1 cells per rank .byte $05,$05,$03,$03,$01,$01,$01; E665 ....... ; ---------------------------------------------------------------------- ; offsetPointInDirection - Computes the cell that lies Y steps in compass direction X from the ; reference point (zp_27 column, zp_28 row), using dirToColDelta/dirToRowDelta and ; offsetCoordinateClamped. The result is left in zp_18 (column) and zp_19 (row); carry set means the ; raw coordinate left the 40x40 map, and when the column already left it the row is not computed at ; all. ; In: X = direction 0..7 (0 = north, clockwise), Y = number of steps, zp_27/zp_28 = origin column/row ; Out: zp_18/zp_19 = target cell, C=1 when the point fell off the map; self-modifies the LDX immediate ; at $E67D (L_E67C+1) ; Called from: findTrainerAdvanceCell $E44D, isGroupDestinationTooRisky $E69C, the drone target scan ; $EF9F ; ---------------------------------------------------------------------- offsetPointInDirection: stx L_E67C+1 ; E66C patch the direction into the LDX immediate at $E67D so it can be reused for the row table lda dirToColDelta,x ; E66F column delta -1/0/+1 of that direction tax ; E672 X = column step for offsetCoordinateClamped L_E673: lda cellColB ; E673 origin column jsr offsetCoordinateClamped ; E675 column + step * distance, clamped to 0..39 bcs L_E689 ; E678 left the map - report failure, the row is not computed sta scratch18 ; E67A candidate column L_E67C: ldx #$FF ; E67C operand patched at $E66C with the direction index lda dirToRowDelta,x ; E67E row delta -1/0/+1 of that direction tax ; E681 X = row step lda cellRowB ; E682 origin row jsr offsetCoordinateClamped ; E684 row + step * distance, clamped to 0..39 sta scratch19 ; E687 candidate row L_E689: rts ; E689 C=1 when either coordinate had to be clamped back onto the 40x40 map ; Compass direction tables shared by the whole trainer AI. Directions are numbered 0-7 clockwise from ; north: 0 N, 1 NE, 2 E, 3 SE, 4 S, 5 SW, 6 W, 7 NW. A ninth entry (index 8) holds the delta pair 0,0 ; so that 'no direction at all' can be expressed as an ordinary index - assessThreatAroundAiComcen ; returns 8 when nothing threatens the AI comcen. Read by $E241/$E274 (comcen push/retreat), ; $E482 (path search), offsetPointInDirection ($E66F/$E67E) and getStepDirection ($E816/$E81E). dirToColDelta: .byte $00,$01,$01,$01,$00,$FF,$FF,$FF; E68A ........ column step of directions 0-7: N 0, NE +1, E +1, SE +1, S 0, SW -1, W -1, NW -1 .byte $00 ; E692 index 8: the null direction, column step 0 ; dirToRowDelta: byteTable, 9 bytes. Row delta of the same 8 directions: -1,-1,0,+1,+1,+1,0,-1, plus ; the 9th zero entry. Direction 0 = north, 2 = east, 4 = south, 6 = west. dirToRowDelta: .byte $FF,$FF,$00,$01,$01,$01,$00,$FF; E693 ........ row step of directions 0-7: N -1, NE -1, E 0, SE +1, S +1, SW +1, W 0, NW -1 .byte $00 ; E69B index 8: the null direction, row step 0 ; ---------------------------------------------------------------------- ; isGroupDestinationTooRisky - influence-map test of one candidate destination for the AI group in ; currentAiGroup ($8955). Steps Y cells in direction X away from (cellColB,cellRowB) with ; offsetPointInDirection, grows that cell into the rectangle the whole formation would cover (the four ; span bytes $890E left / $891A up / $8926 right / $8932 down, clamped to 0..39) and plants the four ; edges into the four CMP operands of its own scan loop. It then walks all 100 units and sums a ; signed 16-bit score: every unit whose square influence area (side 2*unitThreatRadius $0400) overlaps ; the rectangle adds unitThreatWeight ($0464) if it is one of the human player's units 0-49 and ; subtracts it if it is one of the AI's own 50-99 (units of the moving group itself were already given ; radius 0 by buildUnitThreatTables). The verdict compares the enemy surplus with the group's own ; combat value $8956/$8957. ; In: X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row; $8955 ; currentAiGroup, $8954 aiGroupCount, ; span tables $890E/$891A/$8926/$8932, threat tables $0400/$0464 (built by ; buildUnitThreatTables), group value $8956/$8957, ; $896E aggression-phase clock, $91CB game clock ; Out: C=1 = reject (off the map, or enemy threat >= own combat value); C=0 = acceptable. zp_18/zp_19 ; = the candidate cell, ; zp_1A/zp_1B = the signed 16-bit threat surplus, zp_1C = the radius of the unit being weighed. ; Self-modifies the CMP operands at $E704, $E70B, $E715 and $E71C. ; Called from: findTrainerAdvanceCell ($E444) only. ; ---------------------------------------------------------------------- isGroupDestinationTooRisky: jsr offsetPointInDirection ; E69C candidate cell = origin (cellColB,cellRowB) moved Y cells in direction X L_E69F: bcc L_E6A2 ; E69F the candidate is on the map, so weigh it rts ; E6A1 the step ran off the edge: return C=1 = reject L_E6A2: ldy D_8955 ; E6A2 Y = the group being moved cpy D_8954 ; E6A5 is it one of the aiGroupCount live groups? bcc L_E6AC ; E6A8 yes: measure the threat over its formation footprint clc ; E6AA no such group, so there is nothing to be afraid of rts ; E6AB return C=0 = destination acceptable ; ---------------------------------------------------------------------- ; Grow the candidate cell into the rectangle the whole formation would stand on, clamp it to the 40x40 ; map, and write its four edges straight into the four CMP instructions of the unit scan below. ; ---------------------------------------------------------------------- L_E6AC: lda scratch18 ; E6AC candidate column sec ; E6AE prepare the subtraction sbc D_890E,y ; E6AF minus the group's span to the left = left edge of the formation box bpl L_E6B6 ; E6B2 still on the map lda #$00 ; E6B4 hangs off the left edge: clamp the box to column 0 L_E6B6: sta L_E714+1 ; E6B6 self-modify: the CMP at $E714 now tests against the box's left column lda scratch18 ; E6B9 candidate column again clc ; E6BB prepare the addition adc D_8926,y ; E6BC plus the group's span to the right = right edge of the box cmp #$28 ; E6BF past the last column (40)? bcc L_E6C5 ; E6C1 no, keep it L_E6C3: lda #$27 ; E6C3 yes: clamp the box to column 39 L_E6C5: sta L_E71B+1 ; E6C5 self-modify: the CMP at $E71B now tests against the box's right column L_E6C8: lda scratch19 ; E6C8 candidate row sec ; E6CA prepare the subtraction sbc D_891A,y ; E6CB minus the group's span upwards = top edge of the box bpl L_E6D2 ; E6CE still on the map lda #$00 ; E6D0 hangs off the top: clamp the box to row 0 L_E6D2: sta L_E703+1 ; E6D2 self-modify: the CMP at $E703 now tests against the box's top row lda scratch19 ; E6D5 candidate row again clc ; E6D7 prepare the addition adc D_8932,y ; E6D8 plus the group's span downwards = bottom edge of the box cmp #$28 ; E6DB past the last row (40)? bcc L_E6E1 ; E6DD no, keep it lda #$27 ; E6DF yes: clamp the box to row 39 L_E6E1: sta L_E70A+1 ; E6E1 self-modify: the CMP at $E70A now tests against the box's bottom row ; ---------------------------------------------------------------------- ; Walk all 100 units, unit 99 down to unit 0, and accumulate a signed 16-bit 'how dangerous is this ; square' score in zp_1A/zp_1B: enemy influence adds, friendly influence subtracts. A unit only ; counts when its influence square (centre = its cell, half-width = unitThreatRadius) overlaps the ; formation box just computed. ; ---------------------------------------------------------------------- lda #$00 ; E6E4 clear the running score sta scratch1A ; E6E6 threat surplus, low byte sta scratch1B ; E6E8 threat surplus, high byte ldy #$63 ; E6EA start at unit 99 L_E6EC: lda checksumXorConstant,y ; E6EC unitThreatRadius[unit] from the $0400 influence page (also trainerPlaybook0200:play3Col12) beq L_E740 ; E6EF radius 0 (destroyed, spy/comcen, or a member of the moving group): the unit does not count L_E6F1: sta scratch1C ; E6F1 keep this unit's influence radius cpy #$32 ; E6F3 unit 50-99 = one of the AI's own? bcc L_E6FC ; E6F5 no, it is one of the human player's units 0-49 lda D_8955 ; E6F7 own unit: is the group being moved group 0, the AI comcen's own group? beq L_E740 ; E6FA yes: friendly cover is not credited to the comcen group, so skip own units entirely L_E6FC: lda unitRowTable,y ; E6FC row of this unit L_E6FF: tax ; E6FF keep the row for the second test clc ; E700 prepare the addition adc scratch1C ; E701 row + radius = bottom edge of its influence square L_E703: cmp #$FF ; E703 operand patched at $E6D2 with the box's top row bcc L_E740 ; E705 its influence stops above the box: no overlap, next unit txa ; E707 the row again (the CMP above left C=1 for the subtraction) sbc scratch1C ; E708 row - radius = top edge of its influence square L_E70A: cmp #$FF ; E70A operand patched at $E6E1 with the box's bottom row bcs L_E740 ; E70C its influence starts below the box: no overlap lda unitColTable,y ; E70E column of this unit tax ; E711 keep the column for the second test L_E712: adc scratch1C ; E712 column + radius = right edge of its influence square (the CMP above left C=0) L_E714: cmp #$FF ; E714 operand patched at $E6B6 with the box's left column bcc L_E740 ; E716 its influence stops left of the box: no overlap txa ; E718 the column again (the CMP above left C=1 for the subtraction) sbc scratch1C ; E719 column - radius = left edge of its influence square L_E71B: cmp #$FF ; E71B operand patched at $E6C5 with the box's right column bcs L_E740 ; E71D its influence starts right of the box: no overlap cpy #$32 ; E71F the squares overlap, so whose unit is it? bcc addEnemyThreatWeight ; E721 human player's unit 0-49: its weight adds to the danger lda scratch1A ; E723 one of the AI's own: subtract its weight instead (friendly cover) sbc play4Grp12,y ; E725 unitThreatWeight[unit] from the $0464 influence page (the CPY above left C=1) sta scratch1A ; E728 store the surplus low byte lda scratch1B ; E72A surplus high byte sbc #$00 ; E72C propagate the borrow sta scratch1B ; E72E store it back jmp L_E740 ; E730 on to the next unit addEnemyThreatWeight: lda scratch1A ; E733 surplus low byte adc play4Grp12,y ; E735 add unitThreatWeight[unit] (the CPY at $E71F left C=0) sta scratch1A ; E738 store it back lda scratch1B ; E73A surplus high byte sub_E73C: adc #$00 ; E73C propagate the carry sta scratch1B ; E73E store it back L_E740: dey ; E740 next unit bpl L_E6EC ; E741 until unit 0 has been weighed ; ---------------------------------------------------------------------- ; Verdict. A negative or zero surplus means the AI already dominates the square. Otherwise the enemy ; surplus is compared with this group's own combat value, and an exact draw is settled by the game ; clock and the RNG so that both machines would reach the same answer. ; ---------------------------------------------------------------------- judgeGroupThreatTotal: lda scratch1B ; E743 sign of the 16-bit surplus bpl L_E749 ; E745 not negative: weigh it against the group's own strength L_E747: clc ; E747 friendly influence outweighs the enemy there rts ; E748 C=0 = destination acceptable L_E749: lda scratch1A ; E749 surplus low byte ora scratch1B ; E74B is the whole 16-bit surplus exactly zero? beq L_E747 ; E74D no enemy influence at all: accept lda scratch1A ; E74F surplus low byte sec ; E751 prepare the subtraction L_E752: sbc D_8956 ; E752 minus the group's own combat value, low byte L_E755: sta scratch1A ; E755 keep the difference lda scratch1B ; E757 surplus high byte sbc D_8957 ; E759 minus the group's combat value, high byte sta scratch1B ; E75C keep it ora scratch1A ; E75E is the difference exactly zero? bne L_E772 ; E760 no: return the carry of the subtraction (C=1 = threat >= own strength = reject) lda D_896E ; E762 dead heat: consult the aggression-phase clock set at setup ($EF00 area) or by retreatTrainerComcen L_E765: cmp gameClock ; E765 has the game reached that round? the game clock counts down, so 'phase clock >= clock' means yes bcs L_E76D ; E768 yes, late enough in the game: toss a coin sec ; E76A still early: play safe L_E76B: bcs L_E772 ; E76B always taken (the BCS above fell through with C=1): return C=1 = reject L_E76D: jsr nextGameRandom ; E76D step the lock-step game RNG so both machines stay in sync cmp #$80 ; E770 C=1 for half of the random values, i.e. reject a tie 50 percent of the time L_E772: rts ; E772 carry tells the caller whether the destination is too dangerous ; ---------------------------------------------------------------------- ; buildUnitThreatTables - rebuilds the two 100-entry influence-map tables that ; isGroupDestinationTooRisky reads. For each unit 99..0 it writes an influence radius into $0400 and a ; combat weight into $0464. Destroyed units, spies and comcens (type 3 and 4) and every AI unit ; belonging to the group that is about to be moved get radius 0 and weight 0. A grunt or rider gets ; base radius 3, a boomer 6, both plus the terrain sight bonus of the tile the unit is standing on, so ; units on high ground project further. The weight comes from unitCombatValueTable ($E2B7) = ; 2,2,3,0,0. ; In: $8955 currentAiGroup, aiUnitGroup ($8800+absolute unit index), unitTypeTable $F76C, ; unitTerrainUnderTable $FAF0, ; terrainClassTable $077E, terrainSightBonusTable $35BB, unitCombatValueTable $E2B7 ; Out: $0400[0..99] = influence radius, $0464[0..99] = combat weight; zp_18 and zp_19 clobbered ; Called from: findTrainerAdvanceCell ($E42A), immediately before its search loop. ; One entry per unit, walking backwards from unit 99 to unit 0. ; ---------------------------------------------------------------------- buildUnitThreatTables: ldy #$63 ; E773 start at unit 99 L_E775: cpy #$32 ; E775 is this one of the AI's own units 50-99? bcc L_E781 ; E777 no, it is a human player unit: no group test needed lda msgGetOpponentOnPhoneAnd,y; E779 aiUnitGroup[unit] - the group table addressed by absolute index ($8800+unit = $8832+ordinal) cmp D_8955 ; E77C does it belong to the very group we are about to move? beq L_E786 ; E77F yes: a group must not weigh its own members L_E781: lda unitTypeTable,y ; E781 unit type byte, bit 7 = destroyed bpl L_E78A ; E784 alive: work out its influence radius L_E786: lda #$00 ; E786 destroyed, or a member of the moving group: no influence beq L_E7A7 ; E788 always taken (A = 0): store radius 0 L_E78A: ldx #$03 ; E78A default influence radius 3 cells L_E78C: and #$07 ; E78C unit type 0-4 without the status bits sta scratch19 ; E78E keep the type for the weight look-up cmp #$02 ; E790 GRUNT (0) or RIDER (1)? bcc L_E798 ; E792 yes: keep radius 3 bne L_E786 ; E794 type 3 SPY or 4 COMCEN: they project no threat at all ldx #$06 ; E796 BOOMER (2): its guns reach twice as far, radius 6 L_E798: stx scratch18 ; E798 stash the base radius ldx unitTerrainUnderTable,y ; E79A terrain code of the tile hidden under the unit lda D_077E,x ; E79D terrain class 0-8 of that tile tax ; E7A0 class becomes the index lda terrainSightBonusTable,x; E7A1 extra range the terrain grants: 0, 1 or 2 cells clc ; E7A4 prepare the addition adc scratch18 ; E7A5 radius = base radius + terrain bonus L_E7A7: sta checksumXorConstant,y ; E7A7 unitThreatRadius[unit] into the $0400 scratch page (also trainerPlaybook0200:play3Col12) tax ; E7AA is the radius zero? beq L_E7B2 ; E7AB yes: A is already 0, so the weight becomes 0 too ldx scratch19 ; E7AD unit type lda unitCombatValueTable,x ; E7AF combat weight: GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0 L_E7B2: sta play4Grp12,y ; E7B2 unitThreatWeight[unit] into the $0464 scratch page L_E7B5: dey ; E7B5 next unit bpl L_E775 ; E7B6 until unit 0 has an entry rts ; E7B8 both influence tables are current ; ---------------------------------------------------------------------- ; multiplyAbyY - 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y = 0. The ; multiplicand is copied into its own ADC operand and Y is restored from a second patched operand ; before returning, so the caller's step count survives the call. ; In: A = multiplicand, Y = count ; Out: A = A*Y; Y unchanged; X untouched, flags clobbered; self-modifies the operands at $E7CB and ; $E7D0 ; Called from: offsetCoordinateClamped ($EA22) only, where A is a step of 0, +1 or $FF and Y a cell ; distance. ; ---------------------------------------------------------------------- multiplyAbyY: sty L_E7CF+1 ; E7B9 self-modify: stash the count in the LDY at $E7CF so Y can be handed back sta L_E7CA+1 ; E7BC self-modify: the multiplicand becomes the operand of the ADC at $E7CA cpy #$00 ; E7BF multiply by zero? bne L_E7C6 ; E7C1 no: run the addition loop tya ; E7C3 Y is 0, so A = 0 beq L_E7CE ; E7C4 always taken: return zero L_E7C6: dey ; E7C6 one addition per unit of the count beq L_E7CE ; E7C7 count used up: A holds the product L_E7C9: clc ; E7C9 clear the carry before each addition L_E7CA: adc #$FF ; E7CA operand patched at $E7BC: add the multiplicand once more bne L_E7C6 ; E7CC keep adding; a running total of exactly 0 would end the loop early, which the callers' 0/+1/-1 steps never produce L_E7CE: pha ; E7CE hold the product across the Y restore L_E7CF: ldy #$FF ; E7CF operand patched at $E7B9: give the caller its count back pla ; E7D1 product back into A rts ; E7D2 A = A*Y ; ---------------------------------------------------------------------- ; buildDirectionFanTowardTarget - finds the compass heading from the line start ; (lineCurCol,lineCurRow) to the line end (lineEndCol,lineEndRow) with getStepDirection, saves it in ; zp_19 and falls into buildDirectionFan so the eight candidate move directions are laid out around ; that heading. Nothing is built when start and target are the same cell. ; In: zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column, $8955 currentAiGroup ; Out: zp_19 = heading 0-7 ($FF and no fan when start = target); $8896-$889D = the eight candidate ; directions; ; zp_18 and the line-stepper state $8C-$97 clobbered ; Called from: the group-movement code at $E416. ; ---------------------------------------------------------------------- buildDirectionFanTowardTarget: jsr getStepDirection ; E7D3 heading of the first Bresenham step from the group centre toward its target sty scratch19 ; E7D6 remember the heading for buildDirectionFan bpl buildDirectionFan ; E7D8 a real heading 0-7: lay the eight candidates out around it rts ; E7DA Y = $FF, the group is already on its target cell: no fan to build ; dirMirrorTable - eight bytes, entry d = (8 - d) AND 7, i.e. the mirror of a heading offset about the ; north-south axis (0 stays 0, +1 becomes -1 and so on). buildDirectionFan runs the offsets of a ; 'left-handed' group through it so that half the groups sweep clockwise and half anticlockwise. The ; label in the middle of the table is a branch target of the modem-driver build at the same address, ; not a separate block. dirMirrorTable: .byte $00,$07,$06,$05,$04 ; E7DB ..... mirror of heading offsets 0-4: 0, -1, -2, -3, -4 expressed modulo 8 L_E7E0: .byte $03 ; E7E0 mirror of heading offsets 5-7: +3, +2, +1 .byte $02,$01 ; E7E1 .. ; ---------------------------------------------------------------------- ; buildDirectionFan - fills the eight-byte scratch buffer $8896-$889D with the move directions the ; path search should try, in order of preference: entry i = (dirFanOffsetTable[i] + heading + 8) AND ; 7. dirFanOffsetTable ($894A) is built once per game by the setup overlay's ; buildAiHeadingSearchOrder ($EEC3) as 0, +/-1, +/-1, +/-2, +/-2, +/-3, +/-3, +/-4 with a random ; left/right bias, so the search always tries straight ahead first and fans outwards. When bit 0 of ; the group's groupFlags is set every offset is mirrored through dirMirrorTable, giving that group the ; opposite handedness. ; In: zp_19 = wanted heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable ; Out: $8896-$889D = eight candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered ; Called from: decideTrainerGroupAction ($E36E) and by falling through from ; buildDirectionFanTowardTarget ($E7D8). ; The list is consumed by the move search at $E431-$E47E, whose per-rank distance caps are in ; maxAdvanceByRankTable ($E664). ; ---------------------------------------------------------------------- buildDirectionFan: ldx #$00 ; E7E3 assume the fan is not mirrored ldy D_8955 ; E7E5 the group being moved lda D_893E,y ; E7E8 its groupFlags; the low nibble is the random start delay handed out at setup and #$01 ; E7EB bit 0 of that delay serves as the group's fixed handedness beq L_E7F0 ; E7ED clear: fan out the normal way dex ; E7EF set: X = $FF, mirror every offset L_E7F0: stx scratch18 ; E7F0 mirror flag for the loop below ldy #$07 ; E7F2 fill the eight preference slots, 7 down to 0 L_E7F4: lda D_894A,y ; E7F4 heading offset of preference rank Y L_E7F7: bit scratch18 ; E7F7 is this group mirrored? (BIT leaves A alone) bpl L_E7FF ; E7F9 no: use the offset as it is tax ; E7FB offset becomes the index lda dirMirrorTable,x ; E7FC yes: swap left for right L_E7FF: clc ; E7FF clear the carry before adding adc scratch19 ; E800 candidate = wanted heading + offset clc ; E802 the addition above may have carried adc #$08 ; E803 bias by a whole turn so a negative offset stays positive L_E805: and #$07 ; E805 wrap back into a compass direction 0-7 sta D_8896,y ; E807 store the candidate for preference rank Y L_E80A: dey ; E80A next rank bpl L_E7F4 ; E80B until all eight ranks are filled rts ; E80D the fan is ready for the path search ; ---------------------------------------------------------------------- ; getStepDirection - sets a Bresenham walker up between (lineCurCol,lineCurRow) and ; (lineEndCol,lineEndRow), takes exactly one step, and converts the deltas that step applied ($90F5 ; column, $90F4 row) back into one of the eight compass directions by searching ; dirToColDelta/dirToRowDelta from index 7 downwards. stepLine zeroes both deltas before it moves, so ; a zero-length line leaves 0,0 - which matches no entry 0-7 and comes out as Y = $FF. ; In: zp_92/zp_93 = start row/column, zp_94/zp_95 = end row/column ; Out: Y = direction 0-7, or $FF when start and end are the same cell; A = the last delta compared; ; line-stepper state $8C-$97 and $90F4/$90F5 updated ; Called from: buildDirectionFanTowardTarget ($E7D3), assessThreatAroundAiComcen ($EC35), the drone ; code ($EEC7) ; and the $EE00 drone-flight overlay ($EEB1). ; ---------------------------------------------------------------------- getStepDirection: jsr initLineStepper ; E80E set the Bresenham walker up between the two cells jsr stepLine ; E811 take one step; it records the deltas it applied in $90F5 (column) and $90F4 (row), zeroing them first ldy #$07 ; E814 search the direction tables backwards from 7 L_E816: lda dirToColDelta,y ; E816 column step of direction Y cmp lastStepColDelta ; E819 does it match the step just taken? bne L_E826 ; E81C no: try the next direction lda dirToRowDelta,y ; E81E row step of direction Y cmp lastStepRowDelta ; E821 does that match too? beq L_E829 ; E824 both match: Y is the heading L_E826: dey ; E826 next direction down bpl L_E816 ; E827 index 8 (the null delta) is deliberately never tested, so Y = $FF means 'the line has no step' L_E829: rts ; E829 Y = compass direction, or $FF ; ---------------------------------------------------------------------- ; cellDistance - integer distance between two map cells. distanceSquared ($3870) gives |dcol|^2 + ; |drow|^2 (each delta capped at 15, the sum saturating at $FF with C=1), and this routine converts it ; back through squaresTable ($3860) into the largest n with n*n <= dsq. A saturated squared distance ; simply reports 16. ; In: zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row ; Out: A = X = distance 0-16; Y untouched ; Called from: $E410, $E4B4, assessThreatAroundAiComcen ($EC24), the missile code ($ED43/$EE0C/$EE5C) ; and the drone overlay. ; ---------------------------------------------------------------------- cellDistance: jsr distanceSquared ; E82A A = squared distance between (cellColA,cellRowA) and (cellColB,cellRowB), C=1 if it saturated at 255 ldx #$10 ; E82D 16 is the largest distance this routine reports bcs L_E83A ; E82F saturated: everything beyond 15 cells is simply 'far' dex ; E831 otherwise start the search at 15 L_E832: cmp squaresTable,x ; E832 is the squared distance at least X*X? bcs L_E83A ; E835 yes: X is the integer distance dex ; E837 try the next smaller radius bne L_E832 ; E838 X = 0 falls out of the loop as distance 0 L_E83A: txa ; E83A hand the distance back in A as well as X rts ; E83B A = X = distance in cells ; ---------------------------------------------------------------------- ; updateFocusGroupCentre - keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row) ; current. The focus group is the assault group the AI comcen walks along behind. When the id has ; bit 7 set the centre falls back on the AI comcen's own cell, and a suspended group is taken back ; into service as soon as bit 7 of its groupFlags has cleared. An active group whose groupFlags bit 7 ; is set (it has reached its objective) is suspended by ORing $80 into the id. Otherwise the centroid ; of the group's combat members is recomputed, and a group that has lost every member is dropped ; ($895D = $FF). ; In: $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707, $8832 aiUnitGroup ; Out: $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered ; Called from: planTrainerRound ($E20C), once per round just before the comcen push/retreat tail. ; ---------------------------------------------------------------------- updateFocusGroupCentre: ldy D_895D ; E83C focusGroupId: $FF = no group, bit 7 set = the group is suspended bpl L_E860 ; E83F an active group id 0-15: go and refresh its centre lda side1ComcenCol ; E841 no usable group: fall back on the AI comcen's own cell (unit 99) L_E844: sta msgOpponentUsingNonStandardGame; E844 focus centre column ($895E) lda side1ComcenRow ; E847 AI comcen row sta D_895F ; E84A focus centre row cpy #$FF ; E84D was the focus group dropped altogether? beq L_E85F ; E84F yes: nothing left to revive tya ; E851 suspended group: recover its id and #$0F ; E852 strip the suspend bit tay ; E854 index the group tables with it lda D_893E,y ; E855 its groupFlags L_E858: bmi L_E85F ; E858 bit 7 still set (the group is at its objective): leave it suspended sty D_895D ; E85A the flag cleared: put the group back in service bpl L_E860 ; E85D always taken (an id 0-15 is positive): recompute its centre L_E85F: rts ; E85F nothing to update this round L_E860: lda D_893E,y ; E860 groupFlags of the active focus group bpl L_E86C ; E863 still on its way: recompute the centre tya ; E865 bit 7 set: the group has arrived ora #$80 ; E866 mark the id as suspended sta D_895D ; E868 store it back; the centre keeps the comcen fallback from the next call rts ; E86B done for this round L_E86C: lda D_895D ; E86C group id for the centroid scan ldy #$2D ; E86F scan AI ordinals 45 down to 0 - the combat units only, since 46-48 are the spies and 49 the comcen jsr computeGroupCentroid ; E871 A = member count, X = average column, Y = average row bne L_E87B ; E874 the group still has members lda #$FF ; E876 no member left sta D_895D ; E878 drop the focus group; X/Y are 0/$FF here, so the centre is stale for the one round until the fallback runs L_E87B: stx msgOpponentUsingNonStandardGame; E87B focus centre column sty D_895F ; E87E focus centre row rts ; E881 the comcen now has something to escort ; ---------------------------------------------------------------------- ; computeGroupCentroid - averages the position and the ordered destination of every living member of ; one AI group. It scans AI ordinals Y down to 0 (absolute units 50+Y down to 50), accumulates four ; 16-bit sums and divides each by the member count with divideSigned16by8. The wanted group id is ; patched into its own CMP, and the two averaged coordinates are handed back through two patched ; immediate operands so that A can carry the member count. ; In: A = group id, Y = highest AI ordinal to scan (usually $31 = 49 or $2D = 45); $8832 aiUnitGroup, ; $F79E unitType, ; $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow (all indexed by AI ordinal) ; Out: A = member count (Z set when the group is empty), X = average column, Y = average row, zp_1E = ; average destination column, ; zp_1F = average destination row, zp_1C = member count; zp_18-zp_21 clobbered; ; self-modifies the operands at $E89D, $E913 and $E915 ; Called from: computeTrainerGroupStats ($E5B9) and updateFocusGroupCentre ($E871). ; ---------------------------------------------------------------------- computeGroupCentroid: sta L_E89C+1 ; E882 self-modify: the wanted group id becomes the operand of the CMP at $E89C lda #$00 ; E885 clear all nine accumulator bytes sta scratch18 ; E887 column sum, low sta scratch19 ; E889 column sum, high sta scratch1A ; E88B row sum, low sta scratch1B ; E88D row sum, high sta scratch1C ; E88F member count sta scratch1E ; E891 ordered-destination column sum, low L_E893: sta scratch1F ; E893 ordered-destination column sum, high sta scratch20 ; E895 ordered-destination row sum, low sta scratch21 ; E897 ordered-destination row sum, high ; ---------------------------------------------------------------------- ; One pass per AI ordinal, from the caller's starting ordinal down to 0. ; ---------------------------------------------------------------------- L_E899: lda D_8832,y ; E899 aiUnitGroup[ordinal] - which group this AI unit belongs to L_E89C: cmp #$FF ; E89C operand patched at $E882: is it the group we are averaging? L_E89E: bne L_E8DF ; E89E no: next ordinal lda side1UnitTypeTable,y ; E8A0 type byte of AI unit 50+ordinal bmi L_E8DF ; E8A3 bit 7 set: destroyed, so it does not count inc scratch1C ; E8A5 one more live member lda side1UnitColTable,y ; E8A7 its current column clc ; E8AA clear the carry adc scratch18 ; E8AB into the 16-bit column sum sta scratch18 ; E8AD store the low byte lda scratch19 ; E8AF column sum, high byte adc #$00 ; E8B1 propagate the carry sta scratch19 ; E8B3 store it back lda side1UnitRowTable,y ; E8B5 its current row clc ; E8B8 clear the carry adc scratch1A ; E8B9 into the 16-bit row sum sta scratch1A ; E8BB store the low byte lda scratch1B ; E8BD row sum, high byte adc #$00 ; E8BF propagate the carry sta scratch1B ; E8C1 store it back lda side1UnitDestColTable,y ; E8C3 the column it has been ordered to and #$3F ; E8C6 the coordinate is in bits 0-5; bits 6-7 are order flags clc ; E8C8 clear the carry adc scratch1E ; E8C9 into the destination-column sum sta scratch1E ; E8CB store the low byte bcc L_E8D1 ; E8CD no carry inc scratch1F ; E8CF carry into the high byte L_E8D1: lda side1UnitDestRowTable,y ; E8D1 the row it has been ordered to and #$3F ; E8D4 coordinate bits only clc ; E8D6 clear the carry adc scratch20 ; E8D7 into the destination-row sum L_E8D9: sta scratch20 ; E8D9 store the low byte bcc L_E8DF ; E8DB no carry inc scratch21 ; E8DD carry into the high byte L_E8DF: dey ; E8DF next ordinal bpl L_E899 ; E8E0 down to AI ordinal 0 ; ---------------------------------------------------------------------- ; Divide the four sums by the member count. The two positional averages are returned in X and Y ; through the two immediate operands patched below. ; ---------------------------------------------------------------------- ldx scratch1C ; E8E2 member count is the divisor beq L_E916 ; E8E4 no members: skip the divisions and report zero lda scratch18 ; E8E6 column sum, low ldy scratch19 ; E8E8 column sum, high jsr divideSigned16by8 ; E8EA average column sta L_E912+1 ; E8ED self-modify: it becomes the operand of the LDX at $E912 lda scratch1A ; E8F0 row sum, low L_E8F2: ldy scratch1B ; E8F2 row sum, high ldx scratch1C ; E8F4 divisor again (divideSigned16by8 clobbers X) jsr divideSigned16by8 ; E8F6 average row sta L_E914+1 ; E8F9 self-modify: it becomes the operand of the LDY at $E914 ldx scratch1C ; E8FC divisor again lda scratch1E ; E8FE destination-column sum, low ldy scratch1F ; E900 destination-column sum, high jsr divideSigned16by8 ; E902 average ordered destination column sta scratch1E ; E905 returned in zp_1E ldx scratch1C ; E907 divisor again sub_E909: lda scratch20 ; E909 destination-row sum, low ldy scratch21 ; E90B destination-row sum, high jsr divideSigned16by8 ; E90D average ordered destination row sta scratch1F ; E910 returned in zp_1F, overwriting the sum byte that has just been consumed L_E912: ldx #$FF ; E912 operand patched at $E8ED: X = average column L_E914: ldy #$FF ; E914 operand patched at $E8F9: Y = average row L_E916: lda scratch1C ; E916 A = member count, Z set when the group is empty rts ; E918 caller gets count in A, centre in X/Y, average destination in zp_1E/zp_1F ; ---------------------------------------------------------------------- ; orderAiUnitToCell - the trainer's central 'issue a move order' helper. It does nothing when the ; unit's ordered destination already is the requested cell, so redundant orders never take up room in ; the queue or in the packet. Otherwise it falls into queueMirroredMoveOrder, which mirrors both ; coordinates (the engine will mirror them back because it believes the command arrived from a remote ; opponent) and queues $80 MOVE UNIT or $81 MOVE GROUP. ; In: X = target column, A = target row, Y = AI ordinal 0-49 (absolute unit = Y+50) ; Out: nothing when the order is redundant; otherwise $8896 = $80/$81, $8897 = absolute unit index, ; $8898/$8899 = mirrored column/row, and the command appended to the AI queue at $88A0 ; Called from: retreatTrainerComcen ($E262), pushTrainerComcenForward ($E294), issueTrainerGroupOrders ; ($E4F7) ; and haltAiComcen ($EBEC). ; ---------------------------------------------------------------------- orderAiUnitToCell: stx D_8898 ; E919 stage the target column as the command's first coordinate sta D_8899 ; E91C and the target row as the second lda side1UnitDestColTable,y ; E91F the column AI unit 50+Y has already been ordered to and #$3F ; E922 coordinate bits only; bits 6-7 are order flags L_E924: cmp D_8898 ; E924 same column as the new order? bne L_E934 ; E927 no: the order is worth sending lda side1UnitDestRowTable,y ; E929 the row it has already been ordered to and #$3F ; E92C coordinate bits only cmp D_8899 ; E92E same row too? bne L_E934 ; E931 no: send the order rts ; E933 the unit is already under exactly this order: queue nothing L_E934: lda D_8898 ; E934 target column, ready for the mirror below ; ---------------------------------------------------------------------- ; queueMirroredMoveOrder - second half of orderAiUnitToCell. Mirrors the staged coordinates with ; mirrorMapCoordinate (39 - v), picks command $80 cmdMoveUnit or $81 cmdMoveGroup according to bit 6 ; of the unit's flags, converts the AI ordinal into the absolute unit index and tail-jumps into ; queueAiCommand. Both players see their own side at the bottom of the map, so an incoming command's ; coordinates are always mirrored by the engine; the trainer pre-mirrors so the round trip comes out ; right. ; In: $8898/$8899 = target column/row, Y = AI ordinal 0-49, $F73A unitFlags indexed by ordinal ; Out: the four bytes $8896-$8899 staged and queued ; Called from: reached only by falling through from orderAiUnitToCell ($E934); the XREF entries ; $E917/$EA01 belong to the ; modem-driver build that occupies the same addresses. ; ---------------------------------------------------------------------- queueMirroredMoveOrder: jsr mirrorMapCoordinate ; E937 39 - column: the engine mirrors it back when it 'receives' the packet sta D_8898 ; E93A store the mirrored column lda D_8899 ; E93D target row jsr mirrorMapCoordinate ; E940 39 - row sta D_8899 ; E943 store the mirrored row ldx #$80 ; E946 command $80 = cmdMoveUnit lda side1UnitFlagsTable,y ; E948 unitFlags of AI unit 50+Y and #$40 ; E94B bit 6 = the unit is a member of an in-game group beq L_E950 ; E94D not grouped: keep $80 inx ; E94F grouped: use $81 = cmdMoveGroup so its formation follows L_E950: stx D_8896 ; E950 command byte tya ; E953 AI ordinal 0-49 clc ; E954 clear the carry adc #$32 ; E955 plus 50: the AI plays side 1, so its units are 50-99 sta msgPickUpPhoneThenPressSpace; E957 first parameter of the command ($8897) L_E95A: jmp queueAiCommand ; E95A append the staged command to the AI order queue ; ---------------------------------------------------------------------- ; isPathToPlayerComcenClear - walks a Bresenham line from the current line position (which the caller ; has set to the group centre) to the human player's comcen (unit 49) and reports whether any living ; enemy fighting unit stands on it. Only human grunts, riders and boomers (types 0-2) block; the AI's ; own units, destroyed units, spies and the comcen do not. Both exits put the group centre back into ; the line position through loadGroupCentreIntoLinePos. ; In: zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player ; comcen cell, map at $F000 ; Out: C=1 = the lane to the comcen is open, C=0 = blocked or the group id was invalid; zp_92/zp_93 = ; the group centre; ; zp_48/zp_49 (map pointer) and the line-stepper state $8C-$97 clobbered ; Called from: decideTrainerGroupAction ($E3B1), to decide whether the group may make a straight run ; at the comcen. ; ---------------------------------------------------------------------- isPathToPlayerComcenClear: ldy D_8955 ; E95D the group asking for a straight run L_E960: cpy D_8954 ; E960 is it one of the live groups? bcs L_E995 ; E963 no: report 'blocked' and do nothing else lda side0ComcenCol ; E965 column of the human player's comcen (unit 49) sta lineEndCol ; E968 line target column lda side0ComcenRow ; E96A its row sub_E96D: sta lineEndRow ; E96D line target row jsr initLineStepper ; E96F set the walker up from the current position to the comcen ; ---------------------------------------------------------------------- ; Walk the lane one cell at a time, stopping at the first live enemy fighting unit. ; ---------------------------------------------------------------------- L_E972: jsr stepLine ; E972 advance one cell along the lane sub_E975: beq loadGroupCentreIntoLinePos; E975 the line ran out without a blocker: restore the centre and return C=1 ldx lineCurCol ; E977 column the walker is standing on ldy lineCurRow ; E979 and its row L_E97B: jsr readMapCell ; E97B map byte of that cell; bit 7 set means a unit occupies it bpl L_E972 ; E97E bare terrain: keep walking and #$7F ; E980 strip the occupancy bit to get the unit index L_E982: cmp #$32 ; E982 one of the AI's own units 50-99? bcs L_E972 ; E984 yes: a unit never blocks its own side's lane L_E986: tay ; E986 human player's unit: index it lda unitTypeTable,y ; E987 its type byte bmi L_E972 ; E98A destroyed: not an obstacle and #$07 ; E98C unit type 0-4 cmp #$03 ; E98E type 3 SPY or 4 COMCEN? bcs L_E972 ; E990 yes: neither blocks a charge jsr loadGroupCentreIntoLinePos; E992 a live grunt, rider or boomer sits in the lane: put the group centre back L_E995: clc ; E995 C=0 = the lane is blocked (also the exit for an invalid group id) rts ; E996 carry tells the caller whether it may charge ; ---------------------------------------------------------------------- ; loadGroupCentreIntoLinePos - copies the current group's centre (groupCentreCol $88EA / ; groupCentreRow $88F6) into the line walker's position and returns with C=1. It doubles as the 'path ; is clear' exit of isPathToPlayerComcenClear, which is why it ends with SEC. ; In: $8955 currentAiGroup, $88EA/$88F6 per-group centre tables ; Out: zp_93 = centre column, zp_92 = centre row, C=1; A and Y clobbered ; Called from: isPathToPlayerComcenClear ($E992) and as its C=1 exit ($E975). ; ---------------------------------------------------------------------- loadGroupCentreIntoLinePos: ldy D_8955 ; E997 the current group lda D_88EA,y ; E99A its centre column sta lineCurCol ; E99D line walker column lda D_88F6,y ; E99F its centre row L_E9A2: sta lineCurRow ; E9A2 line walker row sec ; E9A4 C=1: this is also the 'lane is clear' answer of isPathToPlayerComcenClear rts ; E9A5 line position restored ; ---------------------------------------------------------------------- ; scoreTargetBlock - scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment ; target. A missile and a drone both cover 2x2 cells, so all four are added up: every unit standing ; in them contributes targetValueByUnitType, positive for the human player's units and negative for ; the AI's own. A block with a negative total is never chosen; otherwise, when the total beats the ; running best in zp_1C, the corner is remembered in zp_95/zp_94 and the highest human unit index ; found in the block becomes the nominal target in zp_1D (which favours the comcen, unit 49). ; In: X = column, Y = row of the top-left cell; zp_1C = best score so far (may be negative to accept ; anything), ; zp_1D = best target so far; map at $F000, unitTypeTable $F76C ; Out: zp_1A = the block's score, zp_1B = highest human unit index in it; on an improvement zp_1C, ; zp_1D, zp_94 and zp_95 are ; updated; zp_48/zp_49 = map pointer; self-modifies the operands at $E9E4 and $E9E8 ; Called from: the missile/drone target scan at $EFA8, and from the $EE00 drone-flight overlay. ; ---------------------------------------------------------------------- scoreTargetBlock: stx L_E9E3+1 ; E9A6 self-modify: keep the block's column in the LDA at $E9E3 sty L_E9E7+1 ; E9A9 self-modify: keep the block's row in the LDA at $E9E7 lda #$00 ; E9AC clear the accumulators sta scratch1A ; E9AE score of this 2x2 block sta scratch1B ; E9B0 highest human unit index seen in it jsr readMapCell ; E9B2 top-left cell; readMapCell also leaves mapCellPtr pointing at it bpl L_E9BA ; E9B5 bare terrain jsr addUnitTargetValue ; E9B7 a unit stands here: score it ; ---------------------------------------------------------------------- ; The other three cells of the block are reached straight through mapCellPtr: +1 is the cell to the ; right, +40 the cell below (the map is 40 cells wide) and +41 the diagonal. ; ---------------------------------------------------------------------- L_E9BA: ldy #$01 ; E9BA offset +1 = the cell to the right lda (mapCellPtr),y ; E9BC read it bpl L_E9C3 ; E9BE bare terrain jsr addUnitTargetValue ; E9C0 score the unit standing there L_E9C3: ldy #$28 ; E9C3 offset +40 = one map row down lda (mapCellPtr),y ; E9C5 read it bpl L_E9CC ; E9C7 bare terrain jsr addUnitTargetValue ; E9C9 score it L_E9CC: ldy #$29 ; E9CC offset +41 = down and to the right lda (mapCellPtr),y ; E9CE read it bpl L_E9D5 ; E9D0 bare terrain jsr addUnitTargetValue ; E9D2 score it L_E9D5: lda scratch1A ; E9D5 total value of the block bmi L_E9F0 ; E9D7 negative: the AI's own units dominate it, so never bombard it L_E9D9: cmp scratch1C ; E9D9 better than the best block found so far? bcs L_E9E1 ; E9DB yes: take it bit scratch1C ; E9DD no, but is the running best itself still negative (nothing chosen yet)? bpl L_E9F0 ; E9DF no: keep the previous best L_E9E1: sta scratch1C ; E9E1 new best score L_E9E3: lda #$FF ; E9E3 operand patched at $E9A6: the block's column sta lineEndCol ; E9E5 remember it as the aim point L_E9E7: lda #$FF ; E9E7 operand patched at $E9A9: the block's row L_E9E9: sta lineEndRow ; E9E9 aim point row lda scratch1B ; E9EB the biggest human unit index in the block sta scratch1D ; E9ED becomes the nominal target unit (49 = the comcen wins any tie) rts ; E9EF a new best target has been recorded L_E9F0: rts ; E9F0 rejected: the running best is left alone ; targetValueByUnitType - five bytes indexed by unit type, the bombardment value of a unit standing in ; a candidate target block. Boomers and the comcen are worth four times a grunt. targetValueByUnitType: .byte $01,$02,$04,$01,$04 ; E9F1 ..... GRUNT 1, RIDER 2, BOOMER 4, SPY 1, COMCEN 4 ; ---------------------------------------------------------------------- ; addUnitTargetValue - helper of scoreTargetBlock. Takes a map cell byte that holds $80 plus a unit ; index, ignores destroyed units, and adds targetValueByUnitType[type] to the block score in zp_1A - ; negated when the unit belongs to the AI itself, so the trainer will not shell its own troops. For a ; human unit it also keeps the largest index seen, which is how the human comcen (unit 49) ends up ; being preferred as the nominal target. ; In: A = map cell byte with bit 7 set, unitTypeTable $F76C, targetValueByUnitType $E9F1, zp_1A/zp_1B ; accumulators ; Out: zp_1A += the (possibly negated) value, zp_1B = highest human unit index seen; X and Y clobbered ; Called from: scoreTargetBlock, once per cell of the 2x2 block ($E9B7, $E9C0, $E9C9, $E9D2). ; ---------------------------------------------------------------------- addUnitTargetValue: and #$7F ; E9F6 drop the 'a unit is here' bit: what is left is the unit index tax ; E9F8 index the unit tables with it lda unitTypeTable,x ; E9F9 its type byte bmi L_EA1A ; E9FC destroyed: worth nothing and #$07 ; E9FE unit type 0-4 tay ; EA00 index the value table lda targetValueByUnitType,y ; EA01 bombardment value of that type cpx #$32 ; EA04 unit 50-99 = one of the AI's own? bcs L_EA10 ; EA06 yes: its value counts against the block cpx scratch1B ; EA08 human unit: is it the highest index seen in this block so far? bcc L_EA15 ; EA0A no: just add its value stx scratch1B ; EA0C yes: it becomes the block's nominal target bcs L_EA15 ; EA0E always taken (the BCS above fell through with C=1) L_EA10: eor #$FF ; EA10 negate the value ... clc ; EA12 clear the carry ... adc #$01 ; EA13 ... two's complement, so friendly units subtract L_EA15: clc ; EA15 clear the carry before accumulating adc scratch1A ; EA16 add into the block score sta scratch1A ; EA18 store it back L_EA1A: rts ; EA1A cell scored ; ---------------------------------------------------------------------- ; offsetCoordinateClamped - returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps ; of the signed step X, held inside the 40-cell battlefield, with the carry reporting whether the ; unclamped value had left the map. The base coordinate and the caller's step count are both parked ; in immediate operands so nothing but A is disturbed. ; In: A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps ; Out: A = clamped coordinate 0-39, C=1 when the raw value was negative or past 39, Y unchanged; ; self-modifies the operands at $EA27 and $EA36 ; Called from: pushTrainerComcenForward/retreatTrainerComcen ($E24C/$E25B/$E27E/$E28D), the path ; search ($E48A/$E499) ; and offsetPointInDirection ($E675/$E684). ; ---------------------------------------------------------------------- offsetCoordinateClamped: sty L_EA35+1 ; EA1B self-modify: save the step count in the LDY at $EA35 so Y survives the multiply sta L_EA26+1 ; EA1E self-modify: the base coordinate becomes the operand of the ADC at $EA26 txa ; EA21 A = the signed step, 0, +1 or $FF jsr multiplyAbyY ; EA22 A = step * number of steps clc ; EA25 clear the carry L_EA26: adc #$FF ; EA26 operand patched at $EA1E: add the base coordinate tay ; EA28 keep the raw, unclamped result for the range test L_EA29: bpl L_EA2D ; EA29 positive: no low clamp needed lda #$00 ; EA2B walked off the top or left edge: clamp to 0 L_EA2D: cmp #$28 ; EA2D past the last cell (40)? bcc L_EA33 ; EA2F no, keep it lda #$27 ; EA31 yes: clamp to 39 L_EA33: cpy #$28 ; EA33 C=1 when the raw value was 40 or more, which also covers a negative value (>= $80) L_EA35: ldy #$FF ; EA35 operand patched at $EA1B: hand the caller its step count back rts ; EA37 A = clamped coordinate, C=1 if it had to be clamped ; ---------------------------------------------------------------------- ; mirrorBlockCornerForOpponent - converts the top-left corner of a 2x2 object (the drone) into the ; coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn ; back into that same corner: X = 39-(X+1), Y = 39-(Y+1). Mirroring alone would give the bottom-right ; corner of the mirrored block, hence the pre-increment. ; In: X = column, Y = row of the block's top-left cell ; Out: X, Y = the mirrored corner; A clobbered ; Called from: the fake drone-position packet at $EEB1 and $EF8F, and from the drone-flight overlay. ; ---------------------------------------------------------------------- mirrorBlockCornerForOpponent: inx ; EA38 the engine subtracts 1 after mirroring, so add it here first txa ; EA39 column+1 into A jsr mirrorMapCoordinate ; EA3A 39 - (column+1) L_EA3D: tax ; EA3D mirrored column back into X iny ; EA3E same for the row tya ; EA3F row+1 into A jsr mirrorMapCoordinate ; EA40 39 - (row+1) tay ; EA43 mirrored row back into Y rts ; EA44 corner is now expressed the way a remote opponent would have sent it ; aiCmdQueuePeak - a single byte of data sitting in the code stream between ; mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI's order queue. It is ; written and compared only inside queueAiCommand and read nowhere else, so it is a development ; leftover. aiCmdQueuePeak: .byte $00 ; EA45 . longest the AI order queue has ever grown this game; nothing ever reads it back ; ---------------------------------------------------------------------- ; queueAiCommand - appends the command staged in $8896..$8899 to the AI's 50-byte order queue at ; $88A0. It looks the total command length up with getCommandLength, keeps a high-water mark in ; aiCmdQueuePeak, and when the queue would reach 50 bytes it drops the command and turns the border ; blue instead, as a visible warning that the AI is producing orders faster than the lock-step packets ; can carry them away. ; In: $8896-$8899 = the staged command, $8952 aiCmdQueueLength, commandLengthTable through ; getCommandLength ($4D9F) ; Out: queue at $88A0 extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the ; copy counter), ; $EA45 peak updated; on overflow $9003 (the border-colour shadow) = 6 and the command is thrown ; away ; Called from: queueMirroredMoveOrder ($E95A) and queueComcenCommand ($EBE0); the trainer setup ; overlay has its own ; call at its own $EC82. ; ---------------------------------------------------------------------- queueAiCommand: inc D_8968 ; EA46 count one more command issued this round lda D_8896 ; EA49 the staged command byte jsr getCommandLength ; EA4C currentCommandLength ($92EE) = 1 command byte plus its parameters clc ; EA4F clear the carry adc D_8952 ; EA50 how long the queue would be afterwards cmp aiCmdQueuePeak ; EA53 beyond the previous high-water mark? bcc L_EA5B ; EA56 no sta aiCmdQueuePeak ; EA58 record the new peak L_EA5B: cmp #$32 ; EA5B the queue at $88A0 holds 50 bytes bcc L_EA65 ; EA5D it still fits lda #$06 ; EA5F colour 6 = blue sta vicBorderShadow ; EA61 overflow: recolour the border through the VIC shadow and drop the command rts ; EA64 command lost (the round counter at $8968 was already bumped) L_EA65: ldx #$00 ; EA65 copy from the first staging byte ldy D_8952 ; EA67 append at the current end of the queue L_EA6A: lda D_8896,x ; EA6A next staged byte sta D_88A0,y ; EA6D into the queue inx ; EA70 advance the staging index iny ; EA71 advance the queue index inc D_8952 ; EA72 the queue is one byte longer dec currentCommandLength ; EA75 one byte of this command done bne L_EA6A ; EA78 until the whole command has been copied rts ; EA7A the order is now waiting to be handed to the engine ; ---------------------------------------------------------------------- ; injectAiCommandsIntoPacket - the heart of the solo trainer. Where the modem build would decode a ; packet that arrived over the wire, this build fabricates one: it copies whole commands out of the ; AI's own queue at $88A0 straight into the comm module's received-packet buffer ($E020, length ; $E01E), so the lock-step engine executes the computer opponent's orders through exactly the same ; command interpreter a human opponent's orders would use. It first runs the missile/drone decision, ; which may claim the whole packet for itself, and then moves as many complete commands as fit while ; the packet stays under 8 bytes, shifting the remainder of the queue down after each one. ; In: $8952 aiCmdQueueLength, the queue at $88A0, $E01E current packet length, getCommandLength ; ($4D9F) ; Out: $E020.. filled, $E01E updated, the queue compacted and $8952 reduced; $8966 cleared then ; possibly set by runAiMissileTurn; ; $92EE clobbered ; Called from: buildTrainerReplyPacket ($E0E4), once per lock-step exchange. ; ---------------------------------------------------------------------- injectAiCommandsIntoPacket: lda #$00 ; EA7B clear the flag first sta D_8966 ; EA7D $8966 bit 7 will mean 'the missile/drone code owns this packet' jsr runAiMissileTurn ; EA80 offer the AI a missile shot or a drone launch; it writes straight into the packet lda D_8966 ; EA83 did it claim the packet? bmi L_EACE ; EA86 yes: leave the order queue for the next exchange lda D_8952 ; EA88 is anything queued at all? beq L_EACE ; EA8B no: send an empty packet ; ---------------------------------------------------------------------- ; One iteration per queued command: measure it, check that it still fits in the packet, copy it into ; the packet, then slide the rest of the queue down over it. ; ---------------------------------------------------------------------- L_EA8D: ldy #$00 ; EA8D read from the front of the queue lda D_88A0,y ; EA8F its command byte jsr getCommandLength ; EA92 currentCommandLength = its total length lda rxPacketLength ; EA95 bytes already in the fabricated 'received' packet clc ; EA98 clear the carry adc currentCommandLength ; EA99 plus this command cmp #$08 ; EA9C a packet carries at most 7 command bytes bcs L_EACE ; EA9E it would not fit: the rest waits for the next exchange D_EAA0: ldx rxPacketLength ; EAA0 append at the current end of the packet buffer L_EAA3: lda D_88A0,y ; EAA3 next byte of the command sta rxPacketBuffer,x ; EAA6 into the received-packet buffer at $E020 inx ; EAA9 advance the packet index iny ; EAAA advance the queue index cpy currentCommandLength ; EAAB whole command copied? bcc L_EAA3 ; EAAE no: keep copying stx rxPacketLength ; EAB0 new packet length ldx #$00 ; EAB3 now compact the queue from its start L_EAB5: lda D_88A0,y ; EAB5 byte just past the command that was taken D_EAB8: sta D_88A0,x ; EAB8 slide it down to the front D_EABB: inx ; EABB advance the write index D_EABC: iny ; EABC advance the read index D_EABD: cpy D_8952 ; EABD until the old end of the queue D_EAC0: bcc L_EAB5 ; EAC0 keep shifting D_EAC2: lda D_8952 ; EAC2 old queue length D_EAC5: sec ; EAC5 prepare the subtraction D_EAC6: sbc currentCommandLength ; EAC6 minus the command just handed over sta D_8952 ; EAC9 new queue length bne L_EA8D ; EACC more orders waiting: try to fit another into the same packet L_EACE: rts ; EACE the packet the engine is about to 'receive' is ready ; ---------------------------------------------------------------------- ; updateAiUnitTracking - per-unit hook, run from trainerUnitStepHook after the engine has stepped each ; unit. It maintains three pieces of AI awareness. For a human unit it latches the first ; grunt/rider/boomer that has picked the AI comcen as its fire target into comcenAttackerUnit ($8972). ; For one of its own units it marks that unit's group as having the human comcen in reach (groupFlags ; bit 6) and stamps the unit for an immediate replan, once the comcen has been spotted and the unit is ; within 8 cells of it. Finally, an AI spy (units 96-98) that has reached the opponent's end zone ; while still inside an ordinary group is split off into a brand new group of its own, seeded with its ; current destination, so it can operate alone. ; In: $90FA currentUnit, $F76C unitTypeTable, $91F0 fireTargetUnit, $F9F5 human comcen energy byte ; (bit 7 = spotted), ; $F671/$F6D5 human comcen cell, $F640/$F6A4 unit cells, aiUnitOrderRound ($87CE+unit), ; aiUnitGroup ($8800+unit), ; aiUnitDeepFlag ($8832+unit), $91CB game clock, $8954 aiGroupCount, $8964 home group id ; Out: $8972 comcenAttackerUnit, groupFlags $893E bit 6, $8953 last allocated group id, the unit's ; group and round stamp, ; and the new group's target cell $88D2/$88DE ; Called from: trainerUnitStepHook ($E124), for every unit the engine steps. ; ---------------------------------------------------------------------- updateAiUnitTracking: ldy currentUnit ; EACF the unit the engine has just stepped cpy #$32 ; EAD2 unit 0-49 or 50-99? bcs L_EAF1 ; EAD4 one of the AI's own: run the contact tracking ldx D_8972 ; EAD6 comcenAttackerUnit, $FF while nobody is known to be shooting at the AI comcen bpl L_EAEE ; EAD9 already latched: nothing to learn lda fireTargetUnit ; EADB the unit this human unit has picked as its fire target cmp #$63 ; EADE unit 99 = the AI comcen bne L_EAEE ; EAE0 aiming at something else lda unitTypeTable,y ; EAE2 its type byte and #$07 ; EAE5 unit type 0-4 cmp #$03 ; EAE7 type 3 SPY or 4 COMCEN? bcs L_EAEE ; EAE9 yes: they cannot shoot, so they are not a threat to record D_EAEB: sty D_8972 ; EAEB remember the first unit that is aiming at the AI comcen L_EAEE: jmp L_EB66 ; EAEE human unit handled ; ---------------------------------------------------------------------- ; AI unit: has it got close enough to the human comcen to count as contact? The two coordinate tests ; accept a difference in -8..+7 by comparing the signed byte against $F8 and then against 8. ; ---------------------------------------------------------------------- L_EAF1: lda side0ComcenEnergy ; EAF1 energy byte of the human comcen; bit 7 doubles as 'the AI has spotted it' bpl L_EB2C ; EAF4 not spotted yet: there is no contact to record lda side0ComcenCol ; EAF6 column of the human comcen sec ; EAF9 prepare the subtraction sbc unitColTable,y ; EAFA minus this AI unit's column cmp #$F8 ; EAFD is the difference at least -8? D_EAFF: bcs L_EB05 ; EAFF yes, within 8 columns to one side D_EB01: cmp #$08 ; EB01 is it less than +8? sub_EB03: bcs L_EB2C ; EB03 no: more than 8 columns away L_EB05: lda side0ComcenRow ; EB05 row of the human comcen sec ; EB08 prepare the subtraction sbc unitRowTable,y ; EB09 minus this AI unit's row cmp #$F8 ; EB0C is the difference at least -8? bcs L_EB14 ; EB0E yes cmp #$08 ; EB10 is it less than +8? bcs L_EB2C ; EB12 no: more than 8 rows away L_EB14: ldx msgGetOpponentOnPhoneAnd,y; EB14 aiUnitGroup[unit], the group table addressed by absolute index cpx D_8964 ; EB17 is it the home/guard group? beq L_EB66 ; EB1A yes: that group is not supposed to chase the comcen lda D_893E,x ; EB1C its groupFlags ora #$40 ; EB1F bit 6 = this group has the human comcen within reach sta D_893E,x ; EB21 store the flags back bmi L_EB2C ; EB24 bit 7 set (the group is already suspended): do not restamp its clock lda gameClock ; EB26 the down-counting round clock sta msgWeWonByAKnockout,y ; EB29 aiUnitOrderRound[unit]: stamping the current clock makes the planner replan this unit at once (also mapGenerator6F00:msgWeWonByAKnockout) ; ---------------------------------------------------------------------- ; An AI spy (units 96-98) that has scored both end zones is promoted out of its group into a group of ; its own so it can act independently behind the human lines. ; ---------------------------------------------------------------------- L_EB2C: cpy #$63 ; EB2C the AI comcen itself (unit 99)? bcs L_EB66 ; EB2E yes: nothing else to do for it cpy #$60 ; EB30 units 96-98 are the AI's three SPYs bcc L_EB66 ; EB32 any other unit: done lda D_8832,y ; EB34 aiUnitDeepFlag[unit], bit 7 = the unit has scored both territory zones bpl L_EB66 ; EB37 this spy has not got that far yet lda msgGetOpponentOnPhoneAnd,y; EB39 its current group id cmp D_8954 ; EB3C does it still belong to one of the ordinary groups? bcs L_EB66 ; EB3F no (already split off, or $FF): leave it alone cmp D_8964 ; EB41 is it in the home/guard group? beq L_EB66 ; EB44 yes: that one stays together inc D_8953 ; EB46 allocate the next group id; note that aiGroupCount $8954 is not bumped with it, and that ids past 11 would run off the end of the twelve-entry per-group tables lda D_8953 ; EB49 the new id tax ; EB4C index the per-group tables with it sta msgGetOpponentOnPhoneAnd,y; EB4D the spy becomes a group of its own lda gameClock ; EB50 the current round sta msgWeWonByAKnockout,y ; EB53 stamp it so the planner picks the new group up on the next scan (also mapGenerator6F00:msgWeWonByAKnockout) lda unitDestColTable,y ; EB56 the destination the spy is already heading for and #$3F ; EB59 coordinate bits only sta D_88D2,x ; EB5B becomes the new group's target column lda unitDestRowTable,y ; EB5E and its destination row and #$3F ; EB61 coordinate bits only sta D_88DE,x ; EB63 becomes the new group's target row L_EB66: rts ; EB66 unit tracked ; ---------------------------------------------------------------------- ; runAiComcenActions - spends whatever is left of the AI comcen's three command slots for this round. ; A stunned comcen needs two of them: one to halt itself and one for the 1-in-8 repair attempt that ; queues command $9E, exactly the odds the human REPAIR tab uses. An unstunned comcen is halted once ; and then spends every remaining slot on maybeQueueComcenEnergyRepair. ; In: $8968 commands already issued this round, $8971 the comcen's stun counter (low nibble of ; unitStunTable[99]) ; Out: $8967 = the comcen's remaining slot budget, commands queued through queueAiCommand; returns ; silently when there is ; no budget left ; Called from: planTrainerRound at $E1CD (comcen too weak to attack) and $E21B (defend mode). ; ---------------------------------------------------------------------- runAiComcenActions: lda #$03 ; EB67 the comcen may spend at most 3 command slots in a round sec ; EB69 prepare the subtraction sbc D_8968 ; EB6A minus the orders the planner has already queued bcc L_EBE3 ; EB6D already over budget: do nothing at all sta D_8967 ; EB6F slots left for the comcen tax ; EB72 keep the budget in X as well lda D_8971 ; EB73 the comcen's stun counter beq L_EB96 ; EB76 not stunned: go straight to the energy repairs cpx #$02 ; EB78 a stun repair attempt costs two slots bcc L_EBE3 ; EB7A not enough left dex ; EB7C spend one ... dex ; EB7D ... and the second stx D_8967 ; EB7E the reduced budget jsr haltAiComcen ; EB81 halt the comcen; a moving comcen cannot be worked on jsr nextGameRandom ; EB84 step the lock-step RNG and #$07 ; EB87 one chance in eight bne L_EBE3 ; EB89 the repair attempt failed this round lda #$9E ; EB8B command $9E = cmdComcenRepairedOrSetupReady, which clears the comcen stun counter on both machines jsr queueComcenCommand ; EB8D queue it ($8897/$8898 are ignored, the command is one byte long) inc D_8967 ; EB90 cancel the DEC that the shared loop tail below is about to do jmp L_EB9F ; EB93 join the energy-repair loop with the remaining budget L_EB96: txa ; EB96 slots left beq L_EBE3 ; EB97 none: nothing to do jsr haltAiComcen ; EB99 halt the comcen so it can repair L_EB9C: jsr maybeQueueComcenEnergyRepair; EB9C spend one slot on an energy repair L_EB9F: dec D_8967 ; EB9F one slot used bne L_EB9C ; EBA2 keep spending until the budget is exhausted rts ; EBA4 the comcen has done what it can this round ; ---------------------------------------------------------------------- ; maybeQueueComcenEnergyRepair - the trainer's version of the human console's energy repair. With 50 ; percent probability, and only while the AI comcen's energy is below 45 of 50, it queues command $9A ; cmdAddEnergy for unit 99 with the same amount the human repair uses, (50 - energy) / 8 + 1. The ; projected energy is accumulated in $8965 (which planTrainerRound seeded from the comcen's real ; energy and uses as its action budget), and the amount is nudged by one whenever that projection ; would land on a multiple of four. ; In: $FA27 = unitEnergyTable[99] (energy in bits 0-5), $8965 projected comcen energy, nextGameRandom ; Out: $8896 = $9A, $8897 = $63 (unit 99), $8898 = the amount, command queued; $8965 raised by the ; same amount ; Called from: runAiComcenActions ($EB9C), once per remaining comcen command slot. ; ---------------------------------------------------------------------- maybeQueueComcenEnergyRepair: jsr nextGameRandom ; EBA5 step the lock-step RNG and #$01 ; EBA8 an energy repair succeeds one time in two, as on the human console beq L_EBE3 ; EBAA failed this time: spend the slot on nothing lda side1ComcenEnergy ; EBAC energy byte of AI comcen unit 99 and #$3F ; EBAF energy 0-50 lives in bits 0-5; bits 6-7 are visibility flags cmp #$2D ; EBB1 45 out of 50 or better? bcs L_EBE3 ; EBB3 healthy enough: do not waste a command on it sta scratch18 ; EBB5 keep the current energy lda #$32 ; EBB7 50 = a full comcen sec ; EBB9 prepare the subtraction sbc scratch18 ; EBBA the shortfall lsr a ; EBBC divide by 8 ... lsr a ; EBBD ... lsr a ; EBBE ... so repairs get smaller as the comcen recovers clc ; EBBF clear the carry adc #$01 ; EBC0 amount = shortfall/8 + 1 sta D_8898 ; EBC2 second parameter of the command clc ; EBC5 clear the carry adc D_8965 ; EBC6 add it to the energy the comcen is projected to end up with sta D_8965 ; EBC9 keep the projection and #$03 ; EBCC did the projection land on a multiple of four? bne L_EBD6 ; EBCE no: the amount stands inc D_8898 ; EBD0 yes: repair one point more so it does not inc D_8965 ; EBD3 and keep the projection in step with it L_EBD6: lda #$63 ; EBD6 unit 99 = the AI comcen sta msgPickUpPhoneThenPressSpace; EBD8 first parameter of the command ($8897) lda #$9A ; EBDB command $9A = cmdAddEnergy (unit, amount); falls straight into queueComcenCommand ; ---------------------------------------------------------------------- ; queueComcenCommand - shared tail of the comcen actions: stores A as the command byte and jumps into ; queueAiCommand, leaving ; $8897 and $8898 as the caller staged them. ; In: A = command byte, $8897/$8898 = parameters already staged ; Out: the command appended to the AI order queue ; Called from: runAiComcenActions ($EB8D) and by falling through from maybeQueueComcenEnergyRepair ; ($EBDB). ; ---------------------------------------------------------------------- queueComcenCommand: sta D_8896 ; EBDD command byte into the staging area jmp queueAiCommand ; EBE0 append the staged bytes to the queue L_EBE3: rts ; EBE3 shared 'do nothing this slot' exit of the comcen action code ; ---------------------------------------------------------------------- ; haltAiComcen - orders the AI comcen to move to the cell it is already standing on. ; orderAiUnitToCell turns that into a real ; $80/$81 command only when the comcen currently has some other destination, so this stops a moving ; comcen and costs nothing when ; it is already still. A comcen has to be stationary before it can repair or launch. ; In: $F6A3 unitCol[99], $F707 unitRow[99] ; Out: possibly one $80/$81 command queued; nothing at all when the comcen is already halted ; Called from: runAiComcenActions ($EB81 stunned path, $EB99 repair path). ; ---------------------------------------------------------------------- haltAiComcen: ldx side1ComcenCol ; EBE4 the column the AI comcen is standing on lda side1ComcenRow ; EBE7 and its row ldy #$31 ; EBEA AI ordinal 49 = absolute unit 99, the AI comcen jmp orderAiUnitToCell ; EBEC ordering it to its own cell halts it; a redundant order is dropped ; ---------------------------------------------------------------------- ; assessThreatAroundAiComcen - builds a directional pressure map around the AI comcen and picks the ; direction to run. Every living human combat unit (ordinals 0-45: the 28 grunts, 12 riders and 6 ; boomers) within 20 cells adds a weight into one of eight compass buckets at $8896-$889D: 1 for ; merely being nearby, +1 within 10 cells, and +4 more when the comcen is inside that unit's weapon ; range (5 cells, 9 for a boomer). The base 1 is dropped once the comcen has actually been hit ; ($897C), so only units that can reach it steer the escape. The tail rankThreatDirections ($EC82, ; just past this chunk) smooths the ring and returns the centre of the quietest three-direction arc. ; In: zp_27/zp_28 = AI comcen column/row (set by the caller at $E1D1), ; unitTypeTable/unitColTable/unitRowTable, ; $897C = 'the comcen was hit this round or last', zp_57 game RNG low byte for the tie break ; Out: A = (total pressure - number of threats) / 2 = overall threat level, X = deepest weapon-range ; surplus (0 = nothing in ; range), Y = safest compass direction 0-7, or 8 (the null delta entry) when nothing threatens; ; $8896-$889D and $0400-$0407 hold the direction buckets, zp_18-zp_23 clobbered ; Called from: planTrainerRound ($E1DB), which files the results into $8962/$8960/$8961, and the drone ; code ($EF7B). ; ---------------------------------------------------------------------- assessThreatAroundAiComcen: lda #$00 ; EBEF clear the accumulators sta scratch1F ; EBF1 total enemy pressure sta scratch20 ; EBF3 number of units that count as a threat sta scratch21 ; EBF5 deepest weapon-range surplus seen sta scratch1E ; EBF7 cleared but never read again in either half of the routine: a leftover ldy #$07 ; EBF9 clear the eight compass buckets, 7 down to 0 L_EBFB: sta D_8896,y ; EBFB bucket for direction Y D_EBFE: dey ; EBFE next direction bpl L_EBFB ; EBFF until all eight are clear ; ---------------------------------------------------------------------- ; One pass per human combat unit, ordinal 45 down to 0 (the roster is 28 grunts, 12 riders, 6 boomers, ; 3 spies and the comcen, so 0-45 is exactly the units that can shoot). ; ---------------------------------------------------------------------- lda #$2D ; EC01 scan the human player's units 45 down to 0 sta scratch18 ; EC03 loop counter L_EC05: ldx scratch18 ; EC05 the unit being weighed lda unitTypeTable,x ; EC07 its type byte D_EC0A: bmi L_EC74 ; EC0A destroyed: skip it ldy #$05 ; EC0C weapon range 5 cells for a grunt or rider and #$02 ; EC0E bit 1 of the type marks a BOOMER (type 2); a SPY would match too, but the scan never reaches one beq L_EC14 ; EC10 not a boomer: keep range 5 L_EC12: ldy #$09 ; EC12 a boomer reaches 9 cells L_EC14: sty scratch19 ; EC14 weapon range of this unit lda unitColTable,x ; EC16 its column sta cellColA ; EC19 point A of the distance test sta lineEndCol ; EC1B and the target cell of the direction test lda unitRowTable,x ; EC1D its row sta cellRowA ; EC20 point A row sta lineEndRow ; EC22 target row jsr cellDistance ; EC24 distance from the AI comcen, which the caller left in cellColB/cellRowB cmp #$14 ; EC27 20 cells bcs L_EC74 ; EC29 too far away to matter sub_EC2B: sta scratch1A ; EC2B keep the distance lda cellColB ; EC2D the comcen's own cell ... sta lineCurCol ; EC2F ... is where the direction is measured from lda cellRowB ; EC31 comcen row sta lineCurRow ; EC33 line walker row jsr getStepDirection ; EC35 Y = compass direction from the comcen toward this unit bpl L_EC3C ; EC38 a real direction 0-7 ldy #$00 ; EC3A the unit is standing on the comcen's own cell: charge it to direction 0 ; ---------------------------------------------------------------------- ; Weigh this unit: a base 1 for being nearby (suppressed once the comcen is actually under fire), +1 ; within 10 cells and +4 more when the comcen sits inside its weapon range. ; ---------------------------------------------------------------------- L_EC3C: inc scratch20 ; EC3C one more threatening unit ldx #$00 ; EC3E the weight this unit will contribute bit D_897C ; EC40 was the comcen hit in this round or the previous one? bmi L_EC46 ; EC43 yes: only units that can actually reach it get any weight inx ; EC45 no: every nearby enemy is worth 1 L_EC46: lda scratch1A ; EC46 the distance again cmp #$0A ; EC48 closer than 10 cells? bcs L_EC5E ; EC4A no: it only counts as a distant presence inx ; EC4C +1 for being close lda scratch19 ; EC4D its weapon range sec ; EC4F prepare the subtraction sbc scratch1A ; EC50 range minus distance: how deep the comcen sits inside its envelope bcc L_EC5E ; EC52 the comcen is out of range of this unit sub_EC54: inx ; EC54 +1 ... inx ; EC55 +2 ... inx ; EC56 +3 ... inx ; EC57 +4 more: the comcen is inside this unit's weapon range sub_EC58: cmp scratch21 ; EC58 is this the deepest penetration so far? L_EC5A: bcc L_EC5E ; EC5A no sta scratch21 ; EC5C remember the worst range surplus, returned in X ; ---------------------------------------------------------------------- ; Add the weight to its compass bucket and to the running total, both saturating at 255. ; ---------------------------------------------------------------------- L_EC5E: txa ; EC5E the weight of this unit clc ; EC5F clear the carry adc D_8896,y ; EC60 into the bucket of its compass direction bcc L_EC67 ; EC63 no overflow lda #$FF ; EC65 saturate the bucket at 255 L_EC67: sta D_8896,y ; EC67 store the bucket back txa ; EC6A the weight again clc ; EC6B clear the carry L_EC6C: adc scratch1F ; EC6C into the overall pressure total bcc L_EC72 ; EC6E no overflow lda #$FF ; EC70 saturate the total at 255 L_EC72: sta scratch1F ; EC72 store it back L_EC74: dec scratch18 ; EC74 next human unit bpl L_EC05 ; EC76 until unit 0 has been weighed ; ---------------------------------------------------------------------- ; Hand the ring over to rankThreatDirections, or - when nothing at all threatened the comcen - report ; the null direction 8, whose entry in dirToColDelta/dirToRowDelta is the zero step. ; ---------------------------------------------------------------------- lda scratch20 ; EC78 did anything threaten the comcen? bne rankThreatDirections ; EC7A yes: smooth the ring and pick the quietest arc tax ; EC7C no threats: A = X = 0 ldy #$08 ; EC7D direction 8 = the null entry of the direction tables, i.e. 'no need to move' jmp threatScanDone ; EC7F share the RTS at the end of rankThreatDirections ; ---------------------------------------------------------------------- ; rankThreatDirections - Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just ; turned every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one ; of the eight compass buckets $8896-$889D. This half blurs that ring circularly, finds the centre of ; the quietest three-direction arc and returns the summary the planner stores in $8960-$8962. ; In: $8896-$889D = raw per-direction pressure, zp_1F = total pressure, zp_20 = number of units that ; contributed, zp_21 = deepest weapon-range penetration, zp_57 = low byte of the lock-step game ; RNG state (tie break) ; Out: A = (zp_1F - zp_20) / 2 = overall threat level, X = zp_21, Y = zp_18 = the quietest compass ; direction 0-7; $8896-$889D holds the blurred ring and $0400-$0407 the raw copy; zp_18, zp_19, ; zp_1A and zp_23 clobbered ; Called from: branch at $EC7A only (the tail of $EBEF); when no unit threatened at all the path at ; $EC7C returns A=0, X=0, Y=8 instead - index 8 is the deliberate null entry (0,0) of ; dirToColDelta/dirToRowDelta ; Take an unsmoothed copy of the eight direction buckets into the $0400 scratch page, because the blur ; below overwrites $8896-$889D while it still needs each direction's original neighbours. ; ; A warning for readers of the rest of this chunk: $EC00-$EFFF is shared by four different overlay ; images (this trainer tail, the modem driver, the track 29 trainer setup and the track 29 drone AI), ; and XREF merges their cross references. Many of the L_/D_/sub_ labels below are branch targets of ; one of the other builds that happen to land on an instruction boundary here; in this file they sit ; in the middle of a routine and nothing reaches them. ; ---------------------------------------------------------------------- rankThreatDirections: ldy #$07 ; EC82 start at direction 7 (north-west) and walk the ring backwards copyThreatRingLoop: lda D_8896,y ; EC84 raw pressure of direction Y from the 8-byte ring at $8896 sta checksumXorConstant,y ; EC87 keep the untouched copy in the $0400 scratch page (the same page finishRound uses for scan lists) (also trainerPlaybook0200:play3Col12) dey ; EC8A next direction down bpl copyThreatRingLoop ; EC8B until all 8 directions have been copied lda #$07 ; EC8D the blur pass also starts at direction 7 sta scratch23 ; EC8F zp_23 = the direction currently being smoothed ; ---------------------------------------------------------------------- ; smoothThreatRing - Circular 1-2-1 blur of the eight-direction threat ring. For i = 7 down to 0 it ; computes (2*raw[i] + raw[i-1] + raw[i+1]) / 4, reading the untouched copy from $0400 and writing the ; result back over $8896+i. Indices wrap modulo 8, so north's neighbours are north-west and ; north-east. ; In: $0400-$0407 = raw direction weights, zp_23 = direction index counter (7 on entry) ; Out: $8896-$889D = blurred weights; zp_19 = the sum's high bits; zp_23 ends at $FF; A/X/Y clobbered ; Called from: nothing - this is the loop head of rankThreatDirections, branched to from the DEC/BPL ; at $ECCA. (XREF shows a JSR from $7AE9, but that call belongs to the other overlay that shares ; these addresses.) ; ---------------------------------------------------------------------- smoothThreatRing: lda scratch23 ; EC91 i = the direction being smoothed L_EC93: tax ; EC93 X indexes the centre sample tay ; EC94 Y will index the neighbour on the counter-clockwise side ... dey ; EC95 ... i - 1 bpl blurSumStart ; EC96 no wrap needed while i-1 is still 0..7 ldy #$07 ; EC98 i was 0, so its counter-clockwise neighbour is direction 7 blurSumStart: lda #$00 ; EC9A clear the two extra bits the weighted sum can produce L_EC9C: sta scratch19 ; EC9C zp_19 = bits 8-9 of the running sum lda checksumXorConstant,x ; EC9E raw pressure of the centre direction (also trainerPlaybook0200:play3Col12) asl a ; ECA1 double it - the centre direction carries weight 2 bcc addCounterClockwiseNeighbour; ECA2 no carry out of the doubling inc scratch19 ; ECA4 carry the 9th bit into zp_19 addCounterClockwiseNeighbour: clc ; ECA6 clear carry before adding a neighbour adc checksumXorConstant,y ; ECA7 + the counter-clockwise neighbour, weight 1 (also trainerPlaybook0200:play3Col12) bcc stepToClockwiseNeighbour; ECAA no carry out of the add inc scratch19 ; ECAC carry into the high bits again stepToClockwiseNeighbour: inx ; ECAE step X to the clockwise neighbour, i + 1 cpx #$08 ; ECAF past the last direction? bcc addClockwiseNeighbour ; ECB1 no, i+1 is still inside the ring ldx #$00 ; ECB3 wrap i+1 round to direction 0 addClockwiseNeighbour: clc ; ECB5 clear carry before the last add adc checksumXorConstant,x ; ECB6 + the clockwise neighbour, weight 1 (also trainerPlaybook0200:play3Col12) bcc finishBlurredValue ; ECB9 no carry inc scratch19 ; ECBB carry into the high bits finishBlurredValue: lsr scratch19 ; ECBD divide the 10-bit sum by 4: high bits down ... ror a ; ECBF ... into A (first halving) lsr scratch19 ; ECC0 and again ... ror a ; ECC2 ... A = (2*raw[i] + raw[i-1] + raw[i+1]) / 4 ldy scratch23 ; ECC3 Y = i again sta D_8896,y ; ECC5 write the blurred pressure back into the live ring at $8896+i dec scratch23 ; ECC8 next direction down bpl smoothThreatRing ; ECCA repeat for directions 7..0 ; ---------------------------------------------------------------------- ; Now look for the calmest 135-degree arc: three adjacent directions whose blurred pressures add up to ; the smallest total. The centre of that arc is the direction the comcen will run towards. ; ---------------------------------------------------------------------- lda #$FF ; ECCC $FF = no arc chosen yet, and as an arc sum 255 is worse than anything real sta scratch18 ; ECCE zp_18 = best (quietest) centre direction so far sta scratch19 ; ECD0 zp_19 = the best arc sum so far ldy #$07 ; ECD2 test the eight arcs starting with the one led by direction 7 ; ---------------------------------------------------------------------- ; findQuietestDirectionArc - For every leading direction y = 7..0 it adds the blurred pressures of y, ; y-1 and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle ; direction y-1. The result is the centre of the calmest 135-degree wedge around the reference point. ; Exact ties are decided by bit 7 of the game RNG state, so the trainer does not always break them the ; same way. ; In: $8896-$889D = blurred direction weights, zp_57 = game RNG state byte 0, zp_18/zp_19 preset to ; $FF ; Out: zp_18 = quietest centre direction 0-7, zp_19 = its arc sum; zp_1A = scratch for the candidate ; centre; A/X/Y clobbered ; Called from: nothing - loop head of rankThreatDirections, branched to from the DEY/BPL at $ECFE. ; (The XREF calls from $EC74/$EC7E belong to the modem-driver build that shares this address.) ; ---------------------------------------------------------------------- findQuietestDirectionArc: tya ; ECD4 A = the arc's leading direction tax ; ECD5 X indexes the first of the three samples lda D_8896,x ; ECD6 blurred pressure of direction y dex ; ECD9 step to y - 1, the middle of the arc bpl addArcMiddleDirection ; ECDA no wrap needed ldx #$07 ; ECDC y was 0, so the middle direction is 7 addArcMiddleDirection: stx scratch1A ; ECDE remember the middle direction - that is what the caller receives clc ; ECE0 clear carry before the add adc D_8896,x ; ECE1 + pressure of the middle direction dex ; ECE4 step to y - 2, the trailing direction bpl addArcTrailingDirection ; ECE5 no wrap needed L_ECE7: ldx #$07 ; ECE7 wrap y-2 round to direction 7 addArcTrailingDirection: clc ; ECE9 clear carry before the last add adc D_8896,x ; ECEA + pressure of the trailing direction; A = the arc total (8-bit, and it can wrap if the whole ring is saturated at $FF) L_ECED: cmp scratch19 ; ECED compare with the quietest arc found so far bcc rememberQuietestArc ; ECEF strictly quieter - take this arc bne nextArcCandidate ; ECF1 louder - keep the previous best L_ECF3: bit gameRngState0 ; ECF3 a tie: toss a coin on bit 7 of the game RNG state. zp_57 is not stepped here, so every tie inside one call falls the same way bmi nextArcCandidate ; ECF5 coin says keep the arc that was found first rememberQuietestArc: sta scratch19 ; ECF7 remember the new best arc sum ... ldx scratch1A ; ECF9 ... and its middle direction ... stx scratch18 ; ECFB ... as the escape direction to return nextArcCandidate: dey ; ECFD next leading direction bpl findQuietestDirectionArc; ECFE eight arcs in all ; ---------------------------------------------------------------------- ; Return the summary: A = threat level, X = deepest weapon-range penetration, Y = escape direction. ; Subtracting the unit count removes the +1 base weight each threatening unit contributed, so what is ; left is the 'extra' pressure from being close or inside somebody's weapon range. ; ---------------------------------------------------------------------- lda scratch1F ; ED00 total pressure accumulated over all threatening units sec ; ED02 no borrow sbc scratch20 ; ED03 minus one per threatening unit: strip the base weight each one added lsr a ; ED05 halve it - A is the threat level the planner keeps in $8962 ldy scratch18 ; ED06 Y = the quietest direction; callers add 4 mod 8 to get the direction the enemy is in ldx scratch21 ; ED08 X = the largest weapon-range surplus seen, 0 = nothing has this point in range ($8961) threatScanDone: rts ; ED0A shared exit; the 'nothing threatens at all' path at $EC7F jumps here with A=0, X=0, Y=8 ; ---------------------------------------------------------------------- ; runAiMissileTurn - The computer opponent's one 'weapon' decision per lock-step exchange. It first ; offers to launch the AI's own drone (launchAiDrone), then, if the human is steering a drone of his ; own and the AI still has missiles and is reloaded, it decides whether to shoot that drone down: the ; aim point is the drone's cell, a reluctance score is built from the range to the AI comcen, the ; comcen's stun state and energy and a missile-conservation rule, and the shot either kills the drone ; (command $92 plus the $90 burst) or lands scattered around it. Whatever happens it falls through ; into fireMissileAtSpottedUnit. ; In: $8966 action budget for this exchange (0 = free), $920D drone state flags, $92B1 AI missiles ; left, $92AE human drones left, $8970 reload countdown, $92F6/$92F7 drone camera window origin, ; $F6A3/$F707 AI comcen cell, $F9C3 comcen stun, $FA27 comcen energy, $920B/$896F drone heading, ; game RNG ; Out: may put command $92 and/or $90 col,row into the fabricated 'received' packet ($E020, length ; $E01E); $8970 reload timer, $8966 spent, $8968 += 2; ; zp_22/zp_23/zp_24/zp_27/zp_28/zp_29/zp_2B/zp_18 clobbered ; Called from: injectAiCommandsIntoPacket ($EA80), which itself runs once per exchange from the $E000 ; jump-table entry with X=1 ; The AI is allowed exactly one drone launch or missile shot per exchange: injectAiCommandsIntoPacket ; clears $8966 to 0 before calling, and each action decrements it to $FF. ; ---------------------------------------------------------------------- runAiMissileTurn: jsr launchAiDrone ; ED0B first chance of the exchange: send a drone up if one is due lda D_8966 ; ED0E $8966 = this exchange's action budget bmi skipAntiDroneShot ; ED11 the drone launch already used it - go straight to the ground-target path lda side1ComcenCol ; ED13 column of the AI's own comcen (unit 99) sta cellColB ; ED16 zp_27/zp_28 = point B of every cellDistance below: the comcen is what the AI defends L_ED18: lda side1ComcenRow ; ED18 row of the AI comcen sta cellRowB ; ED1B zp_28 = reference row bit droneControlFlags ; ED1D $920D bit 6 = the human is steering his own drone right now bvs considerAntiDroneShot ; ED20 he is - think about shooting it down skipAntiDroneShot: jmp fireMissileAtSpottedUnit; ED22 no drone to shoot at, so look for an ordinary target considerAntiDroneShot: lda opponentMissilesLeft ; ED25 $92B1 = missiles the computer opponent has left beq skipAntiDroneShot ; ED28 out of missiles lda D_8970 ; ED2A $8970 = reload countdown; $FF (negative) means ready to fire L_ED2D: bpl skipAntiDroneShot ; ED2D still reloading ; ---------------------------------------------------------------------- ; Aim at the human's drone. $92F6/$92F7 are the top-left corner of his 14x12 drone camera window and ; the drone always sits in the middle of it, hence the +6/+5. ; ---------------------------------------------------------------------- lda #$00 ; ED2F 0 = 'the comcen is undamaged' until proved otherwise L_ED31: sta scratch2B ; ED31 zp_2B = comcen-damaged flag lda droneViewOriginCol ; ED33 left edge of the drone camera window clc ; ED36 clear carry for the add adc #$06 ; ED37 the drone sits 6 columns into the window sta cellColA ; ED39 zp_22 = aim column (point A) lda droneViewOriginRow ; ED3B top edge of the drone camera window clc ; ED3E clear carry adc #$05 ; ED3F the drone sits 5 rows into the window sta cellRowA ; ED41 zp_24 = aim row L_ED43: jsr cellDistance ; ED43 A = range in cells (0-16) from the AI comcen to the drone sta scratch23 ; ED46 zp_23 = reluctance to fire, starting at that range lsr a ; ED48 half the range ... sta scanListIndex ; ED49 ... is what the reload timer will be set to if the shot goes off lda side1ComcenStun ; ED4B stun byte of the AI comcen (unit 99) and #$0F ; ED4E bits 0-3 = the stun counter beq checkComcenEnergy ; ED50 not stunned - check the energy instead dec scratch2B ; ED52 zp_2B = $FF: remember that the comcen is stunned - it costs another point of reluctance at $ED69 L_ED54: cmp #$0E ; ED54 stun counter 14 or 15 = the comcen is effectively out of action bcs skipAntiDroneShot ; ED56 too badly stunned to shoot at a drone bcc bumpReluctance ; ED58 always taken (the CMP above set carry only for >= $0E): skip the energy test checkComcenEnergy: lda side1ComcenEnergy ; ED5A energy byte of the AI comcen and #$03 ; ED5D low two bits of the 0-50 energy value bne applyStunPenalty ; ED5F not a multiple of 4 - no extra hesitation bumpReluctance: inc scratch23 ; ED61 energy divisible by 4: +1 reluctance, a cheap energy-driven hesitation applyStunPenalty: lda scratch23 ; ED63 A = the reluctance BEFORE the stun bump below bit scratch2B ; ED65 was the comcen stunned? (zp_2B = $FF if so) bpl testCloseShot ; ED67 no inc scratch23 ; ED69 stunned: +1 reluctance. A keeps the old value, so the test at $ED6B still uses the unbumped score testCloseShot: cmp #$03 ; ED6B reluctance under 3 - a close shot from a healthy comcen: fire without the conservation test bcc fireAntiDroneMissile ; ED6D take the shot testReluctanceLimit: lda scratch23 ; ED6F reload the (possibly bumped) reluctance cmp #$05 ; ED71 5 or more = far away and/or a hurt comcen ... bcs skipAntiDroneShot ; ED73 ... not worth a missile lda dronesLeft ; ED75 $92AE = drones the human still has in stock L_ED78: asl a ; ED78 x2 ... asl a ; ED79 ... x4: four missiles are reserved for every future drone sta scratch18 ; ED7A zp_18 = the reserve lda opponentMissilesLeft ; ED7C missiles the AI owns sec ; ED7F no borrow wanted sbc scratch18 ; ED80 missiles above the reserve bcs clampMissileSurplus ; ED82 positive surplus lda #$00 ; ED84 below the reserve - treat the surplus as 0 clampMissileSurplus: clc ; ED86 clear carry adc #$01 ; ED87 surplus + 1 cmp scratch23 ; ED89 compare with the reluctance bcc fireMissileAtSpottedUnit; ED8B too few spare missiles for a shot this reluctant - save it for later ; ---------------------------------------------------------------------- ; The shot goes off. Whether it actually hits is decided by one random byte against a threshold that ; grows with the reluctance and jumps by $40 when the drone has changed heading since the last shot, ; so a jinking drone is much harder to hit. ; ---------------------------------------------------------------------- fireAntiDroneMissile: lda scanListIndex ; ED8D half the range, computed at $ED48 sta D_8970 ; ED8F start the reload countdown inc scratch23 ; ED92 reluctance + 1 so that even a perfect shot can miss lda scratch23 ; ED94 A = reluctance + 1 asl a ; ED96 x2 ... asl a ; ED97 x4 ... asl a ; ED98 x8 ... asl a ; ED99 x16 ... asl a ; ED9A x32: the miss threshold is (reluctance+1) * 32 ldx droneHeading ; ED9B $920B = the heading of the drone right now cpx D_896F ; ED9E $896F = the heading it had at the last shot beq storeMissThreshold ; EDA1 unchanged - the drone is flying straight clc ; EDA3 clear carry adc #$40 ; EDA4 it jinked: +$40 makes a hit far less likely storeMissThreshold: sta scratch18 ; EDA6 zp_18 = miss threshold jsr nextGameRandom ; EDA8 step the lock-step game RNG (both machines would get the same byte) cmp scratch18 ; EDAB random byte against the threshold bcs droneShotDownCleanly ; EDAD random >= threshold: a clean hit on the drone lsr scratch23 ; EDAF a miss: scatter radius = reluctance / 2 ... inc scratch23 ; EDB1 ... + 1, so it is at least one cell off lda scratch23 ; EDB3 A = scatter radius jsr randomlySignValue ; EDB5 randomly plus or minus clc ; EDB8 clear carry adc cellColA ; EDB9 shift the aim column sta cellColA ; EDBB zp_22 = the missed column lda scratch23 ; EDBD A = scatter radius again L_EDBF: jsr randomlySignValue ; EDBF randomly plus or minus clc ; EDC2 clear carry adc cellRowA ; EDC3 shift the aim row sta cellRowA ; EDC5 zp_24 = the missed row jmp queueScatteredStrike ; EDC7 queue the strike at the scattered cell droneShotDownCleanly: lda #$92 ; EDCA command $92 = DRONE SHOT DOWN (handler cmdDroneShotDown $538B, clears $920D bits 6-7) sta rxPacketBuffer ; EDCC it is the first byte of the packet - $E01E is still 0 whenever this path is reached inc rxPacketLength ; EDCF packet is now 1 byte long queueScatteredStrike: jsr queueMissileStrikeCommand; EDD2 append the $90 strike so the human also sees the missile burst on the drone's cell ; ---------------------------------------------------------------------- ; fireMissileAtSpottedUnit - Second half of the missile turn, reached whenever no anti-drone shot was ; taken. It fires a comcen missile at the unit stored in $8972 (the last human unit seen shooting at ; the AI comcen) or, when there is none or it is dead, at the human comcen itself while that comcen is ; visible. The target must sit on the AI comcen's 21x17 radar grid; the reload timer is then set to ; half the range and the $90 strike is queued. The tail remembers the human drone's heading for the ; jink test and ticks the reload timer down. ; In: $920D bit 6, $92AE human drones left, $92B1 AI missiles, $8970 reload countdown, $8966 action ; budget, $8972 current missile target, $F76C unit type/dead bits, $F9F5 human comcen ; energy/visibility, $920B drone heading, zp_27/zp_28 = AI comcen cell ; Out: $8972 ($FF when the target was dropped), $8970, $896F, and via queueMissileStrikeCommand the ; bytes $90 col,row in the fabricated packet ; Called from: JMP at $ED22 and BCC at $ED8B in runAiMissileTurn, plus the fall-through from $EDD2 ; Missile conservation: while the human still owns drones the AI keeps two missiles in hand for each ; of them, so it will not empty the rack on ground targets and be defenceless against the next drone. ; ---------------------------------------------------------------------- fireMissileAtSpottedUnit: bit droneControlFlags ; EDD5 $920D bit 6 = the human is flying a drone bvs forgetMissileTarget ; EDD8 he is - the anti-drone code above owns this exchange, so no ground shot lda dronesLeft ; EDDA drones the human still has asl a ; EDDD two missiles reserved per drone sta scratch18 ; EDDE zp_18 = the reserve lda opponentMissilesLeft ; EDE0 missiles the AI owns beq forgetMissileTarget ; EDE3 none at all sec ; EDE5 no borrow wanted sbc scratch18 ; EDE6 missiles minus the reserve bcc forgetMissileTarget ; EDE8 fewer than the reserve - hold fire beq forgetMissileTarget ; EDEA exactly the reserve - hold fire as well lda D_8970 ; EDEC reload countdown bpl missileTurnTail ; EDEF still reloading (>= 0): skip to the tail, which ticks it down lda D_8966 ; EDF1 this exchange's action budget bmi forgetMissileTarget ; EDF4 already spent on the drone launch - drop the target and leave ldx D_8972 ; EDF6 $8972 = human unit last seen shooting at the AI comcen, $FF = none known bmi targetEnemyComcenWithMissile; EDF9 none - try the human comcen instead lda unitTypeTable,x ; EDFB type byte of that unit bpl checkTargetOnRadar ; EDFE still alive (bit 7 clear) - shoot at it ; ---------------------------------------------------------------------- ; targetEnemyComcenWithMissile - Fall-through tail of fireMissileAtSpottedUnit: when the AI has no ; remembered attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49) ; instead, but only while that comcen is currently visible. ; In: $F9F5 = energy/visibility byte of unit 49 (bit 7 = visible right now) ; Out: X = $31 when the comcen may be shot at, otherwise branches to $EE19 which clears the target ; Called from: BMI at $EDF9 and fall-through from $EDFE. XREF lists 'call:E0A5', but that JSR only ; ever runs while the track 29 drone-flight overlay occupies $EE00-$EFFF, so it never enters this ; code ; ---------------------------------------------------------------------- targetEnemyComcenWithMissile: lda side0ComcenEnergy ; EE00 energy/visibility byte of the human comcen bpl forgetMissileTarget ; EE03 bit 7 clear = not visible; nothing to shoot at ldx #$31 ; EE05 $31 = 49 = the human player's COMCEN checkTargetOnRadar: jsr isUnitOnAiRadar ; EE07 put the target cell in zp_22/zp_24 and test it against the AI comcen's radar grid bcs forgetMissileTarget ; EE0A outside the 21x17 grid - the AI cannot see it, so no shot jsr cellDistance ; EE0C range from the AI comcen (zp_27/zp_28) to the target L_EE0F: lsr a ; EE0F half of it ... sta D_8970 ; EE10 ... becomes the reload countdown: long shots take longer to repeat jsr queueMissileStrikeCommand; EE13 append the $90 col,row strike to the fabricated packet jmp missileTurnTail ; EE16 keep $8972 so the same unit stays the target next exchange forgetMissileTarget: lda #$FF ; EE19 $FF = no target sta D_8972 ; EE1B forget the remembered attacker ; ---------------------------------------------------------------------- ; Shared tail: remember the human drone's heading so the next anti-drone shot can tell whether it ; jinked, and tick the reload countdown down towards $FF = ready. ; ---------------------------------------------------------------------- missileTurnTail: lda droneHeading ; EE1E current heading of the human's drone sta D_896F ; EE21 $896F is compared against it at $ED9E next time lda D_8970 ; EE24 reload countdown bmi missileTurnDone ; EE27 already at $FF: the launcher is ready, leave it alone dec D_8970 ; EE29 one exchange closer to being reloaded missileTurnDone: rts ; EE2C back to injectAiCommandsIntoPacket ; ---------------------------------------------------------------------- ; queueMissileStrikeCommand - Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler ; cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to ; have received from the opponent. Both coordinates are pre-mirrored with mirrorMapCoordinate (39-v) ; because the command dispatcher un-mirrors every parameter that arrives from a remote player. ; In: zp_22 = strike column, zp_24 = strike row, $E01E = current packet length ; Out: three bytes appended at $E020+, $E01E advanced by 3, $8966 spent (decremented to $FF), $8968 ; charged two of the round's command slots; X clobbered ; Called from: $EDD2 (anti-drone shot) and $EE13 (shot at a spotted unit) ; ---------------------------------------------------------------------- queueMissileStrikeCommand: ldx rxPacketLength ; EE2D X = current length of the fabricated incoming packet lda #$90 ; EE30 $90 = MISSILE STRIKE sta rxPacketBuffer,x ; EE32 command byte into the received-packet buffer inx ; EE35 next byte lda cellColA ; EE36 strike column jsr mirrorMapCoordinate ; EE38 $3FA3: 39 - column, undoing the mirror the engine will apply to a remote command sta rxPacketBuffer,x ; EE3B first parameter L_EE3E: inx ; EE3E next byte lda cellRowA ; EE3F strike row L_EE41: jsr mirrorMapCoordinate ; EE41 mirror it as well sta rxPacketBuffer,x ; EE44 second parameter inx ; EE47 past the last byte stx rxPacketLength ; EE48 packet is three bytes longer dec D_8966 ; EE4B this exchange's single action is now spent inc D_8968 ; EE4E charge the first of the two command slots a missile costs inc D_8968 ; EE51 and the second: a missile counts double against the four commands a round allows L_EE54: rts ; EE54 done ; ---------------------------------------------------------------------- ; randomlySignValue - Returns A or -A with equal probability, using bit 0 of the lock-step game RNG. ; Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss ; the human's drone. ; In: A = magnitude ; Out: A = zp_18 = +A or -A (two's complement); the game RNG has been stepped ; Called from: $EDB5 (column offset) and $EDBF (row offset) ; ---------------------------------------------------------------------- randomlySignValue: sta scratch18 ; EE55 keep the magnitude jsr nextGameRandom ; EE57 one random byte from the lock-step RNG and #$01 ; EE5A just its lowest bit beq returnSignedValue ; EE5C 0 - leave the magnitude positive lda scratch18 ; EE5E 1 - negate it: two's complement ... eor #$FF ; EE60 ... invert ... clc ; EE62 clear carry for the +1 adc #$01 ; EE63 ... and add one sta scratch18 ; EE65 zp_18 = -magnitude returnSignedValue: lda scratch18 ; EE67 return the signed value rts ; EE69 back to the missile scatter ; ---------------------------------------------------------------------- ; isUnitOnAiRadar - Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17 ; radar grid centred on the computer opponent's own COMCEN. Because mapCellToRadarGrid centres the ; grid on comcenUnit ($9236), this routine swaps that variable to $63 (unit 99, side 1's comcen) for ; the test and puts it back to $31 (unit 49) afterwards - the unconditional $31 is proof that this ; module always assumes the human is side 0. ; In: X = unit index, $F640/$F6A4 = unit column/row arrays ; Out: zp_22 = column, zp_24 = row of that unit (point A for the next cellDistance), C = 0 when the ; cell is on the AI's radar grid and C = 1 when it is off it; $9236 left at $31; X and Y clobbered ; Called from: $EE07 (missile targeting) and $EEFA (drone targeting in THE BOMB). Note it calls into ; the $6F00 comcen-screens overlay, so overlay B must be the resident one ; ---------------------------------------------------------------------- isUnitOnAiRadar: lda unitColTable,x ; EE6A map column of unit X sta cellColA ; EE6D zp_22 = point A column, also the strike coordinate if a missile follows lda unitRowTable,x ; EE6F map row of unit X L_EE72: sta cellRowA ; EE72 zp_24 = point A row tay ; EE74 Y = row, as mapCellToRadarGrid wants it ldx cellColA ; EE75 X = column L_EE77: lda #$63 ; EE77 $63 = 99 = side 1's COMCEN sta comcenUnit ; EE79 pretend the local comcen is the AI's, so the radar grid is centred on it jsr mapCellToRadarGrid ; EE7C $7A13 in the comcen-screens overlay: C=0 when the cell is inside the 21 x 17 grid lda #$31 ; EE7F $31 = 49 = the human player's COMCEN sta comcenUnit ; EE81 restore comcenUnit - the rest of the game expects the local player's comcen there rts ; EE84 carry from the grid test survives both stores ; ---------------------------------------------------------------------- ; launchAiDrone - Sends the computer opponent's drone up when one is due. It writes the launch ; straight into the packet the game is about to 'receive': $97 DRONE LAUNCH followed by a $8D DRONE ; MOVE that places the drone on the AI comcen and points it at the chosen target unit. The AI keeps ; its own copy of the drone position in $8974/$8975 and a 100-step fuel counter in $8973; $8977 = 3 ; holds the next launch off for three exchanges. ; In: $8977 launch delay (must be 0), $8976 target unit (>= 0), $920D bit 7 (no drone of the human's ; in the air), $F6A3/$F707 AI comcen cell, $F640/$F6A4 target cell ; Out: $E020 = $97, $E021-$E024 = $8D heading,col,row, $E01E = 5; $8974/$8975 launch cell, $8973 = $64 ; fuel, $8977 = 3, $896F heading, $8966 spent; zp_92-zp_95 line-stepper state clobbered ; Called from: $ED0B, the first thing runAiMissileTurn does each exchange ; Three gates before a launch: the delay must have run out, a target must have been picked by ; chooseAiDroneTarget, and no drone may be in the air already - the engine tracks only one at a time. ; ---------------------------------------------------------------------- launchAiDrone: lda D_8977 ; EE85 $8977 = exchanges to wait before the next launch ($FF also means the drone-flight overlay is resident) bne noDroneLaunch ; EE88 not due yet lda D_8976 ; EE8A $8976 = the unit the drone should be sent at bmi noDroneLaunch ; EE8D bit 7 set = no target chosen lda droneControlFlags ; EE8F $920D drone state flags bmi noDroneLaunch ; EE92 bit 7 = a drone flight is already in progress - wait for it to end dec D_8966 ; EE94 spend this exchange's single action lda #$97 ; EE97 $97 = DRONE LAUNCH (handler cmdDroneLaunch $53A3: 'DRONE ALERT!' and one drone off the opponent's stock) sta rxPacketBuffer ; EE99 first byte of the fabricated packet lda #$8D ; EE9C $8D = DRONE MOVE, 4 bytes: heading, column, row sta rxPacketArg1 ; EE9E second byte L_EEA1: ldx side1ComcenCol ; EEA1 the drone starts over the AI's own comcen stx D_8974 ; EEA4 $8974 = the AI's private copy of the drone column stx lineCurCol ; EEA7 zp_93 = line-stepper start column ldy side1ComcenRow ; EEA9 comcen row sty D_8975 ; EEAC $8975 = drone row L_EEAF: sty lineCurRow ; EEAF zp_92 = line-stepper start row jsr mirrorBlockCornerForOpponent; EEB1 add one to each coordinate and mirror: exactly undoes the mirror-then-decrement the engine applies to a remote $8D (the drone is a 2x2 object) stx rxPacketArg3 ; EEB4 column parameter of the $8D sty rxPacketArg4 ; EEB7 row parameter ldy D_8976 ; EEBA the chosen target unit lda unitColTable,y ; EEBD its column ... sta lineEndCol ; EEC0 ... becomes the line-stepper target column lda unitRowTable,y ; EEC2 its row ... sta lineEndRow ; EEC5 ... the target row jsr getStepDirection ; EEC7 one Bresenham step towards the target: Y = compass direction 0-7, A = that direction's row delta sta D_896F ; EECA remember the heading for the AI's own bookkeeping sta rxPacketArg2 ; EECD heading parameter of the $8D - but this is A, the row delta (-1/0/+1), not the direction index in Y. cmdDroneMove draws sprite shape (heading+4) AND 7 = 3, 4 or 5, so the AI's drone is always drawn flying south towards the player. Probably a slip for TYA L_EED0: lda #$05 ; EED0 5 bytes: one $97 plus four for the $8D sta rxPacketLength ; EED2 hand the packet to the engine as if the opponent had sent it lda #$64 ; EED5 $64 = 100 steps of fuel, one per exchange, matching the human's drone tank sta D_8973 ; EED7 $8973 = the AI's own fuel counter lda #$03 ; EEDA wait three exchanges before considering another launch sta D_8977 ; EEDC $8977 also stays non-negative, marking the native $EE00 tail as the resident one noDroneLaunch: rts ; EEDF nothing to launch ; ---------------------------------------------------------------------- ; chooseAiDroneTarget - Picks what the computer opponent should send its next drone at and leaves the ; unit index in $8976. It bails out when the AI has no drones left or a target is still pending. In ; game type 2 (THE BOMB) the only worthwhile target is the human COMCEN, as soon as it is visible or ; shows on the AI's radar. Otherwise, once the comcen is visible, it adds up the strength of every ; group that has reached it (double for a group with a clear run at it) and, at a total of 8, commits ; to an all-out attack: every group is flagged committed, all 46 combat units are made due for new ; orders, the drone is held back 21 exchanges and then aimed at the comcen. If the comcen is not ; visible it falls into pickDroneTargetAlongThreatAxis. ; In: $92AF drones the AI has left, $8976, $0BA3 bits 0-2 = game type, $F9F5 human comcen visibility, ; $8902 group strengths, $893E group flags, $8953 highest group id, $8954 group count, $91CB game ; clock ; Out: $8976 = target unit ($31 = the human comcen), $893E bit 6 set for every group, $8800-$882D ; stamped with the clock, $8977 = $15; zp_18, zp_19 clobbered ; Called from: planTrainerRound ($E1FF), once per round and only while the AI is in attack mode ($8963 ; = 0) ; ---------------------------------------------------------------------- chooseAiDroneTarget: lda opponentDronesLeft ; EEE0 $92AF = drones the computer opponent still owns beq droneTargetDone ; EEE3 none left - it will never launch again lda D_8976 ; EEE5 $8976 = pending drone target bpl droneTargetDone ; EEE8 bit 7 clear = a target is already chosen, so leave it alone lda gameTypeOptions ; EEEA game options byte L_EEED: and #$07 ; EEED bits 0-2 = the game type cmp #$02 ; EEEF type 2 = THE BOMB, where only the comcen counts bne chooseTargetByGroupPressure; EEF1 any other game type lda side0ComcenEnergy ; EEF3 energy/visibility of the human comcen bmi targetHumanComcen ; EEF6 bit 7 = visible right now: send the drone at it ldx #$31 ; EEF8 $31 = 49 = the human comcen jsr isUnitOnAiRadar ; EEFA is it at least on the AI comcen's radar grid? L_EEFD: bcs droneTargetDone ; EEFD not on the radar either - no drone target this round bcc targetHumanComcen ; EEFF on the radar - target it ; ---------------------------------------------------------------------- ; Every other game type: measure how much of the AI's army has actually converged on the human comcen. ; $893E bit 5 = this group has a clear line to the comcen (set at $E3C3), bit 6 = one of its members ; has ; come within 8 cells of it (set at $EB1F). ; ---------------------------------------------------------------------- chooseTargetByGroupPressure: lda side0ComcenEnergy ; EF01 comcen visibility again bpl pickDroneTargetAlongThreatAxis; EF04 not visible - go and look for a worthwhile cluster of units instead ldy D_8954 ; EF06 $8954 = number of AI groups ... dey ; EF09 ... so the last group index is one less lda #$00 ; EF0A zp_18 = 0 sta scratch18 ; EF0C running total of converging strength sumConvergingGroupsLoop: lda D_8902,y ; EF0E $8902 = number of units in group Y sta scratch19 ; EF11 zp_19 = this group's weight ldx D_893E,y ; EF13 group flags txa ; EF16 test them ... and #$20 ; EF17 ... bit 5 = the group has a clear run at the human comcen beq testGroupContactFlag ; EF19 no clear run - try the contact bit asl scratch19 ; EF1B a clear run counts double jmp addGroupWeight ; EF1D add it in testGroupContactFlag: txa ; EF20 flags again and #$40 ; EF21 bit 6 = a member of this group has reached the comcen L_EF23: beq nextGroupInSum ; EF23 neither flag - this group does not count addGroupWeight: lda scratch19 ; EF25 the group's weight clc ; EF27 clear carry L_EF28: adc scratch18 ; EF28 add to the running total sta scratch18 ; EF2A keep the total nextGroupInSum: dey ; EF2C next group down bpl sumConvergingGroupsLoop ; EF2D until group 0 has been counted lda scratch18 ; EF2F converging strength cmp #$08 ; EF31 eight units' worth is the trigger for the all-out attack bcc pickDroneTargetAlongThreatAxis; EF33 not enough yet - go back to hunting clusters ; ---------------------------------------------------------------------- ; All-out attack: flag every group as committed, then stamp the order clock of all 46 mobile AI units ; with the current game clock. scanTrainerUnitsForOrders re-plans a unit whose stamp has caught up ; with the down-counting clock ($E2E3), so this makes the whole army replan on the next scan. ; ---------------------------------------------------------------------- commitAllGroups: ldy D_8953 ; EF35 $8953 = highest group id in use commitGroupLoop: lda D_893E,y ; EF38 flags of group Y ora #$40 ; EF3B bit 6 = committed to the attack sta D_893E,y ; EF3D store them back dey ; EF40 next group down bpl commitGroupLoop ; EF41 until group 0 is done ldy #$2D ; EF43 $2D = 45: units 50..95, the AI's 28 grunts, 12 riders and 6 boomers (its 3 spies and comcen are left alone) lda gameClock ; EF45 $91CB = the down-counting game clock stampOrderClockLoop: sta msgGetOpponentOnPhoneAnd,y; EF48 $8800+ordinal = the round this unit may next be given orders dey ; EF4B next AI unit bpl stampOrderClockLoop ; EF4C all 46 of them lda #$15 ; EF4E $15 = 21 exchanges ... sta D_8977 ; EF50 ... before the drone follows the ground attack in targetHumanComcen: lda #$31 ; EF53 $31 = 49 = the human COMCEN sta D_8976 ; EF55 that is the drone's target droneTargetDone: rts ; EF58 no target this round ; ---------------------------------------------------------------------- ; pickDroneTargetAlongThreatAxis - Tail of chooseAiDroneTarget for the case where the human comcen is ; no use as a drone target. It scans the threat around the AI's current objective (the escorted ; group's centre, or the AI comcen when no group is being escorted), and if the pressure reaches that ; group's own strength and something really has the point in weapon range, it walks a ray in the ; direction the enemy is coming from ((quietest+4) AND 7) at radii 4 to 14, scoring each 2x2 block of ; cells with scoreTargetBlock. The highest-numbered human unit in the best-scoring block becomes the ; drone target, and the acceptance threshold $8979 is cut to three quarters so the AI grows less ; choosy as the game goes on. ; In: $895D escorted group ($FF = none), $88EA/$88F6 group centre, $8902 group strength, $8979 ; block-score threshold (set per opening play by the trainer setup overlay: $FF, $FF, 8, $0C, ; $0C), $F6A3/$F707 AI comcen cell ; Out: $8976 = drone target unit (untouched when nothing scored), $8979 scaled by 3/4, $8956 = the ; threshold used, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius, ; zp_18/zp_19/zp_22/zp_24/zp_27/zp_28 and zp_94/zp_95 clobbered ; Called from: BPL at $EF04 and BCC at $EF33 inside chooseAiDroneTarget - it is that routine's ; fall-through tail, not a separate entry ; Choose the point to scan around and the pressure it takes to make the AI bother: the group the ; comcen escorts and its member count, or the AI comcen itself with a fixed threshold of 3. ; ---------------------------------------------------------------------- pickDroneTargetAlongThreatAxis: ldx side1ComcenCol ; EF59 default scan point: the AI's own comcen column ldy side1ComcenRow ; EF5C and row lda #$03 ; EF5F default threat threshold sta D_8956 ; EF61 $8956 doubles as the trainer's 'how much pressure is worth reacting to' value lda D_895D ; EF64 $895D = the group the AI comcen escorts bmi storeScanReferencePoint ; EF67 bit 7 = no group is being escorted - keep the comcen defaults tay ; EF69 Y = that group lda D_8902,y ; EF6A $8902 = its member count L_EF6D: sta D_8956 ; EF6D a big group needs more pressure before its drone is called in ldx D_88EA,y ; EF70 the group's centre column ... lda D_88F6,y ; EF73 ... and centre row ... tay ; EF76 ... become the scan point storeScanReferencePoint: stx cellColB ; EF77 zp_27 = reference column for the threat scan sty cellRowB ; EF79 zp_28 = reference row (the scan routine is written for the comcen but works on any point) jsr assessThreatAroundAiComcen; EF7B A = threat level, X = deepest weapon-range penetration, Y = quietest compass direction cmp D_8956 ; EF7E compare the measured pressure with the threshold bcc pickDroneTargetDone ; EF81 too quiet around the objective - no drone target txa ; EF83 X = the range surplus beq pickDroneTargetDone ; EF84 nothing actually has the objective in weapon range - not worth a drone either ; ---------------------------------------------------------------------- ; Walk a ray from the objective straight into the enemy - the direction opposite the quietest one - ; and score every 2x2 block of cells along it from 4 to 14 cells out. scoreTargetBlock keeps the best ; score in zp_1C and the strongest human unit of that block in zp_1D. ; ---------------------------------------------------------------------- tya ; EF86 A = the quietest direction clc ; EF87 clear carry adc #$04 ; EF88 +4 of the 8 compass points = the exact opposite ... and #$07 ; EF8A ... modulo 8: the direction the enemy is pressing from sta scratch1E ; EF8C zp_1E = the ray's direction lda #$04 ; EF8E start 4 cells out from the objective sta scratch1F ; EF90 zp_1F = the current radius lda D_8979 ; EF92 $8979 = the score a block has to beat sta scratch1C ; EF95 zp_1C = the score to beat. A value with bit 7 set (the $FF that plays 0 and 1 start with) makes scoreTargetBlock accept any block whose score is not negative lda #$FF ; EF97 $FF = no unit picked yet sta scratch1D ; EF99 zp_1D = the unit the best block yields, none yet scanRayLoop: ldx scratch1E ; EF9B direction of the ray ldy scratch1F ; EF9D how far out jsr offsetPointInDirection ; EF9F zp_18/zp_19 = the cell zp_1F cells along the ray, clamped to the map (offsetPointInDirection patches its own LDX operand at $E67C+1 with the direction index); C=1 means it fell off the 40x40 map bcs pickBestScoredBlock ; EFA2 the ray ran off the 40x40 map - stop scanning ldx scratch18 ; EFA4 X = candidate column ldy scratch19 ; EFA6 Y = candidate row jsr scoreTargetBlock ; EFA8 score the 2x2 block at that corner - grunt 1, rider 2, boomer 4, spy 1, comcen 4, negative for the AI's own units. scoreTargetBlock patches $E9E3+1/$E9E7+1 with X and Y so a winning block can drop its corner into zp_95/zp_94; nothing here uses that copy inc scratch1F ; EFAB one cell further out lda scratch1F ; EFAD current radius cmp #$0F ; EFAF stop after 14 cells bcc scanRayLoop ; EFB1 keep walking the ray pickBestScoredBlock: lda scratch1D ; EFB3 zp_1D = highest-numbered human unit standing in the best block. Since the roster runs 0-27 grunts, 28-39 riders, 40-45 boomers, 46-48 spies, 49 comcen, the highest index is also the most valuable unit there bmi pickDroneTargetDone ; EFB5 still $FF - no block beat the threshold, so no drone target this round sta D_8976 ; EFB7 it becomes the drone's target ($8976); launchAiDrone aims at it on a later exchange ; ---------------------------------------------------------------------- ; Having spent a target, drop the acceptance threshold to three quarters, so later drones settle for ; smaller clusters. Plays 0 and 1 start at $FF, which counts as 'accept anything'; after three ; targets the threshold lands on $6C = 108, which no 2x2 block (maximum 16) can ever reach again. ; ---------------------------------------------------------------------- lda D_8979 ; EFBA the current threshold lsr a ; EFBD /2 ... lsr a ; EFBE ... /4 sta scratch18 ; EFBF zp_18 = a quarter of it lda D_8979 ; EFC1 the threshold again sec ; EFC4 no borrow wanted sbc scratch18 ; EFC5 threshold minus a quarter = 3/4 of it bcc pickDroneTargetDone ; EFC7 never taken: A - A/4 cannot borrow, so this is a redundant guard sta D_8979 ; EFC9 store the reduced threshold pickDroneTargetDone: rts ; EFCC back to the planner ; strayTextTail - The last three characters, 'CE' and '.' with bit 7 set, of the string 'INSERT DATA ; DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the modem-driver build of this overlay slot. ; Here the trainer's code runs up to $EFCC, so only this fragment survives. ; In: - ; Out: - ; Called from: nothing; unreferenced ; $EFCD-$EFFF is not code or data of the trainer: it is the tail of the disk sector, and every build ; of ; the $EC00/$EE00 overlay slot carries the same mastering-tool leftovers here. Nothing in the ; disassembly reads any of it. strayTextTail: .byte "CE",'.'|$80 ; EFCD 'CE' plus '.' with bit 7 set - the tail of a string that belongs to another build ; msgDatagamecustomOff2: text, 18 bytes. 'DATAGAMECUSTOM OFF' with bit 7 on the final F. ; Byte-identical to the same block in ovl_E000_T34, ovl_EC00_T29 and ovl_EE00_T29 - mastering-tool ; filler at the tail of every $EC00/$EE00 overlay build. No reference anywhere in the disassembly. msgDatagamecustomOff2: .byte "DATA" ; EFD0 'DATA' - start of the shared filler block found at the end of every $EC00/$EE00 overlay .byte "GAME" ; EFD4 'GAME' .byte "CUSTOM OF",'F'|$80 ; EFD8 'CUSTOM OFF', bit 7 set on the last F to terminate the string ; msgDestroyOff2: text, 11 bytes. 'DESTROY OFF' with bit 7 on the final F. Same shared filler block; ; unreferenced. msgDestroyOff2: .byte "DESTROY OF",'F'|$80 ; EFE2 'DESTROY OFF', bit 7 on the final F; same filler block, never referenced ; msgAccepted2: text, 8 bytes. 'ACCEPTED' with bit 7 on the final D. Same shared filler block; ; unreferenced. msgAccepted2: .byte "ACCEPTE",'D'|$80 ; EFED 'ACCEPTED', bit 7 on the final D; same filler block, never referenced ; Sector padding: $C0,$DF then $00,$FF to the end of the page. Identical in all four builds of this ; overlay slot apart from the very last byte, and read by nothing. overlayTailFiller: .byte $C0,$DF,$00,$FF,$00,$FF,$00,$FF; EFF5 ........ $C0,$DF then alternating $00,$FF padding to the end of the page .byte $00,$FF,$FF ; EFFD ... last three padding bytes; $EFFF is $FF here and in the track 29 variants, $00 in the modem driver