PageSourceSearch

https://webcade.fun/ricochet.js?v=1025436e

js webcade.fun collected 2026-10-08 08:27:23 UTC 10,625 bytes, 279 lines download raw bytes

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.