Repository navigation
Tags
Tags
perf: test for 0 with OR A instead of CP 0 OR A sets Z like CP 0 in a byte less; only the N flag differs, and only DAA reads it (the game has none). compare, ifEquals, ifNotEquals and dispatch now use it for 0, as do the seven CP 0s written out in the game. Two of those followed a DEC that had already set Z, so they're gone. 30 bytes saved: 30,725 bytes, 1,707 free.
feat(asm): typed state dispatch and helpers for common sequences
Branching on a variable now goes through helpers that know its enum:
- dispatch(MEM.KANA_STATE, KanaState, { IDLE: 'ret', AWAITING: 'continue',
... }) needs a case for every value, from the variable's own enum, so a
new state can't go unhandled. The kana and dialogue update routines and
the kana mode check use it.
- compare, ifEquals and ifNotEquals only take values of the variable's kind
(28 sites).
Common sequences got names:
- incByte, decByte, copyByte (byteOf-checked), copyWord: 24 sites;
- tableEntry(table, stride) and addAToHl for table lookups: 7 sites;
- pressed, held, ifPressed and ifHeld for the buttons: 6 sites.
The two state dispatches no longer test the state that carries on, which
frees 9 bytes (30,755 bytes, 1,677 free). Everything else assembles to the
same bytes. Also corrects joypad.ts's header, which still gave the variables'
old addresses.
feat(asm): routine arguments in contracts, called with invoke
A contract now says which registers a routine takes (in), and the build checks
it: a routine that reads a register before setting it must take it, and one
it takes must be read. Routines with arguments are called with invoke, which
needs exactly those arguments, so a forgotten one is a type error:
...invoke(CAT_PLACE, { b: spot.x, c: spot.y })
...invoke(VRAM_PUT, { a: 'a', hl: 'hl' }) // already in place
A plain call() to such a routine no longer type-checks. The 30 routines that
take arguments got their in lists from the new inference in npm run
contracts, and all 48 calls to them use invoke. The ROM is byte-identical.
Knowing the arguments also tightened the clobber check: a register a later
call takes must survive the calls before it. That showed five contracts
listed a result as clobbered: pass_tile (A), practice_pick (A), table_find
(DE), question_copy (DE) and draw_tiles (HL) now declare them as outputs.
Inputs are worked out without counting a routine's outputs as needed at RET,
so an output set on only some paths (table_find's DE) isn't taken for an
argument. F is left out: flag inputs aren't tracked.
style: sort imports with eslint-plugin-simple-import-sort Every file ordered its imports its own way. The new lint rule (fixable with eslint --fix) sorts them: packages and the @asm/@GAMe aliases first, then relative imports, with the names inside each one sorted too. The change is mechanical; the ROM is byte-identical and the site's tests pass.
feat(asm): typed state dispatch and helpers for common sequences
Branching on a variable now goes through helpers that know its enum:
- dispatch(MEM.KANA_STATE, KanaState, { IDLE: 'ret', AWAITING: 'continue',
... }) needs a case for every value, from the variable's own enum, so a
new state can't go unhandled. The kana and dialogue update routines and
the kana mode check use it.
- compare, ifEquals and ifNotEquals only take values of the variable's kind
(28 sites).
Common sequences got names:
- incByte, decByte, copyByte (byteOf-checked), copyWord: 24 sites;
- tableEntry(table, stride) and addAToHl for table lookups: 7 sites;
- pressed, held, ifPressed and ifHeld for the buttons: 6 sites.
The two state dispatches no longer test the state that carries on, which
frees 9 bytes (30,755 bytes, 1,677 free). Everything else assembles to the
same bytes. Also corrects joypad.ts's header, which still gave the variables'
old addresses.
perf: test for 0 with OR A instead of CP 0 OR A sets Z like CP 0 in a byte less; only the N flag differs, and only DAA reads it (the game has none). compare, ifEquals, ifNotEquals and dispatch now use it for 0, as do the seven CP 0s written out in the game. Two of those followed a DEC that had already set Z, so they're gone. 30 bytes saved: 30,725 bytes, 1,707 free.
refactor: write the framework's macros with mnemonics and trim exports - src/asm/macros.ts and flow.ts used the encoding layer (ld_a_nn, jr_cc, ld_rr_nn...): 109 calls now use the same mnemonics as the game code, so the framework reads like what it helps write. Doc comments that named the old functions are updated too. - 75 exports in src/game that nothing else uses are now private: the builders each module wires up itself (buildCatRoutines, buildChartData...), constants and types only their own file needs. - 16 u16() wrappers around a register pair's value are gone: ld() and invoke() take a plain number for a pair. The ROM is byte-identical.
refactor(game): split main.ts into title, results, rounds and boot main.ts held the title screen, the result screens, the round routines, boot and the game flow in 864 lines. Each now lives with what it's about: - title.ts: the logo, the menu and the START prompt (titleModule); - results.ts: the ending and game-over screens, with the stamps (resultsModule); - rounds.ts: the routines that find and play a scene's rounds, next to the data they read (roundsModule), plus engineLoop and resetRunState; - boot.ts: the CPU, RAM and font setup, and the font's tiles (fontModule). main.ts (386 lines) keeps the game flow, the scenes module and the list of modules. Data moved to the modules that use it: the chart's, title's and result screens' palette sets, the speed round's questions (practice), and the dialogue dictionary (dialogueModule now takes the codec). Moving the code alone left the ROM byte-identical. Moving the data changed its order, so the layout changed, but not the size (30,755 bytes).
feat(asm): record layouts declared once
src/asm/struct.ts: struct(name, { field: word() | byte() }) gives each field's offset, the record size, encode() for its ROM data and decode() for tests. FieldsOf picks out fields of one kind, so code that reads a word field can't be handed a byte field.
The scene table entry, a sound voice's state and an OAM entry use it. sceneWord()/sceneByte() take only fields of their kind, buildSceneTable() encodes entries with SCENE_ENTRY, and a ROM test now decodes each scene's entry and checks it points at that scene's data.
The ROM is byte-identical to the previous commit.
feat(asm): one function per mnemonic, with type-checked operands
src/asm/sm83.ts writes instructions the way the assembly reads: ld('a', [MEM.X]), ld(['hl+'], 'a'), ld('hl', TABLE), cp(3), add('hl', 'de'), inc(['hl']), jr('nz', LOOP), call('c', X), ret('z'). Brackets mean memory, as parentheses do in assembly.
Overloads only allow forms the CPU has: ld('b', [MEM.X]) and ld(['hl'], ['hl']) are type errors, and immediates are typed Imm, which excludes addresses, so ld('a', MEM.X) without brackets doesn't compile either. Immediates take plain numbers and are range-checked, so the game no longer needs u8() around them. ops.ts stays as the encoding layer under it (and gains CALL cc,nn).
All the game code and tests move to it: 1564 instructions in 24 files, replacing 13 ld_* variants and the _r/_n pairs of each ALU instruction. The README gets a short guide to writing game code.
The ROM is byte-identical.
PreviousNext