Breed at the Day-Care, hatch Eggs, draw MAIL stationery and browse the Hall of Fame (Closes #29)
- Day-Care and breeding: deposits, withdrawal pricing and growth, compatibility, egg species, DVs and moves, DayCareStep, egg collection and the Odd Egg, compared with the cartridge's routines. - CountStep: walking happiness, DoEggStep, DayCareStep and DoBikeStep in the original's order; HatchEggs with the ROM's hatch animation, compared frame by frame with the original. - MAIL: all ten stationery types and the Portrait MAIL picture, compared with the cartridge's tilemaps, graphics and palettes. - Printing: Bill's PC boxes, Pokedex PRNT, MAIL and the Photo Studio take the original's absent-printer path. - Hall of Fame records (saved apart from the League flag, ready for the League to call) and the Hall of Fame PC, compared with the cartridge's _HallOfFamePC on 255 pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d03f5c5ca
commit
e49bc6de9a
49 files changed
+6028
-88
No files matched your search
@@ -0,0 +1,233 @@
|
||||
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.",
|
||||
);
|
||||
|
||||
test("Hall of Fame PC reads SRAM and matches the resting tilemap, picture and palette", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { at, routine } = loadSymbols();
|
||||
const flat = (name: string) => {
|
||||
const { bank, address } = routine(name);
|
||||
return address < 0x4000 ? address : bank * 0x4000 + address - 0x4000;
|
||||
};
|
||||
const addresses = {
|
||||
romBank: at("hROMBank"),
|
||||
cgb: at("hCGB"),
|
||||
joy: at("hJoyLast"),
|
||||
tilemap: at("wTilemap"),
|
||||
attrs: at("wAttrmap"),
|
||||
palettes: at("wBGPals1"),
|
||||
frame: at("wTextboxFrame"),
|
||||
hof: at("sHallOfFame"),
|
||||
};
|
||||
const pc = routine("_HallOfFamePC");
|
||||
const font = routine("_LoadStandardFont");
|
||||
// Only bypass frame waits and frontpic animation: the native viewer
|
||||
// presents the resting image. All SRAM decoding, layout, text, gender,
|
||||
// frontpic decompression and palette selection execute cartridge code.
|
||||
const skip = [
|
||||
"WaitBGMap",
|
||||
"DelayFrame",
|
||||
"HOF_AnimateFrontpic",
|
||||
"JoyTextDelay",
|
||||
].map(flat);
|
||||
await openReference(page);
|
||||
const result = await page.evaluate(
|
||||
async ({ addresses: a, pc, font, skip }) => {
|
||||
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
||||
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
|
||||
const { hallOfFameScreen } =
|
||||
await import("/src/game/scripts/hall-of-fame.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 cases = [
|
||||
{
|
||||
species: 25,
|
||||
dvs: 0xaaaa,
|
||||
level: 42,
|
||||
nickname: "SPARK",
|
||||
otId: 12345,
|
||||
win: 1,
|
||||
},
|
||||
{
|
||||
species: 201,
|
||||
dvs: 0xffff,
|
||||
level: 100,
|
||||
nickname: "UNOWN",
|
||||
otId: 0,
|
||||
win: 200,
|
||||
},
|
||||
{
|
||||
species: 29,
|
||||
dvs: 0,
|
||||
level: 5,
|
||||
nickname: "NIDORAN♀",
|
||||
otId: 65535,
|
||||
win: 99,
|
||||
},
|
||||
{
|
||||
species: 157,
|
||||
dvs: 0xffff,
|
||||
level: 99,
|
||||
nickname: "FIRE",
|
||||
otId: 42,
|
||||
win: 100,
|
||||
},
|
||||
...Array.from({ length: 251 }, (_, i) => ({
|
||||
species: i + 1,
|
||||
dvs: i % 2 ? 0x2aaa : 0xffff,
|
||||
level: 1 + (i % 100),
|
||||
nickname: "RECORDED",
|
||||
otId: i * 251,
|
||||
win: 1 + (i % 200),
|
||||
})),
|
||||
];
|
||||
const failures: unknown[] = [];
|
||||
for (const [index, input] of cases.entries()) {
|
||||
const { win, ...mon } = input;
|
||||
const team = { win, mons: [mon] };
|
||||
// SRAM bank 1: one team, a terminator after its first Pokémon, and
|
||||
// the absent next team. The PC must load this itself.
|
||||
write(0, 0x0a);
|
||||
write(0x4000, 1);
|
||||
for (let i = 0; i < 196; i++) write(a.hof + i, 0);
|
||||
const record = [
|
||||
win,
|
||||
mon.species,
|
||||
mon.otId >> 8,
|
||||
mon.otId & 255,
|
||||
mon.dvs >> 8,
|
||||
mon.dvs & 255,
|
||||
mon.level,
|
||||
...textTiles(mon.nickname),
|
||||
...Array(10 - mon.nickname.length).fill(0x50),
|
||||
0xff,
|
||||
];
|
||||
record.forEach((value, offset) => write(a.hof + offset, value));
|
||||
write(0, 0);
|
||||
write(0xff70, 1);
|
||||
write(0xff40, 0);
|
||||
write(a.cgb, 1);
|
||||
write(a.frame, index % 8);
|
||||
write(a.joy, 2); // B after the first page.
|
||||
// JoyTextDelay is stubbed to preserve the fixed B input above.
|
||||
const program = [
|
||||
0xf3,
|
||||
0x31,
|
||||
0xfe,
|
||||
0xc7,
|
||||
0x3e,
|
||||
font.bank,
|
||||
0xe0,
|
||||
a.romBank & 255,
|
||||
0xea,
|
||||
0,
|
||||
0x20,
|
||||
0xcd,
|
||||
font.address & 255,
|
||||
font.address >> 8,
|
||||
0x3e,
|
||||
pc.bank,
|
||||
0xe0,
|
||||
a.romBank & 255,
|
||||
0xea,
|
||||
0,
|
||||
0x20,
|
||||
0xcd,
|
||||
pc.address & 255,
|
||||
pc.address >> 8,
|
||||
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 = hallOfFameScreen(assets.pokemon, team, 0, index % 8);
|
||||
const original = Array.from({ length: 360 }, (_, i) =>
|
||||
read(a.tilemap + i),
|
||||
);
|
||||
const mismatches = original.flatMap((tile, i) =>
|
||||
tile === native.map[i]
|
||||
? []
|
||||
: [{ i, original: tile, native: native.map[i] }],
|
||||
);
|
||||
const attrDiff = Array.from({ length: 360 }, (_, i) =>
|
||||
read(a.attrs + i),
|
||||
).filter((v, i) => v !== native.attrs[i]).length;
|
||||
// CGB palette work RAM is bank 5; graphics at $9000 are VRAM bank 0.
|
||||
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 picture = Array.from({ length: 49 * 16 }, (_, i) =>
|
||||
read(0x9000 + i),
|
||||
);
|
||||
const picDiff = picture.filter((v, i) => v !== native.tiles[i]).length;
|
||||
const tileDiff = [...new Set(native.map)]
|
||||
.flatMap((tile) =>
|
||||
Array.from({ length: 16 }, (_, byte) => ({
|
||||
tile,
|
||||
byte,
|
||||
expected: native.tiles[tile * 16 + byte],
|
||||
actual: read(
|
||||
(tile < 128 ? 0x9000 : 0x8800) + (tile % 128) * 16 + byte,
|
||||
),
|
||||
})),
|
||||
)
|
||||
.filter(({ expected, actual }) => expected !== actual);
|
||||
if (
|
||||
read(0xc0f0) !== 0x42 ||
|
||||
mismatches.length ||
|
||||
attrDiff ||
|
||||
picDiff ||
|
||||
tileDiff.length ||
|
||||
JSON.stringify(palette) !== JSON.stringify(native.palettes[0])
|
||||
)
|
||||
failures.push({
|
||||
input,
|
||||
returned: read(0xc0f0) === 0x42,
|
||||
mismatches: mismatches.slice(0, 12),
|
||||
attrDiff,
|
||||
picDiff,
|
||||
tileDiff: tileDiff.slice(0, 5),
|
||||
palette,
|
||||
nativePalette: native.palettes[0],
|
||||
});
|
||||
}
|
||||
reference.dispose();
|
||||
return { cases: cases.length, failures };
|
||||
},
|
||||
{ addresses, pc, font, skip },
|
||||
);
|
||||
expect(result.failures, JSON.stringify(result)).toEqual([]);
|
||||
});
|
||||
Reference in new issue
Block a user