1// Canvas Breakout. 8x12 brick wall, angled paddle bounces, 3 lives, 2// boards speed up. Prize gate: clear board 1 -> 3// onBreakoutGameOver(score, boardsCleared). 4(() => { 5 // Gameplay randomness comes from the server-issued seed so a run can be 6 // replayed and verified; falls back to Math.random if rng.js is absent. 7 const rnd = () => (window.ARNG ? window.ARNG() : Math.random()); 8 const W = 448, H = 536; 9 const BROWS = 8, BCOLS = 12; 10 const BW = 34, BH = 14; 11 const BOX = (W - BCOLS * BW) / 2, BOY = 80; 12 const PY = H - 56; // paddle y 13 const ROW_COLORS = ['#ff3b3b', '#ff3b3b', '#ffb13b', '#ffb13b', '#3bff9d', '#3bff9d', '#ffe12b', '#ffe12b']; 14 const ROW_POINTS = [70, 70, 50, 50, 30, 30, 10, 10]; 15 16 const canvas = document.getElementById('game'); 17 const ctx = canvas.getContext('2d'); 18 19 let bricks, paddle, ball, score, lives, board, boardsCleared, broken; 20 let running, gameOverFired, animT; 21 const input = { left: false, right: false }; 22 23 function buildWall() { 24 bricks = []; 25 for (let r = 0; r < BROWS; r++) for (let c = 0; c < BCOLS; c++) { 26 bricks.push({ r, c, x: BOX + c * BW, y: BOY + r * BH, alive: true }); 27 } 28 } 29 30 function resetBall() { 31 ball = { x: paddle.x, y: PY - 10, vx: 0, vy: 0, stuck: true }; 32 } 33 34 function start() { 35 canvas.width = W; canvas.height = H; 36 paddle = { x: W / 2, w: 64 }; 37 score = 0; lives = 3; board = 0; boardsCleared = 0; broken = 0; 38 running = true; gameOverFired = false; animT = 0; 39 buildWall(); 40 resetBall(); 41 } 42 43 // THE SPEED CURVE, AND THE CEILING THAT USED TO SIT ON IT. 44 // 45 // This is the cabinet's only difficulty ramp â the wall is the same twelve 46 // by eight every board and the paddle is the same width â and it used to be 47 // written `Math.min(8, ...)`. Eight pixels a frame arrives about twenty 48 // bricks into board 3, so board 3 and board 30 were the same wall at the 49 // same speed in front of the same paddle. Nothing climbed after that, and a 50 // player who can return a speed-8 ball can return it all day: the run had no 51 // end, which on this arcade means no score at all, because replay gives a 52 // run forty minutes and then refuses it. 53 // 54 // The eight was never a difficulty decision. It was a collision limit: a 55 // brick is 14 px tall and the ball is tested against the wall once a frame, 56 // so a ball that travels further than a brick between two tests starts 57 // passing straight through them. update() now walks the ball in steps of at 58 // most STEP_MAX and tests at every step, so the physics holds at any speed 59 // and the ceiling that was protecting it can go. 60 // 61 // What is left is a curve that never stops: +0.6 a board, +0.015 a brick, 62 // so roughly two pixels a frame per board, for ever. Board 1 is untouched â 63 // 3.6 with the sub-step loop collapsing to exactly the single step it always 64 // did â and the gate difficulty.js measures is the same gate. 65 function ballSpeed() { 66 return 3.6 + board * 0.6 + broken * 0.015; 67 } 68 69 // The furthest the ball may travel between two collision tests. The tightest 70 // band it must not skip is the paddle's, which is 16 px deep (PY-8 to PY+8), 71 // and a lattice of 8 px cannot step over an interval of 16. 72 const STEP_MAX = 8; 73 74 function launch() { 75 if (!running || !ball.stuck) return; 76 const a = (rnd() * 0.6 - 0.3) - Math.PI / 2; // mostly upward 77 const s = ballSpeed(); 78 ball.vx = Math.cos(a) * s; 79 ball.vy = Math.sin(a) * s; 80 ball.stuck = false; 81 if (window.SFX) SFX.play('shoot'); 82 } 83 84 function endGame() { 85 running = false; 86 if (!gameOverFired) { 87 gameOverFired = true; 88 setTimeout(() => window.onBreakoutGameOver && window.onBreakoutGameOver(score, boardsCleared), 900); 89 } 90 } 91 92 function update() { 93 if (!running) return; 94 animT++; 95 96 if (input.left) paddle.x = Math.max(paddle.w / 2 + 6, paddle.x - 6); 97 if (input.right) paddle.x = Math.min(W - paddle.w / 2 - 6, paddle.x + 6); 98
99 if (ball.stuck) { ball.x = paddle.x; ball.y = PY - 10; return; } 100 101 // keep speed in sync with difficulty 102 const sp = ballSpeed(); 103 const cur = Math.hypot(ball.vx, ball.vy) || 1; 104 ball.vx *= sp / cur; ball.vy *= sp / cur; 105 106 // One frame of travel, walked in steps of at most STEP_MAX so the ball is 107 // never tested only once it is already through something. At every speed 108 // the cabinet used to allow, this is a single step and the arithmetic 109 // below is the arithmetic it has always done. 110 const steps = Math.max(1, Math.ceil(sp / STEP_MAX)); 111 for (let i = 0; i < steps; i++) if (!moveBall(ball.vx / steps, ball.vy / steps)) return; 112 } 113 114 // One sub-step. Returns false when there is nothing left to move this frame: 115 // the board was cleared out from under the ball, or the ball was lost and 116 // the next one is sitting on the paddle. 117 function moveBall(dx, dy) { 118 ball.x += dx; 119 ball.y += dy; 120 121 // walls 122 if (ball.x < 9) { ball.x = 9; ball.vx = Math.abs(ball.vx); } 123 if (ball.x > W - 9) { ball.x = W - 9; ball.vx = -Math.abs(ball.vx); } 124 if (ball.y < 34) { ball.y = 34; ball.vy = Math.abs(ball.vy); } 125 126 // paddle 127 if (ball.vy > 0 && ball.y > PY - 8 && ball.y < PY + 8 && Math.abs(ball.x - paddle.x) < paddle.w / 2 + 5) { 128 const off = (ball.x - paddle.x) / (paddle.w / 2); // -1..1 129 const angle = -Math.PI / 2 + off * 1.05; 130 const s = ballSpeed(); 131 ball.vx = Math.cos(angle) * s; 132 ball.vy = Math.sin(angle) * s; 133 ball.y = PY - 9; 134 if (window.SFX) SFX.play('bounce'); 135 } 136 137 // bricks 138 for (const b of bricks) { 139 if (!b.alive) continue; 140 if (ball.x > b.x - 5 && ball.x < b.x + BW + 5 && ball.y > b.y - 5 && ball.y < b.y + BH + 5) { 141 b.alive = false; 142 broken++; 143 if (window.SFX) SFX.play('hit'); 144 score += ROW_POINTS[b.r]; 145 // reflect on the axis of least penetration 146 const px = Math.min(ball.x - (b.x - 5), (b.x + BW + 5) - ball.x); 147 const py = Math.min(ball.y - (b.y - 5), (b.y + BH + 5) - ball.y); 148 if (px < py) ball.vx = -ball.vx; else ball.vy = -ball.vy; 149 break; 150 } 151 } 152 153 if (bricks.every(b => !b.alive)) { 154 if (window.SFX) SFX.play('clear'); 155 boardsCleared++; 156 board++; 157 buildWall(); 158 resetBall(); 159 return false; 160 } 161 162 // bottom 163 if (ball.y > H - 20) { 164 lives--; 165 if (window.SFX) SFX.play('death'); 166 if (lives <= 0) { endGame(); return false; } 167 resetBall(); 168 return false; 169 } 170 return true; 171 } 172 173 // ---------------------------------------------------------------- draw 174 175 function draw() { 176 ctx.fillStyle = '#000'; 177 ctx.fillRect(0, 0, W, H); 178 179 // HUD 180 ctx.fillStyle = '#fff'; 181 ctx.font = 'bold 14px monospace'; 182 ctx.fillText(`SCORE ${score}`, 10, 24); 183 ctx.fillText(`BOARD ${board + 1}`, W / 2 - 30, 24); 184 window.ARCADE_GATE = { label: 'CLEAR BOARD 1', unlocked: boardsCleared >= 1 }; 185 for (let i = 0; i < lives; i++) { 186 ctx.fillStyle = '#3bd6ff'; 187 ctx.fillRect(W - 30 - i * 26, 16, 20, 5); 188 } 189 if (boardsCleared >= 1) { 190 ctx.fillStyle = '#3bff9d'; 191 ctx.font = 'bold 11px monospace'; 192 ctx.fillText('PRIZE ROLL UNLOCKED!', 10, 44); 193 } 194 ctx.strokeStyle = '#2438d9'; 195 ctx.lineWidth = 3; 196 ctx.strokeRect(4, 32, W - 8, H - 36); 197 198 // bricks 199 for (const b of bricks) { 200 if (!b.alive) continue; 201 ctx.fillStyle = ROW_COLORS[b.r]; 202 ctx.fillRect(b.x + 1, b.y + 1, BW - 2, BH - 2); 203 ctx.fillStyle = 'rgba(255,255,255,0.25)'; 204 ctx.fillRect(b.x + 1, b.y + 1, BW - 2, 3); 205 } 206 207 // paddle 208 ctx.fillStyle = '#3bd6ff'; 209 ctx.fillRect(paddle.x - paddle.w / 2, PY, paddle.w, 10); 210 ctx.fillStyle = 'rgba(255,255,255,0.4)'; 211 ctx.fillRect(paddle.x - paddle.w / 2, PY, paddle.w, 3); 212 213 // ball 214 ctx.fillStyle = '#fff'; 215 ctx.beginPath(); ctx.arc(ball.x, ball.y, 5, 0, Math.PI * 2); ctx.fill(); 216
217 if (ball.stuck && Math.floor(animT / 30) % 2 === 0) { 218 ctx.fillStyle = '#ffe12b'; 219 ctx.font = 'bold 13px monospace'; 220 ctx.textAlign = 'center'; 221 ctx.fillText('PRESS FIRE / SPACE TO LAUNCH', W / 2, H - 90); 222 ctx.textAlign = 'left'; 223 } 224 } 225 226 // ---------------------------------------------------------------- loop 227 228 // Fixed timestep: update() runs at a constant 60Hz whatever the display 229 // does. Stepping once per animation frame ran the whole game at double 230 // speed on a 120Hz screen, and a variable step can never be replayed 231 // deterministically server-side. 232 const FRAME_MS = 1000 / 60; 233 let rafId = null, acc = 0, prevTs = 0; 234 function frame(ts) { 235 if (!prevTs) prevTs = ts; // first frame just sets the clock 236 acc += Math.min(250, ts - prevTs); // cap catch-up after a background tab 237 prevTs = ts; 238 // Paused: hold the clock, run no updates, keep the last frame on screen. 239 // Update COUNT is untouched, so a paused run still replays exactly. 240 if (window.ARCADE_PAUSED) { acc = 0; rafId = requestAnimationFrame(frame); return; } 241 while (acc >= FRAME_MS) { update(); acc -= FRAME_MS; window.ARCADE_FRAME++; } 242 draw(); 243 rafId = requestAnimationFrame(frame); 244 } 245 246 const KEYS = { ArrowLeft: 'left', a: 'left', ArrowRight: 'right', d: 'right' }; 247 const LAUNCH = { ' ': 1, ArrowUp: 1, w: 1 }; 248 document.addEventListener('keydown', e => { 249 if (window.ARCADE_KEY(LAUNCH, e)) { window.__brkLaunch(); e.preventDefault(); return; } 250 const k = window.ARCADE_KEY(KEYS, e); 251 if (k) { window.__brkSetKey(k, true); e.preventDefault(); } 252 }); 253 document.addEventListener('keyup', e => { 254 const k = window.ARCADE_KEY(KEYS, e); 255 if (k) window.__brkSetKey(k, false); 256 }); 257 258 window.startBreakout = () => { 259 start(); 260 if (!rafId) { acc = 0; prevTs = 0; rafId = requestAnimationFrame(frame); } 261 }; 262 window.stopBreakout = () => { 263 running = false; 264 if (rafId) { cancelAnimationFrame(rafId); rafId = null; } 265 }; 266 window.__brkSetKey = (k, down) => { input[k] = !!down; }; 267 window.__brkLaunch = () => launch(); 268 window.__brkStep = (n) => { 269 for (let i = 0; i < (n || 1); i++) update(); 270 draw(); 271 return { score, lives, board, boardsCleared, running, bricksLeft: bricks.filter(b => b.alive).length, ballStuck: ball.stuck, paddleX: Math.round(paddle.x), ballX: Math.round(ball.x), ballY: Math.round(ball.y), 272 broken, ballSpeed: +ballSpeed().toFixed(3) }; 273 }; 274 // test helper: kill all bricks except n 275 window.__brkLeaveBricks = (n) => { 276 const alive = bricks.filter(b => b.alive); 277 for (let i = 0; i < alive.length - n; i++) alive[i].alive = false; 278 }; 279})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.