diff --git a/README.md b/README.md index 23aa2e8..52f13cf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A web platform for comparing fun, cool-looking AI-generated games. Play each ver | 💣 Minesweeper | Every click is a calculated gamble. Use logic to uncover safe tiles while avoiding hidden explosives. | 5 | | 🧱 Tetris | Falling tetrominoes demand split-second decisions. Stack them perfectly or watch entropy consume the board. | 6 | | ⚫ Reversi | A war of strategic placement. Flip your opponent's pieces and dominate the 8×8 battlefield. | 6 | -| 🏓 Breakout | Shatter neon bricks with a blazing ball. Catch power-ups, chain combos, and clear every level before your lives run out. | 6 | +| 🏓 Breakout | Shatter neon bricks with a blazing ball. Catch power-ups, chain combos, and clear every level before your lives run out. | 7 | | 🔢 2048 | Slide, merge, and strategize on a 4×4 grid. Chase the elusive 2048 tile before the board fills up. | 7 | | 🏃 Endless Runner 3D | A neon-soaked 3D sprint through an endless cyberpunk corridor with obstacles, collectibles, and power-ups. | 6 | | 🔮 Marble Madness | Roll a glowing marble across neon platforms suspended in space. Master physics, dodge hazards, and race the clock. | 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 | 893 lines | 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 | 492 lines | 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 11eeb7d..25003db 100644 --- a/games-metadata.json +++ b/games-metadata.json @@ -2154,6 +2154,19 @@ ] } ] + }, + { + "model": "GPT 5.6 Luna", + "modelId": "gpt-5-6-luna", + "date": "2026-07-31", + "linesOfCode": 492, + "path": "/games/breakout/gpt-5-6-luna/index.html", + "features": [ + "sound", + "music", + "particles", + "powerups" + ] } ] }, diff --git a/public/games/breakout/gpt-5-6-luna/index.html b/public/games/breakout/gpt-5-6-luna/index.html new file mode 100644 index 0000000..eb0d9ca --- /dev/null +++ b/public/games/breakout/gpt-5-6-luna/index.html @@ -0,0 +1,491 @@ + + + + + + + LUNAR SHATTER // BREAKOUT + + + + + + +
+
+
+

LUNAR ARRAY / ORBITAL DEMOLITION

+

LUNAR SHATTER // BREAKOUT

+
+
AWAITING SIGNAL
+
+ +
+
+
Score
000000
+
Level
01
+
Lives
♥♥♥
+
Combo
+
+
+ + +
+
+
+ +
+
+ +

THE ECLIPSE
BEGINS

+

Break the crystal array. Build your combo.
Catch the lunar gifts before they fall into the void.

+ +
A / D OR ← → STEER · SPACE LAUNCH · P PAUSE · M AUDIO
PRISM · LENS · NOVA · TIDE · MAGNET · AEGIS
+
+
+
+

ORBIT
PAUSED

The observatory is holding position. Your combo is safe.

PRESS P TO RESUME
+
+
+
01

FIRST LIGHT

The first crystals are waiting in the dark.

CALIBRATING ORBITAL TRAJECTORY
+
+
+

SIGNAL
LOST

The array outlasted the pilot.

Score000000
Level01
Best000000
PRESS R TO TRY AGAIN
+
+
+
S-RANK

UMBRA
SHATTERED

Six arrays cleared. The lunar archive is yours.

Score000000
Best000000
Medals0 / 6
THE ARRAY WILL REMEMBER
+
+
+ + + + diff --git a/tests/breakout-luna.spec.ts b/tests/breakout-luna.spec.ts new file mode 100644 index 0000000..f4f7994 --- /dev/null +++ b/tests/breakout-luna.spec.ts @@ -0,0 +1,211 @@ +import { expect, test, type Page } from "@playwright/test"; + +const GAME_PATH = "/games/breakout/gpt-5-6-luna/index.html"; +const TEST_PATH = `${GAME_PATH}?test=1`; + +type LunaSnapshot = { + screen: string; + score: number; + highScore: number; + level: number; + lives: number; + combo: number; + bricksRemaining: number; + paddle: { x: number; targetX: number; y: number; w: number }; + balls: Array<{ x: number; y: number; vx: number; vy: number; stuck: boolean; captured: boolean; nova: boolean }>; + activePowerups: Record; + layout: { width: number; height: number; boardX: number; boardY: number; boardW: number; boardH: number; brickTop: number; paddleY: number }; + medals: string[]; + muted: boolean; +}; + +async function snapshot(page: Page): Promise { + return page.evaluate(() => { + const api = (window as typeof window & { + __lunaBreakoutTest: { snapshot: () => LunaSnapshot }; + }).__lunaBreakoutTest; + return api.snapshot(); + }); +} + +async function advance(page: Page, milliseconds: number) { + return page.evaluate((ms) => { + const api = (window as typeof window & { __lunaBreakoutTest: { advance: (value: number) => LunaSnapshot } }).__lunaBreakoutTest; + return api.advance(ms); + }, milliseconds); +} + +test.describe("GPT 5.6 Luna Breakout", { + tag: ["@spec:breakout-luna", "@game:breakout/gpt-5-6-luna"], +}, () => { + test("loads the lunar title state without page errors", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + + await page.goto(GAME_PATH); + + await expect(page).toHaveTitle("LUNAR SHATTER // BREAKOUT"); + await expect(page.locator('[data-testid="breakout-canvas"]')).toBeVisible(); + await expect(page.locator('[data-testid="start-button"]')).toBeVisible(); + expect(errors).toEqual([]); + }); + + test("starts audio, moves by pointer, and supports pause and mute", async ({ page }) => { + await page.addInitScript(() => { + const root = window as typeof window & { __lunaAudioStarts?: number }; + root.__lunaAudioStarts = 0; + if (!("OscillatorNode" in window)) return; + const proto = window.OscillatorNode.prototype; + const originalStart = proto.start; + proto.start = function (...args) { + root.__lunaAudioStarts = (root.__lunaAudioStarts ?? 0) + 1; + return originalStart.apply(this, args); + }; + }); + + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto(TEST_PATH); + await page.locator('[data-testid="start-button"]').click(); + await advance(page, 1600); + + const before = await snapshot(page); + await page.mouse.move(120, 400); + await advance(page, 120); + const after = await snapshot(page); + + expect(after.screen).toBe("playing"); + expect(after.paddle.targetX).toBeLessThan(before.paddle.targetX); + expect(after.paddle.x).toBeLessThan(before.paddle.x); + await page.keyboard.press("Space"); + await expect.poll(() => page.evaluate(() => (window as typeof window & { __lunaAudioStarts?: number }).__lunaAudioStarts ?? 0)).toBeGreaterThan(0); + + await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { forcePowerUp: (type: string) => LunaSnapshot } }).__lunaBreakoutTest; + api.forcePowerUp("lens"); + }); + await page.locator('[data-testid="pause-button"]').click(); + await expect(page.locator("#statusText")).toHaveText("HOLDING POSITION"); + const lensTimer = (await snapshot(page)).activePowerups.lens; + await advance(page, 1000); + expect((await snapshot(page)).activePowerups.lens).toBe(lensTimer); + await page.locator("#resumeButton").click(); + await advance(page, 1000); + expect((await snapshot(page)).activePowerups.lens).toBeLessThan(lensTimer); + await page.keyboard.press("m"); + expect((await snapshot(page)).muted).toBe(true); + await page.locator('[data-testid="audio-button"]').click(); + expect((await snapshot(page)).muted).toBe(false); + }); + + test("exposes deterministic collision, combo, power-up, shield, and game-over hooks", async ({ page }) => { + await page.goto(TEST_PATH); + await page.locator('[data-testid="start-button"]').click(); + await advance(page, 1600); + + const initial = await snapshot(page); + const brick = { + x: initial.layout.boardX + initial.layout.boardW / 2 - 32, + y: initial.layout.brickTop + 28, + w: 64, + h: 25, + type: "crystal", + hp: 1, + }; + await page.evaluate((entry) => { + const api = (window as typeof window & { __lunaBreakoutTest: { setBricks: (value: unknown[]) => void; setBall: (value: unknown) => void } }).__lunaBreakoutTest; + api.setBricks([entry]); + api.setBall({ x: entry.x + entry.w / 2, y: entry.y + 48, vx: 0, vy: -340, stuck: false }); + }, brick); + const collision = await advance(page, 220); + + expect(collision.score).toBeGreaterThan(0); + expect(collision.combo).toBe(1); + expect(collision.bricksRemaining).toBe(0); + expect(collision.highScore).toBe(collision.score); + + const effects = await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { forcePowerUp: (type: string) => LunaSnapshot } }).__lunaBreakoutTest; + const lens = api.forcePowerUp("lens"); + const nova = api.forcePowerUp("nova"); + return { lens, nova }; + }); + expect(effects.lens.activePowerups.lens).toBeGreaterThan(0); + expect(effects.lens.paddle.w).toBeGreaterThan(initial.paddle.w); + expect(effects.nova.activePowerups.nova).toBeGreaterThan(0); + expect(effects.nova.balls[0].nova).toBe(true); + + await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { setLevel: (value: number) => LunaSnapshot; forcePowerUp: (type: string) => LunaSnapshot; setBall: (value: unknown) => LunaSnapshot } }).__lunaBreakoutTest; + const level = api.setLevel(1); + api.forcePowerUp("magnet"); + return api.setBall({ x: level.paddle.x, y: level.paddle.y - 60, vx: 0, vy: 340, stuck: false, captured: false }); + }); + const magnetCatch = await advance(page, 220); + expect(magnetCatch.balls[0].captured).toBe(true); + expect(magnetCatch.balls[0].vx).toBe(0); + const relaunched = await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { launch: () => boolean } }).__lunaBreakoutTest; + return api.launch(); + }); + expect(relaunched).toBe(true); + expect((await snapshot(page)).balls[0].captured).toBe(false); + + await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { setLives: (value: number) => void; forcePowerUp: (type: string) => void; setBall: (value: unknown) => void; clearAudioEvents: () => void } }).__lunaBreakoutTest; + api.setLives(1); + api.forcePowerUp("aegis"); + api.clearAudioEvents(); + api.setBall({ x: 640, y: 760, vx: 0, vy: 340, stuck: false }); + }); + const shieldSave = await advance(page, 40); + expect(shieldSave.screen).toBe("playing"); + expect(shieldSave.lives).toBe(1); + expect(shieldSave.activePowerups.aegis).toBeUndefined(); + expect(await page.evaluate(() => (window as typeof window & { __lunaBreakoutTest: { audioEvents: () => Array<{ name: string }> } }).__lunaBreakoutTest.audioEvents().some((event) => event.name === "shieldSave"))).toBe(true); + + await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { setBall: (value: unknown) => void } }).__lunaBreakoutTest; + api.setBall({ x: 640, y: 760, vx: 0, vy: 340, stuck: false }); + }); + const over = await advance(page, 40); + expect(over.screen).toBe("gameover"); + expect(over.lives).toBe(0); + await expect(page.locator('[data-testid="restart-button"]')).toBeVisible(); + }); + + test("presents the final rank and medal summary after the last stage clears", async ({ page }) => { + await page.goto(TEST_PATH); + await page.locator('[data-testid="start-button"]').click(); + await advance(page, 1600); + + await page.evaluate(() => { + const api = (window as typeof window & { __lunaBreakoutTest: { setLevel: (value: number) => void; setBricks: (value: unknown[]) => void; setBall: (value: unknown) => void } }).__lunaBreakoutTest; + api.setLevel(6); + api.setBricks([{ x: 600, y: 170, w: 64, h: 25, type: "standard", hp: 1 }]); + api.setBall({ x: 632, y: 220, vx: 0, vy: -340, stuck: false }); + }); + await advance(page, 260); + + await expect(page.locator("#victoryOverlay")).toBeVisible(); + await expect(page.locator("#finalRank")).toHaveText(/[B-S]-RANK/); + await expect(page.locator("#victoryMedals")).toHaveText("6 / 6"); + }); + + test("keeps the playfield inside desktop and compact viewports", async ({ page }) => { + for (const viewport of [{ width: 1280, height: 720 }, { width: 320, height: 480 }]) { + await page.setViewportSize(viewport); + await page.goto(TEST_PATH); + const canvas = page.locator('[data-testid="breakout-canvas"]'); + const box = await canvas.boundingBox(); + expect(box).not.toBeNull(); + if (box) { + expect(box.x).toBeGreaterThanOrEqual(0); + expect(box.y).toBeGreaterThanOrEqual(0); + expect(box.x + box.width).toBeLessThanOrEqual(viewport.width + 1); + expect(box.y + box.height).toBeLessThanOrEqual(viewport.height + 1); + } + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(viewport.width); + expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBeLessThanOrEqual(viewport.height); + } + }); +});