{
 "chunk": "data_units",
 "unit": "game/pure-data units (index; per-unit detail in data_data_9300.json, data_ovl_8800.json, data_map_F000.json, data_setup_0200_default.json, data_table_0400.json, data_setup_0200.json, data_high_FBB8.json, data_main_0800.json)",
 "routines": [],
 "variables": [],
 "dataBlocks": [
  {
   "addr": "9300",
   "length": 25,
   "type": "byteTable",
   "name": "bitmapRowLoTable",
   "description": "Low bytes of the 25 bitmap character-row addresses $A000 + 320*row (values cycle $00,$40,$80,$C0). Read as bitmapRowLoTable,Y (Y = row 0-24) by calcCellPointersXY ($2AA3) and by plotBitmapPixelRow ($FC80) in the high-memory block; overlay B reads it biased by 3 ($9303, rows 3+) at $7BDA/$7C38.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9319",
   "length": 25,
   "type": "byteTable",
   "name": "bitmapRowHiTable",
   "description": "High bytes of the same 25 row addresses: $A0,$A1,$A2,$A3,$A5,$A6,$A7,$A8,$AA,... (step $140 per row). Read at $2AAC and $FC87; biased alias $931C used by overlay B.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9332",
   "length": 40,
   "type": "byteTable",
   "name": "bitmapColLoTable",
   "description": "Low bytes of column*8 for the 40 bitmap columns (0,8,...,312 mod 256). Read at $2AA7 and $FCAE; biased alias $9335 used by overlay B at $7BDD/$7C3B.",
   "unit": "game/data_9300"
  },
  {
   "addr": "935A",
   "length": 40,
   "type": "byteTable",
   "name": "bitmapColHiTable",
   "description": "High bytes of column*8: $00 for columns 0-31, $01 for columns 32-39. Read at $2AAF and $FCB5.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9382",
   "length": 25,
   "type": "byteTable",
   "name": "screenRowLoTable",
   "description": "Low bytes of the 25 screen-RAM row addresses $8C00 + 40*row (0,$28,$50,...). Read by fillScreenColourRect ($2A71), calcCellPointersXY ($2AB6) and both $6F00 overlays at $82C1.",
   "unit": "game/data_9300"
  },
  {
   "addr": "939B",
   "length": 25,
   "type": "byteTable",
   "name": "screenRowHiTable",
   "description": "High bytes of the screen-RAM row addresses: $8C x7, $8D x6, $8E x7, $8F x5. Read at $2A6C, $2ABB and $82C6 in both $6F00 overlays. Colour RAM is reached by adding $4C to the high byte.",
   "unit": "game/data_9300"
  },
  {
   "addr": "93B4",
   "length": 114,
   "type": "byteTable",
   "name": "fontColorTable",
   "description": "Default screen-RAM colour byte for each of the 114 gameFont glyphs, same index as gameFont. Read by plotGlyph at $2AD6 only when textDrawParams ($2A29) is zero; a zero entry makes the plotter fall back to the explicit colour $2A2A. Values are packed nibble pairs, e.g. $BC, $FC, $6E for the diagonal shading glyphs $30-$37 and $BB/$FF for the heavy bevel glyphs $6A-$71.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9426",
   "length": 912,
   "type": "charset",
   "name": "gameFont",
   "description": "114 glyphs of 8x8 pixels, 8 bytes each, addressed as $9426 + code*8 by plotGlyph ($2AC8). This is not a text font: $00 blank, $01-$05 solid, $06-$13 single-pixel corner/edge pieces, $14-$2F the chunky diagonal / wedge shading set, $30-$33 the two halves of a tall 'N' and 'S' (stacked vertically) and $34-$37 a goalpost 'H' and a comb 'E' (two cells side by side), $38 a small hollow diamond, $39-$48 thin frame pieces, diagonals and the four double-line corners, $49-$56 the top and bottom halves of the ten tall digits 0-9 (see tallDigitGlyphPairTable), $57-$59 three narrow glyphs that together form a 24-pixel strip of small letters and are not reached from any table found so far, $5A-$5E vertical dither patterns, $5F-$71 the heavy raised-bevel frame set. Text on the battlefield screens is drawn with the separate 59-glyph sets at $98BE/$9A96, not with this one.",
   "unit": "game/data_9300"
  },
  {
   "addr": "97B6",
   "length": 90,
   "type": "byteTable",
   "name": "unreferencedGlyphCodeBlock",
   "description": "90 bytes of valid gameFont glyph codes ($00,$02,$04,$05,$07,$14-$2F,$30-$38,$45-$48) sitting between gameFont and tallDigitGlyphPairTable. No traced code reads it: XREF.txt has no label anywhere in $9426-$980F, no instruction in any .s file carries an immediate $97 or $98 high byte, and a scan of the whole 64K runtime image finds no little-endian pointer into $97B6-$980F. Rendering it as 10x9, 9x10, 15x6, 6x15, 5x18 or 18x5 glyph grids, as 45 vertically stacked glyph pairs (the tallDigitGlyphPairTable format) and as ten 3x3 window frame sets (the $9824 format) all produce incoherent art, so the exact structure is unresolved; the value range says it is a screen-code layout of some kind, most likely leftover from an earlier build. (confidence: low on the interpretation, high on the fact that nothing reads it)",
   "unit": "game/data_9300"
  },
  {
   "addr": "9810",
   "length": 20,
   "type": "byteTable",
   "name": "tallDigitGlyphPairTable",
   "description": "Ten 2-byte pairs, indexed by drawTallGlyph ($2BB7) as $9810 + symbol*2. The first code is drawn at row zp_18 and the second one row lower, so each pair forms a 8x16 pixel tall digit: 0=($49,$4A), 1=($4B,$4C), 2=($4D,$4E), 3=($4D,$4F), 4=($50,$51), 5=($52,$4F), 6=($52,$53), 7=($54,$55), 8=($56,$53), 9=($56,$51). Used for the big numbers on the console panels.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9824",
   "length": 90,
   "type": "byteTable",
   "name": "windowFrameTileSets",
   "description": "Ten 9-byte frame tile sets, each nine gameFont codes in row-major order (topLeft, top, topRight, left, centre, right, bottomLeft, bottom, bottomRight), selected through windowFramePtrTable ($2C49) and drawn by drawWindowFrame ($2BDD group) / drawFrameBox. $9824 = thin single box, $982D = thin box built from the $39-$40 pieces, $9836 = octagon with diagonal corners, $983F = solid box with diagonal shadow corners, $9848 = all $03 (solid fill), $9851 = all $00 (blank/erase), $985A = 50 percent dither box, $9863/$986C = heavy raised bevel with a dithered outer edge, $9875 = heavy solid bevel.",
   "unit": "game/data_9300"
  },
  {
   "addr": "987E",
   "length": 64,
   "type": "byteTable",
   "name": "asciiToGlyphTable",
   "description": "Character code $20-$5F -> gameFont glyph index, 64 entries. printChar ($C01A) reads it as asciiToGlyphTable,Y with Y = char - $20 ($C02A). The chat-line editor at $C80A reads exactly the same table through the biased base $985E with the raw character code in X and rejects the character when the entry is $3B, which is why XREF lists a second label $985E for it. Layout: space -> 0, digits '0'-'9' -> $01-$0A, 'A'-'Z' -> $0B-$24, punctuation to the small symbol glyphs, '[' -> $28, ']' -> $29, '\\', '^', '_' -> $3B (not typeable).",
   "unit": "game/data_9300"
  },
  {
   "addr": "98BE",
   "length": 472,
   "type": "charset",
   "name": "consoleFontMulticolour",
   "description": "59 glyphs of 8x8, 8 bytes each: the multicolour / double-width character set of the $C000 text plotter, selected when bit 4 of the $D016 shadow $9001 is set ($C20B). Every pixel is drawn as the bit pair %11 (byte values $00,$03,$0C,$0F,$30,$33,$3C,$3F). Order: 0 blank, 1-10 digits 0-9, 11-36 letters A-Z, 37-58 punctuation and arrows. Stored positive on disk and inverted in place at boot by invertTextFonts ($031B), which EORs $98BE-$9C6D with $FF.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9A96",
   "length": 472,
   "type": "charset",
   "name": "consoleFontHires",
   "description": "The same 59 glyphs in single-width hires form ($9A96 = $98BE + $1D8), used when $9001 bit 4 is clear. Also inverted at boot by invertTextFonts.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9C6E",
   "length": 352,
   "type": "charset",
   "name": "terrainTileShapes",
   "description": "44 one-bit 8x8 terrain tiles, 8 bytes each, addressed as $9C6E + index*8 by setTerrainTilePtr ($2DBD). The index is mapCell - $40, or mapCell - $20 when the cell is below $40 ($27B2), so the lower contour codes $21-$34 and the upper contour codes $41-$54 share tiles $01-$14. Tiles: $00 blank ground, $01-$14 contour/ridge line segments, $15-$18 the four quadrants of a small round blob and $19-$1C its filled forms (river, cells $55-$5C), $1D-$20 dense noise (forest, $5D-$60), $21-$24 sparse dots (lower hill, $61-$64), $25-$26 very sparse dots (summit, $65-$66), $27 a boxed cross (the recycler, $67), $28-$2B four checkerboard blocks (end-zone/goal cells $68-$6B). The main program doubles each nybble through nybbleToMulticolourTable to draw them 16x16 in the tactical view; overlay B's drawRadarGlyph ($7396) uses the same base for the radar screens.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9DCE",
   "length": 160,
   "type": "charset",
   "name": "unitSymbolShapes",
   "description": "20 one-bit 8x8 unit symbols continuing the $9C6E table at indices $2C-$3F: five unit types x four facings, matching unitGlyphBaseTable at $0799 ($2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN, plus unitFlags AND 3). They are the small single-cell symbols; the big 16x16 multicolour versions live at $6C5D in the main program.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9E6E",
   "length": 36,
   "type": "sprite",
   "name": "spareCursorShapes",
   "description": "Two 18-byte sprite fragments in the same format as cursorBlockShape: six 24-pixel rows drawing a 6x6 hollow box ($9E6E) and a 6x6 plus/cross ($9E80). Nothing in the disassembly reads them - they look like alternative cursor shapes kept next to the one that is used. (confidence: medium)",
   "unit": "game/data_9300"
  },
  {
   "addr": "9E92",
   "length": 18,
   "type": "sprite",
   "name": "cursorBlockShape",
   "description": "18 bytes = six 3-byte sprite rows drawing a small solid 4x4 block. setCursorShapeBlock ($2FFA) copies exactly $12 bytes from here into the top of sprite $91CC's shape ($8A00 + n*64) for the plain battlefield cursor.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9EA4",
   "length": 63,
   "type": "sprite",
   "name": "cursorBoxSprite",
   "description": "One complete 24x21 sprite (21 rows of 3 bytes): an 11x9 hollow rectangle centred in the sprite. showCursorBoxSprite ($3306) copies it into sprite 2 with X counting $3E..0; it is the normal battlefield centre cursor.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9EE3",
   "length": 63,
   "type": "sprite",
   "name": "crosshairSprite",
   "description": "One complete 24x21 sprite: a diamond crosshair with a vertical stem above and below and horizontal ticks. showCrosshairSprite ($3302) copies it into sprite 2 by loading X with $7D (= $9EA4 + $3F).",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F22",
   "length": 21,
   "type": "sprite",
   "name": "comcenCrossMarkerShape",
   "description": "21 bytes = seven 3-byte sprite rows drawing a plus/cross marker. showComcenCrossMarker ($3383) copies it (LDY #$14, 21 bytes) into rows 7-13 of sprite 2 at $8A95 and colours it light red; overlay B calls it at $75B6 to mark the comcen on the strategic grid.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F37",
   "length": 10,
   "type": "byteTable",
   "name": "circleDxPtrLoTable",
   "description": "Low bytes of the ten dx point-list pointers used by drawCircleInSprite ($3239), indexed by radius-1: $9F69,$9F6F,$9F73,$9F79,$9F7F,$9F8B,$9F97,$9FA1,$9FAF,$9FBF. Read at $323D into zp_AD.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F41",
   "length": 10,
   "type": "byteTable",
   "name": "circleDxPtrHiTable",
   "description": "High bytes of the dx pointers, all $9F. Read at $3242 into zp_AE.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F4B",
   "length": 10,
   "type": "byteTable",
   "name": "circleDyPtrLoTable",
   "description": "Low bytes of the ten dy point-list pointers: $9F6C,$9F71,$9F76,$9F7C,$9F85,$9F91,$9F9C,$9FA8,$9FB7,$9FC8. Read at $3247 into zp_AF.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F55",
   "length": 10,
   "type": "byteTable",
   "name": "circleDyPtrHiTable",
   "description": "High bytes of the dy pointers, all $9F. Read at $324C into zp_B0.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F5F",
   "length": 10,
   "type": "byteTable",
   "name": "circleStepCountTable",
   "description": "Highest point index of each circle radius 1-10: 2,1,2,2,5,5,4,6,7,8. drawCircleInSprite loads it into circleStepsLeft ($91F2) at $3251 and counts down to 0, so radius 1 plots 3 points (a filled 3x3 dot) and radius 10 plots 9.",
   "unit": "game/data_9300"
  },
  {
   "addr": "9F69",
   "length": 104,
   "type": "byteTable",
   "name": "circlePointDeltaLists",
   "description": "The ten dx lists and ten dy lists themselves, interleaved so that each radius' dx list is immediately followed by its dy list (radius 1: dx $9F69 = 00,01,01 / dy $9F6C = 00,00,01; radius 10: dx $9FBF = 0A,0A,0A,09,09,08,07,07,08 / dy $9FC8 = 00,01,02,03,04,05,06,07,06). computeCircleOctantPoints reflects each (dx,dy) into the eight octants around the sprite centre (12,10).",
   "unit": "game/data_9300"
  },
  {
   "addr": "9FD1",
   "length": 47,
   "type": "unknown",
   "name": "graphicsBlockTail",
   "description": "47 bytes to the end of track 28 sector 12, not referenced by anything. Byte-for-byte a 50 percent dither: alternating $FF/$00 pairs whose phase flips every eight bytes, with a handful of stray values ($3F,$33,$C0,$F3,$30). The identical pattern appears in the tail of the $8800 block ($897F-$89FF, track 29 sector 10), so both are almost certainly the same stale bitmap fragment left in the mastering machine's memory. (confidence: medium)",
   "unit": "game/data_9300"
  },
  {
   "addr": "8800",
   "length": 28,
   "type": "text",
   "name": "msgGetOpponentOnPhoneAnd",
   "description": "Bit-7-terminated message text \"GET OPPONENT ON PHONE AND...\". Message slot $20 while a modem game is being set up: main $0B1D patches messagePtrLoTable/HiTable entry $20 to point here (A=$00, Y=$88) right before waitForAnswerOriginateKey.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "881C",
   "length": 27,
   "type": "text",
   "name": "msgPressAOrOAndSetModem",
   "description": "Bit-7-terminated message text \"PRESS A OR O AND SET MODEM.\". Message slot $21, patched next to it at $0B26 (A=$1C, Y=$88). Tells the player to press A(nswer) or O(riginate).",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "8837",
   "length": 23,
   "type": "text",
   "name": "msgPressSpaceWhenReady",
   "description": "Bit-7-terminated message text \"PRESS SPACE WHEN READY.\". Message slot $21 set by showModemSetupPrompt ($0C8C, called from overlay A $79E3), which puts the caller's string in slot $20 and this one in slot $21.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "884E",
   "length": 10,
   "type": "text",
   "name": "msgTrainerName",
   "description": "Bit-7-terminated message text \"TRAINER   \". Ten characters, read by exchangePlayerNamesAndSettings ($ED4A in the modem-driver build): in solo-trainer mode ($0BA5 bit 7) the whole name handshake is skipped and this string is copied into opponentName $FFE8 instead.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "8858",
   "length": 15,
   "type": "text",
   "name": "msgOpponentReady",
   "description": "Bit-7-terminated message text \"OPPONENT READY.\". Message id $04 (messagePtrLoTable[$04]=$58, HiTable[$04]=$88).",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "8867",
   "length": 23,
   "type": "text",
   "name": "msgWaitingForOpponent",
   "description": "Bit-7-terminated message text \"WAITING FOR OPPONENT...\". Message id $0B.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "887E",
   "length": 25,
   "type": "text",
   "name": "msgWaitingForConnection",
   "description": "Bit-7-terminated message text \"WAITING FOR CONNECTION...\". Message id $10.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "8897",
   "length": 31,
   "type": "text",
   "name": "msgPickUpPhoneThenPressSpace",
   "description": "Bit-7-terminated message text \"PICK UP PHONE THEN PRESS SPACE.\". Message id $17.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "88B6",
   "length": 32,
   "type": "text",
   "name": "msgPressSpaceWaitHangupPhone",
   "description": "Bit-7-terminated message text \"PRESS SPACE, WAIT, HANGUP PHONE.\". Message id $18.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "88D6",
   "length": 26,
   "type": "text",
   "name": "msgOpponentPickingGameMap",
   "description": "Bit-7-terminated message text \"OPPONENT PICKING GAME/MAP.\". Message id $1A - shown to player 1 while it waits for player 0's map seed.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "88F0",
   "length": 28,
   "type": "text",
   "name": "msgOpponentReadyForNewGame",
   "description": "Bit-7-terminated message text \"OPPONENT READY FOR NEW GAME.\". Message id $1F.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "895E",
   "length": 33,
   "type": "text",
   "name": "msgOpponentUsingNonStandardGame",
   "description": "Bit-7-terminated message text \"OPPONENT USING NON-STANDARD GAME.\". Shown when the program-checksum byte exchanged during the identity handshake does not match: overlay A stores the expected value $DB at $79CC, compares it after exchangePlayerNamesAndSettings and, on a mismatch, does LDA #$5E / LDY #$89 / JSR showModemSetupPrompt ($79DF-$79E3), which puts this string in message slot $20 and \"PRESS SPACE WHEN READY.\" ($8837) in slot $21. Because the address is built from two immediates the disassembler produced no reference, which is why XREF shows only trainer writes here.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "890C",
   "length": 32,
   "type": "byteTable",
   "name": "mapNameAlphabet",
   "description": "The 32-character alphabet ' ABCDEFGHIJKLMNOPQRSTUVWXYZ+-*/=' used to convert between a five-character map name and the 24-bit map seed. The map-name editor in overlay A searches it backwards from index $1F at $779B to validate a typed character, and packs the name at $7804 by shifting zp_5A/5B/5C left five times per character and ORing in the index, so name = seed in base 32 (5 chars x 5 bits). $785D uses the same table to print a seed back as a name. This is also what makes the copy-protection work: the 32 x 3-byte table at $0401 holds seed triples, and the player types the matching name out of the manual.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "892C",
   "length": 25,
   "type": "byteTable",
   "name": "gameTypeDefaultsSide0",
   "description": "Side 0 half of the game-type parameter defaults: five rows of five bytes, read through selectSideDefaultsTable ($7CAE) and applied by applyGameTypeDefaults ($7C2E). Rows are TERRAIN PTS (-> $92AC), RECYCLER (-> $92A6), COMCEN SPEED (-> $92B4), DRONES (-> $92AE) and MISSILES (-> $92B0); columns are the game type folded to 0-4 (0 SCRIMAGE, 1 QB SNEAK, 2 THE BOMB/FACE-OFF/SLUGGERS, 3 FULL WAR, 4 DEFENDER). Values: 01 00 01 01 01 / 02 02 02 01 02 / 02 03 02 01 00 / 00 00 03 05 02 / 00 08 0C 14 14, i.e. SCRIMAGE has no drones and no missiles while FULL WAR gives 5 drones and 20 missiles.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "8945",
   "length": 25,
   "type": "byteTable",
   "name": "gameTypeDefaultsSide1",
   "description": "Side 1 half of the same 5x5 table: 01 01 01 01 00 / 02 02 02 01 01 / 02 02 02 01 01 / 00 00 03 05 05 / 00 08 0C 14 08. The only asymmetric column is DEFENDER (4), where side 0 gets 2 drones and 20 missiles (the attacker) and side 1 gets 5 drones and 8 missiles (the defender). Note that the trainer AI's buildDirectionFan ($E7E3) later reads its eight compass offsets from $894A, which is the middle of this table and is never rewritten - see the insight below.",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "897F",
   "length": 129,
   "type": "unknown",
   "name": "messageBlockTail",
   "description": "129 unreferenced bytes to the end of track 29 sector 10. They are a 50 percent dither: pairs of $00/$FF whose phase flips every eight bytes, plus a few stray values ($03,$F3,$BF,$0C,$DC,$B3,$3F,$C0,$CC,$01,$DF). The tail of the $9300 graphics block ($9FD1-$9FFF, track 28 sector 12) contains the same pattern, so both look like the same stale bitmap fragment from the mastering machine rather than game data. (confidence: medium)",
   "unit": "game/ovl_8800"
  },
  {
   "addr": "F000",
   "length": 400,
   "type": "byteTable",
   "name": "unitStartTemplate0",
   "description": "Unit start-position template 0 (400 bytes = four parallel 100-entry arrays, one entry per unit 0-99): +0 unitCol (values 3-36), +100 unitRow (side 0 units 0-49 in rows 20-33, side 1 units 50-99 in rows 6-19), +200 unitFlags (bits 0-1 facing - 0 for side 0, 1 for side 1; bit 5 set on every side-1 unit; bit 6 = 'belongs to a group'; bits 2-4 = group id 0-7, so $40+4*g on side 0 and $61+4*g on side 1, and $00/$21 for ungrouped units), +300 unitType ($10+type for side 0 and $18+type for side 1, type 0 GRUNT .. 4 COMCEN, $FF = slot not in play). FULL WAR / DEFENDER roster: 28 GRUNT, 12 RIDER, 6 BOOMER, 3 SPY, 1 COMCEN per side (all 50 slots used). Groups 0-5 are populated (flags $40,$44,$48,$4C,$50,$54 on side 0), 22 units are ungrouped. loadUnitStartTemplate ($ECF3, in the comm-module tail) copies 512 bytes from $F000 + 400*n to $F640 once per session, where n = unitLayoutByGameType[$78B8 + gameType & 7] = 2,2,2,1,3,0,0.",
   "unit": "game/map_F000"
  },
  {
   "addr": "F190",
   "length": 400,
   "type": "byteTable",
   "name": "unitStartTemplate1",
   "description": "Unit start-position template 1 (400 bytes = four parallel 100-entry arrays, one entry per unit 0-99): +0 unitCol (values 3-36), +100 unitRow (side 0 units 0-49 in rows 20-33, side 1 units 50-99 in rows 6-19), +200 unitFlags (bits 0-1 facing - 0 for side 0, 1 for side 1; bit 5 set on every side-1 unit; bit 6 = 'belongs to a group'; bits 2-4 = group id 0-7, so $40+4*g on side 0 and $61+4*g on side 1, and $00/$21 for ungrouped units), +300 unitType ($10+type for side 0 and $18+type for side 1, type 0 GRUNT .. 4 COMCEN, $FF = slot not in play). FACE-OFF roster: 28 RIDER, 3 SPY, 1 COMCEN per side, 18 slots marked $FF (not in play). Groups 0-7 used. loadUnitStartTemplate ($ECF3, in the comm-module tail) copies 512 bytes from $F000 + 400*n to $F640 once per session, where n = unitLayoutByGameType[$78B8 + gameType & 7] = 2,2,2,1,3,0,0.",
   "unit": "game/map_F000"
  },
  {
   "addr": "F320",
   "length": 400,
   "type": "byteTable",
   "name": "unitStartTemplate2",
   "description": "Unit start-position template 2 (400 bytes = four parallel 100-entry arrays, one entry per unit 0-99): +0 unitCol (values 3-36), +100 unitRow (side 0 units 0-49 in rows 20-33, side 1 units 50-99 in rows 6-19), +200 unitFlags (bits 0-1 facing - 0 for side 0, 1 for side 1; bit 5 set on every side-1 unit; bit 6 = 'belongs to a group'; bits 2-4 = group id 0-7, so $40+4*g on side 0 and $61+4*g on side 1, and $00/$21 for ungrouped units), +300 unitType ($10+type for side 0 and $18+type for side 1, type 0 GRUNT .. 4 COMCEN, $FF = slot not in play). SCRIMAGE / QB SNEAK / THE BOMB roster: 5 GRUNT, 5 RIDER and 1 COMCEN per side, 39 slots $FF. No unit belongs to a group (flags $00 on side 0, $21 on side 1). applyGameTypeDefaults then removes units 0-6 for QB SNEAK and everything except the two comcens for THE BOMB. loadUnitStartTemplate ($ECF3, in the comm-module tail) copies 512 bytes from $F000 + 400*n to $F640 once per session, where n = unitLayoutByGameType[$78B8 + gameType & 7] = 2,2,2,1,3,0,0.",
   "unit": "game/map_F000"
  },
  {
   "addr": "F4B0",
   "length": 400,
   "type": "byteTable",
   "name": "unitStartTemplate3",
   "description": "Unit start-position template 3 (400 bytes = four parallel 100-entry arrays, one entry per unit 0-99): +0 unitCol (values 3-36), +100 unitRow (side 0 units 0-49 in rows 20-33, side 1 units 50-99 in rows 6-19), +200 unitFlags (bits 0-1 facing - 0 for side 0, 1 for side 1; bit 5 set on every side-1 unit; bit 6 = 'belongs to a group'; bits 2-4 = group id 0-7, so $40+4*g on side 0 and $61+4*g on side 1, and $00/$21 for ungrouped units), +300 unitType ($10+type for side 0 and $18+type for side 1, type 0 GRUNT .. 4 COMCEN, $FF = slot not in play). SLUGGERS roster: 12 GRUNT, 12 RIDER, 4 BOOMER, 3 SPY, 1 COMCEN per side, 18 slots $FF. Groups 0-7 used. loadUnitStartTemplate ($ECF3, in the comm-module tail) copies 512 bytes from $F000 + 400*n to $F640 once per session, where n = unitLayoutByGameType[$78B8 + gameType & 7] = 2,2,2,1,3,0,0.",
   "unit": "game/map_F000"
  },
  {
   "addr": "F640",
   "length": 192,
   "type": "unknown",
   "name": "mapBlockTail",
   "description": "The last 192 bytes of track 18 sectors 8-14 are not part of any template - they are a fragment of 6502 code left in the mastering machine's memory. Disassembled it is an EARLIER version of printOptionsList ($19F3 in the shipped build): it sets a text window at column $0C rows 9-$12 with colour $61, prints a string at $CAB2, tests $92CD and $0881, prints $6610, checks the link flag $E03B, prints $6698 and $CA53, then falls into a film-slot routine that writes $91D5/$91D6/$91D7, calls $C097, $5F41, $1205 and jumps to $1648. The shipped build keeps the same flags at $0B94-$0BA8 and the same strings at $CC.. instead of $0876../$66.., so this code is from a different (older) build. At run time the area is unitCol[0..99] ($F640) and unitRow[0..99] ($F6A4), and startGameSession deliberately stores $FF into $F640 before calling loadUnitStartTemplate so the copy always happens. (confidence: high that it is leftover code, medium on which build)",
   "unit": "game/map_F000"
  },
  {
   "addr": "0200",
   "length": 150,
   "type": "byteTable",
   "name": "trainerPlay0",
   "description": "Solo-trainer opening play 0: 150 bytes = three 50-entry arrays indexed by AI unit ordinal 0-49 (absolute unit 50-99). +0 group id per unit ($FF = no group, entry 49 = 0 = the AI comcen's own group), +50 destination column, +100 destination row (all in rows $06-$13, the AI's half). sub_EC91/$ECD4 in the trainer setup overlay computes the base as $0200 + 150*plan with multiply8x8, then copies the three slices to aiUnitGroup ($8832), unitDestCol+50 ($F802) and unitDestRow+50 ($F866) with Y counting $31..0. This play: groups 1-10 handed to units 0-9 individually (mainGroup $FF, 10 groups); columns spread from $21 down to $07 across the front, all ten at row $12. Its companion scalars come from mainGroupByPlanTable ($EC00 = FF,FF,04,04,04), groupCountByPlanTable ($EC05 = 0A,0A,04,04,04) and aiPlanParamTable ($EC0A = FF,FF,08,0C,0C).",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "0296",
   "length": 150,
   "type": "byteTable",
   "name": "trainerPlay1",
   "description": "Solo-trainer opening play 1: 150 bytes = three 50-entry arrays indexed by AI unit ordinal 0-49 (absolute unit 50-99). +0 group id per unit ($FF = no group, entry 49 = 0 = the AI comcen's own group), +50 destination column, +100 destination row (all in rows $06-$13, the AI's half). sub_EC91/$ECD4 in the trainer setup overlay computes the base as $0200 + 150*plan with multiply8x8, then copies the three slices to aiUnitGroup ($8832), unitDestCol+50 ($F802) and unitDestRow+50 ($F866) with Y counting $31..0. This play: same ten single-unit groups but numbered the other way round (10..1); columns evenly spaced $24,$1C,$18,$14,$10,$0C,$04 with two units held back at row $12. Its companion scalars come from mainGroupByPlanTable ($EC00 = FF,FF,04,04,04), groupCountByPlanTable ($EC05 = 0A,0A,04,04,04) and aiPlanParamTable ($EC0A = FF,FF,08,0C,0C).",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "032C",
   "length": 150,
   "type": "byteTable",
   "name": "trainerPlay2",
   "description": "Solo-trainer opening play 2: 150 bytes = three 50-entry arrays indexed by AI unit ordinal 0-49 (absolute unit 50-99). +0 group id per unit ($FF = no group, entry 49 = 0 = the AI comcen's own group), +50 destination column, +100 destination row (all in rows $06-$13, the AI's half). sub_EC91/$ECD4 in the trainer setup overlay computes the base as $0200 + 150*plan with multiply8x8, then copies the three slices to aiUnitGroup ($8832), unitDestCol+50 ($F802) and unitDestRow+50 ($F866) with Y counting $31..0. This play: four real groups (1,2,3 and the assault group 4) covering units 0-27, a wedge of pairs at rows $0F-$11. Its companion scalars come from mainGroupByPlanTable ($EC00 = FF,FF,04,04,04), groupCountByPlanTable ($EC05 = 0A,0A,04,04,04) and aiPlanParamTable ($EC0A = FF,FF,08,0C,0C).",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "03C2",
   "length": 150,
   "type": "byteTable",
   "name": "trainerPlay3",
   "description": "Solo-trainer opening play 3: 150 bytes = three 50-entry arrays indexed by AI unit ordinal 0-49 (absolute unit 50-99). +0 group id per unit ($FF = no group, entry 49 = 0 = the AI comcen's own group), +50 destination column, +100 destination row (all in rows $06-$13, the AI's half). sub_EC91/$ECD4 in the trainer setup overlay computes the base as $0200 + 150*plan with multiply8x8, then copies the three slices to aiUnitGroup ($8832), unitDestCol+50 ($F802) and unitDestRow+50 ($F866) with Y counting $31..0. This play: same four groups with group 4 leading (units 0-11 and 24-27), rows $0C-$10. Its companion scalars come from mainGroupByPlanTable ($EC00 = FF,FF,04,04,04), groupCountByPlanTable ($EC05 = 0A,0A,04,04,04) and aiPlanParamTable ($EC0A = FF,FF,08,0C,0C).",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "0458",
   "length": 150,
   "type": "byteTable",
   "name": "trainerPlay4",
   "description": "Solo-trainer opening play 4: 150 bytes = three 50-entry arrays indexed by AI unit ordinal 0-49 (absolute unit 50-99). +0 group id per unit ($FF = no group, entry 49 = 0 = the AI comcen's own group), +50 destination column, +100 destination row (all in rows $06-$13, the AI's half). sub_EC91/$ECD4 in the trainer setup overlay computes the base as $0200 + 150*plan with multiply8x8, then copies the three slices to aiUnitGroup ($8832), unitDestCol+50 ($F802) and unitDestRow+50 ($F866) with Y counting $31..0. This play: the default plan: group 4 takes units 0-27 and 40-45, groups 1/2/3 the flanks; the widest spread of destinations (rows $07-$10). Its companion scalars come from mainGroupByPlanTable ($EC00 = FF,FF,04,04,04), groupCountByPlanTable ($EC05 = 0A,0A,04,04,04) and aiPlanParamTable ($EC0A = FF,FF,08,0C,0C).",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "04EE",
   "length": 18,
   "type": "unknown",
   "name": "playbookTail",
   "description": "18 bytes of filler after the fifth play (5 x 150 = 750 bytes end at $04ED). Content: $20,$20, then $06,$01,$13,$14,$0C,$0F,$01,$04, then eight more $20 - that is ' FASTLOAD' followed by spaces in C64 SCREEN codes, so it is a scrap of the mastering machine's screen memory, not game data. (confidence: medium)",
   "unit": "game/setup_0200_default"
  },
  {
   "addr": "0400",
   "length": 1,
   "type": "byteTable",
   "name": "checksumXorConstant",
   "description": "$B0. The constant verifyProgramChecksum ($0461) EORs into the final sum before storing it in programChecksum $0B7E. It sits one byte below the start of the checksummed range ($0402), so it can never influence itself.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0401",
   "length": 96,
   "type": "byteTable",
   "name": "mapNameSeedTable",
   "description": "32 entries of 3 bytes for the manual look-up protection at $09A6: randomSeed ($09C3) AND $1F picks an entry, the triple is copied to $0B95-$0B97 and used as the 24-bit map seed, the generated map is shown with 'IDENTIFY THIS MAP (SEE MANUAL)' ($0A48) and the five-character name the player types is packed through mapNameAlphabet ($890C) into zp_5A-zp_5C and compared with the same triple. Three failures wipe memory ($0A1E). Note $0401 (entry 0's first byte) is the only table byte outside the checksummed range.",
   "unit": "game/table_0400"
  },
  {
   "addr": "04AE",
   "length": 82,
   "type": "unknown",
   "name": "scratchPageTail",
   "description": "$00 on disk. At run time this is shared scratch: the 2x2 tile buffer $04C8-$04E7 filled by the terrain drawing code at $279F/$27CD, statistics accumulators, and the map generator's blob stacks. The trainer's playbook load (track 29 s6-8 -> $0200-$04FF) also overwrites it.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0500",
   "length": 36,
   "type": "byteTable",
   "name": "messagePtrLoTable",
   "description": "Low bytes of the 36 status-message pointers, indexed by message id $00-$23; read by the message system at $C536/$C5E5 and patched at run time by setMessageSlot ($C924). Ids $1E, $20 and $21 are $FFFF placeholders on disk: $20/$21 are filled with $8800/$881C by mainProgram $0B1D/$0B26, with $895E/$8837 by showModemSetupPrompt ($0C8C), and with the modem driver's own $EFxx strings by $EED6/$EEFF.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0524",
   "length": 36,
   "type": "byteTable",
   "name": "messagePtrHiTable",
   "description": "High bytes of the same 36 pointers. $05/$06 = strings in this block, $87 = strings in the $6F00 overlay variant B, $88 = strings in the loaded $8800 message block, $FF = unset placeholder.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0548",
   "length": 36,
   "type": "text",
   "name": "chatMessageLine",
   "description": "Message id $00, the chat / typed line. $0548 is a leading marker glyph ($1E = cursor, or $9E written by sub_C62A to terminate at once = blank line) and $0549-$056B hold up to 35 characters mirrored from $9253 with bit 7 set on the last. On disk it is 35 spaces followed by $A0.",
   "unit": "game/table_0400"
  },
  {
   "addr": "056C",
   "length": 10,
   "type": "text",
   "name": "msgWorking",
   "description": "Message id $01 \"WORKING...\" (bit-7 terminated), sound $80 = silent.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0576",
   "length": 13,
   "type": "text",
   "name": "msgGameAborted",
   "description": "Message id $03 \"GAME ABORTED.\", sound $03.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0583",
   "length": 16,
   "type": "text",
   "name": "msgNotAGameFilm",
   "description": "Message id $09 \"NOT A GAME FILM!\", sound $03 - a film load was rejected.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0593",
   "length": 33,
   "type": "text",
   "name": "msgSetupUnitsPressF1WhenDone",
   "description": "Message id $0A \"SETUP UNITS. PRESS F 1 WHEN DONE.\", silent - the unit setup phase prompt.",
   "unit": "game/table_0400"
  },
  {
   "addr": "05B4",
   "length": 28,
   "type": "text",
   "name": "msgTimeoutRunStopToResume",
   "description": "Message id $19 \"TIMEOUT. RUN/STOP TO RESUME.\", sound $19.",
   "unit": "game/table_0400"
  },
  {
   "addr": "05D0",
   "length": 7,
   "type": "text",
   "name": "msgResumed",
   "description": "Message id $1B \"RESUMED\", sound $19.",
   "unit": "game/table_0400"
  },
  {
   "addr": "05D7",
   "length": 18,
   "type": "text",
   "name": "msgTheBattleBegins",
   "description": "Message id $1C \"THE BATTLE BEGINS!\", sound $03.",
   "unit": "game/table_0400"
  },
  {
   "addr": "05E9",
   "length": 17,
   "type": "text",
   "name": "msgLastQtrWarning",
   "description": "Message id $1D \"LAST QTR WARNING!\", sound $19 - the game is played in quarters.",
   "unit": "game/table_0400"
  },
  {
   "addr": "05FA",
   "length": 14,
   "type": "text",
   "name": "msgPhoneTrouble",
   "description": "Message id $22 \"PHONE TROUBLE.\", sound $15 - modem link failure.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0608",
   "length": 10,
   "type": "text",
   "name": "msgTryAgain",
   "description": "Message id $23 \"TRY AGAIN.\", sound $03.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0612",
   "length": 36,
   "type": "byteTable",
   "name": "messageSoundTable",
   "description": "Sound effect number played when message id X is shown (read at $C557, played through sub_C89A); $80 = silent. $0E for the two 'we lost' messages, $0D for the two 'we won' messages, $19 for GAME OVER / TIMEOUT / RESUMED / LAST QTR WARNING, $15 for PHONE TROUBLE, $06 for the two phone prompts.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0636",
   "length": 23,
   "type": "text",
   "name": "optionSummaryTemplate",
   "description": "\" MENUS XXXX DAMAGE XXX.\" (bit-7 terminated) - a rules summary line whose XXXX and XXX fields are overwritten in place by the $6F00 overlays ($870C, $8739-$8751) with OFF,/ON, and FULL/HALF/NONE before it is printed.",
   "unit": "game/table_0400"
  },
  {
   "addr": "064D",
   "length": 17,
   "type": "text",
   "name": "optionWordTable",
   "description": "Five bit-7 terminated words at fixed offsets, copied into optionSummaryTemplate: +0 \"OFF\", +3 \"ON\", +5 \"FULL\", +9 \"HALF\", +13 \"NONE\". XREF.txt calls this label eaCopyrightMessage, which is wrong (that name belongs to the boot-time `ea` file that once occupied $02A8-$030B).",
   "unit": "game/table_0400"
  },
  {
   "addr": "065E",
   "length": 10,
   "type": "text",
   "name": "msgClicks",
   "description": "\"   CLICKS:\" - the score header printed by $C6BC (LDA #$5E / LDY #$06 / JSR $C133). \"Clicks\" are Modem Wars' score unit.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0668",
   "length": 7,
   "type": "text",
   "name": "msgUs",
   "description": "\"    US:\" - printed straight after msgClicks, followed by the local player's 16-bit score from $92A2/$92A4.",
   "unit": "game/table_0400"
  },
  {
   "addr": "066F",
   "length": 8,
   "type": "text",
   "name": "msgThem",
   "description": "\"   THEM:\" - the opponent's score label, printed by the same $C6B6 routine.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0677",
   "length": 40,
   "type": "text",
   "name": "msgTypeMapName",
   "description": "Three lines separated by $0D: \"TYPE MAP NAME\" / \" (F7 RANDOM)\" / \" (F5 SYM/RND)\" - the prompt of the map name editor in overlay A, also used by the manual look-up protection at $09EF.",
   "unit": "game/table_0400"
  },
  {
   "addr": "069F",
   "length": 27,
   "type": "text",
   "name": "msgBtnToAccept",
   "description": "Two lines separated by $0D: \"BTN TO ACCEPT\" / \"(F 1 TO FLIP)\" - shown while the generated map is offered for acceptance.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0770",
   "length": 40,
   "type": "byteTable",
   "name": "tileVariantMaskTable",
   "description": "Indexed by map cell value minus $40 (cells below $40 use cell-$20), covering cells $40-$67. The value is ANDed with a scenario random number and added to the base tile by randomTileVariant ($7872, overlay A): cells $49, $4B and $65 -> mask 1; cells $4D, $51, $59, $5D, $61 -> mask 3; everything else 0.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0798",
   "length": 1,
   "type": "unknown",
   "name": "tileVariantPad",
   "description": "$00 pad between tileVariantMaskTable and unitGlyphBaseTable.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0799",
   "length": 6,
   "type": "byteTable",
   "name": "unitGlyphBaseTable",
   "description": "Base glyph index of the one-character overview symbol per unit type: $2C GRUNT, $30 RIDER, $34 BOOMER, $38 SPY, $3C COMCEN (plus one $00 pad). The facing (unitFlags AND 3) is added; the result indexes the 8x8 shapes at $9C6E + code*8, and, doubled after subtracting $2C, the 16x16 glyph pointer tables ($28A0, $6C35, $7512, $7538).",
   "unit": "game/table_0400"
  },
  {
   "addr": "079F",
   "length": 75,
   "type": "byteTable",
   "name": "terrainClassTable",
   "description": "Map cell value -> terrain class 0-8. Addressed as $077E,Y with the raw cell value, so the body runs from $077E+$21 = $079F to $077E+$6B = $07E9: cells $21-$34 -> 3 (lower contour), $35-$3F -> $FF (invalid), $40 -> 0 (blank), $41-$54 -> 5 (upper contour), $55-$5C -> 1 (river), $5D-$60 -> 2 (forest), $61-$64 -> 4 (lower hill), $65-$66 -> 6 (summit), $67 -> 7 (recycler), $68-$6B -> 8 (end zone). The same bytes are read as $07BE,X by $5BC8 with the index already reduced to cell-$40. The trainer also reads it as terrainClassTable $077E from $E773.",
   "unit": "game/table_0400"
  },
  {
   "addr": "07EA",
   "length": 9,
   "type": "byteTable",
   "name": "terrainColourTable",
   "description": "Colour-RAM nibble per terrain class 0-8: 7,3,5,8,8,8,8,0,0 (yellow plain, cyan river, green forest, orange for every contour/hill/summit class, black for classes 7-8). Read at $27AB and $5BCB immediately after a terrainClassTable lookup.",
   "unit": "game/table_0400"
  },
  {
   "addr": "07F3",
   "length": 13,
   "type": "unknown",
   "name": "table0400Tail",
   "description": "$00 filler to the end of the page; unreferenced.",
   "unit": "game/table_0400"
  },
  {
   "addr": "0200",
   "length": 256,
   "type": "byteTable",
   "name": "contourRuleTable",
   "description": "Terrain-contour rule table of the random map generator, read as contourRuleTable,Y at $7484 in overlay A. Y is an 8-bit mask of which of the eight neighbours of the current cell are higher terrain, bit order bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE (verified from the edge-clamp masks at $7463-$7481: $D6 at column 0, $6B at column 39, $F8 at row 0, $1F at row 39). Entry semantics: $00 (index 0 only) do nothing; $41-$51 a contour tile code to place, from which the generator subtracts contourTileOffset ($0324, 0 for the summit ring and $20 for the lower-hill ring) - $51 north edge, $4D south, $4B west, $49 east, $45 N+W corner, $46 N+E, $47 W+S, $48 E+S, $44/$43/$42/$41 the isolated NW/NE/SW/SE diagonals; $80+n (105 entries) randomly erase neighbour n instead; $91/$95/$97 (indexes $05, $A0, $84) erase that neighbour or else fill; $FF (93 entries) fill the cell with the interior tile. Loaded from track 18 sector 15 by loadSetupBlock ($10BB) / loadMapAndSetup_10AB and never written back to disk. Only valid between $0B35 and $7AE9, after which the trainer playbook (track 29 s6-8) overwrites the page.",
   "unit": "game/setup_0200"
  },
  {
   "addr": "0389",
   "length": 119,
   "type": "unknown",
   "name": "setupBlockPadding",
   "description": "$0389-$03FF, all zero on disk: the tail of track 18 sector 16 after the one-shot start-up code at $0300-$0388. Not referenced as code or data. At run time the page is reused for the film directory, the RLE sprite-shape buffer, overlay A's map-generator variables and the trainer playbook.",
   "unit": "game/setup_0200"
  },
  {
   "addr": "FC50",
   "length": 4,
   "type": "byteTable",
   "name": "multicolorFillPatterns",
   "description": "$00,$55,$AA,$FF - colour index 0-3 replicated into all four multicolour pixel pairs of a bitmap byte. Read by plotBitmapPixelRow as $FC50,X with X = the colour index.",
   "unit": "game/high_FBB8"
  },
  {
   "addr": "FC54",
   "length": 4,
   "type": "byteTable",
   "name": "multicolorPixelMasks",
   "description": "$C0,$30,$0C,$03 - the bit mask of multicolour pixel pair (X AND 3) inside a bitmap byte. ANDed with the fill pattern and merged into the byte by plotBitmapPixelRow.",
   "unit": "game/high_FBB8"
  },
  {
   "addr": "FD70",
   "length": 180,
   "type": "sprite",
   "name": "missileCursorShapesRle",
   "description": "RLE-compressed set of eight 24x21 sprites, first byte $01 = the escape marker. runMissileScreen ($7587 in the $6F00 overlay variant B) does LDA #$70 / LDY #$FD / JSR unpackRleTo0200 ($67CB), which expands it to exactly 512 bytes at $0200-$03FF = eight shapes on a 64-byte stride; loadDirectionShapeSprite ($3340) then copies shape number ($920B) from $0200 + 64*n into sprite 0 ($8A00) or sprite 5 ($8B40). The eight shapes are a small arrow/missile marker in the eight compass headings in the order N, NE, E, SE, S, SW, W, NW.",
   "unit": "game/high_FBB8"
  },
  {
   "addr": "FE24",
   "length": 367,
   "type": "sprite",
   "name": "droneShapesRle",
   "description": "RLE-compressed set of eight 24x21 sprites, first byte $05 = the escape marker. enterDroneScreen ($7FE6, overlay variant B) does LDA #$24 / LDY #$FE / JSR unpackRleTo0200, giving eight 64-byte shapes at $0200-$03FF that loadDirectionShapeSprite copies into the sprite data. The shapes are a top-down aeroplane silhouette (the drone) in the eight compass headings N, NE, E, SE, S, SW, W, NW. The stream ends with the run $05,$0E,$00 at $FF90-$FF92, which pads the buffer out to 512 bytes.",
   "unit": "game/high_FBB8"
  },
  {
   "addr": "FF93",
   "length": 63,
   "type": "sprite",
   "name": "targetingReticleSprite",
   "description": "One complete uncompressed 24x21 sprite: four corner brackets with a centre cross - the drone screen's targeting reticle. Copied into sprite 2 by overlay B at $80EB (LDA $FF93,Y) when the drone screen is built, so the drone starts over the comcen with this frame drawn around it. It ends exactly at $FFD1, the last byte of the relocated high-memory block.",
   "unit": "game/high_FBB8"
  },
  {
   "addr": "1DA2",
   "length": 66,
   "type": "byteTable",
   "name": "scrollViewParamTables",
   "description": "Twenty-two parallel 3-byte tables (one entry per direction: 0 = -1, 1 = 0, 2 = +1) that parameterise scrollViewBitmap ($1DE4), which shifts the 7x5-cell tactical view one cell without redrawing it. In order: $1DA2 bitmap copy start index ($5F,$00,$00), $1DA5 end index ($FF,$70,$60), $1DA8 the DEY/INY opcode patched into both copy loops ($88,$C8,$C8), $1DAB/$1DAE bitmap source and destination byte offsets (0,0,$10 / $10,0,0), $1DB1/$1DB4 bitmap source address lo/hi ($AAC8,$A208,$A488), $1DB7/$1DBA bitmap destination lo/hi ($AD48,$A208,$A208), $1DBD/$1DC0 bitmap row stride lo/hi ($FEC0 = -320 up, $0140 = +320), $1DC3 rows to copy (8,10,8), $1DC6/$1DC9 screen copy start/end index, $1DCC/$1DCF screen source/destination column offsets, $1DD2/$1DD5 screen source lo/hi ($8D59,$8C41,$8C91), $1DD8/$1DDB screen destination lo/hi ($8DA9,$8C41,$8C41) and $1DDE/$1DE1 screen row stride lo/hi ($FFD8 = -40 up, $0028 = +40). Colour RAM is handled by adding $4C to the screen high byte.",
   "unit": "game/main_0800"
  },
  {
   "addr": "2BDD",
   "length": 27,
   "type": "byteTable",
   "name": "windowLeftTable",
   "description": "Left screen column of the 27 predefined windows 0-26, read by drawWindowFrame with X = window index. 0 = the overview panel (column 0), 1 = the tactical view (column $18), 2 = the status panel, 5-$16 the $6F00 overlay screens, $18/$19 the main menu frames.",
   "unit": "game/main_0800"
  },
  {
   "addr": "2BF8",
   "length": 27,
   "type": "byteTable",
   "name": "windowTopTable",
   "description": "Top screen row of windows 0-26.",
   "unit": "game/main_0800"
  },
  {
   "addr": "2C13",
   "length": 27,
   "type": "byteTable",
   "name": "windowWidthTable",
   "description": "Width in character cells including the frame: window 0 is $16 = 22 wide (the 20x20 overview map plus its frame), window 1 is $10 = 16 (the 7x5-cell tactical view at 2 cells per map cell plus frame), window $18 is the full 40 columns.",
   "unit": "game/main_0800"
  },
  {
   "addr": "2C2E",
   "length": 27,
   "type": "byteTable",
   "name": "windowHeightTable",
   "description": "Height in character cells of windows 0-26 (window 0 = $16 = 22, window 1 = $0C = 12).",
   "unit": "game/main_0800"
  },
  {
   "addr": "2C49",
   "length": 54,
   "type": "addrTable",
   "name": "windowFramePtrTable",
   "description": "27 little-endian pointers selecting each window's 9-byte frame tile set out of the ten sets at $9824-$987D in the graphics block. Windows 0-3 use the four decorated sets $985A/$9863/$986C/$9875, the rest reuse $9824 (thin box), $982D, $9836 (octagon), $983F (shadowed), $9848 (solid) and $9851 (blank/erase).",
   "unit": "game/main_0800"
  },
  {
   "addr": "5A59",
   "length": 33,
   "type": "byteTable",
   "name": "overviewPatternLo",
   "description": "Low bytes of the overview-map pattern pointer for terrain index $00-$20 (33 entries), read by buildOverviewQuadrant at $5C40 with Y = mapCell - $40, or mapCell - $20 when the cell is below $40, clamped below $21 ($5BC3). Index 0 (blank ground) -> $5A9B; indices 1-8 cycle through the four contour-edge patterns $5A9F/$5AA3/$5AA7/$5AAB; 9-$0C -> $5AAF; $0D-$14 -> $5AB3; $15-$18 (river cells $55-$58) -> $5AC7/$5ACB/$5ACF/$5AD3; $19-$1C -> $5AC3; $1D-$20 (forest cells $5D-$60) -> $5ABB.",
   "unit": "game/main_0800"
  },
  {
   "addr": "5A7A",
   "length": 33,
   "type": "byteTable",
   "name": "overviewPatternHi",
   "description": "High bytes of the same 33 pointers - all $5A, because every pattern lives in this block.",
   "unit": "game/main_0800"
  },
  {
   "addr": "5A9B",
   "length": 60,
   "type": "byteTable",
   "name": "overviewPatterns",
   "description": "Fifteen patterns of four bitmap scanline bytes each. The overview draws each 2x2 block of map cells as one 8x8 character, so a pattern carries both horizontal halves as nibble pairs ($00/$33/$CC/$FF) and the quadrant mask picks the half that belongs to the cell. Contents: $5A9B 00 00 00 00 (blank), $5A9F 00 00 33 33, $5AA3 00 CC 33 33, $5AA7 33 00 00 00, $5AAB 33 CC 00 00 (the four contour edges), $5AAF 33 33 33 33, $5AB3 00 FF 00 00, $5AB7 FF FF FF FF (a live unit), $5ABB 33 CC 33 CC (forest checker), $5ABF FF 00 FF 00 (a cloaked unit), $5AC3 CC FF FF 33, $5AC7 33 FF FF FF, $5ACB CC FF FF FF, $5ACF FF FF FF 33, $5AD3 FF FF FF CC (the four river blob quadrants). $5AB7 and $5ABF are the only two not reachable through overviewPatternLo/Hi - buildOverviewQuadrant uses them directly for units.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6782",
   "length": 5,
   "type": "byteTable",
   "name": "sidVoiceRegOffsetTable",
   "description": "SID register offset per voice, indexed by voice*2 so the driver can use the same X for its own state and for the chip: $00, (pad), $07, (pad), $0E.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6787",
   "length": 11,
   "type": "byteTable",
   "name": "sndCommandSlotTable",
   "description": "Sound-script command 0-10 -> byte offset of its state slot from sndVoiceState ($6792): 0 duration ($00, also ends processing), 1 control/waveform ($01), 2 loop counter A ($06), 3 loop counter B ($07), 4/5 spare ($0C/$0D), 6 frequency ($12, 2 bytes), 7 pulse width ($18, 2), 8 AD/SR ($1E, 2), 9 frequency delta ($24, 2), 10 pulse delta ($2A, 2). runSoundScript adds voice*2 to the entry and treats every slot from $12 up as 16-bit ($66DF CPY #$12).",
   "unit": "game/main_0800"
  },
  {
   "addr": "6792",
   "length": 48,
   "type": "byteTable",
   "name": "sndVoiceState",
   "description": "Work RAM, zero on disk: eight 6-byte rows of per-voice sound state (duration, control, loop counters, spares, frequency, pulse width, ADSR, and the two deltas), cleared per voice by startSound.",
   "unit": "game/main_0800"
  },
  {
   "addr": "67C2",
   "length": 6,
   "type": "byteTable",
   "name": "sndDriverVars",
   "description": "Work RAM, zero on disk: sndVolume, sndLastByte ($67C3, the current script byte), sndScratch, sndInitLock, sndCurrentSound, sndStartVoiceIndex.",
   "unit": "game/main_0800"
  },
  {
   "addr": "67C8",
   "length": 3,
   "type": "byteTable",
   "name": "sndVoiceSoundId",
   "description": "Work RAM, zero on disk: the sound id currently playing on each of the three voices (0 = free). $67CA is polled by the text engine at $C909/$C913 before it plays a UI beep.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6831",
   "length": 174,
   "type": "wordTable",
   "name": "soundPointerTable",
   "description": "29 sound effects x three little-endian voice-script addresses (6 bytes per sound, $0000 = the sound does not use that voice). Sound $00 points all three voices at the 60-tick silence script $68DF (stop everything) and $1A/$1B/$1C silence voice 0/1/2 individually. Multi-voice effects are $06 ($6980/$69C9/$68DF), $0D ($6AA7/$6AB5/$6AC3), $0E ($6ADC/$6AEC/$6AFC), $15 ($68E7/$68F0/$68DF) and $19 ($6BFB/$6C15/$6C1B). Sounds $01-$07 and $0C use voice 0, $08-$0B voice 1 and $0F-$18 voice 2. messageSoundTable ($0612) picks the sound played with each status message.",
   "unit": "game/main_0800"
  },
  {
   "addr": "68DF",
   "length": 854,
   "type": "byteTable",
   "name": "soundScriptData",
   "description": "The bytecode for all the voice scripts, in the format interpreted by runSoundScript ($66C7): a byte $00-$0A stores the following one or two bytes into the state slot named by sndCommandSlotTable (slot 0 = duration ends the frame, duration 0 ends the voice); $40-$7F is a loop whose low six bits select a counter slot and which is followed by a 16-bit target address; $80-$FF adds the scenario random byte zp_5A to the frequency low byte (noise jitter). $68DF is the shared 60-tick silence script and $68E3 ('01 00 00 00') is the common end-of-sound target that the many '46 E3 68' jumps branch to.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6C35",
   "length": 40,
   "type": "addrTable",
   "name": "unitGlyphPointerTable",
   "description": "20 little-endian pointers to the 32-byte unit glyphs that follow, indexed by unitType*4 + facing - the caller computes (unitFlags AND 3) + unitGlyphBaseTable[$0799 + unitType] - $2C and doubles it.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6C5D",
   "length": 640,
   "type": "charset",
   "name": "unitGlyphShapes",
   "description": "20 multicolour unit glyphs of 16x16 pixels, 32 bytes each, stored as four 8-byte character cells in the order top-left, top-right, bottom-left, bottom-right. Every lit pixel is the bit pair %11, so the byte values are combinations of $00/$03/$0C/$30/$C0/$33/$3C/$F0/$FC/$FF. The 20 shapes are five unit types x four facings, all clearly recognisable when rendered: 0-3 GRUNT (a stubby figure with a head, up/down/left/right), 4-7 RIDER (a mounted figure with a wide base), 8-11 BOOMER (a heavy block with a protruding barrel), 12-15 SPY (a small chevron/arrow), 16-19 COMCEN (a large domed block). These are the big glyphs used in the 16x16-per-cell tactical view; the single-cell versions are the 8x8 shapes at $9DCE.",
   "unit": "game/main_0800"
  },
  {
   "addr": "6EDD",
   "length": 35,
   "type": "unknown",
   "name": "mainProgramPadding",
   "description": "$6EDD-$6EFF, all zero: filler to the end of the main program image (track 22-27). Unreferenced.",
   "unit": "game/main_0800"
  }
 ],
 "misclassified": [],
 "insights": [
  "[game/data_9300] The block loaded from track 28 sectors 0-12 is the game's entire static graphics library, laid out with no gaps at all: six address tables ($9300-$93B3), fontColorTable, gameFont, one unreferenced 90-byte glyph-code block, the tall-digit pairs, the window frame sets, the ASCII map, the two 59-glyph console fonts, 44 terrain tiles + 20 unit symbols, five sprite shapes, the circle geometry, and 47 bytes of stale dither at the very end.",
  "[game/data_9300] Cell addressing for the whole game comes from here: calcCellPointersXY ($2A9F/$2AAC) builds zp_B4/B5 = $A000 + 320*row + 8*col from bitmapRowLo/Hi + bitmapColLo/Hi and zp_B6/B7 = $8C00 + 40*row + col from screenRowLo/Hi; colour RAM is the same pointer with $4C added to the high byte.",
  "[game/data_9300] There are two independent character sets in this one block. gameFont ($9426, 114 glyphs, colours in $93B4) is a tile/frame set used by the main program's plotGlyph; the console text plotter in the $C000 overlay uses the 59-glyph pair at $98BE (multicolour) and $9A96 (hires), which the boot-time routine invertTextFonts ($031B) EORs with $FF so the shipped disk holds them as positives.",
  "[game/data_9300] asciiToGlyphTable is addressed from two different bases: $987E with (char - $20) by printChar, and $985E with the raw character code by the chat-line editor at $C80A. Entry value $3B means 'this character has no glyph' and is how the editor rejects a keystroke.",
  "[game/data_9300] Terrain tiles and unit symbols share one 8-bytes-per-shape table at $9C6E: indices $00-$2B are the 44 terrain tiles (mapCell - $40, or mapCell - $20 below $40, so both contour rings draw the same art) and $2C-$3F are the 20 unit symbols (unitGlyphBaseTable $0799 + facing). That is why overlay B's radar routine can plot units and terrain through the same setTerrainTilePtr call.",
  "[game/data_9300] drawCircleInSprite is a full 8-way-symmetric Bresenham circle stored as data: 4 pointer tables + a step count + 20 short delta lists ($9F37-$9FD0) give radii 1-10, which the game uses for unit range rings and the blob markers on the drone/missile screens.",
  "[game/data_9300] $97B6-$980F (90 bytes of gameFont codes) is dead data - proven by the absence of any label, any immediate $97/$98 and any pointer into the range in the whole 64K image.",
  "[game/ovl_8800] $8800 \"GET OPPONENT ON PHONE AND...\"",
  "[game/ovl_8800] $881C \"PRESS A OR O AND SET MODEM.\"",
  "[game/ovl_8800] $8837 \"PRESS SPACE WHEN READY.\"",
  "[game/ovl_8800] $884E \"TRAINER   \" (ten characters, the solo opponent's name)",
  "[game/ovl_8800] $8858 \"OPPONENT READY.\"",
  "[game/ovl_8800] $8867 \"WAITING FOR OPPONENT...\"",
  "[game/ovl_8800] $887E \"WAITING FOR CONNECTION...\"",
  "[game/ovl_8800] $8897 \"PICK UP PHONE THEN PRESS SPACE.\"",
  "[game/ovl_8800] $88B6 \"PRESS SPACE, WAIT, HANGUP PHONE.\"",
  "[game/ovl_8800] $88D6 \"OPPONENT PICKING GAME/MAP.\"",
  "[game/ovl_8800] $88F0 \"OPPONENT READY FOR NEW GAME.\"",
  "[game/ovl_8800] $895E \"OPPONENT USING NON-STANDARD GAME.\"",
  "[game/ovl_8800] The 512-byte block from track 29 sectors 9-10 is only about half message strings ($8800-$890B and $895E-$897E, 12 strings, 316 bytes). The middle holds two real tables the strings have nothing to do with - the 32-character map-name alphabet at $890C and the two 5x5 game-type default tables at $892C/$8945 - and the last 129 bytes are stale dither.",
  "[game/ovl_8800] Message ids resolve through the pointer tables at $0500/$0524: ids $04, $0B, $10, $17, $18, $1A and $1F point into this block; ids $20/$21 are $FFFF on disk and are patched at run time, to $8800/$881C by mainProgram $0B1D/$0B26 and later to the modem driver's own strings at $EFxx by $EED6/$EEFF.",
  "[game/ovl_8800] The map name really is the map seed: the five typed characters are packed 5 bits each (index into the $890C alphabet) into zp_5A/5B/5C, which is exactly the 24-bit seed generateBattlefieldMap uses, and exactly what the 32 x 3-byte manual-protection table at $0401 stores.",
  "[game/ovl_8800] Confirms the original bug already suspected in game_ovl_E000_T35_E000: the trainer's buildDirectionFan reads eight direction offsets from $894A, which is row 1 columns 0-4 plus row 2 columns 0-2 of gameTypeDefaultsSide1 (02 02 02 01 01 02 02 02). Nothing ever writes that area, so on a real machine the AI's eight 'direction ranks' are always base+2 or base+1.",
  "[game/map_F000] Track 18 sectors 8-14 = four canned starting deployments of 400 bytes each at $F000, $F190, $F320, $F4B0, plus 192 bytes of leftover. Each is four 100-entry arrays: unitCol, unitRow, unitFlags, unitType - exactly the first four of the fourteen 100-byte unit-record arrays that live at $F640, $F6A4, $F708 and $F76C at run time.",
  "[game/map_F000] unitLayoutByGameType ($78B8 in overlay A) maps the seven game types to these templates: SCRIMAGE, QB SNEAK and THE BOMB -> template 2 (11 units a side), FACE-OFF -> template 1 (all riders), SLUGGERS -> template 3, FULL WAR and DEFENDER -> template 0 (the full 50-unit roster 28/12/6/3/1).",
  "[game/map_F000] The standard full roster is 28 GRUNT + 12 RIDER + 6 BOOMER + 3 SPY + 1 COMCEN = 50 units per side, and unit 49 / unit 99 really are the two command centres in the shipped data.",
  "[game/map_F000] unitFlags encoding read straight off the templates: bits 0-1 facing, bit 5 set for every side-1 unit, bit 6 'is in a group', bits 2-4 the group id 0-7 (matching the seven group names ALPHA..GROVER at $C92C). Side 0 values are $40+4*g and side 1 values $61+4*g, with $00/$21 for ungrouped units.",
  "[game/map_F000] The templates place side 0 (units 0-49) in map rows 20-33 and side 1 (units 50-99) in rows 6-19, i.e. each side starts inside its own half of the 40x40 field, and columns span 3-36.",
  "[game/map_F000] The 192-byte tail at $F640 is a fragment of an older build of the game's own options-menu code (an earlier printOptionsList that uses flags at $0876-$0881 and strings at $6610/$6698 instead of the shipped $0B94-$0BA8 / $CCxx), which dates this disk block to a snapshot taken on the development machine.",
  "[game/map_F000] Nothing on the disk is a battlefield map: the map is always generated from a 24-bit seed by generateBattlefieldMap, which overwrites $F000-$F63F before the first frame.",
  "[game/setup_0200_default] The playbook is five 150-byte plays at $0200, $0296, $032C, $03C2 and $0458, each [groupId x50] [destCol x50] [destRow x50]; the trainer's default entry point sub_EC91 always starts with plan index 4 and picks a random 1-3 for $8964, while $EC52/$EC6F can re-enter at L_EC93 with another plan.",
  "[game/setup_0200_default] Only the first 10-28 units of each play are really play-specific: from about ordinal 14 onwards every play shares the same tail of destination columns (21 13 04 24 14 23 22 21 20 15 05 14 05 14 22 21 14 13 14 22 21 05 05 06 06 13 13 23 21 15 04 06 22 14 05 14) and rows, so a 'play' is really an opening for the leading formations laid over a fixed default deployment.",
  "[game/setup_0200_default] That shared tail also proves the relationship with track 18 sectors 8-14: the identical 36-byte run appears at $F360 inside unit start-position template 2, and a 50-byte run from play 3 appears at $F4E2 inside template 3.",
  "[game/setup_0200_default] Plans 0 and 1 have mainGroupByPlanTable = $FF (no assault group) and 10 groups; plans 2-4 use assault group 4 and 4 groups. Entry 49 of every group array is 0, i.e. the AI comcen is always in group 0.",
  "[game/setup_0200_default] The 18 spare bytes at the end of the block spell ' FASTLOAD' in C64 screen codes - a scrap of the mastering machine's screen RAM.",
  "[game/table_0400] $0548 chat line placeholder (35 spaces)",
  "[game/table_0400] $056C \"WORKING...\"",
  "[game/table_0400] $0576 \"GAME ABORTED.\"",
  "[game/table_0400] $0583 \"NOT A GAME FILM!\"",
  "[game/table_0400] $0593 \"SETUP UNITS. PRESS F 1 WHEN DONE.\"",
  "[game/table_0400] $05B4 \"TIMEOUT. RUN/STOP TO RESUME.\"",
  "[game/table_0400] $05D0 \"RESUMED\"",
  "[game/table_0400] $05D7 \"THE BATTLE BEGINS!\"",
  "[game/table_0400] $05E9 \"LAST QTR WARNING!\"",
  "[game/table_0400] $05FA \"PHONE TROUBLE.\"",
  "[game/table_0400] $0608 \"TRY AGAIN.\"",
  "[game/table_0400] $0636 \" MENUS XXXX DAMAGE XXX.\" (template, fields overwritten in place)",
  "[game/table_0400] $064D \"OFF\" / \"ON\" / \"FULL\" / \"HALF\" / \"NONE\" (five words at +0/+3/+5/+9/+13)",
  "[game/table_0400] $065E \"   CLICKS:\"",
  "[game/table_0400] $0668 \"    US:\"",
  "[game/table_0400] $066F \"   THEM:\"",
  "[game/table_0400] $0677 \"TYPE MAP NAME\" CR \" (F7 RANDOM)\" CR \" (F5 SYM/RND)\"",
  "[game/table_0400] $069F \"BTN TO ACCEPT\" CR \"(F 1 TO FLIP)\"",
  "[game/table_0400] Complete message id map, decoded from messagePtrLoTable/HiTable + messageSoundTable: $00 chat line ($0548, silent), $01 WORKING... ($056C, silent), $02 END OF FILM. ($8707, snd 3), $03 GAME ABORTED. ($0576, 3), $04 OPPONENT READY. ($8858, 3), $05 UNIT RECYCLED. ($8713, 4), $06/$07 SHOW ****/SPEED * ($8721/$872A, 3), $08 SPOTTED RECYCLER! ($8731, 4), $09 NOT A GAME FILM! ($0583, 3), $0A SETUP UNITS. PRESS F 1 WHEN DONE. ($0593, silent), $0B WAITING FOR OPPONENT... ($8867, 3), $0C COMCEN STUNNED! ($8742, 4), $0D SPOTTED ENEMY COMCEN! ($8751, 4), $0E SPY UNDER ATTACK! ($8766, 4), $0F SPOTTED ENEMY SPY! ($8777, 4), $10 WAITING FOR CONNECTION... ($887E, silent), $11 WE LOST ON POINTS! ($8789, $0E), $12 WE WON ON POINTS! ($879B, $0D), $13 IT WAS A DRAW. ($87AC, silent), $14 WE WERE KNOCKED OUT! ($87BA, $0E), $15 WE WON BY A KNOCKOUT! ($87CE, $0D), $16 GAME OVER. ($87E3, $19), $17 PICK UP PHONE THEN PRESS SPACE. ($8897, 6), $18 PRESS SPACE, WAIT, HANGUP PHONE. ($88B6, 6), $19 TIMEOUT. RUN/STOP TO RESUME. ($05B4, $19), $1A OPPONENT PICKING GAME/MAP. ($88D6, 3), $1B RESUMED ($05D0, $19), $1C THE BATTLE BEGINS! ($05D7, 3), $1D LAST QTR WARNING! ($05E9, $19), $1E unset ($FFFF, 3), $1F OPPONENT READY FOR NEW GAME. ($88F0, silent), $20/$21 unset ($FFFF) and patched at run time, $22 PHONE TROUBLE. ($05FA, $15), $23 TRY AGAIN. ($0608, 3). The $87xx strings live in the $6F00 overlay variant B.",
  "[game/table_0400] The terrain data model is entirely in this block: terrainClassTable maps the raw cell code to one of nine classes, terrainColourTable gives the colour-RAM nibble per class, tileVariantMaskTable gives the random variation mask per base tile, and unitGlyphBaseTable maps the five unit types to their symbol. Every cell code $21-$34 has the same class as $41-$54 shifted by $20, which is the contour offset the map generator uses ($0324 contourTileOffset).",
  "[game/table_0400] Only two of the nine terrain classes get a distinct colour: everything from class 3 to class 6 (contours, hills, summit) is drawn orange ($08), so the elevation rings differ only in shape.",
  "[game/setup_0200] The block that ships in track 18 sectors 15-16 is 256 bytes of pure data (contourRuleTable) followed by 137 bytes of one-shot start-up code ($0300-$0388, surveyed in game_setup_0200_0200.json) and 119 zero bytes; there are no strings in it.",
  "[game/setup_0200] Neighbour-mask bit order for the whole map generator: bit0=NW, bit1=N, bit2=NE, bit3=W, bit4=E, bit5=SW, bit6=S, bit7=SE (reading order, centre skipped).",
  "[game/high_FBB8] The high-memory block ends with graphics, not code: two RLE-compressed eight-direction sprite sets ($FD70 missile marker, $FE24 drone) and one uncompressed 63-byte sprite ($FF93 targeting reticle), filling $FD70-$FFD1 exactly.",
  "[game/high_FBB8] The RLE format used here and by the $C000 overlay's five blocks ($CD52/$CDD3/$CE3B/$CECD/$CF3A) is: byte 0 = escape marker, then 'marker, count, value' emits value count times and anything else is a literal; unpackRleTo0200 always produces exactly 512 bytes into $0200-$03FF. The missile set compresses 512 bytes to 180 and the drone set to 367.",
  "[game/high_FBB8] The unpacked buffer uses a 64-byte stride, not 63: loadDirectionShapeSprite computes $0200 + 64*shape ($3340-$3358), so each sprite has one spare byte after it. Decoding either block with a 63-byte stride produces shapes that drift out of frame.",
  "[game/high_FBB8] Both sprite sets are ordered by compass heading N, NE, E, SE, S, SW, W, NW, matching the direction byte $920B that the callers pass ($5322 in the main program, $802A/$810B/$829E in overlay B).",
  "[game/high_FBB8] There are no message strings anywhere in $FBB8-$FFD1. The only text-like data at the top of memory is the ten-byte disk serial at $FFDE (read from the BAM) and the opponent's name at $FFE8, both of which are zero on the disk image and filled at run time.",
  "[game/main_0800] There are no message strings in any of the pure-data tails of the main program - all of the game's text lives in the $0400 block, the $8800 block, the $C000 overlay and the $6F00 overlays.",
  "[game/main_0800] The 20 big unit glyphs at $6C5D and the 20 small ones at $9DCE are the same five types x four facings, reached through the same index arithmetic (unitGlyphBaseTable $0799 + facing): the tactical view uses the 16x16 multicolour pair table at $6C35, the overview map and the radar screens use the 8x8 one-bit shapes at $9C6E + code*8.",
  "[game/main_0800] The sound system is a three-voice bytecode interpreter: 29 sounds x 3 voice scripts in a pointer table ($6831), 854 bytes of script ($68DF-$6C34), an 11-entry command/slot map ($6787), a 3-entry SID register offset table ($6782) and 54 bytes of work RAM ($6792-$67C7). Scripts can loop with two counters and add RNG jitter to the frequency, which is how the engine gets varied explosion noises out of so little data.",
  "[game/main_0800] scrollViewBitmap never redraws the tactical view when the cursor moves one cell: it block-copies the bitmap, screen RAM and colour RAM by one cell using self-modified loops whose 22 parameters (including the DEY/INY opcode itself) come from the table group at $1DA2, then fills in only the newly exposed column and/or row.",
  "[game/main_0800] The 27 windows the game can draw are fully data-driven: left/top/width/height at $2BDD/$2BF8/$2C13/$2C2E and a frame style pointer at $2C49 that selects one of ten 9-glyph tile sets in the graphics block. Window 0 is the 22x22 overview frame and window 1 the 16x12 tactical frame.",
  "[game/main_0800] The strategic overview compresses two map cells per character in each axis, so each of the 33 terrain indices maps to a 4-byte pattern that already contains both horizontal halves; only 15 distinct patterns exist ($5A9B-$5AD6), including a solid $FF blob for a live unit and an $FF/$00 stripe for a cloaked one, which are the only two the index table never points at.",
  "[game/data_9300] graphicsData9300.s carries no names for anything past fontColorTable/gameFont; every block from $97B6 to $9FFF listed above is currently anonymous ($9800/$9C00 only have labels because the boot-time title colour RAM and screen RAM lived at the same addresses - boot/titleColorRam9800 and boot/titleScreenRam9C00 - which is why XREF.txt attributes $9800/$9C00 to two units at once).",
  "[game/data_9300] The file header says '$9300-$9FFF - screen address tables, fonts, terrain and unit graphics'; that is right, but it hides that the block also carries all ten window frame tile sets, the ASCII translation table, four complete sprites and the whole circle-drawing geometry.",
  "[game/ovl_8800] Almost every D_88xx / D_89xx label in messages8800.s and in XREF.txt is NOT a string boundary: they are the addresses the solo trainer writes when it uses this whole page as work RAM after the message block is dead. That is why the disassembler chopped e.g. msgPressAOrOAndSetModem into $881C + D_8831 + D_8832 and msgOpponentUsingNonStandardGame into 30 one-byte labels. The AI meanings of those addresses are documented in game_ovl_E000_T35_* ($8800 replan clock per unit, $8832 group id, $8864 penetration flags, $8896 command staging, $88A0 order queue, $88D2/$88DE/$88EA/$88F6/$8902/$890E/$891A/$8926/$8932/$893E per-group arrays, $8952-$897C scalars).",
  "[game/ovl_8800] $890C is labelled only as a trainer variable in XREF (written at $E5DC); its real, mastered content is the 32-character map-name alphabet read by the map generator at $779B/$7804/$785D.",
  "[game/map_F000] battlefieldMapF000.s is headed '$F000-$F63F is the 40x40 map, one byte per cell', and the loader routine is called loadMapAndSetup_10AB. That describes the RUN-TIME use of the memory, not the disk contents: track 18 sectors 8-14 hold four 400-byte unit start-position templates, not a map. Proof: (1) 80 percent (1279 of 1600) of the 1600 bytes are outside the legal terrain range $21-$6B (many are $00-$20, and $6D/$71/$75/$79 are above the top code $6B), so rendering it through terrainClassTable produces noise; (2) the 100-byte structure is exact - four repetitions of col/row/flags/type, with the type arrays containing only $10-$14, $18-$1C and $FF; (3) loadUnitStartTemplate ($ECF3) copies from $F000 + 400*n; (4) the destination-column and destination-row halves of solo-trainer play 0 ($0232/$0264 in the track 29 playbook) appear verbatim inside template 2 at $F360 and $F3C8.",
  "[game/map_F000] The labels in battlefieldMapF000.s from $F640 on (unitColTable, side0ComcenCol $F671, side1UnitColTable $F672, unitFlagsTableAlias $F688, side1ComcenCol $F6A3, unitRowTable $F6A4, side0ComcenRow $F6D5, side1UnitRowTable $F6D6, unitTypeTableAlias $F6EC - and the older D_F640/D_F671/... names still in XREF.txt) describe the RUN-TIME unit-record arrays, which is right; but the bytes printed under them are the leftover code fragment, not array data. Likewise D_F30B/D_F333/D_F334 (written by the map generator at $6F81-$6F8B) and D_F600 (read/written at $7536/$7541) are run-time map cells that happen to fall inside unit start-position templates 1 and 3 on the disk.",
  "[game/setup_0200_default] trainerPlaybook0200.s inherits stale labels from the other tenants of this page: eaLoadGame ($02B8), eaFileName ($02EC), sub_0300, sub_031B, L_02D5, L_048B and the D_02A2/D_02A3/D_02AB trio are the boot-time `ea` loader, the track-18 start-up code and the BAM buffer used by checkDiskId - none of them has anything to do with the playbook bytes shown at those addresses.",
  "[game/setup_0200_default] The file header says the setup code 'copies a 150-byte slice into the AI's group and destination tables'; more precisely it copies three 50-byte slices into three different tables, one of which ($8832) is in the $8800 block and two of which ($F802/$F866) are the +50 halves of the global unit destination arrays.",
  "[game/table_0400] The label eaCopyrightMessage that XREF.txt attaches to $064D is stale - the `ea` boot file lived at $02A8-$030B, not here. $064D is optionWordTable (OFF/ON/FULL/HALF/NONE).",
  "[game/table_0400] D_0410/D_0420/D_0430/D_0432/D_0450/D_0464 inside mapNameSeedTable and D_04C8/D_04C9/D_04D0/D_04D1/D_04DF/D_04E7 inside scratchPageTail are run-time scratch addresses (unit scan lists, group scratch, the 2x2 tile buffer), not fields of the tables the disk holds there.",
  "[game/table_0400] $0400-$04FF is not stable data at all: only $0500-$07FF survives a game session. The page is successively the protection table + checksum routine, the BAM buffer for checkDiskId, the 512-byte RLE sprite buffer, overlay A's map-generator variables, the film directory, and the trainer's playbook.",
  "[game/setup_0200] XREF.txt and the include lists of other files carry boot-era names for addresses in this block: $0300 appears as basicVectorHijack and $02B8/$02EC as eaLoadGame/eaFileName, all from boot/ea.s which loaded to $02A8-$030B before the game started.",
  "[game/high_FBB8] highMemoryFBB8.s calls $FD70 arrowGlyphTable ('177 bytes ... no static reference to this table exists anywhere in the disassembly; it may be unused leftover data') and $FE21 aircraftShapeTable ('367 bytes ... the record framing is not fully resolved ... nothing references the table; probably stale data'). Both are wrong on all three counts: they are referenced (from overlay B $7596 and $7FF0), they are not raw shape rows but RLE streams whose first byte is the escape marker consumed by unpackRleTo0200 ($67CB), and the '$01,$16' / '$05,$15' 'headers' the comment describes are simply escape/count/value triples. The boundary is also off by three: block 1 is $FD70-$FE23 (180 bytes) and block 2 starts at $FE24, not $FE21.",
  "[game/high_FBB8] unknownShapeTail ($FF90, 66 bytes, 'most likely unused leftover memory ... stale bytes that resemble the end of the $8800 message block') is wrong too. $FF90-$FF92 is the last RLE run of the drone block, and $FF93-$FFD1 is a perfectly ordinary 63-byte sprite (the targeting reticle) that overlay B reads at $80EB. The $8800 block's tail ($897F-$89FF) is an unrelated $00/$FF dither pattern and does not match these bytes at all.",
  "[game/main_0800] $6792-$67CA is described in the listing as five byteTables; on disk it is all zero and at run time it is the sound driver's work RAM (eight 6-byte voice-state rows plus nine scalars), not table data. The individual D_6793..D_67CA labels are its fields.",
  "[game/main_0800] The $1DA2-$1DE3 tables are named individually in the listing but they are one column-store: 22 parallel arrays indexed by the same direction value 0-2, all consumed by the single routine scrollViewBitmap.",
  "[game/main_0800] $6C35 is listed as pointing at 'the 32-byte unit glyphs below' with only the first target named (unitGlyphShapes); the other 19 targets ($6C7D..$6EBD) have no labels at all."
 ]
}
