### PARTIAL OUTPUT (time limit hit; the model was still generating) ### === thinking so far === The user wants a complete falling-blocks puzzle game in a single self-contained HTML file. No external libraries, no internet resources, no images — everything inline. Let me plan this carefully and make it genuinely good, not just functional. **Core requirements checklist:** - 10×20 playfield on canvas ✓ - 7 tetromino shapes, 4 squares each, distinct colors ✓ - Controls: ←/→ move, ↑ rotate, ↓ soft drop, space hard drop ✓ - Rotation with wall-kick / collision respect (no clipping) ✓ - Line clears, rows fall, multi-line bonus scoring ✓ - Score, lines, level display; speed increases with level ✓ - Next-piece preview ✓ - Game over at top, visible game-over state, restart key (R) ✓ - Clean, readable dark visual style ✓ **Design decisions (my taste):** I want this to feel like a crafted, authored game, not a tutorial copy. Some choices: 1. **Visual identity**: A deep, near-black charcoal background with a restrained palette. I'll avoid the garish default tetris rainbow — instead I'll pick a curated set of slightly desaturated but distinct hues that harmonize on dark: a warm coral/vermilion for I... actually, classic assignments are expected (I=cyan, O=yellow, etc.) but I can tune them. Let me pick a refined palette: - I: teal/cyan `#4EC9B0`-ish — let me use a coherent set: - I: `#56B6C1` (cyan-teal) - O: `#E5C07B` (amber) - T: `#C678DD` (soft violet) - S: `#98C379` (green) - Z: `#E06C75` (rose) - J: `#61AFEF` (blue) - L: `#D19A66` (orange) This is the One Dark palette essentially — harmonious on dark backgrounds, distinct enough. Good. 2. **Block rendering**: Not flat squares — I'll give each cell a subtle bevel: a lighter top-left inner edge, a darker bottom-right, and a small inner highlight. Crisp and readable without gradients as decoration (beveling blocks is functional game rendering, a craft detail, not a gradient banner). I'll keep it subtle. 3. **Typography**: Use a monospace stack for the HUD — gives it a terminal/arcade feel and it's not Inter/Roboto/system-ui as identity. Something like `"Courier New"`? That's a bit weak. I can use `ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace` — monospace fonts are fine and fitting for an arcade game. Letter-spacing on labels, uppercase micro-labels. 4. **Layout**: Centered column: title wordmark ("TETRA" or something — I'll call it "QUADRA" or "TETRA·10"? Let me name it **"TETRA"** — simple. Actually something more authored: **"GRAVITron"**? Hmm. I'll go with **TETRA** with a small tagline). HUD as a stat row (score / lines / level) with tabular numbers. Canvas with a thin border and slight inner glow of a neutral kind... no glow — a clean 1px border with the playfield having a subtle grid. Next preview to the side or above. I'll do a side panel next to the canvas (flex row) containing NEXT box, stats, controls legend. On narrow screens it stacks. 5. **Grid**: Very subtle grid lines (rgba white 0.04) plus faint column guides. Empty cells slightly darker than page background or same. 6. **Ghost piece**: A nice extra — showing where the piece will land (outline). This is standard quality-of-life and shows craft. I'll render the ghost as low-alpha fill or outline. Yes, include it. 7. **Effects**: - Line clear flash: rows flash white then collapse — I'll implement a brief clear animation (rows highlighted, then removed after ~180ms). During the animation, the game pauses spawning briefly. Simpler: do the flash then clear. I'll implement a small state machine: `state = 'playing' | 'clearing' | 'over'`. - Hard drop: tiny screen shake or a quick particle? Particles on line clear — small squares... I dislike plain small squares as particles. But this is canvas pixel art context; I could do shrinking/rotating rects with the piece colors — for line clears, spawn little shards that fly. Hmm, requirement says no images but canvas-drawn particles are fine. To keep it tasteful, I'll do a quick white flash on cleared rows plus a subtle score "pop" in HUD. Maybe skip particles to keep it clean — a crisp flash + falling is enough. Actually a small touch: cleared rows flash and the blocks above ease down? Easing gravity for the collapse is complex; instant is classic. I'll do flash → clear. Keep it snappy. - Level-up: show a brief "LEVEL 2" toast overlay on the canvas? A small inline message area. I'll add a transient canvas overlay text for level-up. Nice touch. 8. **Overlays**: Start state? The requirement doesn't demand a start screen, but a paused/ready state with "Press any key" is good UX. I'll start the game immediately on load but maybe show "PRESS ANY KEY TO START"? Hmm — restarting via R. Let me include three states: `ready` (press any key), `playing`, `clearing`, `over`. Ready screen with title and control legend drawn on canvas overlay (DOM overlay div is easier to style — I'll use a DOM overlay positioned over the canvas). Overlay for game over: "GAME OVER — score — press R to restart". Semi-transparent dark backdrop over canvas, blurred? backdrop-filter blur is fine. 9. **Mechanics details**: - Board: 10 × 20, but I need hidden rows for spawn/rotation. I'll make the board array 20 visible rows + 2 hidden rows above (index 0,1 hidden). Actually simpler: board of 22 rows, render rows 2–21. Spawn at row 0 (hidden). Game over when a new piece collides immediately at spawn, OR when blocks exist in hidden rows after lock... Standard: game over if the newly spawned piece can't be placed. Also "stack reaches the top" — if a locked piece occupies any hidden row region... I'll say: if locked blocks are in row < 2 (hidden area), game over. Both conditions. - Rotation: I'll implement rotation with a simple wall-kick: try offsets [(0,0), (-1,0), (1,0), (0,-1), (-2,0), (2,0)] (for I piece maybe larger). Use matrix rotation on piece shape matrices. Each piece stored as a matrix (array of arrays with 0/1), rotate via transpose+reverse. For I piece using a 4×4 matrix, standard rotation works. Kick attempts: dx 0, -1, +1, -2, +2, and dy -1 (up nudge). That prevents clipping through walls/stack — rotation only applies if the resulting position is fully valid. - Soft drop: while held, gravity ×10 or move down each 40ms; also award 1 point per cell soft-dropped (standard). Hard drop: 2 points per cell. - Gravity timing: level 1 start 800ms per row, each level decrease: e.g., `interval = Math.max(60, 800 * Math.pow(0.85, level-1))`. Level = 1 + floor(lines/10). - Scoring: 1 line = 100×level, 2 = 300×level, 3 = 500×level, 4 = 800×level. Plus soft/hard drop points. - Lock delay? Keep simple: lock on landing when gravity tick can't move down — immediate lock is classic and fine, but a tiny lock delay (e.g., piece can still move for 300ms after landing) improves feel. I'll implement a simple reset-on-move lock delay capped: when the piece can't move down, start lockTimer (500ms); moving/rotating resets it (max resets maybe not capped — fine for casual). Actually to keep code manageable: if piece can't move down on a gravity tick, lock immediately. Hmm, immediate lock makes hard rotations near stack frustrating. I'll add lock delay of 450ms that resets on successful move/rotate (with a cap of resets... I'll allow unlimited resets but delay only resets while the piece hasn't moved down — that can cause infinite stalling only by player's own action, acceptable in casual games). Keep it simple: 500ms lock delay, reset on move/rotate, no cap. Fine. - DAS/ARR for held left/right: key repeat via keydown events is okay but browsers have inconsistent repeat. I'll implement my own: track held keys, initial delay 170ms, repeat 40ms. This is a craft detail that makes the game feel responsive. Yes, implement a small input handler with a `held` map and per-frame updates in the game loop. - Soft drop as held key too: continuous fast gravity. 10. **Game loop**: `requestAnimationFrame` with delta time. State machine updates. Render every frame (or only when dirty — render every frame is fine, includes ghost calculation). 11. **Next preview**: small canvas (4×4 cells). Draw next piece centered. 12. **Pause**: P key to pause — nice extra. I'll add pause overlay. Requirement doesn't ask but it's standard. Keep it — low cost. 13. **Restart**: R key anytime (or when game over). Restart key visible in overlay text. 14. **Scoring pop animation**: when score increases from line clear, briefly scale the score number (CSS class toggle). Also "+300" floating text? I'll do a small floating combo text on canvas at clear location: "TRIPLE +900" etc. Tasteful, brief. Names: SINGLE/DOUBLE/TRIPLE/QUADRA (avoid TETRIS trademark? "QUAD" — I'll use "SINGLE · DOUBLE · TRIPLE · TETRA"). Actually the game is called TETRA so quad clear = "TETRA!". Cute. 15. **Sound**: No external resources — WebAudio is allowed (inline). A tiny blip on move/rotate/lock/clear via oscillators would elevate feel. But sound can be annoying and adds code. I'll add minimal synthesized sounds with a mute toggle (M key)? Hmm, scope. The requirements are already large. I'll add very minimal audio: lock thud, clear chime, using a shared AudioContext created on first user input. Keep it ~25 lines. Actually, let me weigh: the user asked for a complete game; sound is beyond spec and risks bugs. But it's a signature-feel thing. I'll include a compact sound module with toggle (M). Small risk, big feel payoff. Yes, include but keep tiny: `beep(freq, dur, type, vol)`. 16. **Canvas sizing**: Cell size 30px → 300×600 canvas. For crispness on HiDPI, I could scale by devicePixelRatio. I'll do DPR-aware canvas setup: canvas CSS size 300×600, backing store ×dpr, ctx.scale(dpr,dpr). Good craft. 17. **Layout styling**: - Page: dark `#0e0f12` background, subtle vignette? Skip vignette. Maybe a very faint dot/noise pattern via repeating-linear-gradient? I'd rather keep flat and clean. A thin top rule / wordmark. - Wrapper: flex, gap, centered. Left: canvas. Right sidebar: NEXT preview canvas, stats (SCORE, LINES, LEVEL with big tabular numbers), divider, controls list (keys as styled `` chips). - kbd chips: bordered, monospace, small. - Colors: text `#d7dae0`, muted `#7a8290`, accents per-piece. - Border on canvas: 1px `#2a2d34`, maybe 3px. Sidebar panels with 1px borders, no rounded-heavy — small radius 8px, restrained. - No gradient anywhere. Flat colors, bevel on blocks only (that's per-cell highlight rendering, appropriate). 18. **Responsive**: On small screens, stack sidebar horizontally below/above canvas. Use flex-wrap. Also touch controls? Not required; skip to control scope (maybe mention keyboard needed). I'll add a note for keyboard. Could add basic touch buttons... scope creep; skip. Now write the code carefully. **Data structures:** ```js const COLS=10, ROWS=20, HIDDEN=2, TOTAL=ROWS+HIDDEN; const SHAPES = { I: {matrix:[[1,1,1,1]], ... } ``` Better: define matrices per piece: ```js const PIECES = [ { name:'I', color:'#56B6C1', m:[[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]] }, { name:'O', color:'#E5C07B', m:[[1,1],[1,1]] }, { name:'T', color:'#C678DD', m:[[0,1,0],[1,1,1],[0,0,0]] }, { name:'S', color:'#98C379', m:[[0,1,1],[1,1,0],[0,0,0]] }, { name:'Z', color:'#E06C75', m:[[1,1,0],[0,1,1],[0,0,0]] }, { name:'J', color:'#61AFEF', m:[[1,0,0],[1,1,1],[0,0,0]] }, { name:'L', color:'#D19A66', m:[[0,0,1],[1,1,1],[0,0,0]] }, ]; ``` 7-bag randomizer for fairness — nicer than pure random. Implement shuffle bag. Piece object: `{type, m, x, y, color}`. Position x,y as top-left of matrix in board coords (including hidden rows; y can be negative? With hidden rows, spawn y=0 with matrices that have empty top row... I set spawn y=0, x = floor((COLS - width)/2). For I piece matrix 4×4 with row1 filled, spawn y=0 puts it in hidden row 1 — fine. For 3×3 pieces with bottom two rows filled (T,S,Z,J,L have filled rows at index 1 (and T index0? T: [[0,1,0],[1,1,1]] filled rows 0 and 1) — T's filled rows are 0 and 1 → at y=0, occupies hidden rows 0,1, invisible initially! Hmm. That means the piece appears "popped" into view after first gravity tick. Standard behavior: piece spawns partially hidden then drops into view — that's fine and actually standard. But with HIDDEN=2 and spawn y=0, a piece occupying rows 0–1 is fully hidden; player sees nothing for one tick. Alternative: spawn at y=HIDDEN-2? Let me instead make matrices have their blocks in the *bottom* rows and spawn y = HIDDEN - height so the bottom row of the piece sits at the top visible row (row index HIDDEN). Let me define all matrices with filled cells at bottom: - I: [[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,1,1,1]] — height 4, spawn y = 2-4 = -2. Negative y — my board collision must handle y<0 (cells above top are valid/empty). Render only rows ≥ HIDDEN. Hmm negative y with collision check: treat y<0 cells as empty (allowed). That works: `cell empty if py<0`. Simpler alternative: keep matrices as given (filled rows at top-ish) and spawn y = 0 with hidden rows 0..1; pieces with blocks in rows 0–1 will be partly hidden — but I defined T with blocks in rows 0-1 which are hidden. Let me just normalize: define each matrix so filled rows are at the bottom, and spawn with y such that the lowest filled row of the piece is at board row HIDDEN-1 (bottom hidden row) — then the piece is just above the visible field and immediately drops into view. Or spawn lowest filled row at HIDDEN (first visible row) so it appears immediately. Classic Tetris spawns visible. I'll spawn so that the piece's bottom row aligns at the first visible row... but then game over check "spawn collision" handles top-out. Let me define matrices bottom-aligned: - I: rows: [ [0,0,0,0],[0,0,0,0],[0,0,0,0],[1,1,1,1] ], spawn y = HIDDEN-1? bottom filled row index 3 → board row = y+3. If y=0, board row 3 (visible, since visible rows are 2..21). Hmm HIDDEN=2, so y=0 puts I's blocks at board row 3 = second visible row. Fine. - T: [[0,0,0],[0,1,1,1]... wait 3×3: [[0,0,0],[0,1,0],[1,1,1]] bottom-aligned. y=0 → blocks at rows 1,2 → row1 hidden? row1 is hidden (rows 0,1 hidden). So T's top nub hidden initially, bottom row visible. Good enough — appears immediately. - O: [[1,1],[1,1]] y=0 → rows 0,1 fully hidden! Bad. Bottom-align O: it's already full 2×2, bottom row at index1 → y=0 → row 1 hidden, row 0... both hidden. So O invisible for first tick. Fix: spawn y = HIDDEN - (height-1)? For O height 2: y = 2-1 = 1 → rows 1,2: bottom row visible. General: spawn y = HIDDEN - 1 - (bottomFilledIndex)? Let me just compute: for each matrix, find the lowest filled row index `li`. Spawn y = HIDDEN - 1 - li + 1 = HIDDEN - li. Then lowest filled row lands at board row HIDDEN (first visible). For O: li=1, y=1 → rows 1,2 → row 2 visible. For I: li=3, y=2-3=-1 → negative. Negative y complicates collision slightly (cells with py<0 are above the board → treat as free unless x out of range). That's fine — standard. And lock: if piece locks with any cell at py<0 → game over (stack reached top). Also spawn collision → game over. Let me handle collision: ```js function collides(m, x, y){ for each cell (r,c) where m[r][c]: const bx = x+c, by = y+r; if (bx < 0 || bx >= COLS || by >= TOTAL) return true; if (by >= 0 && board[by][bx]) return true; return false; } ``` Cells with by<0 are allowed (above board). Good — enables rotation/spawn above top. Spawn: `piece = { m, x: Math.floor((COLS - m[0].length)/2), y: HIDDEN - lowestFilledRow, ... }` then if collides at spawn → try y-1, y-2? Standard just checks collision; if collide → game over. I'll check: if collides(m, x, y) → game over. But with by<0 allowed, only actual overlap triggers. Good. Rotation: ```js function rotate(m, dir){ const N = m.length; const res = Array.from({length:N},()=>Array(N).fill(0)); // note m may be NxN or rectangular; all my matrices are square (I 4x4, O 2x2, others 3x3). Good, keep all square. for r,c: dir>0 ? res[c][N-1-r] = m[r][c] : res[N-1-c][r] = m[r][c]; } ``` Ensure all matrices square: I 4×4 ✓, O 2×2 ✓, T/S/Z/J/L 3×3 ✓. Wall kick offsets: `[[0,0],[-1,0],[1,0],[0,-1],[-2,0],[2,0],[0,-2]]`? For I piece rotating at wall, need ±2 horizontal and vertical. Try list: [0,0],[-1,0],[1,0],[0,-1],[-2,0],[2,0],[0,1]? [0,1] down-kick rarely needed; floor kicks — keep [0,-1] and maybe [-1,-1],[1,-1]. I'll use kicks = [[0,0],[-1,0],[1,0],[0,-1],[-1,-1],[1,-1],[-2,0],[2,0]]. Sufficient for a casual game, no clipping guaranteed since we validate. **Line clear flow:** After lock: find full rows → if any: state='clearing', store rows, flashTimer=180ms; after timer, remove rows (splice from board, unshift empty rows), scoring, combo text, spawn next. If none: spawn next immediately. Board includes hidden rows; full-row detection across all rows (hidden rows can fill → count toward clears and game over condition). Actually if a hidden row fills completely that means stack reached top basically. Detection over all TOTAL rows. Game over condition: after lock, if any filled cell in rows < HIDDEN... hmm not exactly — piece locking partially above visible field isn't necessarily over; classic rule: game over when spawn is blocked or blocks lock fully within hidden zone. I'll use: after locking, if any cell of the locked piece is at by < HIDDEN... that ends games too aggressively? Standard guideline: game over if the piece locks entirely above the skyline or spawn blocked. I'll go: game over if spawn collides OR locked piece has any cell with by < HIDDEN... Let me think: with HIDDEN=2, if you stack to the very top, pieces will start locking with cells in hidden rows. Using "any cell above visible" triggers when a piece locks 1 cell above skyline — that's reasonable and matches "stack reaches the top". But rotating a T at the very top could lock a nub into hidden row without being game-over-worthy... To be forgiving: game over if the piece locks with its cells such that *no part is visible* (all cells by < HIDDEN) OR spawn blocked. Hmm, but then stack can creep fully to top row and keep playing with garbage. Compromise: game over if locked piece has any cell at by < HIDDEN-? ... Simplest robust: game over when spawn position collides (i.e., next piece can't spawn). Plus visual: if stack reaches visible top row, spawn will almost certainly collide. But there's a case: stack fills top visible row but piece spawns in hidden rows fine, then can't move down → locks immediately at spawn → next spawn likely collides. It self-resolves. I'll use spawn-collision as the game-over rule, plus also end if a locked piece is entirely in hidden rows. That satisfies "stack reaches the top." Actually simpler and clear: after lock, check `if (board.slice(0,HIDDEN).some(row => row.every... )` no. Keep: game over if new piece collides at spawn. I'll also draw a "danger" indication? Skip. **Ghost piece:** copy piece, drop until collide, draw outline at that y with piece color at 30% alpha fill + stroke. **Rendering:** - `draw()`: clear, background fill `#121318` slightly different from page `#0e0f12`. Grid lines. Locked cells. Ghost. Active piece. Clearing flash rows (white overlay with alpha pulsing). Then overlays handled in DOM. - `drawCell(ctx, px, py, size, color, alpha)`: base fill, then lighter top edge: fillRect top strip with rgba(255,255,255,0.18)? Classic bevel: - fill base color - top+left 2px lighter: rgba(255,255,255,.25) - bottom+right 2px darker: rgba(0,0,0,.35) - inner subtle: skip Plus 1px gap between cells (draw at px+1,py+1,size-2) — gives clean grid read. - Flash for clearing rows: fill row rect white with alpha oscillating. **HUD DOM:** ```html
...
``` Overlay states: ready (title + "PRESS ANY KEY"), paused, game over (score + "PRESS R TO RESTART"). I'll build overlay content via JS with innerHTML per state. **Input handling:** - keydown/keyup listeners. preventDefault for arrows/space. - Map: ArrowLeft/Right → move with DAS; ArrowUp → rotate (on keydown, and prevent repeat — ignore e.repeat); ArrowDown → softDrop flag; Space → hardDrop (on keydown, ignore repeat); P → pause; R → restart; M → mute. - In ready state, any key starts. - Own repeat: track `keys = {left: {held, timer}}`. In update(dt): if left held: timer -= dt; on initial press move immediately, then when timer<=0 move and timer=ARR. Implement: on keydown (not repeat) → move once, set das=170. In update: if held and das elapsed... Manage with two values: `das` countdown then `arr` countdown. Simple implementation: ```js const move = {dir:0, das:0, arr:0}; onKeyDown left: move.dir=-1; movePiece(-1); move.das=DAS; move.arr=0; move.waiting=true; update: if move.dir: if move.waiting { move.das-=dt; if das<=0 {waiting=false; move.arr=ARR; step()} } else { move.arr-=dt; if <=0 {arr=ARR; step()} } ``` Handle both directions: last pressed wins — store dir per key and use most recent. Keep simple: `heldDir` variable; on keydown set & immediate move; on keyup if other key still held switch. Edge cases minor. I'll track `left`/`right` booleans and `activeDir` (last pressed). Soft drop: `softDrop` boolean → gravity interval divided by 12, and each soft-drop cell grants +1. Implementation: gravity accumulator; while accumulating with soft drop, count cells moved down due to soft-drop speed and add points. Simpler: when soft drop active, on each successful down-step add 1 point. But gravity tick moves one row per tick regardless; with interval /12 it ticks 12× more often. Points: add 1 per tick while softDrop and piece moved down. Fine. Hard drop: move down until collision, +2 per cell, lock immediately. **Lock delay:** when gravity tick finds can't move down: if `lockTimer` not started, start it (450ms). If piece moves/rotates successfully and can move down again... standard: reset lockTimer on successful shift/rotate only if piece can now fall? Simplify: on successful move/rotate, if piece can move down, clear lockTimer. If can't move down, keep timer (don't reset infinitely — or reset with cap). I'll do: successful move/rotate while grounded resets lockTimer (uncapped — casual). During grounded state, update: lockTimer += dt; if >= LOCK_DELAY → lock. Meanwhile gravity ticks do nothing while grounded. Actually simpler unified: in update, try gravity accumulation; if piece grounded (can't move down), accumulate lockTimer instead. Move/rotate success resets lockTimer to 0. Hard drop locks instantly. Good. Wait — resets: if player rotates repeatedly, lockTimer resets forever, game stalls — but it's the player's own piece; classic games cap resets. I'll cap at like 15 resets? Meh — casual, skip cap. Fine. **Clearing animation:** rows stored; during 'clearing' state, timer counts down 260ms; render flashes those rows (alpha = timer-based, maybe strobing: alpha = 0.5+0.5*sin(t*...)). On finish: remove rows, apply score, floating text, spawn next. During clearing, input for movement disabled (piece is null anyway — set piece=null during clearing). **Floating text:** array of {text, x, y, t} drawn on canvas rising and fading over 900ms, bold monospace, in piece color or white. Triggered on clears: "DOUBLE +600" etc, and on level up "LEVEL 3". Also on tetra: "TETRA +800". **Sound (compact):** ```js let actx=null, muted=false; function sfx(freq, dur=0.06, type='square', vol=0.12, slide=0){ if(muted) return; actx = actx || new (window.AudioContext||window.webkitAudioContext)(); const o=actx.createOscillator(), g=actx.createGain(); o.type=type; o.frequency.value=freq; if(slide) o.frequency.exponentialRampToValueAtTime(slide, actx.currentTime+dur); g.gain.value=vol; g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+dur); o.connect(g).connect(actx.destination); o.start(); o.stop(actx.currentTime+dur); } ``` Sounds: move: sfx(200,'square',0.03) low tick; rotate: 320; lock: 140 with slide down; clear: two notes arpeggio (setTimeout or scheduled); hard drop: noise-ish low; game over: descending. Keep small. Resume context on first gesture (created on keydown anyway). Mute toggle M — show small indicator in panel. I'll include a "SOUND: ON/OFF" line in keys list that updates. **Bag randomizer:** ```js let bag=[]; function nextType(){ if(!bag.length){bag=shuffle([0..6])} return bag.pop(); } ``` Queue: keep `next` single piece (preview shows 1). Could do queue of 1. Fine. **Level speed:** `gravityMs = Math.max(50, Math.round(750 * Math.pow(0.82, level-1)))`. Level 1:750, 2:615, 3:504, 4:413, 5:339, 6:278, 7:228, 8:187, 9:153, 10:126, 11:104, 12:85, 13:70, 14+:70/50. Reasonable. Level = Math.floor(lines/10)+1. On level change: floating "LEVEL n", sfx. **DPR canvas setup:** ```js function setupCanvas(cv, w, h){ const dpr = window.devicePixelRatio||1; cv.width = w*dpr; cv.height = h*dpr; cv.style.width = w+'px'; cv.style.height = h+'px'; const ctx = cv.getContext('2d'); ctx.scale(dpr,dpr); // careful: scale persists; do it once return ctx; } ``` But I re-scale each frame? No — set transform each draw: `ctx.setTransform(dpr,0,0,dpr,0,0)` at start of draw. Safer. CELL = 30 → board 300×600. Preview: 4 cells × 22px = 88, plus padding → canvas 96×96? Compute piece bbox and center it. Preview drawing: given next piece matrix, trim empty rows/cols to get bbox, center within preview canvas. **Page CSS:** ```css :root { colors } * { box-sizing } body { background:#0d0f13; color:#d8dbe2; font-family: mono stack; min-height:100vh; display:flex; align-items:center; justify-content:center; } ``` Background: I could add an extremely subtle radial vignette — no, I'll keep flat but add faint horizontal scanline? Keep flat, clean. Maybe subtle CSS grid texture on body via repeating-linear-gradient at 2% opacity — skip, flat is cleaner. Stage layout: `display:flex; gap:28px; align-items:stretch; padding:24px;` Panel width ~200px. Title "TETRA" letter-spaced, with the four-cell logo drawn? Cute idea: render a tiny T tetromino via CSS grid of 4 divs next to the title — small authored touch. Or inline SVG. I'll do a small 4-square mark using CSS grid (2×3) in the accent color. Simple: a `.mark` div with grid-template and 4 cells. Actually inline 4 spans. Let me do: logo = small canvas? Overkill. CSS grid 3 cols: cells at positions forming T. Fine, tiny detail: ```html

TETRA

``` `.mark{display:grid;grid-template-columns:repeat(3,9px);grid-template-rows:repeat(2,9px);gap:2px}` with i cells colored and placed: T shape = top row all 3, bottom middle. Use grid-area placement on 4 ? T needs 4 cells: top:3 cells + bottom middle. 4 : first spans columns 1/4 in row1? grid-column:1/4 for one i (that's one cell spanning 3), then middle bottom. Wait that's the T: row1: one i spanning cols 1-3; row2: one i at col 2. That's only 2 elements. OK: `.mark i:first-child{grid-column:1/4; background:accent}` `.mark i:nth-child(2){grid-column:2; background:accent}`. Two elements. Fine. Stats: three blocks with label (10px uppercase letterspaced muted) and value (22px tabular). Score pop animation: `.pop { animation: pop .18s }` transform scale — applied by re-adding class. Keys list: rows like `[←] [→] Move` with kbd chips. kbd: `border:1px solid #2e323b; border-bottom-width:2px; border-radius:4px; padding:1px 6px; font-size:11px; background:#181b21`. Overlay: absolutely positioned over board, `backdrop-filter: blur(2px)`; background rgba(13,15,19,.72); centered text. Hidden when playing. Game over: big "GAME OVER" in the rose color, final score, "PRESS R TO RESTART". Ready: title mark, "PRESS ANY KEY TO START" pulsing (CSS animation opacity). Paused: "PAUSED — P TO RESUME". Also top of board maybe show level-up flash — floating text handles it. **Edge: prevent arrow key scrolling** — e.preventDefault() for handled keys. **Restart (R):** resets board, score, lines, level, bag, state='playing', spawn. From any state. **Pause (P):** toggles between playing/paused. Paused overlay. Also auto-pause on blur? Nice: window blur → pause if playing. Add it. **Timing details in update(dt):** ```js function update(dt){ if(state!=='playing') { if(state==='clearing'){ clearTimer-=dt; if(<=0) finishClear(); } handle DAS? no } ... } ``` During clearing, ignore movement (piece null). DAS timers reset on lock. Also floating texts update during any state (run in all states? update in playing+clearing). **Rendering order per frame:** 1. setTransform(dpr) 2. bg fill 3. grid lines (vertical/horizontal, rgba(255,255,255,0.045)) 4. locked cells (skip if clearing rows? draw all; flash overlay on top) 5. clearing rows: draw white overlay alpha computed, and maybe fade those rows' cells: draw cells then overlay white with alpha; near end, alpha high. Also make them appear to "burn": alpha = 0.4 + 0.4*sin? I'll do: progress p = 1 - clearTimer/CLEAR_MS; flash alpha = p<0.5 ? 0.85 : (0.85*(1-(p-0.5)*2))? Keep simple: alpha oscillates: 0.3+0.6*Math.abs(Math.sin(p*Math.PI*3)). Fine. 6. ghost (only when piece exists & state playing/paused-under-overlay... in paused we still render behind overlay — fine, or skip rendering board while paused to prevent "planning"? Classic games hide board when paused. Nice touch: when paused, don't draw pieces (draw empty board). I'll do that — draw grid only. Authored detail.) 7. active piece 8. floating texts 9. top hidden rows: not rendered (render offset: visible row r maps to py=(r-HIDDEN)*CELL). Wait — rows 0,1 hidden; only draw rows HIDDEN..TOTAL-1. But active piece may have cells at by