{
  "chunk": "game_ovl_E000_T35_E68A",
  "unit": "game/ovl_E000_T35",
  "routines": [
    {
      "addr": "E69C",
      "name": "isGroupDestinationTooRisky",
      "summary": "Risk test for a candidate destination of the AI group in currentAiGroup ($8955). Computes the cell 'distance Y in direction X' from (zp_27,zp_28) with computeCellInDirection, expands it by the group's bounding box (groupSpanLeft/Up/Right/Down) into a rectangle clamped to 0..39, then walks all 100 units and sums unitThreatWeight ($0464) of every unit whose square influence area (unitThreatRadius $0400) overlaps that rectangle: plus for the human player's units 0-49, minus for the AI's own units 50-99 (own group members were already zeroed by buildUnitThreatTables). The tail at $E743 rejects a negative or zero total, otherwise subtracts the group's own strength ($8956/$8957) and returns carry.",
      "inputs": "X = direction 0-7, Y = distance in cells, zp_27/zp_28 = origin column/row, $8955 currentAiGroup, $8954 aiGroupCount, group span tables $890E/$891A/$8926/$8932, $0400/$0464 threat tables (built by buildUnitThreatTables), $8956/$8957 group strength, $896E, $91CB",
      "outputs": "C=1 -> reject (off map, or enemy threat >= group strength; exact ties broken with nextGameRandom); C=0 -> acceptable. zp_18/zp_19 = candidate column/row, zp_1A/zp_1B = signed 16-bit threat total, zp_1C clobbered; self-modifies the operands at $E704,$E70B,$E715,$E71C",
      "confidence": "medium"
    },
    {
      "addr": "E773",
      "name": "buildUnitThreatTables",
      "summary": "Rebuilds the two 100-entry influence-map tables used by isGroupDestinationTooRisky. For each unit 99..0 it stores an influence radius in $0400 and a strength weight in $0464. Dead units, spies/comcens (type >= 3) and AI units belonging to the group currently being moved get radius 0 and weight 0; grunts/riders get base 3, boomers base 6, each plus terrainSightBonusTable of the terrain the unit stands on. The weight comes from unitTypeStrengthTable ($E2B7) = 2,2,3,0,0.",
      "inputs": "$8955 currentAiGroup, aiUnitGroup table ($8800+unit), unitType $F76C, unitUnderCell $FAF0, terrainClassTable $077E, terrainSightBonusTable $35BB, unitTypeStrengthTable $E2B7",
      "outputs": "$0400[0..99] = threat radius, $0464[0..99] = threat weight; zp_18, zp_19 clobbered",
      "confidence": "high"
    },
    {
      "addr": "E7B9",
      "name": "multiplyAbyY",
      "summary": "8-bit multiply by repeated addition: returns A = A * Y (mod 256), with A = 0 when Y = 0. The multiplicand is stashed in the self-modified operand at $E7CB and Y is restored from the operand at $E7D0 before returning. Only used by offsetCoordinateClamped, where A is a step delta of 0, +1 or $FF and Y a cell distance.",
      "inputs": "A = multiplicand, Y = count",
      "outputs": "A = A*Y; Y preserved; X, flags clobbered; self-modifies $E7CB and $E7D0",
      "confidence": "high"
    },
    {
      "addr": "E7D3",
      "name": "buildDirectionFanTowardTarget",
      "summary": "Works out the compass direction from the line start (zp_93,zp_92) to the line end (zp_95,zp_94) with getStepDirection, saves it in zp_19 and, when a direction was found, falls into buildDirectionFan to lay out the eight candidate move directions around it. Called from the group-movement code at $E416.",
      "inputs": "zp_92/zp_93 = start row/column, zp_94/zp_95 = target row/column",
      "outputs": "zp_19 = heading 0-7 ($FF and no fan built when start == target); $8896-$889D = candidate directions; clobbers A/X/Y, zp_18, $8C-$97 via initLineStepper/stepLine",
      "confidence": "medium"
    },
    {
      "addr": "E7E3",
      "name": "buildDirectionFan",
      "summary": "Fills the eight-byte scratch buffer $8896-$889D with candidate move directions: entry i = (dirFanOffsetTable[i] + heading + 8) AND 7, where the offset is mirrored through dirMirrorTable ($E7DB, (8-x) AND 7) when bit 0 of the group's groupFlags is set, so left-handed and right-handed groups fan out the opposite way. The list is consumed by the move search at $E431-$E47E, whose per-slot minimum-distance thresholds are in $E664.",
      "inputs": "zp_19 = heading 0-7, $8955 currentAiGroup, $893E groupFlags, $894A dirFanOffsetTable",
      "outputs": "$8896-$889D = 8 candidate directions; zp_18 = $00/$FF mirror flag; A/X/Y clobbered",
      "confidence": "medium"
    },
    {
      "addr": "E80E",
      "name": "getStepDirection",
      "summary": "Runs initLineStepper + one stepLine between (zp_93,zp_92) and (zp_95,zp_94) and converts the resulting per-step deltas ($90F5 column, $90F4 row) into one of the eight compass directions by searching dirToColDelta/dirToRowDelta backwards.",
      "inputs": "zp_92/zp_93 start row/col, zp_94/zp_95 end row/col",
      "outputs": "Y = direction 0-7, or $FF when the deltas match no entry (start == end); A = last compared delta; line-stepper state $8C-$97, $90F4/$90F5 updated",
      "confidence": "high"
    },
    {
      "addr": "E82A",
      "name": "cellDistance",
      "summary": "Integer distance between two map cells: calls distanceSquared ($3870) for |dcol|^2+|drow|^2 and converts it back with squaresTable, returning the largest n with n*n <= dsq. Saturated squared distances return 16.",
      "inputs": "zp_22/zp_24 = cell column/row, zp_27/zp_28 = reference column/row",
      "outputs": "A = X = distance 0-16",
      "confidence": "high"
    },
    {
      "addr": "E83C",
      "name": "updateFocusGroupCentre",
      "summary": "Keeps focusGroupId ($895D) and its centre ($895E/$895F) up to date; the AI comcen escorts this group. When the id has bit 7 set the centre falls back to the AI comcen's own cell and the group is re-enabled if its groupFlags bit 7 has cleared; an enabled group whose groupFlags bit 7 is set is disabled by ORing $80 into the id; otherwise the centroid of the group's members 50-95 is recomputed with computeGroupCentroid and the group is dropped ($895D = $FF) when it has no members left.",
      "inputs": "$895D focusGroupId, $893E groupFlags, unitCol[99] $F6A3, unitRow[99] $F707",
      "outputs": "$895D, $895E = centre column, $895F = centre row; zp_18-zp_21 clobbered",
      "confidence": "medium"
    },
    {
      "addr": "E882",
      "name": "computeGroupCentroid",
      "summary": "Scans AI units (index 50+Y down to 50) and averages the position and the ordered destination of every living member whose aiUnitGroup entry equals A. Four 16-bit sums are accumulated in zp_18-zp_21 and divided by the member count with divideSigned16by8.",
      "inputs": "A = group id (patched into the compare at $E89D), Y = highest AI-relative unit index to scan (usually $31 or $2D); $8832 aiUnitGroup, $F79E unitType, $F672 unitCol, $F6D6 unitRow, $F802 unitDestCol, $F866 unitDestRow",
      "outputs": "A = member count (Z set when none), 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 $E89D, $E913, $E915",
      "confidence": "high"
    },
    {
      "addr": "E919",
      "name": "orderAiUnitToCell",
      "summary": "Central 'issue a move order' helper of the trainer AI. Returns immediately when the unit's ordered destination ($F802/$F866 masked to 6 bits) already is the requested cell; otherwise it mirrors both coordinates with mirrorMapCoordinate (because the engine treats them as an opponent packet and mirrors them back) and queues command $80 MOVE, or $81 MOVE GROUP when the unit's unitFlags bit 6 marks it as a group member.",
      "inputs": "X = target column, A = target row, Y = AI-relative unit index 0-49 (absolute unit = Y+50)",
      "outputs": "$8896 = $80/$81, $8897 = absolute unit index, $8898/$8899 = mirrored column/row, then queueAiCommand; nothing when the order is redundant",
      "confidence": "high"
    },
    {
      "addr": "E937",
      "name": "queueMirroredMoveOrder",
      "summary": "Second half of orderAiUnitToCell, only reached by falling through in this build (the XREF entries $E917/$EA01 belong to the T34 build): mirrors $8898/$8899, chooses command $80 or $81 from unitFlags bit 6, writes the unit index and tail-jumps into queueAiCommand.",
      "inputs": "$8898/$8899 = target column/row, Y = AI-relative unit index, $F73A unitFlags",
      "outputs": "command staged in $8896-$8899 and queued",
      "confidence": "high"
    },
    {
      "addr": "E95D",
      "name": "isPathToPlayerComcenClear",
      "summary": "Walks a Bresenham line from the current position (zp_93,zp_92) to the human player's comcen (unitCol[49]/unitRow[49]) and reports whether any living enemy combat unit (index < 50, type 0-2) stands on it. Used at $E3B1 to decide whether the group can make a straight run at the comcen. Both exits leave (zp_93,zp_92) restored to the group's centre through loadGroupCentreIntoLinePos.",
      "inputs": "zp_92/zp_93 = start row/column, $8955 currentAiGroup, $8954 aiGroupCount, $F671/$F6D5 player comcen position, map at $F000",
      "outputs": "C=1 = line reaches the comcen unobstructed, C=0 = blocked or invalid group; zp_92/zp_93 = group centre; zp_48/zp_49 and the line-stepper state clobbered",
      "confidence": "medium"
    },
    {
      "addr": "E997",
      "name": "loadGroupCentreIntoLinePos",
      "summary": "Copies the current group's centre (groupCentreCol $88EA / groupCentreRow $88F6) into the line-walker position zp_93/zp_92 and returns with C=1. Used as both a subroutine and the C=1 exit path of isPathToPlayerComcenClear.",
      "inputs": "$8955 currentAiGroup, $88EA/$88F6",
      "outputs": "zp_93 = column, zp_92 = row, C=1, A/Y clobbered",
      "confidence": "high"
    },
    {
      "addr": "E9A6",
      "name": "scoreTargetBlock",
      "summary": "Scores the 2x2 block of map cells whose top-left corner is (X,Y) as a bombardment target: every unit standing in the four cells contributes targetValueByUnitType (grunt 1, rider 2, boomer 4, spy 1, comcen 4), positive for the human player's units and negative for the AI's own. A block with a negative total is rejected; otherwise, if the total beats the running best in zp_1C, the corner is stored in zp_95/zp_94 and the highest player unit index seen in the block (which favours the comcen, unit 49) becomes the chosen target in zp_1D. Called from the missile/drone target search at $EFA8.",
      "inputs": "X = column, Y = row of the top-left cell, zp_1C = best score so far (threshold), zp_1D = best unit so far, map at $F000, $F76C unitType",
      "outputs": "zp_1A = block score, zp_1B = highest player unit index in the block; on improvement zp_1C, zp_1D, zp_94, zp_95 updated; zp_48/zp_49 = map pointer; self-modifies $E9E4 and $E9E8",
      "confidence": "medium"
    },
    {
      "addr": "E9F6",
      "name": "addUnitTargetValue",
      "summary": "Helper of scoreTargetBlock: takes a map cell byte holding $80|unitIndex, ignores dead units, and adds targetValueByUnitType[type] to zp_1A, negated for the AI's own units 50-99. For a player unit it also keeps the largest index seen in zp_1B.",
      "inputs": "A = map cell byte with bit 7 set, $F76C unitType, $E9F1 targetValueByUnitType",
      "outputs": "zp_1A += weight, zp_1B = max player unit index; X, Y clobbered",
      "confidence": "high"
    },
    {
      "addr": "EA1B",
      "name": "offsetCoordinateClamped",
      "summary": "Returns clamp(A + X*Y, 0..39) - the base coordinate A displaced by Y steps of the signed delta X - and reports through the carry whether the unclamped result left the 0..39 map range.",
      "inputs": "A = base coordinate, X = signed step (0, +1 or $FF), Y = number of steps",
      "outputs": "A = clamped coordinate 0-39, C=1 when the raw value was outside 0..39, Y preserved; self-modifies $EA27 and $EA36",
      "confidence": "high"
    },
    {
      "addr": "EA38",
      "name": "mirrorBlockCornerForOpponent",
      "summary": "Converts the top-left corner (X,Y) of a 2x2 object into the coordinates that the engine's mirrorCmdCoordsIfRemoteDec ($5005, mirror then decrement) will turn back into (X,Y): X = 39-(X+1), Y = 39-(Y+1). Used when the AI writes a drone position into the fake incoming packet at $EEB1.",
      "inputs": "X = column, Y = row of the block's top-left cell",
      "outputs": "X, Y = mirrored coordinates; A clobbered",
      "confidence": "high"
    },
    {
      "addr": "EA46",
      "name": "queueAiCommand",
      "summary": "Appends the command staged in $8896.. to the AI's outgoing queue at $88A0. Bumps aiCommandsThisRound ($8968), looks the command length up with getCommandLength, records a high-water mark in aiCmdQueuePeak ($EA45) and, if the queue would grow to 50 bytes or more, drops the command and sets the border-colour shadow $9003 to 6 as a visible overflow warning instead.",
      "inputs": "$8896-$8899 = staged command, $8952 aiCmdQueueLength, commandLengthTable via getCommandLength",
      "outputs": "$88A0 queue extended, $8952 += length, $8968 incremented, $92EE = length (consumed as the copy counter), $EA45 peak; on overflow $9003 = 6",
      "confidence": "high"
    },
    {
      "addr": "EA7B",
      "name": "injectAiCommandsIntoPacket",
      "summary": "The heart of the solo trainer: instead of receiving a packet over the modem, the AI copies whole commands out of its own queue at $88A0 straight into the comm module's received-packet buffer $E020/$E01E, so the lock-step engine executes them as if the opponent had sent them. It first calls sub_ED0B (drone/missile logic), stops if that set bit 7 of $8966, and then moves as many complete commands as fit while the packet stays below 8 bytes, shifting the remainder of the queue down each time. Called every exchange from $E0E4.",
      "inputs": "$8952 aiCmdQueueLength, $88A0 queue, $E01E current packet length, getCommandLength",
      "outputs": "$E020.. receive buffer filled, $E01E updated, queue compacted, $8952 reduced, $8966 = 0 (then possibly set by sub_ED0B), $92EE clobbered",
      "confidence": "high"
    },
    {
      "addr": "EACF",
      "name": "updateAiUnitTracking",
      "summary": "Per-unit hook run from sub_E0E8 after the engine steps each unit. For a human unit (index < 50) it latches the first grunt/rider/boomer whose chosen fire target ($91F0) is the AI comcen (unit 99) into comcenAttackerUnit ($8972). For an AI unit it flags the unit's group as having contact with the player's comcen (groupFlags bit 6, plus a game-clock stamp) when that comcen has been spotted and the unit is within 8 cells of it; finally, for the spy units 96-98 that have reached the opponent's end zone and have no group yet, it allocates a fresh group id ($8953) and seeds that group's target from the unit's own destination.",
      "inputs": "$90FA currentUnit, $F76C unitType, $91F0 chosenTargetUnit, $F9F5 player comcen energy/spotted bits, $F671/$F6D5 player comcen position, $F640/$F6A4 positions, $8800 aiUnitOrderRound, $8832 aiUnitGroup, $8864 aiUnitDeepFlag, $91CB roundsRemaining, $8954, $8964",
      "outputs": "$8972, $893E groupFlags, $8953 lastGroupId, aiUnitGroup entry, $87CE+unit round stamp (= aiUnitOrderRound), $88D2/$88DE group target",
      "confidence": "medium"
    },
    {
      "addr": "EB67",
      "name": "runAiComcenActions",
      "summary": "Spends the AI comcen's remaining command budget for the round (3 minus aiCommandsThisRound). When the comcen is stunned it needs two slots: it halts the comcen and, with a 1-in-8 chance, queues command $9E (comcen repaired). Otherwise it halts the comcen once and then calls maybeQueueComcenEnergyRepair for every remaining slot. Entered from $E1CD and $E21B.",
      "inputs": "$8968 aiCommandsThisRound, $8971 comcenStunCounter",
      "outputs": "$8967 comcenActionBudget, commands queued through queueAiCommand; returns silently when the budget is exhausted",
      "confidence": "medium"
    },
    {
      "addr": "EBA5",
      "name": "maybeQueueComcenEnergyRepair",
      "summary": "With 50 percent probability, and only while the AI comcen's energy is below 45 of 50, queues command $9A (ADD ENERGY) for unit 99 with an amount of 1 + (50-energy)/8, occasionally bumped by one so that the running total stays aligned to 4. This is how the solo trainer heals its own command centre; the projected energy is accumulated in $8965.",
      "inputs": "$FA27 = unitEnergy[99] (bits 0-5), $8965 projected comcen energy, nextGameRandom",
      "outputs": "$8896 = $9A, $8897 = $63, $8898 = amount, command queued; $8965 increased",
      "confidence": "medium"
    },
    {
      "addr": "EBDD",
      "name": "queueComcenCommand",
      "summary": "Tail entry shared by runAiComcenActions and maybeQueueComcenEnergyRepair: stores A as the command byte in $8896 and jumps into queueAiCommand, leaving $8897/$8898 as previously prepared.",
      "inputs": "A = command byte, $8897/$8898 = parameters already staged",
      "outputs": "command appended to the AI queue",
      "confidence": "high"
    },
    {
      "addr": "EBE4",
      "name": "haltAiComcen",
      "summary": "Orders the AI comcen (unit 99) to move to the cell it already occupies, which orderAiUnitToCell turns into a MOVE command only when the comcen currently has a different destination - i.e. it stops the comcen so that it can repair or launch. Called before the repair and drone actions in runAiComcenActions.",
      "inputs": "$F6A3 unitCol[99], $F707 unitRow[99]",
      "outputs": "possibly a $80/$81 command queued; nothing when the comcen is already halted",
      "confidence": "medium"
    },
    {
      "addr": "EBEF",
      "name": "assessThreatAroundAiComcen",
      "summary": "Builds a directional threat map around the AI comcen and picks an escape direction. For each of the human player's units 0-45 within 20 cells it adds a weight (1 for being close, 4 more when the comcen is inside the unit's weapon range of 5, or 9 for boomers, plus 1 when the comcen is standing still) into one of eight direction buckets in $8896-$889D. The tail (past the end of this chunk, $EC82-$ED0A) copies the buckets to $0400, smooths them circularly as (2*b[i]+b[i-1]+b[i+1])/4 and picks the centre of the least threatening three-direction window, breaking ties on the RNG state.",
      "inputs": "zp_27/zp_28 = AI comcen column/row, $F76C/$F640/$F6A4 player unit records, $897C comcenIsMoving, zp_57 RNG state",
      "outputs": "A = (total threat - threat count)/2, X = deepest weapon-range penetration (0 = nothing in range), Y = safest direction 0-7 or 8 when no threats; $8896-$889D and $0400-$0407 = direction buckets, zp_18-zp_23 clobbered. The caller at $E1DB stores them into $8962/$8961/$8960",
      "confidence": "medium"
    }
  ],
  "variables": [
    {"addr": "8800", "scope": "abs", "name": "aiUnitOrderRound", "meaning": "50 bytes, one per AI unit (indexed as $8800+i with i = unit-50, or $87CE+unit): the roundsRemaining value stamped when the unit was last given an order; compared with $91CB to decide whether an order has expired", "confidence": "medium"},
    {"addr": "8832", "scope": "abs", "name": "aiUnitGroup", "meaning": "50 bytes, one per AI unit ($8832+i, or $8800+unit for unit 50-99): the AI group the unit belongs to; loaded from the play template by the $EC00 track-29 setup overlay and re-assigned at $EB4D", "confidence": "high"},
    {"addr": "8864", "scope": "abs", "name": "aiUnitDeepFlag", "meaning": "50 bytes ($8864+i, or $8832+unit): bit 7 set by sub_E0E8 when the unit's unitType zone-score bits 3 and 4 are both set (unit has reached the opponent's end zone); read at $EB34 and $E579", "confidence": "medium"},
    {"addr": "8896", "scope": "abs", "name": "aiCommandStaging", "meaning": "8-byte scratch area: command byte + up to 3 parameters for queueAiCommand, and also the 8-slot per-direction array used by buildDirectionFan and assessThreatAroundAiComcen", "confidence": "high"},
    {"addr": "8897", "scope": "abs", "name": "aiCmdParam0", "meaning": "first parameter of the staged command (unit index for $80/$81/$9A)", "confidence": "high"},
    {"addr": "8898", "scope": "abs", "name": "aiCmdParam1", "meaning": "second parameter (mirrored column for $80/$81, energy amount for $9A)", "confidence": "high"},
    {"addr": "8899", "scope": "abs", "name": "aiCmdParam2", "meaning": "third parameter (mirrored row for $80/$81)", "confidence": "high"},
    {"addr": "88A0", "scope": "abs", "name": "aiCommandQueue", "meaning": "the AI's outgoing command byte queue, up to 50 bytes; filled by queueAiCommand and drained into the fake received packet by injectAiCommandsIntoPacket", "confidence": "high"},
    {"addr": "88D2", "scope": "abs", "name": "groupTargetCol", "meaning": "per-group (12 entries) target map column", "confidence": "medium"},
    {"addr": "88DE", "scope": "abs", "name": "groupTargetRow", "meaning": "per-group (12 entries) target map row", "confidence": "medium"},
    {"addr": "88EA", "scope": "abs", "name": "groupCentreCol", "meaning": "per-group average column of the living members (written by sub_E531 from computeGroupCentroid)", "confidence": "high"},
    {"addr": "88F6", "scope": "abs", "name": "groupCentreRow", "meaning": "per-group average row of the living members", "confidence": "high"},
    {"addr": "8902", "scope": "abs", "name": "groupUnitCount", "meaning": "per-group number of living members (0 = group wiped out)", "confidence": "high"},
    {"addr": "890E", "scope": "abs", "name": "groupSpanLeft", "meaning": "per-group extent of the formation to the left of its centre (centre column - minimum column)", "confidence": "high"},
    {"addr": "891A", "scope": "abs", "name": "groupSpanUp", "meaning": "per-group extent above the centre (centre row - minimum row)", "confidence": "high"},
    {"addr": "8926", "scope": "abs", "name": "groupSpanRight", "meaning": "per-group extent to the right of the centre (maximum column - centre + 1)", "confidence": "high"},
    {"addr": "8932", "scope": "abs", "name": "groupSpanDown", "meaning": "per-group extent below the centre (maximum row - centre + 1)", "confidence": "high"},
    {"addr": "893E", "scope": "abs", "name": "groupFlags", "meaning": "per-group flag byte: bit 0 mirrors the direction fan, bit 5 set when the group commits to the enemy comcen ($E3C3), bit 6 = group has contact with the player comcen ($EB1F), bit 7 = group disabled/suspended, low nibble also used as a mode", "confidence": "medium"},
    {"addr": "894A", "scope": "abs", "name": "dirFanOffsetTable", "meaning": "8 direction offsets used by buildDirectionFan. NEVER written by any code in the disassembly, so at run time it holds whatever the $8800 message-string block left there ($02,$02,$02,$01,$01,$02,$02,$02) - probably a bug", "confidence": "medium"},
    {"addr": "8952", "scope": "abs", "name": "aiCmdQueueLength", "meaning": "number of bytes currently in aiCommandQueue; cleared at $EC17 by the track-29 setup overlay", "confidence": "high"},
    {"addr": "8953", "scope": "abs", "name": "lastAiGroupId", "meaning": "highest group id in use; incremented at $EB46 when a deep spy unit gets its own group", "confidence": "medium"},
    {"addr": "8954", "scope": "abs", "name": "aiGroupCount", "meaning": "number of pre-planned AI groups (lastAiGroupId+1). Only ever written by the track-29 setup overlay ($ECC6); read here at $E6A5, $E960, $EB3C", "confidence": "medium"},
    {"addr": "8955", "scope": "abs", "name": "currentAiGroup", "meaning": "group id the AI is currently thinking about; selects the per-group tables in almost every routine of this module", "confidence": "high"},
    {"addr": "8956", "scope": "abs", "name": "groupStrengthLo", "meaning": "16-bit sum (with $8957) of unitTypeStrengthTable over the current group's living members; compared against the enemy threat total by isGroupDestinationTooRisky", "confidence": "high"},
    {"addr": "8957", "scope": "abs", "name": "groupStrengthHi", "meaning": "high byte of groupStrength", "confidence": "high"},
    {"addr": "895D", "scope": "abs", "name": "focusGroupId", "meaning": "the group the AI comcen escorts; bit 7 set = suspended, $FF = none", "confidence": "medium"},
    {"addr": "895E", "scope": "abs", "name": "focusGroupCol", "meaning": "column of focusGroupId's centre (AI comcen column when there is no focus group)", "confidence": "high"},
    {"addr": "895F", "scope": "abs", "name": "focusGroupRow", "meaning": "row of focusGroupId's centre", "confidence": "high"},
    {"addr": "8960", "scope": "abs", "name": "comcenEscapeDirection", "meaning": "direction 0-7 returned by assessThreatAroundAiComcen as the least threatened one, or 8 = no threat (index 8 of dirToColDelta/dirToRowDelta is the (0,0) entry)", "confidence": "medium"},
    {"addr": "8961", "scope": "abs", "name": "comcenThreatDepth", "meaning": "largest (weapon range - distance) over the player units threatening the AI comcen; used as the retreat distance", "confidence": "medium"},
    {"addr": "8962", "scope": "abs", "name": "comcenThreatScore", "meaning": "overall danger around the AI comcen, (total threat - threat count)/2; compared with $897A at $E29A", "confidence": "medium"},
    {"addr": "8963", "scope": "abs", "name": "comcenFleeFlag", "meaning": "$FF when the comcen is in weapon range, the focus group is active and the comcen is not already moving (retreat path at $E266); 0 otherwise", "confidence": "medium"},
    {"addr": "8964", "scope": "abs", "name": "homeDefenceGroupId", "meaning": "group id reserved for defending the AI comcen (random 1-3, or $FF for play 1); excluded from the comcen-contact retasking at $EB17/$EB41", "confidence": "medium"},
    {"addr": "8965", "scope": "abs", "name": "comcenProjectedEnergy", "meaning": "AI comcen energy as it will be after the $9A repair commands still sitting in the queue have executed; reloaded from unitEnergy[99] each round at $E195", "confidence": "medium"},
    {"addr": "8966", "scope": "abs", "name": "aiScratchFlag", "meaning": "multi-purpose byte: the play/scenario index 0-4 during setup, a copy of groupFlags at $E31E (bits 6/7 tested), and a bit-7 abort flag for injectAiCommandsIntoPacket", "confidence": "low"},
    {"addr": "8967", "scope": "abs", "name": "comcenActionBudget", "meaning": "number of command slots left for the AI comcen this round (3 - aiCommandsThisRound)", "confidence": "high"},
    {"addr": "8968", "scope": "abs", "name": "aiCommandsThisRound", "meaning": "commands the AI has issued in the current round; cleared at $E176, incremented by queueAiCommand, capped at 3-5 by the callers", "confidence": "high"},
    {"addr": "896D", "scope": "abs", "name": "maxOrderStep", "meaning": "upper bound on how far a group may be ordered in one command (2, 3 or 5 depending on the AI mode); applied at $E4C7", "confidence": "medium"},
    {"addr": "896E", "scope": "abs", "name": "comcenEmergencyStamp", "meaning": "roundsRemaining stamped when the AI comcen last had to run away ($E269); used as a tie-breaker in isGroupDestinationTooRisky and at $E3E2", "confidence": "low"},
    {"addr": "8971", "scope": "abs", "name": "comcenStunCounter", "meaning": "low nibble of unitStunDig[99]: how long the AI comcen is stunned; 0 = fit", "confidence": "high"},
    {"addr": "8972", "scope": "abs", "name": "comcenAttackerUnit", "meaning": "index of the first human combat unit seen firing at the AI comcen ($FF = none); used by the drone/missile code at $EDF6", "confidence": "medium"},
    {"addr": "8976", "scope": "abs", "name": "aiStrikeTargetUnit", "meaning": "player unit index chosen as the drone/missile target by the scan at $EF9B ($FF = none); when it is $31 (the player comcen) the group code at $E3AA attacks directly", "confidence": "medium"},
    {"addr": "8977", "scope": "abs", "name": "eeOverlayState", "meaning": "state of the swappable $EE00 page: bit 7 = the track-29 drone sub-overlay is resident (the AI think routines are skipped), otherwise a 3..0 countdown after the module's own $EE00 code has been reloaded", "confidence": "medium"},
    {"addr": "8979", "scope": "abs", "name": "strikeScoreThreshold", "meaning": "minimum block score a bombardment target must beat; reduced by 25 percent after each successful pick ($EFBA)", "confidence": "medium"},
    {"addr": "897A", "scope": "abs", "name": "comcenThreatThreshold", "meaning": "threat score above which the comcen reacts ($E29A). Set to 5 by the track-29 setup overlay ($EC44) and never written by this module", "confidence": "medium"},
    {"addr": "897C", "scope": "abs", "name": "comcenIsMoving", "meaning": "$FF when the AI comcen's unitGroupFlags bit 1 (moving) was set at the last check; a stationary comcen makes every threat count one point more", "confidence": "medium"},
    {"addr": "EA45", "scope": "abs", "name": "aiCmdQueuePeak", "meaning": "high-water mark of the AI command queue length; written and compared only inside queueAiCommand and never read anywhere else - a leftover statistic", "confidence": "high"},
    {"addr": "9003", "scope": "abs", "name": "borderColorShadow", "meaning": "VIC border colour shadow copied to $D020 by the raster IRQ; queueAiCommand sets it to 6 to signal a command-queue overflow", "confidence": "high"},
    {"addr": "90FA", "scope": "abs", "name": "currentUnit", "meaning": "index of the unit the engine is stepping; read by updateAiUnitTracking", "confidence": "high"},
    {"addr": "91CB", "scope": "abs", "name": "roundsRemaining", "meaning": "game clock, counts down; used as a time stamp by the AI", "confidence": "high"},
    {"addr": "91F0", "scope": "abs", "name": "chosenTargetUnit", "meaning": "unit index the engine picked as the fire target of the unit just processed ($FF none)", "confidence": "high"},
    {"addr": "92EE", "scope": "abs", "name": "currentCommandLength", "meaning": "length of the command last looked up by getCommandLength; used as the byte counter in queueAiCommand and injectAiCommandsIntoPacket", "confidence": "high"},
    {"addr": "E01E", "scope": "abs", "name": "rxPacketLength", "meaning": "length of the packet 'received' from the opponent; the trainer AI writes it itself", "confidence": "high"},
    {"addr": "E020", "scope": "abs", "name": "rxPacketBuffer", "meaning": "buffer holding the opponent's command bytes for the current exchange (max 7 bytes used here)", "confidence": "high"},
    {"addr": "0400", "scope": "abs", "name": "unitThreatRadius", "meaning": "100-byte scratch table (reuses the copy-protection answer area): influence radius of every unit, built by buildUnitThreatTables; also reused for the 8 smoothed direction buckets at $EC87", "confidence": "high"},
    {"addr": "0464", "scope": "abs", "name": "unitThreatWeight", "meaning": "100-byte scratch table parallel to unitThreatRadius: strength weight of each unit", "confidence": "high"},
    {"addr": "F640", "scope": "abs", "name": "unitCol", "meaning": "per-unit map column (100 entries); $F671 = player comcen (unit 49), $F672 = first AI unit, $F6A3 = AI comcen (unit 99)", "confidence": "high"},
    {"addr": "F6A4", "scope": "abs", "name": "unitRow", "meaning": "per-unit map row; $F6D5 = player comcen, $F6D6 = first AI unit, $F707 = AI comcen", "confidence": "high"},
    {"addr": "F708", "scope": "abs", "name": "unitFlags", "meaning": "per-unit flags; bit 6 = member of a group (tested at $E948 via $F73A = AI unit 0)", "confidence": "medium"},
    {"addr": "F76C", "scope": "abs", "name": "unitType", "meaning": "per-unit type byte: bits 0-2 type (0 grunt, 1 rider, 2 boomer, 3 spy, 4 comcen), bits 3-4 zone-scored markers, bit 7 = dead/absent; $F79E = AI unit 0", "confidence": "high"},
    {"addr": "F7D0", "scope": "abs", "name": "unitDestCol", "meaning": "per-unit ordered destination column in bits 0-5; $F802 = AI unit 0, $F833 = AI comcen", "confidence": "high"},
    {"addr": "F834", "scope": "abs", "name": "unitDestRow", "meaning": "per-unit ordered destination row in bits 0-5; $F866 = AI unit 0, $F897 = AI comcen", "confidence": "high"},
    {"addr": "F960", "scope": "abs", "name": "unitStunDig", "meaning": "per-unit stun counter in the low nibble; $F9C3 = AI comcen, read at $E198", "confidence": "high"},
    {"addr": "F9C4", "scope": "abs", "name": "unitEnergy", "meaning": "per-unit energy in bits 0-5 (0-50) with spotted/visible bits 6-7; $F9F5 = player comcen (bit 7 tested as 'comcen spotted'), $FA27 = AI comcen", "confidence": "high"},
    {"addr": "FAF0", "scope": "abs", "name": "unitUnderCell", "meaning": "per-unit saved map byte of the cell the unit stands on; used as the terrain index in buildUnitThreatTables", "confidence": "high"},
    {"addr": "FB54", "scope": "abs", "name": "unitGroupFlags", "meaning": "per-unit flags; bit 1 = moving. $FBB7 = AI comcen, sampled into $897B/$897C", "confidence": "medium"},
    {"addr": "18", "scope": "zp", "name": "aiScratch18", "meaning": "candidate column from computeCellInDirection; low byte of the column sum in computeGroupCentroid; loop index in assessThreatAroundAiComcen; mirror flag in buildDirectionFan", "confidence": "medium"},
    {"addr": "19", "scope": "zp", "name": "aiScratch19", "meaning": "candidate row from computeCellInDirection; heading for buildDirectionFan; unit type / weapon range in buildUnitThreatTables and assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "1A", "scope": "zp", "name": "aiScoreLo", "meaning": "low byte of the 16-bit threat total in isGroupDestinationTooRisky; block score in scoreTargetBlock; cell distance in assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "1B", "scope": "zp", "name": "aiScoreHi", "meaning": "high byte of the threat total; highest player unit index found in scoreTargetBlock", "confidence": "medium"},
    {"addr": "1C", "scope": "zp", "name": "aiBestScore", "meaning": "per-unit influence radius while scanning in isGroupDestinationTooRisky; member count in computeGroupCentroid; running best block score in scoreTargetBlock", "confidence": "medium"},
    {"addr": "1D", "scope": "zp", "name": "aiBestTargetUnit", "meaning": "best target unit index found by scoreTargetBlock ($FF = none); also a member counter in sub_E531", "confidence": "medium"},
    {"addr": "1E", "scope": "zp", "name": "groupDestCentreCol", "meaning": "average destination column produced by computeGroupCentroid; cleared as a threat accumulator in assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "1F", "scope": "zp", "name": "groupDestCentreRow", "meaning": "average destination row from computeGroupCentroid; total threat weight in assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "20", "scope": "zp", "name": "aiSumLo20", "meaning": "destination-row sum in computeGroupCentroid; number of threatening units in assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "21", "scope": "zp", "name": "aiSumHi21", "meaning": "high byte of the destination-row sum; deepest weapon-range penetration in assessThreatAroundAiComcen", "confidence": "medium"},
    {"addr": "22", "scope": "zp", "name": "cellColA", "meaning": "column argument of distanceSquared/cellDistance; also the group bounding box minimum column in sub_E531", "confidence": "medium"},
    {"addr": "24", "scope": "zp", "name": "cellRowA", "meaning": "row argument of distanceSquared/cellDistance; group bounding box minimum row in sub_E531", "confidence": "medium"},
    {"addr": "26", "scope": "zp", "name": "chosenDirection", "meaning": "direction slot selected from the direction fan by the move search at $E435", "confidence": "medium"},
    {"addr": "27", "scope": "zp", "name": "originCol", "meaning": "origin column for computeCellInDirection and cellDistance (usually the group centre or the AI comcen)", "confidence": "high"},
    {"addr": "28", "scope": "zp", "name": "originRow", "meaning": "origin row for computeCellInDirection and cellDistance", "confidence": "high"},
    {"addr": "92", "scope": "zp", "name": "lineStartRow", "meaning": "line-walker start row (shared with initLineStepper/stepLine)", "confidence": "high"},
    {"addr": "93", "scope": "zp", "name": "lineStartCol", "meaning": "line-walker start column", "confidence": "high"},
    {"addr": "94", "scope": "zp", "name": "lineEndRow", "meaning": "line-walker end row; also the chosen target row written by scoreTargetBlock", "confidence": "high"},
    {"addr": "95", "scope": "zp", "name": "lineEndCol", "meaning": "line-walker end column; also the chosen target column written by scoreTargetBlock", "confidence": "high"},
    {"addr": "48", "scope": "zp", "name": "mapCellPtr", "meaning": "pointer to the current map cell returned by readMapCell; scoreTargetBlock reads offsets +1, +$28 and +$29 through it to cover a 2x2 block", "confidence": "high"}
  ],
  "dataBlocks": [
    {"addr": "E68A", "length": 9, "type": "byteTable", "name": "dirToColDelta", "description": "Column delta of the 8 compass directions numbered clockwise from north: 0,+1,+1,+1,0,-1,-1,-1. The 9th entry ($E692) is 0 and pairs with dirToRowDelta[8] to form a harmless 'no direction' entry used when assessThreatAroundAiComcen finds no threats."},
    {"addr": "E693", "length": 9, "type": "byteTable", "name": "dirToRowDelta", "description": "Row delta of the same 8 directions: -1,-1,0,+1,+1,+1,0,-1, plus the 9th zero entry. Direction 0 = north, 2 = east, 4 = south, 6 = west."},
    {"addr": "E7DB", "length": 8, "type": "byteTable", "name": "dirMirrorTable", "description": "00,07,06,05,04,03,02,01 = (8 - d) AND 7: mirrors a direction across the north-south axis. Used by buildDirectionFan to flip the candidate fan for groups whose groupFlags bit 0 is set. (The label L_E7E0 in the middle of the table is a T34 branch target, not a separate block.)"},
    {"addr": "E9F1", "length": 5, "type": "byteTable", "name": "targetValueByUnitType", "description": "01,02,04,01,04 - bombardment value of a unit by type (grunt 1, rider 2, boomer 4, spy 1, comcen 4), used by addUnitTargetValue."},
    {"addr": "EA45", "length": 1, "type": "byteTable", "name": "aiCmdQueuePeak", "description": "One byte of data embedded in the code stream between mirrorBlockCornerForOpponent and queueAiCommand: the high-water mark of the AI command queue length. Written and compared only inside queueAiCommand; no other code reads it."}
  ],
  "misclassified": [],
  "insights": [
    "The T35 build of the $E000 module is not a link variant at all: it is the SOLO TRAINER's computer opponent. Chunk $E68A-$EC7F contains its movement, risk-assessment and command-generation core. The AI plays side 1 (units 50-99, comcen 99) against the human's side 0 (units 0-49, comcen 49).",
    "How the AI reaches the game engine: injectAiCommandsIntoPacket ($EA7B, called every exchange from $E0E4) copies whole commands out of its own queue at $88A0 directly into the comm module's received-packet buffer $E020/$E01E, so processCommandExchange executes them exactly as if a modem opponent had sent them. Commands are staged in $8896-$8899, appended by queueAiCommand ($EA46) and the packet is capped at 7 bytes. Because they travel as 'remote' commands, orderAiUnitToCell ($E919) pre-mirrors both coordinates with mirrorMapCoordinate (and mirrorBlockCornerForOpponent, $EA38, additionally pre-decrements for 2x2 objects) so that the engine's mirrorCmdCoordsIfRemote(Dec) recovers the intended cell. Unit indices are NOT mirrored.",
    "AI data model in the $8800 block (the modem message strings loaded there are dead once the trainer runs): three 50-byte per-AI-unit tables - $8800 last-order round stamp, $8832 group id, $8864 'reached the enemy end zone' flag - each also addressed with the absolute unit index through the bases $87CE/$8800/$8832; twelve-entry per-group tables at $88D2 targetCol, $88DE targetRow, $88EA/$88F6 centre, $8902 member count, $890E/$891A/$8926/$8932 formation spans left/up/right/down, $893E flags; then scalars $8952-$897C. The 50-byte command queue is at $88A0.",
    "Direction encoding used throughout the module: 0-7 clockwise from north via dirToColDelta ($E68A) / dirToRowDelta ($E693), with a deliberate 9th (0,0) entry so that 'no direction' = 8 is safe. getStepDirection ($E80E) converts a Bresenham step from the shared line walker ($FBB8/$FBFA, deltas in $90F4/$90F5) into that encoding; offsetCoordinateClamped ($EA1B) and computeCellInDirection ($E66C) turn (direction, distance) back into a clamped 0..39 cell and report leaving the map through the carry.",
    "Movement risk model: buildUnitThreatTables ($E773) builds an influence map - per unit a radius (3 grunt/rider, 6 boomer, 0 spy/comcen, plus terrainSightBonusTable of the cell it stands on) in $0400 and a weight (2,2,3,0,0 by type) in $0464, with the moving group's own members zeroed. isGroupDestinationTooRisky ($E69C) then sums the weights of all enemy units whose influence square overlaps the group's footprint at a candidate cell, subtracts friendly ones, and rejects the cell when the result reaches the group's own strength ($8956/$8957); exact ties are decided by nextGameRandom, so the AI is deliberately non-deterministic here.",
    "Comcen behaviour: assessThreatAroundAiComcen ($EBEF) buckets the player's units 0-45 by direction around the AI comcen, smooths the 8 buckets circularly as (2b[i]+b[i-1]+b[i+1])/4 and returns the centre of the LEAST threatening 3-direction window as the escape direction (minimum search at $ECED, random tie-break on the RNG state zp_57). runAiComcenActions ($EB67) then spends the round's leftover command slots: haltAiComcen ($EBE4) stops it, maybeQueueComcenEnergyRepair ($EBA5) queues command $9A ADD ENERGY for unit 99 (1 + (50-energy)/8 points, 50 percent of the time, only below 45/50 energy) and a stunned comcen has a 1-in-8 chance per round of queuing $9E COMCEN REPAIRED. The trainer therefore heals its own command centre - a built-in handicap for the human.",
    "Two bytes this module reads but never writes: $894A (the 8-entry direction-fan offset table for buildDirectionFan) and $897A (the comcen threat threshold). $897A is initialised to 5 by the track-29 $EC00 setup overlay ($EC44), but nothing anywhere in the disassembly writes $894A, so at run time it contains the bytes the $8800 message-string overlay left there ($02,$02,$02,$01,$01,$02,$02,$02) and the eight 'candidate directions' collapse to just heading+1 and heading+2. This looks like a genuine bug.",
    "queueAiCommand sets the VIC border-colour shadow $9003 to 6 and drops the command when the queue would reach 50 bytes, and keeps an otherwise unused high-water mark in $EA45 - both are debugging leftovers that survived into the shipped build.",
    "WARNING for other annotators: disassembly/XREF.txt merges the cross-references of ovl_E000_T34 and ovl_E000_T35 for every $E000-$EFFF address (duplicated entries such as 'call:E917,E917' are the give-away). Several caller lists attributed to T35 labels here (sub_E937 call:E917/$EA01, sub_E9A6 call:EE37/EE73/EE7D/EE88/EE92, sub_EA38 call:EEC1/EF8F, D_8976 w:EC33) are T34 addresses; in T35 sub_E937 is only fall-through, sub_E9A6 is called just from $EFA8 and sub_EA38 just from $EEB1. Similarly, several D_/L_ labels inside this chunk ($EAA0-$EAC6, $EAEB, $EAFF, $EB01, $EB03, $EBFE, $EC05, $EC0A) are branch targets of the T34 build sitting in the middle of T35 routines.",
    "The AI's group plan is not computed at run time: the track-29 $EC00 setup overlay ($EC0F) picks one of five canned plays by game type, loads a 768-byte playbook from track 29 sectors 6-8 into $0200-$04FF and copies a 150-byte slice of it into aiUnitGroup ($8832), unitDestCol ($F802) and unitDestRow ($F866), setting aiGroupCount ($8954) to 11 (plays 0-1) or 5 (plays 2-4) and picking a random home-defence group id in $8964. This chunk only executes that plan."
  ]
}
