Vault Light
Top-down stealth heist in a neon manor, built on one promise: the cone painted on the floor is exactly the volume that can see you. Guards patrol straight routes and their gaze stops at the first wall, so walls are real cover rather than decoration, and the shadow pools are the only guaranteed hiding inside an open room. Sight raises an alert guard who walks to where you ARE; the noise of a sprint only draws one to where you WERE, so running is a trade, not a default. Take every bag, then reach the vault door; the suspicion meter that fills you up is the same number that catches you.
* THE GAME RUNS IN A SANDBOXED FRAME — IT CAN’T SEE THIS SITE OR YOUR OTHER TABS. BEST SCORES STAY IN THIS BROWSER.
Cartridge promptTHE EXACT PROMPT THAT BUILT THIS GAME
Build a tiny browser game called "Vault Light".
Game spec
---------
A single-file top-down stealth cartridge on a fixed 800x450 canvas, a 25x14 grid of 32px cells, playable with keyboard, mouse or touch. THE CENTRAL CONTRACT is that the painted cone and the detection test are the same code: one rayHit(guard, angle) marches outward until a wall, the renderer fills a 40-step fan of rayHit endpoints across the field of view, and sees() asks only whether the point lies inside the fan's angles and no further than rayHit at that angle - so the two cannot drift into the classic genre failure of being caught while standing in what the screen swore was safe. That is asserted, not assumed: a probe samples random guard poses and angles and fails if the fan it drew ever disagrees with the answer it acted on, and it mutation-tests itself by deleting the wall check to prove it can fail. Wall occlusion is checked twice, once logically over every guard and once by reading pixels off the canvas (the cell before a wall is lit, the cell behind it is not). Detection feeds ONE suspicion accumulator that also drives capture and the meter, so nothing is decided off-screen, and it fills faster the closer the guard. Guards run a patrol/hunt/search machine: seeing you sets a hunt target at your position, while sprint noise only sets a search target at the place you made it, which makes sprinting (178px/s vs 104) a real trade against a noise radius of 96-156px against 190-260 quiet, and an alerted guard moves 1.45x faster for 4s. Shadow pools are one array that both the renderer and the visibility test read, so a pool can never be painted over a spot that does not actually hide you. Levels are generated and validated by scripts/scratch/vl-gen.py, which authors walls as rectangles, asserts every patrol route is straight-line walkable, and refuses to emit a floor where a bag, a pool or the door is unreachable from the start; looting every bag is what unlocks the vault door. Difficulty (fov 1.05/1.28/1.5, range 128/152/176, speed 42/54/66, spot rate, hunt memory, lives 4/3/2) is measured rather than asserted: a route-finder reports how much of each floor's open cells a patrol can ever light (35% on easy rising to 57% on hard, so guards are never scenery) and how much suspicion an optimal route accumulates (no single crossing is unsurvivable), while a calibrated player model finishes 88%/63%/38% of runs across the three difficulties with a deliberate death count on hard, and an idle player never opens a vault at any difficulty. Movement is owned by the simulation: keyboard and pointer both only set the same held-key flags, and tap-to-walk plans a real 4-connected BFS cell path using the same body-radius wall test the mover uses, so a tap around a corner arrives instead of grinding the player into the partition (a measured 57px stall before this existed, and the check is mutation-tested by swapping the pathfinder for a straight line). Audio is synthesised in-file, the score leaves via postMessage to the parent, and there are no external requests.
Hard constraints — the game will be embedded in a sandboxed iframe on a static gallery site, and a headless browser will screenshot it:
1. Output exactly ONE self-contained HTML file. All CSS and JS inline. Zero external requests: no CDNs, no external fonts, images, or sounds, no fetch/XHR/WebSocket. Any sound must be synthesized with WebAudio.
2. The iframe sandbox is "allow-scripts allow-pointer-lock" (plus an autoplay permission). So: no localStorage, cookies, or indexedDB (opaque origin — access throws); no popups, window.open, or navigation; no access to the parent page; alert()/prompt()/confirm() are silently blocked.
3. The playfield must be visible within 800 ms of load — that is when the screenshot is taken. No dead white screen. If there is a start screen, make it visually rich.
4. Controls: keyboard AND mouse must both work, plus on-screen touch buttons for phones. Call preventDefault() on game keys (arrows, space) so the page never scrolls.
5. The view is 16:9 and must scale to fill the viewport with no overflow or scrollbars.
6. Show a visible score. If the genre has a losing state, show a game-over screen with a restart (button and a key). Idle/clicker games skip game over but still need clear numeric feedback.
7. If you track a score, report it to the arcade shell so the player's best is remembered between visits: on every score change, call parent.postMessage({ source: "ai-arcade", type: "score", score: <number> }, "*"). It is safe to call it no matter what the parent does with it.
8. Keep it tiny: aim for under 400 lines, 60 fps, no frameworks. That 400-line target does not apply to 3D games — true 3D (a camera and perspective-projected world) or pseudo-3D (projected road/track, depth-sorted sprites) — which need their own software renderer and typically land at 800–1400 lines. The rest of this constraint still binds every game: 60 fps, no frameworks, no external assets, and no padding to reach a length.
9. Difficulty: the arcade shell passes the player's chosen difficulty as a URL query parameter. Read it with new URLSearchParams(location.search).get("difficulty") — the value is "easy", "medium", or "hard" (missing or unknown means "medium"). Make the three levels meaningfully different — speed, lives, enemy pressure, reaction time, whatever fits the genre; clicker/idle games with no losing state use starting resources and upgrade cost curves instead. Show the active level in the HUD. Without the parameter the game must be fully playable at its default (medium) tuning.
Return only the complete HTML file, no commentary.