{
 "chunk": "game_trainerAiE000_E000",
 "dataTypes": {},
 "notes": {
  "E000": {
   "line": "slot 0: per-frame poll / packet request, X selects the function",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E003": {
   "line": "slot 1: open (X=0) or close (X>0) the link flags",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E006": {
   "line": "slot 2: fetch a received chat byte - the trainer never has one",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E009": {
   "line": "slot 3: send a chat byte - the trainer throws it away",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E00C": {
   "line": "slot 4: per-IRQ service tick - nothing to service without a UART",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E00F": {
   "block": [
    "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."
   ],
   "line": "$60 = RTS: harmless body for any vector pointed here",
   "unit": "game/trainerAiE000"
  },
  "E010": {
   "line": "$40 = RTI: the module's default NMI handler, target of the initial $E031/$E032 = $E010",
   "unit": "game/trainerAiE000"
  },
  "E011": {
   "line": "answer/originate flag: waitForAnswerOriginateKey ($0CDE) stores 0 for 'A' and 1 for 'O'. Modem-only; nothing in the trainer build reads it",
   "unit": "game/trainerAiE000"
  },
  "E012": {
   "line": "operand bytes $E013/$E014 are the keyboard vector, patched to scanKeyboard $0DB7 by $0F4B/$0F50",
   "routine": [
    "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."
   ],
   "unit": "game/trainerAiE000"
  },
  "E015": {
   "line": "slot 5: read one key through the patchable vector above",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E018": {
   "block": [
    "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."
   ],
   "line": "five unused $00 bytes where the modem build has its sixth jump-table slot",
   "unit": "game/trainerAiE000"
  },
  "E01D": {
   "block": [
    "$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."
   ],
   "line": "exchange flags: bit 7 = opponent packet received, bit 6 = exchange complete, bits 0/5 = send requested / in progress. handleCommRequest pins it at $C0",
   "unit": "game/trainerAiE000"
  },
  "E01E": {
   "line": "length 0-7 of the 'received' packet the trainer fabricates in $E020",
   "unit": "game/trainerAiE000"
  },
  "E01F": {
   "line": "length of the outgoing packet the game built in $E027; the trainer never reads the bytes",
   "unit": "game/trainerAiE000"
  },
  "E020": {
   "line": "received-packet buffer $E020-$E026, byte 0 = the command byte the engine will execute",
   "unit": "game/trainerAiE000"
  },
  "E021": {
   "line": "received packet byte 1 (first command argument)",
   "unit": "game/trainerAiE000"
  },
  "E022": {
   "line": "received packet byte 2",
   "unit": "game/trainerAiE000"
  },
  "E023": {
   "line": "received packet byte 3",
   "unit": "game/trainerAiE000"
  },
  "E024": {
   "line": "received packet bytes 4-6, the tail of the 7-byte buffer",
   "unit": "game/trainerAiE000"
  },
  "E027": {
   "line": "outgoing-packet buffer $E027-$E02D, filled by sendOutgoingPacket ($568C); discarded here",
   "unit": "game/trainerAiE000"
  },
  "E028": {
   "line": "outgoing packet byte 1",
   "unit": "game/trainerAiE000"
  },
  "E029": {
   "line": "outgoing packet byte 2",
   "unit": "game/trainerAiE000"
  },
  "E02A": {
   "line": "outgoing packet bytes 3-6",
   "unit": "game/trainerAiE000"
  },
  "E02E": {
   "line": "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",
   "unit": "game/trainerAiE000"
  },
  "E02F": {
   "line": "build id reported by the peer; never filled in without a link",
   "unit": "game/trainerAiE000"
  },
  "E030": {
   "line": "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",
   "unit": "game/trainerAiE000"
  },
  "E031": {
   "line": "NMI chain vector low byte, initially $10 -> $E010 (the RTI above); the game patches it to $1298 at $0BF3",
   "unit": "game/trainerAiE000"
  },
  "E032": {
   "line": "NMI chain vector high byte ($E0 here, patched to $12)",
   "unit": "game/trainerAiE000"
  },
  "E033": {
   "line": "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",
   "unit": "game/trainerAiE000"
  },
  "E034": {
   "line": "$E034-$E037: modem-build link variables (command phase, retry state); never touched here",
   "unit": "game/trainerAiE000"
  },
  "E038": {
   "line": "modem-build link variable; unused in the trainer build",
   "unit": "game/trainerAiE000"
  },
  "E039": {
   "line": "disk-access suspend request: suspendCommModule stores $C0 here at $0F67, resumeCommModule 0 at $0F78",
   "unit": "game/trainerAiE000"
  },
  "E03A": {
   "line": "suspend acknowledge; handleCommRequest copies $E039 into it so the wait at $0F6A/$0F7B always succeeds at once",
   "unit": "game/trainerAiE000"
  },
  "E03B": {
   "line": "link-active flag, $FF after setCommLinkState(X=0); read all over the main program ($0F60, $1331, $1A40, $4E46, ...)",
   "unit": "game/trainerAiE000"
  },
  "E03C": {
   "line": "carrier / link status; kept at $FF so the bit-6 test in pollMenuSession ($56B1) and the bit-7 test in hangUpModemSetState ($1B4B) always pass",
   "unit": "game/trainerAiE000"
  },
  "E03D": {
   "line": "$E03D = link-ready sample read at $1BAB; $E03E is a modem-build byte and stays zero",
   "unit": "game/trainerAiE000"
  },
  "E03F": {
   "line": "modem-build reply timer; unused here",
   "unit": "game/trainerAiE000"
  },
  "E040": {
   "line": "modem-build connection phase; unused here",
   "unit": "game/trainerAiE000"
  },
  "E041": {
   "line": "modem-build 'skip init string' flag; unused here",
   "unit": "game/trainerAiE000"
  },
  "E042": {
   "line": "modem-build UART restart request; unused here",
   "unit": "game/trainerAiE000"
  },
  "E043": {
   "line": "request 1 = 'transmit the outgoing packet'?",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E045": {
   "line": "no - test the remaining request codes",
   "unit": "game/trainerAiE000"
  },
  "E047": {
   "line": "yes: instead of transmitting, run one step of the computer opponent",
   "unit": "game/trainerAiE000"
  },
  "E04A": {
   "line": "request 3 = 'cancel the pending send'?",
   "unit": "game/trainerAiE000"
  },
  "E04C": {
   "line": "no - fall through to the poll / acknowledge case",
   "unit": "game/trainerAiE000"
  },
  "E04E": {
   "line": "cancel: drop whatever was queued",
   "unit": "game/trainerAiE000"
  },
  "E050": {
   "line": "outgoing packet length = 0",
   "unit": "game/trainerAiE000"
  },
  "E053": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E054": {
   "block": [
    "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."
   ],
   "line": "$C0 = bit 7 'opponent packet received' + bit 6 'exchange complete'",
   "unit": "game/trainerAiE000"
  },
  "E056": {
   "line": "report the exchange as finished the instant it is asked about",
   "unit": "game/trainerAiE000"
  },
  "E059": {
   "line": "take the disk-access suspend request...",
   "unit": "game/trainerAiE000"
  },
  "E05C": {
   "line": "...and answer it immediately, so $0F5F/$0F71 never block",
   "unit": "game/trainerAiE000"
  },
  "E05F": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E060": {
   "line": "$FF = link up",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E062": {
   "line": "X=0 means open, anything else means close",
   "unit": "game/trainerAiE000"
  },
  "E064": {
   "line": "open - keep $FF",
   "unit": "game/trainerAiE000"
  },
  "E066": {
   "line": "close - the three flags become 0",
   "unit": "game/trainerAiE000"
  },
  "E068": {
   "line": "and the outgoing packet is discarded",
   "unit": "game/trainerAiE000"
  },
  "E06B": {
   "line": "link-ready sample",
   "unit": "game/trainerAiE000"
  },
  "E06E": {
   "line": "carrier status - $FF makes every carrier test in the game succeed",
   "unit": "game/trainerAiE000"
  },
  "E071": {
   "line": "link-active flag the main program polls everywhere",
   "unit": "game/trainerAiE000"
  },
  "E074": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E075": {
   "line": "C=1 = 'no character available'",
   "routine": [
    "reportNoReceivedByte - SEC + RTS: the 'no character available' answer of commGetReceivedByteEntry in the trainer build.",
    "In:  none",
    "Out: C=1",
    "Called from: commGetReceivedByteEntry $E006 (JMP)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E076": {
   "line": "the trainer never delivers opponent chat text",
   "unit": "game/trainerAiE000"
  },
  "E077": {
   "line": "C=0 = 'accepted' / 'nothing to report'",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E078": {
   "line": "shared bare RTS",
   "unit": "game/trainerAiE000"
  },
  "E079": {
   "line": "drone timer; bit 7 marks the drone-flight overlay as resident at $EE00",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E07C": {
   "line": "the drone-flight AI occupies $EE00-$EFFF, so planTrainerRound must not run (it would call chooseAiDroneTarget at $EEE0)",
   "unit": "game/trainerAiE000"
  },
  "E07E": {
   "line": "run the once-per-round planner",
   "unit": "game/trainerAiE000"
  },
  "E081": {
   "line": "back to finishRound",
   "unit": "game/trainerAiE000"
  },
  "E082": {
   "block": [
    "Guards first: the AI only produces orders while a real battle is running."
   ],
   "line": "start with an empty incoming packet",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E084": {
   "line": "received packet length = 0",
   "unit": "game/trainerAiE000"
  },
  "E087": {
   "line": "gamePhase is 0 only while the battle proper runs",
   "unit": "game/trainerAiE000"
  },
  "E089": {
   "line": "side selection / unit setup / menus - stay silent",
   "unit": "game/trainerAiE000"
  },
  "E08B": {
   "line": "film playback flag",
   "unit": "game/trainerAiE000"
  },
  "E08E": {
   "line": "bit 7 = replaying a film; the recorded stream supplies the orders",
   "unit": "game/trainerAiE000"
  },
  "E090": {
   "line": "non-zero once the game has ended",
   "unit": "game/trainerAiE000"
  },
  "E093": {
   "line": "game over - nothing left to order",
   "unit": "game/trainerAiE000"
  },
  "E095": {
   "line": "pause / disconnect state",
   "unit": "game/trainerAiE000"
  },
  "E097": {
   "line": "paused - freeze the opponent as well",
   "unit": "game/trainerAiE000"
  },
  "E099": {
   "block": [
    "$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."
   ],
   "line": "drone timer: bit 7 = the drone-flight overlay is resident at $EE00",
   "unit": "game/trainerAiE000"
  },
  "E09C": {
   "line": "our own tail is resident - take the normal path",
   "unit": "game/trainerAiE000"
  },
  "E09E": {
   "line": "drone control flags",
   "unit": "game/trainerAiE000"
  },
  "E0A1": {
   "line": "bit 0 = a drone-move command is in force, i.e. a drone is airborne",
   "unit": "game/trainerAiE000"
  },
  "E0A3": {
   "line": "drone gone - swap our own tail back in",
   "unit": "game/trainerAiE000"
  },
  "E0A5": {
   "line": "still flying: call the drone-flight AI now loaded at $EE00 (track 29 s4-5, NOT the code assembled at $EE00 in this file)",
   "unit": "game/trainerAiE000"
  },
  "E0A8": {
   "line": "done for this exchange",
   "unit": "game/trainerAiE000"
  },
  "E0AB": {
   "line": "point the fast loader at $EE00",
   "unit": "game/trainerAiE000"
  },
  "E0AE": {
   "line": "Y = track $23 = 35, this module's own track",
   "unit": "game/trainerAiE000"
  },
  "E0B0": {
   "line": "A = 2 sectors",
   "unit": "game/trainerAiE000"
  },
  "E0B2": {
   "line": "X = first sector 14: the native $EE00-$EFFF tail",
   "unit": "game/trainerAiE000"
  },
  "E0B4": {
   "line": "reload it over the drone AI",
   "unit": "game/trainerAiE000"
  },
  "E0B7": {
   "line": "$FF = no drone target chosen",
   "unit": "game/trainerAiE000"
  },
  "E0B9": {
   "line": "let chooseAiDroneTarget pick a fresh one",
   "unit": "game/trainerAiE000"
  },
  "E0BC": {
   "line": "3 exchanges of cool-down before the next launch",
   "unit": "game/trainerAiE000"
  },
  "E0BE": {
   "line": "storing 3 also clears bit 7, marking the drone overlay as gone",
   "unit": "game/trainerAiE000"
  },
  "E0C1": {
   "line": "drone control flags",
   "unit": "game/trainerAiE000"
  },
  "E0C4": {
   "line": "bit 0 = a drone is airborne",
   "unit": "game/trainerAiE000"
  },
  "E0C6": {
   "line": "no drone - just run the normal turn",
   "unit": "game/trainerAiE000"
  },
  "E0C8": {
   "line": "$FF marks the drone overlay resident and blocks planning",
   "unit": "game/trainerAiE000"
  },
  "E0CA": {
   "line": "store it in the drone timer",
   "unit": "game/trainerAiE000"
  },
  "E0CD": {
   "line": "point the fast loader at $EE00",
   "unit": "game/trainerAiE000"
  },
  "E0D0": {
   "line": "Y = track $1D = 29",
   "unit": "game/trainerAiE000"
  },
  "E0D2": {
   "line": "X = first sector 4",
   "unit": "game/trainerAiE000"
  },
  "E0D4": {
   "line": "A = 2 sectors: the drone-flight AI",
   "unit": "game/trainerAiE000"
  },
  "E0D6": {
   "line": "load it over our own tail",
   "unit": "game/trainerAiE000"
  },
  "E0D9": {
   "line": "and fly the drone straight away",
   "unit": "game/trainerAiE000"
  },
  "E0DC": {
   "block": [
    "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."
   ],
   "line": "launch countdown",
   "unit": "game/trainerAiE000"
  },
  "E0DF": {
   "line": "already 0 - launchAiDrone may fire this exchange",
   "unit": "game/trainerAiE000"
  },
  "E0E1": {
   "line": "otherwise tick it down by one exchange",
   "unit": "game/trainerAiE000"
  },
  "E0E4": {
   "line": "copy queued AI orders into the fabricated received packet",
   "unit": "game/trainerAiE000"
  },
  "E0E7": {
   "line": "return to sendOutgoingPacket",
   "unit": "game/trainerAiE000"
  },
  "E0E8": {
   "line": "the unit the engine has just stepped",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E0EB": {
   "line": "type byte: bit 7 = dead, bits 3/4 = end zones already scored, bits 0-2 = type",
   "unit": "game/trainerAiE000"
  },
  "E0EE": {
   "line": "dead slot - nothing to observe",
   "unit": "game/trainerAiE000"
  },
  "E0F0": {
   "line": "$32 = 50: units 0-49 are the human player's",
   "unit": "game/trainerAiE000"
  },
  "E0F2": {
   "line": "human unit - hand it to the observer",
   "unit": "game/trainerAiE000"
  },
  "E0F4": {
   "line": "keep only the two zone-scored bits",
   "unit": "game/trainerAiE000"
  },
  "E0F6": {
   "line": "both the near zone (bit 3, columns 10-19) and the far zone (bit 4, columns 0-9) scored?",
   "unit": "game/trainerAiE000"
  },
  "E0F8": {
   "line": "not deep in the player's half yet",
   "unit": "game/trainerAiE000"
  },
  "E0FA": {
   "line": "$8832+unit is $8864+ordinal: this AI unit's 'has penetrated' flag",
   "unit": "game/trainerAiE000"
  },
  "E0FD": {
   "line": "mark it penetrated...",
   "unit": "game/trainerAiE000"
  },
  "E0FF": {
   "line": "...so computeTrainerGroupStats stops counting it as still at home",
   "unit": "game/trainerAiE000"
  },
  "E102": {
   "line": "$63 = 99 = the AI's own comcen",
   "unit": "game/trainerAiE000"
  },
  "E104": {
   "line": "any other AI unit - hand it to the observer",
   "unit": "game/trainerAiE000"
  },
  "E106": {
   "line": "this round's 'comcen was hit' flag...",
   "unit": "game/trainerAiE000"
  },
  "E109": {
   "line": "...becomes the one-round-old sticky flag",
   "unit": "game/trainerAiE000"
  },
  "E10C": {
   "line": "assume 'not hit' this round",
   "unit": "game/trainerAiE000"
  },
  "E10E": {
   "line": "display flags of unit 99 ($FB54 array)",
   "unit": "game/trainerAiE000"
  },
  "E111": {
   "line": "bit 1 is set by recordUnitHit ($3C1A) whenever the unit takes damage",
   "unit": "game/trainerAiE000"
  },
  "E113": {
   "line": "not hit",
   "unit": "game/trainerAiE000"
  },
  "E115": {
   "line": "X = $FF = hit",
   "unit": "game/trainerAiE000"
  },
  "E116": {
   "line": "a fresh hit also re-arms the sticky flag",
   "unit": "game/trainerAiE000"
  },
  "E119": {
   "line": "record whether the comcen was hit this round",
   "unit": "game/trainerAiE000"
  },
  "E11C": {
   "line": "done with the comcen",
   "unit": "game/trainerAiE000"
  },
  "E11F": {
   "line": "drone timer, bit 7 = the drone-flight overlay is resident, i.e. the AI's drone is airborne",
   "unit": "game/trainerAiE000"
  },
  "E122": {
   "line": "the AI suspends its unit observation for as long as its drone is in the air",
   "unit": "game/trainerAiE000"
  },
  "E124": {
   "line": "track what this unit has seen and what it is shooting at",
   "unit": "game/trainerAiE000"
  },
  "E127": {
   "line": "back to processUnitStep",
   "unit": "game/trainerAiE000"
  },
  "E128": {
   "line": "destination low byte = $00",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E12A": {
   "line": "destination high byte = $EE",
   "unit": "game/trainerAiE000"
  },
  "E12C": {
   "line": "hand $EE00 to the fast loader's setLoadDest",
   "unit": "game/trainerAiE000"
  },
  "E12F": {
   "line": "column of the human comcen (unit 49)",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E132": {
   "line": "scatter it by a random offset",
   "unit": "game/trainerAiE000"
  },
  "E135": {
   "line": "rally column the AI's groups head for",
   "unit": "game/trainerAiE000"
  },
  "E138": {
   "line": "row of the human comcen",
   "unit": "game/trainerAiE000"
  },
  "E13B": {
   "line": "scatter that as well",
   "unit": "game/trainerAiE000"
  },
  "E13E": {
   "line": "rally row",
   "unit": "game/trainerAiE000"
  },
  "E141": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E142": {
   "block": [
    "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."
   ],
   "line": "offsets +1,-1,+1,+1,-1,-1,+2 (entries 0-6)",
   "unit": "game/trainerAiE000"
  },
  "E149": {
   "line": "entry 7: -2 cells",
   "unit": "game/trainerAiE000"
  },
  "E14A": {
   "line": "patch the base coordinate into the ADC immediate at $E164",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E14D": {
   "line": "alternate entry that reuses the base already patched in (no caller in this build)",
   "unit": "game/trainerAiE000"
  },
  "E150": {
   "line": "keep 3 bits: offset table index 0-7",
   "unit": "game/trainerAiE000"
  },
  "E152": {
   "line": "use it as the table index",
   "unit": "game/trainerAiE000"
  },
  "E153": {
   "line": "signed cell offset -2..+2",
   "unit": "game/trainerAiE000"
  },
  "E156": {
   "line": "energy byte of the human comcen; bit 7 = 'currently spotted'",
   "unit": "game/trainerAiE000"
  },
  "E159": {
   "line": "visible right now - aim with the raw offset",
   "unit": "game/trainerAiE000"
  },
  "E15B": {
   "line": "not visible: double the scatter",
   "unit": "game/trainerAiE000"
  },
  "E15C": {
   "line": "sticky 'the comcen has been seen at some point' flag",
   "unit": "game/trainerAiE000"
  },
  "E15F": {
   "line": "seen before - x2 scatter is enough",
   "unit": "game/trainerAiE000"
  },
  "E161": {
   "line": "never seen at all - x4, so the AI just roams",
   "unit": "game/trainerAiE000"
  },
  "E162": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E163": {
   "line": "operand byte patched at $E14A with the base coordinate",
   "unit": "game/trainerAiE000"
  },
  "E165": {
   "line": "result still positive - keep it",
   "unit": "game/trainerAiE000"
  },
  "E167": {
   "line": "clamp to the left/top edge of the map",
   "unit": "game/trainerAiE000"
  },
  "E169": {
   "line": "$28 = 40 columns/rows on the battlefield",
   "unit": "game/trainerAiE000"
  },
  "E16B": {
   "line": "inside the map",
   "unit": "game/trainerAiE000"
  },
  "E16D": {
   "line": "clamp to the last cell, 39",
   "unit": "game/trainerAiE000"
  },
  "E16F": {
   "line": "A = coordinate 0-39",
   "unit": "game/trainerAiE000"
  },
  "E170": {
   "line": "non-zero once the game has ended",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E173": {
   "line": "still playing",
   "unit": "game/trainerAiE000"
  },
  "E175": {
   "line": "game over - no plan",
   "unit": "game/trainerAiE000"
  },
  "E176": {
   "line": "A is 0 here: clear the count of commands queued this round",
   "unit": "game/trainerAiE000"
  },
  "E179": {
   "block": [
    "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."
   ],
   "line": "human comcen energy byte, bit 7 = currently spotted",
   "unit": "game/trainerAiE000"
  },
  "E17C": {
   "line": "not spotted this round",
   "unit": "game/trainerAiE000"
  },
  "E17E": {
   "line": "spotted: bit 7 sticks in $896C, so randomiseCoordinate scatters less from now on...",
   "unit": "game/trainerAiE000"
  },
  "E181": {
   "line": "...and the rally point is refreshed immediately",
   "unit": "game/trainerAiE000"
  },
  "E183": {
   "line": "otherwise count the rally-point refresh timer down",
   "unit": "game/trainerAiE000"
  },
  "E186": {
   "line": "not due yet",
   "unit": "game/trainerAiE000"
  },
  "E188": {
   "line": "pick a new random cell near the human comcen",
   "unit": "game/trainerAiE000"
  },
  "E18B": {
   "line": "5 = next refresh in six rounds",
   "unit": "game/trainerAiE000"
  },
  "E18D": {
   "line": "reload the timer",
   "unit": "game/trainerAiE000"
  },
  "E190": {
   "block": [
    "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."
   ],
   "line": "AI comcen energy (unit 99)",
   "unit": "game/trainerAiE000"
  },
  "E193": {
   "line": "bits 0-5 hold the 0-50 energy points",
   "unit": "game/trainerAiE000"
  },
  "E195": {
   "line": "that is this round's action budget",
   "unit": "game/trainerAiE000"
  },
  "E198": {
   "line": "AI comcen stun byte (unit 99 of the $F960 array)",
   "unit": "game/trainerAiE000"
  },
  "E19B": {
   "line": "low nibble = the STUN'D counter 0-15 shown on the console",
   "unit": "game/trainerAiE000"
  },
  "E19D": {
   "line": "remember how badly the comcen is stunned",
   "unit": "game/trainerAiE000"
  },
  "E1A0": {
   "line": "game options byte",
   "unit": "game/trainerAiE000"
  },
  "E1A3": {
   "line": "bit 4 = the COMCEN DAMAGE option",
   "unit": "game/trainerAiE000"
  },
  "E1A5": {
   "line": "damage rules off - use a flat budget",
   "unit": "game/trainerAiE000"
  },
  "E1A7": {
   "line": "stun counter",
   "unit": "game/trainerAiE000"
  },
  "E1AA": {
   "line": "not stunned - keep the energy budget",
   "unit": "game/trainerAiE000"
  },
  "E1AC": {
   "line": "$0F = maximum stun",
   "unit": "game/trainerAiE000"
  },
  "E1AE": {
   "line": "completely out - the AI does nothing at all this round",
   "unit": "game/trainerAiE000"
  },
  "E1B0": {
   "line": "stun 8-14",
   "unit": "game/trainerAiE000"
  },
  "E1B2": {
   "line": "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",
   "unit": "game/trainerAiE000"
  },
  "E1B4": {
   "line": "stun 1-7: take the energy budget...",
   "unit": "game/trainerAiE000"
  },
  "E1B7": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E1B8": {
   "line": "...and charge a 16 point penalty",
   "unit": "game/trainerAiE000"
  },
  "E1BA": {
   "line": "budget went negative - only repair the comcen",
   "unit": "game/trainerAiE000"
  },
  "E1BC": {
   "line": "store the reduced budget",
   "unit": "game/trainerAiE000"
  },
  "E1BF": {
   "line": "always taken (the SBC above left C=1)",
   "unit": "game/trainerAiE000"
  },
  "E1C1": {
   "line": "$32 = 50 = a full budget",
   "unit": "game/trainerAiE000"
  },
  "E1C3": {
   "line": "store it",
   "unit": "game/trainerAiE000"
  },
  "E1C6": {
   "line": "this round's budget",
   "unit": "game/trainerAiE000"
  },
  "E1C9": {
   "line": "$14 = 20 points",
   "unit": "game/trainerAiE000"
  },
  "E1CB": {
   "line": "enough left to manoeuvre",
   "unit": "game/trainerAiE000"
  },
  "E1CD": {
   "line": "too little - spend the round on comcen repair and energy",
   "unit": "game/trainerAiE000"
  },
  "E1D0": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E1D1": {
   "block": [
    "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."
   ],
   "line": "AI comcen column",
   "unit": "game/trainerAiE000"
  },
  "E1D4": {
   "line": "reference point B of the threat scan",
   "unit": "game/trainerAiE000"
  },
  "E1D6": {
   "line": "AI comcen row",
   "unit": "game/trainerAiE000"
  },
  "E1D9": {
   "line": "reference point B of the threat scan",
   "unit": "game/trainerAiE000"
  },
  "E1DB": {
   "line": "bucket the human units around it into 8 compass directions",
   "unit": "game/trainerAiE000"
  },
  "E1DE": {
   "line": "A = overall threat score",
   "unit": "game/trainerAiE000"
  },
  "E1E1": {
   "line": "Y = safest direction 0-7 (8 = nothing anywhere)",
   "unit": "game/trainerAiE000"
  },
  "E1E4": {
   "line": "X = deepest weapon-range penetration, 0 = no enemy weapon covers the comcen",
   "unit": "game/trainerAiE000"
  },
  "E1E7": {
   "line": "assume attack mode (0)",
   "unit": "game/trainerAiE000"
  },
  "E1E9": {
   "line": "is any enemy weapon covering the comcen?",
   "unit": "game/trainerAiE000"
  },
  "E1EB": {
   "line": "no threat at all - attack",
   "unit": "game/trainerAiE000"
  },
  "E1ED": {
   "line": "Y = $FF = defend",
   "unit": "game/trainerAiE000"
  },
  "E1EE": {
   "line": "is an assault group selected? bit 7 = none",
   "unit": "game/trainerAiE000"
  },
  "E1F1": {
   "line": "no group to rely on - defend",
   "unit": "game/trainerAiE000"
  },
  "E1F3": {
   "line": "was the comcen hit this round or last?",
   "unit": "game/trainerAiE000"
  },
  "E1F6": {
   "line": "yes - defend",
   "unit": "game/trainerAiE000"
  },
  "E1F8": {
   "line": "threatened but coping - attack after all",
   "unit": "game/trainerAiE000"
  },
  "E1F9": {
   "line": "0 = attack, $FF = defend",
   "unit": "game/trainerAiE000"
  },
  "E1FC": {
   "line": "A = the mode just chosen",
   "unit": "game/trainerAiE000"
  },
  "E1FD": {
   "line": "defending - do not spend a drone",
   "unit": "game/trainerAiE000"
  },
  "E1FF": {
   "line": "attacking - decide what the next drone should be sent at",
   "unit": "game/trainerAiE000"
  },
  "E202": {
   "line": "issue orders to every AI unit whose replan clock has come due",
   "unit": "game/trainerAiE000"
  },
  "E205": {
   "line": "commands queued so far this round",
   "unit": "game/trainerAiE000"
  },
  "E208": {
   "line": "5 is the per-round ceiling",
   "unit": "game/trainerAiE000"
  },
  "E20A": {
   "line": "budget spent - stop here",
   "unit": "game/trainerAiE000"
  },
  "E20C": {
   "line": "refresh the assault group's id and centre",
   "unit": "game/trainerAiE000"
  },
  "E20F": {
   "line": "attack (0) or defend ($FF)?",
   "unit": "game/trainerAiE000"
  },
  "E212": {
   "line": "defend - pull the comcen back and recall the group",
   "unit": "game/trainerAiE000"
  },
  "E214": {
   "line": "action budget",
   "unit": "game/trainerAiE000"
  },
  "E217": {
   "line": "$20 = 32 points",
   "unit": "game/trainerAiE000"
  },
  "E219": {
   "line": "plenty of energy - walk the comcen up behind the attack",
   "unit": "game/trainerAiE000"
  },
  "E21B": {
   "line": "low budget - repair the comcen instead",
   "unit": "game/trainerAiE000"
  },
  "E21E": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E21F": {
   "line": "assault group id, bit 7 = none selected",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E222": {
   "line": "no group to follow",
   "unit": "game/trainerAiE000"
  },
  "E224": {
   "line": "comcen stun counter",
   "unit": "game/trainerAiE000"
  },
  "E227": {
   "line": "stunned - it cannot move",
   "unit": "game/trainerAiE000"
  },
  "E229": {
   "line": "the comcen's own replan clock",
   "unit": "game/trainerAiE000"
  },
  "E22C": {
   "line": "the clock counts down, so 'stamp >= clock' means the round has arrived",
   "unit": "game/trainerAiE000"
  },
  "E22F": {
   "line": "not due yet",
   "unit": "game/trainerAiE000"
  },
  "E231": {
   "line": "current round",
   "unit": "game/trainerAiE000"
  },
  "E234": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E235": {
   "line": "next comcen replan in 3 rounds",
   "unit": "game/trainerAiE000"
  },
  "E237": {
   "line": "no underflow",
   "unit": "game/trainerAiE000"
  },
  "E239": {
   "line": "clamp at round 0",
   "unit": "game/trainerAiE000"
  },
  "E23B": {
   "line": "re-arm the comcen replan clock",
   "unit": "game/trainerAiE000"
  },
  "E23E": {
   "line": "safest direction from the threat scan",
   "unit": "game/trainerAiE000"
  },
  "E241": {
   "line": "column step -1/0/+1 for that direction",
   "unit": "game/trainerAiE000"
  },
  "E244": {
   "line": "X = column step for offsetCoordinateClamped",
   "unit": "game/trainerAiE000"
  },
  "E245": {
   "line": "how deep the enemy weapons reach",
   "unit": "game/trainerAiE000"
  },
  "E248": {
   "line": "one cell beyond that",
   "unit": "game/trainerAiE000"
  },
  "E249": {
   "line": "$895E = assault group centre column (the label is a stale message-string name)",
   "unit": "game/trainerAiE000"
  },
  "E24C": {
   "line": "column = centre + step * distance, clamped to 0..39",
   "unit": "game/trainerAiE000"
  },
  "E24F": {
   "line": "stash the target column",
   "unit": "game/trainerAiE000"
  },
  "E251": {
   "line": "same direction...",
   "unit": "game/trainerAiE000"
  },
  "E254": {
   "line": "...row step -1/0/+1",
   "unit": "game/trainerAiE000"
  },
  "E257": {
   "line": "X = row step",
   "unit": "game/trainerAiE000"
  },
  "E258": {
   "line": "assault group centre row",
   "unit": "game/trainerAiE000"
  },
  "E25B": {
   "line": "row = centre + step * distance, clamped",
   "unit": "game/trainerAiE000"
  },
  "E25E": {
   "line": "X = column, A = row",
   "unit": "game/trainerAiE000"
  },
  "E260": {
   "line": "$31 = AI ordinal 49 = unit 99, the AI comcen",
   "unit": "game/trainerAiE000"
  },
  "E262": {
   "line": "queue the MOVE",
   "unit": "game/trainerAiE000"
  },
  "E265": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E266": {
   "line": "current round",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E269": {
   "line": "stamp the commit clock: groups may now use their full strength",
   "unit": "game/trainerAiE000"
  },
  "E26C": {
   "line": "comcen stun counter",
   "unit": "game/trainerAiE000"
  },
  "E26F": {
   "line": "stunned - it cannot run, go straight to recalling the group",
   "unit": "game/trainerAiE000"
  },
  "E271": {
   "line": "safest direction",
   "unit": "game/trainerAiE000"
  },
  "E274": {
   "line": "its column step",
   "unit": "game/trainerAiE000"
  },
  "E277": {
   "line": "X = column step",
   "unit": "game/trainerAiE000"
  },
  "E278": {
   "line": "run as far as the enemy reaches",
   "unit": "game/trainerAiE000"
  },
  "E27B": {
   "line": "AI comcen column",
   "unit": "game/trainerAiE000"
  },
  "E27E": {
   "line": "clamped destination column",
   "unit": "game/trainerAiE000"
  },
  "E281": {
   "line": "stash it",
   "unit": "game/trainerAiE000"
  },
  "E283": {
   "line": "safest direction again",
   "unit": "game/trainerAiE000"
  },
  "E286": {
   "line": "its row step",
   "unit": "game/trainerAiE000"
  },
  "E289": {
   "line": "X = row step",
   "unit": "game/trainerAiE000"
  },
  "E28A": {
   "line": "AI comcen row",
   "unit": "game/trainerAiE000"
  },
  "E28D": {
   "line": "clamped destination row",
   "unit": "game/trainerAiE000"
  },
  "E290": {
   "line": "X = column",
   "unit": "game/trainerAiE000"
  },
  "E292": {
   "line": "$31 = AI ordinal 49 = unit 99",
   "unit": "game/trainerAiE000"
  },
  "E294": {
   "line": "queue the comcen MOVE",
   "unit": "game/trainerAiE000"
  },
  "E297": {
   "line": "threat score from the scan",
   "unit": "game/trainerAiE000"
  },
  "E29A": {
   "line": "recall threshold, set to 5 by the setup overlay at $EC44",
   "unit": "game/trainerAiE000"
  },
  "E29D": {
   "line": "manageable - leave the assault group where it is",
   "unit": "game/trainerAiE000"
  },
  "E29F": {
   "line": "assault group id",
   "unit": "game/trainerAiE000"
  },
  "E2A2": {
   "line": "$FF = no group at all",
   "unit": "game/trainerAiE000"
  },
  "E2A4": {
   "line": "nothing to recall",
   "unit": "game/trainerAiE000"
  },
  "E2A6": {
   "line": "strip the disable bit, leaving the group index 0-11",
   "unit": "game/trainerAiE000"
  },
  "E2A8": {
   "line": "Y = the group index for computeTrainerGroupStats",
   "unit": "game/trainerAiE000"
  },
  "E2A9": {
   "line": "recompute its members, strength, centre and box",
   "unit": "game/trainerAiE000"
  },
  "E2AC": {
   "line": "empty group - nothing to recall",
   "unit": "game/trainerAiE000"
  },
  "E2AE": {
   "line": "pretend the group has no slow units...",
   "unit": "game/trainerAiE000"
  },
  "E2B0": {
   "line": "...so issueTrainerGroupOrders does not double the replan delay - this is an urgent recall",
   "unit": "game/trainerAiE000"
  },
  "E2B3": {
   "line": "march the whole group home to the comcen",
   "unit": "game/trainerAiE000"
  },
  "E2B6": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E2B7": {
   "block": [
    "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)."
   ],
   "line": "GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0",
   "unit": "game/trainerAiE000"
  },
  "E2BC": {
   "block": [
    "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."
   ],
   "line": "current AI ordinal being scanned, 49 down to 0",
   "unit": "game/trainerAiE000"
  },
  "E2BD": {
   "line": "$31 = 49, the highest AI ordinal (unit 99)",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E2BF": {
   "line": "start the walk at the comcen and count down",
   "unit": "game/trainerAiE000"
  },
  "E2C2": {
   "block": [
    "Loop over AI ordinals 49..0.  It maintains the embedded counter at $E2BC and stops early once four",
    "commands have been queued."
   ],
   "line": "commands queued so far this round",
   "unit": "game/trainerAiE000"
  },
  "E2C5": {
   "line": "4 is the ceiling for unit orders",
   "unit": "game/trainerAiE000"
  },
  "E2C7": {
   "line": "budget spent",
   "unit": "game/trainerAiE000"
  },
  "E2C9": {
   "line": "default replan urgency: re-plan again in at most 5 rounds",
   "unit": "game/trainerAiE000"
  },
  "E2CB": {
   "line": "reload the default urgency before every unit",
   "unit": "game/trainerAiE000"
  },
  "E2CE": {
   "line": "current AI ordinal 49..0",
   "unit": "game/trainerAiE000"
  },
  "E2D1": {
   "line": "type byte of unit ordinal+50",
   "unit": "game/trainerAiE000"
  },
  "E2D4": {
   "line": "still alive",
   "unit": "game/trainerAiE000"
  },
  "E2D6": {
   "line": "dead: clear its replan clock...",
   "unit": "game/trainerAiE000"
  },
  "E2D8": {
   "line": "...at $8800+ordinal",
   "unit": "game/trainerAiE000"
  },
  "E2DB": {
   "line": "...and take it out of every group",
   "unit": "game/trainerAiE000"
  },
  "E2DD": {
   "line": "$FF = belongs to no group",
   "unit": "game/trainerAiE000"
  },
  "E2E0": {
   "line": "this unit's replan clock",
   "unit": "game/trainerAiE000"
  },
  "E2E3": {
   "line": "the clock counts down; stamp >= clock means the planned round has come",
   "unit": "game/trainerAiE000"
  },
  "E2E6": {
   "line": "not due yet",
   "unit": "game/trainerAiE000"
  },
  "E2E8": {
   "line": "re-plan the whole group this unit belongs to",
   "unit": "game/trainerAiE000"
  },
  "E2EB": {
   "line": "next ordinal down",
   "unit": "game/trainerAiE000"
  },
  "E2EE": {
   "line": "loop until ordinal 0 has been done",
   "unit": "game/trainerAiE000"
  },
  "E2F0": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E2F1": {
   "line": "group id of this AI unit",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E2F4": {
   "line": "groups 1..11 are ordinary objectives",
   "unit": "game/trainerAiE000"
  },
  "E2F6": {
   "block": [
    "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."
   ],
   "line": "group 0: is the comcen stunned?",
   "unit": "game/trainerAiE000"
  },
  "E2F9": {
   "line": "stunned - leave the escort alone",
   "unit": "game/trainerAiE000"
  },
  "E2FB": {
   "line": "urgency 3 for the comcen's escort",
   "unit": "game/trainerAiE000"
  },
  "E2FD": {
   "line": "urgency for group 0",
   "unit": "game/trainerAiE000"
  },
  "E300": {
   "line": "is an assault group already selected?",
   "unit": "game/trainerAiE000"
  },
  "E303": {
   "line": "yes - group 0 is not the one to re-plan",
   "unit": "game/trainerAiE000"
  },
  "E305": {
   "line": "AI comcen ordered destination column",
   "unit": "game/trainerAiE000"
  },
  "E308": {
   "line": "strip the flag bits, keep the 0-39 cell",
   "unit": "game/trainerAiE000"
  },
  "E30A": {
   "line": "group 0 target column = wherever the comcen is heading",
   "unit": "game/trainerAiE000"
  },
  "E30D": {
   "line": "AI comcen ordered destination row",
   "unit": "game/trainerAiE000"
  },
  "E310": {
   "line": "strip the flag bits, keep the 0-39 cell",
   "unit": "game/trainerAiE000"
  },
  "E312": {
   "line": "group 0 target row",
   "unit": "game/trainerAiE000"
  },
  "E315": {
   "line": "A = group id 0",
   "unit": "game/trainerAiE000"
  },
  "E317": {
   "line": "always taken",
   "unit": "game/trainerAiE000"
  },
  "E319": {
   "line": "nothing to do for this group",
   "unit": "game/trainerAiE000"
  },
  "E31A": {
   "line": "Y = group id",
   "unit": "game/trainerAiE000"
  },
  "E31B": {
   "line": "flags of that group",
   "unit": "game/trainerAiE000"
  },
  "E31E": {
   "line": "keep a copy for the tests below (bit 6 = has made contact with the player's comcen)",
   "unit": "game/trainerAiE000"
  },
  "E321": {
   "line": "clear bits 4-7, leaving the start-delay nibble",
   "unit": "game/trainerAiE000"
  },
  "E323": {
   "line": "write the trimmed flags back",
   "unit": "game/trainerAiE000"
  },
  "E326": {
   "line": "members, strength, centre and bounding box of the group",
   "unit": "game/trainerAiE000"
  },
  "E329": {
   "line": "the group has members",
   "unit": "game/trainerAiE000"
  },
  "E32B": {
   "line": "empty group - nothing to order",
   "unit": "game/trainerAiE000"
  },
  "E32C": {
   "line": "group id again",
   "unit": "game/trainerAiE000"
  },
  "E32F": {
   "line": "the home-defence group chosen at setup ($EC39/$ECA3)",
   "unit": "game/trainerAiE000"
  },
  "E332": {
   "line": "not the home group - decide between charging and digging in",
   "unit": "game/trainerAiE000"
  },
  "E334": {
   "line": "urgency 2 - re-plan the home guard often",
   "unit": "game/trainerAiE000"
  },
  "E336": {
   "line": "urgency for the home guard",
   "unit": "game/trainerAiE000"
  },
  "E339": {
   "line": "AI comcen column",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E33C": {
   "line": "line target column",
   "unit": "game/trainerAiE000"
  },
  "E33E": {
   "line": "AI comcen row",
   "unit": "game/trainerAiE000"
  },
  "E341": {
   "line": "line target row",
   "unit": "game/trainerAiE000"
  },
  "E343": {
   "block": [
    "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."
   ],
   "line": "group centre column (set by computeTrainerGroupStats)",
   "unit": "game/trainerAiE000"
  },
  "E345": {
   "line": "search origin / reference column",
   "unit": "game/trainerAiE000"
  },
  "E347": {
   "line": "group centre row",
   "unit": "game/trainerAiE000"
  },
  "E349": {
   "line": "search origin / reference row",
   "unit": "game/trainerAiE000"
  },
  "E34B": {
   "line": "line target column",
   "unit": "game/trainerAiE000"
  },
  "E34D": {
   "line": "wanted group destination column",
   "unit": "game/trainerAiE000"
  },
  "E34F": {
   "line": "line target row",
   "unit": "game/trainerAiE000"
  },
  "E351": {
   "line": "wanted group destination row",
   "unit": "game/trainerAiE000"
  },
  "E353": {
   "line": "issue the move orders for the whole group",
   "unit": "game/trainerAiE000"
  },
  "E356": {
   "block": [
    "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."
   ],
   "line": "cached group flags",
   "unit": "game/trainerAiE000"
  },
  "E359": {
   "line": "bit 6 set: the group has already made contact with the player's comcen",
   "unit": "game/trainerAiE000"
  },
  "E35B": {
   "line": "count of members that have not scored both end zones",
   "unit": "game/trainerAiE000"
  },
  "E35D": {
   "line": "some are still at home - form a defensive line instead",
   "unit": "game/trainerAiE000"
  },
  "E35F": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E362": {
   "line": "groups 1..11 head for the rally point",
   "unit": "game/trainerAiE000"
  },
  "E364": {
   "line": "group 0 (the comcen escort): attack or defend?",
   "unit": "game/trainerAiE000"
  },
  "E367": {
   "line": "defending - the escort stays where it is",
   "unit": "game/trainerAiE000"
  },
  "E369": {
   "line": "safest direction around the comcen",
   "unit": "game/trainerAiE000"
  },
  "E36C": {
   "line": "use it as the fan heading",
   "unit": "game/trainerAiE000"
  },
  "E36E": {
   "line": "lay out the 8 candidate directions in $8896-$889D",
   "unit": "game/trainerAiE000"
  },
  "E371": {
   "line": "try to advance 5 cells",
   "unit": "game/trainerAiE000"
  },
  "E373": {
   "line": "wanted advance distance for the search",
   "unit": "game/trainerAiE000"
  },
  "E376": {
   "line": "run the threat-aware path search",
   "unit": "game/trainerAiE000"
  },
  "E379": {
   "line": "nothing to do",
   "unit": "game/trainerAiE000"
  },
  "E37A": {
   "block": [
    "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."
   ],
   "line": "rally column near the human comcen",
   "unit": "game/trainerAiE000"
  },
  "E37D": {
   "line": "line target column",
   "unit": "game/trainerAiE000"
  },
  "E37F": {
   "line": "rally row",
   "unit": "game/trainerAiE000"
  },
  "E382": {
   "line": "line target row",
   "unit": "game/trainerAiE000"
  },
  "E384": {
   "line": "human comcen energy byte, bit 7 = spotted",
   "unit": "game/trainerAiE000"
  },
  "E387": {
   "line": "spotted",
   "unit": "game/trainerAiE000"
  },
  "E389": {
   "line": "rounds left in the game",
   "unit": "game/trainerAiE000"
  },
  "E38C": {
   "line": "$28 = 40 rounds",
   "unit": "game/trainerAiE000"
  },
  "E38E": {
   "line": "plenty of time left - just search for a safe advance",
   "unit": "game/trainerAiE000"
  },
  "E390": {
   "line": "late in the game - be twice as bold",
   "unit": "game/trainerAiE000"
  },
  "E392": {
   "line": "cached group flags",
   "unit": "game/trainerAiE000"
  },
  "E395": {
   "line": "no contact with the comcen yet - just search",
   "unit": "game/trainerAiE000"
  },
  "E397": {
   "line": "group flags",
   "unit": "game/trainerAiE000"
  },
  "E39A": {
   "line": "bit 7 = this group has the spotted comcen in its sights",
   "unit": "game/trainerAiE000"
  },
  "E39C": {
   "line": "store the flags back",
   "unit": "game/trainerAiE000"
  },
  "E39F": {
   "line": "double the group's combat value, low byte...",
   "unit": "game/trainerAiE000"
  },
  "E3A2": {
   "line": "...and high byte, so the risk test accepts twice as much opposition",
   "unit": "game/trainerAiE000"
  },
  "E3A5": {
   "line": "cached group flags",
   "unit": "game/trainerAiE000"
  },
  "E3A8": {
   "line": "no contact - fall back to the cautious search",
   "unit": "game/trainerAiE000"
  },
  "E3AA": {
   "line": "the unit the next drone is aimed at",
   "unit": "game/trainerAiE000"
  },
  "E3AD": {
   "line": "$31 = 49 = the human comcen",
   "unit": "game/trainerAiE000"
  },
  "E3AF": {
   "line": "the drone is already going for the comcen - the group beelines to the rally point too",
   "unit": "game/trainerAiE000"
  },
  "E3B1": {
   "line": "is the straight line to the human comcen free of living enemy combat units?",
   "unit": "game/trainerAiE000"
  },
  "E3B4": {
   "line": "blocked - fall back to the cautious search",
   "unit": "game/trainerAiE000"
  },
  "E3B6": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E3B9": {
   "line": "number of members",
   "unit": "game/trainerAiE000"
  },
  "E3BC": {
   "line": "4 or more",
   "unit": "game/trainerAiE000"
  },
  "E3BE": {
   "line": "too small to commit",
   "unit": "game/trainerAiE000"
  },
  "E3C0": {
   "line": "group flags",
   "unit": "game/trainerAiE000"
  },
  "E3C3": {
   "line": "bit 5 = committed to the all-out attack (chooseAiDroneTarget weighs this doubled)",
   "unit": "game/trainerAiE000"
  },
  "E3C5": {
   "line": "store the committed flag back",
   "unit": "game/trainerAiE000"
  },
  "E3C8": {
   "line": "urgency 3",
   "unit": "game/trainerAiE000"
  },
  "E3CA": {
   "line": "re-plan this group again in 3 rounds at the latest",
   "unit": "game/trainerAiE000"
  },
  "E3CD": {
   "line": "charge straight at the rally point",
   "unit": "game/trainerAiE000"
  },
  "E3D0": {
   "line": "group centre column",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E3D2": {
   "line": "hold the column, only the row changes",
   "unit": "game/trainerAiE000"
  },
  "E3D4": {
   "line": "$1D = row 29",
   "unit": "game/trainerAiE000"
  },
  "E3D6": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E3D9": {
   "line": "prepare the two adds",
   "unit": "game/trainerAiE000"
  },
  "E3DA": {
   "line": "plus the group's reach above its centre",
   "unit": "game/trainerAiE000"
  },
  "E3DD": {
   "line": "plus its reach below, so the whole formation box sits inside the AI's own half",
   "unit": "game/trainerAiE000"
  },
  "E3E0": {
   "line": "defensive line row",
   "unit": "game/trainerAiE000"
  },
  "E3E2": {
   "line": "commit clock, set at setup ($ED85) and re-stamped by every comcen retreat",
   "unit": "game/trainerAiE000"
  },
  "E3E5": {
   "line": "the clock counts down: stamp >= clock means the commit round has passed",
   "unit": "game/trainerAiE000"
  },
  "E3E8": {
   "line": "committed - let the group use its real strength",
   "unit": "game/trainerAiE000"
  },
  "E3EA": {
   "line": "group combat value, high byte",
   "unit": "game/trainerAiE000"
  },
  "E3ED": {
   "line": "over 255 - definitely cap it",
   "unit": "game/trainerAiE000"
  },
  "E3EF": {
   "line": "low byte",
   "unit": "game/trainerAiE000"
  },
  "E3F2": {
   "line": "9 is the cautious ceiling",
   "unit": "game/trainerAiE000"
  },
  "E3F4": {
   "line": "already weak enough",
   "unit": "game/trainerAiE000"
  },
  "E3F6": {
   "line": "cap the value at 9 so the risk test only accepts very quiet cells",
   "unit": "game/trainerAiE000"
  },
  "E3F8": {
   "line": "high byte of the capped value = 0",
   "unit": "game/trainerAiE000"
  },
  "E3FB": {
   "line": "9 = the cautious ceiling",
   "unit": "game/trainerAiE000"
  },
  "E3FD": {
   "line": "low byte of the capped value",
   "unit": "game/trainerAiE000"
  },
  "E400": {
   "block": [
    "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."
   ],
   "line": "group centre column",
   "unit": "game/trainerAiE000"
  },
  "E402": {
   "line": "search origin column",
   "unit": "game/trainerAiE000"
  },
  "E404": {
   "line": "group centre row",
   "unit": "game/trainerAiE000"
  },
  "E406": {
   "line": "search origin row",
   "unit": "game/trainerAiE000"
  },
  "E408": {
   "line": "target column",
   "unit": "game/trainerAiE000"
  },
  "E40A": {
   "line": "point A of the distance measure",
   "unit": "game/trainerAiE000"
  },
  "E40C": {
   "line": "target row",
   "unit": "game/trainerAiE000"
  },
  "E40E": {
   "line": "point A row",
   "unit": "game/trainerAiE000"
  },
  "E410": {
   "line": "distance from the group to its target, 0-16 cells",
   "unit": "game/trainerAiE000"
  },
  "E413": {
   "line": "that is how far the group would like to advance",
   "unit": "game/trainerAiE000"
  },
  "E416": {
   "line": "heading towards the target plus the 8 candidate directions in $8896-$889D",
   "unit": "game/trainerAiE000"
  },
  "E419": {
   "line": "heading 0-7, or $FF when the group already stands on the target",
   "unit": "game/trainerAiE000"
  },
  "E41B": {
   "line": "valid heading - search for a safe cell along it",
   "unit": "game/trainerAiE000"
  },
  "E41D": {
   "line": "already there: zero movement delta",
   "unit": "game/trainerAiE000"
  },
  "E41F": {
   "line": "no column shift",
   "unit": "game/trainerAiE000"
  },
  "E421": {
   "line": "no row shift",
   "unit": "game/trainerAiE000"
  },
  "E423": {
   "line": "replan again in 2 rounds",
   "unit": "game/trainerAiE000"
  },
  "E425": {
   "line": "zp_29 = replan delay in rounds",
   "unit": "game/trainerAiE000"
  },
  "E427": {
   "line": "skip the search and just re-stamp the members",
   "unit": "game/trainerAiE000"
  },
  "E42A": {
   "line": "build the per-unit influence map in the $0400/$0464 scratch pages",
   "routine": [
    "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)"
   ],
   "unit": "game/trainerAiE000"
  },
  "E42D": {
   "line": "start with direction rank 0, the best one in the fan",
   "unit": "game/trainerAiE000"
  },
  "E42F": {
   "line": "zp_22 doubles as the rank index for the length of the search",
   "unit": "game/trainerAiE000"
  },
  "E431": {
   "block": [
    "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."
   ],
   "line": "distance reached along this rank = 0",
   "unit": "game/trainerAiE000"
  },
  "E433": {
   "line": "zp_23 = distance reached along this rank",
   "unit": "game/trainerAiE000"
  },
  "E435": {
   "line": "current rank",
   "unit": "game/trainerAiE000"
  },
  "E437": {
   "line": "the direction the fan put in that rank",
   "unit": "game/trainerAiE000"
  },
  "E43A": {
   "line": "keep it as the candidate heading",
   "unit": "game/trainerAiE000"
  },
  "E43C": {
   "line": "distance reached so far",
   "unit": "game/trainerAiE000"
  },
  "E43E": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E43F": {
   "line": "probe two cells further out",
   "unit": "game/trainerAiE000"
  },
  "E441": {
   "line": "candidate distance",
   "unit": "game/trainerAiE000"
  },
  "E443": {
   "line": "Y = distance for the risk test",
   "unit": "game/trainerAiE000"
  },
  "E444": {
   "line": "too much enemy influence on the formation box at that spot?",
   "unit": "game/trainerAiE000"
  },
  "E447": {
   "line": "rejected - stop extending this rank",
   "unit": "game/trainerAiE000"
  },
  "E449": {
   "line": "candidate heading",
   "unit": "game/trainerAiE000"
  },
  "E44B": {
   "line": "candidate distance",
   "unit": "game/trainerAiE000"
  },
  "E44D": {
   "line": "does the cell itself stay on the 40x40 map?",
   "unit": "game/trainerAiE000"
  },
  "E450": {
   "line": "off the map - stop extending",
   "unit": "game/trainerAiE000"
  },
  "E452": {
   "line": "accept the new distance",
   "unit": "game/trainerAiE000"
  },
  "E454": {
   "line": "remember it as the distance reached on this rank",
   "unit": "game/trainerAiE000"
  },
  "E456": {
   "line": "as far as the group wanted to go?",
   "unit": "game/trainerAiE000"
  },
  "E459": {
   "line": "yes - take it",
   "unit": "game/trainerAiE000"
  },
  "E45B": {
   "line": "rank",
   "unit": "game/trainerAiE000"
  },
  "E45D": {
   "line": "per-rank distance cap: 5,5,5,3,3,1,1,1",
   "unit": "game/trainerAiE000"
  },
  "E460": {
   "line": "cap reached - take it",
   "unit": "game/trainerAiE000"
  },
  "E462": {
   "line": "keep pushing outward along this rank",
   "unit": "game/trainerAiE000"
  },
  "E464": {
   "line": "distance already accepted on this rank",
   "unit": "game/trainerAiE000"
  },
  "E466": {
   "line": "far enough anyway?",
   "unit": "game/trainerAiE000"
  },
  "E469": {
   "line": "yes - take it",
   "unit": "game/trainerAiE000"
  },
  "E46B": {
   "line": "rank",
   "unit": "game/trainerAiE000"
  },
  "E46D": {
   "line": "per-rank cap",
   "unit": "game/trainerAiE000"
  },
  "E470": {
   "line": "at the cap - take it",
   "unit": "game/trainerAiE000"
  },
  "E472": {
   "line": "try the next-best direction",
   "unit": "game/trainerAiE000"
  },
  "E474": {
   "line": "the new rank index",
   "unit": "game/trainerAiE000"
  },
  "E476": {
   "line": "the fan holds 8 candidate directions",
   "unit": "game/trainerAiE000"
  },
  "E478": {
   "line": "still ranks left to try",
   "unit": "game/trainerAiE000"
  },
  "E47A": {
   "line": "nothing worked at all: stay put",
   "unit": "game/trainerAiE000"
  },
  "E47C": {
   "line": "distance 0...",
   "unit": "game/trainerAiE000"
  },
  "E47E": {
   "line": "...and direction 0",
   "unit": "game/trainerAiE000"
  },
  "E480": {
   "block": [
    "Turn the chosen (direction, distance) pair back into an absolute cell and fall into the order issuer."
   ],
   "line": "chosen heading",
   "unit": "game/trainerAiE000"
  },
  "E482": {
   "line": "its column step -1/0/+1",
   "unit": "game/trainerAiE000"
  },
  "E485": {
   "line": "X = column step",
   "unit": "game/trainerAiE000"
  },
  "E486": {
   "line": "chosen distance",
   "unit": "game/trainerAiE000"
  },
  "E488": {
   "line": "search origin column",
   "unit": "game/trainerAiE000"
  },
  "E48A": {
   "line": "target column = origin + step * distance, clamped to 0..39",
   "unit": "game/trainerAiE000"
  },
  "E48D": {
   "line": "chosen target column",
   "unit": "game/trainerAiE000"
  },
  "E48F": {
   "line": "chosen heading",
   "unit": "game/trainerAiE000"
  },
  "E491": {
   "line": "its row step -1/0/+1",
   "unit": "game/trainerAiE000"
  },
  "E494": {
   "line": "X = row step",
   "unit": "game/trainerAiE000"
  },
  "E495": {
   "line": "chosen distance",
   "unit": "game/trainerAiE000"
  },
  "E497": {
   "line": "search origin row",
   "unit": "game/trainerAiE000"
  },
  "E499": {
   "line": "target row, clamped",
   "unit": "game/trainerAiE000"
  },
  "E49C": {
   "line": "chosen target row - fall into the order issuer",
   "unit": "game/trainerAiE000"
  },
  "E49E": {
   "block": [
    "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."
   ],
   "line": "walk AI ordinals 49..0",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E4A0": {
   "line": "group id of this unit",
   "unit": "game/trainerAiE000"
  },
  "E4A3": {
   "line": "the group being ordered",
   "unit": "game/trainerAiE000"
  },
  "E4A6": {
   "line": "not a member",
   "unit": "game/trainerAiE000"
  },
  "E4A8": {
   "line": "current round",
   "unit": "game/trainerAiE000"
  },
  "E4AB": {
   "line": "stamp the member's replan clock ($8800+ordinal)",
   "unit": "game/trainerAiE000"
  },
  "E4AE": {
   "line": "next ordinal",
   "unit": "game/trainerAiE000"
  },
  "E4AF": {
   "line": "loop over all 50 slots",
   "unit": "game/trainerAiE000"
  },
  "E4B1": {
   "block": [
    "Work out where the formation may actually stand and how long it should be left alone afterwards."
   ],
   "line": "clip the target so the whole formation box stays on the map; leaves the delta in zp_2A/zp_2B",
   "unit": "game/trainerAiE000"
  },
  "E4B4": {
   "line": "distance from the group centre to the clipped target",
   "unit": "game/trainerAiE000"
  },
  "E4B7": {
   "line": "does the group contain GRUNTs or BOOMERs?",
   "unit": "game/trainerAiE000"
  },
  "E4BA": {
   "line": "all fast - use the raw distance",
   "unit": "game/trainerAiE000"
  },
  "E4BC": {
   "line": "slow group - leave it alone twice as long",
   "unit": "game/trainerAiE000"
  },
  "E4BD": {
   "line": "replan delay in rounds",
   "unit": "game/trainerAiE000"
  },
  "E4BF": {
   "line": "minimum 4 rounds",
   "unit": "game/trainerAiE000"
  },
  "E4C1": {
   "line": "already at least 4",
   "unit": "game/trainerAiE000"
  },
  "E4C3": {
   "line": "floor the delay at 4",
   "unit": "game/trainerAiE000"
  },
  "E4C5": {
   "line": "store the floored delay",
   "unit": "game/trainerAiE000"
  },
  "E4C7": {
   "line": "urgency of this group (2 home guard, 3 comcen escort / committed attack, 5 default)",
   "unit": "game/trainerAiE000"
  },
  "E4CA": {
   "line": "is the delay longer than the urgency allows?",
   "unit": "game/trainerAiE000"
  },
  "E4CC": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E4CE": {
   "line": "cap the delay at the urgency",
   "unit": "game/trainerAiE000"
  },
  "E4D0": {
   "block": [
    "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."
   ],
   "line": "walk AI ordinals 49..0 again",
   "unit": "game/trainerAiE000"
  },
  "E4D2": {
   "line": "zp_22 is the loop counter for this pass",
   "unit": "game/trainerAiE000"
  },
  "E4D4": {
   "line": "current ordinal",
   "unit": "game/trainerAiE000"
  },
  "E4D6": {
   "line": "its group id",
   "unit": "game/trainerAiE000"
  },
  "E4D9": {
   "line": "the group being ordered",
   "unit": "game/trainerAiE000"
  },
  "E4DC": {
   "line": "not a member",
   "unit": "game/trainerAiE000"
  },
  "E4DE": {
   "line": "its replan clock",
   "unit": "game/trainerAiE000"
  },
  "E4E1": {
   "line": "must still carry the stamp written above",
   "unit": "game/trainerAiE000"
  },
  "E4E4": {
   "line": "a formation mate already re-planned it",
   "unit": "game/trainerAiE000"
  },
  "E4E6": {
   "line": "the unit's own ordered destination column",
   "unit": "game/trainerAiE000"
  },
  "E4E9": {
   "line": "drop the flag bits, keep the 0-39 cell",
   "unit": "game/trainerAiE000"
  },
  "E4EB": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E4EC": {
   "line": "shift it by the group's column delta",
   "unit": "game/trainerAiE000"
  },
  "E4EE": {
   "line": "X = target column",
   "unit": "game/trainerAiE000"
  },
  "E4EF": {
   "line": "its ordered destination row",
   "unit": "game/trainerAiE000"
  },
  "E4F2": {
   "line": "drop the flag bits, keep the 0-39 cell",
   "unit": "game/trainerAiE000"
  },
  "E4F4": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E4F5": {
   "line": "shift it by the group's row delta",
   "unit": "game/trainerAiE000"
  },
  "E4F7": {
   "line": "queue $80 MOVE (or $81 MOVE GROUP) for this unit",
   "unit": "game/trainerAiE000"
  },
  "E4FA": {
   "line": "ordinal again",
   "unit": "game/trainerAiE000"
  },
  "E4FC": {
   "line": "current round",
   "unit": "game/trainerAiE000"
  },
  "E4FF": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E500": {
   "line": "next replan in 'delay' rounds",
   "unit": "game/trainerAiE000"
  },
  "E502": {
   "line": "no underflow",
   "unit": "game/trainerAiE000"
  },
  "E504": {
   "line": "clamp at round 0",
   "unit": "game/trainerAiE000"
  },
  "E506": {
   "line": "new replan clock for this unit",
   "unit": "game/trainerAiE000"
  },
  "E509": {
   "line": "keep a copy for the formation mates",
   "unit": "game/trainerAiE000"
  },
  "E50B": {
   "line": "the unit's authoritative flags byte",
   "unit": "game/trainerAiE000"
  },
  "E50E": {
   "line": "bits 2-6: in-game group id (2-4), side (5) and the 'in a group' bit (6)",
   "unit": "game/trainerAiE000"
  },
  "E510": {
   "line": "bit 6 clear means the unit is not in a player-style group",
   "unit": "game/trainerAiE000"
  },
  "E512": {
   "line": "no formation mates to update",
   "unit": "game/trainerAiE000"
  },
  "E514": {
   "line": "the group key to match",
   "unit": "game/trainerAiE000"
  },
  "E516": {
   "line": "scan all 50 AI ordinals",
   "unit": "game/trainerAiE000"
  },
  "E518": {
   "line": "flags of that unit",
   "unit": "game/trainerAiE000"
  },
  "E51B": {
   "line": "same mask",
   "unit": "game/trainerAiE000"
  },
  "E51D": {
   "line": "same in-game group?",
   "unit": "game/trainerAiE000"
  },
  "E51F": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E521": {
   "line": "the replan clock just computed",
   "unit": "game/trainerAiE000"
  },
  "E523": {
   "line": "give the formation mate the same clock, so the group is not ordered twice",
   "unit": "game/trainerAiE000"
  },
  "E526": {
   "line": "next ordinal",
   "unit": "game/trainerAiE000"
  },
  "E527": {
   "line": "loop over all 50 slots",
   "unit": "game/trainerAiE000"
  },
  "E529": {
   "line": "next ordinal",
   "unit": "game/trainerAiE000"
  },
  "E52B": {
   "line": "all 50 slots done",
   "unit": "game/trainerAiE000"
  },
  "E52D": {
   "line": "keep going (out of branch range)",
   "unit": "game/trainerAiE000"
  },
  "E530": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E531": {
   "line": "remember which group is being measured",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E534": {
   "line": "0 for the accumulators",
   "unit": "game/trainerAiE000"
  },
  "E536": {
   "line": "members that have not scored both end zones",
   "unit": "game/trainerAiE000"
  },
  "E538": {
   "line": "combat value low byte",
   "unit": "game/trainerAiE000"
  },
  "E53B": {
   "line": "combat value high byte",
   "unit": "game/trainerAiE000"
  },
  "E53E": {
   "line": "zp_24 = highest destination column seen (max tracker starts at 0)",
   "unit": "game/trainerAiE000"
  },
  "E540": {
   "line": "zp_25 = highest destination row seen",
   "unit": "game/trainerAiE000"
  },
  "E542": {
   "line": "no slow units found yet",
   "unit": "game/trainerAiE000"
  },
  "E545": {
   "line": "$FF for the minimum trackers",
   "unit": "game/trainerAiE000"
  },
  "E546": {
   "line": "zp_22 = lowest destination column seen",
   "unit": "game/trainerAiE000"
  },
  "E548": {
   "line": "zp_23 = lowest destination row seen",
   "unit": "game/trainerAiE000"
  },
  "E54A": {
   "block": [
    "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."
   ],
   "line": "walk AI ordinals 49..0",
   "unit": "game/trainerAiE000"
  },
  "E54C": {
   "line": "group id of this unit",
   "unit": "game/trainerAiE000"
  },
  "E54F": {
   "line": "the group being measured",
   "unit": "game/trainerAiE000"
  },
  "E552": {
   "line": "not a member",
   "unit": "game/trainerAiE000"
  },
  "E554": {
   "line": "type byte",
   "unit": "game/trainerAiE000"
  },
  "E557": {
   "line": "dead",
   "unit": "game/trainerAiE000"
  },
  "E559": {
   "line": "bits 0-2 = unit type 0-4",
   "unit": "game/trainerAiE000"
  },
  "E55B": {
   "line": "X = unit type, used as the index into unitCombatValueTable",
   "unit": "game/trainerAiE000"
  },
  "E55C": {
   "line": "type 0 GRUNT is slow",
   "unit": "game/trainerAiE000"
  },
  "E55E": {
   "line": "type 2 BOOMER is slow too",
   "unit": "game/trainerAiE000"
  },
  "E560": {
   "line": "RIDER, SPY and COMCEN are not",
   "unit": "game/trainerAiE000"
  },
  "E562": {
   "line": "$FF = the group contains slow units",
   "unit": "game/trainerAiE000"
  },
  "E564": {
   "line": "flag the group as slow",
   "unit": "game/trainerAiE000"
  },
  "E567": {
   "line": "combat weight of this type (2,2,3,0,0)",
   "unit": "game/trainerAiE000"
  },
  "E56A": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E56B": {
   "line": "add it into the 16-bit group value",
   "unit": "game/trainerAiE000"
  },
  "E56E": {
   "line": "new low byte",
   "unit": "game/trainerAiE000"
  },
  "E571": {
   "line": "high byte",
   "unit": "game/trainerAiE000"
  },
  "E574": {
   "line": "carry into the high byte",
   "unit": "game/trainerAiE000"
  },
  "E576": {
   "line": "new high byte",
   "unit": "game/trainerAiE000"
  },
  "E579": {
   "line": "this unit's 'has scored both end zones' flag",
   "unit": "game/trainerAiE000"
  },
  "E57C": {
   "line": "already deep in the player's half",
   "unit": "game/trainerAiE000"
  },
  "E57E": {
   "line": "still at home - count it",
   "unit": "game/trainerAiE000"
  },
  "E580": {
   "line": "its ordered destination column",
   "unit": "game/trainerAiE000"
  },
  "E583": {
   "line": "strip the flag bits",
   "unit": "game/trainerAiE000"
  },
  "E585": {
   "line": "below the running minimum?",
   "unit": "game/trainerAiE000"
  },
  "E587": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E589": {
   "line": "new leftmost destination",
   "unit": "game/trainerAiE000"
  },
  "E58B": {
   "line": "above the running maximum?",
   "unit": "game/trainerAiE000"
  },
  "E58D": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E58F": {
   "line": "new rightmost destination",
   "unit": "game/trainerAiE000"
  },
  "E591": {
   "line": "its ordered destination row",
   "unit": "game/trainerAiE000"
  },
  "E594": {
   "line": "strip the flag bits",
   "unit": "game/trainerAiE000"
  },
  "E596": {
   "line": "below the running minimum?",
   "unit": "game/trainerAiE000"
  },
  "E598": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E59A": {
   "line": "new topmost destination",
   "unit": "game/trainerAiE000"
  },
  "E59C": {
   "line": "above the running maximum?",
   "unit": "game/trainerAiE000"
  },
  "E59E": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E5A0": {
   "line": "new bottom-most destination",
   "unit": "game/trainerAiE000"
  },
  "E5A2": {
   "line": "next ordinal",
   "unit": "game/trainerAiE000"
  },
  "E5A3": {
   "line": "loop over all 50 slots",
   "unit": "game/trainerAiE000"
  },
  "E5A5": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E5A8": {
   "line": "is this the assault group?",
   "unit": "game/trainerAiE000"
  },
  "E5AB": {
   "line": "no",
   "unit": "game/trainerAiE000"
  },
  "E5AD": {
   "line": "$8895 = $8864+49 = the AI comcen's own 'has penetrated' flag",
   "unit": "game/trainerAiE000"
  },
  "E5B0": {
   "line": "the comcen is already deep in enemy ground",
   "unit": "game/trainerAiE000"
  },
  "E5B2": {
   "line": "otherwise the assault group counts as one more unit still at home",
   "unit": "game/trainerAiE000"
  },
  "E5B4": {
   "block": [
    "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."
   ],
   "line": "scan ordinals 49..0",
   "unit": "game/trainerAiE000"
  },
  "E5B6": {
   "line": "group id to match",
   "unit": "game/trainerAiE000"
  },
  "E5B9": {
   "line": "average position and average ordered destination of the members",
   "unit": "game/trainerAiE000"
  },
  "E5BC": {
   "line": "Y = average row",
   "unit": "game/trainerAiE000"
  },
  "E5BE": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E5C1": {
   "line": "member count",
   "unit": "game/trainerAiE000"
  },
  "E5C3": {
   "line": "group size",
   "unit": "game/trainerAiE000"
  },
  "E5C6": {
   "line": "the group has members",
   "unit": "game/trainerAiE000"
  },
  "E5C8": {
   "line": "C=1 = empty group",
   "unit": "game/trainerAiE000"
  },
  "E5C9": {
   "line": "nothing more to do",
   "unit": "game/trainerAiE000"
  },
  "E5CA": {
   "line": "X = average column",
   "unit": "game/trainerAiE000"
  },
  "E5CB": {
   "line": "group centre column",
   "unit": "game/trainerAiE000"
  },
  "E5CE": {
   "line": "and the line walker's current column, for the callers",
   "unit": "game/trainerAiE000"
  },
  "E5D0": {
   "line": "average row",
   "unit": "game/trainerAiE000"
  },
  "E5D2": {
   "line": "group centre row",
   "unit": "game/trainerAiE000"
  },
  "E5D5": {
   "line": "and the line walker's current row",
   "unit": "game/trainerAiE000"
  },
  "E5D7": {
   "line": "average ordered destination column",
   "unit": "game/trainerAiE000"
  },
  "E5D9": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E5DA": {
   "line": "minus the leftmost destination",
   "unit": "game/trainerAiE000"
  },
  "E5DC": {
   "line": "= how far the formation reaches left of its centre",
   "unit": "game/trainerAiE000"
  },
  "E5DF": {
   "line": "rightmost destination",
   "unit": "game/trainerAiE000"
  },
  "E5E1": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E5E2": {
   "line": "minus the average",
   "unit": "game/trainerAiE000"
  },
  "E5E4": {
   "line": "clear carry for the +1 below",
   "unit": "game/trainerAiE000"
  },
  "E5E5": {
   "line": "+1 so the box includes the edge cell itself",
   "unit": "game/trainerAiE000"
  },
  "E5E7": {
   "line": "= how far the formation reaches right",
   "unit": "game/trainerAiE000"
  },
  "E5EA": {
   "line": "average ordered destination row",
   "unit": "game/trainerAiE000"
  },
  "E5EC": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E5ED": {
   "line": "minus the topmost destination",
   "unit": "game/trainerAiE000"
  },
  "E5EF": {
   "line": "= reach above the centre",
   "unit": "game/trainerAiE000"
  },
  "E5F2": {
   "line": "bottom-most destination",
   "unit": "game/trainerAiE000"
  },
  "E5F4": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E5F5": {
   "line": "minus the average",
   "unit": "game/trainerAiE000"
  },
  "E5F7": {
   "line": "clear carry for the +1 below",
   "unit": "game/trainerAiE000"
  },
  "E5F8": {
   "line": "+1 for the edge cell",
   "unit": "game/trainerAiE000"
  },
  "E5FA": {
   "line": "= reach below the centre",
   "unit": "game/trainerAiE000"
  },
  "E5FD": {
   "line": "C=0 = the group exists",
   "unit": "game/trainerAiE000"
  },
  "E5FE": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E5FF": {
   "line": "wanted target column",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E601": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E602": {
   "line": "group id",
   "unit": "game/trainerAiE000"
  },
  "E605": {
   "line": "minus the formation's reach to the left",
   "unit": "game/trainerAiE000"
  },
  "E608": {
   "line": "left edge still >= column 0",
   "unit": "game/trainerAiE000"
  },
  "E60A": {
   "line": "too far left: the leftmost the centre may sit at",
   "unit": "game/trainerAiE000"
  },
  "E60D": {
   "line": "push the centre right so the box fits",
   "unit": "game/trainerAiE000"
  },
  "E60F": {
   "line": "target column",
   "unit": "game/trainerAiE000"
  },
  "E611": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E612": {
   "line": "plus the reach to the right",
   "unit": "game/trainerAiE000"
  },
  "E615": {
   "line": "$28 = 40 columns",
   "unit": "game/trainerAiE000"
  },
  "E617": {
   "line": "right edge fits",
   "unit": "game/trainerAiE000"
  },
  "E619": {
   "line": "$27 = column 39; the CMP above left C=1, so this SBC is exact without a SEC",
   "unit": "game/trainerAiE000"
  },
  "E61B": {
   "line": "minus the reach right",
   "unit": "game/trainerAiE000"
  },
  "E61E": {
   "line": "clipped centre column",
   "unit": "game/trainerAiE000"
  },
  "E620": {
   "line": "clipped column",
   "unit": "game/trainerAiE000"
  },
  "E622": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E623": {
   "line": "group id (Y already holds it - a redundant reload)",
   "unit": "game/trainerAiE000"
  },
  "E626": {
   "line": "minus the group's average ordered column",
   "unit": "game/trainerAiE000"
  },
  "E628": {
   "line": "column delta, also kept here for the setup overlay",
   "unit": "game/trainerAiE000"
  },
  "E62B": {
   "line": "column delta the caller adds to every member",
   "unit": "game/trainerAiE000"
  },
  "E62D": {
   "line": "wanted target row",
   "unit": "game/trainerAiE000"
  },
  "E62F": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E630": {
   "line": "group id (redundant reload)",
   "unit": "game/trainerAiE000"
  },
  "E633": {
   "line": "minus the reach upward",
   "unit": "game/trainerAiE000"
  },
  "E636": {
   "line": "top edge still >= row 0",
   "unit": "game/trainerAiE000"
  },
  "E638": {
   "line": "too high: the topmost the centre may sit at",
   "unit": "game/trainerAiE000"
  },
  "E63B": {
   "line": "push the centre down so the box fits",
   "unit": "game/trainerAiE000"
  },
  "E63D": {
   "line": "target row",
   "unit": "game/trainerAiE000"
  },
  "E63F": {
   "line": "prepare the add",
   "unit": "game/trainerAiE000"
  },
  "E640": {
   "line": "plus the reach downward",
   "unit": "game/trainerAiE000"
  },
  "E643": {
   "line": "$28 = 40 rows",
   "unit": "game/trainerAiE000"
  },
  "E645": {
   "line": "bottom edge fits",
   "unit": "game/trainerAiE000"
  },
  "E647": {
   "line": "$27 = row 39",
   "unit": "game/trainerAiE000"
  },
  "E649": {
   "line": "explicit SEC here, unlike the column path above which relies on the CMP carry",
   "unit": "game/trainerAiE000"
  },
  "E64A": {
   "line": "minus the reach down",
   "unit": "game/trainerAiE000"
  },
  "E64D": {
   "line": "clipped centre row",
   "unit": "game/trainerAiE000"
  },
  "E64F": {
   "line": "clipped row",
   "unit": "game/trainerAiE000"
  },
  "E651": {
   "line": "prepare the subtraction",
   "unit": "game/trainerAiE000"
  },
  "E652": {
   "line": "group id (redundant reload)",
   "unit": "game/trainerAiE000"
  },
  "E655": {
   "line": "minus the group's average ordered row",
   "unit": "game/trainerAiE000"
  },
  "E657": {
   "line": "row delta the caller adds to every member",
   "unit": "game/trainerAiE000"
  },
  "E659": {
   "line": "clipped column",
   "unit": "game/trainerAiE000"
  },
  "E65B": {
   "line": "remember it as this group's target cell",
   "unit": "game/trainerAiE000"
  },
  "E65E": {
   "line": "clipped row",
   "unit": "game/trainerAiE000"
  },
  "E660": {
   "line": "and its target row",
   "unit": "game/trainerAiE000"
  },
  "E663": {
   "line": "done",
   "unit": "game/trainerAiE000"
  },
  "E664": {
   "block": [
    "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."
   ],
   "line": "5,5,5,3,3,1,1,1 cells per rank",
   "unit": "game/trainerAiE000"
  },
  "E66C": {
   "line": "patch the direction into the LDX immediate at $E67D so it can be reused for the row table",
   "routine": [
    "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"
   ],
   "unit": "game/trainerAiE000"
  },
  "E66F": {
   "line": "column delta -1/0/+1 of that direction",
   "unit": "game/trainerAiE000"
  },
  "E672": {
   "line": "X = column step for offsetCoordinateClamped",
   "unit": "game/trainerAiE000"
  },
  "E673": {
   "line": "origin column",
   "unit": "game/trainerAiE000"
  },
  "E675": {
   "line": "column + step * distance, clamped to 0..39",
   "unit": "game/trainerAiE000"
  },
  "E678": {
   "line": "left the map - report failure, the row is not computed",
   "unit": "game/trainerAiE000"
  },
  "E67A": {
   "line": "candidate column",
   "unit": "game/trainerAiE000"
  },
  "E67C": {
   "line": "operand patched at $E66C with the direction index",
   "unit": "game/trainerAiE000"
  },
  "E67E": {
   "line": "row delta -1/0/+1 of that direction",
   "unit": "game/trainerAiE000"
  },
  "E681": {
   "line": "X = row step",
   "unit": "game/trainerAiE000"
  },
  "E682": {
   "line": "origin row",
   "unit": "game/trainerAiE000"
  },
  "E684": {
   "line": "row + step * distance, clamped to 0..39",
   "unit": "game/trainerAiE000"
  },
  "E687": {
   "line": "candidate row",
   "unit": "game/trainerAiE000"
  },
  "E689": {
   "line": "C=1 when either coordinate had to be clamped back onto the 40x40 map",
   "unit": "game/trainerAiE000"
  }
 },
 "unit": "game/trainerAiE000",
 "unitLabels": {
  "game/trainerAiE000": {
   "E011": "isOriginateMode",
   "E01E": "rxPacketLength",
   "E01F": "txPacketLength",
   "E020": "rxPacketBuffer",
   "E021": "rxPacketArg1",
   "E022": "rxPacketArg2",
   "E023": "rxPacketArg3",
   "E024": "rxPacketArg4",
   "E027": "txPacketBuffer",
   "E028": "txPacketArg1",
   "E029": "txPacketArg2",
   "E02A": "txPacketArg3",
   "E02E": "hostOutCount",
   "E02F": "peerCommBuildId",
   "E030": "commBuildId",
   "E031": "nmiChainVector",
   "E032": "nmiChainVectorHi",
   "E033": "ciaIcrShadow",
   "E039": "nmiSuspendRequest",
   "E03A": "nmiSuspendAck",
   "E03B": "isLinkActive",
   "E03C": "linkStatus",
   "E03D": "linkStatusSample",
   "E078": "commNoOpRts",
   "E14D": "randomiseCoordinateSameBase",
   "E343": "sendGroupStraightToTarget",
   "E400": "searchGroupPathToTarget"
  }
 },
 "zpComments": {
  "1D": "general scratch: bounds mode flag of computeGroupBounds, division remainder (hi); computeTrainerGroupStats counts the group members that have not scored both end zones here",
  "1E": "general scratch: running terrain height in the line-of-sight walk; computeGroupCentroid leaves the group's average ordered destination column here",
  "1F": "general scratch: line-of-sight rise flag, divide bit counter; computeGroupCentroid leaves the group's average ordered destination row here",
  "22": "map column of point A for distanceSquared/cellDistance; the trainer also uses it as the group's wanted target column, as the direction-rank index in findTrainerAdvanceCell and as the unit loop counter in issueTrainerGroupOrders",
  "23": "scratch: first (clipped) column of an area scan; in the trainer the distance reached so far by the direction search, and the group's topmost ordered destination row",
  "24": "map row of point A for distanceSquared/cellDistance; the trainer's wanted target row and, while computeTrainerGroupStats runs, the group's rightmost ordered destination column",
  "25": "columns left to scan on the current row of an area scan; computeTrainerGroupStats reuses it as the group's bottom-most ordered destination row",
  "26": "rows left to scan in an area scan; the trainer keeps the direction chosen by findTrainerAdvanceCell here",
  "27": "map column of point B (the reference point) for distanceSquared/cellDistance; the trainer's search origin column",
  "28": "map row of point B (the reference point); the trainer's search origin row",
  "29": "index of the last entry written to the area-scan lists at $0400/$0432/$0464; the trainer reuses it as the replan delay in rounds",
  "2A": "scratch: hill-site index in the map generator; in the trainer the group's column movement delta, and the candidate distance inside the direction search",
  "2B": "scratch: lane counter in the map generator; in the trainer the group's row movement delta",
  "92": "line stepper: current map row; computeTrainerGroupStats leaves the group centre row here for the trainer's line and search code",
  "93": "line stepper: current map column; computeTrainerGroupStats leaves the group centre column here",
  "94": "line stepper: target map row; the trainer's wanted group destination row before it is copied into zp_24",
  "95": "line stepper: target map column; the trainer's wanted group destination column before it is copied into zp_22"
 }
}