{
 "chunk": "game_droneAiEE00_EE00",
 "unit": "game/droneAiEE00",
 "unitLabels": {
  "game/droneAiEE00": {
   "EE00": "flyTrainerDroneStep",
   "EE0F": "setRxPacketLengthOne",
   "EE14": "droneStepDone",
   "EE15": "pickDroneAimPointAndDecide",
   "EE19": "loadTargetUnitCell",
   "EE26": "measureRangeToTarget",
   "EE2D": "midCheckDroneOnMapEE2D",
   "EE41": "checkTargetStunned",
   "EE55": "midStoreDetonateThresholdEE55",
   "EE59": "compareScoreWithThreshold",
   "EE5D": "aimAtBlockNearTarget",
   "EE67": "storeAimRow",
   "EE6B": "scoreFourBlocksAroundTarget",
   "EE85": "midScanDiagonalBlockEE85",
   "EE8B": "scoreBlockNorthOfTarget",
   "EE95": "setLineStepperEndpoints",
   "EEA1": "storeLineStartColumn",
   "EEAF": "storeLineStartRow",
   "EEB6": "emitDroneDetonateCommand",
   "EEBE": "mirrorDroneCellIntoPacket",
   "EED0": "maybeJinkDroneHeading",
   "EEDF": "testDroneOnPlayerRadar",
   "EEEB": "requireRadarScreenShown",
   "EEED": "midCheckFuelForJinkEEED",
   "EF01": "requireRoomToWeave",
   "EF0F": "steerDroneAndEmitMove",
   "EF23": "desiredHeadingBelowCurrent",
   "EF25": "pickShorterTurnBackwards",
   "EF2B": "applyHeadingStep",
   "EF2C": "addStepToHeading",
   "EF35": "convertHeadingToStep",
   "EF38": "lookUpDirectionCode",
   "EF48": "readRowDeltaNorthSouth",
   "EF4F": "readColDeltaEastWest",
   "EF53": "storeColDelta",
   "EF57": "readDiagonalDeltaPair",
   "EF58": "loadPackedDiagonalDelta",
   "EF6D": "advanceDroneCell",
   "EF77": "addRowDeltaToDroneRow",
   "EF9A": "storeRxPacketLength",
   "EF9E": "droneDistanceToCell",
   "EFAB": "staleCommTailBytes",
   "EFB3": "staleTailStoreTargetUnit",
   "EFCC": "staleTailReturn",
   "EFCD": "msgInsertDiskTail",
   "EFD0": "msgDataDiskName",
   "EFD4": "msgGameDiskName",
   "EFD8": "msgCustomOff",
   "EFE2": "msgDestroyOff",
   "EFED": "msgAccepted",
   "EFF5": "trailingPadEFF5"
  }
 },
 "dataTypes": {
  "EFCD": [
   "text",
   3
  ],
  "EFD0": [
   "text",
   4
  ],
  "EFD4": [
   "text",
   4
  ],
  "EFD8": [
   "text",
   10
  ],
  "EFE2": [
   "text",
   11
  ],
  "EFED": [
   "text",
   8
  ]
 },
 "notes": {
  "EE00": {
   "unit": "game/droneAiEE00",
   "routine": [
    "flyTrainerDroneStep - one flight step of the solo trainer's drone, run once per lock-step command exchange for as long as this track-29 s4-5 page is the resident $EE00 image.  The whole page has exactly one product: a fake 'packet received from the opponent' in rxPacketBuffer $E020 with its length in rxPacketLength $E01E, holding either $8F (drone released, 1 byte), $8E col row (detonate, 3 bytes) or $8D heading col row (fly one cell, 4 bytes).  Everything else in the game - the command interpreter, the game film, the screens - then treats the AI's drone exactly like a modem opponent's.  This entry only disposes of the two trivial cases (the flight is already over, or the fuel has run out) and falls through into pickDroneAimPointAndDecide for the real decision.",
    "In:  droneArrivalFlags $92F9 (bit 7 = a $92 'drone finished' command has already gone through a packet merger, i.e. the drone was shot down or has arrived), aiDroneFuel $8973 (steps left, $64 = 100 at launch), drone cell $8974/$8975, drone target unit $8976, aiDroneDetonateThreshold $8978, unit arrays $F640/$F6A4/$F960 and the map at $F000",
    "Out: $E01E and $E020.. = the fabricated opponent packet; $8973 decremented; on the flight path also $896F (heading) and $8974/$8975 (cell); zp_18, zp_19, zp_1C, zp_1D, zp_22, zp_24, zp_27, zp_28 and the line-stepper state zp_8C-zp_97 clobbered",
    "Called from: the trainer module's buildTrainerReplyPacket - the 'jsr $EE00' at $E0A5, reached only while droneOverlayState $8977 has bit 7 set (this page loaded).  Note that in the trainer's own $EE00 image the same address is just a fall-through label of fireMissileAtSpottedUnit, so that one JSR means two entirely different things depending on which page is resident."
   ],
   "block": [
    "Is there still a drone to fly, and does it still have fuel?  A drone flight costs two disk loads:",
    "$E0C1-$E0D9 of the trainer loads this page (track 29 s4-5) as soon as droneStateFlags $920D bit 0 says a",
    "drone is airborne, and $E0AB-$E0BE loads the trainer's own tail (track 35 s14-15) back when that bit",
    "clears.  While this page is resident the trainer issues no other orders at all, because all of its other",
    "think routines are in the page that was swapped out."
   ],
   "line": "droneArrivalFlags: bit 7 goes up the moment command $92 (drone finished) passes through mergeIncomingPacket ($4F42) or mergeOutgoingPacket ($4FA8)"
  },
  "EE03": {
   "unit": "game/droneAiEE00",
   "line": "branch if the flight is already over (shot down by the player's missile, or arrived) - emit nothing this exchange"
  },
  "EE05": {
   "unit": "game/droneAiEE00",
   "line": "spend one step of drone fuel; launchAiDrone loaded aiDroneFuel with $64 = 100 steps"
  },
  "EE08": {
   "unit": "game/droneAiEE00",
   "line": "still 0 or more steps left: go and decide where to fly"
  },
  "EE0A": {
   "unit": "game/droneAiEE00",
   "line": "out of fuel: command $8F = cmdDroneRelease ($537B), which for a remote command clears $920D bits 0-5 - that is what makes the trainer swap this page back out next exchange"
  },
  "EE0C": {
   "unit": "game/droneAiEE00",
   "line": "the fabricated packet's only command byte"
  },
  "EE0F": {
   "unit": "game/droneAiEE00",
   "line": "$8F carries no arguments"
  },
  "EE11": {
   "unit": "game/droneAiEE00",
   "line": "one byte of 'received' packet is waiting for mergeIncomingPacket ($4F05)"
  },
  "EE14": {
   "unit": "game/droneAiEE00",
   "line": "back to $E0A8 in the trainer module"
  },
  "EE15": {
   "unit": "game/droneAiEE00",
   "routine": [
    "pickDroneAimPointAndDecide - decides between blowing the drone up where it stands and flying it one more cell.  Within 6 cells of its target unit it scores the 2x2 block the drone covers with scoreTargetBlock ($E9A6) and detonates as soon as that score reaches aiDroneDetonateThreshold $8978 - or only three quarters of it when the target is stunned and so cannot walk out from under the blast.  Otherwise it scores the four 2x2 blocks that touch the target's cell, keeps the best corner as the aim point in zp_95/zp_94, points the line stepper from the drone's cell at it and asks getStepDirection ($E80E) for a compass heading; 'no direction' means the drone is already standing on its aim cell, which detonates as well.",
    "In:  $8976 drone target unit, unit position arrays $F640/$F6A4, drone cell $8974/$8975, aiDroneDetonateThreshold $8978, unitStunTable $F960, the map through scoreTargetBlock",
    "Out: zp_1C = best block score, zp_1D = highest-numbered player unit in it, zp_95/zp_94 = aim column/row, zp_93/zp_92 = line start column/row, Y = heading 0-7 with N clear when a direction was found; continues in emitDroneDetonateCommand or maybeJinkDroneHeading",
    "Called from: nothing - $EE08 falls into it.  It is a block of flyTrainerDroneStep, not a subroutine."
   ],
   "block": [
    "Phase 1 - how far is the drone from the unit it was launched at, and is the cluster underneath it worth",
    "spending the drone on?  zp_1C is the running best/threshold that scoreTargetBlock compares against, so",
    "clearing it here means 'only a block worth 0 or more counts'."
   ],
   "line": "start the scoring with a best-so-far of 0..."
  },
  "EE17": {
   "unit": "game/droneAiEE00",
   "line": "...scoreTargetBlock ignores any 2x2 block that scores below zp_1C"
  },
  "EE19": {
   "unit": "game/droneAiEE00",
   "line": "Y = the unit the drone was sent at, picked once per round by chooseAiDroneTarget ($EEE0 of the trainer's own page)"
  },
  "EE1C": {
   "unit": "game/droneAiEE00",
   "line": "target's map column - unitColTable is the first of the 100-entry unit arrays at $F640, each $64 apart"
  },
  "EE1F": {
   "unit": "game/droneAiEE00",
   "line": "point A of cellDistance = the target's cell"
  },
  "EE21": {
   "unit": "game/droneAiEE00",
   "line": "target's map row (unitRowTable = $F640 + $64)"
  },
  "EE24": {
   "unit": "game/droneAiEE00",
   "line": "point A row"
  },
  "EE26": {
   "unit": "game/droneAiEE00",
   "line": "A = X = distance in cells from the drone to the target; Y survives the call, which is why the target index can stay there"
  },
  "EE29": {
   "unit": "game/droneAiEE00",
   "line": "6 cells is the radius inside which detonating is worth considering"
  },
  "EE2B": {
   "unit": "game/droneAiEE00",
   "line": "branch if the drone is still far out - just pick an aim cell and fly, Y still holds the target unit"
  },
  "EE2D": {
   "unit": "game/droneAiEE00",
   "routine": [
    "midCheckDroneOnMapEE2D - NOT an entry point in this image.  $EE2D is the 'ldx $8974 / bmi' on-map test in the middle of pickDroneAimPointAndDecide; the label exists only because the modem driver (track 34) has a real routine at this address and XREF merges all five images of $EE00-$EFFF.",
    "In:  -",
    "Out: -",
    "Called from: nothing in this image; the two 'call:EDD2,EE13' references in XREF.txt belong to the trainer's own page."
   ],
   "line": "drone column; it is a signed value and can go negative, because the flight code never clamps it to 0..39"
  },
  "EE30": {
   "unit": "game/droneAiEE00",
   "line": "branch if the drone has drifted off the left/top edge of the map - it cannot be standing over anything"
  },
  "EE32": {
   "unit": "game/droneAiEE00",
   "line": "drone row for the block score.  NOTE: this also destroys the target unit index in Y, which is a bug on the branch below"
  },
  "EE35": {
   "unit": "game/droneAiEE00",
   "line": "branch if the drone row is off the map: aimAtBlockNearTarget then indexes the unit arrays with the drone row instead of the target unit index (probable bug, only reachable once the drone has walked off the map)"
  },
  "EE37": {
   "unit": "game/droneAiEE00",
   "line": "score the 2x2 block whose top-left corner is (X,Y) = the drone's cell: the sum of targetValueByUnitType ($E9F1 = grunt 1, rider 2, boomer 4, spy 1, comcen 4) over the units in its four cells, counted positive for the player's units 0-49 and negative for the AI's own 50-99"
  },
  "EE3A": {
   "unit": "game/droneAiEE00",
   "line": "the score of the block under the drone (still 0 if the block scored negative)"
  },
  "EE3C": {
   "unit": "game/droneAiEE00",
   "line": "aiDroneDetonateThreshold, copied at trainer setup ($ECC9) out of aiPlanParamTable ($EC0A = $FF,$FF,$08,$0C,$0C), indexed by the opening play the trainer drew ($8966, 0-4); $FF for plays 0 and 1 means 'never detonate early', so those drones only ever blow up when they reach their aim cell"
  },
  "EE3F": {
   "unit": "game/droneAiEE00",
   "line": "branch if the cluster under the drone is worth the threshold - blow up here"
  },
  "EE41": {
   "unit": "game/droneAiEE00",
   "block": [
    "A stunned target cannot walk out of the blast, so accept a cheaper cluster: three quarters of the",
    "threshold.  unitStunTable $F960 holds the stun/pinned countdown in its low nibble (bits 5-7 are the",
    "dig-in state, bit 7 = dug in)."
   ],
   "line": "Y = the target unit index again (it was overwritten by the drone row at $EE32)"
  },
  "EE44": {
   "unit": "game/droneAiEE00",
   "line": "the target's stun byte"
  },
  "EE47": {
   "unit": "game/droneAiEE00",
   "line": "low nibble = stun/pinned countdown; nonzero is what makes the status panel print STUN'D"
  },
  "EE49": {
   "unit": "game/droneAiEE00",
   "line": "branch if the target is not stunned - keep the full threshold and fly on"
  },
  "EE4B": {
   "unit": "game/droneAiEE00",
   "line": "aiDroneDetonateThreshold T"
  },
  "EE4E": {
   "unit": "game/droneAiEE00",
   "line": "T/2"
  },
  "EE4F": {
   "unit": "game/droneAiEE00",
   "line": "park T/2"
  },
  "EE51": {
   "unit": "game/droneAiEE00",
   "line": "T/4"
  },
  "EE52": {
   "unit": "game/droneAiEE00",
   "line": "clear carry before the add (the LSRs left it holding a shifted-out bit)"
  },
  "EE53": {
   "unit": "game/droneAiEE00",
   "line": "T/4 + T/2 = 3T/4"
  },
  "EE55": {
   "unit": "game/droneAiEE00",
   "routine": [
    "midStoreDetonateThresholdEE55 - NOT an entry point in this image.  $EE55 is the 'sta zp_18' that parks the relaxed (3/4) detonate threshold used against a stunned target, inside pickDroneAimPointAndDecide.  The label is imported from the other images of this page.",
    "In:  -",
    "Out: -",
    "Called from: nothing in this image."
   ],
   "line": "zp_18 = the relaxed threshold"
  },
  "EE57": {
   "unit": "game/droneAiEE00",
   "line": "score of the block under the drone"
  },
  "EE59": {
   "unit": "game/droneAiEE00",
   "line": "test it against three quarters of the threshold"
  },
  "EE5B": {
   "unit": "game/droneAiEE00",
   "line": "good enough over a stunned target - detonate"
  },
  "EE5D": {
   "unit": "game/droneAiEE00",
   "block": [
    "Phase 2 - choose an aim cell.  The drone is a 2x2 object addressed by its top-left corner, so the four",
    "corners that put the target's own cell inside the drone footprint are (col,row), (col-1,row),",
    "(col-1,row-1) and (col,row-1).  Each is handed to scoreTargetBlock, which keeps the best score in zp_1C,",
    "the highest player unit index in it in zp_1D and writes the winning corner straight into",
    "lineEndCol/lineEndRow (zp_95/zp_94) - exactly the two bytes the line stepper wants as its end point.",
    "Y = the target unit index on entry, except on the buggy path from $EE35."
   ],
   "line": "target's column (Y is the target unit index; see the note at $EE32 for the one path where it is not)"
  },
  "EE60": {
   "unit": "game/droneAiEE00",
   "line": "zp_18 = target column, kept because X gets reloaded for each of the four corners"
  },
  "EE62": {
   "unit": "game/droneAiEE00",
   "line": "seed the stepper's end column with the target cell itself, in case all four blocks score negative and none of them is recorded"
  },
  "EE64": {
   "unit": "game/droneAiEE00",
   "line": "target's row"
  },
  "EE67": {
   "unit": "game/droneAiEE00",
   "line": "zp_19 = target row"
  },
  "EE69": {
   "unit": "game/droneAiEE00",
   "line": "seed the end row the same way"
  },
  "EE6B": {
   "unit": "game/droneAiEE00",
   "line": "$FF = 'no best block yet': scoreTargetBlock's 'bit zp_1C / bpl' test accepts any score when the running best is negative"
  },
  "EE6D": {
   "unit": "game/droneAiEE00",
   "line": "arm the scan"
  },
  "EE6F": {
   "unit": "game/droneAiEE00",
   "line": "corner 1: X = the target's own column..."
  },
  "EE71": {
   "unit": "game/droneAiEE00",
   "line": "...Y = its row, i.e. the block covering (col,row)..(col+1,row+1)"
  },
  "EE73": {
   "unit": "game/droneAiEE00",
   "line": "score it; on an improvement the corner lands in zp_95/zp_94 and the best player unit in zp_1D"
  },
  "EE76": {
   "unit": "game/droneAiEE00",
   "line": "corner 2: target column..."
  },
  "EE78": {
   "unit": "game/droneAiEE00",
   "line": "...minus one"
  },
  "EE79": {
   "unit": "game/droneAiEE00",
   "line": "skip both left-hand corners when the target stands in column 0"
  },
  "EE7B": {
   "unit": "game/droneAiEE00",
   "line": "same row"
  },
  "EE7D": {
   "unit": "game/droneAiEE00",
   "line": "score the block at (col-1,row)"
  },
  "EE80": {
   "unit": "game/droneAiEE00",
   "line": "corner 3: column..."
  },
  "EE82": {
   "unit": "game/droneAiEE00",
   "line": "...minus one again"
  },
  "EE83": {
   "unit": "game/droneAiEE00",
   "line": "target row..."
  },
  "EE85": {
   "unit": "game/droneAiEE00",
   "routine": [
    "midScanDiagonalBlockEE85 - NOT an entry point in this image.  $EE85 is the 'dey' that turns corner (col-1,row) into the diagonal corner (col-1,row-1) inside the four-block scan of pickDroneAimPointAndDecide.  The label is an artifact of the T34/T35 images of this page (in the trainer's own page $EE85 is launchAiDrone).",
    "In:  -",
    "Out: -",
    "Called from: nothing in this image."
   ],
   "line": "...minus one: the diagonal corner (col-1,row-1)"
  },
  "EE86": {
   "unit": "game/droneAiEE00",
   "line": "skip it when the target stands in row 0"
  },
  "EE88": {
   "unit": "game/droneAiEE00",
   "line": "score the block at (col-1,row-1)"
  },
  "EE8B": {
   "unit": "game/droneAiEE00",
   "line": "corner 4: the target's own column"
  },
  "EE8D": {
   "unit": "game/droneAiEE00",
   "line": "target row..."
  },
  "EE8F": {
   "unit": "game/droneAiEE00",
   "line": "...minus one"
  },
  "EE90": {
   "unit": "game/droneAiEE00",
   "line": "skip when the target stands in row 0"
  },
  "EE92": {
   "unit": "game/droneAiEE00",
   "line": "score the block at (col,row-1)"
  },
  "EE95": {
   "unit": "game/droneAiEE00",
   "block": [
    "Phase 3 - take one Bresenham step from the drone towards the chosen corner.  initLineStepper ($FBB8)",
    "wants the start cell in zp_93/zp_92 (column/row) and the end cell in zp_95/zp_94, and getStepDirection",
    "($E80E) runs it once and turns the resulting per-step deltas into a compass direction 0-7 (clockwise",
    "from north) in Y.  A drone coordinate that has gone negative is clamped to 0 with the end point shifted",
    "by the same amount - except that the subtraction is written the wrong way round, so the shifted end",
    "point comes out negated (see the line comments)."
   ],
   "line": "drone column"
  },
  "EE98": {
   "unit": "game/droneAiEE00",
   "line": "branch if it is still a sane 0..39 value - use it as the line start"
  },
  "EE9A": {
   "unit": "game/droneAiEE00",
   "line": "prepare the borrow-free subtract"
  },
  "EE9B": {
   "unit": "game/droneAiEE00",
   "line": "A = droneCol - aimCol.  To clamp the start to 0 and keep the vector, the end point needs aimCol - droneCol; this computes the negative of that (probable bug - reachable only after the drone has walked off the left edge)"
  },
  "EE9D": {
   "unit": "game/droneAiEE00",
   "line": "store the shifted end column"
  },
  "EE9F": {
   "unit": "game/droneAiEE00",
   "line": "and clamp the start column to the left edge"
  },
  "EEA1": {
   "unit": "game/droneAiEE00",
   "line": "zp_93 = line-stepper start column"
  },
  "EEA3": {
   "unit": "game/droneAiEE00",
   "line": "drone row, same treatment"
  },
  "EEA6": {
   "unit": "game/droneAiEE00",
   "line": "branch if the row is on the map"
  },
  "EEA8": {
   "unit": "game/droneAiEE00",
   "line": "prepare the subtract"
  },
  "EEA9": {
   "unit": "game/droneAiEE00",
   "line": "A = droneRow - aimRow (the same sign slip as the column above)"
  },
  "EEAB": {
   "unit": "game/droneAiEE00",
   "line": "shifted end row"
  },
  "EEAD": {
   "unit": "game/droneAiEE00",
   "line": "clamp the start row to the top edge"
  },
  "EEAF": {
   "unit": "game/droneAiEE00",
   "line": "zp_92 = line-stepper start row"
  },
  "EEB1": {
   "unit": "game/droneAiEE00",
   "line": "one line step from the drone towards the aim cell, converted to a heading: Y = 0-7 with N clear when the step deltas matched dirToColDelta/dirToRowDelta ($E68A/$E693), or Y = $FF with N set when nothing matched"
  },
  "EEB4": {
   "unit": "game/droneAiEE00",
   "line": "branch with a usable heading.  Falling through means the stepper did not move at all, i.e. the drone is already standing on its aim cell - so detonate"
  },
  "EEB6": {
   "unit": "game/droneAiEE00",
   "routine": [
    "emitDroneDetonateCommand - writes the 3-byte command $8E (detonate) plus the drone's current cell into the fabricated received packet.  Both coordinates go through mirrorBlockCornerForOpponent ($EA38, v -> 39-(v+1) = 38-v) because the engine treats everything in that buffer as having arrived from the remote player and runs mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) over the parameters, which turns them back into the real cell.  The handler cmdDroneDetonate ($5336) then plays the blast and clears $920D bits 0-5, ending the flight.",
    "In:  drone cell $8974/$8975 (top-left corner of the 2x2 drone)",
    "Out: $E020 = $8E, $E021/$E022 = pre-mirrored column/row, $E01E = 3; A, X, Y clobbered",
    "Called from: $EE3F (the cluster under the drone is worth the threshold), $EE5B (relaxed threshold over a stunned target) and $EEB4 (the drone is already on its aim cell).  All three are branches, not calls."
   ],
   "line": "command $8E = cmdDroneDetonate ($5336)"
  },
  "EEB8": {
   "unit": "game/droneAiEE00",
   "line": "the packet's command byte"
  },
  "EEBB": {
   "unit": "game/droneAiEE00",
   "line": "X = drone column"
  },
  "EEBE": {
   "unit": "game/droneAiEE00",
   "line": "Y = drone row"
  },
  "EEC1": {
   "unit": "game/droneAiEE00",
   "line": "pre-compensate the engine's remote-coordinate mirror: X = 38-X, Y = 38-Y"
  },
  "EEC4": {
   "unit": "game/droneAiEE00",
   "line": "argument 1 = column as the player's machine will read it"
  },
  "EEC7": {
   "unit": "game/droneAiEE00",
   "line": "argument 2 = row"
  },
  "EECA": {
   "unit": "game/droneAiEE00",
   "line": "command byte plus two arguments"
  },
  "EECC": {
   "unit": "game/droneAiEE00",
   "line": "hand the 3-byte packet to the engine"
  },
  "EECF": {
   "unit": "game/droneAiEE00",
   "line": "back to $E0A8"
  },
  "EED0": {
   "unit": "game/droneAiEE00",
   "routine": [
    "maybeJinkDroneHeading - takes the heading getStepDirection worked out and, under a narrow set of conditions, throws it away for a random one.  The drone only weaves when the human could actually shoot it down: he still has missile strikes, the drone's cell is inside the 21x17 radar window centred on his comcen, that radar/missile screen is the one currently on show, the drone has at least 12 steps of fuel left and it is at least 3 cells from its aim point.  Then two exchanges in three (nextGameRandom >= $56) the heading becomes a random 0-7.  Since steerDroneAndEmitMove turns at most one 45-degree step per exchange the result is a wobble, not a jump.  Purpose inferred; medium confidence.",
    "In:  Y = heading 0-7 from getStepDirection, missilesLeft $92B0 (the LOCAL player's missiles), drone cell $8974/$8975, comcenUnit $9236, currentScreen $90FB, aiDroneFuel $8973, aim cell zp_95/zp_94",
    "Out: zp_19 = the heading steerDroneAndEmitMove should turn towards; zp_22/zp_24/zp_27/zp_28 clobbered; falls into steerDroneAndEmitMove",
    "Called from: nothing - $EEB4 branches into it."
   ],
   "block": [
    "Evasive weave.  Every one of the five tests below asks the same question from a different angle: can the",
    "human see this drone and do something about it?  If not, the drone flies the straight line."
   ],
   "line": "remember the heading the line stepper asked for"
  },
  "EED2": {
   "unit": "game/droneAiEE00",
   "line": "missile strikes the LOCAL (human) player has left - $9236/$92B0 are all the local player's own state"
  },
  "EED5": {
   "unit": "game/droneAiEE00",
   "line": "he cannot shoot at all - fly straight in"
  },
  "EED7": {
   "unit": "game/droneAiEE00",
   "line": "X = drone column for the radar-window test"
  },
  "EEDA": {
   "unit": "game/droneAiEE00",
   "line": "redundant store: droneDistanceToCell ($EF9E) writes the same two bytes again before anything reads them"
  },
  "EEDC": {
   "unit": "game/droneAiEE00",
   "line": "Y = drone row"
  },
  "EEDF": {
   "unit": "game/droneAiEE00",
   "line": "redundant in the same way"
  },
  "EEE1": {
   "unit": "game/droneAiEE00",
   "line": "overlay B helper: X = col + 10 - comcenCol, Y = row + 8 - comcenRow, C = 1 when the cell falls outside the 21x17 radar grid.  $9236 is deliberately left alone, so this is the HUMAN player's comcen window - unlike isUnitOnAiRadar ($EE6A of the trainer's own page), which forces $9236 to $63 (unit 99) first"
  },
  "EEE4": {
   "unit": "game/droneAiEE00",
   "line": "off the player's radar - nothing to hide from"
  },
  "EEE6": {
   "unit": "game/droneAiEE00",
   "line": "which game screen is on show"
  },
  "EEE9": {
   "unit": "game/droneAiEE00",
   "line": "screen 1 = the comcen RADAR / MISSILE screen, the only place a missile strike can be aimed"
  },
  "EEEB": {
   "unit": "game/droneAiEE00",
   "line": "the player is not watching - fly straight"
  },
  "EEED": {
   "unit": "game/droneAiEE00",
   "routine": [
    "midCheckFuelForJinkEEED - NOT an entry point in this image.  $EEED is the 'lda $8973 / cmp #$0C' fuel test inside maybeJinkDroneHeading.  The label is an artifact of the other images of this page.",
    "In:  -",
    "Out: -",
    "Called from: nothing in this image."
   ],
   "line": "steps of fuel left"
  },
  "EEF0": {
   "unit": "game/droneAiEE00",
   "line": "12 steps"
  },
  "EEF2": {
   "unit": "game/droneAiEE00",
   "line": "nearly out of fuel - stop wasting steps and go straight in"
  },
  "EEF4": {
   "unit": "game/droneAiEE00",
   "line": "aim column chosen by the four-block scan"
  },
  "EEF6": {
   "unit": "game/droneAiEE00",
   "line": "point A of cellDistance = the aim cell"
  },
  "EEF8": {
   "unit": "game/droneAiEE00",
   "line": "aim row"
  },
  "EEFA": {
   "unit": "game/droneAiEE00",
   "line": "point A row"
  },
  "EEFC": {
   "unit": "game/droneAiEE00",
   "line": "distance from the drone to its aim point"
  },
  "EEFF": {
   "unit": "game/droneAiEE00",
   "line": "3 cells"
  },
  "EF01": {
   "unit": "game/droneAiEE00",
   "line": "almost there - do not risk missing the target for the sake of dodging"
  },
  "EF03": {
   "unit": "game/droneAiEE00",
   "line": "dead instruction: the distance is stashed in X but nothing reads it, and steerDroneAndEmitMove reloads X a few instructions later"
  },
  "EF04": {
   "unit": "game/droneAiEE00",
   "line": "the deterministic game RNG on $57-$59; it preserves X and Y"
  },
  "EF07": {
   "unit": "game/droneAiEE00",
   "line": "$56 = 86, so 170 of the 256 possible bytes pass: weave about two exchanges in three"
  },
  "EF09": {
   "unit": "game/droneAiEE00",
   "line": "no weave this exchange"
  },
  "EF0B": {
   "unit": "game/droneAiEE00",
   "line": "a random compass heading 0-7"
  },
  "EF0D": {
   "unit": "game/droneAiEE00",
   "line": "replace the sensible heading with it"
  },
  "EF0F": {
   "unit": "game/droneAiEE00",
   "routine": [
    "steerDroneAndEmitMove - turns the drone at most one 45-degree step towards the desired heading, moves it one cell that way and reports the move as command $8D.  Instruction for instruction this is overlay B's steerDrone ($731E-$7391), the routine that flies the human player's drone, with the AI's variables substituted ($896F/$8974/$8975 for droneHeading/droneCol/droneRow $920B/$92F6/$92F7), with the desired heading read from zp_19 instead of looked up from the joystick, and with the turn sound ($10) removed.  That duplication is useful evidence: the heading encoding and the three lookup tables are shared between the player's drone and the AI's.  Neither coordinate is clamped, so a drone that keeps missing can walk clean off the 40x40 map.",
    "In:  zp_19 = desired heading 0-7 (clockwise from north), $896F = the drone's current heading, $8974/$8975 = drone cell",
    "Out: $896F turned by at most one step, $8974/$8975 advanced by one cell, $E020 = $8D, $E021 = (heading + 4) & 7, $E022/$E023 = pre-mirrored column/row, $E01E = 4",
    "Called from: nothing - it is the fall-through of maybeJinkDroneHeading and the target of all six 'do not weave' branches at $EED5, $EEE4, $EEEB, $EEF2, $EF01 and $EF09."
   ],
   "block": [
    "Turn towards the desired heading the short way round.  Headings are 0-7 clockwise from north, so the",
    "difference is read modulo 8: a positive difference of 1..3 turns clockwise (+1) and one of 4..7 is",
    "shorter anticlockwise (-1); a negative difference of -1..-4 turns anticlockwise and -5..-7 clockwise."
   ],
   "line": "desired heading"
  },
  "EF11": {
   "unit": "game/droneAiEE00",
   "line": "prepare the subtract"
  },
  "EF12": {
   "unit": "game/droneAiEE00",
   "line": "difference from the drone's current heading"
  },
  "EF15": {
   "unit": "game/droneAiEE00",
   "line": "already pointing the right way - no turn at all"
  },
  "EF17": {
   "unit": "game/droneAiEE00",
   "line": "branch when the desired heading is the lower number (difference came out negative)"
  },
  "EF19": {
   "unit": "game/droneAiEE00",
   "line": "assume one step anticlockwise"
  },
  "EF1B": {
   "unit": "game/droneAiEE00",
   "line": "half a turn: a positive difference of 4 or more is shorter going the other way"
  },
  "EF1D": {
   "unit": "game/droneAiEE00",
   "line": "difference 4..7 - keep the -1 step"
  },
  "EF1F": {
   "unit": "game/droneAiEE00",
   "line": "difference 1..3: one step clockwise"
  },
  "EF21": {
   "unit": "game/droneAiEE00",
   "line": "always taken (X = 1, so Z is clear)"
  },
  "EF23": {
   "unit": "game/droneAiEE00",
   "line": "negative difference: assume one step clockwise"
  },
  "EF25": {
   "unit": "game/droneAiEE00",
   "line": "$FC = -4: differences of -1..-4 are shorter anticlockwise"
  },
  "EF27": {
   "unit": "game/droneAiEE00",
   "line": "difference -5..-7 - keep the +1 step"
  },
  "EF29": {
   "unit": "game/droneAiEE00",
   "line": "one step anticlockwise"
  },
  "EF2B": {
   "unit": "game/droneAiEE00",
   "line": "A = the step, +1 or -1"
  },
  "EF2C": {
   "unit": "game/droneAiEE00",
   "line": "clear carry for the add"
  },
  "EF2D": {
   "unit": "game/droneAiEE00",
   "line": "apply it to the current heading"
  },
  "EF30": {
   "unit": "game/droneAiEE00",
   "line": "headings wrap inside 0-7"
  },
  "EF32": {
   "unit": "game/droneAiEE00",
   "line": "the drone's new heading"
  },
  "EF35": {
   "unit": "game/droneAiEE00",
   "block": [
    "Turn the heading into a one-cell step.  headingToDirectionTable ($7316) maps heading 0-7",
    "(N,NE,E,SE,S,SW,W,NW) to the game's direction code (0 N, 1 S, 2 E, 3 W, 4 NW, 5 NE, 6 SE, 7 SW).  Codes",
    "0-1 are pure row moves and 2-3 pure column moves, both indexing directionDeltaTable ($20F0 =",
    "-1,+1,+1,-1) directly; codes 4-7 index diagonalDirectionTable ($1FB2+4 = $30,$20,$21,$31), whose high",
    "nibble is the column index and low nibble the row index into that same delta table."
   ],
   "line": "the drone's heading"
  },
  "EF38": {
   "unit": "game/droneAiEE00",
   "line": "direction code 0-7"
  },
  "EF3B": {
   "unit": "game/droneAiEE00",
   "line": "row delta defaults to 'no move'"
  },
  "EF3D": {
   "unit": "game/droneAiEE00",
   "line": "column delta defaults to 'no move'"
  },
  "EF3F": {
   "unit": "game/droneAiEE00",
   "line": "codes 4-7 are the diagonals"
  },
  "EF41": {
   "unit": "game/droneAiEE00",
   "line": "branch to the packed-pair path"
  },
  "EF43": {
   "unit": "game/droneAiEE00",
   "line": "codes 2-3 are east/west"
  },
  "EF45": {
   "unit": "game/droneAiEE00",
   "line": "branch to the column-only path"
  },
  "EF47": {
   "unit": "game/droneAiEE00",
   "line": "codes 0-1 index the delta table directly: entry 0 = -1 (north), entry 1 = +1 (south)"
  },
  "EF48": {
   "unit": "game/droneAiEE00",
   "line": "the row delta"
  },
  "EF4B": {
   "unit": "game/droneAiEE00",
   "line": "keep it in Y; the column delta stays 0"
  },
  "EF4C": {
   "unit": "game/droneAiEE00",
   "line": "join the common move code"
  },
  "EF4F": {
   "unit": "game/droneAiEE00",
   "line": "codes 2-3: entry 2 = +1 (east), entry 3 = -1 (west)"
  },
  "EF50": {
   "unit": "game/droneAiEE00",
   "line": "the column delta"
  },
  "EF53": {
   "unit": "game/droneAiEE00",
   "line": "keep it in X; the row delta stays 0"
  },
  "EF54": {
   "unit": "game/droneAiEE00",
   "line": "join the common move code"
  },
  "EF57": {
   "unit": "game/droneAiEE00",
   "line": "codes 4-7 index the packed diagonal table"
  },
  "EF58": {
   "unit": "game/droneAiEE00",
   "line": "$30 = NW, $20 = NE, $21 = SE, $31 = SW, as (columnIndex << 4) | rowIndex"
  },
  "EF5B": {
   "unit": "game/droneAiEE00",
   "line": "save the packed byte for the second nibble"
  },
  "EF5C": {
   "unit": "game/droneAiEE00",
   "line": "low nibble = index of the row delta"
  },
  "EF5E": {
   "unit": "game/droneAiEE00",
   "line": "index the delta table with it"
  },
  "EF5F": {
   "unit": "game/droneAiEE00",
   "line": "row delta"
  },
  "EF62": {
   "unit": "game/droneAiEE00",
   "line": "Y = row delta"
  },
  "EF63": {
   "unit": "game/droneAiEE00",
   "line": "the packed byte back"
  },
  "EF64": {
   "unit": "game/droneAiEE00",
   "line": "shift the high nibble down..."
  },
  "EF65": {
   "unit": "game/droneAiEE00",
   "line": "...four places..."
  },
  "EF66": {
   "unit": "game/droneAiEE00",
   "line": "...to get the column..."
  },
  "EF67": {
   "unit": "game/droneAiEE00",
   "line": "...delta index"
  },
  "EF68": {
   "unit": "game/droneAiEE00",
   "line": "index the delta table with it"
  },
  "EF69": {
   "unit": "game/droneAiEE00",
   "line": "column delta"
  },
  "EF6C": {
   "unit": "game/droneAiEE00",
   "line": "X = column delta"
  },
  "EF6D": {
   "unit": "game/droneAiEE00",
   "block": [
    "Move the drone one cell and report it.  Nothing here clamps the result to 0..39 - the negative-coordinate",
    "patch-up back at $EE95 is the only thing that copes with a drone that has left the map."
   ],
   "line": "column delta"
  },
  "EF6E": {
   "unit": "game/droneAiEE00",
   "line": "clear carry for the add"
  },
  "EF6F": {
   "unit": "game/droneAiEE00",
   "line": "drone column"
  },
  "EF72": {
   "unit": "game/droneAiEE00",
   "line": "the drone's new column"
  },
  "EF75": {
   "unit": "game/droneAiEE00",
   "line": "X = new column, ready for the mirror call"
  },
  "EF76": {
   "unit": "game/droneAiEE00",
   "line": "row delta"
  },
  "EF77": {
   "unit": "game/droneAiEE00",
   "line": "clear carry for the add"
  },
  "EF78": {
   "unit": "game/droneAiEE00",
   "line": "drone row"
  },
  "EF7B": {
   "unit": "game/droneAiEE00",
   "line": "the drone's new row"
  },
  "EF7E": {
   "unit": "game/droneAiEE00",
   "line": "Y = new row"
  },
  "EF7F": {
   "unit": "game/droneAiEE00",
   "line": "command $8D = drone move; the packet mergers execute it on the spot ($4F23) instead of queueing it"
  },
  "EF81": {
   "unit": "game/droneAiEE00",
   "line": "the packet's command byte"
  },
  "EF84": {
   "unit": "game/droneAiEE00",
   "line": "the drone's heading"
  },
  "EF87": {
   "unit": "game/droneAiEE00",
   "line": "clear carry for the add"
  },
  "EF88": {
   "unit": "game/droneAiEE00",
   "line": "+4 headings = 180 degrees"
  },
  "EF8A": {
   "unit": "game/droneAiEE00",
   "line": "wrap into 0-7: the player sees the battlefield mirrored, so a drone flying south on the AI's map is flying north on his, and cmdDroneMove picks the sprite shape straight from this byte"
  },
  "EF8C": {
   "unit": "game/droneAiEE00",
   "line": "argument 1 = the heading"
  },
  "EF8F": {
   "unit": "game/droneAiEE00",
   "line": "pre-mirror the new cell: X = 38-X, Y = 38-Y"
  },
  "EF92": {
   "unit": "game/droneAiEE00",
   "line": "argument 2 = column"
  },
  "EF95": {
   "unit": "game/droneAiEE00",
   "line": "argument 3 = row"
  },
  "EF98": {
   "unit": "game/droneAiEE00",
   "line": "command byte plus three arguments"
  },
  "EF9A": {
   "unit": "game/droneAiEE00",
   "line": "the exchange's fabricated 'received' packet is complete"
  },
  "EF9D": {
   "unit": "game/droneAiEE00",
   "line": "back to $E0A8"
  },
  "EF9E": {
   "unit": "game/droneAiEE00",
   "routine": [
    "droneDistanceToCell - measures the distance from the cell in zp_22/zp_24 to the drone.  It fills the drone's cell in as point B of cellDistance ($E82A) and tail-jumps into it, so the caller gets back the integer distance (the largest n with n*n <= dcol^2 + drow^2, saturating at 16) in A and X.  Nothing along the way touches Y, which is what lets pickDroneAimPointAndDecide keep the target unit index there across the call.",
    "In:  zp_22/zp_24 = the cell to measure from, drone cell $8974/$8975",
    "Out: A = X = distance 0-16, zp_27/zp_28 = the drone's cell, Y preserved",
    "Called from: $EE26 (range to the target unit) and $EEFC (range to the aim point) - the only two JSRs inside this page."
   ],
   "line": "drone column"
  },
  "EFA1": {
   "unit": "game/droneAiEE00",
   "line": "point B of cellDistance = the drone"
  },
  "EFA3": {
   "unit": "game/droneAiEE00",
   "line": "drone row"
  },
  "EFA6": {
   "unit": "game/droneAiEE00",
   "line": "point B row"
  },
  "EFA8": {
   "unit": "game/droneAiEE00",
   "line": "tail call: returns A = X = distance between (zp_22,zp_24) and the drone"
  },
  "EFAB": {
   "unit": "game/droneAiEE00",
   "block": [
    "staleCommTailBytes ($EFAB-$EFCF, 37 bytes) - dead in this image.",
    "Everything from here to $EFF4 is byte-for-byte identical in all four programs that are ever loaded at",
    "$EE00-$EFFF (the trainer's own tail from T35 s14-15, the modem driver from T34, the track-29 s0-3 $EC00",
    "sub-overlay and this drone AI), because the map-generator overlay addresses the strings below by",
    "absolute address and they therefore may not move.  These 37 bytes are simply what happened to precede",
    "them in the original source.",
    "In the trainer's own page the same bytes are the tail of pickDroneTargetAlongThreatAxis: they bump the",
    "ray radius and, on the way out, store the chosen unit in $8976 and scale the acceptance threshold $8979",
    "down to 75%.  In the modem driver they are the middle of the message 'INSERT DATA DISK AND PRESS SPACE.'",
    "starting at $EFAF.  The line comments below describe that trainer-page meaning; nothing here runs in this",
    "image.",
    "Latent bug: overlay A's promptInsertDiskAndWaitSpace ($81B6) unconditionally installs $EFAF as message",
    "slot $1E and copies four characters into $EFB6-$EFB9.  With this page (or the trainer's own, or the",
    "$EC00 sub-overlay) resident, $EFAF is the $C9 of 'cmp #$0F' - a complete one-character string 'I' - and",
    "the four-byte patch lands on top of code."
   ],
   "line": "trainer page: step the search ray outwards (zp_1F = radius)"
  },
  "EFAD": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: the new radius"
  },
  "EFAF": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: stop after radius 14.  Modem page: this $C9 opcode byte is the 'I' that starts 'INSERT DATA DISK...', and is a whole terminated string on its own here"
  },
  "EFB1": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: keep walking the ray.  Here the target, $EF9B, is the operand byte of the 'sta $E01E' at storeRxPacketLength - a branch into the middle of an instruction, which is the clearest proof that this block is not code in this image"
  },
  "EFB3": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: the best player unit scoreTargetBlock found, $FF when none"
  },
  "EFB5": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: nothing worth sending a drone at - leave $8976 alone"
  },
  "EFB7": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: that unit becomes the AI's next drone target"
  },
  "EFBA": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: the target-value threshold"
  },
  "EFBD": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: /2"
  },
  "EFBE": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: /4"
  },
  "EFBF": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: park threshold/4"
  },
  "EFC1": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: the threshold again"
  },
  "EFC4": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: prepare the subtract"
  },
  "EFC5": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: 3/4 of the threshold - the AI grows steadily less picky about drone targets"
  },
  "EFC7": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: never let it go negative"
  },
  "EFC9": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: store the reduced threshold"
  },
  "EFCC": {
   "unit": "game/droneAiEE00",
   "line": "trainer page: return to chooseAiDroneTarget"
  },
  "EFCD": {
   "unit": "game/droneAiEE00",
   "block": [
    "msgInsertDiskTail ($EFCD, 3 bytes) - 'CE' plus '.' with bit 7 set as the string terminator: the last",
    "three characters of the modem driver's message 'INSERT DATA DISK AND PRESS SPACE.'.  Meaningless in this",
    "image, but it has to stay put so that the strings below keep their addresses."
   ],
   "line": "end of '...PRESS SPACE.' in the modem-driver image of this page"
  },
  "EFD0": {
   "unit": "game/droneAiEE00",
   "block": [
    "Shared, position-fixed strings ($EFD0-$EFF4).  These belong to the map-generator/setup overlay",
    "(overlay A at $6F00-$87FF), not to the $E000 module at all: overlay A refers to them by hard-coded",
    "address, so every image of this page has to carry them at exactly these addresses.",
    "msgDataDiskName ($EFD0, 4 bytes, no terminator) - the literal 'DATA', which promptInsertDataDisk",
    "($81B2) copies into the INSERT-DISK message before printing it."
   ],
   "line": "disk name patched into the INSERT-DISK prompt"
  },
  "EFD4": {
   "unit": "game/droneAiEE00",
   "block": [
    "msgGameDiskName ($EFD4, 4 bytes, no terminator) - 'GAME', the other disk name, used by",
    "promptInsertGameDisk ($81A5)."
   ],
   "line": "the other disk name"
  },
  "EFD8": {
   "unit": "game/droneAiEE00",
   "block": [
    "msgCustomOff ($EFD8, 10 bytes) - 'CUSTOM OF' plus 'F' with bit 7 set (the string terminator).  Overlay A",
    "installs it as message slot $1E for the CUSTOM rules switch and patches the word at $EFDF between 'OFF'",
    "and 'ON '."
   ],
   "line": "message slot $1E for the CUSTOM switch; the ON/OFF word starts at $EFDF"
  },
  "EFE2": {
   "unit": "game/droneAiEE00",
   "block": [
    "msgDestroyOff ($EFE2, 11 bytes) - 'DESTROY OF' plus the terminating 'F'.  Message slot $1E for the",
    "DESTROY switch; the ON/OFF word overlay A patches sits at $EFEA."
   ],
   "line": "message slot $1E for the DESTROY switch; the ON/OFF word starts at $EFEA"
  },
  "EFED": {
   "unit": "game/droneAiEE00",
   "block": [
    "msgAccepted ($EFED, 8 bytes) - 'ACCEPTE' plus the terminating 'D'.  Overlay A prints it centred on row",
    "15 when the fire button confirms a value in the custom-game editor."
   ],
   "line": "printed centred on row 15 when a custom-game value is confirmed"
  },
  "EFF5": {
   "unit": "game/droneAiEE00",
   "block": [
    "trailingPadEFF5 ($EFF5-$EFFF, 11 bytes) - $C0,$DF,$00,$FF,$00,$FF,$00,$FF,$00,$FF,$FF.  Unreferenced by",
    "anything, and identical in every image of this page except the very last byte ($FF here and in the",
    "trainer's own tail, $00 in the modem driver).  Almost certainly padding left by the mastering system",
    "rather than data the game ever reads."
   ],
   "line": "unreferenced trailing pad to the end of the page"
  },
  "EFFD": {
   "unit": "game/droneAiEE00",
   "line": "last three pad bytes; the final $FF is $00 in the modem-driver image of this page"
  }
 }
}