| > **Abandoned path:** We gave up on this as a from-scratch game-generation path. |
| > These patterns and examples remain here as reference/source material for the |
| > pivot, especially template remixing. |
|
|
| # Game Patterns for Single-File HTML Games |
|
|
| Lean browser-native patterns for generated games. Use one `.html` file. Use raw Canvas 2D, or three.js by CDN importmap for 3D. No build step, multiple files, backend runtime, alternate engines, multiplayer stacks, distribution notes, or architecture essays. |
|
|
| ## Single Game Loop |
|
|
| Use exactly one `requestAnimationFrame` loop. Clamp delta-time so tab switches do not launch entities through walls. |
|
|
| ```javascript |
| let lastTime = performance.now(); |
|
|
| function update(dt) { |
| // dt is seconds. Move with pixels-per-second values: |
| player.x += player.vx * dt; |
| player.y += player.vy * dt; |
| } |
|
|
| function render() { |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| // draw background, world, actors, particles, HUD in that order |
| } |
|
|
| function loop(now) { |
| const dt = Math.min((now - lastTime) / 1000, 0.05); |
| lastTime = now; |
| update(dt); |
| render(); |
| requestAnimationFrame(loop); |
| } |
|
|
| requestAnimationFrame(loop); |
| ``` |
|
|
| Fixed timestep only when physics must be deterministic: |
|
|
| ```javascript |
| const STEP = 1 / 60; |
| let previous = performance.now(); |
| let accumulator = 0; |
|
|
| function frame(now) { |
| accumulator += Math.min((now - previous) / 1000, 0.1); |
| previous = now; |
| while (accumulator >= STEP) { |
| update(STEP); |
| accumulator -= STEP; |
| } |
| render(); |
| requestAnimationFrame(frame); |
| } |
|
|
| requestAnimationFrame(frame); |
| ``` |
|
|
| ## Canvas Setup and DPR Resize |
|
|
| CSS size controls layout. Backing-store size uses device pixels to avoid blur. |
|
|
| ```javascript |
| const canvas = document.querySelector("canvas"); |
| const ctx = canvas.getContext("2d"); |
|
|
| function resizeCanvas() { |
| const dpr = Math.max(1, window.devicePixelRatio || 1); |
| const rect = canvas.getBoundingClientRect(); |
| canvas.width = Math.round(rect.width * dpr); |
| canvas.height = Math.round(rect.height * dpr); |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); |
| game.width = rect.width; |
| game.height = rect.height; |
| } |
|
|
| window.addEventListener("resize", resizeCanvas); |
| resizeCanvas(); |
| ``` |
|
|
| Minimal single-file shell: |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <style> |
| html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; } |
| canvas { display: block; width: 100vw; height: 100vh; background: #0b1020; } |
| </style> |
| </head> |
| <body> |
| <canvas></canvas> |
| <script> |
| const game = {}; |
| const canvas = document.querySelector("canvas"); |
| const ctx = canvas.getContext("2d"); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| ## Canvas Draw Essentials |
|
|
| Clear every frame. Draw back-to-front. Use `save()`/`restore()` around transforms. |
|
|
| ```javascript |
| ctx.clearRect(0, 0, game.width, game.height); |
|
|
| ctx.fillStyle = "#68d391"; |
| ctx.fillRect(platform.x, platform.y, platform.w, platform.h); |
|
|
| ctx.strokeStyle = "#ffffff"; |
| ctx.strokeRect(goal.x, goal.y, goal.w, goal.h); |
|
|
| ctx.save(); |
| ctx.translate(player.x + player.w / 2, player.y + player.h / 2); |
| ctx.rotate(player.angle); |
| ctx.fillStyle = "#ffd166"; |
| ctx.fillRect(-player.w / 2, -player.h / 2, player.w, player.h); |
| ctx.restore(); |
| ``` |
|
|
| ## Draw Images and Spritesheets |
|
|
| Use `drawImage` for whole images or source rectangles from a spritesheet. |
|
|
| ```javascript |
| const image = new Image(); |
| image.src = "data:image/png;base64,PUT_BASE64_HERE"; |
|
|
| function drawSprite(entity) { |
| ctx.drawImage(image, entity.x, entity.y, entity.w, entity.h); |
| } |
|
|
| function drawFrame(sheet, frame, x, y, w, h) { |
| const sx = frame * w; |
| ctx.drawImage(sheet, sx, 0, w, h, x, y, w, h); |
| } |
| ``` |
|
|
| ## Keyboard Held-State Map |
|
|
| Track held keys with `keydown` and `keyup`. Read the map in `update`. |
|
|
| ```javascript |
| const keys = Object.create(null); |
|
|
| function setKey(e, down) { |
| keys[e.code] = down; |
| if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Space"].includes(e.code)) { |
| e.preventDefault(); |
| } |
| } |
|
|
| window.addEventListener("keydown", (e) => setKey(e, true)); |
| window.addEventListener("keyup", (e) => setKey(e, false)); |
|
|
| function readMovement() { |
| const x = (keys.ArrowRight || keys.KeyD ? 1 : 0) - (keys.ArrowLeft || keys.KeyA ? 1 : 0); |
| const y = (keys.ArrowDown || keys.KeyS ? 1 : 0) - (keys.ArrowUp || keys.KeyW ? 1 : 0); |
| return { x, y }; |
| } |
| ``` |
|
|
| ## Mouse Position |
|
|
| Convert viewport coordinates to canvas CSS-pixel coordinates. |
|
|
| ```javascript |
| const mouse = { x: 0, y: 0, down: false }; |
|
|
| canvas.addEventListener("mousemove", (e) => { |
| const rect = canvas.getBoundingClientRect(); |
| mouse.x = e.clientX - rect.left; |
| mouse.y = e.clientY - rect.top; |
| }); |
| canvas.addEventListener("mousedown", () => { mouse.down = true; }); |
| canvas.addEventListener("mouseup", () => { mouse.down = false; }); |
| ``` |
|
|
| ## Touch Basics |
|
|
| Use passive `false` so `preventDefault()` works and the page does not scroll. |
|
|
| ```javascript |
| const touch = { active: false, x: 0, y: 0 }; |
|
|
| function updateTouch(e) { |
| const t = e.touches[0] || e.changedTouches[0]; |
| const rect = canvas.getBoundingClientRect(); |
| touch.active = e.type !== "touchend" && e.type !== "touchcancel"; |
| touch.x = t.clientX - rect.left; |
| touch.y = t.clientY - rect.top; |
| e.preventDefault(); |
| } |
|
|
| canvas.addEventListener("touchstart", updateTouch, { passive: false }); |
| canvas.addEventListener("touchmove", updateTouch, { passive: false }); |
| canvas.addEventListener("touchend", updateTouch, { passive: false }); |
| canvas.addEventListener("touchcancel", updateTouch, { passive: false }); |
| ``` |
|
|
| ## Pointer Lock Mouse-Look |
|
|
| Request lock from a click. Use `movementX/Y` while locked. |
|
|
| ```javascript |
| const look = { yaw: 0, pitch: 0 }; |
|
|
| canvas.addEventListener("click", () => canvas.requestPointerLock()); |
|
|
| document.addEventListener("mousemove", (e) => { |
| if (document.pointerLockElement !== canvas) return; |
| look.yaw += e.movementX * 0.002; |
| look.pitch = clamp(look.pitch + e.movementY * 0.002, -1.3, 1.3); |
| }); |
| ``` |
|
|
| ## Collision Functions |
|
|
| Rects use top-left `x,y,w,h`. Circles use center `x,y,r`. |
|
|
| ```javascript |
| function rectsOverlap(a, b) { |
| return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; |
| } |
|
|
| function circlesOverlap(a, b) { |
| const dx = a.x - b.x; |
| const dy = a.y - b.y; |
| const r = a.r + b.r; |
| return dx * dx + dy * dy <= r * r; |
| } |
|
|
| function pointInRect(px, py, r) { |
| return px >= r.x && px <= r.x + r.w && py >= r.y && py <= r.y + r.h; |
| } |
|
|
| function clamp(value, min, max) { |
| return Math.max(min, Math.min(max, value)); |
| } |
| ``` |
|
|
| ## Resolve AABB Overlap |
|
|
| For player vs solid block. Move the dynamic rect out along the shallowest axis. |
|
|
| ```javascript |
| function resolveRect(dynamic, solid) { |
| if (!rectsOverlap(dynamic, solid)) return { x: 0, y: 0 }; |
|
|
| const left = dynamic.x + dynamic.w - solid.x; |
| const right = solid.x + solid.w - dynamic.x; |
| const top = dynamic.y + dynamic.h - solid.y; |
| const bottom = solid.y + solid.h - dynamic.y; |
| const moveX = left < right ? -left : right; |
| const moveY = top < bottom ? -top : bottom; |
|
|
| if (Math.abs(moveX) < Math.abs(moveY)) { |
| dynamic.x += moveX; |
| dynamic.vx = 0; |
| return { x: moveX, y: 0 }; |
| } |
| dynamic.y += moveY; |
| dynamic.vy = 0; |
| return { x: 0, y: moveY }; |
| } |
| ``` |
|
|
| ## Physics Basics |
|
|
| Velocity is pixels per second. Acceleration changes velocity. Friction damps velocity. |
|
|
| ```javascript |
| function applyPhysics(e, dt) { |
| e.vx += e.ax * dt; |
| e.vy += e.ay * dt; |
| e.vy += 1400 * dt; // gravity |
| e.vx *= Math.pow(0.0005, dt); // friction tuned per second |
| e.vx = clamp(e.vx, -320, 320); |
| e.vy = clamp(e.vy, -900, 900); |
| e.x += e.vx * dt; |
| e.y += e.vy * dt; |
| } |
|
|
| function jump(e) { |
| if (e.onGround) { |
| e.vy = -620; |
| e.onGround = false; |
| } |
| } |
| ``` |
|
|
| ## Entity Structs |
|
|
| Keep objects small and predictable. |
|
|
| ```javascript |
| function makeEntity(x, y, w, h) { |
| return { |
| x, y, w, h, |
| vx: 0, vy: 0, |
| ax: 0, ay: 0, |
| hp: 1, |
| onGround: false, |
| frame: 0, |
| frameTime: 0 |
| }; |
| } |
|
|
| function animate(entity, dt, frameCount, fps) { |
| entity.frameTime += dt; |
| if (entity.frameTime >= 1 / fps) { |
| entity.frame = (entity.frame + 1) % frameCount; |
| entity.frameTime = 0; |
| } |
| } |
| ``` |
|
|
| ## HUD Text on Canvas |
|
|
| Draw HUD last so it stays above the game. |
|
|
| ```javascript |
| function drawHud() { |
| ctx.save(); |
| ctx.font = "16px system-ui, sans-serif"; |
| ctx.textBaseline = "top"; |
| ctx.fillStyle = "white"; |
| ctx.strokeStyle = "black"; |
| ctx.lineWidth = 4; |
| const text = "Score: " + score + " Time: " + Math.ceil(timeLeft); |
| ctx.strokeText(text, 12, 12); |
| ctx.fillText(text, 12, 12); |
| ctx.restore(); |
| } |
| ``` |
|
|
| ## Tilemap Draw and Collision |
|
|
| Use a 2D array. `0` is empty, `1` is solid. Convert world pixels to tile coordinates with `Math.floor`. |
|
|
| ```javascript |
| const TILE = 32; |
| const map = [ |
| [1, 1, 1, 1, 1, 1], |
| [1, 0, 0, 0, 0, 1], |
| [1, 0, 1, 0, 2, 1], |
| [1, 1, 1, 1, 1, 1] |
| ]; |
|
|
| function tileAtPixel(x, y) { |
| const col = Math.floor(x / TILE); |
| const row = Math.floor(y / TILE); |
| return map[row]?.[col] ?? 1; |
| } |
|
|
| function isSolidAt(x, y) { |
| return tileAtPixel(x, y) === 1; |
| } |
|
|
| function drawMap() { |
| for (let row = 0; row < map.length; row++) { |
| for (let col = 0; col < map[row].length; col++) { |
| if (map[row][col] === 0) continue; |
| ctx.fillStyle = map[row][col] === 1 ? "#2d3748" : "#f6e05e"; |
| ctx.fillRect(col * TILE, row * TILE, TILE, TILE); |
| } |
| } |
| } |
| ``` |
|
|
| ## Tile Collision for a Rect |
|
|
| Check the four corners after movement. |
|
|
| ```javascript |
| function rectHitsSolid(e) { |
| return ( |
| isSolidAt(e.x, e.y) || |
| isSolidAt(e.x + e.w, e.y) || |
| isSolidAt(e.x, e.y + e.h) || |
| isSolidAt(e.x + e.w, e.y + e.h) |
| ); |
| } |
| ``` |
|
|
| ## Simple Audio Beep |
|
|
| Create or resume audio only after a click/tap/key press. |
|
|
| ```javascript |
| let audioCtx; |
|
|
| function getAudio() { |
| audioCtx ||= new (window.AudioContext || window.webkitAudioContext)(); |
| if (audioCtx.state === "suspended") audioCtx.resume(); |
| return audioCtx; |
| } |
|
|
| function beep(freq = 440, duration = 0.08, type = "square") { |
| const ac = getAudio(); |
| const osc = ac.createOscillator(); |
| const gain = ac.createGain(); |
| osc.type = type; |
| osc.frequency.value = freq; |
| gain.gain.setValueAtTime(0.08, ac.currentTime); |
| gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + duration); |
| osc.connect(gain).connect(ac.destination); |
| osc.start(); |
| osc.stop(ac.currentTime + duration); |
| } |
| ``` |
|
|
| ## Load and Play a Sample |
|
|
| Use data URLs or remote URLs that allow loading. Create a fresh source per play. |
|
|
| ```javascript |
| async function loadSample(url) { |
| const ac = getAudio(); |
| const response = await fetch(url); |
| const bytes = await response.arrayBuffer(); |
| return ac.decodeAudioData(bytes); |
| } |
|
|
| function playSample(buffer, volume = 0.4) { |
| const ac = getAudio(); |
| const source = ac.createBufferSource(); |
| const gain = ac.createGain(); |
| source.buffer = buffer; |
| gain.gain.value = volume; |
| source.connect(gain).connect(ac.destination); |
| source.start(); |
| } |
| ``` |
|
|
| ## Three.js Minimal Single-File Boilerplate |
|
|
| Canonical 3D start: CDN importmap, scene, camera, renderer, light, one mesh, resize, movement, orbit-style camera. Runs in a plain `.html` file. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <style> |
| html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #0b1020; } |
| canvas { display: block; } |
| </style> |
| <script type="importmap"> |
| { |
| "imports": { |
| "three": "https://unpkg.com/three@0.160.0/build/three.module.js", |
| "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" |
| } |
| } |
| </script> |
| </head> |
| <body> |
| <script type="module"> |
| import * as THREE from "three"; |
| import { OrbitControls } from "three/addons/controls/OrbitControls.js"; |
|
|
| const scene = new THREE.Scene(); |
| scene.background = new THREE.Color(0x0b1020); |
|
|
| const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 1000); |
| camera.position.set(0, 2, 6); |
|
|
| const renderer = new THREE.WebGLRenderer({ antialias: true }); |
| renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); |
| renderer.setSize(innerWidth, innerHeight); |
| document.body.appendChild(renderer.domElement); |
|
|
| const light = new THREE.DirectionalLight(0xffffff, 2); |
| light.position.set(3, 5, 4); |
| scene.add(light); |
| scene.add(new THREE.AmbientLight(0xffffff, 0.35)); |
|
|
| const player = new THREE.Mesh( |
| new THREE.BoxGeometry(1, 1, 1), |
| new THREE.MeshStandardMaterial({ color: 0x66e3ff, roughness: 0.45 }) |
| ); |
| scene.add(player); |
|
|
| const floor = new THREE.Mesh( |
| new THREE.PlaneGeometry(20, 20), |
| new THREE.MeshStandardMaterial({ color: 0x233044 }) |
| ); |
| floor.rotation.x = -Math.PI / 2; |
| floor.position.y = -0.6; |
| scene.add(floor); |
|
|
| const controls = new OrbitControls(camera, renderer.domElement); |
| controls.enableDamping = true; |
|
|
| const keys = Object.create(null); |
| addEventListener("keydown", (e) => { keys[e.code] = true; }); |
| addEventListener("keyup", (e) => { keys[e.code] = false; }); |
|
|
| addEventListener("resize", () => { |
| camera.aspect = innerWidth / innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(innerWidth, innerHeight); |
| }); |
|
|
| let last = performance.now(); |
| function loop(now) { |
| const dt = Math.min((now - last) / 1000, 0.05); |
| last = now; |
|
|
| const speed = 3 * dt; |
| if (keys.KeyW || keys.ArrowUp) player.position.z -= speed; |
| if (keys.KeyS || keys.ArrowDown) player.position.z += speed; |
| if (keys.KeyA || keys.ArrowLeft) player.position.x -= speed; |
| if (keys.KeyD || keys.ArrowRight) player.position.x += speed; |
| player.rotation.y += dt; |
|
|
| controls.target.copy(player.position); |
| controls.update(); |
| renderer.render(scene, camera); |
| requestAnimationFrame(loop); |
| } |
|
|
| requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| ## Cleanup Listeners |
|
|
| When restarting a generated game in the same page, remove old listeners and cancel old loops. |
|
|
| ```javascript |
| const cleanup = []; |
| let rafId = 0; |
|
|
| function on(target, type, handler, options) { |
| target.addEventListener(type, handler, options); |
| cleanup.push(() => target.removeEventListener(type, handler, options)); |
| } |
|
|
| function stopGame() { |
| cancelAnimationFrame(rafId); |
| while (cleanup.length) cleanup.pop()(); |
| } |
| ``` |
|
|
| ## GOTCHAS |
|
|
| - One game loop only. Do not start both `setInterval` and `requestAnimationFrame`. |
| - Do not redeclare top-level `const canvas`, `const ctx`, `const scene`, etc. inside the same script. |
| - Canvas must be DPR-aware: CSS pixels for logic, backing pixels for sharp rendering. |
| - Always clear the canvas each frame unless trails are intentional. |
| - Draw order matters: background, map, entities, particles, HUD. |
| - Clamp delta-time spikes after tab switches or debugger pauses. |
| - Audio must start from a user gesture in many browsers. |
| - three.js CDN module imports need a correct `type="importmap"` plus `type="module"`. |
| - Use simple AABB/circle collision first. Skip polygon collision, broad-phase trees, and heavy physics unless the game truly needs them. |
| - Clean up listeners and animation frames before replacing/restarting a game. |
|
|
| ## Complete Compact Game Exemplars |
|
|
| These are complete, compact, testable single-file games. Retrieve one by genre, then adapt its structure rather than inventing the engine from scratch. Each example has one game loop, visible HUD, controls, a goal, and retry behavior. Standalone copies live in `knowledge/examples/`. |
|
|
| ### Top-Down Checkpoint Racer |
|
|
| Use when: Use when the user asks for 2D racing, checkpoint runs, lap timers, or arcade car handling. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Top Down Checkpoint Racer</title> |
| <style> |
| html,body{margin:0;height:100%;overflow:hidden;background:#10202a;font-family:system-ui,sans-serif} |
| canvas{display:block;width:100vw;height:100vh;background:#16313b} |
| #hud{position:fixed;left:12px;top:12px;color:white;background:rgba(0,0,0,.38);padding:10px 12px;border-radius:12px;box-shadow:0 8px 24px #0008} |
| </style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"); |
| const keys={},track={x:90,y:70,w:700,h:440},car={x:145,y:285,a:0,v:0,w:18,h:30,lap:0,next:0,time:0,best:null,done:false}; |
| const cps=[{x:120,y:230,w:80,h:100},{x:410,y:80,w:130,h:70},{x:735,y:250,w:70,h:120},{x:360,y:445,w:140,h:55}]; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)} |
| addEventListener("resize",resize);resize(); |
| addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space"&&car.done)reset()}); |
| addEventListener("keyup",e=>keys[e.code]=0); |
| function reset(){Object.assign(car,{x:145,y:285,a:0,v:0,lap:0,next:0,time:0,done:false})} |
| function insideRoad(x,y){const outer=x>track.x&&x<track.x+track.w&&y>track.y&&y<track.y+track.h;const inner=x>track.x+160&&x<track.x+track.w-160&&y>track.y+120&&y<track.y+track.h-120;return outer&&!inner} |
| function inRect(p,r){return p.x>r.x&&p.x<r.x+r.w&&p.y>r.y&&p.y<r.y+r.h} |
| function update(dt){ |
| if(car.done)return; |
| car.time+=dt; |
| if(keys.ArrowLeft||keys.KeyA)car.a-=2.8*dt; |
| if(keys.ArrowRight||keys.KeyD)car.a+=2.8*dt; |
| if(keys.ArrowUp||keys.KeyW)car.v+=170*dt; |
| if(keys.ArrowDown||keys.KeyS)car.v-=130*dt; |
| car.v*=Math.pow(.25,dt);car.v=Math.max(-90,Math.min(260,car.v)); |
| car.x+=Math.sin(car.a)*car.v*dt;car.y-=Math.cos(car.a)*car.v*dt; |
| if(!insideRoad(car.x,car.y)){car.v*=.82;car.x-=Math.sin(car.a)*80*dt;car.y+=Math.cos(car.a)*80*dt} |
| if(inRect(car,cps[car.next])){car.next=(car.next+1)%cps.length;if(car.next===0){car.lap++;car.best=car.time;car.time=0;if(car.lap>=3)car.done=true}} |
| } |
| function render(){ |
| const w=innerWidth,h=innerHeight;ctx.clearRect(0,0,w,h);ctx.save();ctx.translate((w-880)/2,(h-580)/2); |
| ctx.fillStyle="#295f47";ctx.fillRect(0,0,880,580); |
| ctx.fillStyle="#38414a";ctx.fillRect(track.x,track.y,track.w,track.h);ctx.fillStyle="#295f47";ctx.fillRect(track.x+160,track.y+120,track.w-320,track.h-240); |
| ctx.strokeStyle="#f6e05e";ctx.lineWidth=4;ctx.strokeRect(cps[car.next].x,cps[car.next].y,cps[car.next].w,cps[car.next].h); |
| for(const [i,c] of cps.entries()){ctx.strokeStyle=i===car.next?"#fff":"#7dd3fc";ctx.lineWidth=2;ctx.strokeRect(c.x,c.y,c.w,c.h)} |
| ctx.save();ctx.translate(car.x,car.y);ctx.rotate(car.a);ctx.fillStyle="#ff4d6d";ctx.fillRect(-car.w/2,-car.h/2,car.w,car.h);ctx.fillStyle="#bff";ctx.fillRect(-5,-10,10,8);ctx.restore(); |
| ctx.restore();hud.textContent=car.done?`Champion! 3 laps in ${car.best?.toFixed(1)}s. Space to retry.`:`Laps ${car.lap}/3 | Gate ${car.next+1}/4 | Speed ${Math.round(car.v)} | Time ${car.time.toFixed(1)}`; |
| } |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the road membership test, checkpoint progression, collision slowdown, and car transform rendering. |
|
|
| ### Pseudo-3D Road Racer |
|
|
| Use when: Use when the user asks for OutRun-style racing, fast road games, horizon projection, or retro driving. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Pseudo 3D Road Racer</title> |
| <style> |
| html,body{margin:0;height:100%;overflow:hidden;background:#78b7ff;font-family:system-ui,sans-serif} |
| canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:#fff;background:#0008;padding:10px 12px;border-radius:12px} |
| </style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"); |
| const keys={},road=[],player={x:0,speed:0,z:0,score:0,goal:9000,done:false}; |
| for(let i=0;i<700;i++)road.push({curve:Math.sin(i*.045)*1.8+(i>250&&i<380?2.2:0),hill:Math.sin(i*.033)*70}); |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)} |
| addEventListener("resize",resize);resize();addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space"&&player.done)reset()});addEventListener("keyup",e=>keys[e.code]=0); |
| function reset(){Object.assign(player,{x:0,speed:0,z:0,score:0,done:false})} |
| function update(dt){if(player.done)return;if(keys.ArrowUp||keys.KeyW)player.speed+=900*dt;else player.speed-=300*dt;if(keys.ArrowDown||keys.KeyS)player.speed-=700*dt;player.speed=Math.max(0,Math.min(1900,player.speed));const steer=(keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0);player.x+=steer*dt*(1+player.speed/420);player.z+=player.speed*dt;player.score=Math.max(player.score,player.z);if(Math.abs(player.x)>1.08)player.speed*=.965;if(player.z>=player.goal)player.done=true} |
| function segment(i){return road[Math.floor(i)%road.length]} |
| function render(){ |
| const w=innerWidth,h=innerHeight;ctx.clearRect(0,0,w,h);ctx.fillStyle="#79c8ff";ctx.fillRect(0,0,w,h*.48);ctx.fillStyle="#4f9b45";ctx.fillRect(0,h*.48,w,h*.52); |
| let base=Math.floor(player.z/80),camX=player.x,dx=0,x=0,prevY=h,prevW=w; |
| for(let n=0;n<90;n++){const seg=segment(base+n),scale=1/(n*.055+1),y=h*.88-scale*(h*.55+seg.hill),rw=scale*w*.88,mid=w/2+(x-camX*900)*scale;dx+=seg.curve;x+=dx; |
| ctx.fillStyle=n%2?"#3f3f46":"#52525b";ctx.beginPath();ctx.moveTo(mid-rw,y);ctx.lineTo(mid+rw,y);ctx.lineTo(w/2+prevW,prevY);ctx.lineTo(w/2-prevW,prevY);ctx.fill(); |
| ctx.fillStyle="#f8fafc";ctx.fillRect(mid-rw*.04,y,rw*.08,Math.max(2,(prevY-y)*.18)); |
| if(n%12===0){ctx.fillStyle="#dc2626";ctx.fillRect(mid-rw*1.2,y-30*scale,22*scale,50*scale);ctx.fillStyle="#fff";ctx.fillRect(mid+rw*1.15,y-25*scale,30*scale,30*scale)} |
| prevY=y;prevW=rw} |
| ctx.save();ctx.translate(w/2,h*.82);ctx.fillStyle="#ef4444";ctx.beginPath();ctx.moveTo(0,-45);ctx.lineTo(42,34);ctx.lineTo(-42,34);ctx.fill();ctx.fillStyle="#111";ctx.fillRect(-55,18,28,18);ctx.fillRect(27,18,28,18);ctx.restore(); |
| hud.textContent=player.done?`Finish! Score ${Math.round(player.score)}. Space to retry.`:`Speed ${Math.round(player.speed)} | Distance ${Math.round(player.z)}/${player.goal} | Stay off the grass`; |
| } |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the segment projection loop, curve accumulation, speed/distance goal, and roadside sprite pattern. |
|
|
| ### Canvas Tile Platformer |
|
|
| Use when: Use when the user asks for platformers, jumping, coins, hazards, tile maps, or side-view adventures. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Canvas Platformer</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#162033;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"),keys={}; |
| const T=32,map=[ |
| "1111111111111111111111111", |
| "1000000000000000000000001", |
| "1000000000000000000000G01", |
| "1000111000001110000011111", |
| "100000000C000000000000001", |
| "1000001111110000011100001", |
| "100C000000000000000000001", |
| "111100000000C000111100001", |
| "1000000001111110000000001", |
| "100000H00000000000000C001", |
| "1001111111000000111110001", |
| "1000000000000000000000001", |
| "1111111111111111111111111"]; |
| const player={x:48,y:300,w:24,h:28,vx:0,vy:0,onGround:false,coins:0,win:false,dead:false}; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space"&&(player.dead||player.win))location.reload()});addEventListener("keyup",e=>keys[e.code]=0); |
| function tile(x,y){return map[Math.floor(y/T)]?.[Math.floor(x/T)]||"1"}function solid(x,y){return tile(x,y)==="1"}function setTile(px,py,ch){const r=Math.floor(py/T),c=Math.floor(px/T);map[r]=map[r].slice(0,c)+ch+map[r].slice(c+1)} |
| function moveAxis(axis,amount){player[axis]+=amount;const pts=[[player.x,player.y],[player.x+player.w,player.y],[player.x,player.y+player.h],[player.x+player.w,player.y+player.h]];if(pts.some(p=>solid(p[0],p[1]))){if(axis==="x"){player.x-=amount;player.vx=0}else{if(amount>0)player.onGround=true;player.y-=amount;player.vy=0}}} |
| function update(dt){if(player.dead||player.win)return;player.vx=((keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0))*210;if((keys.ArrowUp||keys.KeyW)&&player.onGround){player.vy=-520;player.onGround=false}player.vy=Math.min(900,player.vy+1400*dt);moveAxis("x",player.vx*dt);player.onGround=false;moveAxis("y",player.vy*dt);const cx=player.x+player.w/2,cy=player.y+player.h/2,t=tile(cx,cy);if(t==="C"){player.coins++;setTile(cx,cy,"0")}if(t==="H")player.dead=true;if(t==="G"&&player.coins>=4)player.win=true} |
| function render(){const w=innerWidth,h=innerHeight;ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(Math.min(0,w/2-player.x),Math.min(0,h/2-player.y));for(let r=0;r<map.length;r++)for(let c=0;c<map[r].length;c++){const ch=map[r][c],x=c*T,y=r*T;if(ch==="1"){ctx.fillStyle="#334155";ctx.fillRect(x,y,T,T)}if(ch==="C"){ctx.fillStyle="#fde047";ctx.beginPath();ctx.arc(x+16,y+16,9,0,7);ctx.fill()}if(ch==="H"){ctx.fillStyle="#ef4444";ctx.fillRect(x+4,y+20,24,12)}if(ch==="G"){ctx.fillStyle="#22c55e";ctx.fillRect(x+8,y+4,16,28)}}ctx.fillStyle=player.dead?"#64748b":"#38bdf8";ctx.fillRect(player.x,player.y,player.w,player.h);ctx.restore();hud.textContent=player.win?"You win! Space to retry.":player.dead?"Ouch. Space to retry.":`Coins ${player.coins}/4 | Arrows/WASD, jump with Up/W`;} |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy axis-separated movement, tile collision, coin pickup mutation, hazard/goal checks, and camera translation. |
|
|
| ### Snake Grid Puzzle |
|
|
| Use when: Use when the user asks for Snake, grid growth games, food collection, or simple escalating arcade loops. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Snake</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#08111f;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"); |
| const cols=24,rows=18,cell=28;let snake,dir,next,food,score,dead,won,timer,step; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| function reset(){snake=[{x:5,y:9},{x:4,y:9},{x:3,y:9}];dir={x:1,y:0};next={x:1,y:0};score=0;dead=false;won=false;timer=0;step=.15;placeFood()} |
| function placeFood(){do{food={x:Math.floor(Math.random()*cols),y:Math.floor(Math.random()*rows)}}while(snake.some(s=>s.x===food.x&&s.y===food.y))} |
| addEventListener("keydown",e=>{if(e.code==="Space"&&(dead||won))reset();const m={ArrowUp:{x:0,y:-1},KeyW:{x:0,y:-1},ArrowDown:{x:0,y:1},KeyS:{x:0,y:1},ArrowLeft:{x:-1,y:0},KeyA:{x:-1,y:0},ArrowRight:{x:1,y:0},KeyD:{x:1,y:0}}[e.code];if(m&&!(m.x===-dir.x&&m.y===-dir.y))next=m}); |
| function tick(){dir=next;const head={x:snake[0].x+dir.x,y:snake[0].y+dir.y};if(head.x<0||head.y<0||head.x>=cols||head.y>=rows||snake.some(s=>s.x===head.x&&s.y===head.y)){dead=true;return}snake.unshift(head);if(head.x===food.x&&head.y===food.y){score++;step=Math.max(.06,step*.96);if(score>=12)won=true;else placeFood()}else snake.pop()} |
| function update(dt){if(dead||won)return;timer+=dt;while(timer>step){timer-=step;tick()}} |
| function render(){const ox=(innerWidth-cols*cell)/2,oy=(innerHeight-rows*cell)/2;ctx.clearRect(0,0,innerWidth,innerHeight);ctx.fillStyle="#111827";ctx.fillRect(ox,oy,cols*cell,rows*cell);ctx.fillStyle="#f43f5e";ctx.fillRect(ox+food.x*cell+5,oy+food.y*cell+5,cell-10,cell-10);snake.forEach((s,i)=>{ctx.fillStyle=i?"#22c55e":"#86efac";ctx.fillRect(ox+s.x*cell+3,oy+s.y*cell+3,cell-6,cell-6)});hud.textContent=won?"Snake charmer! Space to retry.":dead?"Bonked. Space to retry.":`Score ${score}/12 | Arrows/WASD`} |
| let last=performance.now();reset();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the fixed-step timer, direction guard against reverse turns, food placement, body growth, and restart flow. |
|
|
| ### Tetris Falling Blocks |
|
|
| Use when: Use when the user asks for Tetris, falling block puzzles, rotations, line clears, or score survival games. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Tetris Mini</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#0f172a;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"),W=10,H=20,S=26; |
| const shapes=[[[1,1,1,1]],[[1,1],[1,1]],[[0,1,0],[1,1,1]],[[1,0,0],[1,1,1]],[[0,0,1],[1,1,1]],[[1,1,0],[0,1,1]],[[0,1,1],[1,1,0]]],colors=["#67e8f9","#fde047","#c084fc","#60a5fa","#fb923c","#f87171","#4ade80"]; |
| let board,piece,next,score,lines,drop,dead,won; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| function cloneShape(s){return s.map(r=>r.slice())}function makePiece(){const id=Math.floor(Math.random()*shapes.length);return{x:3,y:0,m:cloneShape(shapes[id]),c:colors[id]}} |
| function reset(){board=Array.from({length:H},()=>Array(W).fill(0));score=0;lines=0;drop=0;dead=false;won=false;piece=makePiece();next=makePiece()} |
| function hit(p,ox=0,oy=0,m=p.m){return m.some((r,y)=>r.some((v,x)=>v&&(p.x+x+ox<0||p.x+x+ox>=W||p.y+y+oy>=H||board[p.y+y+oy]?.[p.x+x+ox])))} |
| function lock(){piece.m.forEach((r,y)=>r.forEach((v,x)=>{if(v){if(piece.y+y<0)dead=true;else board[piece.y+y][piece.x+x]=piece.c}}));for(let y=H-1;y>=0;y--)if(board[y].every(Boolean)){board.splice(y,1);board.unshift(Array(W).fill(0));score+=100;lines++;y++}if(lines>=8)won=true;piece=next;next=makePiece();if(hit(piece))dead=true} |
| function rot(m){return m[0].map((_,x)=>m.map(r=>r[x]).reverse())} |
| addEventListener("keydown",e=>{if(e.code==="Space"&&(dead||won))reset();if(dead||won)return;if(e.code==="ArrowLeft"&&!hit(piece,-1,0))piece.x--;if(e.code==="ArrowRight"&&!hit(piece,1,0))piece.x++;if(e.code==="ArrowDown"&&!hit(piece,0,1))piece.y++;if(e.code==="ArrowUp"){const r=rot(piece.m);if(!hit(piece,0,0,r))piece.m=r}}); |
| function update(dt){if(dead||won)return;drop+=dt;if(drop>.55){drop=0;if(hit(piece,0,1))lock();else piece.y++}} |
| function block(x,y,c){ctx.fillStyle=c;ctx.fillRect(x*S,y*S,S-2,S-2)} |
| function render(){ctx.clearRect(0,0,innerWidth,innerHeight);const ox=(innerWidth-W*S)/2,oy=(innerHeight-H*S)/2;ctx.save();ctx.translate(ox,oy);ctx.fillStyle="#1e293b";ctx.fillRect(0,0,W*S,H*S);board.forEach((r,y)=>r.forEach((c,x)=>c&&block(x,y,c)));piece.m.forEach((r,y)=>r.forEach((v,x)=>v&&block(piece.x+x,piece.y+y,piece.c)));ctx.restore();hud.textContent=won?"Line lord! Space to retry.":dead?"Stacked out. Space to retry.":`Score ${score} | Lines ${lines}/8 | Arrows move/rotate`} |
| let last=performance.now();reset();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the board matrix, piece collision, rotation test, lock/clear loop, and game-over detection. |
|
|
| ### Sokoban Push Puzzle |
|
|
| Use when: Use when the user asks for crate puzzles, box pushing, warehouse logic, or small deterministic puzzle rooms. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Sokoban Mini</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#101827;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"),S=42; |
| const base=["########","#..G...#","#..B...#","#.PB.G.#","#..B...#","#...G..#","########"]; |
| let map,player,boxes,goals,moves,won; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| function reset(){map=base.map(r=>r.replace(/[PBG]/g,".").split(""));boxes=[];goals=[];moves=0;won=false;base.forEach((r,y)=>[...r].forEach((ch,x)=>{if(ch==="P")player={x,y};if(ch==="B")boxes.push({x,y});if(ch==="G")goals.push({x,y})}))} |
| function boxAt(x,y){return boxes.find(b=>b.x===x&&b.y===y)}function wall(x,y){return map[y]?.[x]==="#"}function goal(x,y){return goals.some(g=>g.x===x&&g.y===y)} |
| function move(dx,dy){if(won)return;const nx=player.x+dx,ny=player.y+dy,b=boxAt(nx,ny);if(wall(nx,ny))return;if(b){const bx=b.x+dx,by=b.y+dy;if(wall(bx,by)||boxAt(bx,by))return;b.x=bx;b.y=by}player.x=nx;player.y=ny;moves++;won=boxes.every(b=>goal(b.x,b.y))} |
| addEventListener("keydown",e=>{if(e.code==="Space")reset();const d={ArrowUp:[0,-1],KeyW:[0,-1],ArrowDown:[0,1],KeyS:[0,1],ArrowLeft:[-1,0],KeyA:[-1,0],ArrowRight:[1,0],KeyD:[1,0]}[e.code];if(d)move(d[0],d[1])}); |
| function update(){} |
| function render(){ctx.clearRect(0,0,innerWidth,innerHeight);const ox=(innerWidth-map[0].length*S)/2,oy=(innerHeight-map.length*S)/2;ctx.save();ctx.translate(ox,oy);for(let y=0;y<map.length;y++)for(let x=0;x<map[y].length;x++){ctx.fillStyle=wall(x,y)?"#334155":"#1e293b";ctx.fillRect(x*S,y*S,S-2,S-2);if(goal(x,y)){ctx.fillStyle="#facc15";ctx.beginPath();ctx.arc(x*S+S/2,y*S+S/2,9,0,7);ctx.fill()}}for(const b of boxes){ctx.fillStyle=goal(b.x,b.y)?"#22c55e":"#fb923c";ctx.fillRect(b.x*S+7,b.y*S+7,S-14,S-14)}ctx.fillStyle="#38bdf8";ctx.fillRect(player.x*S+9,player.y*S+6,S-18,S-12);ctx.restore();hud.textContent=won?`All crates parked in ${moves} moves. Space resets.`:`Moves ${moves} | Push boxes onto gold dots | Space reset`} |
| let last=performance.now();reset();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the static map parsing, push validation, goal matching, move counter, reset, and win detection. |
|
|
| ### Wolfenstein-Style Raycaster FPS |
|
|
| Use when: Use when the user asks for old-school FPS, maze shooters, 2.5D raycasting, or Wolfenstein-like games. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Raycaster FPS</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#111;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"),keys={}; |
| const map=["1111111111","1000000001","1011110101","1001000101","1001000001","1001111101","1000000001","1010111101","10000000E1","1111111111"],P={x:1.5,y:1.5,a:0,ammo:6,enemy:true,win:false,dead:false}; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space")shoot();if(e.code==="KeyR"&&(P.win||P.dead))location.reload()});addEventListener("keyup",e=>keys[e.code]=0); |
| function wall(x,y){return map[Math.floor(y)]?.[Math.floor(x)]==="1"}function cell(x,y){return map[Math.floor(y)]?.[Math.floor(x)]||"1"} |
| function shoot(){if(P.win||P.dead||P.ammo<=0)return;P.ammo--;let ex=8.5,ey=8.5,ang=Math.atan2(ey-P.y,ex-P.x);if(P.enemy&&Math.abs(Math.atan2(Math.sin(ang-P.a),Math.cos(ang-P.a)))<.18){P.enemy=false}} |
| function update(dt){if(P.win||P.dead)return;P.a+=((keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0))*2.5*dt;let sp=((keys.ArrowUp||keys.KeyW?1:0)-(keys.ArrowDown||keys.KeyS?1:0))*2.4*dt,nx=P.x+Math.cos(P.a)*sp,ny=P.y+Math.sin(P.a)*sp;if(!wall(nx,P.y))P.x=nx;if(!wall(P.x,ny))P.y=ny;if(cell(P.x,P.y)==="E"&&!P.enemy)P.win=true;if(P.enemy&&Math.hypot(P.x-8.5,P.y-8.5)<.45)P.dead=true} |
| function cast(ang){let dist=0;while(dist<12){dist+=.025;let x=P.x+Math.cos(ang)*dist,y=P.y+Math.sin(ang)*dist;if(wall(x,y))return dist}return 12} |
| function render(){const w=innerWidth,h=innerHeight;ctx.clearRect(0,0,w,h);ctx.fillStyle="#172033";ctx.fillRect(0,0,w,h/2);ctx.fillStyle="#2b2018";ctx.fillRect(0,h/2,w,h/2);for(let x=0;x<w;x+=3){const ang=P.a-.55+x/w*1.1,d=cast(ang),hh=Math.min(h, h/(d*Math.cos(ang-P.a)));ctx.fillStyle=`rgb(${Math.max(40,210-d*18)},${Math.max(40,160-d*12)},${Math.max(50,120-d*9)})`;ctx.fillRect(x,h/2-hh/2,3,hh)}if(P.enemy){const ex=8.5-P.x,ey=8.5-P.y,ea=Math.atan2(ey,ex)-P.a,ed=Math.hypot(ex,ey);if(Math.abs(ea)<.55){const sx=w/2+Math.tan(ea)*w,sz=h/ed;ctx.fillStyle="#ef4444";ctx.fillRect(sx-sz/2,h/2-sz/2,sz,sz)}}ctx.fillStyle="#fff";ctx.fillRect(w/2-8,h/2,16,2);ctx.fillRect(w/2,h/2-8,2,16);ctx.save();ctx.scale(5,5);for(let y=0;y<map.length;y++)for(let x=0;x<map[y].length;x++){ctx.fillStyle=map[y][x]==="1"?"#94a3b8":map[y][x]==="E"?"#22c55e":"#111827";ctx.fillRect(x,y,1,1)}ctx.fillStyle="#38bdf8";ctx.fillRect(P.x-.15,P.y-.15,.3,.3);ctx.restore();hud.textContent=P.win?"Exit secured. R to retry.":P.dead?"Caught. R to retry.":`Ammo ${P.ammo} | WASD/arrows move | Space shoots red guard, then reach green exit`} |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the ray marching renderer, minimap, movement with wall checks, enemy sprite projection, ammo, and exit condition. |
|
|
| ### Three.js Space Flight |
|
|
| Use when: Use when the user asks for 3D space, flight, asteroids, ring checkpoints, or chase-camera obstacle games. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Three Space Flight</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#000;font-family:system-ui,sans-serif}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| <script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script> |
| </head> |
| <body> |
| <div id="hud"></div> |
| <script type="module"> |
| import * as THREE from "three"; |
| const hud=document.getElementById("hud"),scene=new THREE.Scene(),camera=new THREE.PerspectiveCamera(70,innerWidth/innerHeight,.1,1200),renderer=new THREE.WebGLRenderer({antialias:true}); |
| renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.setSize(innerWidth,innerHeight);document.body.appendChild(renderer.domElement); |
| scene.add(new THREE.AmbientLight(0x88aaff,.8));const light=new THREE.PointLight(0xffffff,2);light.position.set(0,8,8);scene.add(light); |
| const ship=new THREE.Mesh(new THREE.ConeGeometry(.35,1.1,4),new THREE.MeshStandardMaterial({color:0x66e3ff,emissive:0x114466}));ship.rotation.x=Math.PI/2;scene.add(ship); |
| const stars=new THREE.Group();for(let i=0;i<260;i++){const s=new THREE.Mesh(new THREE.SphereGeometry(.025,6,6),new THREE.MeshBasicMaterial({color:0xffffff}));s.position.set((Math.random()-.5)*80,(Math.random()-.5)*50,-Math.random()*260);stars.add(s)}scene.add(stars); |
| const rings=[],rocks=[];for(let i=1;i<=8;i++){const r=new THREE.Mesh(new THREE.TorusGeometry(1.5,.08,8,32),new THREE.MeshBasicMaterial({color:0xfacc15}));r.position.set(Math.sin(i)*6,Math.cos(i*.7)*3,-i*28);scene.add(r);rings.push(r)}for(let i=0;i<28;i++){const a=new THREE.Mesh(new THREE.DodecahedronGeometry(.5+Math.random()*.8),new THREE.MeshStandardMaterial({color:0x777777}));a.position.set((Math.random()-.5)*16,(Math.random()-.5)*8,-10-Math.random()*230);scene.add(a);rocks.push(a)} |
| const keys={};let score=0,health=3,done=false;addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space"&&done)location.reload()});addEventListener("keyup",e=>keys[e.code]=0);addEventListener("resize",()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)}); |
| function update(dt){if(done)return;ship.position.x+=((keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0))*8*dt;ship.position.y+=((keys.ArrowUp||keys.KeyW?1:0)-(keys.ArrowDown||keys.KeyS?1:0))*6*dt;ship.position.x=Math.max(-8,Math.min(8,ship.position.x));ship.position.y=Math.max(-5,Math.min(5,ship.position.y));for(const o of [...rings,...rocks]){o.position.z+=18*dt;if(o.position.z>6)o.position.z-=240;o.rotation.x+=dt;o.rotation.y+=dt*.7}for(const r of rings)if(r.visible&&r.position.distanceTo(ship.position)<1.6){r.visible=false;score++}for(const a of rocks)if(a.position.distanceTo(ship.position)<.8){health--;a.position.z=-230;if(health<=0)done=true}if(score>=8)done=true;camera.position.set(ship.position.x*.25,ship.position.y*.25,6);camera.lookAt(ship.position.x,ship.position.y,-8)} |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);renderer.render(scene,camera);hud.textContent=score>=8?"All rings cleared! Space to retry.":health<=0?"Hull breached. Space to retry.":`Rings ${score}/8 | Hull ${health} | WASD/arrows fly`;requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the importmap, starfield, ring/asteroid loops, chase camera, distance collision, and HUD state. |
|
|
| ### Canvas Space Shooter |
|
|
| Use when: Use when the user asks for arcade shooters, bullets, enemy waves, survival, or 2D space action. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Canvas Space Shooter</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#020617;font-family:system-ui,sans-serif}canvas{display:block;width:100vw;height:100vh}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| </head> |
| <body> |
| <canvas id="game"></canvas><div id="hud"></div> |
| <script> |
| const canvas=document.getElementById("game"),ctx=canvas.getContext("2d"),hud=document.getElementById("hud"),keys={}; |
| let ship,bullets,enemies,stars,spawn,wave,score,dead,won,cool; |
| function resize(){const d=Math.max(1,devicePixelRatio||1);canvas.width=innerWidth*d;canvas.height=innerHeight*d;ctx.setTransform(d,0,0,d,0,0)}addEventListener("resize",resize);resize(); |
| function reset(){ship={x:innerWidth/2,y:innerHeight-70,w:28,h:34,hp:3};bullets=[];enemies=[];stars=Array.from({length:90},()=>({x:Math.random()*innerWidth,y:Math.random()*innerHeight,s:1+Math.random()*3}));spawn=0;wave=1;score=0;dead=false;won=false;cool=0} |
| addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="Space"&&(dead||won))reset()});addEventListener("keyup",e=>keys[e.code]=0); |
| function rect(a,b){return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y} |
| function update(dt){if(dead||won)return;ship.x+=((keys.ArrowRight||keys.KeyD?1:0)-(keys.ArrowLeft||keys.KeyA?1:0))*320*dt;ship.y+=((keys.ArrowDown||keys.KeyS?1:0)-(keys.ArrowUp||keys.KeyW?1:0))*240*dt;ship.x=Math.max(0,Math.min(innerWidth-ship.w,ship.x));ship.y=Math.max(0,Math.min(innerHeight-ship.h,ship.y));cool-=dt;if((keys.Space||keys.KeyJ)&&cool<=0){cool=.16;bullets.push({x:ship.x+ship.w/2-3,y:ship.y,w:6,h:14})}for(const s of stars){s.y+=s.s*40*dt;if(s.y>innerHeight)s.y=0}for(const b of bullets)b.y-=520*dt;bullets=bullets.filter(b=>b.y>-20);spawn-=dt;if(spawn<=0){spawn=Math.max(.25,1-wave*.1);enemies.push({x:Math.random()*(innerWidth-36),y:-40,w:32,h:28,vy:80+wave*18})}for(const e of enemies)e.y+=e.vy*dt;for(const e of enemies)for(const b of bullets)if(!e.dead&&rect(e,b)){e.dead=true;b.y=-99;score++}for(const e of enemies)if(!e.dead&&rect(e,ship)){e.dead=true;ship.hp--;if(ship.hp<=0)dead=true}enemies=enemies.filter(e=>!e.dead&&e.y<innerHeight+60);wave=1+Math.floor(score/8);if(score>=30)won=true} |
| function render(){ctx.clearRect(0,0,innerWidth,innerHeight);ctx.fillStyle="#fff";for(const s of stars)ctx.fillRect(s.x,s.y,2,2);ctx.fillStyle="#38bdf8";ctx.beginPath();ctx.moveTo(ship.x+ship.w/2,ship.y);ctx.lineTo(ship.x+ship.w,ship.y+ship.h);ctx.lineTo(ship.x,ship.y+ship.h);ctx.fill();ctx.fillStyle="#facc15";for(const b of bullets)ctx.fillRect(b.x,b.y,b.w,b.h);ctx.fillStyle="#ef4444";for(const e of enemies)ctx.fillRect(e.x,e.y,e.w,e.h);hud.textContent=won?"Wave cleared! Space to retry.":dead?"Ship lost. Space to retry.":`Score ${score}/30 | Hull ${ship.hp} | WASD/arrows, Space/J fire`} |
| let last=performance.now();reset();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);render();requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the bullet/enemy arrays, spawn timer, collision cleanup, hull/score goals, and starfield background. |
|
|
| ### Three.js Lane Obstacle Runner |
|
|
| Use when: Use when the user asks for 3D runners, lane dodging, pickups, obstacles, or forward-motion games. |
|
|
| ```html |
| <!doctype html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Three Obstacle Runner</title> |
| <style>html,body{margin:0;height:100%;overflow:hidden;background:#111827;font-family:system-ui,sans-serif}#hud{position:fixed;left:12px;top:12px;color:white;background:#0008;padding:10px 12px;border-radius:12px}</style> |
| <script type="importmap">{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}</script> |
| </head> |
| <body> |
| <div id="hud"></div> |
| <script type="module"> |
| import * as THREE from "three"; |
| const hud=document.getElementById("hud"),scene=new THREE.Scene(),camera=new THREE.PerspectiveCamera(65,innerWidth/innerHeight,.1,300),renderer=new THREE.WebGLRenderer({antialias:true}); |
| renderer.setPixelRatio(Math.min(devicePixelRatio,2));renderer.setSize(innerWidth,innerHeight);document.body.appendChild(renderer.domElement);scene.background=new THREE.Color(0x111827);scene.add(new THREE.HemisphereLight(0xffffff,0x334155,2)); |
| const runner=new THREE.Mesh(new THREE.BoxGeometry(.7,1,.7),new THREE.MeshStandardMaterial({color:0x38bdf8}));runner.position.set(0,.5,3);scene.add(runner); |
| const floor=new THREE.Mesh(new THREE.BoxGeometry(7,.1,220),new THREE.MeshStandardMaterial({color:0x334155}));floor.position.z=-70;scene.add(floor); |
| const lanes=[-2,0,2],objs=[];let lane=1,score=0,hp=3,done=false,speed=15,spawn=0;const keys={}; |
| addEventListener("keydown",e=>{keys[e.code]=1;if(e.code==="ArrowLeft"||e.code==="KeyA")lane=Math.max(0,lane-1);if(e.code==="ArrowRight"||e.code==="KeyD")lane=Math.min(2,lane+1);if(e.code==="Space"&&done)location.reload()});addEventListener("keyup",e=>keys[e.code]=0);addEventListener("resize",()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)}); |
| function addObj(){const good=Math.random()<.35,size=good ? .6 : 1,o=new THREE.Mesh(new THREE.BoxGeometry(size,size,size),new THREE.MeshStandardMaterial({color:good?0xfacc15:0xef4444}));o.userData.good=good;o.position.set(lanes[Math.floor(Math.random()*3)],.5,-80);scene.add(o);objs.push(o)} |
| function update(dt){if(done)return;spawn-=dt;if(spawn<=0){spawn=.55;addObj()}runner.position.x+=(lanes[lane]-runner.position.x)*12*dt;for(const o of objs){o.position.z+=speed*dt;o.rotation.x+=dt;o.rotation.y+=dt;if(o.position.z>6){scene.remove(o);o.userData.remove=true}else if(!o.userData.hit&&o.position.distanceTo(runner.position)<.9){o.userData.hit=true;o.visible=false;if(o.userData.good)score++;else hp--;if(hp<=0||score>=12)done=true}}for(let i=objs.length-1;i>=0;i--)if(objs[i].userData.remove)objs.splice(i,1);speed+=dt*.45;camera.position.set(0,4.2,8);camera.lookAt(runner.position.x,.5,-10)} |
| let last=performance.now();function loop(now){const dt=Math.min((now-last)/1000,.05);last=now;update(dt);renderer.render(scene,camera);hud.textContent=score>=12?"Treasure run complete! Space to retry.":hp<=0?"Tripped out. Space to retry.":`Gems ${score}/12 | Health ${hp} | A/D or arrows change lanes`;requestAnimationFrame(loop)}requestAnimationFrame(loop); |
| </script> |
| </body> |
| </html> |
| ``` |
|
|
| Model should copy: Copy the lane interpolation, object spawning/recycling, pickup vs obstacle handling, camera setup, and importmap. |
|
|