diff --git a/README.md b/README.md
index eb68376..23aa2e8 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ A web platform for comparing fun, cool-looking AI-generated games. Play each ver
| 🔮 Marble Madness | Roll a glowing marble across neon platforms suspended in space. Master physics, dodge hazards, and race the clock. | 6 |
| 🧭 3D Maze | Navigate a procedurally generated labyrinth in first-person 3D. Find the glowing exit before time runs out. | 8 |
| ⛳ Mini Golf 3D | Aim, pull back, and sink impossible banks across neon-drenched 3D mini golf courses, including a nine-hole solar eclipse odyssey. | 5 |
-| 💎 Tile Matching | Swap, match, and chain your way to the top. Line up three or more gems to trigger cascading combos and dazzling special tiles. | 7 |
+| 💎 Tile Matching | Swap, match, and chain your way to the top. Line up three or more gems to trigger cascading combos and dazzling special tiles. | 8 |
| 👾 Space Invaders | Defend Earth from waves of descending alien invaders. Blast through formations, grab power-ups, and survive the onslaught. | 5 |
| 🔢 Sudoku | Solve a glowing logic grid under pressure with notes, streaks, and satisfying neon feedback for every sharp deduction. | 2 |
| 🏎️ Neon Horizon Racer | Drift through a neon synthwave highway, dodge traffic, chain near misses, and chase checkpoints into the glowing horizon. | 6 |
@@ -41,7 +41,7 @@ A web platform for comparing fun, cool-looking AI-generated games. Play each ver
| OpenAI GPT 5.4 | 1,763 lines | 1,875 lines | 2,278 lines | 1,482 lines | 1,419 lines | 779 lines | 1,960 lines | 2,279 lines | 1,837 lines | 2,401 lines | 2,852 lines | 3,116 lines | 2,135 lines | 3,371 lines | N/A | 3,093 lines | N/A | N/A | N/A |
| OpenAI GPT 5.5 | N/A | N/A | N/A | N/A | N/A | 2,031 lines | 2,369 lines | N/A | N/A | N/A | 2,413 lines | 1,643 lines | 2,058 lines | N/A | 1,398 lines | 2,155 lines | N/A | N/A | N/A |
| OpenAI GPT 5.6 Terra | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 930 lines | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 147 lines | N/A | N/A |
-| OpenAI GPT 5.6 Luna | N/A | N/A | 1,444 lines | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 289 lines | N/A | N/A |
+| OpenAI GPT 5.6 Luna | N/A | N/A | 1,444 lines | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 893 lines | N/A | N/A | N/A | N/A | N/A | 289 lines | N/A | N/A |
| OpenAI GPT 5.6 Sol | N/A | N/A | N/A | 1,736 lines | N/A | N/A | N/A | 304 lines | 2,421 lines | 2,134 lines | 3,263 lines | N/A | N/A | N/A | 278 lines | 1,542 lines | 2,447 lines | 715 lines | N/A |
| OpenAI GPT 5.4 Mini | 1,309 lines | 1,223 lines | 1,267 lines | 1,389 lines | 570 lines | 541 lines | 1,309 lines | 1,687 lines | 1,486 lines | 1,345 lines | 2,093 lines | 1,035 lines | N/A | N/A | 2,734 lines | 3,965 lines | N/A | N/A | N/A |
| Google Gemini 3.1 Pro | 60 lines | 184 lines | 394 lines | 230 lines | 63 lines | 58 lines | 44 lines | 46 lines | 65 lines | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
diff --git a/games-metadata.json b/games-metadata.json
index 04c485a..11eeb7d 100644
--- a/games-metadata.json
+++ b/games-metadata.json
@@ -4746,6 +4746,19 @@
]
}
]
+ },
+ {
+ "model": "GPT 5.6 Luna",
+ "modelId": "gpt-5-6-luna",
+ "date": "2026-07-31",
+ "linesOfCode": 893,
+ "path": "/games/tile-matching/gpt-5-6-luna/index.html",
+ "features": [
+ "sound",
+ "music",
+ "particles",
+ "powerups"
+ ]
}
]
},
diff --git a/public/games/tile-matching/gpt-5-6-luna/index.html b/public/games/tile-matching/gpt-5-6-luna/index.html
new file mode 100644
index 0000000..f620ada
--- /dev/null
+++ b/public/games/tile-matching/gpt-5-6-luna/index.html
@@ -0,0 +1,892 @@
+
+
+
+
+
+LUNAR ARRAY // GPT 5.6 Luna
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
OpenAI // GPT 5.6 Luna // Observatory Deck
+
LUNAR ARRAYMatch the light. Bend the gravity.
+
A tactile match-three instrument for building constellations, breaking moon-dust, and spending one perfectly timed Lunar Pulse.
+
+
+
+
+
+
Drag or click two neighbors · Arrows + Enter move · H hint · S shuffle · P pause · M mute
+
+
+
+
+
+
The array is holding
+
Pause / telemetry frozen
+
Your board is waiting exactly where you left it.
+
+
+
+
+
+
+
Orbit solution confirmed
+
Perilune complete
+
★☆☆
+
Score0
Best chain0
Pulse uses0
Stars1
+
+
+
+
+
+
+
Telemetry lost
+
The orbit slipped
+
The array still has a route. Try a different line.
+
+
+
+
+
+
+
Orbit run complete
+
The array goes dark
+
Score0
Best chain0
Pulse uses0
Session best0
+
+
+
+
+
+
+
+
diff --git a/tests/model-identity.spec.ts b/tests/model-identity.spec.ts
index 991e325..3aaaf52 100644
--- a/tests/model-identity.spec.ts
+++ b/tests/model-identity.spec.ts
@@ -34,7 +34,7 @@ test.describe("Model identity system", {
await page.goto("/games/tile-matching");
const cards = page.locator('[data-model-id][data-model-color]');
- await expect(cards).toHaveCount(7);
+ await expect(cards).toHaveCount(8);
await expect(cards.locator("h3")).toHaveText([
"Claude Opus 4.6",
"Claude Opus 5",
@@ -43,6 +43,7 @@ test.describe("Model identity system", {
"OpenAI GPT 5.4 Mini",
"OpenAI GPT 5.5",
"OpenAI GPT 5.6 Sol",
+ "OpenAI GPT 5.6 Luna",
]);
await expect(page.locator('[data-model-id="gpt-5-4"]')).toHaveAttribute(
diff --git a/tests/tile-matching-luna.spec.ts b/tests/tile-matching-luna.spec.ts
new file mode 100644
index 0000000..8715660
--- /dev/null
+++ b/tests/tile-matching-luna.spec.ts
@@ -0,0 +1,416 @@
+import { expect, test, type Page } from "@playwright/test";
+
+type Cell = { r: number; c: number };
+type Tile = { type: number; special: number; id: number } | null;
+type Snapshot = {
+ grid: number;
+ board: Tile[][];
+ mask: number[][];
+ blockers: number[][];
+ score: number;
+ moves: number;
+ timeLeft: number;
+ level: number;
+ levelName: string;
+ mode: string;
+ objective: { kind: string; target: number; progress: number; complete: boolean; scoreTarget?: number; pulseTarget?: number; collected: number[] };
+ chain: number;
+ bestChain: number;
+ blockerCleared: number;
+ pulse: number;
+ pulseUses: number;
+ pulseArmed: boolean;
+ pulseDirection: string;
+ pendingGravity: string | null;
+ gravity: string;
+ cursor: Cell;
+ selection: Cell | null;
+ hasMove: boolean;
+ matchCount: number;
+ active: boolean;
+ locked: boolean;
+ paused: boolean;
+ screen: string;
+ layout: { width: number; height: number; boardX: number; boardY: number; boardSize: number; cell: number; mode: string };
+};
+
+type LunaApi = {
+ constants: { GRID: number; BEAM_ROW: number; BEAM_COL: number; BURST: number; CORE: number; FUSION: Record };
+ snapshot(): Snapshot;
+ setSeed(seed: number): boolean;
+ startMission(index: number): boolean;
+ startEndless(): boolean;
+ setSpeed(value: number): void;
+ setBoard(board: number[][], specials?: Array): boolean;
+ setMask(mask: number[][]): boolean;
+ setBlockers(entries: Array| ): boolean;
+ setMoves(value: number): void;
+ setTime(value: number): void;
+ setScore(value: number): void;
+ setPulse(value: number): void;
+ setObjective(spec: { kind: string; target?: number; colors?: number[]; perColor?: number; scoreTarget?: number; pulseTarget?: number }): void;
+ swap(r1: number, c1: number, r2: number, c2: number): Promise;
+ detonate(r: number, c: number): Promise;
+ resolve(): Promise;
+ collapse(): Snapshot;
+ shuffle(manual?: boolean): boolean;
+ hint(force?: boolean): { a: Cell; b: Cell; score: number; reason: string } | null;
+ bestMove(): { a: Cell; b: Cell; score: number; reason: string } | null;
+ pulse(direction: string): Promise;
+ finishTurn(): Promise;
+ advance(ms: number): Snapshot;
+ audioEvents(): string[];
+ clearAudioEvents(): void;
+ cellRect(r: number, c: number): { x: number; y: number; w: number; h: number };
+};
+
+declare global {
+ interface Window { __lunarArrayTest: LunaApi; __lunaAudioStarts?: number }
+}
+
+const GAME = "/games/tile-matching/gpt-5-6-luna/index.html";
+const GRID = 8;
+const stableBoard = Array.from({ length: GRID }, (_, r) =>
+ Array.from({ length: GRID }, (_, c) => (r + c) % 6),
+);
+const clone = (board: number[][]) => board.map((row) => row.slice());
+
+function fourMatchBoard() {
+ const board = clone(stableBoard);
+ board[3] = [1, 1, 2, 1, 4, 5, 0, 2];
+ board[2][2] = 1;
+ return board;
+}
+
+function lMatchBoard() {
+ const board = clone(stableBoard);
+ board[4][4] = 5;
+ board[4][3] = 1;
+ board[4][5] = 1;
+ board[3][4] = 1;
+ board[2][4] = 1;
+ board[5][4] = 1;
+ return board;
+}
+
+function fiveMatchBoard() {
+ const board = clone(stableBoard);
+ board[6] = [2, 2, 5, 2, 2, 4, 5, 0];
+ board[5][2] = 2;
+ return board;
+}
+
+async function openGame(page: Page, viewport = { width: 1280, height: 720 }) {
+ await page.setViewportSize(viewport);
+ await page.goto(`${GAME}?test=1`);
+ await expect(page).toHaveTitle("LUNAR ARRAY // GPT 5.6 Luna");
+ await expect(page.locator("#titleScreen")).toHaveClass(/active/);
+ await expect.poll(() => page.evaluate(() => Boolean(window.__lunarArrayTest))).toBe(true);
+}
+
+const snapshot = (page: Page) => page.evaluate(() => window.__lunarArrayTest.snapshot());
+
+async function startMission(page: Page, index = 0) {
+ await page.evaluate((mission) => {
+ window.__lunarArrayTest.setSeed(7);
+ window.__lunarArrayTest.setSpeed(50);
+ window.__lunarArrayTest.startMission(mission);
+ }, index);
+}
+
+async function settle(page: Page) {
+ await expect.poll(async () => (await snapshot(page)).locked).toBe(false);
+}
+
+test.describe("GPT 5.6 Luna Tile Matching — Lunar Array", {
+ tag: ["@spec:tile-matching-luna", "@game:tile-matching/gpt-5-6-luna"],
+}, () => {
+ test("boots an 8x8 legal board with procedural audio", async ({ page }) => {
+ await page.addInitScript(() => {
+ window.__lunaAudioStarts = 0;
+ if (!("OscillatorNode" in window)) return;
+ const original = window.OscillatorNode.prototype.start;
+ window.OscillatorNode.prototype.start = function (...args) {
+ window.__lunaAudioStarts = (window.__lunaAudioStarts ?? 0) + 1;
+ return original.apply(this, args);
+ };
+ });
+ await openGame(page);
+ await startMission(page);
+ const state = await snapshot(page);
+ expect(state.grid).toBe(8);
+ expect(state.levelName).toBe("Perilune");
+ expect(state.moves).toBe(22);
+ expect(state.objective.kind).toBe("score");
+ expect(state.objective.target).toBe(3000);
+ expect(state.matchCount).toBe(0);
+ expect(state.hasMove).toBe(true);
+ expect(state.board.flat().filter(Boolean)).toHaveLength(64);
+ await expect.poll(() => page.evaluate(() => window.__lunaAudioStarts ?? 0)).toBeGreaterThan(2);
+ await expect(page.locator("#missionName")).toHaveText("Perilune");
+ await expect(page.locator("#hud")).not.toHaveAttribute("aria-live");
+ });
+
+ test("charges only valid swaps and cascades into a full board", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(12);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, stableBoard);
+ expect(await page.evaluate(() => window.__lunarArrayTest.swap(0, 0, 0, 1))).toBe(false);
+ expect((await snapshot(page)).moves).toBe(12);
+ await page.evaluate((board) => window.__lunarArrayTest.setBoard(board), fourMatchBoard());
+ expect(await page.evaluate(() => window.__lunarArrayTest.swap(2, 2, 3, 2))).toBe(true);
+ await settle(page);
+ const state = await snapshot(page);
+ expect(state.moves).toBe(11);
+ expect(state.score).toBeGreaterThan(0);
+ expect(state.board.flat().filter(Boolean).length).toBeGreaterThanOrEqual(60);
+ expect(state.hasMove).toBe(true);
+ });
+
+ test("forges beam, moonburst, and eclipse core specials", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ const constants = await page.evaluate(() => window.__lunarArrayTest.constants);
+ const cases = [
+ { board: fourMatchBoard(), swap: [2, 2, 3, 2] as const, specials: [constants.BEAM_ROW, constants.BEAM_COL] },
+ { board: lMatchBoard(), swap: [5, 4, 4, 4] as const, specials: [constants.BURST] },
+ { board: fiveMatchBoard(), swap: [5, 2, 6, 2] as const, specials: [constants.CORE] },
+ ];
+ for (const entry of cases) {
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setSeed(11);
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(20);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, entry.board);
+ expect(await page.evaluate(([r1, c1, r2, c2]) => window.__lunarArrayTest.swap(r1, c1, r2, c2), entry.swap)).toBe(true);
+ await settle(page);
+ const specials = (await snapshot(page)).board.flat().filter((tile) => tile && entry.specials.includes(tile.special));
+ expect(specials.length).toBeGreaterThan(0);
+ }
+ });
+
+ test("runs each special fusion and records its audio cue", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ const constants = await page.evaluate(() => window.__lunarArrayTest.constants);
+ const cases = [
+ [constants.BEAM_ROW, constants.BEAM_COL, "fusion:beam_beam"],
+ [constants.BEAM_ROW, constants.BURST, "fusion:beam_burst"],
+ [constants.BURST, constants.BURST, "fusion:burst_burst"],
+ [constants.CORE, constants.BEAM_ROW, "fusion:core_beam"],
+ [constants.CORE, constants.BURST, "fusion:core_burst"],
+ [constants.CORE, constants.CORE, "fusion:core_core"],
+ ] as const;
+ for (const [first, second, event] of cases) {
+ await page.evaluate(([board, a, b]) => {
+ window.__lunarArrayTest.setBoard(board, [{ r: 3, c: 3, special: a }, { r: 3, c: 4, special: b }]);
+ window.__lunarArrayTest.setMoves(20);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ window.__lunarArrayTest.clearAudioEvents();
+ }, [stableBoard, first, second]);
+ expect(await page.evaluate(() => window.__lunarArrayTest.swap(3, 3, 3, 4))).toBe(true);
+ await settle(page);
+ expect(await page.evaluate(() => window.__lunarArrayTest.audioEvents())).toContain(event);
+ expect((await snapshot(page)).score).toBeGreaterThan(0);
+ }
+ });
+
+ test("keeps Eclipse Gate's blocker target attainable", async ({ page }) => {
+ await openGame(page);
+ await startMission(page, 6);
+ const state = await snapshot(page);
+ expect(state.objective.target).toBe(4);
+ expect(state.blockers.flat().filter((layers) => layers > 0)).toHaveLength(4);
+ });
+
+ test("uses Lunar Pulse to chip edge blockers and redirect the next refill", async ({ page }) => {
+ await openGame(page);
+ await startMission(page, 4);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setBlockers([{ r: 0, c: 0, layers: 2 }, { r: 0, c: 1, layers: 1 }]);
+ window.__lunarArrayTest.setMoves(12);
+ window.__lunarArrayTest.setPulse(100);
+ window.__lunarArrayTest.setObjective({ kind: "blockers", target: 99, pulseTarget: 1 });
+ }, fourMatchBoard());
+ const before = await snapshot(page);
+ expect(await page.evaluate(() => window.__lunarArrayTest.pulse("up"))).toBe(true);
+ const after = await snapshot(page);
+ expect(after.moves).toBe(before.moves);
+ expect(after.pulse).toBe(0);
+ expect(after.pulseUses).toBe(1);
+ expect(after.gravity).toBe("up");
+ expect(after.blockers[0][0]).toBe(1);
+ expect(after.blockers[0][1]).toBe(0);
+ await page.evaluate(() => window.__lunarArrayTest.collapse());
+ expect((await snapshot(page)).gravity).toBe("down");
+ });
+
+ test("completes a mission when the final condition is supplied by Pulse", async ({ page }) => {
+ await openGame(page);
+ await startMission(page, 4);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setBlockers([{ r: 0, c: 0, layers: 1 }]);
+ window.__lunarArrayTest.setPulse(100);
+ window.__lunarArrayTest.setObjective({ kind: "blockers", target: 1, pulseTarget: 1 });
+ }, stableBoard);
+ expect(await page.evaluate(() => window.__lunarArrayTest.pulse("up"))).toBe(true);
+ await expect.poll(async () => (await snapshot(page)).screen).toBe("complete");
+ expect((await snapshot(page)).active).toBe(false);
+ });
+
+ test("keeps hints free, charges manual shuffles, and recovers dead boards", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(12);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, fourMatchBoard());
+ expect(await page.evaluate(() => window.__lunarArrayTest.hint(true))).not.toBeNull();
+ expect((await snapshot(page)).moves).toBe(12);
+ expect(await page.evaluate(() => window.__lunarArrayTest.shuffle(true))).toBe(true);
+ const state = await snapshot(page);
+ expect(state.moves).toBe(11);
+ expect(state.matchCount).toBe(0);
+ expect(state.hasMove).toBe(true);
+ });
+
+ test("ends a campaign when the final move is spent on a shuffle", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(1);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, fourMatchBoard());
+ expect(await page.evaluate(() => window.__lunarArrayTest.shuffle(true))).toBe(true);
+ await expect.poll(async () => (await snapshot(page)).screen).toBe("failed");
+ const state = await snapshot(page);
+ expect(state.moves).toBe(0);
+ expect(state.active).toBe(false);
+ });
+
+ test("pauses Orbit Run time and ends at 120 seconds", async ({ page }) => {
+ await openGame(page);
+ await page.evaluate(() => {
+ window.__lunarArrayTest.setSpeed(50);
+ window.__lunarArrayTest.startEndless();
+ window.__lunarArrayTest.setTime(120);
+ });
+ expect((await snapshot(page)).mode).toBe("endless");
+ await page.evaluate(() => window.__lunarArrayTest.advance(3000));
+ expect((await snapshot(page)).timeLeft).toBeCloseTo(117, 0);
+ await page.keyboard.press("p");
+ expect((await snapshot(page)).paused).toBe(true);
+ await page.evaluate(() => window.__lunarArrayTest.advance(5000));
+ expect((await snapshot(page)).timeLeft).toBeCloseTo(117, 0);
+ await page.keyboard.press("p");
+ await page.evaluate(() => window.__lunarArrayTest.advance(117000));
+ await expect.poll(async () => (await snapshot(page)).screen).toBe("endless");
+ expect((await snapshot(page)).active).toBe(false);
+ });
+
+ test("supports pointer swapping and keeps the active layout inside 1280x720", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(10);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, fourMatchBoard());
+ const from = await page.evaluate(() => window.__lunarArrayTest.cellRect(2, 2));
+ const to = await page.evaluate(() => window.__lunarArrayTest.cellRect(3, 2));
+ await page.mouse.click(from.x + from.w / 2, from.y + from.h / 2);
+ await page.mouse.click(to.x + to.w / 2, to.y + to.h / 2);
+ await settle(page);
+ expect((await snapshot(page)).score).toBeGreaterThan(0);
+ for (const selector of ["#board", "#missionName", "#objectiveValue", "#movesValue", "#scoreValue", "#pulseButton"]) {
+ const box = await page.locator(selector).boundingBox();
+ expect(box, selector).not.toBeNull();
+ expect(box!.x + box!.width).toBeLessThanOrEqual(1280);
+ expect(box!.y + box!.height).toBeLessThanOrEqual(720);
+ }
+ });
+
+ test("supports keyboard cursor swaps and touch drags", async ({ page }) => {
+ await openGame(page);
+ await startMission(page);
+ await page.evaluate((board) => {
+ window.__lunarArrayTest.setBoard(board);
+ window.__lunarArrayTest.setMoves(10);
+ window.__lunarArrayTest.setObjective({ kind: "score", target: 999999 });
+ }, fourMatchBoard());
+ await page.locator("#board").focus();
+ const best = await page.evaluate(() => window.__lunarArrayTest.bestMove());
+ expect(best).not.toBeNull();
+ if (!best) return;
+ const moveCursor = async (target: Cell) => {
+ let cursor = (await snapshot(page)).cursor;
+ while (cursor.r > target.r) { await page.keyboard.press("ArrowUp"); cursor = (await snapshot(page)).cursor; }
+ while (cursor.r < target.r) { await page.keyboard.press("ArrowDown"); cursor = (await snapshot(page)).cursor; }
+ while (cursor.c > target.c) { await page.keyboard.press("ArrowLeft"); cursor = (await snapshot(page)).cursor; }
+ while (cursor.c < target.c) { await page.keyboard.press("ArrowRight"); cursor = (await snapshot(page)).cursor; }
+ };
+ await moveCursor(best.a);
+ await page.keyboard.press("Enter");
+ expect((await snapshot(page)).selection).toEqual(best.a);
+ await moveCursor(best.b);
+ expect((await snapshot(page)).selection).toEqual(best.a);
+ await page.keyboard.press("Enter");
+ await settle(page);
+ expect((await snapshot(page)).score).toBeGreaterThan(0);
+
+ await page.evaluate((board) => window.__lunarArrayTest.setBoard(board), fourMatchBoard());
+ const [from, to] = await page.evaluate(() => [
+ window.__lunarArrayTest.cellRect(2, 2),
+ window.__lunarArrayTest.cellRect(3, 2),
+ ]);
+ await page.evaluate(([start, end]) => {
+ const canvas = document.querySelector("#board");
+ if (!canvas) throw new Error("board canvas missing");
+ const emit = (type: string, point: { x: number; y: number; w: number; h: number }) => canvas.dispatchEvent(new PointerEvent(type, {
+ bubbles: true,
+ clientX: point.x + point.w / 2,
+ clientY: point.y + point.h / 2,
+ pointerId: 1,
+ pointerType: "touch",
+ isPrimary: true,
+ }));
+ emit("pointerdown", start);
+ emit("pointermove", end);
+ emit("pointerup", end);
+ }, [from, to]);
+ await settle(page);
+ expect((await snapshot(page)).score).toBeGreaterThan(0);
+ });
+
+ test("stacks the HUD rails cleanly on a phone viewport", async ({ page }) => {
+ await openGame(page, { width: 390, height: 844 });
+ await startMission(page);
+ const leftRail = await page.locator("#leftRail").boundingBox();
+ const rightRail = await page.locator("#rightRail").boundingBox();
+ const board = await page.locator("#board").boundingBox();
+ expect(leftRail).not.toBeNull();
+ expect(rightRail).not.toBeNull();
+ expect(board).not.toBeNull();
+ if (leftRail && rightRail && board) {
+ expect(leftRail.x + leftRail.width).toBeLessThan(rightRail.x);
+ expect(board.x).toBeGreaterThanOrEqual(0);
+ expect(board.x + board.width).toBeLessThanOrEqual(390);
+ expect(board.y + board.height).toBeLessThanOrEqual(844);
+ }
+ });
+
+ test("does not expose the deterministic API outside test mode", async ({ page }) => {
+ await page.goto(GAME);
+ await expect(page.locator("#titleScreen")).toHaveClass(/active/);
+ expect(await page.evaluate(() => "__lunarArrayTest" in window)).toBe(false);
+ });
+});
| |