Files
Crystal/tests/reference-kanto.spec.ts
kawaandClaude Opus 5.5 d3be732118
Checks / web (push) Failing after 18s
Private ROM checks / private-rom (push) Failing after 25m40s
Open Kanto: the S.S. Aqua, the Kanto gyms, the Magnet Train, Mt. Silver and Red (Closes #33)
All 146 Kanto maps, Route 22, Route 28 and Silver Cave are open. The
Magnet Train, Snorlax and the Poké Flute channel, the Diploma, the Trainer
House, Bill's grandpa, Oak's Pokédex rating in Pallet and Goldenrod's
postgame move tutor are ported; big objects cover 2x2 tiles. The Magnet
Train and the Diploma are compared with the cartridge frame by frame.

The playthrough continues from the Hall of Fame through the S.S. Aqua,
the eight Kanto badges, Oak opening Mt. Silver and Red's defeat and
credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:53:57 +02:00

83 lines
4.2 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { existsSync } from "node:fs";
import { romPath, symbolsPath, loadSymbols, openReference } from "./reference-fixture";
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference.");
/** The cartridge's PlaceDiplomaOnScreen with the LCD kept off (so its
* graphics load at once) and only frame waits stubbed: the tilemap, the
* tiles it shows and DiplomaPalettes' palette 0 match diplomaScreen for
* several names and textbox frames. */
test("PlaceDiplomaOnScreen matches the native diploma", async ({ page }) => {
const { at, routine } = loadSymbols();
const flat = (name: string) => {
const { bank, address } = routine(name);
return address < 0x4000 ? address : bank * 0x4000 + address - 0x4000;
};
const a = {
romBank: at("hROMBank"),
cgb: at("hCGB"),
tilemap: at("wTilemap"),
palettes: at("wBGPals1"),
frame: at("wTextboxFrame"),
name: at("wPlayerName"),
};
const routines = { font: routine("_LoadStandardFont"), place: routine("PlaceDiplomaOnScreen") };
const skip = ["WaitBGMap", "DelayFrame", "DelayFrames", "EnableLCD", "DisableLCD"].map(flat);
await openReference(page);
const result = await page.evaluate(
async ({ a, routines, skip }) => {
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
const { diplomaScreen } = await import("/src/game/scripts/kanto.ts");
const { textTiles } = await import("/src/game/rom/font.ts");
const { rgb } = await import("/src/game/scripts/screen.ts");
const rom = new Uint8Array(
await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer(),
);
const assets = new CrystalAssets(rom);
for (const p of skip) rom[p] = 0xc9;
const reference = new BinjgbEmulator();
await reference.load(rom, document.createElement("canvas"));
const write = (p: number, n: number) => reference.writeMemory(p, n & 255);
const read = (p: number) => reference.readMemory(p);
const call = (r: { bank: number; address: number }) => [
0x3e, r.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20,
0xcd, r.address & 255, r.address >> 8,
];
const failures: unknown[] = [];
for (const [index, name] of ["KAWA", "ABCDEFG", "ᴾᴷᴹᴺ", "GOLD"].entries()) {
write(0xff70, 1);
write(0xff40, 0);
write(a.cgb, 1);
write(a.frame, index * 2);
[...textTiles(name), 0x50].forEach((tile, i) => write(a.name + i, tile));
const program = [0xf3, 0x31, 0xfe, 0xc7, ...call(routines.font), ...call(routines.place), 0x3e, 0x42, 0xea, 0xf0, 0xc0, 0x18, 0xfe];
write(0xc0f0, 0);
program.forEach((value, offset) => write(0xc000 + offset, value));
reference.setProgramCounter(0xc000);
for (let frame = 0; frame < 100 && read(0xc0f0) !== 0x42; frame++) reference.advanceFrames(1);
const native = diplomaScreen(assets.reader, name, index * 2);
const map = Array.from({ length: 360 }, (_, i) => read(a.tilemap + i));
write(0xff70, 5);
const palette = Array.from({ length: 4 }, (_, i) => rgb(read(a.palettes + i * 2) | (read(a.palettes + i * 2 + 1) << 8)));
write(0xff70, 1);
write(0xff4f, 0);
const tileDiff = [...new Set(native.map)].flatMap((tile) =>
Array.from({ length: 16 }, (_, byte) => {
const actual = read((tile < 128 ? 0x9000 : 0x8800) + (tile % 128) * 16 + byte);
return actual === native.tiles[tile * 16 + byte] ? [] : [{ tile, byte }];
}).flat(),
);
const mapDiff = map.flatMap((tile, i) => (tile === native.map[i] ? [] : [{ i, original: tile, native: native.map[i] }]));
if (read(0xc0f0) !== 0x42 || mapDiff.length || tileDiff.length || JSON.stringify(palette) !== JSON.stringify(native.palettes[0]))
failures.push({ name, returned: read(0xc0f0) === 0x42, mapDiff: mapDiff.slice(0, 8), tileDiff: tileDiff.slice(0, 4), palette, native: native.palettes[0] });
}
reference.dispose();
return failures;
},
{ a, routines, skip },
);
expect(result, JSON.stringify(result)).toEqual([]);
});