{
 "chunk": "game_trainerAiE000_EC82",
 "unit": "game/trainerAiE000",
 "unitLabels": {
  "game/trainerAiE000": {
   "ECF3": "L_ECF3",
   "ED31": "L_ED31",
   "ED43": "L_ED43",
   "EEED": "L_EEED",
   "EEFD": "L_EEFD",
   "EF28": "L_EF28",
   "EC84": "copyThreatRingLoop",
   "EC9A": "blurSumStart",
   "ECA6": "addCounterClockwiseNeighbour",
   "ECAE": "stepToClockwiseNeighbour",
   "ECB5": "addClockwiseNeighbour",
   "ECBD": "finishBlurredValue",
   "ECDE": "addArcMiddleDirection",
   "ECE9": "addArcTrailingDirection",
   "ECF7": "rememberQuietestArc",
   "ECFD": "nextArcCandidate",
   "ED0A": "threatScanDone",
   "ED22": "skipAntiDroneShot",
   "ED25": "considerAntiDroneShot",
   "ED5A": "checkComcenEnergy",
   "ED61": "bumpReluctance",
   "ED63": "applyStunPenalty",
   "ED6B": "testCloseShot",
   "ED6F": "testReluctanceLimit",
   "ED86": "clampMissileSurplus",
   "ED8D": "fireAntiDroneMissile",
   "EDA6": "storeMissThreshold",
   "EDCA": "droneShotDownCleanly",
   "EDD2": "queueScatteredStrike",
   "EE07": "checkTargetOnRadar",
   "EE19": "forgetMissileTarget",
   "EE1E": "missileTurnTail",
   "EE2C": "missileTurnDone",
   "EE67": "returnSignedValue",
   "EEDF": "noDroneLaunch",
   "EF01": "chooseTargetByGroupPressure",
   "EF0E": "sumConvergingGroupsLoop",
   "EF20": "testGroupContactFlag",
   "EF25": "addGroupWeight",
   "EF2C": "nextGroupInSum",
   "EF35": "commitAllGroups",
   "EF38": "commitGroupLoop",
   "EF48": "stampOrderClockLoop",
   "EF53": "targetHumanComcen",
   "EF58": "droneTargetDone",
   "EF77": "storeScanReferencePoint",
   "EF9B": "scanRayLoop",
   "EFB3": "pickBestScoredBlock",
   "EFCC": "pickDroneTargetDone"
  }
 },
 "dataTypes": {
  "EFCD": [
   "text",
   3
  ],
  "EFD0": [
   "text",
   4
  ],
  "EFD4": [
   "text",
   4
  ],
  "EFD8": [
   "text",
   10
  ],
  "EFE2": [
   "text",
   11
  ],
  "EFED": [
   "text",
   8
  ],
  "EFF5": [
   "byte",
   8
  ],
  "EFFD": [
   "byte",
   3
  ]
 },
 "zpComments": {
  "23": "general scratch: first (clipped) column of the area scan square; distance/direction accumulator in the trainer's move search, and the trainer's reluctance-to-fire score in runAiMissileTurn ($ED46)",
  "29": "index of the last entry written to the area-scan result lists at $0400/$0432/$0464 ($FF = empty); the trainer's missile code parks the pending reload time here ($ED49)",
  "2B": "general scratch: lane or cluster-seed counter in the map generator, group destination row delta in the trainer, and the 'AI comcen is stunned' flag of runAiMissileTurn ($ED31, $FF = stunned)"
 },
 "notes": {
  "EC82": {
   "unit": "game/trainerAiE000",
   "routine": [
    "rankThreatDirections - Tail of assessThreatAroundAiComcen ($EBEF): the scan loop above has just turned every one of the human player's combat units 0-45 into a saturating 'pressure' weight in one of the eight compass buckets $8896-$889D. This half blurs that ring circularly, finds the centre of the quietest three-direction arc and returns the summary the planner stores in $8960-$8962.",
    "In:  $8896-$889D = raw per-direction pressure, zp_1F = total pressure, zp_20 = number of units that contributed, zp_21 = deepest weapon-range penetration, zp_57 = low byte of the lock-step game RNG state (tie break)",
    "Out: A = (zp_1F - zp_20) / 2 = overall threat level, X = zp_21, Y = zp_18 = the quietest compass direction 0-7; $8896-$889D holds the blurred ring and $0400-$0407 the raw copy; zp_18, zp_19, zp_1A and zp_23 clobbered",
    "Called from: branch at $EC7A only (the tail of $EBEF); when no unit threatened at all the path at $EC7C returns A=0, X=0, Y=8 instead - index 8 is the deliberate null entry (0,0) of dirToColDelta/dirToRowDelta"
   ],
   "block": [
    "Take an unsmoothed copy of the eight direction buckets into the $0400 scratch page, because the blur",
    "below overwrites $8896-$889D while it still needs each direction's original neighbours.",
    "",
    "A warning for readers of the rest of this chunk: $EC00-$EFFF is shared by four different overlay",
    "images (this trainer tail, the modem driver, the track 29 trainer setup and the track 29 drone AI),",
    "and XREF merges their cross references.  Many of the L_/D_/sub_ labels below are branch targets of",
    "one of the other builds that happen to land on an instruction boundary here; in this file they sit in",
    "the middle of a routine and nothing reaches them."
   ],
   "line": "start at direction 7 (north-west) and walk the ring backwards"
  },
  "EC84": {
   "unit": "game/trainerAiE000",
   "line": "raw pressure of direction Y from the 8-byte ring at $8896"
  },
  "EC87": {
   "unit": "game/trainerAiE000",
   "line": "keep the untouched copy in the $0400 scratch page (the same page finishRound uses for scan lists)"
  },
  "EC8A": {
   "unit": "game/trainerAiE000",
   "line": "next direction down"
  },
  "EC8B": {
   "unit": "game/trainerAiE000",
   "line": "until all 8 directions have been copied"
  },
  "EC8D": {
   "unit": "game/trainerAiE000",
   "line": "the blur pass also starts at direction 7"
  },
  "EC8F": {
   "unit": "game/trainerAiE000",
   "line": "zp_23 = the direction currently being smoothed"
  },
  "EC91": {
   "unit": "game/trainerAiE000",
   "routine": [
    "smoothThreatRing - Circular 1-2-1 blur of the eight-direction threat ring. For i = 7 down to 0 it computes (2*raw[i] + raw[i-1] + raw[i+1]) / 4, reading the untouched copy from $0400 and writing the result back over $8896+i. Indices wrap modulo 8, so north's neighbours are north-west and north-east.",
    "In:  $0400-$0407 = raw direction weights, zp_23 = direction index counter (7 on entry)",
    "Out: $8896-$889D = blurred weights; zp_19 = the sum's high bits; zp_23 ends at $FF; A/X/Y clobbered",
    "Called from: nothing - this is the loop head of rankThreatDirections, branched to from the DEC/BPL at $ECCA. (XREF shows a JSR from $7AE9, but that call belongs to the other overlay that shares these addresses.)"
   ],
   "line": "i = the direction being smoothed"
  },
  "EC93": {
   "unit": "game/trainerAiE000",
   "line": "X indexes the centre sample"
  },
  "EC94": {
   "unit": "game/trainerAiE000",
   "line": "Y will index the neighbour on the counter-clockwise side ..."
  },
  "EC95": {
   "unit": "game/trainerAiE000",
   "line": "... i - 1"
  },
  "EC96": {
   "unit": "game/trainerAiE000",
   "line": "no wrap needed while i-1 is still 0..7"
  },
  "EC98": {
   "unit": "game/trainerAiE000",
   "line": "i was 0, so its counter-clockwise neighbour is direction 7"
  },
  "EC9A": {
   "unit": "game/trainerAiE000",
   "line": "clear the two extra bits the weighted sum can produce"
  },
  "EC9C": {
   "unit": "game/trainerAiE000",
   "line": "zp_19 = bits 8-9 of the running sum"
  },
  "EC9E": {
   "unit": "game/trainerAiE000",
   "line": "raw pressure of the centre direction"
  },
  "ECA1": {
   "unit": "game/trainerAiE000",
   "line": "double it - the centre direction carries weight 2"
  },
  "ECA2": {
   "unit": "game/trainerAiE000",
   "line": "no carry out of the doubling"
  },
  "ECA4": {
   "unit": "game/trainerAiE000",
   "line": "carry the 9th bit into zp_19"
  },
  "ECA6": {
   "unit": "game/trainerAiE000",
   "line": "clear carry before adding a neighbour"
  },
  "ECA7": {
   "unit": "game/trainerAiE000",
   "line": "+ the counter-clockwise neighbour, weight 1"
  },
  "ECAA": {
   "unit": "game/trainerAiE000",
   "line": "no carry out of the add"
  },
  "ECAC": {
   "unit": "game/trainerAiE000",
   "line": "carry into the high bits again"
  },
  "ECAE": {
   "unit": "game/trainerAiE000",
   "line": "step X to the clockwise neighbour, i + 1"
  },
  "ECAF": {
   "unit": "game/trainerAiE000",
   "line": "past the last direction?"
  },
  "ECB1": {
   "unit": "game/trainerAiE000",
   "line": "no, i+1 is still inside the ring"
  },
  "ECB3": {
   "unit": "game/trainerAiE000",
   "line": "wrap i+1 round to direction 0"
  },
  "ECB5": {
   "unit": "game/trainerAiE000",
   "line": "clear carry before the last add"
  },
  "ECB6": {
   "unit": "game/trainerAiE000",
   "line": "+ the clockwise neighbour, weight 1"
  },
  "ECB9": {
   "unit": "game/trainerAiE000",
   "line": "no carry"
  },
  "ECBB": {
   "unit": "game/trainerAiE000",
   "line": "carry into the high bits"
  },
  "ECBD": {
   "unit": "game/trainerAiE000",
   "line": "divide the 10-bit sum by 4: high bits down ..."
  },
  "ECBF": {
   "unit": "game/trainerAiE000",
   "line": "... into A (first halving)"
  },
  "ECC0": {
   "unit": "game/trainerAiE000",
   "line": "and again ..."
  },
  "ECC2": {
   "unit": "game/trainerAiE000",
   "line": "... A = (2*raw[i] + raw[i-1] + raw[i+1]) / 4"
  },
  "ECC3": {
   "unit": "game/trainerAiE000",
   "line": "Y = i again"
  },
  "ECC5": {
   "unit": "game/trainerAiE000",
   "line": "write the blurred pressure back into the live ring at $8896+i"
  },
  "ECC8": {
   "unit": "game/trainerAiE000",
   "line": "next direction down"
  },
  "ECCA": {
   "unit": "game/trainerAiE000",
   "line": "repeat for directions 7..0"
  },
  "ECCC": {
   "unit": "game/trainerAiE000",
   "block": [
    "Now look for the calmest 135-degree arc: three adjacent directions whose blurred pressures add up to",
    "the smallest total.  The centre of that arc is the direction the comcen will run towards."
   ],
   "line": "$FF = no arc chosen yet, and as an arc sum 255 is worse than anything real"
  },
  "ECCE": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = best (quietest) centre direction so far"
  },
  "ECD0": {
   "unit": "game/trainerAiE000",
   "line": "zp_19 = the best arc sum so far"
  },
  "ECD2": {
   "unit": "game/trainerAiE000",
   "line": "test the eight arcs starting with the one led by direction 7"
  },
  "ECD4": {
   "unit": "game/trainerAiE000",
   "routine": [
    "findQuietestDirectionArc - For every leading direction y = 7..0 it adds the blurred pressures of y, y-1 and y-2 (wrapping round the ring) and keeps the smallest total, remembering the arc's middle direction y-1. The result is the centre of the calmest 135-degree wedge around the reference point. Exact ties are decided by bit 7 of the game RNG state, so the trainer does not always break them the same way.",
    "In:  $8896-$889D = blurred direction weights, zp_57 = game RNG state byte 0, zp_18/zp_19 preset to $FF",
    "Out: zp_18 = quietest centre direction 0-7, zp_19 = its arc sum; zp_1A = scratch for the candidate centre; A/X/Y clobbered",
    "Called from: nothing - loop head of rankThreatDirections, branched to from the DEY/BPL at $ECFE. (The XREF calls from $EC74/$EC7E belong to the modem-driver build that shares this address.)"
   ],
   "line": "A = the arc's leading direction"
  },
  "ECD5": {
   "unit": "game/trainerAiE000",
   "line": "X indexes the first of the three samples"
  },
  "ECD6": {
   "unit": "game/trainerAiE000",
   "line": "blurred pressure of direction y"
  },
  "ECD9": {
   "unit": "game/trainerAiE000",
   "line": "step to y - 1, the middle of the arc"
  },
  "ECDA": {
   "unit": "game/trainerAiE000",
   "line": "no wrap needed"
  },
  "ECDC": {
   "unit": "game/trainerAiE000",
   "line": "y was 0, so the middle direction is 7"
  },
  "ECDE": {
   "unit": "game/trainerAiE000",
   "line": "remember the middle direction - that is what the caller receives"
  },
  "ECE0": {
   "unit": "game/trainerAiE000",
   "line": "clear carry before the add"
  },
  "ECE1": {
   "unit": "game/trainerAiE000",
   "line": "+ pressure of the middle direction"
  },
  "ECE4": {
   "unit": "game/trainerAiE000",
   "line": "step to y - 2, the trailing direction"
  },
  "ECE5": {
   "unit": "game/trainerAiE000",
   "line": "no wrap needed"
  },
  "ECE7": {
   "unit": "game/trainerAiE000",
   "line": "wrap y-2 round to direction 7"
  },
  "ECE9": {
   "unit": "game/trainerAiE000",
   "line": "clear carry before the last add"
  },
  "ECEA": {
   "unit": "game/trainerAiE000",
   "line": "+ pressure of the trailing direction; A = the arc total (8-bit, and it can wrap if the whole ring is saturated at $FF)"
  },
  "ECED": {
   "unit": "game/trainerAiE000",
   "line": "compare with the quietest arc found so far"
  },
  "ECEF": {
   "unit": "game/trainerAiE000",
   "line": "strictly quieter - take this arc"
  },
  "ECF1": {
   "unit": "game/trainerAiE000",
   "line": "louder - keep the previous best"
  },
  "ECF3": {
   "unit": "game/trainerAiE000",
   "line": "a tie: toss a coin on bit 7 of the game RNG state. zp_57 is not stepped here, so every tie inside one call falls the same way"
  },
  "ECF5": {
   "unit": "game/trainerAiE000",
   "line": "coin says keep the arc that was found first"
  },
  "ECF7": {
   "unit": "game/trainerAiE000",
   "line": "remember the new best arc sum ..."
  },
  "ECF9": {
   "unit": "game/trainerAiE000",
   "line": "... and its middle direction ..."
  },
  "ECFB": {
   "unit": "game/trainerAiE000",
   "line": "... as the escape direction to return"
  },
  "ECFD": {
   "unit": "game/trainerAiE000",
   "line": "next leading direction"
  },
  "ECFE": {
   "unit": "game/trainerAiE000",
   "line": "eight arcs in all"
  },
  "ED00": {
   "unit": "game/trainerAiE000",
   "block": [
    "Return the summary: A = threat level, X = deepest weapon-range penetration, Y = escape direction.",
    "Subtracting the unit count removes the +1 base weight each threatening unit contributed, so what is",
    "left is the 'extra' pressure from being close or inside somebody's weapon range."
   ],
   "line": "total pressure accumulated over all threatening units"
  },
  "ED02": {
   "unit": "game/trainerAiE000",
   "line": "no borrow"
  },
  "ED03": {
   "unit": "game/trainerAiE000",
   "line": "minus one per threatening unit: strip the base weight each one added"
  },
  "ED05": {
   "unit": "game/trainerAiE000",
   "line": "halve it - A is the threat level the planner keeps in $8962"
  },
  "ED06": {
   "unit": "game/trainerAiE000",
   "line": "Y = the quietest direction; callers add 4 mod 8 to get the direction the enemy is in"
  },
  "ED08": {
   "unit": "game/trainerAiE000",
   "line": "X = the largest weapon-range surplus seen, 0 = nothing has this point in range ($8961)"
  },
  "ED0A": {
   "unit": "game/trainerAiE000",
   "line": "shared exit; the 'nothing threatens at all' path at $EC7F jumps here with A=0, X=0, Y=8"
  },
  "ED0B": {
   "unit": "game/trainerAiE000",
   "routine": [
    "runAiMissileTurn - The computer opponent's one 'weapon' decision per lock-step exchange. It first offers to launch the AI's own drone (launchAiDrone), then, if the human is steering a drone of his own and the AI still has missiles and is reloaded, it decides whether to shoot that drone down: the aim point is the drone's cell, a reluctance score is built from the range to the AI comcen, the comcen's stun state and energy and a missile-conservation rule, and the shot either kills the drone (command $92 plus the $90 burst) or lands scattered around it. Whatever happens it falls through into fireMissileAtSpottedUnit.",
    "In:  $8966 action budget for this exchange (0 = free), $920D drone state flags, $92B1 AI missiles left, $92AE human drones left, $8970 reload countdown, $92F6/$92F7 drone camera window origin, $F6A3/$F707 AI comcen cell, $F9C3 comcen stun, $FA27 comcen energy, $920B/$896F drone heading, game RNG",
    "Out: may put command $92 and/or $90 col,row into the fabricated 'received' packet ($E020, length $E01E); $8970 reload timer, $8966 spent, $8968 += 2; zp_22/zp_23/zp_24/zp_27/zp_28/zp_29/zp_2B/zp_18 clobbered",
    "Called from: injectAiCommandsIntoPacket ($EA80), which itself runs once per exchange from the $E000 jump-table entry with X=1"
   ],
   "block": [
    "The AI is allowed exactly one drone launch or missile shot per exchange: injectAiCommandsIntoPacket",
    "clears $8966 to 0 before calling, and each action decrements it to $FF."
   ],
   "line": "first chance of the exchange: send a drone up if one is due"
  },
  "ED0E": {
   "unit": "game/trainerAiE000",
   "line": "$8966 = this exchange's action budget"
  },
  "ED11": {
   "unit": "game/trainerAiE000",
   "line": "the drone launch already used it - go straight to the ground-target path"
  },
  "ED13": {
   "unit": "game/trainerAiE000",
   "line": "column of the AI's own comcen (unit 99)"
  },
  "ED16": {
   "unit": "game/trainerAiE000",
   "line": "zp_27/zp_28 = point B of every cellDistance below: the comcen is what the AI defends"
  },
  "ED18": {
   "unit": "game/trainerAiE000",
   "line": "row of the AI comcen"
  },
  "ED1B": {
   "unit": "game/trainerAiE000",
   "line": "zp_28 = reference row"
  },
  "ED1D": {
   "unit": "game/trainerAiE000",
   "line": "$920D bit 6 = the human is steering his own drone right now"
  },
  "ED20": {
   "unit": "game/trainerAiE000",
   "line": "he is - think about shooting it down"
  },
  "ED22": {
   "unit": "game/trainerAiE000",
   "line": "no drone to shoot at, so look for an ordinary target"
  },
  "ED25": {
   "unit": "game/trainerAiE000",
   "line": "$92B1 = missiles the computer opponent has left"
  },
  "ED28": {
   "unit": "game/trainerAiE000",
   "line": "out of missiles"
  },
  "ED2A": {
   "unit": "game/trainerAiE000",
   "line": "$8970 = reload countdown; $FF (negative) means ready to fire"
  },
  "ED2D": {
   "unit": "game/trainerAiE000",
   "line": "still reloading"
  },
  "ED2F": {
   "unit": "game/trainerAiE000",
   "block": [
    "Aim at the human's drone.  $92F6/$92F7 are the top-left corner of his 14x12 drone camera window and",
    "the drone always sits in the middle of it, hence the +6/+5."
   ],
   "line": "0 = 'the comcen is undamaged' until proved otherwise"
  },
  "ED31": {
   "unit": "game/trainerAiE000",
   "line": "zp_2B = comcen-damaged flag"
  },
  "ED33": {
   "unit": "game/trainerAiE000",
   "line": "left edge of the drone camera window"
  },
  "ED36": {
   "unit": "game/trainerAiE000",
   "line": "clear carry for the add"
  },
  "ED37": {
   "unit": "game/trainerAiE000",
   "line": "the drone sits 6 columns into the window"
  },
  "ED39": {
   "unit": "game/trainerAiE000",
   "line": "zp_22 = aim column (point A)"
  },
  "ED3B": {
   "unit": "game/trainerAiE000",
   "line": "top edge of the drone camera window"
  },
  "ED3E": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "ED3F": {
   "unit": "game/trainerAiE000",
   "line": "the drone sits 5 rows into the window"
  },
  "ED41": {
   "unit": "game/trainerAiE000",
   "line": "zp_24 = aim row"
  },
  "ED43": {
   "unit": "game/trainerAiE000",
   "line": "A = range in cells (0-16) from the AI comcen to the drone"
  },
  "ED46": {
   "unit": "game/trainerAiE000",
   "line": "zp_23 = reluctance to fire, starting at that range"
  },
  "ED48": {
   "unit": "game/trainerAiE000",
   "line": "half the range ..."
  },
  "ED49": {
   "unit": "game/trainerAiE000",
   "line": "... is what the reload timer will be set to if the shot goes off"
  },
  "ED4B": {
   "unit": "game/trainerAiE000",
   "line": "stun byte of the AI comcen (unit 99)"
  },
  "ED4E": {
   "unit": "game/trainerAiE000",
   "line": "bits 0-3 = the stun counter"
  },
  "ED50": {
   "unit": "game/trainerAiE000",
   "line": "not stunned - check the energy instead"
  },
  "ED52": {
   "unit": "game/trainerAiE000",
   "line": "zp_2B = $FF: remember that the comcen is stunned - it costs another point of reluctance at $ED69"
  },
  "ED54": {
   "unit": "game/trainerAiE000",
   "line": "stun counter 14 or 15 = the comcen is effectively out of action"
  },
  "ED56": {
   "unit": "game/trainerAiE000",
   "line": "too badly stunned to shoot at a drone"
  },
  "ED58": {
   "unit": "game/trainerAiE000",
   "line": "always taken (the CMP above set carry only for >= $0E): skip the energy test"
  },
  "ED5A": {
   "unit": "game/trainerAiE000",
   "line": "energy byte of the AI comcen"
  },
  "ED5D": {
   "unit": "game/trainerAiE000",
   "line": "low two bits of the 0-50 energy value"
  },
  "ED5F": {
   "unit": "game/trainerAiE000",
   "line": "not a multiple of 4 - no extra hesitation"
  },
  "ED61": {
   "unit": "game/trainerAiE000",
   "line": "energy divisible by 4: +1 reluctance, a cheap energy-driven hesitation"
  },
  "ED63": {
   "unit": "game/trainerAiE000",
   "line": "A = the reluctance BEFORE the stun bump below"
  },
  "ED65": {
   "unit": "game/trainerAiE000",
   "line": "was the comcen stunned? (zp_2B = $FF if so)"
  },
  "ED67": {
   "unit": "game/trainerAiE000",
   "line": "no"
  },
  "ED69": {
   "unit": "game/trainerAiE000",
   "line": "stunned: +1 reluctance. A keeps the old value, so the test at $ED6B still uses the unbumped score"
  },
  "ED6B": {
   "unit": "game/trainerAiE000",
   "line": "reluctance under 3 - a close shot from a healthy comcen: fire without the conservation test"
  },
  "ED6D": {
   "unit": "game/trainerAiE000",
   "line": "take the shot"
  },
  "ED6F": {
   "unit": "game/trainerAiE000",
   "line": "reload the (possibly bumped) reluctance"
  },
  "ED71": {
   "unit": "game/trainerAiE000",
   "line": "5 or more = far away and/or a hurt comcen ..."
  },
  "ED73": {
   "unit": "game/trainerAiE000",
   "line": "... not worth a missile"
  },
  "ED75": {
   "unit": "game/trainerAiE000",
   "line": "$92AE = drones the human still has in stock"
  },
  "ED78": {
   "unit": "game/trainerAiE000",
   "line": "x2 ..."
  },
  "ED79": {
   "unit": "game/trainerAiE000",
   "line": "... x4: four missiles are reserved for every future drone"
  },
  "ED7A": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = the reserve"
  },
  "ED7C": {
   "unit": "game/trainerAiE000",
   "line": "missiles the AI owns"
  },
  "ED7F": {
   "unit": "game/trainerAiE000",
   "line": "no borrow wanted"
  },
  "ED80": {
   "unit": "game/trainerAiE000",
   "line": "missiles above the reserve"
  },
  "ED82": {
   "unit": "game/trainerAiE000",
   "line": "positive surplus"
  },
  "ED84": {
   "unit": "game/trainerAiE000",
   "line": "below the reserve - treat the surplus as 0"
  },
  "ED86": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "ED87": {
   "unit": "game/trainerAiE000",
   "line": "surplus + 1"
  },
  "ED89": {
   "unit": "game/trainerAiE000",
   "line": "compare with the reluctance"
  },
  "ED8B": {
   "unit": "game/trainerAiE000",
   "line": "too few spare missiles for a shot this reluctant - save it for later"
  },
  "ED8D": {
   "unit": "game/trainerAiE000",
   "block": [
    "The shot goes off.  Whether it actually hits is decided by one random byte against a threshold that",
    "grows with the reluctance and jumps by $40 when the drone has changed heading since the last shot,",
    "so a jinking drone is much harder to hit."
   ],
   "line": "half the range, computed at $ED48"
  },
  "ED8F": {
   "unit": "game/trainerAiE000",
   "line": "start the reload countdown"
  },
  "ED92": {
   "unit": "game/trainerAiE000",
   "line": "reluctance + 1 so that even a perfect shot can miss"
  },
  "ED94": {
   "unit": "game/trainerAiE000",
   "line": "A = reluctance + 1"
  },
  "ED96": {
   "unit": "game/trainerAiE000",
   "line": "x2 ..."
  },
  "ED97": {
   "unit": "game/trainerAiE000",
   "line": "x4 ..."
  },
  "ED98": {
   "unit": "game/trainerAiE000",
   "line": "x8 ..."
  },
  "ED99": {
   "unit": "game/trainerAiE000",
   "line": "x16 ..."
  },
  "ED9A": {
   "unit": "game/trainerAiE000",
   "line": "x32: the miss threshold is (reluctance+1) * 32"
  },
  "ED9B": {
   "unit": "game/trainerAiE000",
   "line": "$920B = the heading of the drone right now"
  },
  "ED9E": {
   "unit": "game/trainerAiE000",
   "line": "$896F = the heading it had at the last shot"
  },
  "EDA1": {
   "unit": "game/trainerAiE000",
   "line": "unchanged - the drone is flying straight"
  },
  "EDA3": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "EDA4": {
   "unit": "game/trainerAiE000",
   "line": "it jinked: +$40 makes a hit far less likely"
  },
  "EDA6": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = miss threshold"
  },
  "EDA8": {
   "unit": "game/trainerAiE000",
   "line": "step the lock-step game RNG (both machines would get the same byte)"
  },
  "EDAB": {
   "unit": "game/trainerAiE000",
   "line": "random byte against the threshold"
  },
  "EDAD": {
   "unit": "game/trainerAiE000",
   "line": "random >= threshold: a clean hit on the drone"
  },
  "EDAF": {
   "unit": "game/trainerAiE000",
   "line": "a miss: scatter radius = reluctance / 2 ..."
  },
  "EDB1": {
   "unit": "game/trainerAiE000",
   "line": "... + 1, so it is at least one cell off"
  },
  "EDB3": {
   "unit": "game/trainerAiE000",
   "line": "A = scatter radius"
  },
  "EDB5": {
   "unit": "game/trainerAiE000",
   "line": "randomly plus or minus"
  },
  "EDB8": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "EDB9": {
   "unit": "game/trainerAiE000",
   "line": "shift the aim column"
  },
  "EDBB": {
   "unit": "game/trainerAiE000",
   "line": "zp_22 = the missed column"
  },
  "EDBD": {
   "unit": "game/trainerAiE000",
   "line": "A = scatter radius again"
  },
  "EDBF": {
   "unit": "game/trainerAiE000",
   "line": "randomly plus or minus"
  },
  "EDC2": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "EDC3": {
   "unit": "game/trainerAiE000",
   "line": "shift the aim row"
  },
  "EDC5": {
   "unit": "game/trainerAiE000",
   "line": "zp_24 = the missed row"
  },
  "EDC7": {
   "unit": "game/trainerAiE000",
   "line": "queue the strike at the scattered cell"
  },
  "EDCA": {
   "unit": "game/trainerAiE000",
   "line": "command $92 = DRONE SHOT DOWN (handler cmdDroneShotDown $538B, clears $920D bits 6-7)"
  },
  "EDCC": {
   "unit": "game/trainerAiE000",
   "line": "it is the first byte of the packet - $E01E is still 0 whenever this path is reached"
  },
  "EDCF": {
   "unit": "game/trainerAiE000",
   "line": "packet is now 1 byte long"
  },
  "EDD2": {
   "unit": "game/trainerAiE000",
   "line": "append the $90 strike so the human also sees the missile burst on the drone's cell"
  },
  "EDD5": {
   "unit": "game/trainerAiE000",
   "routine": [
    "fireMissileAtSpottedUnit - Second half of the missile turn, reached whenever no anti-drone shot was taken. It fires a comcen missile at the unit stored in $8972 (the last human unit seen shooting at the AI comcen) or, when there is none or it is dead, at the human comcen itself while that comcen is visible. The target must sit on the AI comcen's 21x17 radar grid; the reload timer is then set to half the range and the $90 strike is queued. The tail remembers the human drone's heading for the jink test and ticks the reload timer down.",
    "In:  $920D bit 6, $92AE human drones left, $92B1 AI missiles, $8970 reload countdown, $8966 action budget, $8972 current missile target, $F76C unit type/dead bits, $F9F5 human comcen energy/visibility, $920B drone heading, zp_27/zp_28 = AI comcen cell",
    "Out: $8972 ($FF when the target was dropped), $8970, $896F, and via queueMissileStrikeCommand the bytes $90 col,row in the fabricated packet",
    "Called from: JMP at $ED22 and BCC at $ED8B in runAiMissileTurn, plus the fall-through from $EDD2"
   ],
   "block": [
    "Missile conservation: while the human still owns drones the AI keeps two missiles in hand for each of",
    "them, so it will not empty the rack on ground targets and be defenceless against the next drone."
   ],
   "line": "$920D bit 6 = the human is flying a drone"
  },
  "EDD8": {
   "unit": "game/trainerAiE000",
   "line": "he is - the anti-drone code above owns this exchange, so no ground shot"
  },
  "EDDA": {
   "unit": "game/trainerAiE000",
   "line": "drones the human still has"
  },
  "EDDD": {
   "unit": "game/trainerAiE000",
   "line": "two missiles reserved per drone"
  },
  "EDDE": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = the reserve"
  },
  "EDE0": {
   "unit": "game/trainerAiE000",
   "line": "missiles the AI owns"
  },
  "EDE3": {
   "unit": "game/trainerAiE000",
   "line": "none at all"
  },
  "EDE5": {
   "unit": "game/trainerAiE000",
   "line": "no borrow wanted"
  },
  "EDE6": {
   "unit": "game/trainerAiE000",
   "line": "missiles minus the reserve"
  },
  "EDE8": {
   "unit": "game/trainerAiE000",
   "line": "fewer than the reserve - hold fire"
  },
  "EDEA": {
   "unit": "game/trainerAiE000",
   "line": "exactly the reserve - hold fire as well"
  },
  "EDEC": {
   "unit": "game/trainerAiE000",
   "line": "reload countdown"
  },
  "EDEF": {
   "unit": "game/trainerAiE000",
   "line": "still reloading (>= 0): skip to the tail, which ticks it down"
  },
  "EDF1": {
   "unit": "game/trainerAiE000",
   "line": "this exchange's action budget"
  },
  "EDF4": {
   "unit": "game/trainerAiE000",
   "line": "already spent on the drone launch - drop the target and leave"
  },
  "EDF6": {
   "unit": "game/trainerAiE000",
   "line": "$8972 = human unit last seen shooting at the AI comcen, $FF = none known"
  },
  "EDF9": {
   "unit": "game/trainerAiE000",
   "line": "none - try the human comcen instead"
  },
  "EDFB": {
   "unit": "game/trainerAiE000",
   "line": "type byte of that unit"
  },
  "EDFE": {
   "unit": "game/trainerAiE000",
   "line": "still alive (bit 7 clear) - shoot at it"
  },
  "EE00": {
   "unit": "game/trainerAiE000",
   "routine": [
    "targetEnemyComcenWithMissile - Fall-through tail of fireMissileAtSpottedUnit: when the AI has no remembered attacker, or that attacker is dead, it shoots at the human's COMCEN (unit $31 = 49) instead, but only while that comcen is currently visible.",
    "In:  $F9F5 = energy/visibility byte of unit 49 (bit 7 = visible right now)",
    "Out: X = $31 when the comcen may be shot at, otherwise branches to $EE19 which clears the target",
    "Called from: BMI at $EDF9 and fall-through from $EDFE. XREF lists 'call:E0A5', but that JSR only ever runs while the track 29 drone-flight overlay occupies $EE00-$EFFF, so it never enters this code"
   ],
   "line": "energy/visibility byte of the human comcen"
  },
  "EE03": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 clear = not visible; nothing to shoot at"
  },
  "EE05": {
   "unit": "game/trainerAiE000",
   "line": "$31 = 49 = the human player's COMCEN"
  },
  "EE07": {
   "unit": "game/trainerAiE000",
   "line": "put the target cell in zp_22/zp_24 and test it against the AI comcen's radar grid"
  },
  "EE0A": {
   "unit": "game/trainerAiE000",
   "line": "outside the 21x17 grid - the AI cannot see it, so no shot"
  },
  "EE0C": {
   "unit": "game/trainerAiE000",
   "line": "range from the AI comcen (zp_27/zp_28) to the target"
  },
  "EE0F": {
   "unit": "game/trainerAiE000",
   "line": "half of it ..."
  },
  "EE10": {
   "unit": "game/trainerAiE000",
   "line": "... becomes the reload countdown: long shots take longer to repeat"
  },
  "EE13": {
   "unit": "game/trainerAiE000",
   "line": "append the $90 col,row strike to the fabricated packet"
  },
  "EE16": {
   "unit": "game/trainerAiE000",
   "line": "keep $8972 so the same unit stays the target next exchange"
  },
  "EE19": {
   "unit": "game/trainerAiE000",
   "line": "$FF = no target"
  },
  "EE1B": {
   "unit": "game/trainerAiE000",
   "line": "forget the remembered attacker"
  },
  "EE1E": {
   "unit": "game/trainerAiE000",
   "block": [
    "Shared tail: remember the human drone's heading so the next anti-drone shot can tell whether it",
    "jinked, and tick the reload countdown down towards $FF = ready."
   ],
   "line": "current heading of the human's drone"
  },
  "EE21": {
   "unit": "game/trainerAiE000",
   "line": "$896F is compared against it at $ED9E next time"
  },
  "EE24": {
   "unit": "game/trainerAiE000",
   "line": "reload countdown"
  },
  "EE27": {
   "unit": "game/trainerAiE000",
   "line": "already at $FF: the launcher is ready, leave it alone"
  },
  "EE29": {
   "unit": "game/trainerAiE000",
   "line": "one exchange closer to being reloaded"
  },
  "EE2C": {
   "unit": "game/trainerAiE000",
   "line": "back to injectAiCommandsIntoPacket"
  },
  "EE2D": {
   "unit": "game/trainerAiE000",
   "routine": [
    "queueMissileStrikeCommand - Appends the 3-byte command $90 col,row (MISSILE STRIKE, handler cmdMissileStrike $53C0: 14 points of damage, 7 to a comcen) to the packet the trainer pretends to have received from the opponent. Both coordinates are pre-mirrored with mirrorMapCoordinate (39-v) because the command dispatcher un-mirrors every parameter that arrives from a remote player.",
    "In:  zp_22 = strike column, zp_24 = strike row, $E01E = current packet length",
    "Out: three bytes appended at $E020+, $E01E advanced by 3, $8966 spent (decremented to $FF), $8968 charged two of the round's command slots; X clobbered",
    "Called from: $EDD2 (anti-drone shot) and $EE13 (shot at a spotted unit)"
   ],
   "line": "X = current length of the fabricated incoming packet"
  },
  "EE30": {
   "unit": "game/trainerAiE000",
   "line": "$90 = MISSILE STRIKE"
  },
  "EE32": {
   "unit": "game/trainerAiE000",
   "line": "command byte into the received-packet buffer"
  },
  "EE35": {
   "unit": "game/trainerAiE000",
   "line": "next byte"
  },
  "EE36": {
   "unit": "game/trainerAiE000",
   "line": "strike column"
  },
  "EE38": {
   "unit": "game/trainerAiE000",
   "line": "$3FA3: 39 - column, undoing the mirror the engine will apply to a remote command"
  },
  "EE3B": {
   "unit": "game/trainerAiE000",
   "line": "first parameter"
  },
  "EE3E": {
   "unit": "game/trainerAiE000",
   "line": "next byte"
  },
  "EE3F": {
   "unit": "game/trainerAiE000",
   "line": "strike row"
  },
  "EE41": {
   "unit": "game/trainerAiE000",
   "line": "mirror it as well"
  },
  "EE44": {
   "unit": "game/trainerAiE000",
   "line": "second parameter"
  },
  "EE47": {
   "unit": "game/trainerAiE000",
   "line": "past the last byte"
  },
  "EE48": {
   "unit": "game/trainerAiE000",
   "line": "packet is three bytes longer"
  },
  "EE4B": {
   "unit": "game/trainerAiE000",
   "line": "this exchange's single action is now spent"
  },
  "EE4E": {
   "unit": "game/trainerAiE000",
   "line": "charge the first of the two command slots a missile costs"
  },
  "EE51": {
   "unit": "game/trainerAiE000",
   "line": "and the second: a missile counts double against the four commands a round allows"
  },
  "EE54": {
   "unit": "game/trainerAiE000",
   "line": "done"
  },
  "EE55": {
   "unit": "game/trainerAiE000",
   "routine": [
    "randomlySignValue - Returns A or -A with equal probability, using bit 0 of the lock-step game RNG. Used twice by runAiMissileTurn to scatter the column and the row of a missile that is meant to miss the human's drone.",
    "In:  A = magnitude",
    "Out: A = zp_18 = +A or -A (two's complement); the game RNG has been stepped",
    "Called from: $EDB5 (column offset) and $EDBF (row offset)"
   ],
   "line": "keep the magnitude"
  },
  "EE57": {
   "unit": "game/trainerAiE000",
   "line": "one random byte from the lock-step RNG"
  },
  "EE5A": {
   "unit": "game/trainerAiE000",
   "line": "just its lowest bit"
  },
  "EE5C": {
   "unit": "game/trainerAiE000",
   "line": "0 - leave the magnitude positive"
  },
  "EE5E": {
   "unit": "game/trainerAiE000",
   "line": "1 - negate it: two's complement ..."
  },
  "EE60": {
   "unit": "game/trainerAiE000",
   "line": "... invert ..."
  },
  "EE62": {
   "unit": "game/trainerAiE000",
   "line": "clear carry for the +1"
  },
  "EE63": {
   "unit": "game/trainerAiE000",
   "line": "... and add one"
  },
  "EE65": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = -magnitude"
  },
  "EE67": {
   "unit": "game/trainerAiE000",
   "line": "return the signed value"
  },
  "EE69": {
   "unit": "game/trainerAiE000",
   "line": "back to the missile scatter"
  },
  "EE6A": {
   "unit": "game/trainerAiE000",
   "routine": [
    "isUnitOnAiRadar - Loads unit X's cell into zp_22/zp_24 and asks whether it lies inside the 21x17 radar grid centred on the computer opponent's own COMCEN. Because mapCellToRadarGrid centres the grid on comcenUnit ($9236), this routine swaps that variable to $63 (unit 99, side 1's comcen) for the test and puts it back to $31 (unit 49) afterwards - the unconditional $31 is proof that this module always assumes the human is side 0.",
    "In:  X = unit index, $F640/$F6A4 = unit column/row arrays",
    "Out: zp_22 = column, zp_24 = row of that unit (point A for the next cellDistance), C = 0 when the cell is on the AI's radar grid and C = 1 when it is off it; $9236 left at $31; X and Y clobbered",
    "Called from: $EE07 (missile targeting) and $EEFA (drone targeting in THE BOMB). Note it calls into the $6F00 comcen-screens overlay, so overlay B must be the resident one"
   ],
   "line": "map column of unit X"
  },
  "EE6D": {
   "unit": "game/trainerAiE000",
   "line": "zp_22 = point A column, also the strike coordinate if a missile follows"
  },
  "EE6F": {
   "unit": "game/trainerAiE000",
   "line": "map row of unit X"
  },
  "EE72": {
   "unit": "game/trainerAiE000",
   "line": "zp_24 = point A row"
  },
  "EE74": {
   "unit": "game/trainerAiE000",
   "line": "Y = row, as mapCellToRadarGrid wants it"
  },
  "EE75": {
   "unit": "game/trainerAiE000",
   "line": "X = column"
  },
  "EE77": {
   "unit": "game/trainerAiE000",
   "line": "$63 = 99 = side 1's COMCEN"
  },
  "EE79": {
   "unit": "game/trainerAiE000",
   "line": "pretend the local comcen is the AI's, so the radar grid is centred on it"
  },
  "EE7C": {
   "unit": "game/trainerAiE000",
   "line": "$7A13 in the comcen-screens overlay: C=0 when the cell is inside the 21 x 17 grid"
  },
  "EE7F": {
   "unit": "game/trainerAiE000",
   "line": "$31 = 49 = the human player's COMCEN"
  },
  "EE81": {
   "unit": "game/trainerAiE000",
   "line": "restore comcenUnit - the rest of the game expects the local player's comcen there"
  },
  "EE84": {
   "unit": "game/trainerAiE000",
   "line": "carry from the grid test survives both stores"
  },
  "EE85": {
   "unit": "game/trainerAiE000",
   "routine": [
    "launchAiDrone - Sends the computer opponent's drone up when one is due. It writes the launch straight into the packet the game is about to 'receive': $97 DRONE LAUNCH followed by a $8D DRONE MOVE that places the drone on the AI comcen and points it at the chosen target unit. The AI keeps its own copy of the drone position in $8974/$8975 and a 100-step fuel counter in $8973; $8977 = 3 holds the next launch off for three exchanges.",
    "In:  $8977 launch delay (must be 0), $8976 target unit (>= 0), $920D bit 7 (no drone of the human's in the air), $F6A3/$F707 AI comcen cell, $F640/$F6A4 target cell",
    "Out: $E020 = $97, $E021-$E024 = $8D heading,col,row, $E01E = 5; $8974/$8975 launch cell, $8973 = $64 fuel, $8977 = 3, $896F heading, $8966 spent; zp_92-zp_95 line-stepper state clobbered",
    "Called from: $ED0B, the first thing runAiMissileTurn does each exchange"
   ],
   "block": [
    "Three gates before a launch: the delay must have run out, a target must have been picked by",
    "chooseAiDroneTarget, and no drone may be in the air already - the engine tracks only one at a time."
   ],
   "line": "$8977 = exchanges to wait before the next launch ($FF also means the drone-flight overlay is resident)"
  },
  "EE88": {
   "unit": "game/trainerAiE000",
   "line": "not due yet"
  },
  "EE8A": {
   "unit": "game/trainerAiE000",
   "line": "$8976 = the unit the drone should be sent at"
  },
  "EE8D": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 set = no target chosen"
  },
  "EE8F": {
   "unit": "game/trainerAiE000",
   "line": "$920D drone state flags"
  },
  "EE92": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 = a drone flight is already in progress - wait for it to end"
  },
  "EE94": {
   "unit": "game/trainerAiE000",
   "line": "spend this exchange's single action"
  },
  "EE97": {
   "unit": "game/trainerAiE000",
   "line": "$97 = DRONE LAUNCH (handler cmdDroneLaunch $53A3: 'DRONE ALERT!' and one drone off the opponent's stock)"
  },
  "EE99": {
   "unit": "game/trainerAiE000",
   "line": "first byte of the fabricated packet"
  },
  "EE9C": {
   "unit": "game/trainerAiE000",
   "line": "$8D = DRONE MOVE, 4 bytes: heading, column, row"
  },
  "EE9E": {
   "unit": "game/trainerAiE000",
   "line": "second byte"
  },
  "EEA1": {
   "unit": "game/trainerAiE000",
   "line": "the drone starts over the AI's own comcen"
  },
  "EEA4": {
   "unit": "game/trainerAiE000",
   "line": "$8974 = the AI's private copy of the drone column"
  },
  "EEA7": {
   "unit": "game/trainerAiE000",
   "line": "zp_93 = line-stepper start column"
  },
  "EEA9": {
   "unit": "game/trainerAiE000",
   "line": "comcen row"
  },
  "EEAC": {
   "unit": "game/trainerAiE000",
   "line": "$8975 = drone row"
  },
  "EEAF": {
   "unit": "game/trainerAiE000",
   "line": "zp_92 = line-stepper start row"
  },
  "EEB1": {
   "unit": "game/trainerAiE000",
   "line": "add one to each coordinate and mirror: exactly undoes the mirror-then-decrement the engine applies to a remote $8D (the drone is a 2x2 object)"
  },
  "EEB4": {
   "unit": "game/trainerAiE000",
   "line": "column parameter of the $8D"
  },
  "EEB7": {
   "unit": "game/trainerAiE000",
   "line": "row parameter"
  },
  "EEBA": {
   "unit": "game/trainerAiE000",
   "line": "the chosen target unit"
  },
  "EEBD": {
   "unit": "game/trainerAiE000",
   "line": "its column ..."
  },
  "EEC0": {
   "unit": "game/trainerAiE000",
   "line": "... becomes the line-stepper target column"
  },
  "EEC2": {
   "unit": "game/trainerAiE000",
   "line": "its row ..."
  },
  "EEC5": {
   "unit": "game/trainerAiE000",
   "line": "... the target row"
  },
  "EEC7": {
   "unit": "game/trainerAiE000",
   "line": "one Bresenham step towards the target: Y = compass direction 0-7, A = that direction's row delta"
  },
  "EECA": {
   "unit": "game/trainerAiE000",
   "line": "remember the heading for the AI's own bookkeeping"
  },
  "EECD": {
   "unit": "game/trainerAiE000",
   "line": "heading parameter of the $8D - but this is A, the row delta (-1/0/+1), not the direction index in Y. cmdDroneMove draws sprite shape (heading+4) AND 7 = 3, 4 or 5, so the AI's drone is always drawn flying south towards the player. Probably a slip for TYA"
  },
  "EED0": {
   "unit": "game/trainerAiE000",
   "line": "5 bytes: one $97 plus four for the $8D"
  },
  "EED2": {
   "unit": "game/trainerAiE000",
   "line": "hand the packet to the engine as if the opponent had sent it"
  },
  "EED5": {
   "unit": "game/trainerAiE000",
   "line": "$64 = 100 steps of fuel, one per exchange, matching the human's drone tank"
  },
  "EED7": {
   "unit": "game/trainerAiE000",
   "line": "$8973 = the AI's own fuel counter"
  },
  "EEDA": {
   "unit": "game/trainerAiE000",
   "line": "wait three exchanges before considering another launch"
  },
  "EEDC": {
   "unit": "game/trainerAiE000",
   "line": "$8977 also stays non-negative, marking the native $EE00 tail as the resident one"
  },
  "EEDF": {
   "unit": "game/trainerAiE000",
   "line": "nothing to launch"
  },
  "EEE0": {
   "unit": "game/trainerAiE000",
   "routine": [
    "chooseAiDroneTarget - Picks what the computer opponent should send its next drone at and leaves the unit index in $8976. It bails out when the AI has no drones left or a target is still pending. In game type 2 (THE BOMB) the only worthwhile target is the human COMCEN, as soon as it is visible or shows on the AI's radar. Otherwise, once the comcen is visible, it adds up the strength of every group that has reached it (double for a group with a clear run at it) and, at a total of 8, commits to an all-out attack: every group is flagged committed, all 46 combat units are made due for new orders, the drone is held back 21 exchanges and then aimed at the comcen. If the comcen is not visible it falls into pickDroneTargetAlongThreatAxis.",
    "In:  $92AF drones the AI has left, $8976, $0BA3 bits 0-2 = game type, $F9F5 human comcen visibility, $8902 group strengths, $893E group flags, $8953 highest group id, $8954 group count, $91CB game clock",
    "Out: $8976 = target unit ($31 = the human comcen), $893E bit 6 set for every group, $8800-$882D stamped with the clock, $8977 = $15; zp_18, zp_19 clobbered",
    "Called from: planTrainerRound ($E1FF), once per round and only while the AI is in attack mode ($8963 = 0)"
   ],
   "line": "$92AF = drones the computer opponent still owns"
  },
  "EEE3": {
   "unit": "game/trainerAiE000",
   "line": "none left - it will never launch again"
  },
  "EEE5": {
   "unit": "game/trainerAiE000",
   "line": "$8976 = pending drone target"
  },
  "EEE8": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 clear = a target is already chosen, so leave it alone"
  },
  "EEEA": {
   "unit": "game/trainerAiE000",
   "line": "game options byte"
  },
  "EEED": {
   "unit": "game/trainerAiE000",
   "line": "bits 0-2 = the game type"
  },
  "EEEF": {
   "unit": "game/trainerAiE000",
   "line": "type 2 = THE BOMB, where only the comcen counts"
  },
  "EEF1": {
   "unit": "game/trainerAiE000",
   "line": "any other game type"
  },
  "EEF3": {
   "unit": "game/trainerAiE000",
   "line": "energy/visibility of the human comcen"
  },
  "EEF6": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 = visible right now: send the drone at it"
  },
  "EEF8": {
   "unit": "game/trainerAiE000",
   "line": "$31 = 49 = the human comcen"
  },
  "EEFA": {
   "unit": "game/trainerAiE000",
   "line": "is it at least on the AI comcen's radar grid?"
  },
  "EEFD": {
   "unit": "game/trainerAiE000",
   "line": "not on the radar either - no drone target this round"
  },
  "EEFF": {
   "unit": "game/trainerAiE000",
   "line": "on the radar - target it"
  },
  "EF01": {
   "unit": "game/trainerAiE000",
   "block": [
    "Every other game type: measure how much of the AI's army has actually converged on the human comcen.",
    "$893E bit 5 = this group has a clear line to the comcen (set at $E3C3), bit 6 = one of its members has",
    "come within 8 cells of it (set at $EB1F)."
   ],
   "line": "comcen visibility again"
  },
  "EF04": {
   "unit": "game/trainerAiE000",
   "line": "not visible - go and look for a worthwhile cluster of units instead"
  },
  "EF06": {
   "unit": "game/trainerAiE000",
   "line": "$8954 = number of AI groups ..."
  },
  "EF09": {
   "unit": "game/trainerAiE000",
   "line": "... so the last group index is one less"
  },
  "EF0A": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = 0"
  },
  "EF0C": {
   "unit": "game/trainerAiE000",
   "line": "running total of converging strength"
  },
  "EF0E": {
   "unit": "game/trainerAiE000",
   "line": "$8902 = number of units in group Y"
  },
  "EF11": {
   "unit": "game/trainerAiE000",
   "line": "zp_19 = this group's weight"
  },
  "EF13": {
   "unit": "game/trainerAiE000",
   "line": "group flags"
  },
  "EF16": {
   "unit": "game/trainerAiE000",
   "line": "test them ..."
  },
  "EF17": {
   "unit": "game/trainerAiE000",
   "line": "... bit 5 = the group has a clear run at the human comcen"
  },
  "EF19": {
   "unit": "game/trainerAiE000",
   "line": "no clear run - try the contact bit"
  },
  "EF1B": {
   "unit": "game/trainerAiE000",
   "line": "a clear run counts double"
  },
  "EF1D": {
   "unit": "game/trainerAiE000",
   "line": "add it in"
  },
  "EF20": {
   "unit": "game/trainerAiE000",
   "line": "flags again"
  },
  "EF21": {
   "unit": "game/trainerAiE000",
   "line": "bit 6 = a member of this group has reached the comcen"
  },
  "EF23": {
   "unit": "game/trainerAiE000",
   "line": "neither flag - this group does not count"
  },
  "EF25": {
   "unit": "game/trainerAiE000",
   "line": "the group's weight"
  },
  "EF27": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "EF28": {
   "unit": "game/trainerAiE000",
   "line": "add to the running total"
  },
  "EF2A": {
   "unit": "game/trainerAiE000",
   "line": "keep the total"
  },
  "EF2C": {
   "unit": "game/trainerAiE000",
   "line": "next group down"
  },
  "EF2D": {
   "unit": "game/trainerAiE000",
   "line": "until group 0 has been counted"
  },
  "EF2F": {
   "unit": "game/trainerAiE000",
   "line": "converging strength"
  },
  "EF31": {
   "unit": "game/trainerAiE000",
   "line": "eight units' worth is the trigger for the all-out attack"
  },
  "EF33": {
   "unit": "game/trainerAiE000",
   "line": "not enough yet - go back to hunting clusters"
  },
  "EF35": {
   "unit": "game/trainerAiE000",
   "block": [
    "All-out attack: flag every group as committed, then stamp the order clock of all 46 mobile AI units",
    "with the current game clock.  scanTrainerUnitsForOrders re-plans a unit whose stamp has caught up with",
    "the down-counting clock ($E2E3), so this makes the whole army replan on the next scan."
   ],
   "line": "$8953 = highest group id in use"
  },
  "EF38": {
   "unit": "game/trainerAiE000",
   "line": "flags of group Y"
  },
  "EF3B": {
   "unit": "game/trainerAiE000",
   "line": "bit 6 = committed to the attack"
  },
  "EF3D": {
   "unit": "game/trainerAiE000",
   "line": "store them back"
  },
  "EF40": {
   "unit": "game/trainerAiE000",
   "line": "next group down"
  },
  "EF41": {
   "unit": "game/trainerAiE000",
   "line": "until group 0 is done"
  },
  "EF43": {
   "unit": "game/trainerAiE000",
   "line": "$2D = 45: units 50..95, the AI's 28 grunts, 12 riders and 6 boomers (its 3 spies and comcen are left alone)"
  },
  "EF45": {
   "unit": "game/trainerAiE000",
   "line": "$91CB = the down-counting game clock"
  },
  "EF48": {
   "unit": "game/trainerAiE000",
   "line": "$8800+ordinal = the round this unit may next be given orders"
  },
  "EF4B": {
   "unit": "game/trainerAiE000",
   "line": "next AI unit"
  },
  "EF4C": {
   "unit": "game/trainerAiE000",
   "line": "all 46 of them"
  },
  "EF4E": {
   "unit": "game/trainerAiE000",
   "line": "$15 = 21 exchanges ..."
  },
  "EF50": {
   "unit": "game/trainerAiE000",
   "line": "... before the drone follows the ground attack in"
  },
  "EF53": {
   "unit": "game/trainerAiE000",
   "line": "$31 = 49 = the human COMCEN"
  },
  "EF55": {
   "unit": "game/trainerAiE000",
   "line": "that is the drone's target"
  },
  "EF58": {
   "unit": "game/trainerAiE000",
   "line": "no target this round"
  },
  "EF59": {
   "unit": "game/trainerAiE000",
   "routine": [
    "pickDroneTargetAlongThreatAxis - Tail of chooseAiDroneTarget for the case where the human comcen is no use as a drone target. It scans the threat around the AI's current objective (the escorted group's centre, or the AI comcen when no group is being escorted), and if the pressure reaches that group's own strength and something really has the point in weapon range, it walks a ray in the direction the enemy is coming from ((quietest+4) AND 7) at radii 4 to 14, scoring each 2x2 block of cells with scoreTargetBlock. The highest-numbered human unit in the best-scoring block becomes the drone target, and the acceptance threshold $8979 is cut to three quarters so the AI grows less choosy as the game goes on.",
    "In:  $895D escorted group ($FF = none), $88EA/$88F6 group centre, $8902 group strength, $8979 block-score threshold (set per opening play by the trainer setup overlay: $FF, $FF, 8, $0C, $0C), $F6A3/$F707 AI comcen cell",
    "Out: $8976 = drone target unit (untouched when nothing scored), $8979 scaled by 3/4, $8956 = the threshold used, zp_1C/zp_1D best score and unit, zp_1E/zp_1F ray direction and radius, zp_18/zp_19/zp_22/zp_24/zp_27/zp_28 and zp_94/zp_95 clobbered",
    "Called from: BPL at $EF04 and BCC at $EF33 inside chooseAiDroneTarget - it is that routine's fall-through tail, not a separate entry"
   ],
   "block": [
    "Choose the point to scan around and the pressure it takes to make the AI bother: the group the comcen",
    "escorts and its member count, or the AI comcen itself with a fixed threshold of 3."
   ],
   "line": "default scan point: the AI's own comcen column"
  },
  "EF5C": {
   "unit": "game/trainerAiE000",
   "line": "and row"
  },
  "EF5F": {
   "unit": "game/trainerAiE000",
   "line": "default threat threshold"
  },
  "EF61": {
   "unit": "game/trainerAiE000",
   "line": "$8956 doubles as the trainer's 'how much pressure is worth reacting to' value"
  },
  "EF64": {
   "unit": "game/trainerAiE000",
   "line": "$895D = the group the AI comcen escorts"
  },
  "EF67": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 = no group is being escorted - keep the comcen defaults"
  },
  "EF69": {
   "unit": "game/trainerAiE000",
   "line": "Y = that group"
  },
  "EF6A": {
   "unit": "game/trainerAiE000",
   "line": "$8902 = its member count"
  },
  "EF6D": {
   "unit": "game/trainerAiE000",
   "line": "a big group needs more pressure before its drone is called in"
  },
  "EF70": {
   "unit": "game/trainerAiE000",
   "line": "the group's centre column ..."
  },
  "EF73": {
   "unit": "game/trainerAiE000",
   "line": "... and centre row ..."
  },
  "EF76": {
   "unit": "game/trainerAiE000",
   "line": "... become the scan point"
  },
  "EF77": {
   "unit": "game/trainerAiE000",
   "line": "zp_27 = reference column for the threat scan"
  },
  "EF79": {
   "unit": "game/trainerAiE000",
   "line": "zp_28 = reference row (the scan routine is written for the comcen but works on any point)"
  },
  "EF7B": {
   "unit": "game/trainerAiE000",
   "line": "A = threat level, X = deepest weapon-range penetration, Y = quietest compass direction"
  },
  "EF7E": {
   "unit": "game/trainerAiE000",
   "line": "compare the measured pressure with the threshold"
  },
  "EF81": {
   "unit": "game/trainerAiE000",
   "line": "too quiet around the objective - no drone target"
  },
  "EF83": {
   "unit": "game/trainerAiE000",
   "line": "X = the range surplus"
  },
  "EF84": {
   "unit": "game/trainerAiE000",
   "line": "nothing actually has the objective in weapon range - not worth a drone either"
  },
  "EF86": {
   "unit": "game/trainerAiE000",
   "block": [
    "Walk a ray from the objective straight into the enemy - the direction opposite the quietest one -",
    "and score every 2x2 block of cells along it from 4 to 14 cells out.  scoreTargetBlock keeps the best",
    "score in zp_1C and the strongest human unit of that block in zp_1D."
   ],
   "line": "A = the quietest direction"
  },
  "EF87": {
   "unit": "game/trainerAiE000",
   "line": "clear carry"
  },
  "EF88": {
   "unit": "game/trainerAiE000",
   "line": "+4 of the 8 compass points = the exact opposite ..."
  },
  "EF8A": {
   "unit": "game/trainerAiE000",
   "line": "... modulo 8: the direction the enemy is pressing from"
  },
  "EF8C": {
   "unit": "game/trainerAiE000",
   "line": "zp_1E = the ray's direction"
  },
  "EF8E": {
   "unit": "game/trainerAiE000",
   "line": "start 4 cells out from the objective"
  },
  "EF90": {
   "unit": "game/trainerAiE000",
   "line": "zp_1F = the current radius"
  },
  "EF92": {
   "unit": "game/trainerAiE000",
   "line": "$8979 = the score a block has to beat"
  },
  "EF95": {
   "unit": "game/trainerAiE000",
   "line": "zp_1C = the score to beat. A value with bit 7 set (the $FF that plays 0 and 1 start with) makes scoreTargetBlock accept any block whose score is not negative"
  },
  "EF97": {
   "unit": "game/trainerAiE000",
   "line": "$FF = no unit picked yet"
  },
  "EF99": {
   "unit": "game/trainerAiE000",
   "line": "zp_1D = the unit the best block yields, none yet"
  },
  "EF9B": {
   "unit": "game/trainerAiE000",
   "line": "direction of the ray"
  },
  "EF9D": {
   "unit": "game/trainerAiE000",
   "line": "how far out"
  },
  "EF9F": {
   "unit": "game/trainerAiE000",
   "line": "zp_18/zp_19 = the cell zp_1F cells along the ray, clamped to the map (offsetPointInDirection patches its own LDX operand at $E67C+1 with the direction index); C=1 means it fell off the 40x40 map"
  },
  "EFA2": {
   "unit": "game/trainerAiE000",
   "line": "the ray ran off the 40x40 map - stop scanning"
  },
  "EFA4": {
   "unit": "game/trainerAiE000",
   "line": "X = candidate column"
  },
  "EFA6": {
   "unit": "game/trainerAiE000",
   "line": "Y = candidate row"
  },
  "EFA8": {
   "unit": "game/trainerAiE000",
   "line": "score the 2x2 block at that corner - grunt 1, rider 2, boomer 4, spy 1, comcen 4, negative for the AI's own units. scoreTargetBlock patches $E9E3+1/$E9E7+1 with X and Y so a winning block can drop its corner into zp_95/zp_94; nothing here uses that copy"
  },
  "EFAB": {
   "unit": "game/trainerAiE000",
   "line": "one cell further out"
  },
  "EFAD": {
   "unit": "game/trainerAiE000",
   "line": "current radius"
  },
  "EFAF": {
   "unit": "game/trainerAiE000",
   "line": "stop after 14 cells"
  },
  "EFB1": {
   "unit": "game/trainerAiE000",
   "line": "keep walking the ray"
  },
  "EFB3": {
   "unit": "game/trainerAiE000",
   "line": "zp_1D = highest-numbered human unit standing in the best block. Since the roster runs 0-27 grunts, 28-39 riders, 40-45 boomers, 46-48 spies, 49 comcen, the highest index is also the most valuable unit there"
  },
  "EFB5": {
   "unit": "game/trainerAiE000",
   "line": "still $FF - no block beat the threshold, so no drone target this round"
  },
  "EFB7": {
   "unit": "game/trainerAiE000",
   "line": "it becomes the drone's target ($8976); launchAiDrone aims at it on a later exchange"
  },
  "EFBA": {
   "unit": "game/trainerAiE000",
   "block": [
    "Having spent a target, drop the acceptance threshold to three quarters, so later drones settle for",
    "smaller clusters.  Plays 0 and 1 start at $FF, which counts as 'accept anything'; after three targets",
    "the threshold lands on $6C = 108, which no 2x2 block (maximum 16) can ever reach again."
   ],
   "line": "the current threshold"
  },
  "EFBD": {
   "unit": "game/trainerAiE000",
   "line": "/2 ..."
  },
  "EFBE": {
   "unit": "game/trainerAiE000",
   "line": "... /4"
  },
  "EFBF": {
   "unit": "game/trainerAiE000",
   "line": "zp_18 = a quarter of it"
  },
  "EFC1": {
   "unit": "game/trainerAiE000",
   "line": "the threshold again"
  },
  "EFC4": {
   "unit": "game/trainerAiE000",
   "line": "no borrow wanted"
  },
  "EFC5": {
   "unit": "game/trainerAiE000",
   "line": "threshold minus a quarter = 3/4 of it"
  },
  "EFC7": {
   "unit": "game/trainerAiE000",
   "line": "never taken: A - A/4 cannot borrow, so this is a redundant guard"
  },
  "EFC9": {
   "unit": "game/trainerAiE000",
   "line": "store the reduced threshold"
  },
  "EFCC": {
   "unit": "game/trainerAiE000",
   "line": "back to the planner"
  },
  "EFCD": {
   "unit": "game/trainerAiE000",
   "routine": [
    "strayTextTail - The last three characters, 'CE' and '.' with bit 7 set, of the string 'INSERT DATA DISK AND PRESS SPACE.' that occupies $EFB6-$EFCF in the modem-driver build of this overlay slot. Here the trainer's code runs up to $EFCC, so only this fragment survives.",
    "In:  -",
    "Out: -",
    "Called from: nothing; unreferenced"
   ],
   "block": [
    "$EFCD-$EFFF is not code or data of the trainer: it is the tail of the disk sector, and every build of",
    "the $EC00/$EE00 overlay slot carries the same mastering-tool leftovers here.  Nothing in the",
    "disassembly reads any of it."
   ],
   "line": "'CE' plus '.' with bit 7 set - the tail of a string that belongs to another build"
  },
  "EFD0": {
   "unit": "game/trainerAiE000",
   "line": "'DATA' - start of the shared filler block found at the end of every $EC00/$EE00 overlay"
  },
  "EFD4": {
   "unit": "game/trainerAiE000",
   "line": "'GAME'"
  },
  "EFD8": {
   "unit": "game/trainerAiE000",
   "line": "'CUSTOM OFF', bit 7 set on the last F to terminate the string"
  },
  "EFE2": {
   "unit": "game/trainerAiE000",
   "line": "'DESTROY OFF', bit 7 on the final F; same filler block, never referenced"
  },
  "EFED": {
   "unit": "game/trainerAiE000",
   "line": "'ACCEPTED', bit 7 on the final D; same filler block, never referenced"
  },
  "EFF5": {
   "unit": "game/trainerAiE000",
   "block": [
    "Sector padding: $C0,$DF then $00,$FF to the end of the page.  Identical in all four builds of this",
    "overlay slot apart from the very last byte, and read by nothing."
   ],
   "line": "$C0,$DF then alternating $00,$FF padding to the end of the page"
  },
  "EFFD": {
   "unit": "game/trainerAiE000",
   "line": "last three padding bytes; $EFFF is $FF here and in the track 29 variants, $00 in the modem driver"
  }
 }
}