import { describe, expect, it } from "vitest"; import { readFileSync, existsSync } from "node:fs"; import { createHash } from "node:crypto"; import { RomReader } from "../src/game/rom/reader"; import { decompress } from "../src/game/rom/lz"; import { CrystalAssets } from "../src/game/rom/assets"; import { GameWorld } from "../src/game/world/world"; import { NATIVE_MAPS } from "../src/game/rom/profile"; import { runScript } from "../src/game/scripts/interpreter"; import { drawTile } from "../src/game/rendering/pixels"; import { CRYSTAL_ROM } from "../src/rom/validate"; describe("native ROM formats", () => { it("resolves fixed and banked pointers and rejects out-of-bounds reads", () => { const reader = new RomReader(new Uint8Array(0xc000)); expect(reader.address(2, 0x4321)).toBe(0x8321); expect(reader.address(2, 0x1234)).toBe(0x1234); expect(() => reader.address(3, 0x4000)).toThrow(); expect(() => reader.word(0xbfff)).toThrow(); expect(() => reader.slice(-1, 1)).toThrow(); }); it("decodes all LZ commands, long runs, overlapping copies and termination", () => { const decode = (input: number[]) => [ ...decompress(new RomReader(Uint8Array.from(input)), 0), ]; expect(decode([0x02, 1, 2, 3, 0x22, 7, 0x43, 8, 9, 0x61, 0xff])).toEqual([ 1, 2, 3, 7, 7, 7, 8, 9, 8, 9, 0, 0, ]); expect(decode([0, 5, 0x83, 0x80, 0xff])).toEqual([5, 5, 5, 5, 5]); expect(decode([0, 0b10010000, 0xa0, 0, 0, 0xff])).toEqual([144, 9]); expect(decode([2, 1, 2, 3, 0xc2, 0, 2, 0xff])).toEqual([1, 2, 3, 3, 2, 1]); expect(decode([0xec, 39, 0xff])).toEqual(Array(40).fill(0)); expect(() => decode([0x80, 0x80, 0xff])).toThrow("back-reference"); expect(() => decompress(new RomReader(new Uint8Array([0xec, 99, 0xff])), 0, 10), ).toThrow("oversized"); expect(() => decode([0x20, 4])).toThrow(); }); it("decodes planar pixels with transparency and flips", () => { const pixels = new Uint8ClampedArray(8 * 8 * 4); const tile = new Uint8Array(16); tile[0] = 0x80; tile[1] = 0x40; const palette = [ [0, 0, 0], [255, 0, 0], [0, 255, 0], [0, 0, 255], ] as const; drawTile(pixels, 8, 8, tile, 0, 0, 0, palette, { transparent: true, flipX: true, }); expect([...pixels.slice(7 * 4, 8 * 4)]).toEqual([255, 0, 0, 255]); expect([...pixels.slice(6 * 4, 7 * 4)]).toEqual([0, 255, 0, 255]); expect(pixels[3]).toBe(0); }); it("aborts unknown script commands without committing earlier event writes", () => { const reader = new RomReader(Uint8Array.from([0x33, 5, 0, 0x0e, 0, 0, 0])); const state = { events: new Set(), flags: new Set(), variables: {}, }; expect(() => runScript(reader, 0, state, "TEST")).toThrow("not ported"); expect(state.events.size).toBe(0); }); it("bounds looping scripts", () => { const reader = new RomReader(Uint8Array.from([0x03, 0, 0])); expect(() => runScript( reader, 0, { events: new Set(), flags: new Set(), variables: {} }, "TEST", ), ).toThrow("budget"); }); }); const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; describe.skipIf(!existsSync(romPath))( "native engine with local user ROM (never checked in)", () => { const load = () => { const bytes = readFileSync(romPath); expect(createHash("sha1").update(bytes).digest("hex")).toBe( CRYSTAL_ROM.sha1, ); return new CrystalAssets(new Uint8Array(bytes)); }; it("decodes every supported map, tileset, event table and sprite", () => { const assets = load(); for (const id of Object.keys(NATIVE_MAPS)) { const map = assets.map(id), tileset = assets.tileset(map); expect(map.blocks.length).toBe((map.width * map.height) / 4); expect(tileset.graphics.length).toBeGreaterThanOrEqual(0x600); expect(assets.palettes(map, 1).length).toBe(8); for (const warp of map.warps) { expect(warp.x).toBeLessThan(map.width); expect(warp.y).toBeLessThan(map.height); if (warp.targetMap in NATIVE_MAPS) expect( assets.map(warp.targetMap).warps[warp.targetWarp - 1], ).toBeDefined(); } } expect(assets.sprite(1)?.graphics.length).toBe(384); expect(assets.sprite(0x60)?.graphics.length).toBe(384); expect(assets.map("24:7").warps[0]).toEqual({ x: 7, y: 0, targetMap: "24:6", targetWarp: 3, }); }); it.skipIf( !existsSync(".reference/pokecrystal/gfx/tilesets/players_room.2bpp"), )( "matches decompressed graphics against the locally rebuilt PRET reference", () => { const path = ".reference/pokecrystal/gfx/tilesets/players_room.2bpp"; const assets = load(); expect(assets.tileset(assets.map("24:7")).graphics).toEqual( new Uint8Array(readFileSync(path)), ); }, ); it("walks from the bedroom to downstairs and the town, with collision and return warps", () => { const world = new GameWorld( load(), () => new Date("2026-09-22T12:00:00"), ); expect(world.x).toBe(3); expect(world.y).toBe(3); expect(world.move("up")).toBe(true); const steps = world.tilesWalked; expect(world.move("up")).toBe(false); // Radio/furniture is solid. expect([world.x, world.y, world.tilesWalked]).toEqual([3, 2, steps]); expect(world.move("down")).toBe(true); const routeTo = (targetX: number, targetY: number) => { const directions = [ ["up", 0, -1], ["down", 0, 1], ["left", -1, 0], ["right", 1, 0], ] as const; const queue = [ { x: world.x, y: world.y, path: [] as ("up" | "down" | "left" | "right")[], }, ]; const visited = new Set(); for (let i = 0; i < queue.length; i++) { const node = queue[i], key = `${node.x},${node.y}`; if (visited.has(key)) continue; visited.add(key); if (node.x === targetX && node.y === targetY) { for (const direction of node.path) expect(world.move(direction)).toBe(true); return; } for (const [direction, dx, dy] of directions) { const x = node.x + dx, y = node.y + dy; if (x < 0 || y < 0 || x >= world.map.width || y >= world.map.height) continue; if ( world.assets.permission( world.assets.collision(world.map, x, y), ) !== 0 ) continue; if ( world.objects().some((object) => object.x === x && object.y === y) ) continue; if ( world.map.warps.some((warp) => warp.x === x && warp.y === y) && (x !== targetX || y !== targetY) ) continue; queue.push({ x, y, path: [...node.path, direction] }); } } throw new Error(`No route to ${targetX},${targetY}`); }; routeTo(7, 0); expect(world.mapId).toBe("24:6"); routeTo(6, 7); expect(world.mapId).toBe("24:4"); expect(world.tilesWalked).toBeGreaterThan(10); const arrival = world.map.warps.find( (warp) => warp.targetMap === "24:6", )!; routeTo(arrival.x, arrival.y); expect(world.mapId).toBe("24:6"); const save = world.save(); expect(() => world.restore({ ...save, x: 127 })).toThrow("position"); expect(world.save()).toEqual(save); const restored = new GameWorld(world.assets); restored.restore(save); expect(restored.save()).toEqual(save); }); it("reads the bedroom radio through native event and text interpreters", () => { const world = new GameWorld(load()); world.restore({ ...world.save(), x: 3, y: 2, direction: "up" }); const previousEvents = world.state.events.size; world.interact(); expect(world.diagnostic).toBe(""); expect(world.dialogue?.pages.length).toBe(4); expect(world.state.events.size).toBe(previousEvents); while (world.dialogue) world.advanceDialogue(); expect(world.state.events.size).toBe(previousEvents + 1); world.interact(); expect(world.dialogue?.pages.length).toBe(1); }); }, );