File size: 15,718 Bytes
839dd8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# The YokoCoder:4B Master Guide to HTML/CSS/JS (2D Canvas) & Three.js (3D WebGL) Video Game Engineering

This guide serves both as a **reference manual for game developers** and as a **core architectural specification distilled into the `YokoCoder:4B` dataset**. Every pattern below is engineered to eliminate the most common failure modes in browser-based 2D and 3D gamesβ€”such as `ReferenceError: dt is not defined`, stacked `requestAnimationFrame` loops, variable-framerate physics drift, hitbox mismatches, and Three.js memory leaks.

---

## 1. Single-File HTML/CSS/JS Game Architecture

A production-quality single-file HTML game must be 100% self-contained (zero external image or audio file dependencies that could fail to load via CORS) and strictly ordered into five deterministic layers inside `<script>`:

```text
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. CONFIGURATION & CONSTANTS (Immutable tuning parameters, colors)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 2. RUNTIME STATE & INPUT REGISTRY (Single source of truth, lexical)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 3. PROCEDURAL ASSETS & WEB AUDIO SYNTH (Lazy AudioContext, sprites)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 4. PURE UPDATE(dt) & COLLISION SOLVERS (Deterministic math, no DOM)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 5. RENDER(dt, alpha) & RAF SCHEDULER (Canvas/WebGL draw, single loop)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```

### 1.1 The Responsive Canvas Shell (Smooth Vector vs. Crisp Pixel Art)

Always prevent mobile pinch-zoom, double-tap selection, and Space/Arrow page scrolling:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>YokoCoder Arcade Engine</title>
  <style>
    *, *::before, *::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: #0b0f19;
      display: flex;
      align-items: center;
      justify-content: center;
      font-family: system-ui, -apple-system, sans-serif;
      touch-action: none;
    }
    canvas {
      display: block;
      touch-action: none;
      box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
      /* Enable for low-res 8-bit pixel art games: */
      /* image-rendering: pixelated; image-rendering: crisp-edges; */
    }
  </style>
</head>
<body>
  <canvas id="game"></canvas>
  <script>
    // Game engine code goes here
  </script>
</body>
</html>
```

### 1.2 HiDPI Scaling for Vector Games vs. Integer Scaling for Pixel Art

- **Smooth Vector Games (`400x600` logical space)**:
  Scale the physical canvas backing store by `dpr = Math.min(window.devicePixelRatio || 1, 2)` and call `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` so all game logic uses fixed logical coordinates (`W = 400`, `H = 600`) regardless of screen resolution.
- **Native Pixel-Art Games (`144x256` or `160x288` logical buffer)**:
  Keep `canvas.width = 144` and `canvas.height = 256`, set `ctx.imageSmoothingEnabled = false`, and scale the CSS width/height by an integer multiplier `scale = Math.max(1, Math.floor(Math.min(window.innerWidth / 144, window.innerHeight / 256)))`.

---

## 2. The Golden Game Loop & Why `ReferenceError: dt is not defined` Happens

### 2.1 Root Cause of `Uncaught ReferenceError: dt is not defined`

One of the most frequent bugs generated by small coding models is computing `const dt = ...` as a block-scoped local variable inside `loop(now)`, and then referencing `dt` inside `draw()` or `update()` **without passing `dt` as a function parameter**:

```javascript
// ❌ BROKEN: dt is block-scoped inside loop(), so draw() throws ReferenceError!
function draw() {
  wingPhase += dt * 10; // πŸ’₯ Uncaught ReferenceError: dt is not defined
}
function loop(now) {
  const dt = Math.min(0.1, (now - lastTime) / 1000);
  lastTime = now;
  update(dt);
  draw(); // ❌ Forgot to pass dt!
  requestAnimationFrame(loop);
}
```

### 2.2 The Invariant Fix: Explicit `update(dt)` and `draw(dt)` Signatures + Fixed-Step Accumulator

To guarantee that `dt` is **never** undefined and physics is **100% identical on 60Hz, 120Hz, 144Hz, and 240Hz monitors**:
1. Declare timing variables (`lastTime`, `accumulator`, `rafId`) in the module scope before `loop`.
2. Guard the first frame (`if (!lastTime) lastTime = now;`) so `now - lastTime` is never `NaN` or huge on startup.
3. Clamp `frameDt = Math.min(0.1, Math.max(0, (now - lastTime) / 1000))` so switching browser tabs never causes physics tunneling.
4. Step physics in fixed `FIXED_DT = 1 / 120` increments and pass `FIXED_DT` explicitly into `update(dt)` and `frameDt` explicitly into `draw(dt)`.

```javascript
const FIXED_DT = 1 / 120;
let lastTime = 0;
let accumulator = 0;
let rafId = null;

function update(dt = FIXED_DT) {
  // Pure physics & collision using explicit parameter `dt`
}

function draw(dt = FIXED_DT, alpha = 1) {
  // Visual animation timers (wing flap, particles, shaders) use explicit `dt`
}

function loop(now = performance.now()) {
  rafId = requestAnimationFrame(loop);

  if (lastTime === 0) lastTime = now;
  const frameDt = Math.min(0.1, Math.max(0, (now - lastTime) / 1000));
  lastTime = now;

  accumulator += frameDt;
  while (accumulator >= FIXED_DT) {
    update(FIXED_DT);
    accumulator -= FIXED_DT;
  }

  draw(frameDt, accumulator / FIXED_DT);
}

// Start the single persistent RAF loop once:
rafId = requestAnimationFrame(loop);
```

---

## 3. State Machine & Input Discipline

### 3.1 Never Spawn Duplicate `requestAnimationFrame` Loops on Restart

A classic bug occurs when `startGame()` or `resetGame()` calls `loop()` while a previous `requestAnimationFrame(loop)` is still runningβ€”doubling or tripling game speed after every death!

**Rule**: Either keep **one persistent `requestAnimationFrame` loop** running across all states (`READY`, `PLAYING`, `GAMEOVER`) and branch on `state` inside `update(dt)`, OR always call `if (rafId !== null) cancelAnimationFrame(rafId);` before scheduling a new frame.

### 3.2 Time-Based Game-Over Grace Period (Never Use `frames % 10 === 0`!)

Checking `if (frames % 10 === 0)` inside a click/keydown handler to "debounce" restarts drops **90% of user clicks** because input events fire asynchronously relative to frame ticks! Instead, track a time-based cooldown `gameOverLockTimer`:

```javascript
const STATE = { READY: 'READY', PLAYING: 'PLAYING', GAMEOVER: 'GAMEOVER' };
let state = STATE.READY;
let gameOverLockTimer = 0; // Seconds remaining before restart input is accepted

function enterGameOver() {
  if (state === STATE.GAMEOVER) return;
  state = STATE.GAMEOVER;
  gameOverLockTimer = 0.35; // 350ms grace period prevents accidental instant skip
  bestScore = Math.max(bestScore, score);
  saveHighScore(bestScore);
}

function handleAction() {
  if (state === STATE.READY) {
    resetMatch();
    state = STATE.PLAYING;
    bird.flap();
  } else if (state === STATE.PLAYING) {
    bird.flap();
  } else if (state === STATE.GAMEOVER && gameOverLockTimer <= 0) {
    resetMatch();
    state = STATE.PLAYING;
    bird.flap();
  }
}
```

---

## 4. 2D Collision Detection Mathematics

### 4.1 AABB vs. AABB (Axis-Aligned Bounding Box)

Two rectangles $A = (x_A, y_A, w_A, h_A)$ and $B = (x_B, y_B, w_B, h_B)$ intersect if and only if projections on both axes overlap:

```javascript
function aabbOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
  return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}
```

### 4.2 Forgiving Circle-vs-AABB Collision (Ideal for Rotating Sprites & Birds)

When a sprite rotates visually (like the Flappy Bird tilting between $-25^\circ$ and $+90^\circ$), an axis-aligned rectangle has sharp invisible corners that feel unfair to players. A **circle** of center $(c_x, c_y)$ and radius $r$ is invariant under rotation. Clamp $(c_x, c_y)$ to the rectangle $[x, x+w] \times [y, y+h]$ to find the closest point $(p_x, p_y)$:

```javascript
function circleIntersectsAABB(cx, cy, r, rx, ry, rw, rh) {
  const closestX = Math.max(rx, Math.min(cx, rx + rw));
  const closestY = Math.max(ry, Math.min(cy, ry + rh));
  const dx = cx - closestX;
  const dy = cy - closestY;
  return (dx * dx + dy * dy) < (r * r);
}
```

### 4.3 Preventing the "Fly Over Top Pipe" Bypass Bug

In scrolling obstacle games, if the player flaps near $y = 0$ and the pipe collision check only tests `bird.y < pipe.top` while `bird.y` is allowed to go negative (or if top pipe caps start at $y = 0$), always:
1. Clamp the player ceiling: `if (bird.y - bird.r < 0) { bird.y = bird.r; bird.vy = 0; }`, **and**
2. Treat any horizontal overlap with a pipe while `bird.y - bird.r < pipe.gapTop` as a collision, even if `bird.y < 0`.

---

## 5. Three.js 3D Game Engineering Guide

### 5.1 Modern Import Map & Scene Setup

Use ES Module Import Maps or explicit CDN script tags with a locked Three.js version (`0.160.0`) so `THREE` is reliably available without a bundler:

```html
<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>
<script type="module">
import * as THREE from 'three';
</script>
```

### 5.2 Preventing the Three.js "Black Screen" Bug

Four mistakes cause a blank or black Three.js canvas:
1. **Using `MeshStandardMaterial` or `MeshPhysicalMaterial` without lights**: PBR materials require at least an ambient/hemisphere light (`THREE.HemisphereLight`) and a directional/point light (`THREE.DirectionalLight`). Only `MeshBasicMaterial` renders without lights.
2. **Camera inside the geometry or looking the wrong way**: Always set `camera.position.set(x, y, z)` and `camera.lookAt(targetX, targetY, targetZ)` explicitly.
3. **Zero-height container**: Ensure `html, body { width: 100%; height: 100%; margin: 0; }` and size the renderer via `renderer.setSize(window.innerWidth, window.innerHeight)`.
4. **Missing `camera.updateProjectionMatrix()` on resize**:

```javascript
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
```

### 5.3 Hierarchical Joint Rigging (`THREE.Group` Pivot Pattern)

By default, Three.js primitives (`BoxGeometry`, `CylinderGeometry`) rotate around their geometric center $(0, 0, 0)$. To animate limbs, wings, or doors around a **joint hinge** (shoulder, hip, wing root):
1. Create a `const joint = new THREE.Group()` positioned at the hinge coordinates on the parent body.
2. Create the limb mesh and offset its local position by half its length (`limbMesh.position.y = -limbLength / 2`).
3. Rotate `joint.rotation.x`β€”never the offset child mesh directly!

### 5.4 Preventing WebGL GPU Memory Leaks (`dispose()` Lifecycle)

Unlike JavaScript objects, Three.js `BufferGeometry`, `Material`, and `Texture` instances allocate GPU buffers in WebGL that are **not** freed by the browser's garbage collector when removed from a `Scene`.
- **Rule 1**: Pre-allocate shared geometries and materials once at startup and reuse them across spawned obstacles, bullets, and particles (`const sharedPipeGeo = new THREE.CylinderGeometry(...)`).
- **Rule 2**: For high-count particles or voxel chunks, use `THREE.InstancedMesh` or merged face buffers.
- **Rule 3**: Whenever a unique mesh is permanently destroyed, dispose its GPU resources if not shared:

```javascript
function removeAndDispose(scene, mesh) {
  scene.remove(mesh);
  if (mesh.geometry && !mesh.userData.sharedGeo) mesh.geometry.dispose();
  if (mesh.material && !mesh.userData.sharedMat) {
    if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose());
    else mesh.material.dispose();
  }
}
```

---

## 6. Zero-Asset Procedural Web Audio Synthesizer

Browsers block audio until the first user gesture (`keydown` or `pointerdown`). Use a lazy-initialized `AudioContext` helper to synthesize crisp retro arcade sound effects in pure code:

```javascript
const SFX = (() => {
  let ctx = null;
  function unlock() {
    if (!ctx) {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (AC) ctx = new AC();
    }
    if (ctx && ctx.state === 'suspended') ctx.resume();
    return ctx;
  }
  function tone(type, fStart, fEnd, duration, gainVal = 0.12) {
    const c = unlock();
    if (!c) return;
    const now = c.currentTime;
    const osc = c.createOscillator();
    const gain = c.createGain();
    osc.type = type;
    osc.frequency.setValueAtTime(fStart, now);
    osc.frequency.exponentialRampToValueAtTime(Math.max(20, fEnd), now + duration);
    gain.gain.setValueAtTime(gainVal, now);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
    osc.connect(gain).connect(c.destination);
    osc.start(now);
    osc.stop(now + duration);
  }
  return {
    flap:  () => tone('sine', 320, 640, 0.09, 0.12),
    score: () => { tone('triangle', 587.33, 587.33, 0.07, 0.12); setTimeout(() => tone('triangle', 880, 880, 0.14, 0.12), 65); },
    hit:   () => tone('sawtooth', 160, 38, 0.26, 0.18),
  };
})();
```

---

## 7. Pre-Flight Verification Checklist for Every Game

Before emitting the final `</html>` tag, verify all 8 invariants:
1. **Scope Audit**: Every parameter used in `update(dt)` and `draw(dt)` is either declared in its signature or in module-level state.
2. **First-Frame Timestamp Guard**: `if (!lastTime) lastTime = now;` prevents `NaN` or huge initial `dt`.
3. **Tab-Switch Clamp**: `frameDt = Math.min(0.1, (now - lastTime) / 1000)` prevents teleporting through walls.
4. **Single RAF Loop**: Restarting a match resets state variables without spawning a second concurrent `requestAnimationFrame(loop)`.
5. **Input Responsiveness**: Restart works reliably after a short time-based grace period (`0.35s`), never gated on `frames % N === 0`.
6. **Zero-Allocation Hot Loop**: Particle and projectile pools use swap-and-pop (`pool[i] = pool[pool.length - 1]; pool.pop()`) instead of `Array.shift()` or `Array.filter()` inside 60fps loops.
7. **Safe Storage**: All `localStorage.getItem` / `setItem` calls are wrapped in `try { ... } catch {}` so sandboxed iframes and `file://` origins never throw `SecurityError`.
8. **Three.js Resource Hygiene**: Geometries and materials are shared across obstacle instances, lights are present for PBR materials, and window resize updates both `camera.aspect` and `camera.updateProjectionMatrix()`.