{
 "chunk": "game_trainerAiE000_E68A",
 "unit": "game/trainerAiE000",
 "unitLabels": {
  "game/trainerAiE000": {
   "E733": "addEnemyThreatWeight",
   "E743": "judgeGroupThreatTotal"
  }
 },
 "notes": {
  "E68A": {
   "unit": "game/trainerAiE000",
   "block": [
    "Compass direction tables shared by the whole trainer AI.  Directions are numbered 0-7 clockwise",
    "from north: 0 N, 1 NE, 2 E, 3 SE, 4 S, 5 SW, 6 W, 7 NW.  A ninth entry (index 8) holds the delta",
    "pair 0,0 so that 'no direction at all' can be expressed as an ordinary index - assessThreatAroundAiComcen",
    "returns 8 when nothing threatens the AI comcen.  Read by $E241/$E274 (comcen push/retreat),",
    "$E482 (path search), offsetPointInDirection ($E66F/$E67E) and getStepDirection ($E816/$E81E)."
   ],
   "line": "column step of directions 0-7: N 0, NE +1, E +1, SE +1, S 0, SW -1, W -1, NW -1"
  },
  "E692": {
   "unit": "game/trainerAiE000",
   "line": "index 8: the null direction, column step 0"
  },
  "E693": {
   "unit": "game/trainerAiE000",
   "line": "row step of directions 0-7: N -1, NE -1, E 0, SE +1, S +1, SW +1, W 0, NW -1"
  },
  "E69B": {
   "unit": "game/trainerAiE000",
   "line": "index 8: the null direction, row step 0"
  },
  "E69C": {
   "unit": "game/trainerAiE000",
   "routine": [
    "isGroupDestinationTooRisky - influence-map test of one candidate destination for the AI group in currentAiGroup ($8955).",
    "Steps Y cells in direction X away from (cellColB,cellRowB) with offsetPointInDirection, grows that cell into the rectangle the",
    "whole formation would cover (the four span bytes $890E left / $891A up / $8926 right / $8932 down, clamped to 0..39) and plants",
    "the four edges into the four CMP operands of its own scan loop.  It then walks all 100 units and sums a signed 16-bit score:",
    "every unit whose square influence area (side 2*unitThreatRadius $0400) overlaps the rectangle adds unitThreatWeight ($0464) if",
    "it is one of the human player's units 0-49 and subtracts it if it is one of the AI's own 50-99 (units of the moving group",
    "itself were already given radius 0 by buildUnitThreatTables).  The verdict compares the enemy surplus with the group's own",
    "combat value $8956/$8957.",
    "In:  X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row; $8955 currentAiGroup, $8954 aiGroupCount,",
    "     span tables $890E/$891A/$8926/$8932, threat tables $0400/$0464 (built by buildUnitThreatTables), group value $8956/$8957,",
    "     $896E aggression-phase clock, $91CB game clock",
    "Out: C=1 = reject (off the map, or enemy threat >= own combat value); C=0 = acceptable.  zp_18/zp_19 = the candidate cell,",
    "     zp_1A/zp_1B = the signed 16-bit threat surplus, zp_1C = the radius of the unit being weighed.",
    "     Self-modifies the CMP operands at $E704, $E70B, $E715 and $E71C.",
    "Called from: findTrainerAdvanceCell ($E444) only."
   ],
   "line": "candidate cell = origin (cellColB,cellRowB) moved Y cells in direction X"
  },
  "E69F": {
   "unit": "game/trainerAiE000",
   "line": "the candidate is on the map, so weigh it"
  },
  "E6A1": {
   "unit": "game/trainerAiE000",
   "line": "the step ran off the edge: return C=1 = reject"
  },
  "E6A2": {
   "unit": "game/trainerAiE000",
   "line": "Y = the group being moved"
  },
  "E6A5": {
   "unit": "game/trainerAiE000",
   "line": "is it one of the aiGroupCount live groups?"
  },
  "E6A8": {
   "unit": "game/trainerAiE000",
   "line": "yes: measure the threat over its formation footprint"
  },
  "E6AA": {
   "unit": "game/trainerAiE000",
   "line": "no such group, so there is nothing to be afraid of"
  },
  "E6AB": {
   "unit": "game/trainerAiE000",
   "line": "return C=0 = destination acceptable"
  },
  "E6AC": {
   "unit": "game/trainerAiE000",
   "block": [
    "Grow the candidate cell into the rectangle the whole formation would stand on, clamp it to the 40x40",
    "map, and write its four edges straight into the four CMP instructions of the unit scan below."
   ],
   "line": "candidate column"
  },
  "E6AE": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "E6AF": {
   "unit": "game/trainerAiE000",
   "line": "minus the group's span to the left = left edge of the formation box"
  },
  "E6B2": {
   "unit": "game/trainerAiE000",
   "line": "still on the map"
  },
  "E6B4": {
   "unit": "game/trainerAiE000",
   "line": "hangs off the left edge: clamp the box to column 0"
  },
  "E6B6": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the CMP at $E714 now tests against the box's left column"
  },
  "E6B9": {
   "unit": "game/trainerAiE000",
   "line": "candidate column again"
  },
  "E6BB": {
   "unit": "game/trainerAiE000",
   "line": "prepare the addition"
  },
  "E6BC": {
   "unit": "game/trainerAiE000",
   "line": "plus the group's span to the right = right edge of the box"
  },
  "E6BF": {
   "unit": "game/trainerAiE000",
   "line": "past the last column (40)?"
  },
  "E6C1": {
   "unit": "game/trainerAiE000",
   "line": "no, keep it"
  },
  "E6C3": {
   "unit": "game/trainerAiE000",
   "line": "yes: clamp the box to column 39"
  },
  "E6C5": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the CMP at $E71B now tests against the box's right column"
  },
  "E6C8": {
   "unit": "game/trainerAiE000",
   "line": "candidate row"
  },
  "E6CA": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "E6CB": {
   "unit": "game/trainerAiE000",
   "line": "minus the group's span upwards = top edge of the box"
  },
  "E6CE": {
   "unit": "game/trainerAiE000",
   "line": "still on the map"
  },
  "E6D0": {
   "unit": "game/trainerAiE000",
   "line": "hangs off the top: clamp the box to row 0"
  },
  "E6D2": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the CMP at $E703 now tests against the box's top row"
  },
  "E6D5": {
   "unit": "game/trainerAiE000",
   "line": "candidate row again"
  },
  "E6D7": {
   "unit": "game/trainerAiE000",
   "line": "prepare the addition"
  },
  "E6D8": {
   "unit": "game/trainerAiE000",
   "line": "plus the group's span downwards = bottom edge of the box"
  },
  "E6DB": {
   "unit": "game/trainerAiE000",
   "line": "past the last row (40)?"
  },
  "E6DD": {
   "unit": "game/trainerAiE000",
   "line": "no, keep it"
  },
  "E6DF": {
   "unit": "game/trainerAiE000",
   "line": "yes: clamp the box to row 39"
  },
  "E6E1": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the CMP at $E70A now tests against the box's bottom row"
  },
  "E6E4": {
   "unit": "game/trainerAiE000",
   "block": [
    "Walk all 100 units, unit 99 down to unit 0, and accumulate a signed 16-bit 'how dangerous is this",
    "square' score in zp_1A/zp_1B: enemy influence adds, friendly influence subtracts.  A unit only",
    "counts when its influence square (centre = its cell, half-width = unitThreatRadius) overlaps the",
    "formation box just computed."
   ],
   "line": "clear the running score"
  },
  "E6E6": {
   "unit": "game/trainerAiE000",
   "line": "threat surplus, low byte"
  },
  "E6E8": {
   "unit": "game/trainerAiE000",
   "line": "threat surplus, high byte"
  },
  "E6EA": {
   "unit": "game/trainerAiE000",
   "line": "start at unit 99"
  },
  "E6EC": {
   "unit": "game/trainerAiE000",
   "line": "unitThreatRadius[unit] from the $0400 influence page"
  },
  "E6EF": {
   "unit": "game/trainerAiE000",
   "line": "radius 0 (destroyed, spy/comcen, or a member of the moving group): the unit does not count"
  },
  "E6F1": {
   "unit": "game/trainerAiE000",
   "line": "keep this unit's influence radius"
  },
  "E6F3": {
   "unit": "game/trainerAiE000",
   "line": "unit 50-99 = one of the AI's own?"
  },
  "E6F5": {
   "unit": "game/trainerAiE000",
   "line": "no, it is one of the human player's units 0-49"
  },
  "E6F7": {
   "unit": "game/trainerAiE000",
   "line": "own unit: is the group being moved group 0, the AI comcen's own group?"
  },
  "E6FA": {
   "unit": "game/trainerAiE000",
   "line": "yes: friendly cover is not credited to the comcen group, so skip own units entirely"
  },
  "E6FC": {
   "unit": "game/trainerAiE000",
   "line": "row of this unit"
  },
  "E6FF": {
   "unit": "game/trainerAiE000",
   "line": "keep the row for the second test"
  },
  "E700": {
   "unit": "game/trainerAiE000",
   "line": "prepare the addition"
  },
  "E701": {
   "unit": "game/trainerAiE000",
   "line": "row + radius = bottom edge of its influence square"
  },
  "E703": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E6D2 with the box's top row"
  },
  "E705": {
   "unit": "game/trainerAiE000",
   "line": "its influence stops above the box: no overlap, next unit"
  },
  "E707": {
   "unit": "game/trainerAiE000",
   "line": "the row again (the CMP above left C=1 for the subtraction)"
  },
  "E708": {
   "unit": "game/trainerAiE000",
   "line": "row - radius = top edge of its influence square"
  },
  "E70A": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E6E1 with the box's bottom row"
  },
  "E70C": {
   "unit": "game/trainerAiE000",
   "line": "its influence starts below the box: no overlap"
  },
  "E70E": {
   "unit": "game/trainerAiE000",
   "line": "column of this unit"
  },
  "E711": {
   "unit": "game/trainerAiE000",
   "line": "keep the column for the second test"
  },
  "E712": {
   "unit": "game/trainerAiE000",
   "line": "column + radius = right edge of its influence square (the CMP above left C=0)"
  },
  "E714": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E6B6 with the box's left column"
  },
  "E716": {
   "unit": "game/trainerAiE000",
   "line": "its influence stops left of the box: no overlap"
  },
  "E718": {
   "unit": "game/trainerAiE000",
   "line": "the column again (the CMP above left C=1 for the subtraction)"
  },
  "E719": {
   "unit": "game/trainerAiE000",
   "line": "column - radius = left edge of its influence square"
  },
  "E71B": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E6C5 with the box's right column"
  },
  "E71D": {
   "unit": "game/trainerAiE000",
   "line": "its influence starts right of the box: no overlap"
  },
  "E71F": {
   "unit": "game/trainerAiE000",
   "line": "the squares overlap, so whose unit is it?"
  },
  "E721": {
   "unit": "game/trainerAiE000",
   "line": "human player's unit 0-49: its weight adds to the danger"
  },
  "E723": {
   "unit": "game/trainerAiE000",
   "line": "one of the AI's own: subtract its weight instead (friendly cover)"
  },
  "E725": {
   "unit": "game/trainerAiE000",
   "line": "unitThreatWeight[unit] from the $0464 influence page (the CPY above left C=1)"
  },
  "E728": {
   "unit": "game/trainerAiE000",
   "line": "store the surplus low byte"
  },
  "E72A": {
   "unit": "game/trainerAiE000",
   "line": "surplus high byte"
  },
  "E72C": {
   "unit": "game/trainerAiE000",
   "line": "propagate the borrow"
  },
  "E72E": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "E730": {
   "unit": "game/trainerAiE000",
   "line": "on to the next unit"
  },
  "E733": {
   "unit": "game/trainerAiE000",
   "line": "surplus low byte"
  },
  "E735": {
   "unit": "game/trainerAiE000",
   "line": "add unitThreatWeight[unit] (the CPY at $E71F left C=0)"
  },
  "E738": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "E73A": {
   "unit": "game/trainerAiE000",
   "line": "surplus high byte"
  },
  "E73C": {
   "unit": "game/trainerAiE000",
   "line": "propagate the carry"
  },
  "E73E": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "E740": {
   "unit": "game/trainerAiE000",
   "line": "next unit"
  },
  "E741": {
   "unit": "game/trainerAiE000",
   "line": "until unit 0 has been weighed"
  },
  "E743": {
   "unit": "game/trainerAiE000",
   "block": [
    "Verdict.  A negative or zero surplus means the AI already dominates the square.  Otherwise the enemy",
    "surplus is compared with this group's own combat value, and an exact draw is settled by the game clock",
    "and the RNG so that both machines would reach the same answer."
   ],
   "line": "sign of the 16-bit surplus"
  },
  "E745": {
   "unit": "game/trainerAiE000",
   "line": "not negative: weigh it against the group's own strength"
  },
  "E747": {
   "unit": "game/trainerAiE000",
   "line": "friendly influence outweighs the enemy there"
  },
  "E748": {
   "unit": "game/trainerAiE000",
   "line": "C=0 = destination acceptable"
  },
  "E749": {
   "unit": "game/trainerAiE000",
   "line": "surplus low byte"
  },
  "E74B": {
   "unit": "game/trainerAiE000",
   "line": "is the whole 16-bit surplus exactly zero?"
  },
  "E74D": {
   "unit": "game/trainerAiE000",
   "line": "no enemy influence at all: accept"
  },
  "E74F": {
   "unit": "game/trainerAiE000",
   "line": "surplus low byte"
  },
  "E751": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "E752": {
   "unit": "game/trainerAiE000",
   "line": "minus the group's own combat value, low byte"
  },
  "E755": {
   "unit": "game/trainerAiE000",
   "line": "keep the difference"
  },
  "E757": {
   "unit": "game/trainerAiE000",
   "line": "surplus high byte"
  },
  "E759": {
   "unit": "game/trainerAiE000",
   "line": "minus the group's combat value, high byte"
  },
  "E75C": {
   "unit": "game/trainerAiE000",
   "line": "keep it"
  },
  "E75E": {
   "unit": "game/trainerAiE000",
   "line": "is the difference exactly zero?"
  },
  "E760": {
   "unit": "game/trainerAiE000",
   "line": "no: return the carry of the subtraction (C=1 = threat >= own strength = reject)"
  },
  "E762": {
   "unit": "game/trainerAiE000",
   "line": "dead heat: consult the aggression-phase clock set at setup ($EF00 area) or by retreatTrainerComcen"
  },
  "E765": {
   "unit": "game/trainerAiE000",
   "line": "has the game reached that round?  the game clock counts down, so 'phase clock >= clock' means yes"
  },
  "E768": {
   "unit": "game/trainerAiE000",
   "line": "yes, late enough in the game: toss a coin"
  },
  "E76A": {
   "unit": "game/trainerAiE000",
   "line": "still early: play safe"
  },
  "E76B": {
   "unit": "game/trainerAiE000",
   "line": "always taken (the BCS above fell through with C=1): return C=1 = reject"
  },
  "E76D": {
   "unit": "game/trainerAiE000",
   "line": "step the lock-step game RNG so both machines stay in sync"
  },
  "E770": {
   "unit": "game/trainerAiE000",
   "line": "C=1 for half of the random values, i.e. reject a tie 50 percent of the time"
  },
  "E772": {
   "unit": "game/trainerAiE000",
   "line": "carry tells the caller whether the destination is too dangerous"
  },
  "E773": {
   "unit": "game/trainerAiE000",
   "routine": [
    "buildUnitThreatTables - rebuilds the two 100-entry influence-map tables that isGroupDestinationTooRisky reads.",
    "For each unit 99..0 it writes an influence radius into $0400 and a combat weight into $0464.  Destroyed units, spies and",
    "comcens (type 3 and 4) and every AI unit belonging to the group that is about to be moved get radius 0 and weight 0.",
    "A grunt or rider gets base radius 3, a boomer 6, both plus the terrain sight bonus of the tile the unit is standing on,",
    "so units on high ground project further.  The weight comes from unitCombatValueTable ($E2B7) = 2,2,3,0,0.",
    "In:  $8955 currentAiGroup, aiUnitGroup ($8800+absolute unit index), unitTypeTable $F76C, unitTerrainUnderTable $FAF0,",
    "     terrainClassTable $077E, terrainSightBonusTable $35BB, unitCombatValueTable $E2B7",
    "Out: $0400[0..99] = influence radius, $0464[0..99] = combat weight; zp_18 and zp_19 clobbered",
    "Called from: findTrainerAdvanceCell ($E42A), immediately before its search loop."
   ],
   "block": [
    "One entry per unit, walking backwards from unit 99 to unit 0."
   ],
   "line": "start at unit 99"
  },
  "E775": {
   "unit": "game/trainerAiE000",
   "line": "is this one of the AI's own units 50-99?"
  },
  "E777": {
   "unit": "game/trainerAiE000",
   "line": "no, it is a human player unit: no group test needed"
  },
  "E779": {
   "unit": "game/trainerAiE000",
   "line": "aiUnitGroup[unit] - the group table addressed by absolute index ($8800+unit = $8832+ordinal)"
  },
  "E77C": {
   "unit": "game/trainerAiE000",
   "line": "does it belong to the very group we are about to move?"
  },
  "E77F": {
   "unit": "game/trainerAiE000",
   "line": "yes: a group must not weigh its own members"
  },
  "E781": {
   "unit": "game/trainerAiE000",
   "line": "unit type byte, bit 7 = destroyed"
  },
  "E784": {
   "unit": "game/trainerAiE000",
   "line": "alive: work out its influence radius"
  },
  "E786": {
   "unit": "game/trainerAiE000",
   "line": "destroyed, or a member of the moving group: no influence"
  },
  "E788": {
   "unit": "game/trainerAiE000",
   "line": "always taken (A = 0): store radius 0"
  },
  "E78A": {
   "unit": "game/trainerAiE000",
   "line": "default influence radius 3 cells"
  },
  "E78C": {
   "unit": "game/trainerAiE000",
   "line": "unit type 0-4 without the status bits"
  },
  "E78E": {
   "unit": "game/trainerAiE000",
   "line": "keep the type for the weight look-up"
  },
  "E790": {
   "unit": "game/trainerAiE000",
   "line": "GRUNT (0) or RIDER (1)?"
  },
  "E792": {
   "unit": "game/trainerAiE000",
   "line": "yes: keep radius 3"
  },
  "E794": {
   "unit": "game/trainerAiE000",
   "line": "type 3 SPY or 4 COMCEN: they project no threat at all"
  },
  "E796": {
   "unit": "game/trainerAiE000",
   "line": "BOOMER (2): its guns reach twice as far, radius 6"
  },
  "E798": {
   "unit": "game/trainerAiE000",
   "line": "stash the base radius"
  },
  "E79A": {
   "unit": "game/trainerAiE000",
   "line": "terrain code of the tile hidden under the unit"
  },
  "E79D": {
   "unit": "game/trainerAiE000",
   "line": "terrain class 0-8 of that tile"
  },
  "E7A0": {
   "unit": "game/trainerAiE000",
   "line": "class becomes the index"
  },
  "E7A1": {
   "unit": "game/trainerAiE000",
   "line": "extra range the terrain grants: 0, 1 or 2 cells"
  },
  "E7A4": {
   "unit": "game/trainerAiE000",
   "line": "prepare the addition"
  },
  "E7A5": {
   "unit": "game/trainerAiE000",
   "line": "radius = base radius + terrain bonus"
  },
  "E7A7": {
   "unit": "game/trainerAiE000",
   "line": "unitThreatRadius[unit] into the $0400 scratch page"
  },
  "E7AA": {
   "unit": "game/trainerAiE000",
   "line": "is the radius zero?"
  },
  "E7AB": {
   "unit": "game/trainerAiE000",
   "line": "yes: A is already 0, so the weight becomes 0 too"
  },
  "E7AD": {
   "unit": "game/trainerAiE000",
   "line": "unit type"
  },
  "E7AF": {
   "unit": "game/trainerAiE000",
   "line": "combat weight: GRUNT 2, RIDER 2, BOOMER 3, SPY 0, COMCEN 0"
  },
  "E7B2": {
   "unit": "game/trainerAiE000",
   "line": "unitThreatWeight[unit] into the $0464 scratch page"
  },
  "E7B5": {
   "unit": "game/trainerAiE000",
   "line": "next unit"
  },
  "E7B6": {
   "unit": "game/trainerAiE000",
   "line": "until unit 0 has an entry"
  },
  "E7B8": {
   "unit": "game/trainerAiE000",
   "line": "both influence tables are current"
  },
  "E7B9": {
   "unit": "game/trainerAiE000",
   "routine": [
    "multiplyAbyY - 8-bit multiply by repeated addition: A = A * Y modulo 256, and A = 0 when Y = 0.",
    "The multiplicand is copied into its own ADC operand and Y is restored from a second patched operand before returning,",
    "so the caller's step count survives the call.",
    "In:  A = multiplicand, Y = count",
    "Out: A = A*Y; Y unchanged; X untouched, flags clobbered; self-modifies the operands at $E7CB and $E7D0",
    "Called from: offsetCoordinateClamped ($EA22) only, where A is a step of 0, +1 or $FF and Y a cell distance."
   ],
   "line": "self-modify: stash the count in the LDY at $E7CF so Y can be handed back"
  },
  "E7BC": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the multiplicand becomes the operand of the ADC at $E7CA"
  },
  "E7BF": {
   "unit": "game/trainerAiE000",
   "line": "multiply by zero?"
  },
  "E7C1": {
   "unit": "game/trainerAiE000",
   "line": "no: run the addition loop"
  },
  "E7C3": {
   "unit": "game/trainerAiE000",
   "line": "Y is 0, so A = 0"
  },
  "E7C4": {
   "unit": "game/trainerAiE000",
   "line": "always taken: return zero"
  },
  "E7C6": {
   "unit": "game/trainerAiE000",
   "line": "one addition per unit of the count"
  },
  "E7C7": {
   "unit": "game/trainerAiE000",
   "line": "count used up: A holds the product"
  },
  "E7C9": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry before each addition"
  },
  "E7CA": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E7BC: add the multiplicand once more"
  },
  "E7CC": {
   "unit": "game/trainerAiE000",
   "line": "keep adding; a running total of exactly 0 would end the loop early, which the callers' 0/+1/-1 steps never produce"
  },
  "E7CE": {
   "unit": "game/trainerAiE000",
   "line": "hold the product across the Y restore"
  },
  "E7CF": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E7B9: give the caller its count back"
  },
  "E7D1": {
   "unit": "game/trainerAiE000",
   "line": "product back into A"
  },
  "E7D2": {
   "unit": "game/trainerAiE000",
   "line": "A = A*Y"
  },
  "E7D3": {
   "unit": "game/trainerAiE000",
   "routine": [
    "buildDirectionFanTowardTarget - finds the compass heading from the line start (lineCurCol,lineCurRow) to the line end",
    "(lineEndCol,lineEndRow) with getStepDirection, saves it in zp_19 and falls into buildDirectionFan so the eight candidate",
    "move directions are laid out around that heading.  Nothing is built when start and target are the same cell.",
    "In:  zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column, $8955 currentAiGroup",
    "Out: zp_19 = heading 0-7 ($FF and no fan when start = target); $8896-$889D = the eight candidate directions;",
    "     zp_18 and the line-stepper state $8C-$97 clobbered",
    "Called from: the group-movement code at $E416."
   ],
   "line": "heading of the first Bresenham step from the group centre toward its target"
  },
  "E7D6": {
   "unit": "game/trainerAiE000",
   "line": "remember the heading for buildDirectionFan"
  },
  "E7D8": {
   "unit": "game/trainerAiE000",
   "line": "a real heading 0-7: lay the eight candidates out around it"
  },
  "E7DA": {
   "unit": "game/trainerAiE000",
   "line": "Y = $FF, the group is already on its target cell: no fan to build"
  },
  "E7DB": {
   "unit": "game/trainerAiE000",
   "block": [
    "dirMirrorTable - eight bytes, entry d = (8 - d) AND 7, i.e. the mirror of a heading offset about the",
    "north-south axis (0 stays 0, +1 becomes -1 and so on).  buildDirectionFan runs the offsets of a",
    "'left-handed' group through it so that half the groups sweep clockwise and half anticlockwise.",
    "The label in the middle of the table is a branch target of the modem-driver build at the same address,",
    "not a separate block."
   ],
   "line": "mirror of heading offsets 0-4: 0, -1, -2, -3, -4 expressed modulo 8"
  },
  "E7E0": {
   "unit": "game/trainerAiE000",
   "line": "mirror of heading offsets 5-7: +3, +2, +1"
  },
  "E7E3": {
   "unit": "game/trainerAiE000",
   "routine": [
    "buildDirectionFan - fills the eight-byte scratch buffer $8896-$889D with the move directions the path search should try,",
    "in order of preference: entry i = (dirFanOffsetTable[i] + heading + 8) AND 7.  dirFanOffsetTable ($894A) is built once per",
    "game by the setup overlay's buildAiHeadingSearchOrder ($EEC3) as 0, +/-1, +/-1, +/-2, +/-2, +/-3, +/-3, +/-4 with a random",
    "left/right bias, so the search always tries straight ahead first and fans outwards.  When bit 0 of the group's groupFlags",
    "is set every offset is mirrored through dirMirrorTable, giving that group the opposite handedness.",
    "In:  zp_19 = wanted heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable",
    "Out: $8896-$889D = eight candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered",
    "Called from: decideTrainerGroupAction ($E36E) and by falling through from buildDirectionFanTowardTarget ($E7D8).",
    "The list is consumed by the move search at $E431-$E47E, whose per-rank distance caps are in maxAdvanceByRankTable ($E664)."
   ],
   "line": "assume the fan is not mirrored"
  },
  "E7E5": {
   "unit": "game/trainerAiE000",
   "line": "the group being moved"
  },
  "E7E8": {
   "unit": "game/trainerAiE000",
   "line": "its groupFlags; the low nibble is the random start delay handed out at setup"
  },
  "E7EB": {
   "unit": "game/trainerAiE000",
   "line": "bit 0 of that delay serves as the group's fixed handedness"
  },
  "E7ED": {
   "unit": "game/trainerAiE000",
   "line": "clear: fan out the normal way"
  },
  "E7EF": {
   "unit": "game/trainerAiE000",
   "line": "set: X = $FF, mirror every offset"
  },
  "E7F0": {
   "unit": "game/trainerAiE000",
   "line": "mirror flag for the loop below"
  },
  "E7F2": {
   "unit": "game/trainerAiE000",
   "line": "fill the eight preference slots, 7 down to 0"
  },
  "E7F4": {
   "unit": "game/trainerAiE000",
   "line": "heading offset of preference rank Y"
  },
  "E7F7": {
   "unit": "game/trainerAiE000",
   "line": "is this group mirrored?  (BIT leaves A alone)"
  },
  "E7F9": {
   "unit": "game/trainerAiE000",
   "line": "no: use the offset as it is"
  },
  "E7FB": {
   "unit": "game/trainerAiE000",
   "line": "offset becomes the index"
  },
  "E7FC": {
   "unit": "game/trainerAiE000",
   "line": "yes: swap left for right"
  },
  "E7FF": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry before adding"
  },
  "E800": {
   "unit": "game/trainerAiE000",
   "line": "candidate = wanted heading + offset"
  },
  "E802": {
   "unit": "game/trainerAiE000",
   "line": "the addition above may have carried"
  },
  "E803": {
   "unit": "game/trainerAiE000",
   "line": "bias by a whole turn so a negative offset stays positive"
  },
  "E805": {
   "unit": "game/trainerAiE000",
   "line": "wrap back into a compass direction 0-7"
  },
  "E807": {
   "unit": "game/trainerAiE000",
   "line": "store the candidate for preference rank Y"
  },
  "E80A": {
   "unit": "game/trainerAiE000",
   "line": "next rank"
  },
  "E80B": {
   "unit": "game/trainerAiE000",
   "line": "until all eight ranks are filled"
  },
  "E80D": {
   "unit": "game/trainerAiE000",
   "line": "the fan is ready for the path search"
  },
  "E80E": {
   "unit": "game/trainerAiE000",
   "routine": [
    "getStepDirection - sets a Bresenham walker up between (lineCurCol,lineCurRow) and (lineEndCol,lineEndRow), takes exactly one",
    "step, and converts the deltas that step applied ($90F5 column, $90F4 row) back into one of the eight compass directions by",
    "searching dirToColDelta/dirToRowDelta from index 7 downwards.  stepLine zeroes both deltas before it moves, so a zero-length",
    "line leaves 0,0 - which matches no entry 0-7 and comes out as Y = $FF.",
    "In:  zp_92/zp_93 = start row/column, zp_94/zp_95 = end row/column",
    "Out: Y = direction 0-7, or $FF when start and end are the same cell; A = the last delta compared;",
    "     line-stepper state $8C-$97 and $90F4/$90F5 updated",
    "Called from: buildDirectionFanTowardTarget ($E7D3), assessThreatAroundAiComcen ($EC35), the drone code ($EEC7)",
    "and the $EE00 drone-flight overlay ($EEB1)."
   ],
   "line": "set the Bresenham walker up between the two cells"
  },
  "E811": {
   "unit": "game/trainerAiE000",
   "line": "take one step; it records the deltas it applied in $90F5 (column) and $90F4 (row), zeroing them first"
  },
  "E814": {
   "unit": "game/trainerAiE000",
   "line": "search the direction tables backwards from 7"
  },
  "E816": {
   "unit": "game/trainerAiE000",
   "line": "column step of direction Y"
  },
  "E819": {
   "unit": "game/trainerAiE000",
   "line": "does it match the step just taken?"
  },
  "E81C": {
   "unit": "game/trainerAiE000",
   "line": "no: try the next direction"
  },
  "E81E": {
   "unit": "game/trainerAiE000",
   "line": "row step of direction Y"
  },
  "E821": {
   "unit": "game/trainerAiE000",
   "line": "does that match too?"
  },
  "E824": {
   "unit": "game/trainerAiE000",
   "line": "both match: Y is the heading"
  },
  "E826": {
   "unit": "game/trainerAiE000",
   "line": "next direction down"
  },
  "E827": {
   "unit": "game/trainerAiE000",
   "line": "index 8 (the null delta) is deliberately never tested, so Y = $FF means 'the line has no step'"
  },
  "E829": {
   "unit": "game/trainerAiE000",
   "line": "Y = compass direction, or $FF"
  },
  "E82A": {
   "unit": "game/trainerAiE000",
   "routine": [
    "cellDistance - integer distance between two map cells.  distanceSquared ($3870) gives |dcol|^2 + |drow|^2 (each delta capped",
    "at 15, the sum saturating at $FF with C=1), and this routine converts it back through squaresTable ($3860) into the largest n",
    "with n*n <= dsq.  A saturated squared distance simply reports 16.",
    "In:  zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row",
    "Out: A = X = distance 0-16; Y untouched",
    "Called from: $E410, $E4B4, assessThreatAroundAiComcen ($EC24), the missile code ($ED43/$EE0C/$EE5C) and the drone overlay."
   ],
   "line": "A = squared distance between (cellColA,cellRowA) and (cellColB,cellRowB), C=1 if it saturated at 255"
  },
  "E82D": {
   "unit": "game/trainerAiE000",
   "line": "16 is the largest distance this routine reports"
  },
  "E82F": {
   "unit": "game/trainerAiE000",
   "line": "saturated: everything beyond 15 cells is simply 'far'"
  },
  "E831": {
   "unit": "game/trainerAiE000",
   "line": "otherwise start the search at 15"
  },
  "E832": {
   "unit": "game/trainerAiE000",
   "line": "is the squared distance at least X*X?"
  },
  "E835": {
   "unit": "game/trainerAiE000",
   "line": "yes: X is the integer distance"
  },
  "E837": {
   "unit": "game/trainerAiE000",
   "line": "try the next smaller radius"
  },
  "E838": {
   "unit": "game/trainerAiE000",
   "line": "X = 0 falls out of the loop as distance 0"
  },
  "E83A": {
   "unit": "game/trainerAiE000",
   "line": "hand the distance back in A as well as X"
  },
  "E83B": {
   "unit": "game/trainerAiE000",
   "line": "A = X = distance in cells"
  },
  "E83C": {
   "unit": "game/trainerAiE000",
   "routine": [
    "updateFocusGroupCentre - keeps focusGroupId ($895D) and the focus centre ($895E column / $895F row) current.  The focus group",
    "is the assault group the AI comcen walks along behind.  When the id has bit 7 set the centre falls back on the AI comcen's own",
    "cell, and a suspended group is taken back into service as soon as bit 7 of its groupFlags has cleared.  An active group whose",
    "groupFlags bit 7 is set (it has reached its objective) is suspended by ORing $80 into the id.  Otherwise the centroid of the",
    "group's combat members is recomputed, and a group that has lost every member is dropped ($895D = $FF).",
    "In:  $895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707, $8832 aiUnitGroup",
    "Out: $895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered",
    "Called from: planTrainerRound ($E20C), once per round just before the comcen push/retreat tail."
   ],
   "line": "focusGroupId: $FF = no group, bit 7 set = the group is suspended"
  },
  "E83F": {
   "unit": "game/trainerAiE000",
   "line": "an active group id 0-15: go and refresh its centre"
  },
  "E841": {
   "unit": "game/trainerAiE000",
   "line": "no usable group: fall back on the AI comcen's own cell (unit 99)"
  },
  "E844": {
   "unit": "game/trainerAiE000",
   "line": "focus centre column ($895E)"
  },
  "E847": {
   "unit": "game/trainerAiE000",
   "line": "AI comcen row"
  },
  "E84A": {
   "unit": "game/trainerAiE000",
   "line": "focus centre row"
  },
  "E84D": {
   "unit": "game/trainerAiE000",
   "line": "was the focus group dropped altogether?"
  },
  "E84F": {
   "unit": "game/trainerAiE000",
   "line": "yes: nothing left to revive"
  },
  "E851": {
   "unit": "game/trainerAiE000",
   "line": "suspended group: recover its id"
  },
  "E852": {
   "unit": "game/trainerAiE000",
   "line": "strip the suspend bit"
  },
  "E854": {
   "unit": "game/trainerAiE000",
   "line": "index the group tables with it"
  },
  "E855": {
   "unit": "game/trainerAiE000",
   "line": "its groupFlags"
  },
  "E858": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 still set (the group is at its objective): leave it suspended"
  },
  "E85A": {
   "unit": "game/trainerAiE000",
   "line": "the flag cleared: put the group back in service"
  },
  "E85D": {
   "unit": "game/trainerAiE000",
   "line": "always taken (an id 0-15 is positive): recompute its centre"
  },
  "E85F": {
   "unit": "game/trainerAiE000",
   "line": "nothing to update this round"
  },
  "E860": {
   "unit": "game/trainerAiE000",
   "line": "groupFlags of the active focus group"
  },
  "E863": {
   "unit": "game/trainerAiE000",
   "line": "still on its way: recompute the centre"
  },
  "E865": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 set: the group has arrived"
  },
  "E866": {
   "unit": "game/trainerAiE000",
   "line": "mark the id as suspended"
  },
  "E868": {
   "unit": "game/trainerAiE000",
   "line": "store it back; the centre keeps the comcen fallback from the next call"
  },
  "E86B": {
   "unit": "game/trainerAiE000",
   "line": "done for this round"
  },
  "E86C": {
   "unit": "game/trainerAiE000",
   "line": "group id for the centroid scan"
  },
  "E86F": {
   "unit": "game/trainerAiE000",
   "line": "scan AI ordinals 45 down to 0 - the combat units only, since 46-48 are the spies and 49 the comcen"
  },
  "E871": {
   "unit": "game/trainerAiE000",
   "line": "A = member count, X = average column, Y = average row"
  },
  "E874": {
   "unit": "game/trainerAiE000",
   "line": "the group still has members"
  },
  "E876": {
   "unit": "game/trainerAiE000",
   "line": "no member left"
  },
  "E878": {
   "unit": "game/trainerAiE000",
   "line": "drop the focus group; X/Y are 0/$FF here, so the centre is stale for the one round until the fallback runs"
  },
  "E87B": {
   "unit": "game/trainerAiE000",
   "line": "focus centre column"
  },
  "E87E": {
   "unit": "game/trainerAiE000",
   "line": "focus centre row"
  },
  "E881": {
   "unit": "game/trainerAiE000",
   "line": "the comcen now has something to escort"
  },
  "E882": {
   "unit": "game/trainerAiE000",
   "routine": [
    "computeGroupCentroid - averages the position and the ordered destination of every living member of one AI group.  It scans AI",
    "ordinals Y down to 0 (absolute units 50+Y down to 50), accumulates four 16-bit sums and divides each by the member count with",
    "divideSigned16by8.  The wanted group id is patched into its own CMP, and the two averaged coordinates are handed back through",
    "two patched immediate operands so that A can carry the member count.",
    "In:  A = group id, Y = highest AI ordinal to scan (usually $31 = 49 or $2D = 45); $8832 aiUnitGroup, $F79E unitType,",
    "     $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow (all indexed by AI ordinal)",
    "Out: A = member count (Z set when the group is empty), X = average column, Y = average row, zp_1E = average destination column,",
    "     zp_1F = average destination row, zp_1C = member count; zp_18-zp_21 clobbered;",
    "     self-modifies the operands at $E89D, $E913 and $E915",
    "Called from: computeTrainerGroupStats ($E5B9) and updateFocusGroupCentre ($E871)."
   ],
   "line": "self-modify: the wanted group id becomes the operand of the CMP at $E89C"
  },
  "E885": {
   "unit": "game/trainerAiE000",
   "line": "clear all nine accumulator bytes"
  },
  "E887": {
   "unit": "game/trainerAiE000",
   "line": "column sum, low"
  },
  "E889": {
   "unit": "game/trainerAiE000",
   "line": "column sum, high"
  },
  "E88B": {
   "unit": "game/trainerAiE000",
   "line": "row sum, low"
  },
  "E88D": {
   "unit": "game/trainerAiE000",
   "line": "row sum, high"
  },
  "E88F": {
   "unit": "game/trainerAiE000",
   "line": "member count"
  },
  "E891": {
   "unit": "game/trainerAiE000",
   "line": "ordered-destination column sum, low"
  },
  "E893": {
   "unit": "game/trainerAiE000",
   "line": "ordered-destination column sum, high"
  },
  "E895": {
   "unit": "game/trainerAiE000",
   "line": "ordered-destination row sum, low"
  },
  "E897": {
   "unit": "game/trainerAiE000",
   "line": "ordered-destination row sum, high"
  },
  "E899": {
   "unit": "game/trainerAiE000",
   "block": [
    "One pass per AI ordinal, from the caller's starting ordinal down to 0."
   ],
   "line": "aiUnitGroup[ordinal] - which group this AI unit belongs to"
  },
  "E89C": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E882: is it the group we are averaging?"
  },
  "E89E": {
   "unit": "game/trainerAiE000",
   "line": "no: next ordinal"
  },
  "E8A0": {
   "unit": "game/trainerAiE000",
   "line": "type byte of AI unit 50+ordinal"
  },
  "E8A3": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 set: destroyed, so it does not count"
  },
  "E8A5": {
   "unit": "game/trainerAiE000",
   "line": "one more live member"
  },
  "E8A7": {
   "unit": "game/trainerAiE000",
   "line": "its current column"
  },
  "E8AA": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "E8AB": {
   "unit": "game/trainerAiE000",
   "line": "into the 16-bit column sum"
  },
  "E8AD": {
   "unit": "game/trainerAiE000",
   "line": "store the low byte"
  },
  "E8AF": {
   "unit": "game/trainerAiE000",
   "line": "column sum, high byte"
  },
  "E8B1": {
   "unit": "game/trainerAiE000",
   "line": "propagate the carry"
  },
  "E8B3": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "E8B5": {
   "unit": "game/trainerAiE000",
   "line": "its current row"
  },
  "E8B8": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "E8B9": {
   "unit": "game/trainerAiE000",
   "line": "into the 16-bit row sum"
  },
  "E8BB": {
   "unit": "game/trainerAiE000",
   "line": "store the low byte"
  },
  "E8BD": {
   "unit": "game/trainerAiE000",
   "line": "row sum, high byte"
  },
  "E8BF": {
   "unit": "game/trainerAiE000",
   "line": "propagate the carry"
  },
  "E8C1": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "E8C3": {
   "unit": "game/trainerAiE000",
   "line": "the column it has been ordered to"
  },
  "E8C6": {
   "unit": "game/trainerAiE000",
   "line": "the coordinate is in bits 0-5; bits 6-7 are order flags"
  },
  "E8C8": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "E8C9": {
   "unit": "game/trainerAiE000",
   "line": "into the destination-column sum"
  },
  "E8CB": {
   "unit": "game/trainerAiE000",
   "line": "store the low byte"
  },
  "E8CD": {
   "unit": "game/trainerAiE000",
   "line": "no carry"
  },
  "E8CF": {
   "unit": "game/trainerAiE000",
   "line": "carry into the high byte"
  },
  "E8D1": {
   "unit": "game/trainerAiE000",
   "line": "the row it has been ordered to"
  },
  "E8D4": {
   "unit": "game/trainerAiE000",
   "line": "coordinate bits only"
  },
  "E8D6": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "E8D7": {
   "unit": "game/trainerAiE000",
   "line": "into the destination-row sum"
  },
  "E8D9": {
   "unit": "game/trainerAiE000",
   "line": "store the low byte"
  },
  "E8DB": {
   "unit": "game/trainerAiE000",
   "line": "no carry"
  },
  "E8DD": {
   "unit": "game/trainerAiE000",
   "line": "carry into the high byte"
  },
  "E8DF": {
   "unit": "game/trainerAiE000",
   "line": "next ordinal"
  },
  "E8E0": {
   "unit": "game/trainerAiE000",
   "line": "down to AI ordinal 0"
  },
  "E8E2": {
   "unit": "game/trainerAiE000",
   "block": [
    "Divide the four sums by the member count.  The two positional averages are returned in X and Y",
    "through the two immediate operands patched below."
   ],
   "line": "member count is the divisor"
  },
  "E8E4": {
   "unit": "game/trainerAiE000",
   "line": "no members: skip the divisions and report zero"
  },
  "E8E6": {
   "unit": "game/trainerAiE000",
   "line": "column sum, low"
  },
  "E8E8": {
   "unit": "game/trainerAiE000",
   "line": "column sum, high"
  },
  "E8EA": {
   "unit": "game/trainerAiE000",
   "line": "average column"
  },
  "E8ED": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: it becomes the operand of the LDX at $E912"
  },
  "E8F0": {
   "unit": "game/trainerAiE000",
   "line": "row sum, low"
  },
  "E8F2": {
   "unit": "game/trainerAiE000",
   "line": "row sum, high"
  },
  "E8F4": {
   "unit": "game/trainerAiE000",
   "line": "divisor again (divideSigned16by8 clobbers X)"
  },
  "E8F6": {
   "unit": "game/trainerAiE000",
   "line": "average row"
  },
  "E8F9": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: it becomes the operand of the LDY at $E914"
  },
  "E8FC": {
   "unit": "game/trainerAiE000",
   "line": "divisor again"
  },
  "E8FE": {
   "unit": "game/trainerAiE000",
   "line": "destination-column sum, low"
  },
  "E900": {
   "unit": "game/trainerAiE000",
   "line": "destination-column sum, high"
  },
  "E902": {
   "unit": "game/trainerAiE000",
   "line": "average ordered destination column"
  },
  "E905": {
   "unit": "game/trainerAiE000",
   "line": "returned in zp_1E"
  },
  "E907": {
   "unit": "game/trainerAiE000",
   "line": "divisor again"
  },
  "E909": {
   "unit": "game/trainerAiE000",
   "line": "destination-row sum, low"
  },
  "E90B": {
   "unit": "game/trainerAiE000",
   "line": "destination-row sum, high"
  },
  "E90D": {
   "unit": "game/trainerAiE000",
   "line": "average ordered destination row"
  },
  "E910": {
   "unit": "game/trainerAiE000",
   "line": "returned in zp_1F, overwriting the sum byte that has just been consumed"
  },
  "E912": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E8ED: X = average column"
  },
  "E914": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E8F9: Y = average row"
  },
  "E916": {
   "unit": "game/trainerAiE000",
   "line": "A = member count, Z set when the group is empty"
  },
  "E918": {
   "unit": "game/trainerAiE000",
   "line": "caller gets count in A, centre in X/Y, average destination in zp_1E/zp_1F"
  },
  "E919": {
   "unit": "game/trainerAiE000",
   "routine": [
    "orderAiUnitToCell - the trainer's central 'issue a move order' helper.  It does nothing when the unit's ordered destination",
    "already is the requested cell, so redundant orders never take up room in the queue or in the packet.  Otherwise it falls into",
    "queueMirroredMoveOrder, which mirrors both coordinates (the engine will mirror them back because it believes the command",
    "arrived from a remote opponent) and queues $80 MOVE UNIT or $81 MOVE GROUP.",
    "In:  X = target column, A = target row, Y = AI ordinal 0-49 (absolute unit = Y+50)",
    "Out: nothing when the order is redundant; otherwise $8896 = $80/$81, $8897 = absolute unit index,",
    "     $8898/$8899 = mirrored column/row, and the command appended to the AI queue at $88A0",
    "Called from: retreatTrainerComcen ($E262), pushTrainerComcenForward ($E294), issueTrainerGroupOrders ($E4F7)",
    "and haltAiComcen ($EBEC)."
   ],
   "line": "stage the target column as the command's first coordinate"
  },
  "E91C": {
   "unit": "game/trainerAiE000",
   "line": "and the target row as the second"
  },
  "E91F": {
   "unit": "game/trainerAiE000",
   "line": "the column AI unit 50+Y has already been ordered to"
  },
  "E922": {
   "unit": "game/trainerAiE000",
   "line": "coordinate bits only; bits 6-7 are order flags"
  },
  "E924": {
   "unit": "game/trainerAiE000",
   "line": "same column as the new order?"
  },
  "E927": {
   "unit": "game/trainerAiE000",
   "line": "no: the order is worth sending"
  },
  "E929": {
   "unit": "game/trainerAiE000",
   "line": "the row it has already been ordered to"
  },
  "E92C": {
   "unit": "game/trainerAiE000",
   "line": "coordinate bits only"
  },
  "E92E": {
   "unit": "game/trainerAiE000",
   "line": "same row too?"
  },
  "E931": {
   "unit": "game/trainerAiE000",
   "line": "no: send the order"
  },
  "E933": {
   "unit": "game/trainerAiE000",
   "line": "the unit is already under exactly this order: queue nothing"
  },
  "E934": {
   "unit": "game/trainerAiE000",
   "line": "target column, ready for the mirror below"
  },
  "E937": {
   "unit": "game/trainerAiE000",
   "routine": [
    "queueMirroredMoveOrder - second half of orderAiUnitToCell.  Mirrors the staged coordinates with mirrorMapCoordinate (39 - v),",
    "picks command $80 cmdMoveUnit or $81 cmdMoveGroup according to bit 6 of the unit's flags, converts the AI ordinal into the",
    "absolute unit index and tail-jumps into queueAiCommand.  Both players see their own side at the bottom of the map, so an",
    "incoming command's coordinates are always mirrored by the engine; the trainer pre-mirrors so the round trip comes out right.",
    "In:  $8898/$8899 = target column/row, Y = AI ordinal 0-49, $F73A unitFlags indexed by ordinal",
    "Out: the four bytes $8896-$8899 staged and queued",
    "Called from: reached only by falling through from orderAiUnitToCell ($E934); the XREF entries $E917/$EA01 belong to the",
    "modem-driver build that occupies the same addresses."
   ],
   "line": "39 - column: the engine mirrors it back when it 'receives' the packet"
  },
  "E93A": {
   "unit": "game/trainerAiE000",
   "line": "store the mirrored column"
  },
  "E93D": {
   "unit": "game/trainerAiE000",
   "line": "target row"
  },
  "E940": {
   "unit": "game/trainerAiE000",
   "line": "39 - row"
  },
  "E943": {
   "unit": "game/trainerAiE000",
   "line": "store the mirrored row"
  },
  "E946": {
   "unit": "game/trainerAiE000",
   "line": "command $80 = cmdMoveUnit"
  },
  "E948": {
   "unit": "game/trainerAiE000",
   "line": "unitFlags of AI unit 50+Y"
  },
  "E94B": {
   "unit": "game/trainerAiE000",
   "line": "bit 6 = the unit is a member of an in-game group"
  },
  "E94D": {
   "unit": "game/trainerAiE000",
   "line": "not grouped: keep $80"
  },
  "E94F": {
   "unit": "game/trainerAiE000",
   "line": "grouped: use $81 = cmdMoveGroup so its formation follows"
  },
  "E950": {
   "unit": "game/trainerAiE000",
   "line": "command byte"
  },
  "E953": {
   "unit": "game/trainerAiE000",
   "line": "AI ordinal 0-49"
  },
  "E954": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "E955": {
   "unit": "game/trainerAiE000",
   "line": "plus 50: the AI plays side 1, so its units are 50-99"
  },
  "E957": {
   "unit": "game/trainerAiE000",
   "line": "first parameter of the command ($8897)"
  },
  "E95A": {
   "unit": "game/trainerAiE000",
   "line": "append the staged command to the AI order queue"
  },
  "E95D": {
   "unit": "game/trainerAiE000",
   "routine": [
    "isPathToPlayerComcenClear - walks a Bresenham line from the current line position (which the caller has set to the group",
    "centre) to the human player's comcen (unit 49) and reports whether any living enemy fighting unit stands on it.  Only human",
    "grunts, riders and boomers (types 0-2) block; the AI's own units, destroyed units, spies and the comcen do not.  Both exits",
    "put the group centre back into the line position through loadGroupCentreIntoLinePos.",
    "In:  zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player comcen cell, map at $F000",
    "Out: C=1 = the lane to the comcen is open, C=0 = blocked or the group id was invalid; zp_92/zp_93 = the group centre;",
    "     zp_48/zp_49 (map pointer) and the line-stepper state $8C-$97 clobbered",
    "Called from: decideTrainerGroupAction ($E3B1), to decide whether the group may make a straight run at the comcen."
   ],
   "line": "the group asking for a straight run"
  },
  "E960": {
   "unit": "game/trainerAiE000",
   "line": "is it one of the live groups?"
  },
  "E963": {
   "unit": "game/trainerAiE000",
   "line": "no: report 'blocked' and do nothing else"
  },
  "E965": {
   "unit": "game/trainerAiE000",
   "line": "column of the human player's comcen (unit 49)"
  },
  "E968": {
   "unit": "game/trainerAiE000",
   "line": "line target column"
  },
  "E96A": {
   "unit": "game/trainerAiE000",
   "line": "its row"
  },
  "E96D": {
   "unit": "game/trainerAiE000",
   "line": "line target row"
  },
  "E96F": {
   "unit": "game/trainerAiE000",
   "line": "set the walker up from the current position to the comcen"
  },
  "E972": {
   "unit": "game/trainerAiE000",
   "block": [
    "Walk the lane one cell at a time, stopping at the first live enemy fighting unit."
   ],
   "line": "advance one cell along the lane"
  },
  "E975": {
   "unit": "game/trainerAiE000",
   "line": "the line ran out without a blocker: restore the centre and return C=1"
  },
  "E977": {
   "unit": "game/trainerAiE000",
   "line": "column the walker is standing on"
  },
  "E979": {
   "unit": "game/trainerAiE000",
   "line": "and its row"
  },
  "E97B": {
   "unit": "game/trainerAiE000",
   "line": "map byte of that cell; bit 7 set means a unit occupies it"
  },
  "E97E": {
   "unit": "game/trainerAiE000",
   "line": "bare terrain: keep walking"
  },
  "E980": {
   "unit": "game/trainerAiE000",
   "line": "strip the occupancy bit to get the unit index"
  },
  "E982": {
   "unit": "game/trainerAiE000",
   "line": "one of the AI's own units 50-99?"
  },
  "E984": {
   "unit": "game/trainerAiE000",
   "line": "yes: a unit never blocks its own side's lane"
  },
  "E986": {
   "unit": "game/trainerAiE000",
   "line": "human player's unit: index it"
  },
  "E987": {
   "unit": "game/trainerAiE000",
   "line": "its type byte"
  },
  "E98A": {
   "unit": "game/trainerAiE000",
   "line": "destroyed: not an obstacle"
  },
  "E98C": {
   "unit": "game/trainerAiE000",
   "line": "unit type 0-4"
  },
  "E98E": {
   "unit": "game/trainerAiE000",
   "line": "type 3 SPY or 4 COMCEN?"
  },
  "E990": {
   "unit": "game/trainerAiE000",
   "line": "yes: neither blocks a charge"
  },
  "E992": {
   "unit": "game/trainerAiE000",
   "line": "a live grunt, rider or boomer sits in the lane: put the group centre back"
  },
  "E995": {
   "unit": "game/trainerAiE000",
   "line": "C=0 = the lane is blocked (also the exit for an invalid group id)"
  },
  "E996": {
   "unit": "game/trainerAiE000",
   "line": "carry tells the caller whether it may charge"
  },
  "E997": {
   "unit": "game/trainerAiE000",
   "routine": [
    "loadGroupCentreIntoLinePos - copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6) into the line",
    "walker's position and returns with C=1.  It doubles as the 'path is clear' exit of isPathToPlayerComcenClear, which is why it",
    "ends with SEC.",
    "In:  $8955 currentAiGroup, $88EA/$88F6 per-group centre tables",
    "Out: zp_93 = centre column, zp_92 = centre row, C=1; A and Y clobbered",
    "Called from: isPathToPlayerComcenClear ($E992) and as its C=1 exit ($E975)."
   ],
   "line": "the current group"
  },
  "E99A": {
   "unit": "game/trainerAiE000",
   "line": "its centre column"
  },
  "E99D": {
   "unit": "game/trainerAiE000",
   "line": "line walker column"
  },
  "E99F": {
   "unit": "game/trainerAiE000",
   "line": "its centre row"
  },
  "E9A2": {
   "unit": "game/trainerAiE000",
   "line": "line walker row"
  },
  "E9A4": {
   "unit": "game/trainerAiE000",
   "line": "C=1: this is also the 'lane is clear' answer of isPathToPlayerComcenClear"
  },
  "E9A5": {
   "unit": "game/trainerAiE000",
   "line": "line position restored"
  },
  "E9A6": {
   "unit": "game/trainerAiE000",
   "routine": [
    "scoreTargetBlock - scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment target.  A missile and a",
    "drone both cover 2x2 cells, so all four are added up: every unit standing in them contributes targetValueByUnitType, positive",
    "for the human player's units and negative for the AI's own.  A block with a negative total is never chosen; otherwise, when the",
    "total beats the running best in zp_1C, the corner is remembered in zp_95/zp_94 and the highest human unit index found in the",
    "block becomes the nominal target in zp_1D (which favours the comcen, unit 49).",
    "In:  X = column, Y = row of the top-left cell; zp_1C = best score so far (may be negative to accept anything),",
    "     zp_1D = best target so far; map at $F000, unitTypeTable $F76C",
    "Out: zp_1A = the block's score, zp_1B = highest human unit index in it; on an improvement zp_1C, zp_1D, zp_94 and zp_95 are",
    "     updated; zp_48/zp_49 = map pointer; self-modifies the operands at $E9E4 and $E9E8",
    "Called from: the missile/drone target scan at $EFA8, and from the $EE00 drone-flight overlay."
   ],
   "line": "self-modify: keep the block's column in the LDA at $E9E3"
  },
  "E9A9": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: keep the block's row in the LDA at $E9E7"
  },
  "E9AC": {
   "unit": "game/trainerAiE000",
   "line": "clear the accumulators"
  },
  "E9AE": {
   "unit": "game/trainerAiE000",
   "line": "score of this 2x2 block"
  },
  "E9B0": {
   "unit": "game/trainerAiE000",
   "line": "highest human unit index seen in it"
  },
  "E9B2": {
   "unit": "game/trainerAiE000",
   "line": "top-left cell; readMapCell also leaves mapCellPtr pointing at it"
  },
  "E9B5": {
   "unit": "game/trainerAiE000",
   "line": "bare terrain"
  },
  "E9B7": {
   "unit": "game/trainerAiE000",
   "line": "a unit stands here: score it"
  },
  "E9BA": {
   "unit": "game/trainerAiE000",
   "block": [
    "The other three cells of the block are reached straight through mapCellPtr: +1 is the cell to the",
    "right, +40 the cell below (the map is 40 cells wide) and +41 the diagonal."
   ],
   "line": "offset +1 = the cell to the right"
  },
  "E9BC": {
   "unit": "game/trainerAiE000",
   "line": "read it"
  },
  "E9BE": {
   "unit": "game/trainerAiE000",
   "line": "bare terrain"
  },
  "E9C0": {
   "unit": "game/trainerAiE000",
   "line": "score the unit standing there"
  },
  "E9C3": {
   "unit": "game/trainerAiE000",
   "line": "offset +40 = one map row down"
  },
  "E9C5": {
   "unit": "game/trainerAiE000",
   "line": "read it"
  },
  "E9C7": {
   "unit": "game/trainerAiE000",
   "line": "bare terrain"
  },
  "E9C9": {
   "unit": "game/trainerAiE000",
   "line": "score it"
  },
  "E9CC": {
   "unit": "game/trainerAiE000",
   "line": "offset +41 = down and to the right"
  },
  "E9CE": {
   "unit": "game/trainerAiE000",
   "line": "read it"
  },
  "E9D0": {
   "unit": "game/trainerAiE000",
   "line": "bare terrain"
  },
  "E9D2": {
   "unit": "game/trainerAiE000",
   "line": "score it"
  },
  "E9D5": {
   "unit": "game/trainerAiE000",
   "line": "total value of the block"
  },
  "E9D7": {
   "unit": "game/trainerAiE000",
   "line": "negative: the AI's own units dominate it, so never bombard it"
  },
  "E9D9": {
   "unit": "game/trainerAiE000",
   "line": "better than the best block found so far?"
  },
  "E9DB": {
   "unit": "game/trainerAiE000",
   "line": "yes: take it"
  },
  "E9DD": {
   "unit": "game/trainerAiE000",
   "line": "no, but is the running best itself still negative (nothing chosen yet)?"
  },
  "E9DF": {
   "unit": "game/trainerAiE000",
   "line": "no: keep the previous best"
  },
  "E9E1": {
   "unit": "game/trainerAiE000",
   "line": "new best score"
  },
  "E9E3": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E9A6: the block's column"
  },
  "E9E5": {
   "unit": "game/trainerAiE000",
   "line": "remember it as the aim point"
  },
  "E9E7": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $E9A9: the block's row"
  },
  "E9E9": {
   "unit": "game/trainerAiE000",
   "line": "aim point row"
  },
  "E9EB": {
   "unit": "game/trainerAiE000",
   "line": "the biggest human unit index in the block"
  },
  "E9ED": {
   "unit": "game/trainerAiE000",
   "line": "becomes the nominal target unit (49 = the comcen wins any tie)"
  },
  "E9EF": {
   "unit": "game/trainerAiE000",
   "line": "a new best target has been recorded"
  },
  "E9F0": {
   "unit": "game/trainerAiE000",
   "line": "rejected: the running best is left alone"
  },
  "E9F1": {
   "unit": "game/trainerAiE000",
   "block": [
    "targetValueByUnitType - five bytes indexed by unit type, the bombardment value of a unit standing in a",
    "candidate target block.  Boomers and the comcen are worth four times a grunt."
   ],
   "line": "GRUNT 1, RIDER 2, BOOMER 4, SPY 1, COMCEN 4"
  },
  "E9F6": {
   "unit": "game/trainerAiE000",
   "routine": [
    "addUnitTargetValue - helper of scoreTargetBlock.  Takes a map cell byte that holds $80 plus a unit index, ignores destroyed",
    "units, and adds targetValueByUnitType[type] to the block score in zp_1A - negated when the unit belongs to the AI itself, so",
    "the trainer will not shell its own troops.  For a human unit it also keeps the largest index seen, which is how the human",
    "comcen (unit 49) ends up being preferred as the nominal target.",
    "In:  A = map cell byte with bit 7 set, unitTypeTable $F76C, targetValueByUnitType $E9F1, zp_1A/zp_1B accumulators",
    "Out: zp_1A += the (possibly negated) value, zp_1B = highest human unit index seen; X and Y clobbered",
    "Called from: scoreTargetBlock, once per cell of the 2x2 block ($E9B7, $E9C0, $E9C9, $E9D2)."
   ],
   "line": "drop the 'a unit is here' bit: what is left is the unit index"
  },
  "E9F8": {
   "unit": "game/trainerAiE000",
   "line": "index the unit tables with it"
  },
  "E9F9": {
   "unit": "game/trainerAiE000",
   "line": "its type byte"
  },
  "E9FC": {
   "unit": "game/trainerAiE000",
   "line": "destroyed: worth nothing"
  },
  "E9FE": {
   "unit": "game/trainerAiE000",
   "line": "unit type 0-4"
  },
  "EA00": {
   "unit": "game/trainerAiE000",
   "line": "index the value table"
  },
  "EA01": {
   "unit": "game/trainerAiE000",
   "line": "bombardment value of that type"
  },
  "EA04": {
   "unit": "game/trainerAiE000",
   "line": "unit 50-99 = one of the AI's own?"
  },
  "EA06": {
   "unit": "game/trainerAiE000",
   "line": "yes: its value counts against the block"
  },
  "EA08": {
   "unit": "game/trainerAiE000",
   "line": "human unit: is it the highest index seen in this block so far?"
  },
  "EA0A": {
   "unit": "game/trainerAiE000",
   "line": "no: just add its value"
  },
  "EA0C": {
   "unit": "game/trainerAiE000",
   "line": "yes: it becomes the block's nominal target"
  },
  "EA0E": {
   "unit": "game/trainerAiE000",
   "line": "always taken (the BCS above fell through with C=1)"
  },
  "EA10": {
   "unit": "game/trainerAiE000",
   "line": "negate the value ..."
  },
  "EA12": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry ..."
  },
  "EA13": {
   "unit": "game/trainerAiE000",
   "line": "... two's complement, so friendly units subtract"
  },
  "EA15": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry before accumulating"
  },
  "EA16": {
   "unit": "game/trainerAiE000",
   "line": "add into the block score"
  },
  "EA18": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "EA1A": {
   "unit": "game/trainerAiE000",
   "line": "cell scored"
  },
  "EA1B": {
   "unit": "game/trainerAiE000",
   "routine": [
    "offsetCoordinateClamped - returns clamp(A + X*Y, 0..39): the base coordinate A displaced by Y steps of the signed step X, held",
    "inside the 40-cell battlefield, with the carry reporting whether the unclamped value had left the map.  The base coordinate and",
    "the caller's step count are both parked in immediate operands so nothing but A is disturbed.",
    "In:  A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps",
    "Out: A = clamped coordinate 0-39, C=1 when the raw value was negative or past 39, Y unchanged;",
    "     self-modifies the operands at $EA27 and $EA36",
    "Called from: pushTrainerComcenForward/retreatTrainerComcen ($E24C/$E25B/$E27E/$E28D), the path search ($E48A/$E499)",
    "and offsetPointInDirection ($E675/$E684)."
   ],
   "line": "self-modify: save the step count in the LDY at $EA35 so Y survives the multiply"
  },
  "EA1E": {
   "unit": "game/trainerAiE000",
   "line": "self-modify: the base coordinate becomes the operand of the ADC at $EA26"
  },
  "EA21": {
   "unit": "game/trainerAiE000",
   "line": "A = the signed step, 0, +1 or $FF"
  },
  "EA22": {
   "unit": "game/trainerAiE000",
   "line": "A = step * number of steps"
  },
  "EA25": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EA26": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $EA1E: add the base coordinate"
  },
  "EA28": {
   "unit": "game/trainerAiE000",
   "line": "keep the raw, unclamped result for the range test"
  },
  "EA29": {
   "unit": "game/trainerAiE000",
   "line": "positive: no low clamp needed"
  },
  "EA2B": {
   "unit": "game/trainerAiE000",
   "line": "walked off the top or left edge: clamp to 0"
  },
  "EA2D": {
   "unit": "game/trainerAiE000",
   "line": "past the last cell (40)?"
  },
  "EA2F": {
   "unit": "game/trainerAiE000",
   "line": "no, keep it"
  },
  "EA31": {
   "unit": "game/trainerAiE000",
   "line": "yes: clamp to 39"
  },
  "EA33": {
   "unit": "game/trainerAiE000",
   "line": "C=1 when the raw value was 40 or more, which also covers a negative value (>= $80)"
  },
  "EA35": {
   "unit": "game/trainerAiE000",
   "line": "operand patched at $EA1B: hand the caller its step count back"
  },
  "EA37": {
   "unit": "game/trainerAiE000",
   "line": "A = clamped coordinate, C=1 if it had to be clamped"
  },
  "EA38": {
   "unit": "game/trainerAiE000",
   "routine": [
    "mirrorBlockCornerForOpponent - converts the top-left corner of a 2x2 object (the drone) into the coordinates that the engine's",
    "mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn back into that same corner: X = 39-(X+1), Y = 39-(Y+1).",
    "Mirroring alone would give the bottom-right corner of the mirrored block, hence the pre-increment.",
    "In:  X = column, Y = row of the block's top-left cell",
    "Out: X, Y = the mirrored corner; A clobbered",
    "Called from: the fake drone-position packet at $EEB1 and $EF8F, and from the drone-flight overlay."
   ],
   "line": "the engine subtracts 1 after mirroring, so add it here first"
  },
  "EA39": {
   "unit": "game/trainerAiE000",
   "line": "column+1 into A"
  },
  "EA3A": {
   "unit": "game/trainerAiE000",
   "line": "39 - (column+1)"
  },
  "EA3D": {
   "unit": "game/trainerAiE000",
   "line": "mirrored column back into X"
  },
  "EA3E": {
   "unit": "game/trainerAiE000",
   "line": "same for the row"
  },
  "EA3F": {
   "unit": "game/trainerAiE000",
   "line": "row+1 into A"
  },
  "EA40": {
   "unit": "game/trainerAiE000",
   "line": "39 - (row+1)"
  },
  "EA43": {
   "unit": "game/trainerAiE000",
   "line": "mirrored row back into Y"
  },
  "EA44": {
   "unit": "game/trainerAiE000",
   "line": "corner is now expressed the way a remote opponent would have sent it"
  },
  "EA45": {
   "unit": "game/trainerAiE000",
   "block": [
    "aiCmdQueuePeak - a single byte of data sitting in the code stream between mirrorBlockCornerForOpponent",
    "and queueAiCommand: the high-water mark of the AI's order queue.  It is written and compared only",
    "inside queueAiCommand and read nowhere else, so it is a development leftover."
   ],
   "line": "longest the AI order queue has ever grown this game; nothing ever reads it back"
  },
  "EA46": {
   "unit": "game/trainerAiE000",
   "routine": [
    "queueAiCommand - appends the command staged in $8896..$8899 to the AI's 50-byte order queue at $88A0.  It looks the total",
    "command length up with getCommandLength, keeps a high-water mark in aiCmdQueuePeak, and when the queue would reach 50 bytes it",
    "drops the command and turns the border blue instead, as a visible warning that the AI is producing orders faster than the",
    "lock-step packets can carry them away.",
    "In:  $8896-$8899 = the staged command, $8952 aiCmdQueueLength, commandLengthTable through getCommandLength ($4D9F)",
    "Out: queue at $88A0 extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the copy counter),",
    "     $EA45 peak updated; on overflow $9003 (the border-colour shadow) = 6 and the command is thrown away",
    "Called from: queueMirroredMoveOrder ($E95A) and queueComcenCommand ($EBE0); the trainer setup overlay has its own",
    "call at its own $EC82."
   ],
   "line": "count one more command issued this round"
  },
  "EA49": {
   "unit": "game/trainerAiE000",
   "line": "the staged command byte"
  },
  "EA4C": {
   "unit": "game/trainerAiE000",
   "line": "currentCommandLength ($92EE) = 1 command byte plus its parameters"
  },
  "EA4F": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EA50": {
   "unit": "game/trainerAiE000",
   "line": "how long the queue would be afterwards"
  },
  "EA53": {
   "unit": "game/trainerAiE000",
   "line": "beyond the previous high-water mark?"
  },
  "EA56": {
   "unit": "game/trainerAiE000",
   "line": "no"
  },
  "EA58": {
   "unit": "game/trainerAiE000",
   "line": "record the new peak"
  },
  "EA5B": {
   "unit": "game/trainerAiE000",
   "line": "the queue at $88A0 holds 50 bytes"
  },
  "EA5D": {
   "unit": "game/trainerAiE000",
   "line": "it still fits"
  },
  "EA5F": {
   "unit": "game/trainerAiE000",
   "line": "colour 6 = blue"
  },
  "EA61": {
   "unit": "game/trainerAiE000",
   "line": "overflow: recolour the border through the VIC shadow and drop the command"
  },
  "EA64": {
   "unit": "game/trainerAiE000",
   "line": "command lost (the round counter at $8968 was already bumped)"
  },
  "EA65": {
   "unit": "game/trainerAiE000",
   "line": "copy from the first staging byte"
  },
  "EA67": {
   "unit": "game/trainerAiE000",
   "line": "append at the current end of the queue"
  },
  "EA6A": {
   "unit": "game/trainerAiE000",
   "line": "next staged byte"
  },
  "EA6D": {
   "unit": "game/trainerAiE000",
   "line": "into the queue"
  },
  "EA70": {
   "unit": "game/trainerAiE000",
   "line": "advance the staging index"
  },
  "EA71": {
   "unit": "game/trainerAiE000",
   "line": "advance the queue index"
  },
  "EA72": {
   "unit": "game/trainerAiE000",
   "line": "the queue is one byte longer"
  },
  "EA75": {
   "unit": "game/trainerAiE000",
   "line": "one byte of this command done"
  },
  "EA78": {
   "unit": "game/trainerAiE000",
   "line": "until the whole command has been copied"
  },
  "EA7A": {
   "unit": "game/trainerAiE000",
   "line": "the order is now waiting to be handed to the engine"
  },
  "EA7B": {
   "unit": "game/trainerAiE000",
   "routine": [
    "injectAiCommandsIntoPacket - the heart of the solo trainer.  Where the modem build would decode a packet that arrived over the",
    "wire, this build fabricates one: it copies whole commands out of the AI's own queue at $88A0 straight into the comm module's",
    "received-packet buffer ($E020, length $E01E), so the lock-step engine executes the computer opponent's orders through exactly",
    "the same command interpreter a human opponent's orders would use.  It first runs the missile/drone decision, which may claim",
    "the whole packet for itself, and then moves as many complete commands as fit while the packet stays under 8 bytes, shifting",
    "the remainder of the queue down after each one.",
    "In:  $8952 aiCmdQueueLength, the queue at $88A0, $E01E current packet length, getCommandLength ($4D9F)",
    "Out: $E020.. filled, $E01E updated, the queue compacted and $8952 reduced; $8966 cleared then possibly set by runAiMissileTurn;",
    "     $92EE clobbered",
    "Called from: buildTrainerReplyPacket ($E0E4), once per lock-step exchange."
   ],
   "line": "clear the flag first"
  },
  "EA7D": {
   "unit": "game/trainerAiE000",
   "line": "$8966 bit 7 will mean 'the missile/drone code owns this packet'"
  },
  "EA80": {
   "unit": "game/trainerAiE000",
   "line": "offer the AI a missile shot or a drone launch; it writes straight into the packet"
  },
  "EA83": {
   "unit": "game/trainerAiE000",
   "line": "did it claim the packet?"
  },
  "EA86": {
   "unit": "game/trainerAiE000",
   "line": "yes: leave the order queue for the next exchange"
  },
  "EA88": {
   "unit": "game/trainerAiE000",
   "line": "is anything queued at all?"
  },
  "EA8B": {
   "unit": "game/trainerAiE000",
   "line": "no: send an empty packet"
  },
  "EA8D": {
   "unit": "game/trainerAiE000",
   "block": [
    "One iteration per queued command: measure it, check that it still fits in the packet, copy it into the",
    "packet, then slide the rest of the queue down over it."
   ],
   "line": "read from the front of the queue"
  },
  "EA8F": {
   "unit": "game/trainerAiE000",
   "line": "its command byte"
  },
  "EA92": {
   "unit": "game/trainerAiE000",
   "line": "currentCommandLength = its total length"
  },
  "EA95": {
   "unit": "game/trainerAiE000",
   "line": "bytes already in the fabricated 'received' packet"
  },
  "EA98": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EA99": {
   "unit": "game/trainerAiE000",
   "line": "plus this command"
  },
  "EA9C": {
   "unit": "game/trainerAiE000",
   "line": "a packet carries at most 7 command bytes"
  },
  "EA9E": {
   "unit": "game/trainerAiE000",
   "line": "it would not fit: the rest waits for the next exchange"
  },
  "EAA0": {
   "unit": "game/trainerAiE000",
   "line": "append at the current end of the packet buffer"
  },
  "EAA3": {
   "unit": "game/trainerAiE000",
   "line": "next byte of the command"
  },
  "EAA6": {
   "unit": "game/trainerAiE000",
   "line": "into the received-packet buffer at $E020"
  },
  "EAA9": {
   "unit": "game/trainerAiE000",
   "line": "advance the packet index"
  },
  "EAAA": {
   "unit": "game/trainerAiE000",
   "line": "advance the queue index"
  },
  "EAAB": {
   "unit": "game/trainerAiE000",
   "line": "whole command copied?"
  },
  "EAAE": {
   "unit": "game/trainerAiE000",
   "line": "no: keep copying"
  },
  "EAB0": {
   "unit": "game/trainerAiE000",
   "line": "new packet length"
  },
  "EAB3": {
   "unit": "game/trainerAiE000",
   "line": "now compact the queue from its start"
  },
  "EAB5": {
   "unit": "game/trainerAiE000",
   "line": "byte just past the command that was taken"
  },
  "EAB8": {
   "unit": "game/trainerAiE000",
   "line": "slide it down to the front"
  },
  "EABB": {
   "unit": "game/trainerAiE000",
   "line": "advance the write index"
  },
  "EABC": {
   "unit": "game/trainerAiE000",
   "line": "advance the read index"
  },
  "EABD": {
   "unit": "game/trainerAiE000",
   "line": "until the old end of the queue"
  },
  "EAC0": {
   "unit": "game/trainerAiE000",
   "line": "keep shifting"
  },
  "EAC2": {
   "unit": "game/trainerAiE000",
   "line": "old queue length"
  },
  "EAC5": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EAC6": {
   "unit": "game/trainerAiE000",
   "line": "minus the command just handed over"
  },
  "EAC9": {
   "unit": "game/trainerAiE000",
   "line": "new queue length"
  },
  "EACC": {
   "unit": "game/trainerAiE000",
   "line": "more orders waiting: try to fit another into the same packet"
  },
  "EACE": {
   "unit": "game/trainerAiE000",
   "line": "the packet the engine is about to 'receive' is ready"
  },
  "EACF": {
   "unit": "game/trainerAiE000",
   "routine": [
    "updateAiUnitTracking - per-unit hook, run from trainerUnitStepHook after the engine has stepped each unit.  It maintains three",
    "pieces of AI awareness.  For a human unit it latches the first grunt/rider/boomer that has picked the AI comcen as its fire",
    "target into comcenAttackerUnit ($8972).  For one of its own units it marks that unit's group as having the human comcen in",
    "reach (groupFlags bit 6) and stamps the unit for an immediate replan, once the comcen has been spotted and the unit is within",
    "8 cells of it.  Finally, an AI spy (units 96-98) that has reached the opponent's end zone while still inside an ordinary group",
    "is split off into a brand new group of its own, seeded with its current destination, so it can operate alone.",
    "In:  $90FA currentUnit, $F76C unitTypeTable, $91F0 fireTargetUnit, $F9F5 human comcen energy byte (bit 7 = spotted),",
    "     $F671/$F6D5 human comcen cell, $F640/$F6A4 unit cells, aiUnitOrderRound ($87CE+unit), aiUnitGroup ($8800+unit),",
    "     aiUnitDeepFlag ($8832+unit), $91CB game clock, $8954 aiGroupCount, $8964 home group id",
    "Out: $8972 comcenAttackerUnit, groupFlags $893E bit 6, $8953 last allocated group id, the unit's group and round stamp,",
    "     and the new group's target cell $88D2/$88DE",
    "Called from: trainerUnitStepHook ($E124), for every unit the engine steps."
   ],
   "line": "the unit the engine has just stepped"
  },
  "EAD2": {
   "unit": "game/trainerAiE000",
   "line": "unit 0-49 or 50-99?"
  },
  "EAD4": {
   "unit": "game/trainerAiE000",
   "line": "one of the AI's own: run the contact tracking"
  },
  "EAD6": {
   "unit": "game/trainerAiE000",
   "line": "comcenAttackerUnit, $FF while nobody is known to be shooting at the AI comcen"
  },
  "EAD9": {
   "unit": "game/trainerAiE000",
   "line": "already latched: nothing to learn"
  },
  "EADB": {
   "unit": "game/trainerAiE000",
   "line": "the unit this human unit has picked as its fire target"
  },
  "EADE": {
   "unit": "game/trainerAiE000",
   "line": "unit 99 = the AI comcen"
  },
  "EAE0": {
   "unit": "game/trainerAiE000",
   "line": "aiming at something else"
  },
  "EAE2": {
   "unit": "game/trainerAiE000",
   "line": "its type byte"
  },
  "EAE5": {
   "unit": "game/trainerAiE000",
   "line": "unit type 0-4"
  },
  "EAE7": {
   "unit": "game/trainerAiE000",
   "line": "type 3 SPY or 4 COMCEN?"
  },
  "EAE9": {
   "unit": "game/trainerAiE000",
   "line": "yes: they cannot shoot, so they are not a threat to record"
  },
  "EAEB": {
   "unit": "game/trainerAiE000",
   "line": "remember the first unit that is aiming at the AI comcen"
  },
  "EAEE": {
   "unit": "game/trainerAiE000",
   "line": "human unit handled"
  },
  "EAF1": {
   "unit": "game/trainerAiE000",
   "block": [
    "AI unit: has it got close enough to the human comcen to count as contact?  The two coordinate tests",
    "accept a difference in -8..+7 by comparing the signed byte against $F8 and then against 8."
   ],
   "line": "energy byte of the human comcen; bit 7 doubles as 'the AI has spotted it'"
  },
  "EAF4": {
   "unit": "game/trainerAiE000",
   "line": "not spotted yet: there is no contact to record"
  },
  "EAF6": {
   "unit": "game/trainerAiE000",
   "line": "column of the human comcen"
  },
  "EAF9": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EAFA": {
   "unit": "game/trainerAiE000",
   "line": "minus this AI unit's column"
  },
  "EAFD": {
   "unit": "game/trainerAiE000",
   "line": "is the difference at least -8?"
  },
  "EAFF": {
   "unit": "game/trainerAiE000",
   "line": "yes, within 8 columns to one side"
  },
  "EB01": {
   "unit": "game/trainerAiE000",
   "line": "is it less than +8?"
  },
  "EB03": {
   "unit": "game/trainerAiE000",
   "line": "no: more than 8 columns away"
  },
  "EB05": {
   "unit": "game/trainerAiE000",
   "line": "row of the human comcen"
  },
  "EB08": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EB09": {
   "unit": "game/trainerAiE000",
   "line": "minus this AI unit's row"
  },
  "EB0C": {
   "unit": "game/trainerAiE000",
   "line": "is the difference at least -8?"
  },
  "EB0E": {
   "unit": "game/trainerAiE000",
   "line": "yes"
  },
  "EB10": {
   "unit": "game/trainerAiE000",
   "line": "is it less than +8?"
  },
  "EB12": {
   "unit": "game/trainerAiE000",
   "line": "no: more than 8 rows away"
  },
  "EB14": {
   "unit": "game/trainerAiE000",
   "line": "aiUnitGroup[unit], the group table addressed by absolute index"
  },
  "EB17": {
   "unit": "game/trainerAiE000",
   "line": "is it the home/guard group?"
  },
  "EB1A": {
   "unit": "game/trainerAiE000",
   "line": "yes: that group is not supposed to chase the comcen"
  },
  "EB1C": {
   "unit": "game/trainerAiE000",
   "line": "its groupFlags"
  },
  "EB1F": {
   "unit": "game/trainerAiE000",
   "line": "bit 6 = this group has the human comcen within reach"
  },
  "EB21": {
   "unit": "game/trainerAiE000",
   "line": "store the flags back"
  },
  "EB24": {
   "unit": "game/trainerAiE000",
   "line": "bit 7 set (the group is already suspended): do not restamp its clock"
  },
  "EB26": {
   "unit": "game/trainerAiE000",
   "line": "the down-counting round clock"
  },
  "EB29": {
   "unit": "game/trainerAiE000",
   "line": "aiUnitOrderRound[unit]: stamping the current clock makes the planner replan this unit at once"
  },
  "EB2C": {
   "unit": "game/trainerAiE000",
   "block": [
    "An AI spy (units 96-98) that has scored both end zones is promoted out of its group into a group of its",
    "own so it can act independently behind the human lines."
   ],
   "line": "the AI comcen itself (unit 99)?"
  },
  "EB2E": {
   "unit": "game/trainerAiE000",
   "line": "yes: nothing else to do for it"
  },
  "EB30": {
   "unit": "game/trainerAiE000",
   "line": "units 96-98 are the AI's three SPYs"
  },
  "EB32": {
   "unit": "game/trainerAiE000",
   "line": "any other unit: done"
  },
  "EB34": {
   "unit": "game/trainerAiE000",
   "line": "aiUnitDeepFlag[unit], bit 7 = the unit has scored both territory zones"
  },
  "EB37": {
   "unit": "game/trainerAiE000",
   "line": "this spy has not got that far yet"
  },
  "EB39": {
   "unit": "game/trainerAiE000",
   "line": "its current group id"
  },
  "EB3C": {
   "unit": "game/trainerAiE000",
   "line": "does it still belong to one of the ordinary groups?"
  },
  "EB3F": {
   "unit": "game/trainerAiE000",
   "line": "no (already split off, or $FF): leave it alone"
  },
  "EB41": {
   "unit": "game/trainerAiE000",
   "line": "is it in the home/guard group?"
  },
  "EB44": {
   "unit": "game/trainerAiE000",
   "line": "yes: that one stays together"
  },
  "EB46": {
   "unit": "game/trainerAiE000",
   "line": "allocate the next group id; note that aiGroupCount $8954 is not bumped with it, and that ids past 11 would run off the end of the twelve-entry per-group tables"
  },
  "EB49": {
   "unit": "game/trainerAiE000",
   "line": "the new id"
  },
  "EB4C": {
   "unit": "game/trainerAiE000",
   "line": "index the per-group tables with it"
  },
  "EB4D": {
   "unit": "game/trainerAiE000",
   "line": "the spy becomes a group of its own"
  },
  "EB50": {
   "unit": "game/trainerAiE000",
   "line": "the current round"
  },
  "EB53": {
   "unit": "game/trainerAiE000",
   "line": "stamp it so the planner picks the new group up on the next scan"
  },
  "EB56": {
   "unit": "game/trainerAiE000",
   "line": "the destination the spy is already heading for"
  },
  "EB59": {
   "unit": "game/trainerAiE000",
   "line": "coordinate bits only"
  },
  "EB5B": {
   "unit": "game/trainerAiE000",
   "line": "becomes the new group's target column"
  },
  "EB5E": {
   "unit": "game/trainerAiE000",
   "line": "and its destination row"
  },
  "EB61": {
   "unit": "game/trainerAiE000",
   "line": "coordinate bits only"
  },
  "EB63": {
   "unit": "game/trainerAiE000",
   "line": "becomes the new group's target row"
  },
  "EB66": {
   "unit": "game/trainerAiE000",
   "line": "unit tracked"
  },
  "EB67": {
   "unit": "game/trainerAiE000",
   "routine": [
    "runAiComcenActions - spends whatever is left of the AI comcen's three command slots for this round.  A stunned comcen needs two",
    "of them: one to halt itself and one for the 1-in-8 repair attempt that queues command $9E, exactly the odds the human REPAIR",
    "tab uses.  An unstunned comcen is halted once and then spends every remaining slot on maybeQueueComcenEnergyRepair.",
    "In:  $8968 commands already issued this round, $8971 the comcen's stun counter (low nibble of unitStunTable[99])",
    "Out: $8967 = the comcen's remaining slot budget, commands queued through queueAiCommand; returns silently when there is",
    "     no budget left",
    "Called from: planTrainerRound at $E1CD (comcen too weak to attack) and $E21B (defend mode)."
   ],
   "line": "the comcen may spend at most 3 command slots in a round"
  },
  "EB69": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EB6A": {
   "unit": "game/trainerAiE000",
   "line": "minus the orders the planner has already queued"
  },
  "EB6D": {
   "unit": "game/trainerAiE000",
   "line": "already over budget: do nothing at all"
  },
  "EB6F": {
   "unit": "game/trainerAiE000",
   "line": "slots left for the comcen"
  },
  "EB72": {
   "unit": "game/trainerAiE000",
   "line": "keep the budget in X as well"
  },
  "EB73": {
   "unit": "game/trainerAiE000",
   "line": "the comcen's stun counter"
  },
  "EB76": {
   "unit": "game/trainerAiE000",
   "line": "not stunned: go straight to the energy repairs"
  },
  "EB78": {
   "unit": "game/trainerAiE000",
   "line": "a stun repair attempt costs two slots"
  },
  "EB7A": {
   "unit": "game/trainerAiE000",
   "line": "not enough left"
  },
  "EB7C": {
   "unit": "game/trainerAiE000",
   "line": "spend one ..."
  },
  "EB7D": {
   "unit": "game/trainerAiE000",
   "line": "... and the second"
  },
  "EB7E": {
   "unit": "game/trainerAiE000",
   "line": "the reduced budget"
  },
  "EB81": {
   "unit": "game/trainerAiE000",
   "line": "halt the comcen; a moving comcen cannot be worked on"
  },
  "EB84": {
   "unit": "game/trainerAiE000",
   "line": "step the lock-step RNG"
  },
  "EB87": {
   "unit": "game/trainerAiE000",
   "line": "one chance in eight"
  },
  "EB89": {
   "unit": "game/trainerAiE000",
   "line": "the repair attempt failed this round"
  },
  "EB8B": {
   "unit": "game/trainerAiE000",
   "line": "command $9E = cmdComcenRepairedOrSetupReady, which clears the comcen stun counter on both machines"
  },
  "EB8D": {
   "unit": "game/trainerAiE000",
   "line": "queue it ($8897/$8898 are ignored, the command is one byte long)"
  },
  "EB90": {
   "unit": "game/trainerAiE000",
   "line": "cancel the DEC that the shared loop tail below is about to do"
  },
  "EB93": {
   "unit": "game/trainerAiE000",
   "line": "join the energy-repair loop with the remaining budget"
  },
  "EB96": {
   "unit": "game/trainerAiE000",
   "line": "slots left"
  },
  "EB97": {
   "unit": "game/trainerAiE000",
   "line": "none: nothing to do"
  },
  "EB99": {
   "unit": "game/trainerAiE000",
   "line": "halt the comcen so it can repair"
  },
  "EB9C": {
   "unit": "game/trainerAiE000",
   "line": "spend one slot on an energy repair"
  },
  "EB9F": {
   "unit": "game/trainerAiE000",
   "line": "one slot used"
  },
  "EBA2": {
   "unit": "game/trainerAiE000",
   "line": "keep spending until the budget is exhausted"
  },
  "EBA4": {
   "unit": "game/trainerAiE000",
   "line": "the comcen has done what it can this round"
  },
  "EBA5": {
   "unit": "game/trainerAiE000",
   "routine": [
    "maybeQueueComcenEnergyRepair - the trainer's version of the human console's energy repair.  With 50 percent probability, and",
    "only while the AI comcen's energy is below 45 of 50, it queues command $9A cmdAddEnergy for unit 99 with the same amount the",
    "human repair uses, (50 - energy) / 8 + 1.  The projected energy is accumulated in $8965 (which planTrainerRound seeded from the",
    "comcen's real energy and uses as its action budget), and the amount is nudged by one whenever that projection would land on a",
    "multiple of four.",
    "In:  $FA27 = unitEnergyTable[99] (energy in bits 0-5), $8965 projected comcen energy, nextGameRandom",
    "Out: $8896 = $9A, $8897 = $63 (unit 99), $8898 = the amount, command queued; $8965 raised by the same amount",
    "Called from: runAiComcenActions ($EB9C), once per remaining comcen command slot."
   ],
   "line": "step the lock-step RNG"
  },
  "EBA8": {
   "unit": "game/trainerAiE000",
   "line": "an energy repair succeeds one time in two, as on the human console"
  },
  "EBAA": {
   "unit": "game/trainerAiE000",
   "line": "failed this time: spend the slot on nothing"
  },
  "EBAC": {
   "unit": "game/trainerAiE000",
   "line": "energy byte of AI comcen unit 99"
  },
  "EBAF": {
   "unit": "game/trainerAiE000",
   "line": "energy 0-50 lives in bits 0-5; bits 6-7 are visibility flags"
  },
  "EBB1": {
   "unit": "game/trainerAiE000",
   "line": "45 out of 50 or better?"
  },
  "EBB3": {
   "unit": "game/trainerAiE000",
   "line": "healthy enough: do not waste a command on it"
  },
  "EBB5": {
   "unit": "game/trainerAiE000",
   "line": "keep the current energy"
  },
  "EBB7": {
   "unit": "game/trainerAiE000",
   "line": "50 = a full comcen"
  },
  "EBB9": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EBBA": {
   "unit": "game/trainerAiE000",
   "line": "the shortfall"
  },
  "EBBC": {
   "unit": "game/trainerAiE000",
   "line": "divide by 8 ..."
  },
  "EBBD": {
   "unit": "game/trainerAiE000",
   "line": "..."
  },
  "EBBE": {
   "unit": "game/trainerAiE000",
   "line": "... so repairs get smaller as the comcen recovers"
  },
  "EBBF": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EBC0": {
   "unit": "game/trainerAiE000",
   "line": "amount = shortfall/8 + 1"
  },
  "EBC2": {
   "unit": "game/trainerAiE000",
   "line": "second parameter of the command"
  },
  "EBC5": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EBC6": {
   "unit": "game/trainerAiE000",
   "line": "add it to the energy the comcen is projected to end up with"
  },
  "EBC9": {
   "unit": "game/trainerAiE000",
   "line": "keep the projection"
  },
  "EBCC": {
   "unit": "game/trainerAiE000",
   "line": "did the projection land on a multiple of four?"
  },
  "EBCE": {
   "unit": "game/trainerAiE000",
   "line": "no: the amount stands"
  },
  "EBD0": {
   "unit": "game/trainerAiE000",
   "line": "yes: repair one point more so it does not"
  },
  "EBD3": {
   "unit": "game/trainerAiE000",
   "line": "and keep the projection in step with it"
  },
  "EBD6": {
   "unit": "game/trainerAiE000",
   "line": "unit 99 = the AI comcen"
  },
  "EBD8": {
   "unit": "game/trainerAiE000",
   "line": "first parameter of the command ($8897)"
  },
  "EBDB": {
   "unit": "game/trainerAiE000",
   "line": "command $9A = cmdAddEnergy (unit, amount); falls straight into queueComcenCommand"
  },
  "EBDD": {
   "unit": "game/trainerAiE000",
   "routine": [
    "queueComcenCommand - shared tail of the comcen actions: stores A as the command byte and jumps into queueAiCommand, leaving",
    "$8897 and $8898 as the caller staged them.",
    "In:  A = command byte, $8897/$8898 = parameters already staged",
    "Out: the command appended to the AI order queue",
    "Called from: runAiComcenActions ($EB8D) and by falling through from maybeQueueComcenEnergyRepair ($EBDB)."
   ],
   "line": "command byte into the staging area"
  },
  "EBE0": {
   "unit": "game/trainerAiE000",
   "line": "append the staged bytes to the queue"
  },
  "EBE3": {
   "unit": "game/trainerAiE000",
   "line": "shared 'do nothing this slot' exit of the comcen action code"
  },
  "EBE4": {
   "unit": "game/trainerAiE000",
   "routine": [
    "haltAiComcen - orders the AI comcen to move to the cell it is already standing on.  orderAiUnitToCell turns that into a real",
    "$80/$81 command only when the comcen currently has some other destination, so this stops a moving comcen and costs nothing when",
    "it is already still.  A comcen has to be stationary before it can repair or launch.",
    "In:  $F6A3 unitCol[99], $F707 unitRow[99]",
    "Out: possibly one $80/$81 command queued; nothing at all when the comcen is already halted",
    "Called from: runAiComcenActions ($EB81 stunned path, $EB99 repair path)."
   ],
   "line": "the column the AI comcen is standing on"
  },
  "EBE7": {
   "unit": "game/trainerAiE000",
   "line": "and its row"
  },
  "EBEA": {
   "unit": "game/trainerAiE000",
   "line": "AI ordinal 49 = absolute unit 99, the AI comcen"
  },
  "EBEC": {
   "unit": "game/trainerAiE000",
   "line": "ordering it to its own cell halts it; a redundant order is dropped"
  },
  "EBEF": {
   "unit": "game/trainerAiE000",
   "routine": [
    "assessThreatAroundAiComcen - builds a directional pressure map around the AI comcen and picks the direction to run.  Every",
    "living human combat unit (ordinals 0-45: the 28 grunts, 12 riders and 6 boomers) within 20 cells adds a weight into one of",
    "eight compass buckets at $8896-$889D: 1 for merely being nearby, +1 within 10 cells, and +4 more when the comcen is inside that",
    "unit's weapon range (5 cells, 9 for a boomer).  The base 1 is dropped once the comcen has actually been hit ($897C), so only",
    "units that can reach it steer the escape.  The tail rankThreatDirections ($EC82, just past this chunk) smooths the ring and",
    "returns the centre of the quietest three-direction arc.",
    "In:  zp_27/zp_28 = AI comcen column/row (set by the caller at $E1D1), unitTypeTable/unitColTable/unitRowTable,",
    "     $897C = 'the comcen was hit this round or last', zp_57 game RNG low byte for the tie break",
    "Out: A = (total pressure - number of threats) / 2 = overall threat level, X = deepest weapon-range surplus (0 = nothing in",
    "     range), Y = safest compass direction 0-7, or 8 (the null delta entry) when nothing threatens;",
    "     $8896-$889D and $0400-$0407 hold the direction buckets, zp_18-zp_23 clobbered",
    "Called from: planTrainerRound ($E1DB), which files the results into $8962/$8960/$8961, and the drone code ($EF7B)."
   ],
   "line": "clear the accumulators"
  },
  "EBF1": {
   "unit": "game/trainerAiE000",
   "line": "total enemy pressure"
  },
  "EBF3": {
   "unit": "game/trainerAiE000",
   "line": "number of units that count as a threat"
  },
  "EBF5": {
   "unit": "game/trainerAiE000",
   "line": "deepest weapon-range surplus seen"
  },
  "EBF7": {
   "unit": "game/trainerAiE000",
   "line": "cleared but never read again in either half of the routine: a leftover"
  },
  "EBF9": {
   "unit": "game/trainerAiE000",
   "line": "clear the eight compass buckets, 7 down to 0"
  },
  "EBFB": {
   "unit": "game/trainerAiE000",
   "line": "bucket for direction Y"
  },
  "EBFE": {
   "unit": "game/trainerAiE000",
   "line": "next direction"
  },
  "EBFF": {
   "unit": "game/trainerAiE000",
   "line": "until all eight are clear"
  },
  "EC01": {
   "unit": "game/trainerAiE000",
   "block": [
    "One pass per human combat unit, ordinal 45 down to 0 (the roster is 28 grunts, 12 riders, 6 boomers,",
    "3 spies and the comcen, so 0-45 is exactly the units that can shoot)."
   ],
   "line": "scan the human player's units 45 down to 0"
  },
  "EC03": {
   "unit": "game/trainerAiE000",
   "line": "loop counter"
  },
  "EC05": {
   "unit": "game/trainerAiE000",
   "line": "the unit being weighed"
  },
  "EC07": {
   "unit": "game/trainerAiE000",
   "line": "its type byte"
  },
  "EC0A": {
   "unit": "game/trainerAiE000",
   "line": "destroyed: skip it"
  },
  "EC0C": {
   "unit": "game/trainerAiE000",
   "line": "weapon range 5 cells for a grunt or rider"
  },
  "EC0E": {
   "unit": "game/trainerAiE000",
   "line": "bit 1 of the type marks a BOOMER (type 2); a SPY would match too, but the scan never reaches one"
  },
  "EC10": {
   "unit": "game/trainerAiE000",
   "line": "not a boomer: keep range 5"
  },
  "EC12": {
   "unit": "game/trainerAiE000",
   "line": "a boomer reaches 9 cells"
  },
  "EC14": {
   "unit": "game/trainerAiE000",
   "line": "weapon range of this unit"
  },
  "EC16": {
   "unit": "game/trainerAiE000",
   "line": "its column"
  },
  "EC19": {
   "unit": "game/trainerAiE000",
   "line": "point A of the distance test"
  },
  "EC1B": {
   "unit": "game/trainerAiE000",
   "line": "and the target cell of the direction test"
  },
  "EC1D": {
   "unit": "game/trainerAiE000",
   "line": "its row"
  },
  "EC20": {
   "unit": "game/trainerAiE000",
   "line": "point A row"
  },
  "EC22": {
   "unit": "game/trainerAiE000",
   "line": "target row"
  },
  "EC24": {
   "unit": "game/trainerAiE000",
   "line": "distance from the AI comcen, which the caller left in cellColB/cellRowB"
  },
  "EC27": {
   "unit": "game/trainerAiE000",
   "line": "20 cells"
  },
  "EC29": {
   "unit": "game/trainerAiE000",
   "line": "too far away to matter"
  },
  "EC2B": {
   "unit": "game/trainerAiE000",
   "line": "keep the distance"
  },
  "EC2D": {
   "unit": "game/trainerAiE000",
   "line": "the comcen's own cell ..."
  },
  "EC2F": {
   "unit": "game/trainerAiE000",
   "line": "... is where the direction is measured from"
  },
  "EC31": {
   "unit": "game/trainerAiE000",
   "line": "comcen row"
  },
  "EC33": {
   "unit": "game/trainerAiE000",
   "line": "line walker row"
  },
  "EC35": {
   "unit": "game/trainerAiE000",
   "line": "Y = compass direction from the comcen toward this unit"
  },
  "EC38": {
   "unit": "game/trainerAiE000",
   "line": "a real direction 0-7"
  },
  "EC3A": {
   "unit": "game/trainerAiE000",
   "line": "the unit is standing on the comcen's own cell: charge it to direction 0"
  },
  "EC3C": {
   "unit": "game/trainerAiE000",
   "block": [
    "Weigh this unit: a base 1 for being nearby (suppressed once the comcen is actually under fire), +1",
    "within 10 cells and +4 more when the comcen sits inside its weapon range."
   ],
   "line": "one more threatening unit"
  },
  "EC3E": {
   "unit": "game/trainerAiE000",
   "line": "the weight this unit will contribute"
  },
  "EC40": {
   "unit": "game/trainerAiE000",
   "line": "was the comcen hit in this round or the previous one?"
  },
  "EC43": {
   "unit": "game/trainerAiE000",
   "line": "yes: only units that can actually reach it get any weight"
  },
  "EC45": {
   "unit": "game/trainerAiE000",
   "line": "no: every nearby enemy is worth 1"
  },
  "EC46": {
   "unit": "game/trainerAiE000",
   "line": "the distance again"
  },
  "EC48": {
   "unit": "game/trainerAiE000",
   "line": "closer than 10 cells?"
  },
  "EC4A": {
   "unit": "game/trainerAiE000",
   "line": "no: it only counts as a distant presence"
  },
  "EC4C": {
   "unit": "game/trainerAiE000",
   "line": "+1 for being close"
  },
  "EC4D": {
   "unit": "game/trainerAiE000",
   "line": "its weapon range"
  },
  "EC4F": {
   "unit": "game/trainerAiE000",
   "line": "prepare the subtraction"
  },
  "EC50": {
   "unit": "game/trainerAiE000",
   "line": "range minus distance: how deep the comcen sits inside its envelope"
  },
  "EC52": {
   "unit": "game/trainerAiE000",
   "line": "the comcen is out of range of this unit"
  },
  "EC54": {
   "unit": "game/trainerAiE000",
   "line": "+1 ..."
  },
  "EC55": {
   "unit": "game/trainerAiE000",
   "line": "+2 ..."
  },
  "EC56": {
   "unit": "game/trainerAiE000",
   "line": "+3 ..."
  },
  "EC57": {
   "unit": "game/trainerAiE000",
   "line": "+4 more: the comcen is inside this unit's weapon range"
  },
  "EC58": {
   "unit": "game/trainerAiE000",
   "line": "is this the deepest penetration so far?"
  },
  "EC5A": {
   "unit": "game/trainerAiE000",
   "line": "no"
  },
  "EC5C": {
   "unit": "game/trainerAiE000",
   "line": "remember the worst range surplus, returned in X"
  },
  "EC5E": {
   "unit": "game/trainerAiE000",
   "block": [
    "Add the weight to its compass bucket and to the running total, both saturating at 255."
   ],
   "line": "the weight of this unit"
  },
  "EC5F": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EC60": {
   "unit": "game/trainerAiE000",
   "line": "into the bucket of its compass direction"
  },
  "EC63": {
   "unit": "game/trainerAiE000",
   "line": "no overflow"
  },
  "EC65": {
   "unit": "game/trainerAiE000",
   "line": "saturate the bucket at 255"
  },
  "EC67": {
   "unit": "game/trainerAiE000",
   "line": "store the bucket back"
  },
  "EC6A": {
   "unit": "game/trainerAiE000",
   "line": "the weight again"
  },
  "EC6B": {
   "unit": "game/trainerAiE000",
   "line": "clear the carry"
  },
  "EC6C": {
   "unit": "game/trainerAiE000",
   "line": "into the overall pressure total"
  },
  "EC6E": {
   "unit": "game/trainerAiE000",
   "line": "no overflow"
  },
  "EC70": {
   "unit": "game/trainerAiE000",
   "line": "saturate the total at 255"
  },
  "EC72": {
   "unit": "game/trainerAiE000",
   "line": "store it back"
  },
  "EC74": {
   "unit": "game/trainerAiE000",
   "line": "next human unit"
  },
  "EC76": {
   "unit": "game/trainerAiE000",
   "line": "until unit 0 has been weighed"
  },
  "EC78": {
   "unit": "game/trainerAiE000",
   "block": [
    "Hand the ring over to rankThreatDirections, or - when nothing at all threatened the comcen - report",
    "the null direction 8, whose entry in dirToColDelta/dirToRowDelta is the zero step."
   ],
   "line": "did anything threaten the comcen?"
  },
  "EC7A": {
   "unit": "game/trainerAiE000",
   "line": "yes: smooth the ring and pick the quietest arc"
  },
  "EC7C": {
   "unit": "game/trainerAiE000",
   "line": "no threats: A = X = 0"
  },
  "EC7D": {
   "unit": "game/trainerAiE000",
   "line": "direction 8 = the null entry of the direction tables, i.e. 'no need to move'"
  },
  "EC7F": {
   "unit": "game/trainerAiE000",
   "line": "share the RTS at the end of rankThreatDirections"
  }
 },
 "dataTypes": {
  "E68A": [
   "byte",
   9
  ],
  "E693": [
   "byte",
   9
  ],
  "E7DB": [
   "byte",
   8
  ],
  "E9F1": [
   "byte",
   5
  ],
  "EA45": [
   "byte",
   1
  ]
 },
 "zpComments": {
  "1C": "general scratch: frame tile code, division remainder (mid), member count / max row of a group; also the running best score of the trainer's 2x2 bombardment search",
  "1D": "general scratch: bounds mode flag of computeGroupBounds (bit 7 = use destinations), division remainder (hi); the trainer's bombardment search keeps the chosen target unit here"
 }
}