modemwars/tools/phase2_annotate.js
2026-08-23 02:09:40 -05:00

163 lines
12 KiB
JavaScript

export const meta = {
name: 'modemwars-annotate',
description: 'Phase 2: arbitrate variable names, then write detailed line-by-line comments for every chunk',
phases: [
{ title: 'Variables', detail: 'one arbiter agent decides zero-page and global variable names' },
{ title: 'Rebuild', detail: 'rebuild the listing with the arbitrated names applied' },
{ title: 'Annotate', detail: 'one agent per chunk writes annotations/20_<chunk>.json' },
{ title: 'Verify', detail: 'rebuild, reassemble and compare every unit' },
],
}
const chunks = args.chunks
const RESULT = {
type: 'object',
properties: {
path: { type: 'string' },
linesCommented: { type: 'integer' },
routines: { type: 'integer' },
problems: { type: 'array', items: { type: 'string' } },
},
required: ['path', 'linesCommented', 'routines', 'problems'],
}
const common = `
You are annotating a reverse-engineered disassembly of "Modem Wars" (Commodore 64, Electronic Arts / Ozark
Softscape, Dan Bunten, 1988): a two-player real-time strategy game played over a modem or null-modem link, with a
solo trainer, "game film" record/playback, a console with STATS / REPAIR / MISC panels, and a 40x40 battlefield
map. Work in /home/scott/claude/modemwars.
Read first: docs/overview.md (disk, boot, loader protocol, memory map, overlays, game concepts) and the section of
docs/knowledgeBase.md for your unit (every routine and data block found by the survey pass). disassembly/XREF.txt
lists callers/readers/writers of every label and of every zero-page location. disassembly/zeropage.inc holds the
variable names.
The listing is ca65 syntax; every instruction line ends with "; XXXX" (its address) and then the comment text.
Each file starts with a Contents table of its routines. Labels are unit-scoped: the same address holds different
code in the other overlay variant. Hardware registers are named (VIC_*, SID_*, CIA1_*, CIA2_*). The game runs
with $01=$35 (no ROMs, I/O visible) and scans the keyboard and joystick itself. Strings end with a character
whose bit 7 is set. Graphics data is rendered as bit art in the comments.
Facts established by the survey pass - use these names and do not re-derive them:
* Units: 100 records in parallel 100-byte arrays starting at $F640, each array $64 apart - column, row,
flags/facing, type (0 GRUNT 1 RIDER 2 BOOMER 3 SPY 4 COMCEN, bit 7 = dead), destination column/row,
secondary target, cloak, blitz, stun/dug-in, energy (0-50), move state, draw flags, terrain hidden under
the unit, group id. Units 0-49 are player 0, 50-99 player 1; unit 49 and unit 99 are the comcens.
* Map: $F000-$F63F, one byte per cell of the 40x40 battlefield. A cell holding $80|index means that unit
stands there and the terrain it covers is kept in the per-unit "terrain under" array.
* Screen: multicolour bitmap, VIC bank 2 - bitmap $A000, video matrix $8C00, colour RAM $D800. Text is
drawn into the bitmap with the game's own font, through the text engine in game/textEngineC000.s.
Left panel = whole map at 4x4 pixels per cell; right panel = 7x5 cells at 16x16; console below it.
* Opponent: one jump table at $E000 backed by EITHER game/modemDriverE000.s (a real software modem) OR
game/trainerAiE000.s (the solo trainer's computer opponent). $E000 per-frame poll, $E003 open/close,
$E006 get byte, $E009 send byte, $E00C per-frame, $E015 get key.
* Commands: single bytes $80-$A6 plus 0-3 argument bytes, queued in a ring and dispatched through
commandHandlerTable at $4D2A. The same stream is what a "game film" records and replays.
* Side-dependent branches are self-modified: several "cpy #$32" tests are followed by a BCC/BCS whose
opcode byte is written at run time from the player's side. Treat that opcode byte as a variable.
Annotation JSON format (the build merges all annotations/*.json; everything is unit-scoped):
{
"chunk": "<id>", "unit": "<unit>",
"unitLabels": { "<unit>": { "XXXX": "camelCaseName" } },
"notes": { "XXXX": { "unit": "<unit>",
"routine": ["name - what it does", "In: ...", "Out: ...", "Called from: ..."],
"block": ["comment block printed before this address"],
"line": "end-of-line comment for the instruction or data at this address" } },
"dataTypes": { "XXXX": ["text"|"addr"|"word"|"byte"|"grid"|"bitmap"|"sprite", length, optionalRowWidth] },
"zpComments": { "XX": "better description of a zero-page variable (do not rename)" }
}
Addresses: uppercase hex, 4 digits (2 for zero page), no "$". Plain ASCII only - no accented characters, arrows
or box-drawing characters.
HARD RULES
* Write ONLY your own file annotations/20_<chunk>.json. Never edit disassembly/*.s, tools/*, docs/* or another
agent's annotation file, and never edit the generated files annotations/05_*, 06_*, 10_*, 11_*, 15_*.
* Do NOT run tools/build.py or disassembly/verify.sh - other agents are running in parallel and the build
rewrites the whole disassembly directory. A separate verification agent runs it at the end.
* Do not rename anything outside your own address range. You MAY rename labels inside your range when you have a
better name (camelCase, specific: waitForRasterBottom, unitHealthTable, isFilmPlaying).
* Never invent facts. Mark uncertainty in the comment itself ("probably", "?").
`
phase('Variables')
const arb = await agent(`${common}
TASK: you are the naming arbiter for variables. survey/variableProposals.json lists, per address, every name and
meaning proposed by the survey agents (often several per address, sometimes contradictory). The zero-page section
at the end of disassembly/XREF.txt shows every reader and writer. Decide ONE final camelCase name and a one-line
description for:
(a) every zero-page address that has a proposal or is used by the game (keep "zp_XX" only when nothing can be
inferred - but read the readers/writers in the listing before giving up);
(b) absolute RAM variables that live outside the code units: $8A00-$8BFF (sprite shapes), $9000-$92FF (VIC
shadow registers, buffers, game state), $F640-$FBB7 (unit record arrays and work RAM), $FFD2-$FFFF;
(c) the well-known flags inside the main program: $0B7D-$0BB4 (the settings block), $87FF, $90B7, $90EA, $90FB.
Resolve conflicts by reading the code, not by counting votes. Names must be unique across the whole program and
must not collide with routine names in docs/knowledgeBase.md. Prefer the plural/array form for the per-unit
arrays (unitColTable, unitRowTable, unitTypeTable, ...).
Write /home/scott/claude/modemwars/annotations/15_variables.json:
{ "zp": { "XX": ["name", "description"] },
"labels": { "XXXX": "name" }, <- addresses outside every code unit range
"unitLabels": { "game/mainProgram0800": { "0BA5": "isSoloTrainer" } }, <- variables inside a unit range
"notes": { "XXXX": { "unit": "game/mainProgram0800", "line": "description" } }
}
You MAY run "python3 tools/build.py 2>&1 | grep -i warning" and "./disassembly/verify.sh | grep -c OK" (expect 24)
because you are the only agent running in this phase; fix any duplicate-name warning it reports.
Return path, number of zp names decided (linesCommented), number of absolute variables (routines), problems.`,
{ label: 'arbiter:variables', phase: 'Variables', schema: RESULT })
phase('Rebuild')
const rebuilt = await agent(`Work in /home/scott/claude/modemwars. Run:
python3 tools/build.py 2>&1 | grep -iE "warning|error|traceback"
./disassembly/verify.sh | grep -c OK (must print 24)
python3 tools/chunks.py 700 > /dev/null
If verify does not print 24, show the failing unit and its first ca65 error
(./disassembly/verify.sh 2>&1 | grep -B2 -A2 Error | head -30), trace it to the offending entry in
annotations/15_variables.json (bad label name, a text run crossing a label, a non-ASCII character) and fix that
annotation file - never edit disassembly/*.s or tools/*. Repeat until it verifies.
Return path="disassembly", linesCommented=0, routines=0, problems=[what you fixed].`,
{ label: 'rebuild', phase: 'Rebuild', schema: RESULT })
phase('Annotate')
const results = await pipeline(chunks, (c) => agent(`${common}
YOUR CHUNK: unit "${c.unit}", file disassembly/${c.file}, addresses $${c.startAddr}-$${c.endAddr}.
Locate it with: grep -n "; ${c.startAddr}" disassembly/${c.file} and grep -n "; ${c.endAddr}" disassembly/${c.file}
then read the whole range with sed -n. Read it all before writing anything.
TASK: write the detailed annotation for this chunk to /home/scott/claude/modemwars/annotations/20_${c.id}.json
(format above). Requirements:
1. Every routine that starts in your range gets a "routine" header: what it does (1-3 sentences), "In:" the
registers and variables it consumes, "Out:" the results and side effects, and "Called from:" its callers taken
from XREF.txt when there are only a few. Improve the survey's wording where you understand it better; keep
its name unless it is clearly wrong.
2. A "line" comment on essentially EVERY instruction, explaining intent rather than the mnemonic ("start at the
left edge of the viewport", not "load 0 into X"). In every compare/branch name the thing being tested
("branch if the unit is dead"). Explain every magic number: screen offsets, colours, screen codes, sprite
pointers, VIC/SID/CIA bit meanings, raster lines, timer values, table strides, unit-array offsets ($64 apart).
3. "block" comments before each logical section and loop: what it iterates over and what it maintains.
4. Data inside your range: label it and describe the structure (element size, what the index means). Use
dataTypes "text" for strings (exact length, including the terminator byte), "addr" for tables of 16-bit
addresses, "grid" with a row width for tables read as rows, "bitmap"/"sprite" for graphics.
5. Self-modifying code: say exactly which instruction is patched, by whom, and with what (labels look like
L_1234+1 for the operand byte).
6. Note anything that looks like dead or leftover code, and say why you think so.
Work top to bottom; do not skip routines. Follow calls into other files when the meaning of a call matters.
When done, validate with: python3 -c "import json;json.load(open('annotations/20_${c.id}.json'))"
Return: path, number of line comments written, number of routine headers, problems (mis-traced code, survey names
you corrected, unresolved questions).`,
{ label: `annotate:${c.id}`, phase: 'Annotate', schema: RESULT }))
phase('Verify')
const verified = await agent(`Work in /home/scott/claude/modemwars. You are the only agent running now.
1. grep -lP '[^\\x00-\\x7F]' annotations/*.json docs/*.md 2>/dev/null - replace any non-ASCII character found.
2. python3 -c "import json,glob;[json.load(open(f)) for f in glob.glob('annotations/*.json')]" - fix invalid JSON.
3. python3 tools/build.py 2>&1 | grep -iE "warning|error|traceback"
4. ./disassembly/verify.sh | grep -v "^OK" - every one of the 24 units must reassemble byte-exact.
If a unit fails, find the first ca65 error (./disassembly/verify.sh 2>&1 | grep -B2 -A2 Error | head -30), trace it
to the annotation entry that caused it and fix THAT annotation file. Never edit disassembly/*.s or tools/*.
Duplicate-name warnings from build.py are auto-resolved but ugly: fix the annotation that caused each one.
Repeat until verify reports 24 OK and build.py prints no warnings.
Return path="disassembly", linesCommented=0, routines=0, problems=[everything you had to fix].`,
{ label: 'verify', phase: 'Verify', schema: RESULT })
return { arbiter: arb, rebuilt, chunks: results.filter(Boolean), verified }