diff --git a/README.md b/README.md index 25173db..fca7f58 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ A web platform for comparing fun, cool-looking AI-generated games. Play each ver | 💎 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 | | 👾 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. | 5 | +| 🏎️ Neon Horizon Racer | Drift through a neon synthwave highway, dodge traffic, chain near misses, and chase checkpoints into the glowing horizon. | 6 | | 🏎️ Coastal Rush '86 | Race a red convertible through a branching, sun-soaked 1980s arcade road trip before the clock runs out. | 7 | | 🕰️ Clockwork Caper | Stage perfect heists across a clockwork museum by recording echoes of your past actions, outsmarting guards, and escaping before the final chime. | 3 | | 🏁 Sunbeam Kart Rally | Charge through a three-lap golden-hour festival circuit, drift past orchards, a windmill, forest, and vineyards, and battle up to five rivals with sun-powered items. | 2 | @@ -36,7 +36,7 @@ A web platform for comparing fun, cool-looking AI-generated games. Play each ver | Claude Fable 5 | 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 | N/A | N/A | 2,108 lines | N/A | N/A | N/A | | Claude Opus 4.6 | 852 lines | 1,009 lines | 1,166 lines | 1,467 lines | 1,133 lines | 984 lines | 1,092 lines | 1,101 lines | 1,162 lines | 1,347 lines | 1,820 lines | 1,547 lines | N/A | N/A | N/A | N/A | N/A | N/A | N/A | | Claude Opus 4.8 | 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 | N/A | N/A | 1,417 lines | N/A | N/A | N/A | -| Claude Opus 5 | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 4,648 lines | N/A | N/A | N/A | N/A | N/A | N/A | 3,321 lines | 3,570 lines | +| Claude Opus 5 | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 4,648 lines | N/A | N/A | N/A | 2,920 lines | N/A | N/A | 3,321 lines | 3,570 lines | | Claude Sonnet 4.6 | 828 lines | 809 lines | 973 lines | 1,167 lines | 900 lines | 727 lines | 1,108 lines | 1,384 lines | 1,313 lines | 1,362 lines | 1,239 lines | 1,203 lines | 937 lines | 814 lines | 1,450 lines | N/A | N/A | N/A | N/A | | 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 | diff --git a/games-metadata.json b/games-metadata.json index 3de0f06..fa16162 100644 --- a/games-metadata.json +++ b/games-metadata.json @@ -5502,6 +5502,26 @@ "icon": "🏎️", "accentColor": "#ff4fd8", "versions": [ + { + "model": "Claude Opus 5", + "modelId": "opus-5", + "date": "2026-07-29", + "linesOfCode": 2920, + "path": "/games/outrun-racer/opus-5/index.html", + "features": [ + "sound", + "music", + "3d", + "particles" + ], + "assets": { + "files": 2, + "bytes": 1058216, + "kinds": [ + "model" + ] + } + }, { "model": "OpenAI GPT 5.6 Sol", "modelId": "gpt-5-6-sol", diff --git a/public/games/outrun-racer/opus-5/assets/manifest.json b/public/games/outrun-racer/opus-5/assets/manifest.json new file mode 100644 index 0000000..7b180da --- /dev/null +++ b/public/games/outrun-racer/opus-5/assets/manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "ownerModelId": "opus-5", + "assets": [ + { + "path": "skyweave-fleet.glb", + "kind": "model", + "source": "procedural-export", + "generator": "Blender 5.2.0 LTS via Blender Assets MCP", + "prompt": "Author four original hovercraft with silhouettes that stay distinguishable at racing distance, each built by lofting authored cross-sections rather than assembling primitives. PlayerSkiff: a broad canard racer whose hull is lofted from nine hard-chined sections, with a ribbed glass canopy seated in a deck recess, a raised vented engine deck, shoulder intake scoops, swept canards rooted inside the hull and carrying emissive leading edges and mid-span nacelles, underslung ride plates with underglow, a dorsal stabiliser, and twin ducted thrusters built as surfaces of revolution with lipped shells, inner burner rings and centre hubs. RivalLance: a long narrow needle with forward-swept wings, ventral skids, a tall single fin and one oversized duct. RivalDelta: a broad flat delta planform with emissive leading-edge root extensions, outboard fins and three small ducts in a row. RivalHauler: a heavy catamaran of two parallel hulls joined by a bridge deck and nose beam, with four ducts and twin fins. Every craft shares one eight-material palette named SKY_HullPlate, SKY_HullTrim, SKY_Canopy, SKY_Graphite, SKY_LiveryBase, SKY_RailCyan, SKY_RailMagenta and SKY_RailAmber, so the runtime can retint livery surfaces per rival by material name. Noses point toward Blender +Y so they export to Three.js -Z forward; origins sit at the centre of mass on the ride plane. Bevel modifiers are baked on export. Named roots PlayerSkiff, RivalLance, RivalDelta and RivalHauler are exported as selected roots only to GLB with no UVs, no vertex colours, no textures, no animations and no compression.", + "purpose": "Player and rival hovercraft silhouettes for the four-craft Skyweave race grid", + "usageRights": "Procedurally authored for this repository with rights to use and redistribute", + "load": "initial" + }, + { + "path": "skyweave-structures.glb", + "kind": "model", + "source": "procedural-export", + "generator": "Blender 5.2.0 LTS via Blender Assets MCP", + "prompt": "Author the suspended-ribbon circuit kit and the drowned-city skyline that frames it, all from swept box sections, lofted profiles and surfaces of revolution. GateArch: a 29 metre checkpoint portal with a tapered outer arch, an emissive inner ring, radial bracing between the two arches, a lit tie beam, chunky footings and angled banner blades; its origin sits on the ribbon deck centre so it drops straight onto a track sample. BranchPylon: a leaning mast on a plinth carrying four cyan chevrons on one side and four magenta chevrons on the other, marking which way each fork leads. RibbonAnchor: the suspension spine that hangs beneath the deck, with outrigger arms, diagonal stays, tip pods and a ballast bulb, so the ribbon reads as carried rather than floating. CanopyTruss: a fifty metre overhead gantry with arched upper and lower chords, a zig-zag web, outboard legs and hanging route lamps that the ribbon passes beneath. HazardBollard: a small deck hazard with a flared skirt, an emissive warning band and three base fins. TowerBlock_A, TowerBlock_B and TowerBlock_C: three distinct megastructures for instanced skyline placement, respectively a 172 metre tapered slab spire with setbacks, banded window strips, a lit crown and a mast beacon; an 80 metre stepped ziggurat with amber bands, a roof deck, flanking buttresses and a severed skybridge reaching out over the flood; and a 119 metre cluster of three flooded silos of differing heights tied by service decks, ringed with light bands and capped with lit heads. Towers carry their origin at the base. All parts share the SKY_ material palette. Named roots GateArch, BranchPylon, RibbonAnchor, CanopyTruss, HazardBollard, TowerBlock_A, TowerBlock_B and TowerBlock_C are exported as selected roots only to GLB with no UVs, no vertex colours, no textures, no animations and no compression.", + "purpose": "Checkpoint portals, fork markers, ribbon suspension structure, route framing, deck hazards and instanced skyline landmarks", + "usageRights": "Procedurally authored for this repository with rights to use and redistribute", + "load": "initial" + } + ] +} diff --git a/public/games/outrun-racer/opus-5/assets/skyweave-fleet.glb b/public/games/outrun-racer/opus-5/assets/skyweave-fleet.glb new file mode 100644 index 0000000..9d58c0d Binary files /dev/null and b/public/games/outrun-racer/opus-5/assets/skyweave-fleet.glb differ diff --git a/public/games/outrun-racer/opus-5/assets/skyweave-structures.glb b/public/games/outrun-racer/opus-5/assets/skyweave-structures.glb new file mode 100644 index 0000000..4268400 Binary files /dev/null and b/public/games/outrun-racer/opus-5/assets/skyweave-structures.glb differ diff --git a/public/games/outrun-racer/opus-5/index.html b/public/games/outrun-racer/opus-5/index.html new file mode 100644 index 0000000..b15ba2b --- /dev/null +++ b/public/games/outrun-racer/opus-5/index.html @@ -0,0 +1,2919 @@ + + + + + +Neon Horizon Racer: Skyweave // Claude Opus 5 + + + +
+
+
+ + +
+
+
Lap
1/3
+
Position
4/4
+
Time
0:00.0
+
+
+
Sector
I
+
Gates
0
+
+
+
+
0M/S
+
Mag-lock nominal
+
+
+
+
Charge
+
+
+
Fork ahead — steer left to commit
Undercut
+
+
+ +
+
PAUSED
Esc to resume
+ + +
+
+

Neon Horizon Racer

+

Skyweave

+

+ A mag-locked skiff, a light-ribbon strung between drowned towers, and three rivals who will + not lift. The ribbon banks past vertical and rolls right through itself — stay locked, + ride the rail edge for charge, and spend it where it hurts them most. +

+
+
AD
SteerLean across the ribbon
+
WS
Throttle / BrakeLift for tight banking
+
Shift
TuckFaster, far less steering
+
Space
BoostBurns charge
+
C
HopClear bollards and forks
+
Esc
PauseSilences the run
+
+
+ + Charge builds from risk: rail edge, gate centres, close passes, and slipstream. +
+
+
Loading fleet and structures…
+
+
+ + + + + + + + + + + diff --git a/tests/games-load.spec.ts b/tests/games-load.spec.ts index 79da1e6..140b5f1 100644 --- a/tests/games-load.spec.ts +++ b/tests/games-load.spec.ts @@ -15,7 +15,7 @@ const ALL_GAMES = [ { id: "space-invaders", models: ["opus-4-6", "sonnet-4-6", "gpt-5-4", "gpt-5-4-mini", "gpt-5-5"] }, { id: "pac-man", models: ["sonnet-4-6", "gpt-5-4", "gpt-5-5"] }, { id: "sudoku", models: ["sonnet-4-6", "gpt-5-4"] }, - { id: "outrun-racer", models: ["gemma-4-12b", "gpt-5-6-sol"] }, + { id: "outrun-racer", models: ["gemma-4-12b", "gpt-5-6-sol", "opus-5"] }, { id: "coastal-rush-86", models: ["gpt-5-5", "gpt-5-4", "gpt-5-4-mini", "gpt-5-6-sol", "fable-5"] }, { id: "clockwork-caper", models: ["gpt-5-6-sol", "gpt-5-6-terra", "gpt-5-6-luna"] }, { id: "kart-racing", models: ["opus-5", "gpt-5-6-sol"] }, diff --git a/tests/outrun-racer-opus-5.spec.ts b/tests/outrun-racer-opus-5.spec.ts new file mode 100644 index 0000000..24aa153 --- /dev/null +++ b/tests/outrun-racer-opus-5.spec.ts @@ -0,0 +1,539 @@ +import { expect, test, type Page } from "@playwright/test"; + +const GAME = "/games/outrun-racer/opus-5/index.html"; + +type Snapshot = { + mode: string; + seed: number; + raceTime: number; + assetsReady: boolean; + track: { + lines: number; nodes: number; laps: number; gates: number; + branches: number; bollards: number; length: number; style: string; + }; + player: { + line: number; s: number; lateral: number; halfWidth: number; speed: number; + charge: number; boosting: boolean; tucking: boolean; airborne: boolean; hop: number; + lap: number; rank: number; gates: number; progress: number; finished: boolean; + branch: string | null; edgeStreak: number; bank: number; upY: number; sector: number; + }; + racers: Array<{ + id: string; ai: boolean; line: number; lap: number; rank: number; + speed: number; charge: number; progress: number; finished: boolean; + traits: { aggression: number; discipline: number; reflex: number; grip: number }; + }>; + economy: { edge: number; gate: number; prop: number; draft: number; spent: number }; + stats: { topSpeed: number; gatesThreaded: number; bestLap: number; longestEdge: number; chargeEarned: number }; + objects: { assetRoots: string[]; particles: number; towers: number; anchors: number }; +}; + +type Diagnostics = { + renderer: { calls: number; triangles: number; geometries: number; textures: number }; + scene: { meshes: number; materials: number; instancedMeshes: number }; + settings: { dpr: number; shadows: boolean; postPasses: number; audioPaused: boolean }; +}; + +/** + * Scoped to this spec rather than declared globally: several existing specs + * already augment `Window` with their own incompatible hook signatures, and + * adding another would only deepen that conflict. + */ +type SkyweaveWindow = Window & { + __THREE_GAME_TEST_HOOKS__: { + seed(seed: number): Snapshot; + setState(state: "title" | "active-play" | "fail" | "win" | "stress"): Snapshot; + advance(seconds: number): Snapshot; + snapshot(): Snapshot; + }; + __THREE_GAME_DIAGNOSTICS__: { snapshot(): Diagnostics }; + __SKYWEAVE_TEST__: { + placeAt(line: number, distance: number, lateral?: number): Snapshot; + placeInverted(): Snapshot; + setSpeed(v: number): Snapshot; + setCharge(c: number): Snapshot; + press(code: string, down?: boolean): Snapshot; + release(): Snapshot; + hop(): Snapshot; + boost(): Snapshot; + holdCentre(seconds: number, opts?: { tuck?: boolean; boost?: boolean; throttle?: number; brake?: number }): Snapshot; + setEdgeRide(): Snapshot; + threadGate(centred?: boolean): Snapshot; + setDraft(): Snapshot; + setScrape(): Snapshot; + takeBranch(which?: number): Snapshot; + runBranch(which?: number): { entered: number; exited: number; s: number; snapshot: Snapshot }; + finishRace(rank?: number): Snapshot; + trackReport(): { + length: number; samples: number; maxBank: number; minBank: number; + invertedSamples: number; + branchLengths: Array<{ name: string; raced: number; mainSpan: number }>; + }; + }; +}; + +async function openGame(page: Page) { + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto(`${GAME}?test=1`); + await expect(page.locator("#startButton")).toBeEnabled({ timeout: 25_000 }); + await expect + .poll(() => page.evaluate(() => Boolean((window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__)), { + timeout: 25_000, + }) + .toBe(true); +} + +/** Reset to a clean, stationary start on the main line for a focused probe. */ +async function primed(page: Page, seed = 7) { + return page.evaluate((s) => { + const w = window as unknown as SkyweaveWindow; + w.__THREE_GAME_TEST_HOOKS__.seed(s); + w.__THREE_GAME_TEST_HOOKS__.setState("active-play"); + w.__SKYWEAVE_TEST__.release(); + w.__SKYWEAVE_TEST__.placeAt(0, 300, 0); + w.__SKYWEAVE_TEST__.setSpeed(0); + return w.__SKYWEAVE_TEST__.setCharge(0); + }, seed); +} + +test.describe("Claude Opus 5 Neon Horizon Racer: Skyweave", () => { + test("loads the pinned runtime and Blender-authored assets without external requests", async ({ page }) => { + const external: string[] = []; + const errors: string[] = []; + const assets: string[] = []; + const runtime: string[] = []; + page.on("request", (request) => { + const url = new URL(request.url()); + if (url.protocol.startsWith("http") && !["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname)) { + external.push(request.url()); + } + if (url.pathname.includes("/assets/")) assets.push(url.pathname); + if (url.pathname.startsWith("/vendor/three/")) runtime.push(url.pathname); + }); + page.on("pageerror", (error) => errors.push(error.message)); + page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); + + await openGame(page); + + expect(external).toEqual([]); + expect(errors).toEqual([]); + expect(runtime).toEqual(expect.arrayContaining(["/vendor/three/0.185.1/three.module.min.js"])); + expect(assets).toEqual(expect.arrayContaining([ + "/games/outrun-racer/opus-5/assets/skyweave-fleet.glb", + "/games/outrun-racer/opus-5/assets/skyweave-structures.glb", + ])); + + const snapshot = await page.evaluate(() => + (window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__.snapshot()); + expect(snapshot.assetsReady).toBe(true); + expect(snapshot.objects.assetRoots).toEqual(expect.arrayContaining([ + "PlayerSkiff", "RivalLance", "RivalDelta", "RivalHauler", + "GateArch", "BranchPylon", "RibbonAnchor", "CanopyTruss", "HazardBollard", + "TowerBlock_A", "TowerBlock_B", "TowerBlock_C", + ])); + // one standalone file: no external scripts or stylesheets beyond the importmap + expect(await page.locator('script[src]:not([type="importmap"]), link[rel="stylesheet"][href]').count()).toBe(0); + }); + + test("keeps deterministic mutation controls private in normal play", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto(GAME); + await expect(page.locator("#startButton")).toBeEnabled({ timeout: 25_000 }); + expect(await page.evaluate(() => "__THREE_GAME_TEST_HOOKS__" in window)).toBe(false); + expect(await page.evaluate(() => "__THREE_GAME_DIAGNOSTICS__" in window)).toBe(false); + expect(await page.evaluate(() => "__SKYWEAVE_TEST__" in window)).toBe(false); + }); + + test("builds a closed ribbon circuit that rolls through full inversion", async ({ page }) => { + await openGame(page); + const { snapshot, report } = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + return { + snapshot: w.__THREE_GAME_TEST_HOOKS__.setState("active-play"), + report: w.__SKYWEAVE_TEST__.trackReport(), + }; + }); + + expect(snapshot.mode).toBe("race"); + expect(snapshot.track.style).toBe("suspended-ribbon-circuit"); + expect(snapshot.track.lines).toBe(3); + expect(snapshot.track.branches).toBe(2); + expect(snapshot.track.laps).toBe(3); + expect(snapshot.track.gates).toBeGreaterThanOrEqual(9); + expect(snapshot.track.bollards).toBeGreaterThan(10); + expect(snapshot.track.length).toBeGreaterThan(2000); + expect(snapshot.racers).toHaveLength(4); + expect(snapshot.racers.filter((r) => r.ai)).toHaveLength(3); + + // Two authored corkscrews, so the deck genuinely passes overhead. + expect(report.maxBank).toBeGreaterThan(Math.PI * 1.8); + expect(report.invertedSamples).toBeGreaterThan(40); + expect(report.branchLengths).toHaveLength(2); + for (const branch of report.branchLengths) expect(branch.raced).toBeGreaterThan(100); + }); + + test("a full race covers three complete circuits", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + H.seed(5); + H.setState("title"); + (document.getElementById("startButton") as HTMLButtonElement).click(); + const opening = H.snapshot(); + const laps: number[] = []; + let previous = opening.player.lap; + let guard = 0; + let snap = opening; + while (snap.mode !== "finished" && guard++ < 90) { + snap = T.holdCentre(4); + if (snap.player.lap !== previous) { laps.push(snap.player.lap); previous = snap.player.lap; } + } + return { + openingS: opening.player.s, + openingLap: opening.player.lap, + laps, + finished: snap.player.finished, + circuits: snap.player.progress / snap.track.length, + bestLap: snap.stats.bestLap, + raceTime: snap.raceTime, + }; + }); + + // The grid must line up past the start line; behind it, the opening + // crossing would bank a lap that was never raced. + expect(result.openingS).toBeGreaterThan(0); + expect(result.openingLap).toBe(1); + expect(result.laps).toEqual([2, 3, 4]); + expect(result.finished).toBe(true); + expect(result.circuits).toBeGreaterThan(2.9); + expect(result.circuits).toBeLessThan(3.3); + expect(result.bestLap).toBeGreaterThan(5); + expect(result.raceTime).toBeGreaterThan(result.bestLap * 2); + }); + + test("seeding rebuilds the world reproducibly", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const run = (seed: number) => { + w.__THREE_GAME_TEST_HOOKS__.seed(seed); + w.__THREE_GAME_TEST_HOOKS__.setState("active-play"); + w.__SKYWEAVE_TEST__.release(); + w.__SKYWEAVE_TEST__.press("KeyW"); + w.__SKYWEAVE_TEST__.press("KeyD"); + const snap = w.__THREE_GAME_TEST_HOOKS__.advance(6); + w.__SKYWEAVE_TEST__.release(); + return snap; + }; + const a = run(11); + const b = run(11); + const c = run(12); + return { + sameSeed: JSON.stringify(a.player) === JSON.stringify(b.player), + differentSeed: JSON.stringify(a.player) !== JSON.stringify(c.player), + traitsA: a.racers.map((r) => r.traits.reflex), + traitsC: c.racers.map((r) => r.traits.reflex), + seedEcho: c.seed, + }; + }); + expect(result.sameSeed).toBe(true); + expect(result.differentSeed).toBe(true); + expect(result.seedEcho).toBe(12); + expect(result.traitsA).not.toEqual(result.traitsC); + }); + + test("throttle builds speed and steering moves the craft both ways", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + const reset = () => { + H.seed(7); H.setState("active-play"); T.release(); + T.placeAt(0, 300, 0); T.setSpeed(0); T.setCharge(0); + }; + reset(); + T.press("KeyW"); + const rolling = H.advance(6).player.speed; + T.press("KeyD"); + const right = H.advance(1.4).player.lateral; + T.press("KeyD", false); + T.press("KeyA"); + const left = H.advance(2.4).player.lateral; + T.release(); + const braked = H.advance(0).player.speed; + T.press("KeyS"); + const afterBrake = H.advance(1.5).player.speed; + T.release(); + return { rolling, right, left, braked, afterBrake }; + }); + expect(result.rolling).toBeGreaterThan(30); + expect(result.right).toBeGreaterThan(1); + expect(result.left).toBeLessThan(result.right); + expect(result.afterBrake).toBeLessThan(result.braked); + }); + + test("tucking raises the ceiling and costs steering authority", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + const reset = () => { + H.seed(7); H.setState("active-play"); T.release(); + T.placeAt(0, 300, 0); T.setSpeed(0); T.setCharge(0); + }; + // Terminal speed is measured on the centre line: 25 s of lateral drift + // would otherwise pin the craft to a rail and mask the tuck ceiling. + reset(); + const plain = T.holdCentre(25).player.speed; + reset(); + const tucked = T.holdCentre(25, { tuck: true }).player; + reset(); + const boosted = T.holdCentre(25, { boost: true }).player.speed; + + reset(); + T.press("KeyW"); H.advance(10); T.press("KeyD"); + const openSteer = Math.abs(H.advance(1.5).player.lateral); + reset(); + T.press("KeyW"); T.press("ShiftLeft"); H.advance(10); T.press("KeyD"); + const tuckSteer = Math.abs(H.advance(1.5).player.lateral); + T.release(); + return { plain, tucked: tucked.speed, tucking: tucked.tucking, boosted, openSteer, tuckSteer }; + }); + expect(result.tucking).toBe(true); + expect(result.tucked).toBeGreaterThan(result.plain); + expect(result.boosted).toBeGreaterThan(result.tucked); + expect(result.tuckSteer).toBeLessThan(result.openSteer); + }); + + test("hopping leaves the deck and lands again", async ({ page }) => { + await openGame(page); + await primed(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + T.press("KeyW"); + H.advance(6); + const launched = T.hop().player; + const mid = H.advance(0.2).player; + const landed = H.advance(1.4).player; + T.release(); + return { launched, mid, landed }; + }); + expect(result.launched.airborne).toBe(true); + expect(result.mid.hop).toBeGreaterThan(0.2); + expect(result.landed.airborne).toBe(false); + expect(result.landed.hop).toBe(0); + }); + + test("each charge source credits only itself and boost spends the meter", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + const reset = () => { + H.seed(7); H.setState("active-play"); T.release(); + T.placeAt(0, 300, 0); T.setSpeed(0); T.setCharge(0); + }; + reset(); const edge = T.setEdgeRide().economy; + reset(); const centred = T.threadGate(true); + reset(); const offCentre = T.threadGate(false).economy; + reset(); const draft = T.setDraft().economy; + + reset(); + T.press("KeyW"); + H.advance(12); + T.setCharge(1); + const before = H.snapshot().player; + T.press("Space"); + const boosted = H.advance(2.5); + T.release(); + return { edge, centred: centred.economy, gates: centred.player.gates, offCentre, draft, before, boosted }; + }); + + expect(result.edge.edge).toBeGreaterThan(0); + expect(result.edge.gate).toBe(0); + expect(result.centred.gate).toBeGreaterThan(0); + expect(result.gates).toBeGreaterThan(0); + // A centred thread pays more than a shoulder-of-the-arch one. + expect(result.centred.gate).toBeGreaterThan(result.offCentre.gate); + expect(result.draft.draft).toBeGreaterThan(0); + + expect(result.boosted.player.boosting).toBe(true); + expect(result.boosted.player.speed).toBeGreaterThan(result.before.speed); + expect(result.boosted.player.charge).toBeLessThan(1); + expect(result.boosted.economy.spent).toBeGreaterThan(0); + }); + + test("rail contact scrubs speed but the run always recovers", async ({ page }) => { + await openGame(page); + await primed(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + T.press("KeyW"); + const before = H.advance(10).player.speed; + const scraped = T.setScrape().player; + const recovered = H.advance(5).player; + T.release(); + return { before, scraped, recovered, mode: H.snapshot().mode }; + }); + expect(result.mode).toBe("race"); + expect(result.scraped.finished).toBe(false); + expect(Math.abs(result.recovered.lateral)).toBeLessThanOrEqual(result.recovered.halfWidth); + expect(result.recovered.speed).toBeGreaterThan(result.before * 0.5); + expect(Number.isFinite(result.recovered.speed)).toBe(true); + }); + + test("the craft stays mag-locked through a full inversion", async ({ page }) => { + await openGame(page); + await primed(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + const placed = T.placeInverted().player; + T.press("KeyW"); + const after = H.advance(0.8).player; + T.release(); + return { placed, after }; + }); + expect(result.placed.upY).toBeLessThan(-0.7); + expect(Math.abs(result.placed.lateral)).toBeLessThanOrEqual(result.placed.halfWidth); + expect(Number.isFinite(result.after.lateral)).toBe(true); + expect(Number.isFinite(result.after.speed)).toBe(true); + expect(Math.abs(result.after.lateral)).toBeLessThanOrEqual(result.after.halfWidth); + }); + + test("both branch lines fork away and rejoin the main circuit", async ({ page }) => { + await openGame(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + const run = (which: number) => { + H.seed(7); H.setState("active-play"); T.release(); + const r = T.runBranch(which); + return { entered: r.entered, exited: r.exited, branch: r.snapshot.player.branch }; + }; + return { one: run(1), two: run(2) }; + }); + expect(result.one.entered).toBe(1); + expect(result.one.exited).toBe(0); + expect(result.two.entered).toBe(2); + expect(result.two.exited).toBe(0); + expect(result.one.branch).toBeNull(); + }); + + test("finishing shows the placement board and restarting re-arms the countdown", async ({ page }) => { + await openGame(page); + const won = await page.evaluate(() => + (window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__.setState("win")); + expect(won.mode).toBe("finished"); + expect(won.player.rank).toBe(1); + await expect(page.locator("#finishScreen")).toBeVisible(); + await expect(page.locator("#placeBig")).toContainText("1"); + await expect(page.locator("#finishOrder .r")).toHaveCount(4); + + const lost = await page.evaluate(() => + (window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__.setState("fail")); + expect(lost.player.rank).toBe(4); + await expect(page.locator("#placeBig")).toContainText("4"); + + await page.locator("#restartButton").click(); + await expect(page.locator("#finishScreen")).toBeHidden(); + await expect(page.locator("#hud")).toHaveClass(/visible/); + await expect(page.locator("#countdown")).toHaveText("3"); + expect(await page.evaluate(() => + (window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__.snapshot().mode)).toBe("countdown"); + }); + + test("pausing halts the simulation and silences the run", async ({ page }) => { + await openGame(page); + await page.evaluate(() => (window as unknown as SkyweaveWindow).__THREE_GAME_TEST_HOOKS__.setState("active-play")); + await page.keyboard.press("Escape"); + await expect(page.locator("#pauseBadge")).toHaveClass(/show/); + const paused = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const before = w.__THREE_GAME_TEST_HOOKS__.snapshot(); + const after = w.__THREE_GAME_TEST_HOOKS__.advance(2); + return { + mode: after.mode, + moved: Math.abs(after.player.s - before.player.s), + audioPaused: w.__THREE_GAME_DIAGNOSTICS__.snapshot().settings.audioPaused, + }; + }); + expect(paused.mode).toBe("paused"); + expect(paused.moved).toBeLessThan(0.001); + expect(paused.audioPaused).toBe(true); + + await page.keyboard.press("Escape"); + await expect(page.locator("#pauseBadge")).not.toHaveClass(/show/); + expect(await page.evaluate(() => + (window as unknown as SkyweaveWindow).__THREE_GAME_DIAGNOSTICS__.snapshot().settings.audioPaused)).toBe(false); + }); + + test("starts cleanly when Web Audio is unavailable", async ({ page }) => { + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + await page.addInitScript(() => { + Object.defineProperty(window, "AudioContext", { value: undefined, configurable: true }); + Object.defineProperty(window, "webkitAudioContext", { value: undefined, configurable: true }); + }); + await openGame(page); + await page.locator("#startButton").click(); + await expect(page.locator("#hud")).toHaveClass(/visible/); + const snapshot = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + w.__SKYWEAVE_TEST__.press("KeyW"); + const s = w.__THREE_GAME_TEST_HOOKS__.advance(4); + w.__SKYWEAVE_TEST__.release(); + return s; + }); + expect(["countdown", "race"]).toContain(snapshot.mode); + expect(errors).toEqual([]); + }); + + test("losing focus releases every held control", async ({ page }) => { + await openGame(page); + await primed(page); + const result = await page.evaluate(() => { + const w = window as unknown as SkyweaveWindow; + const H = w.__THREE_GAME_TEST_HOOKS__; + const T = w.__SKYWEAVE_TEST__; + T.press("KeyW"); T.press("KeyD"); T.press("ShiftLeft"); + const held = H.advance(5).player; + window.dispatchEvent(new Event("blur")); + const drifting = H.advance(2).player; + return { held, drifting }; + }); + expect(result.held.speed).toBeGreaterThan(20); + expect(result.drifting.tucking).toBe(false); + expect(result.drifting.boosting).toBe(false); + expect(result.drifting.speed).toBeLessThan(result.held.speed); + expect(result.drifting.speed).toBeGreaterThan(0); + }); + + test("renders with deliberate budgets and an instanced world", async ({ page }) => { + await openGame(page); + const diagnostics = await page.evaluate(async () => { + const w = window as unknown as SkyweaveWindow; + w.__THREE_GAME_TEST_HOOKS__.setState("active-play"); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + return w.__THREE_GAME_DIAGNOSTICS__.snapshot(); + }); + expect(diagnostics.settings.dpr).toBeLessThanOrEqual(1.5); + expect(diagnostics.settings.shadows).toBe(false); + expect(diagnostics.settings.postPasses).toBe(0); + // Skyline, ribbon anchors and deck hazards are all instanced kits. + expect(diagnostics.scene.instancedMeshes).toBeGreaterThanOrEqual(3); + expect(diagnostics.scene.meshes).toBeGreaterThan(40); + expect(diagnostics.renderer.calls).toBeGreaterThan(0); + expect(diagnostics.renderer.triangles).toBeGreaterThan(10_000); + }); +});