### PARTIAL OUTPUT (time limit hit; the model was still generating) ###
=== thinking so far ===
The user wants a complete falling-blocks puzzle game (Tetris) in a single self-contained HTML file. Let me plan this out carefully and make it genuinely good — not just functional, but polished with a distinctive visual identity.
**Core requirements checklist:**
1. 10×20 playfield on canvas ✓
2. Seven tetromino shapes, four squares each, distinct colors ✓
3. Controls: ←/→ move, ↑ rotate, ↓ soft drop, space hard drop ✓
4. Rotation with wall/block collision checks (wall kicks ideally) ✓
5. Line clears with proper cascade, multi-line bonus scoring ✓
6. Score, lines, level display; speed scales with level ✓
7. Next-piece preview ✓
8. Game over at top, visible state, restart key (R) ✓
9. Clean dark visual style ✓
**Design decisions:**
Visual identity: I want a dark, refined look — not the typical purple-gradient arcade cliché. I'll go for a deep charcoal/near-black background with a warm amber/gold accent as the signature color, or maybe a cool desaturated palette with one accent. Let me think about the palette.
Standard tetromino colors are cyan, yellow, purple, green, red, blue, orange. The requirement says "each with its own color" — I can pick my own refined palette rather than the garish neon defaults. I'll design a cohesive palette: desaturated-but-distinct hues that read well on dark. Something like:
- I: teal/cyan `#4ECDC4` — hmm, let me pick a curated set:
- I: `#5FB4C9` (steel teal)
- O: `#E8C468` (muted gold)
- T: `#B58BC9` — maybe `#A97BC9`? Let me go with a lavender that isn't purple-gradient territory: `#B08BC9`
- S: `#7FBF7F` → `#74B874`
- Z: `#D97A7A` (dusty red)
- J: `#7A9BD9` (dusty blue)
- L: `#E0A05E` (amber orange)
Actually, to be distinctive, I could theme the whole thing around a "terminal / blueprint" aesthetic. Let me go with a warm-dark scheme: background `#0E0F12`ish, panel `#16181D`, subtle grid lines, amber accent `#E8A33D` for score highlights. Typography: I can't load external fonts (no internet resources!). So I need system fonts only. The user said "no internet resources" — so no Google Fonts, no CDN. I'll use a monospace stack which fits the game aesthetic: `"Courier New", ui-monospace, monospace` or better `ui-monospace, "SF Mono", "Cascadia Mono", Consolas, monospace`. Monospace fits the puzzle/game feel and avoids the generic Inter/Roboto look. For headings I could use letter-spacing to make it feel designed.
**Rendering approach:**
- Main canvas for playfield: 10×20 grid, each cell ~30px → 300×600. I'll add device-pixel-ratio scaling for crisp rendering.
- Blocks: draw with subtle inner shading — a flat fill with a slightly lighter top-left bevel and darker bottom edge, or a minimal style: fill + 1px inset border + subtle top highlight. I'll do a clean "flat with inner stroke and corner highlight" look. Maybe round the corners slightly? Rounded corners on blocks look modern. I'll draw each cell as a rounded rect inset by 1px with a subtle gradient... no, I'm avoiding decorative gradients as design crutches, but a subtle vertical shading within a game block is functional shading (giving blocks physical depth), which is legitimate for a game. Still, I can achieve depth with a lighter top edge line and darker bottom edge line — crisp and clean without gradients. Let me do: base fill, then a 2px lighter strip at top (rgba white 0.25), 2px darker at bottom (rgba black 0.35), and 1px dark outline. That reads as classic beveled-but-flat tiles. Actually for a cleaner look: flat fill with slightly darker border and a small inner highlight square? Keep it simple: fill + inner border darker + top highlight line. Good.
- Ghost piece: show where the piece will land (very helpful, standard in good implementations). Semi-transparent outline of the piece. This is a nice quality touch.
- Grid: very subtle lines `rgba(255,255,255,0.04)`.
- Row clear effect: flash animation before removal? I can do a quick flash: when lines clear, animate a white flash over those rows for ~200ms then collapse. That adds life. Implementation: on clear, store clearing rows, set a `clearingTimer`, during which the rows render as brightening; after timer, actually remove rows. Keep game logic paused for spawn during this. This is a "signature moment" detail.
- Also hard-drop could have a tiny landing effect — maybe a quick downward trail. Keep it subtle: draw a brief "drop trail" fading. Could be nice but adds complexity; maybe a simple screen-shake on hard drop? A very subtle 2px shake for 100ms on hard drop landing adds tactile feel. I'll implement a small shake offset. And a subtle pulse on the score when it changes? Maybe skip; keep focused.
**Layout:**
- Centered layout: game title small on top-left of a side panel? Layout: playfield centered, side panel on right with NEXT preview, SCORE, LINES, LEVEL, and controls legend at bottom. Dark panels with thin borders. Avoid card-grid feel — one panel beside the board is natural for a game HUD.
Let me sketch:
```
BLOCKFALL (title, letterspaced, monospace)
┌─────────┬──────┐
│ │ NEXT │
│ │ [box]│
│ board │──────│
│ │ SCORE│
│ │ 0000 │
│ │ LINES│
│ │ LEVEL│
└─────────┴──────┘
controls legend line
```
The side panel items stacked — that's fine for a game HUD, it's functional not filler.
**Game mechanics details:**
Pieces (using standard matrices). I'll define each piece as a 4×4 or minimal matrix of rotations. Simplest robust approach: define shapes as arrays of rotation states, each state an array of [x,y] offsets, or as square matrices rotated programmatically. I'll define base matrices and rotate programmatically with collision-checked kicks.
Standard spawn orientations (I'll use compact coordinate lists or matrices):
- I: 4×4 matrix `[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]]`
- O: 2×2 `[[1,1],[1,1]]`
- T: `[[0,1,0],[1,1,1],[0,0,0]]`
- S: `[[0,1,1],[1,1,0],[0,0,0]]`
- Z: `[[1,1,0],[0,1,1],[0,0,0]]`
- J: `[[1,0,0],[1,1,1],[0,0,0]]`
- L: `[[0,0,1],[1,1,1],[0,0,0]]`
Rotation: rotate matrix clockwise (for up arrow). For O piece, rotation is identity. Collision-checked rotation with simple wall kicks: try offsets [0, -1, +1, -2, +2] horizontally and maybe vertical adjustments. For a solid feel, I'll implement basic kicks: attempt `dx` in [0, -1, 1, 0, -2, 2, 0] with `dy` in [0, 0, 0, -1, 0, 0, -1]... Let me do a simple kick table: try candidate offsets `[[0,0],[-1,0],[1,0],[0,-1],[-2,0],[2,0],[-1,-1],[1,-1]]` — first valid wins. That handles most wall/floor situations well. Good enough and honest ("rotation must respect walls and stacked blocks" — collision check ensures no clipping; kicks make it feel good).
Gravity: level-based speed. Classic formula-ish: `dropInterval = Math.max(60, 800 - (level-1)*70)` or use a curve: `interval = 800 * Math.pow(0.82, level-1)` clamped min 60ms. Level = 1 + floor(lines/10). Scoring: 1 line=100, 2=300, 3=500, 4=800, multiplied by level. Soft drop: +1 per cell; hard drop: +2 per cell.
Randomization: 7-bag randomizer for fair piece distribution — good craft detail. Shuffle bag of 7 indices; refill when empty.
**Controls:**
- Keydown handling with DAS-like key repeat? For left/right, browsers have native key repeat which is okay but sluggish. I'll implement my own repeat: track held keys, initial delay 170ms, repeat 50ms. Also soft drop held = faster gravity (like 50ms interval) with +1 score per cell. Hard drop on keydown space (prevent default scrolling).
- R to restart after game over (and also allowed anytime? I'll allow R anytime to restart, or only on game over — requirement says restart key on game over; I'll make R work anytime for convenience but primarily game over). Also maybe P for pause? Not required, but a nice touch — pause with P and overlay. I'll add it; it's cheap and improves the game. Keep the overlays: "PAUSED", "GAME OVER — press R".
- Also Enter could restart on game over too. Keep R as stated.
**Game over condition:** when a newly spawned piece collides immediately at spawn position → game over. Also if stack reaches top... spawn collision is the standard trigger. Spawn at top rows (y=0 or slightly negative). I'll spawn pieces with their matrix top at y=0; if collision at spawn (allowing negative y within buffer?), simpler: spawn at y=0; if collides, game over. To be safer, spawn at y = -1 so flat-top pieces get a moment. Hmm, with rotation matrices having empty top rows, spawning at y=0 is fine mostly. I'll spawn at y=0 and check collision — if collides, game over. Also during the piece's life, if a piece is locked and any cell is above row 0... Actually with 20 visible rows and no buffer, cells can't go above 0 because movement up is only via rotation kicks which I'll disallow if it would move blocks above the field — wait, my kick table includes dy=-1 which could push cells above row 0 if y=0. I'll make `collides` treat y<0 as occupied (block) except... hmm. Standard approach: board has 20 rows; pieces spawn with y possibly negative and cells with negative y are allowed (not rendered, not colliding), collision only for y>=20 (floor), x<0, x>=10, or y>=0 && board[y][x]. Then game over check: after spawn, if collides at spawn (considering only cells at y>=0? no — if spawn collides including negative-y cells being fine)...
Simplest robust: spawn at y=0 with the piece's matrix (first row often empty for T/S/Z/J/L, so blocks appear at row 1). If `collides` at spawn → game over. For kicks with dy=-1: if y becomes -1, cells at negative y — I'll treat negative y as empty (allowed), and cells with y<0 don't get written to board but also shouldn't collide. But then a rotation could push a block "above the field" and after locking... piece can't lock above field unless it can move up there. If a cell is at y=-1 and it locks, that block is lost and stack reaching top isn't detected. To handle "stack reaches the top" honestly: disallow upward kicks that would place cells above y=0? That makes T-spins at the wall harder but it's fine for this game. Alternative: allow up to y=-2 buffer with a hidden buffer zone of 2 rows.
Cleanest: use a board of 20 visible rows but track piece cells normally; forbid any piece cell at y<0 permanently (collision returns true for y<0). Rotation kicks with dy=-1 will then simply fail near the top, which is correct behavior (can't rotate into the top). Since pieces spawn fully within the field (y=0), everything stays inside. Locking above top: piece locks wherever it is, all within 0..19. Game over when spawn collides. Also I should render the stack when it's near top — fine.
Edge case: piece spawns at y=0, its occupied rows might be at matrix row 0 (e.g., S/Z/J/L have blocks at row 0; I piece blocks at row 1). If stack is at row 0 and piece spawns colliding → game over. Good, that satisfies "stack reaches the top."
**Line clear with flash:**
When lock happens: find full rows. If any, set `clearingRows = rows`, `clearTimer = 260ms`, state stays but gravity pauses (don't spawn next piece until clear animation done). During clearing, draw those rows with a flash (white overlay pulsing, maybe expanding from center? Simple: brightness ramp). After timer, remove rows, add score, spawn next. Add score multiplier × level. Also maybe a tiny particle burst? Keep to the flash + slight shake for 4-line clears. Actually a subtle shake on any clear, scaled by count — nice tactile touch.
**Particles:** small celebratory particles when lines clear? Could add small squares flying... "no plain small squares as particles" — I avoid that; if I add particles, they'd be small glowing shards/lines. Maybe skip particles to keep it clean; the flash + shake is enough. Actually a few thin line-streaks flying from cleared rows would look great and fast. Hmm, I'll add subtle rectangular glints... Let me decide: I'll implement a modest particle system with small rotated rectangles (shards) in the cleared rows' colors, fading — rotated rects with varied sizes, not plain axis-aligned squares. That elevates the moment. Keep count modest (~8 per row) for performance. It's cheap to implement: {x,y,vx,vy,rot,vr,size,color,life}. Update in RAF loop with dt. Draw on the same canvas over everything (or a separate overlay canvas — same is fine).
**DPR handling:** canvas width = cols*cell, height = rows*cell; scale by devicePixelRatio; CSS size fixed. Cell size: 30px → 300×640? rows 20 → 600px tall. On smaller screens, scale via CSS max-height. I'll set canvas CSS to `height: min(70vh, 600px)` with aspect ratio preserved, and internal resolution fixed at 300×600 * dpr. Use `image-rendering` default (crisp edges via proper dpr scaling).
Actually simpler: set canvas internal size to `COLS*CELL*dpr`, CSS size via style with a responsive `--cell` computed... To keep it simple: fixed logical 300×600 canvas, CSS `height: min(72vh, 600px); width: auto; aspect-ratio: 1/2`. And use dpr for sharpness.
Preview canvas: 4×4 cells (e.g., 96×96 logical), draw next piece centered.
**HUD updates:** DOM elements for score/lines/level updated on change (with a brief "bump" CSS animation on score change — transform scale pulse via class toggle).
**Structure of code:**
- Constants: COLS=10, ROWS=20, CELL=30.
- PIECES definition: `shapes` object mapping type → { matrix, color }.
- Board: 2D array of null/type.
- State: `board, current {type, matrix, x, y}, next type, bag, score, lines, level, dropCounter, dropInterval, gameOver, paused, clearing rows/timer, particles[], shake`.
- Loop: `requestAnimationFrame` with dt; update gravity, held-key repeat, clearing animation, particles, shake decay; render everything each frame.
- Input: keydown/keyup listeners; `keysHeld` set with per-key timers (I'll compute repeat in update loop using held timestamps — cleaner: store `lastMoveTime` and do DAS in update).
- Hard drop: compute drop distance, add 2×distance score, set y, lock immediately.
- Lock: write cells; if any cell y<0 → game over (won't happen since we forbid negative); check full rows → clearing flow or spawn next; check spawn collision → game over.
Wait — with collision forbidding y<0, the piece spawns at y=0 fully inside. Good.
Rotation function: `rotate(matrix, dir)` returns new matrix. CW: `new[x][row]`... standard: for clockwise, `res[c][n-1-r] = m[r][c]`. For CCW (maybe add Z key? Up arrow only per requirements; I could also add Z for CCW as a bonus — sure, cheap and nice. Requirements say up rotates; extra Z for counter-rotate is fine.)
Kick attempts on rotate: `offsets = [[0,0],[-1,0],[1,0],[0,-1],[-1,-1],[1,-1],[-2,0],[2,0],[0,-2]]`? Keep list modest: `[[0,0],[-1,0],[1,0],[0,-1],[-2,0],[2,0],[0,-2]]`. Hmm, [0,-2] might allow clipping? No — it's collision-checked, so it can only help. Fine.
Soft drop: while Down held, gravity interval becomes ~45ms and each soft-drop cell adds +1 point. Implementation: in update, if down held, use `softInterval`; when gravity step succeeds due to soft drop, score += 1. Simpler: when down is held, set dropInterval to min(normal, 45); on each successful down move, if down held, +1. But normal gravity steps shouldn't add score. Track which interval caused the step.
Actually cleaner: implement `stepDown()` called by gravity timer; and in update, accumulate `dropCounter += dt`; threshold = downHeld ? Math.min(softMs, interval) : interval; when threshold reached, try move down; if moved && downHeld → score+=1 (soft drop); if can't move → lock. But rapid lock on failed soft drop is standard — lock immediately when down-held and blocked? That makes hard lock feel responsive; but could cause accidental instant locks. Standard: soft drop failing triggers lock attempt (lock delay ~500ms normally; keep simple: lock on fail immediately when gravity step occurs, plus for soft drop immediate). For simplicity and good feel: when move down fails, lock immediately (no lock delay). That's classic simple Tetris. Fine.
Hard drop: while not colliding, y++; then lock. Add trail effect? Draw the hard drop as instant; maybe add a quick vertical "streak" particle per column at the piece's columns... The shake (1-2px, 80ms) on hard drop landing is enough.
**Rendering details:**
- Background of board area: `#0B0C0F` with subtle grid lines `rgba(255,255,255,0.045)`.
- Board border: 1px `#2A2D34` drawn via CSS on canvas container; the canvas itself has the dark bg.
- Draw locked blocks and current piece with `drawCell(x, y, color)`:
```
fill base color rect (x*c, y*c, c, c)
lighter top strip: rgba(255,255,255,0.28) height 3
darker bottom strip: rgba(0,0,0,0.30) height 3?
left highlight thin, right shadow thin
1px inner stroke rgba(0,0,0,0.4)
```
Also a tiny corner highlight dot? Keep to bevel strips. And I'll add a very subtle inner darker border to separate cells.
- Ghost piece: stroke-only rounded cells with color at 35% alpha, dashed? Solid thin stroke at 0.35 alpha, no fill (or fill 0.08). I'll do fill rgba(color,0.10) + stroke rgba(color,0.45).
- Clearing rows flash: overlay `rgba(255,255,255, alpha)` where alpha ramps 0→0.9 over the timer, and maybe cells scale/fade. I'll fade cells out while flashing white. Simple: draw row normally, then white rect with alpha = t*0.85, plus the row's blocks shrink toward the row center horizontally? Keep flash + slight vertical squash: scaleY of cells from 1→0. Nice touch: as timer progresses, cells shrink. Implement by drawing cell with inset growing: `pad = t * CELL/2` and draw shrinking rect with the piece color, plus white flash overlay. Good.
- Game over: dim board with `rgba(6,8,10,0.72)` overlay, "GAME OVER" text (canvas or DOM overlay). I'll use a DOM overlay div positioned over the canvas — easier typography, letterspacing, and can animate in. Overlay shows final score/lines and "PRESS R TO RESTART". Also paused overlay.
**DOM structure:**
```html
title + tagline
...
```
Panel styling: thin borders `1px solid #23262C`, background `#121418`, no rounded overkill (2-4px radius), labels in tiny uppercase letterspaced gray, values in larger monospace amber/white. The accent color: warm amber `#E8A33D` used sparingly: score value, level highlight, title accent. Hmm, or a cooler mint accent? The piece colors already carry chroma; the HUD accent should be one restrained hue. Amber on charcoal is classic and warm. Go amber: `#E5A24B`ish. Actually let me define: `--accent: #E8A33D`.
Background: `#0C0D10` page, subtle noise? Could add a faint radial vignette via CSS — a gradient... I'm avoiding decorative gradients; a solid deep background with a very subtle dot/grid pattern could add texture. I could add an extremely subtle repeating pattern via CSS `background-image` with radial-gradient dots — that's texture, but I'd rather keep it solid and let the game be the focus. Solid `#0B0C0E`.
Title: "BLOCKFALL" or something less generic — "GRAVITY GRID"? "QUADRA"? Seven four-block shapes → "TETRA" names are trademarked-adjacent; I'll call it **"STACKLINE"** or "FOURFOLD"? Let me pick "STACKFALL"? Hmm. "LINEDROP"? I'll go with **"TETRAFALL"**... too derivative. **"BLOCKFALL"** is clear and honest. Or "GRIDLOCK"? Cute but confusing. I'll use **BLOCKFALL** with a small subtitle "a falling-blocks puzzle". Fine.
Controls legend: small table/list with key caps styled as `` boxes: ←/→ move, ↑ rotate, ↓ soft drop, SPACE hard drop, P pause, R restart.
**Held-key DAS implementation:**
```js
const held = {}; // key -> {t: ms since held, moved initial}
keydown: if repeat event (e.repeat) ignore for actions like rotate/harddrop; for left/right/down set held[key].start = now and immediately act once, set nextRepeat = start + DAS(160).
update(dt): for each held key with direction, if now >= nextRepeat, move, nextRepeat += ARR(45).
```
Simplest: store `heldDir` and timers:
```js
let dasTimer = 0, arrTimer = 0;
on keydown Left (not repeat): move(-1); held.left=true; dasTimer=0;
update: if held.left XOR held.right (prefer latest pressed?) — track last pressed direction priority.
```
Simpler robust: keep `keys = {}` map of code→pressTime; a `moveHeld` state {dir, timer}. On keydown of ArrowLeft/Right (not repeat): perform move, set `moveHeld={dir, t:0}` (overwrite; remember the other key still held in keys so on keyup restore). On keyup: if the released key was the active dir, check if the other arrow is still in `keys` → activate it (move once? just set held without move), else moveHeld=null. In update: if moveHeld, t+=dt; while t>DAS: t-=ARR... implement:
```js
if (moveHeld) { moveHeld.t += dt; while (moveHeld.t > DAS) { if (move(dx)) {} moveHeld.t -= ARR; } }
```
Wait that loop: after DAS exceeded, repeat every ARR: `while (moveHeld.t > DAS + ARR*n)` — simpler: `moveHeld.t += dt; if (moveHeld.t > DAS) { moveHeld.t -= ARR; if (moveHeld.t < DAS) moveHeld.t = DAS; move(dx) }` — that gives repeat every ARR ms after DAS. Let me do:
```js
moveHeld.t += dt;
if (moveHeld.t >= DAS) {
moveHeld.t -= ARR;
if (moveHeld.t >= DAS) moveHeld.t = DAS - ARR; // clamp so repeats are ARR apart
move(dx);
}
```
Hmm getting fiddly. Alternative simple:
```js
if (moveHeld.t >= DAS) { moveHeld.repeat = (moveHeld.repeat||DAS) ... }
```
Cleaner: track `nextRepeatAt = DAS` initially; when t >= nextRepeatAt: move; nextRepeatAt += ARR.
```js
moveHeld.t += dt;
while (moveHeld.t >= moveHeld.next) { move(dx); moveHeld.next += ARR; }
```
with moveHeld.next initialized to DAS (170). Edge: if dt huge, loop bounded — cap dt at 100ms. Good.
Soft drop: `downHeld` boolean; gravity threshold = downHeld ? 45 : interval. On keydown down (not repeat): immediate soft step + set downHeld. Actually just let the update loop handle it with the short interval — with 45ms it's responsive enough; but the first step should be immediate on keydown. I'll do immediate on keydown then held handling via interval. The gravity accumulator: when downHeld, on each step add +1 score.
Also rotate on keydown ArrowUp (not repeat) and KeyZ (ccw, bonus). Space hard drop on keydown (not repeat), preventDefault.
Prevent default for arrows/space to stop page scroll.
**Bag randomizer:**
```js
let bag = [];
function nextType() { if (!bag.length) { bag = shuffle(['I','O','T','S','Z','J','L']); } return bag.pop(); }
```
**Spawn:**
```js
function spawn() {
current = { type: nextQueue.shift(), matrix: clone(SHAPES[type]) , x: Math.floor((COLS - matrix.length)/2), y: 0 };
nextQueue.push(nextType());
if (collides(current.matrix, current.x, current.y)) → game over sequence.
}
```
I'll keep `next` as a single piece (preview shows one). Maintain `nextType` var; on spawn, current = next, next = randomType(). Initialize both at start.
x centering: for 4-wide I matrix, x=3 → occupies cols 3-6 ✓. For 3-wide, x = floor((10-3)/2)=3 → cols 3-5, roughly centered ✓ (standard is 3 for left-leaning). O 2×2: x=4 ✓.
**Lock delay / instant lock:** when gravity step fails, lock immediately. But if the piece is blocked mid-air next to an overhang... instant lock is classic. However there's a subtlety: piece spawns, immediately can't move down because stack is high → locks immediately → fine, leads to game over via spawn collision soon. OK.
Hmm, one nuance: on lock, after writing to board, check `board.some cell at row < 2?` Actually game-over-on-spawn covers it.
**Scoring constants:** `{1:100, 2:300, 3:500, 4:800}` × level. Level = Math.floor(lines/10)+1. Speed: `interval = Math.max(60, Math.round(850 * Math.pow(0.8, level-1)))` → L1: 850, L2: 680, L3: 544, L4: 435, L5: 348, ... L10: ~114... let me compute: 850*0.8^9 = 850*0.134=114 → above min. L12: 850*0.8^11≈73→clamped 60ish. Fine. Maybe min 70. OK.
**Clearing flow:**
```js
lock(): write cells; rows = fullRows();
if (rows.length) { clearing = {rows, t:0}; score += SCORES[rows.length]*level (add now or after? add now, show bump); lines += rows.length (add after animation? add now simpler); recompute level & interval; spawn particles from those rows; shake = rows.length*2; }
else spawn();
```
During update: if clearing: clearing.t += dt; if t >= 300: remove rows (splice and unshift empty), clearing=null; spawn(). While clearing, current is null (skip gravity/moves). Input moves should be ignored while current is null — guard all input actions with `if (!current || gameOver || paused) return`.
Wait — ordering: when clearing, the current piece just locked, so current=null until spawn. ✓.
Particles spawn at locked row positions in the piece... use the cleared rows' original block colors (read from board before removal) — nice. Each particle: x,y in px within row, vx random ±(40-160), vy random (-160..40) upward bias, gravity +400 px/s², life 0.6-1s, size 2-5px, rotation spin. Draw as rotated rects with alpha = life. Not plain axis-aligned squares — they're rotated shards with varying aspect. Good.
**Shake:** `shake = intensity`; each frame apply random offset `translate((rand-0.5)*shake, ...)` to canvas drawing, decay `shake *= pow(0.0001, dt)` or `shake = max(0, shake - 20*dt*... )`. I'll use exponential decay: `shake *= Math.exp(-12*dt)` and apply while >0.2.
**Score bump animation:** on score change, add class 'bump' to value el, remove after animation (use `el.classList.remove` then void offsetWidth trick, or listen animationend). Simple:
```js
function bump(el){ el.classList.remove('bump'); void el.offsetWidth; el.classList.add('bump'); }
```
**Game over overlay:** div with heading "GAME OVER", final stats, "PRESS R TO RESTART", class 'show' toggles opacity/transform transition. Paused: "PAUSED — P TO RESUME". One overlay element, content swapped.
**Restart:** reset board, bag, score/lines/level, spawn fresh, hide overlay.
**Canvas sizing:**
```js
const CELL = 30, COLS = 10, ROWS = 20;
canvas.width = COLS*CELL*dpr; canvas.height = ROWS*CELL*dpr; ctx.scale(dpr,dpr); canvas.style.width/height set via CSS (height: min(70vh,600px); aspect-ratio 10/20).
```
For preview: 4 cells × 24px = 96 logical. Draw next piece centered based on its matrix bounding box. Compute occupied cells bounds to center properly.
Font stack: `ui-monospace, "SFMono-Regular", "Cascadia Mono", "JetBrains Mono", Consolas, "Courier New", monospace`. Hmm — "Courier New" as fallback is fine at the end.
Let me also handle `visibilitychange` → auto-pause? Nice touch: pause when tab hidden (only if game running). Cheap: on visibilitychange hidden && running → paused=true, show overlay. I'll include it.
Also mobile/touch? Not required; keyboard game. I'll note controls are keyboard. Could add minimal touch buttons... skip to keep focused; maybe mention nothing. Actually adding touch would bloat; requirement is keyboard. Skip.
**Now write the render loop carefully:**
```js
let last = 0;
function frame(now) {
const dt = Math.min(100, now - last); last = now;
update(dt);
render();
requestAnimationFrame(frame);
}
```
update(dt):
- if paused or gameOver: still update particles? On game over, freeze everything (maybe let particles finish — fine to keep updating particles only). I'll update particles always, but gravity/input/clearing only when playing.
- clearing animation timer.
- DAS handling.
- gravity accumulation → step.
render():
- clear canvas, fill bg
- save, translate shake offset
- draw grid lines
- draw locked cells (skip clearing rows? draw them with flash/shrink effect)
- draw ghost, current piece
- draw clearing white flash
- draw particles
- restore
Cell drawing function:
```js
function drawCell(ctx, px, py, size, color, alpha=1) {
// base
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.fillRect(px+1, py+1, size-2, size-2);
// top light
ctx.fillStyle = 'rgba(255,255,255,0.25)';
ctx.fillRect(px+1, py+1, size-2, 3);
// left light thin
ctx.fillStyle = 'rgba(255,255,255,0.10)';
ctx.fillRect(px+1, py+1, 3, size-2);
// bottom shadow
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.fillRect(px+1, py+size-4, size-2, 3);
ctx.globalAlpha = 1;
}
```
Plus outer 1px gap comes from the +1 inset. Looks like clean beveled tiles. Maybe also round? Keep squares — grid coherence.
Ghost:
```js
ctx.strokeStyle = color w/ alpha .5; strokeRect(px+1.5,...) using globalAlpha.
```
Compute ghost by dropping copy of current until collision.
Grid: vertical & horizontal lines at every cell, strokeStyle 'rgba(255,255,255,0.05)', lineWidth 1 — draw at 0.5 offsets? With dpr scaling, half-pixel matters less; just draw lines and it'll antialias fine.
Also, subtle column shading? No, keep clean.
Clearing row effect:
```js
const p = clearing.t / CLEAR_MS; // 0..1
for each cell in row: shrink: pad = p * CELL*0.5; draw cell rect color with alpha (1-p)
then fillRow white alpha p*0.9?
```
Let me implement: for rows in clearing.rows: for each col, if board cell: draw shrinking colored rect (inset grows with p). Then over the full row: `ctx.fillStyle = rgba(255,255,255, p*0.75)` fill rect row. Also a horizontal bright line expanding from center? The flash is enough.
Order: draw board skipping clearing rows' normal cells (they're replaced by shrink effect).
**HTML/CSS layout & style:**
```css
:root {
--bg: #0B0C0F; --panel: #131519; --line: #262A31; --ink: #E7EAF0; --muted: #8A919E; --accent: #E8A33D;
}
body { background: var(--bg); color: var(--ink); font-family: mono stack; display:grid; place-items:center; min-height:100vh; }
```
Header: title "BLOCKFALL" letterspacing .3em, small; subtitle muted tiny. Maybe a thin rule. Keep the header inline with the game? I'll put title above the board area, left-aligned with board width.
Game container: `display:flex; gap:14px; align-items: stretch;`
Board wrap: `position:relative; border:1px solid var(--line); background:#0E1013; padding: 10px;` Actually canvas includes bg; padding around canvas inside bordered frame gives nice matting. Canvas display:block.
Panel: width ~150px; display flex column; sections separated by 1px borders. Each stat: label (10px, uppercase, letterspaced, muted) + value (22px, accent for score, ink for others). Next box: canvas 96×96 with border.
Controls legend at panel bottom: rows of kbd + label. kbd style: 1px border, 3px radius, 10px font, padding 1px 5px, background #1A1D22.
Overlay: absolute inset-0 over board-wrap padding area; center content; backdrop rgba(10,11,13,0.82); hidden by default with opacity 0 + pointer-events none; .show → visible; transition 200ms. Content: h2 (GAME OVER, letterspaced), stats line, hint with blinking? A gentle blink on "PRESS R TO RESTART" via CSS animation opacity 0.4↔1 — tasteful for a game. OK.
Also small footer line under board with controls summary? The panel covers it. Maybe put a single hint line under the whole game: "P pause · R restart". The legend in panel covers all keys; skip footer to reduce clutter. Actually I'll put the controls legend below the board as one compact row instead of in the panel — panel keeps NEXT + stats. Hmm: panel right side: NEXT, SCORE, LINES, LEVEL, and a small "speed" indicator? Level implies speed. Let me put legend in panel bottom — keeps everything in one column, tidy. Or below board horizontally aligned to board width. I'll put it under the board as a single line of kbd items; it reads naturally like a game's control bar.
Layout final:
```
BLOCKFALL
┌───────────────┬──────────┐
│ │ NEXT │
│ │ [ ] │
│ │ SCORE │
│ board │ 0 │
│ │ LINES │
│ │ 0 │
│ │ LEVEL │
│ │ 1 │
└───────────────┴──────────┘
[←→] move [↑] rotate [↓] soft [space] drop [p] pause [r] restart
```
The keyboard legend below, spanning board+panel width.
Title placement: above, left-aligned to the board frame. Add a tiny amber square glyph before title? A small 8×8 accent block inline as brand mark — one accent element, purposeful. Sure: a small square of accent color (like a tetromino cell) before the title. That's an honest brand mark, not random decoration.
Panel width: match visual weight, ~140px. Board canvas 300×600 logical, displayed at min(66vh) height. On typical 900px-tall screens: 600px board + header ~50 + legend ~30 fits.
Responsive: if viewport narrow, stack panel below? `flex-wrap: wrap`. Add media query: under 520px, panel becomes horizontal row under board. Keep simple: `@media (max-width: 560px)` → column layout, panel row of stats. I'll add a modest media query.
**Score display formatting:** pad score to 6 digits? `String(score).padStart(6,'0')` looks game-like. I'll display raw number with padded zeros — classic arcade. Yes, padStart(6,'0').
**Level speed display:** show interval? No, level only.
**Write the code now.** Let me carefully draft the JS.
```js
const COLS=10, ROWS=20, CELL=30, DAS=170, ARR=45, SOFT=45, CLEAR_MS=320;
const SHAPES = {
I:{m:[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], c:'#5FB7C4'},
O:{m:[[1,1],[1,1]], c:'#E3C05A'},
T:{m:[[0,1,0],[1,1,1],[0,0,0]], c:'#B48CCB'},
S:{m:[[0,1,1],[1,1,0],[0,0,0]], c:'#79BE7E'},
Z:{m:[[1,1,0],[0,1,1],[0,0,0]], c:'#D97B7B'},
J:{m:[[1,0,0],[1,1,1],[0,0,0]], c:'#7A96D9'},
L:{m:[[0,0,1],[1,1,1],[0,0,0]], c:'#DE9A55'},
};
```
Colors — check contrast on dark: all mid-lightness, fine. T lavender #B48CCB is purple-ish but as one of seven piece colors it's semantic, not a theme choice — fine, standard tetromino purple. Good.
`collides(m, x, y)`:
```js
for r,c where m[r][c]: bx=x+c, by=y+r;
if (bx<0||bx>=COLS||by<0||by>=ROWS) return true; // forbids y<0 → stays in field
if (board[by][bx]) return true;
```
Wait — forbidding by<0 means the piece can never be above row 0; spawn at y=0 always inside (matrix may have empty top row; blocks start row 0 or 1). Rotation dy=-1 kicks near top will fail — acceptable and correct per "no clipping".
But hold on: I matrix at spawn: blocks at matrix row 1 → board row 1. If stack fills row 0..? spawn collision check covers. OK.
`rotate(m, dir)`:
```js
const n=m.length; const res=Array.from({length:n},()=>Array(n).fill(0));
// note matrices may be non-square? O is 2x2, others 3x3 or 4x4 — all square. ✓
for r,c: dir>0 ? res[c][n-1-r]=m[r][c] : res[n-1-c][r]=m[r][c];
```
`tryRotate(dir)`:
```js
const rm = rotate(current.matrix, dir);
const kicks=[[0,0],[-1,0],[1,0],[0,-1],[-1,-1],[1,-1],[-2,0],[2,0]];
for (k of kicks) if (!collides(rm, current.x+k[0], current.y+k[1])) { current.matrix=rm; current.x+=k[0]; current.y+=k[1]; return; }
```
`move(dx)`: if current && !collides(m, x+dx, y) → x+=dx; return true.
`stepDown(soft)`:
```js
if (collides(m,x,y+1)) { lock(); } else { y++; if (soft) addScore(1); }
```
`hardDrop()`:
```js
let d=0; while(!collides(m,x,y+1)){y++;d++;} addScore(d*2); shake=Math.min(6, 2+d*0.15); lock();
```
Hmm shake on every hard drop might get tiresome; make it small: shake = d>4 ? 3 : 1.5? I'll set shake = Math.min(5, 1 + d*0.2) with quick decay — subtle.
`lock()`:
```js
const cells=[]; for cells: by=y+r, bx=x+c → board[by][bx]=type;
const rows=[]; for r 0..19: if board[r].every(v=>v) rows.push(r);
if (rows.length) {
rows.forEach(r => { for c: spawnParticle at cell center with color of board[r][c]; });
clearing={rows, t:0};
addScore(SCORES[rows.length]*level);
lines += rows.length; updateLevel(); linesEl bump;
shake = 2 + rows.length*1.5;
} else spawn();
```
Note: capture colors before removal — particles read board[r][c] at spawn time (before removal happens later) ✓.
`update clearing`: t+=dt; when t>=CLEAR_MS:
```js
clearing.rows.sort asc; for r of rows: board.splice(r,1); board.unshift(Array(COLS).fill(null));
```
Careful: splicing by original row indices — if I remove row 15 and row 16, after splice(15) the old row 16 becomes 15... Standard fix: iterate rows in ascending order and splice each — after removing row r1 and unshifting, indices shift. Better: filter approach: `board = board.filter((row,i)=>!rows.includes(i)); while(board.lengthr.slice())`.
Level update: `level = Math.floor(lines/10)+1; interval = Math.max(70, Math.round(850*Math.pow(0.8, level-1)));`
**Input handlers:**
```js
const keys = new Map(); // e.code -> true for arrows
let moveHeld = null; // {dir, t, next}
document.addEventListener('keydown', e => {
if (['ArrowLeft','ArrowRight','ArrowDown','ArrowUp','Space'].includes(e.code)) e.preventDefault();
if (e.repeat) return;
switch(e.code){
case 'ArrowLeft': case 'ArrowRight': {
const dir = e.code==='ArrowLeft' ? -1 : 1;
keys.set(e.code, true);
if (playing()) { if (move(dir)) { } moveHeld = {dir, t:0, next:DAS}; }
else moveHeld = {dir, t:0, next:DAS}; // still track for when resumed? simpler: only if playing
break;
}
case 'ArrowDown': downHeld=true; if (playing()) stepDown(true); break;
case 'ArrowUp': if (playing()) tryRotate(1); break;
case 'KeyZ': if (playing()) tryRotate(-1); break;
case 'Space': if (playing()) hardDrop(); break;
case 'KeyP': togglePause(); break;
case 'KeyR': restart(); break;
}
});
keyup: ArrowLeft/Right → keys.delete; if moveHeld && moveHeld.dir corresponds to released key: if other arrow still held → moveHeld={otherDir,...} else moveHeld=null. ArrowDown → downHeld=false.
```
`playing()` = current && !gameOver && !paused && !clearing? Moves during clearing: current is null then, so guard `current` suffices. playing() = !gameOver && !paused && current.
Note: downHeld soft-drop via gravity accumulation: in update:
```js
if (playing()) {
const interval = downHeld ? Math.min(SOFT, intervalForLevel) : intervalForLevel;
gravAcc += dt;
while (gravAcc >= interval) {
gravAcc -= interval;
if (collides(cur.m, x, y+1)) { lock(); break; }
else { cur.y++; if (downHeld) addScore(1); }
}
}
```
Careful: lock() sets current=null; break out. Also after lock, if no clearing, spawn() sets new current — the while loop might then step the new piece; break after lock regardless. ✓
DAS in update (only if playing && moveHeld):
```js
moveHeld.t += dt;
while (moveHeld.t >= moveHeld.next) { move(moveHeld.dir); moveHeld.next += ARR; }
```
Cap dt at 100 prevents long loops. ✓
Hmm — when a piece locks and new spawns mid-DAS, held movement continues applying to new piece immediately. That's standard behavior. Fine.
Keydown left/right when not playing: I set keys.set anyway so DAS resumes if unpaused? moveHeld processed only when playing, and t accumulates only in update when playing. On keydown I move once only if playing. OK.
**Pause toggle:** paused=!paused; overlay show/hide with content. If gameOver, P does nothing.
**restart():** reset all state, hide overlay, spawn. Works anytime (also mid-game — convenient; requirement only demands on game over). I'll allow R anytime.
**Overlay content function:**
```js
function showOverlay(kind) { build innerHTML: for gameover: h2 GAME OVER, p final score & lines, p.hint PRESS R TO RESTART; pause: h2 PAUSED, p.hint PRESS P TO RESUME. el.classList.add('show'); }
```
Game over: maybe also draw the stack dimmed — overlay bg handles dimming (rgba backdrop over canvas). But overlay is over the padded wrap; make it cover the whole board-wrap. ✓
**Particles:**
```js
function spawnParticles(x,y,color){ for i<6: particles.push({x: px+CELL/2 + rand, y, vx:(Math.random()-0.5)*180, vy:-Math.random()*140-20, g:420, size:2+Math.random()*3.5, rot:Math.random()*Math.PI, vr:(Math.random()-0.5)*10, life:1, decay:1.4+Math.random()*0.8, color}); }
update: vy+=g*dt/1000... work in seconds: dt in ms → s = dt/1000. life -= decay*s.
draw: alpha=life, translate, rotate, fillRect(-s/2,-s*0.25, s, s*0.5) → elongated shard, not a square.
```
Cap particles length (e.g., 300).
**Render:**
```js
function render(){
ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect / fill bg #0E1013 over whole;
// shake
let ox=0, oy=0;
if (shake>0.2){ ox=(Math.random()-0.5)*shake; oy=(Math.random()-0.5)*shake; }
ctx.translate(ox,oy);
drawGrid();
// locked cells
for r,c: if board[r][c] and not in clearing.rows: drawCell(c*CELL, r*CELL, color);
// clearing rows effect
if (clearing){ p=clearing.t/CLEAR_MS; for r of rows: for c: if board: draw shrinking; flash overlay row; }
// ghost + current
if (current){ gy = ghostY(); draw ghost cells (stroke); draw current cells; }
// particles
...
}
```
Ghost draw:
```js
ctx.globalAlpha = 0.28; for cells: strokeStyle = color; lineWidth 1.5; strokeRect(px+2.5, py+2.5, CELL-5, CELL-5); maybe also faint fill 0.07.
```
Hmm two passes: fill alpha 0.08 then stroke alpha 0.35.
Also draw a subtle top "danger" indicator? Skip.
**Preview rendering:** clear, compute bounds of next matrix cells (min/max row/col), center within 96px:
```js
const b = bounds(next.m); const w=(b.maxC-b.minC+1), h=(b.maxR-b.minR+1);
const offX = (PSIZE - w*PCELL)/2 - b.minC*PCELL; similarly Y.
drawCell with PCELL=22.
```
PSIZE canvas logical 100? Let me set preview canvas 4*22+padding: use 110×110 CSS-ish logical with PCELL 22 and padding. I'll compute: logical size 108; PCELL 22; offset accordingly. Set canvas width=108*dpr etc.
Also draw next-piece label in DOM.
**Score bump CSS:**
```css
.value.bump { animation: bump .25s ease; }
@keyframes bump { 0%{transform:scale(1)} 30%{transform:scale(1.18); color:#fff} 100%{transform:scale(1)} }
```
transform on inline element — need display:block or inline-block. Values are block divs. ✓
**Fonts & spacing:** labels 10-11px letter-spacing .18em uppercase muted; values 22px; score uses accent color? Make score value accent, others ink. Level value maybe accent too when it changes... keep: score accent.
**Legend kbd:**
```html
←→ move↑ rotate↓ soft dropspace hard dropP pauseR restart
```
Also mention Z for counter-rotate? Add to legend: `Z ccw`. Sure, small.
**Title area:**
```html
BLOCKFALL
falling-blocks puzzle
```
mark: 10×10 background accent. Title 15px letterspacing .35em. sub muted 11px. Header aligned with board width — put header inside a container that spans board+panel; align-self flex-start.
Layout container: `.game { display:flex; gap:12px; }` and wrap everything in `.shell { }` centered.
Board sizing responsive: canvas `style="height: min(68vh, 600px); aspect-ratio: 1/2; width:auto;"` — but canvas element with width/height attrs; CSS height + aspect-ratio works. I'll set CSS: `canvas#board { display:block; height: min(68vh, 620px); aspect-ratio: 1/2; image-rendering: auto; }` Hmm, width:auto with aspect-ratio should derive width. To be safe set `width:auto; height:min(68vh,620px);` — actually for replaced elements with intrinsic size, CSS height + width:auto uses intrinsic aspect... canvas has intrinsic 300×600 so aspect preserved with height:auto width auto? If I set only height, width follows intrinsic ratio? For canvas, if CSS height set and width auto, width = height × (intrinsic width/height)? Yes, replaced elements preserve intrinsic ratio when one dimension is auto. Safe: also add aspect-ratio. Fine.
Board frame: `.board-frame { position:relative; padding:10px; border:1px solid var(--line); background:#0D0F12; border-radius:4px; }` canvas inside. Overlay absolute covering frame.
Panel: `width:148px; border:1px solid var(--line); background:var(--panel); border-radius:4px; display:flex; flex-direction:column;` sections with padding 12px 14px and border-bottom. Next section: canvas border 1px dashed? Solid thin.
Next canvas CSS width ~104px centered.
Panel sections:
```
NEXT (canvas)
SCORE 000000
LINES 0 LEVEL 1 → two stats in one row? side-by-side two columns to reduce height. I'll make lines & level share a row via flex.
```
Panel height should visually match board — stats stacked with flexible spacing; use `justify-content: space-between`? If board is 600px tall and panel sections total less, gaps appear — okay with border-bottom only on sections; empty space at bottom is fine, or I add a small "speed" bar showing drop speed as a meter? Could add a minimal speed indicator: label "SPEED" + a row of level-based ticks... might be filler. Instead: let panel sections have `flex:1` distribution? Simplest: natural heights, and add `gap` fill by making the stats section grow. It's fine aesthetically to have the panel end naturally; or stretch panel to board height with sections spaced. I'll set panel `align-self: stretch` (matches board-frame height) and use flex column with sections; add `margin-top:auto` to nothing... Actually give each stat section `flex:1` so they distribute evenly across the height — NEXT gets natural size, stats grow. That fills elegantly. Let me do: `.panel section { border-bottom:1px solid var(--line); }` and `.panel { display:flex; flex-direction:column; } .panel .grow{flex:1}` — put flex:1 on the three stat sections so they distribute; each with padding and content vertically centered top. Hmm, with flex:1 and small content there'd be lots of internal whitespace below text — acceptable, gives breathing room; or center content vertically? I'd rather top-align text with generous padding and let the borders divide the space — looks like a designed stat column. Do: `.stat { flex:1; display:flex; flex-direction:column; justify-content:center; gap:6px; padding:0 16px; }` centered vertically within its share. Clean.
Media query small screens: `.game { flex-direction: column; } .panel { flex-direction: row; width:100%; }` etc. Keep it brief.
**Auto-pause on blur:** `document.addEventListener('visibilitychange', () => { if (document.hidden && playing-state && !gameOver && !paused) togglePause(); })`. Sure.
**Game over check nuance:** Also if piece locks partially above visible area — impossible here (y≥0 enforced). Stack reaching top → spawn collision → game over ✓.
**One more detail:** after clear animation, spawn happens; gravAcc should reset to avoid immediate multi-step. Reset gravAcc on spawn.
**Score add:** `addScore(n){ score+=n; scoreEl.textContent=pad(score); bump(scoreEl); }` For soft drop +1 spamming bump every cell — the bump animation restarts rapidly, fine but maybe only bump for line clears / hard drops? Bumping every soft-drop cell is jittery. I'll bump only when n>=10 or on line clear. Implement addScore(n, silent) or check n>=50. Simple: bump only for clears.
Let me now also make sure `update` runs even when overlay shown (paused/gameOver) so particles decay and rendering continues — yes, but skip gravity/DAS. On gameOver, current is still set (piece that failed to spawn?) — in spawn failure, current was assigned but collides; I should set current=null on game over so the field is static and no ghost drawn. Set current=null after detecting collision.
**Restart:** also reset moveHeld, downHeld, gravAcc, clearing, particles, shake. Hide overlay.
**Pad score:** `String(score).padStart(6,'0')`.
**Interval recompute on level change** ✓.
**Colors constant check** — define CELL drawing helper taking ctx so preview reuses.
**Write final HTML.** I'll be careful with the rotation of O piece (2×2 matrix rotates to itself — rotate() handles n=2 fine). I matrix 4×4 ✓.
Let me write out the full code:
```html
Blockfall
```
I'll write it fully in the answer. Let me think about a couple of implementation subtleties first:
1. **Canvas dpr setup:**
```js
const dpr = Math.min(2, window.devicePixelRatio || 1);
function setupCanvas(cv, w, h){ cv.width=w*dpr; cv.height=h*dpr; const c=cv.getContext('2d'); c.setTransform(dpr,0,0,dpr,0,0); return c; }
```
CSS sizes: `#board{height:min(68vh,620px)}` and `#next{width:104px;height:104px}`.
2. **render clearing rows:** need `clearing.rows.includes(r)` — small arrays, fine. Use a Set for clarity.
3. **Ghost y:**
```js
function ghostY(){ let gy=cur.y; while(!collides(cur.m,cur.x,gy+1)) gy++; return gy; }
```
4. **Drawing current piece when overlapping ghost** — ghost is behind/current on top; if piece at bottom overlapping ghost cells, drawing ghost then current over it: current covers, fine.
5. **Lock writes type string into board; render color lookup `SHAPES[type].c`.**
6. **Level/speed:** `intervalFor()` computed on level change, stored in `interval`.
7. **Edge: hard drop when d===0 (already resting)** — lock immediately, shake minimal. Fine.
8. **The clearing flash draws over board area rows** — row rect width COLS*CELL.
9. **Overlay z-index over canvas within frame.**
10. **Prevent arrow scroll also on keyup? Not needed.**
11. **`e.repeat` guard for ArrowDown immediate step:** with repeat guarded, holding down gives one immediate step then SOFT-interval steps via gravity. Good.
12. **Space preventDefault** to stop page scroll ✓. Also prevent default on ArrowUp etc.
13. On game over, show final score in overlay.
One more consideration — **the while-loop in gravity when downHeld:** interval may be small (45ms) but gravity accumulates dt; multiple steps per frame possible at low FPS; fine.
**DAS while clearing:** moveHeld continues accumulating; when current spawns, held movement applies — standard. But guard move() with current existence.
**Also on paused, DAS timers frozen since update skips.** ✓
Now, about "no clipping" in rotation — my collision check handles it; kicks only ever move to valid positions. ✓
Let me also double check `rotate` for CCW: `res[n-1-c][r] = m[r][c]`. Check with T matrix [[0,1,0],[1,1,1],[0,0,0]] CW: expected [[0,0,1],[1,1,1],[0,0,0]]? Let's compute: m[0][1]=1 → res[1][2]=1; m[1][0]=1→res[0][1]=1; m[1][1]=1→res[1][1]=1; m[1][2]=1→res[2][1]=1. So res = [[0,1,0],[0,1,1],[0,1,0]] — that's T pointing down. Correct CW rotation of T (spawn pointing up, CW → pointing right? Standard SRS: spawn T has flat bottom pointing... whatever, it's a valid rotation). ✓ CCW check: res[n-1-c][r]: m[0][1]→res[1][0]=1; m[1][0]→res[2][1]; m[1][1]→res[1][1]; m[1][2]→res[0][1]. res=[[0,1,0],[1,1,0],[0,1,0]] — T pointing up... spawn was up-pointing? m=[[0,1,0],[1,1,1],[0,0,0]] — nub on top, flat bottom → CW gives nub right? I computed nub at res[1][2] → right. CW rotation of an up-pointing T should point right ✓.
**Panel HTML:**
```html
```
Canvas next: set attributes via JS with dpr; CSS size 96×96.
Legend:
```html
←→ move↑ rotate↓ softSpace hard dropZ ccwP pauseR restart
```
Styled: font-size 10.5px muted, gap 14px, flex-wrap, justify space-between? Just gap wrap.
Overlay:
```html
```
`.overlay{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; background:rgba(9,10,13,.82); opacity:0; pointer-events:none; transition:opacity .25s; text-align:center;} .overlay.show{opacity:1}`
h2 letterspaced; hint blinking: `animation: blink 1.2s steps(2) infinite` — or opacity pulse keyframes.
Now write update/render fully and double-check logic order.
```js
let board, cur, nextT, bag, score, lines, level, interval, gravAcc, clearing, particles, shake, gameOver, paused, downHeld, moveHeld;
```
init():
```js
board = Array.from({length:ROWS},()=>Array(COLS).fill(null));
bag=[]; score=0; lines=0; level=1; interval=speedFor(1);
gravAcc=0; clearing=null; particles=[]; shake=0; gameOver=false; paused=false;
downHeld=false; moveHeld=null;
nextT = randType();
spawn();
updateHUD(); hideOverlay();
```
randType: bag refill when empty: shuffle keys.
spawn():
```js
const type = nextT; nextT = randType();
const m = SHAPES[type].m.map(r=>r.slice());
cur = {type, m, x: Math.floor((COLS - m.length)/2), y:0};
gravAcc = 0;
drawNext();
if (collides(cur.m, cur.x, cur.y)) { cur = null; gameOver = true; showOverlay('over'); }
```
collides(m, x, y):
```js
for(let r=0;r=COLS||by<0||by>=ROWS) return true;
if(board[by][bx]) return true;
}
return false;
```
stepDown(soft):
```js
if(!cur) return;
if(collides(cur.m,cur.x,cur.y+1)){ lock(); } else { cur.y++; if(soft) addScore(1); }
```
lock():
```js
const rowsFull=[];
for cells: board[y+r][x+c]=cur.type;
cur=null;
for(r=0..ROWS-1) if(board[r].every(Boolean)) rowsFull.push(r);
if(rowsFull.length){
rowsFull.forEach(r=>{ for(c) spawnParticle(c*CELL+CELL/2, r*CELL+CELL/2, SHAPES[board[r][c]].c); });
clearing={rows:new Set(rowsFull), list:rowsFull, t:0};
addScore(SCORES[rowsFull.length]*level, true);
lines+=rowsFull.length; level=Math.floor(lines/10)+1; interval=speedFor(level);
shake = 2.5 + rowsFull.length*1.5;
updateHUD(); bumpEl(linesEl)...
} else spawn();
```
update(dt):
```js
// particles always
updateParticles(dt);
if(shake>0.05) shake*=Math.exp(-8*dt/1000); else shake=0;
if(gameOver||paused) return;
if(clearing){
clearing.t+=dt;
if(clearing.t>=CLEAR_MS){
board = board.filter((_,i)=>!clearing.rows.has(i));
while(board.length=moveHeld.next){ move(moveHeld.dir); moveHeld.next+=ARR; } }
// gravity
const iv = downHeld? Math.min(SOFT_MS, interval) : interval;
gravAcc+=dt;
while(gravAcc>=iv){
gravAcc-=iv;
if(collides(cur.m,cur.x,cur.y+1)){ lock(); break; }
cur.y++;
if(downHeld) addScore(1);
}
```
Careful: after lock() inside while, cur may be a NEW piece if spawn happened (lock with no clear calls spawn). `break` prevents stepping the new piece. ✓ But also if lock→clearing, cur=null and clearing set; break. ✓
Hmm wait: `if(!cur) return;` earlier — after clearing finishes, spawn creates cur. OK.
Also DAS while loop: move() checks cur exists internally? move:
```js
function move(dx){ if(!cur||collides(cur.m,cur.x+dx,cur.y)) return false; cur.x+=dx; return true; }
```
✓.
Edge: moveHeld continues while cur null during clearing — the while loop calls move which no-ops but burns repeats; fine.
**keyhandling pause toggle:**
```js
function togglePause(){ if(gameOver) return; paused=!paused; paused? showOverlay('pause'): hideOverlay(); }
```
**showOverlay(kind):**
```js
if(kind==='over'){ ovTitle.textContent='GAME OVER'; ovInfo.textContent=`Score ${score} · ${lines} lines · level ${level}`; ovHint.textContent='PRESS R TO RESTART'; }
else { ovTitle.textContent='PAUSED'; ovInfo.textContent=''; ovHint.textContent='PRESS P TO RESUME'; }
overlay.classList.add('show');
```
**restart():** init().
**HUD:** score padded, lines, level; bump on clears (score & lines), level bump too.
**Rendering the board frame mat:** canvas bg fill `#0e1013`; grid lines.
**drawCell(ctx,px,py,s,color,alpha):** as sketched. For preview, reuse with its own ctx.
**Next canvas:** logical 96; PCELL 20; center via bounds. For I piece: w=4,h=1 → offsets fine.
```js
function drawNext(){
nctx.clearRect(0,0,96,96);
// bg transparent (panel behind)
if(!nextT) return;
const m=SHAPES[nextT].m, col=SHAPES[nextT].c;
let minR=9,maxR=-1,minC=9,maxC=-1;
scan cells;
const w=(maxC-minC+1)*20, h=(maxR-minR+1)*20;
const ox=(96-w)/2 - minC*20, oy=(96-h)/2 - minR*20;
for cells: drawCell(nctx, ox+c*20, oy+r*20, 20, col);
}
```
Canvas transparent so panel bg shows — drawCell fills base etc. fine.
**Particles:**
```js
function spawnParticle(x,y,color){
if(particles.length>240) return;
particles.push({x,y, vx:(Math.random()-.5)*220, vy:-Math.random()*160-30, size:2+Math.random()*4, rot:Math.random()*Math.PI, vr:(Math.random()-.5)*12, life:1, decay:1.2+Math.random()*.9, color});
}
function updateParticles(dt){
const s=dt/1000;
for(p of particles){ p.vy+=480*s; p.x+=p.vx*s; p.y+=p.vy*s; p.rot+=p.vr*s; p.life-=p.decay*s; }
particles=particles.filter(p=>p.life>0);
}
draw: ctx.save/translate/rotate/globalAlpha=p.life; fillStyle=color; fillRect(-size/2,-size*0.22,size,size*0.44); restore per particle (or setTransform each).
```
Also clip particles to board? They can fly over the frame padding — canvas is only the board area so they're clipped by canvas edges naturally. Fine.
**Render:**
```js
function render(){
bctx.setTransform(dpr,0,0,dpr,0,0);
bctx.fillStyle='#0e1013';
bctx.fillRect(0,0,COLS*CELL,ROWS*CELL);
// shake
if(shake>0.1) bctx.translate((Math.random()-.5)*shake,(Math.random()-.5)*shake);
// grid
bctx.strokeStyle='rgba(255,255,255,0.045)'; bctx.lineWidth=1;
bctx.beginPath();
for(c=1..9){ x=c*CELL+0.5? just c*CELL; moveTo(x,0);lineTo(x,ROWS*CELL);}
for(r=1..19){...}
bctx.stroke();
// locked
for(r,c) if(board[r][c] && !(clearing&&clearing.rows.has(r))) drawCell(bctx,c*CELL,r*CELL,CELL,SHAPES[board[r][c]].c);
// clearing effect
if(clearing){
const p=Math.min(1,clearing.t/CLEAR_MS);
for(r of clearing.list) for(c=0..9){ const col=SHAPES[board[r][c]].c; const pad=p*CELL*0.5; bctx.globalAlpha=1-p*0.9; bctx.fillStyle=col; bctx.fillRect(c*CELL+pad, r*CELL+pad, CELL-2*pad, CELL-2*pad); }
bctx.globalAlpha=p*0.85; bctx.fillStyle='#fff'; for(r of list) bctx.fillRect(0,r*CELL,COLS*CELL,CELL);
bctx.globalAlpha=1;
}
// ghost & current
if(cur){
const gy=ghostY();
// ghost
bctx.globalAlpha=0.10; bctx.fillStyle=color;
cells fill rects;
bctx.globalAlpha=0.4; bctx.strokeStyle=color; lineWidth 1.5;
cells strokeRect(px+2.5,py+2.5,CELL-5,CELL-5);
bctx.globalAlpha=1;
// current
for cells: drawCell(bctx,(x+c)*CELL,(y+r)*CELL,CELL,color);
}
// particles
for(p) draw shard;
}
```
Note ghost drawn only if gy>cur.y (else skip to avoid outline under piece). If gy===cur.y, ghost cells under current cells — current drawn after covers them mostly but the stroke inset might peek. Guard: if gy>cur.y draw ghost. ✓
**Loop start:** requestAnimationFrame(frame) with last timestamp init: `let last=performance.now()` then frame(now){dt=min(50, now-last)...}. Cap dt 50 to avoid tunneling weirdness (DAS loops bounded).
Also, on window blur auto-pause: visibilitychange.
**Legend styling** and **panel `grow` sections** — set `.panel{align-self:stretch}` since `.game{align-items:stretch}` default stretch: board-frame height defines row height; panel stretches. ✓ But board-frame height depends on canvas height (min(68vh,620px)) + padding — ok.
For smaller heights (e.g., laptop 700px viewport): 68vh ≈ 476px tall board — fine.
**Media query:** `@media (max-width:520px){ .game{flex-direction:column} .panel{flex-direction:row; width:100%} .panel .grow{flex:1} .next-sec{display:flex;align-items:center;gap:10px} }` — keep basic.
Double-check the O piece centering: m.length=2 → x=(10-2)/2=4 ✓. I: m.length=4 → x=3 ✓.
Score values: SCORES = {1:100,2:300,3:500,4:800}. multiply level.
One more check — **`interval` when level jumps multiple (e.g., from 9 lines +4 lines → level 2)** fine.
**Potential bug:** In keydown, when paused and user presses arrows — moveHeld set but update skips; on resume it applies. OK.
**Keyup for down:** downHeld=false. If user holds down across lock, soft drop applies to next piece — standard.
**HUD pad:** score padded to 6; if exceeds 999999 just show more digits (padStart doesn't truncate). ✓
I think the design is solid. Also add a subtle scanline/vignette? No — keep it clean.
Let me also consider the "signature moment": the line-clear flash with shards + slight shake, plus the crisp amber-on-charcoal HUD. Good enough, tasteful.
Time to write the final file. I'll write clean, commented code. Comments in English (matching user language).
Let me write the complete HTML now, carefully.
Also I need to double check drawCell for the preview at size 20: inner strips 3px look proportionally heavy; scale strip = Math.max(2, s*0.12)? Use `const b=Math.max(2, Math.round(s*0.12))` for bevel thickness. s=30 → 4? Let me use b = s>=24?3:2. Simpler: b = Math.max(2, Math.floor(s*0.11)) → 30→3, 20→2. Good.
Font sizes for values: score 20px, lines/level 20px.
Write everything:
Also careful: `bctx.translate` after setTransform each frame — reset at start with setTransform(dpr,...) ✓.
Also the overlay must sit above canvas but within frame padding area — `.board-frame{position:relative}` overlay `inset:0` covers padding too. ✓
Edge: overlay visible on pause but board visible dimmed — good.
The legend keys use arrows characters ← etc. — fine as text.
Final check on requirement "Rotation must respect walls and stacked blocks (no clipping through anything)" — collision-checked rotation with kicks ✓.
Also requirement "down arrow soft-drops" ✓ (+1 per cell, faster).
Now write it. I'll keep the code well-organized with section comments.
Estimate: ~450 lines. Good.
One more subtlety — `update` returning early on gameOver/paused means DAS/downHeld stale states persist but they're only acted on when unpaused; keyup still clears them. ✓
For the shake decay I wrote `shake*=Math.exp(-8*dt/1000)` — at dt=16ms, factor e^-0.128 ≈ 0.88/frame — decays over ~300ms. Good.
Particles vy initial upward −30..−190, gravity 480 px/s² — they arc and fall, exiting board bottom or fading by life. Life ~0.6–1.1s. Good.
Writing final answer now. After the code, a brief explanation.
=== answer so far ===
Here's a complete falling-blocks puzzle game with wall-kick rotation, 7-bag piece randomization, ghost piece preview, and a line-clear flash effect with particle shards:
```html
Blockfall
Blockfall
a falling-blocks puzzle
←→ move↑ rotate↓ soft dropSpace hard dropZ ccwP pauseR restart