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>
This commit is contained in:
1 parent
f05fd8ebee
commit
d3be732118
22 files changed
+1877
-31
No files matched your search
@@ -0,0 +1,82 @@
|
||||
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([]);
|
||||
});
|
||||
Reference in new issue
Block a user