Lay out the Hall of Fame, the Diploma and the credits per cartridge (#84)
Checks / web (push) Failing after 18s
Private ROM checks / private-rom (push) Failing after 25m52s

The European releases keep these routines' instructions and change
their operands: where the count and the ID go, which tiles the ID line
draws, where the player's name follows on the Diploma. game/rom/code.ts
reads those operands from the cartridge (and refuses a routine whose
instructions differ). The credits read the copyright's string from
ParseCredits (six more strings precede it in the releases) and use spill
tables fitted to each release's credits on the cartridge, with Spanish's
one group that spills otherwise while B is held.

The reference specs for these screens now pass against all four
releases, as every other reference spec that applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-18 03:01:10 +02:00
1 parent 91d3b9c9f2
commit e5b3b4a91c
6 files changed
+176 -25

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
import type { RomReader } from "./reader";
import { WRAM } from "./profile";
/** One step of an instruction pattern: an opcode byte that must match, or
* an operand to read ("b" a byte, "w" a little-endian word). */
type Step = number | "b" | "w";
/** An ID line: ld hl, coord; three tiles (ld a / ld [hli], a twice, then
* ld [hl]); ld hl, the number's coord. */
export const ID_LINE: readonly Step[] = [0x21, "w", 0x3e, "b", 0x22, 0x3e, "b", 0x22, 0x36, "b", 0x21, "w"];
/** The operands of the instructions at `address`, which must follow
* `pattern`. The European releases keep many routines' instructions and
* change only their operands (where a string or a number goes, which tiles a
* line draws), so the port reads those operands from the cartridge. Another
* instruction there is logic the port has not ported for that cartridge. */
export function operands(r: RomReader, address: number, pattern: readonly Step[]): number[] {
const found = match(r, address, pattern);
if (!found) throw new Error(`The routine at 0x${address.toString(16)} differs on this cartridge; it is not ported yet.`);
return found.values;
}
/** The pattern's operands at `address` and where its instructions end, or
* undefined when the instructions there are others. */
export function match(r: RomReader, address: number, pattern: readonly Step[]): { values: number[]; end: number } | undefined {
const values: number[] = [];
let p = address;
for (const step of pattern) {
if (step === "b") values.push(r.byte(p++));
else if (step === "w") {
values.push(r.word(p));
p += 2;
} else if (r.byte(p++) !== step) return undefined;
}
return { values, end: p };
}
/** The operands of the first place in `from` .. `from + limit` where the
* pattern's instructions are, for a line a routine draws without a label of
* its own. */
export function findOperands(r: RomReader, from: number, limit: number, pattern: readonly Step[]): number[] {
return findMatch(r, from, limit, pattern).values;
}
export function findMatch(r: RomReader, from: number, limit: number, pattern: readonly Step[]) {
for (let p = from; p < from + limit; p++) {
const found = match(r, p, pattern);
if (found) return found;
}
throw new Error(`The routine at 0x${from.toString(16)} differs on this cartridge; it is not ported yet.`);
}
/** A pointer operand of code in `routine`'s bank, as a ROM offset. */
export const nearPointer = (r: RomReader, routine: number, pointer: number) =>
pointer < 0x4000 ? pointer : r.bank(routine) * 0x4000 + pointer - 0x4000;
/** hlcoord's tile position from a wTilemap address. */
export function tileCoord(address: number): [number, number] {
const offset = address - WRAM.wTilemap;
if (offset < 0 || offset >= 20 * 18) throw new Error("A screen position lies outside the tilemap.");
return [offset % 20, Math.floor(offset / 20)];
}