Jelly Tetris
jellytetris
Minimax FlashOpus
WinnerCheapestFastest
Minimax Flash wins.
Who won? You pick
Pick the build you liked best to see what everyone else thinks.
The prompt
Build **"Jelly Tetris"**: a real-time, interactive 3D game of falling-block puzzle where every piece is a **translucent gummy jelly** that people **move**, **rotate**, **drop** and **poke**, where pieces **squash when they land**, the whole stack **wobbles like one big dessert**, and full rows **swell and pop into juicy droplets**, as ONE self-contained HTML file with **synthesised sound**.
It should feel like a glossy candy macro photograph that you can play: the game logic crisp and exact like the classic, the jelly soft and alive on top of it. The landing squash, the wave through the stack and the line-clear pop are the whole point.
## Tech
- three.js r170 with `WebGPURenderer` (automatic WebGL2 fallback). Load it with an importmap from jsDelivr: map "three", "three/webgpu" and "three/tsl" to `https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.webgpu.min.js`, and "three/addons/" to `https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/`. These are the **only** network requests allowed: no other libraries, fonts, images, models, textures or audio files. Use TSL node materials. Physics and simulation are your own code (no physics libraries).
- Start with `<!doctype html>`, `<meta charset="utf-8">` and a viewport meta tag. System font stacks only.
- It must run inside `<iframe sandbox="allow-scripts allow-pointer-lock">` (no `allow-same-origin`): `localStorage` and cookies throw there, so wrap every storage access in `try/catch` and keep values in memory. No `alert`, pop-ups or top-level navigation.
- Listen for keys with `keydown` / `keyup` on `window` and read `event.key` (the page hosting the game sends synthetic key events, so never rely on `event.code`, `keyCode` or focus on a specific element).
- Fixed simulation timestep (120 Hz) with bounded substeps for the jelly springs, decoupled from frame rate; on a slow frame, drop simulation time rather than exploding. The game clock (gravity, lock delay) runs on real time. Let the jelly sleep once everything is at rest and wake on any landing, clear or poke.
- Preallocate every buffer, geometry and material at start-up (all 200 cells, the falling piece, the ghost, the droplet pool); nothing is created during play.
- A quality scaler that lowers pixel ratio, then shadows, then post effects, then droplet counts when frames get slow, never the jelly feel or input response. Target 60 fps on a mid-range phone.
- Known three r170 pitfalls, avoid them:
- the scene background is drawn after transparent objects: draw the backdrop as a mesh (or give transparent sky objects `depthWrite: true`);
- never `.clone()` a `CanvasTexture` (the clone uploads black);
- create every `InstancedMesh` at its full count and never draw one with fewer than 2 instances (wrong-size uniform buffer, black screen): hide unused instances by scaling them to zero instead of lowering `count`;
- never let a helper overwrite a shared temp vector its caller still uses.
## Priorities
Build it in this order, and finish each tier properly before starting the next. A polished tier 1 beats a broken everything.
1. **Must be excellent:** exact, responsive classic rules (no missed inputs, no wrong collisions); the landing squash and the wobble wave through the stack; the line-clear pop and the plop of the rows above; the jelly look; the demo mode.
2. **Must work:** ghost piece, hold, next queue, scoring and levels, Tetris celebration, poke, game over and restart, sound, touch controls, the interface, the Easter egg.
3. **Polish:** droplets that splat on the glass and slide down, coloured light through the jelly onto the floor of the case, combo pitch rising, the game-over melt.
Never ship a feature half-working: if something from tier 3 can't be done well, leave it out cleanly.
## The game (logic first, exact)
- A **10 x 20** board (2 extra hidden rows above for spawning). The logic is a plain grid and stays exact: the jelly is only how it looks and moves, it never decides a collision.
- The seven classic pieces, classic colours: **I** cyan, **O** yellow, **T** purple, **S** green, **Z** red, **J** blue, **L** orange.
- **Randomness: 7-bag.** Shuffle all seven pieces, deal them, reshuffle when the bag is empty. Each new game gets a fresh random seed (`?seed=<n>` to fix it); demo mode uses a fixed seed.
- **Rotation:** Super Rotation System with the standard wall kicks (separate kick table for I). Rotation is instant in logic; the jelly catches up visually.
- **Gravity** by level (level 1 about 1 row per second, speeding up each level, roughly the classic curve). **Soft drop** about 20x gravity, **hard drop** instant.
- **Lock delay** 0.5 s, reset by a successful move or rotation, at most 15 resets per piece.
- **DAS / ARR** for held left / right: 170 ms delay, then a move every 50 ms.
- **Ghost piece:** a faint, frosted outline of the landing spot.
- **Hold** (one slot, once per piece) and a **Next** queue of 3.
- **Scoring:** 100 / 300 / 500 / 800 x level for 1 / 2 / 3 / 4 lines, +1 per soft-dropped row, +2 per hard-dropped row, a combo bonus of 50 x combo x level for consecutive clearing pieces. Level up every 10 lines.
- **Game over** when a new piece cannot spawn.
## The jelly
- **Cells:** each cell is a rounded jelly cube (corner radius about 18% of the cell). Cells of the **same piece fuse** into one gummy: build the cell from 16 instanced variants by which of its four sides touch a same-piece neighbour, with the touching sides flat and joined, so a T reads as one soft T, not four boxes. When a line clear cuts a piece, the remaining cells round off their cut sides with a small wobble.
- **Springs:** every board cell has a small spring state (offset x/y, squash/stretch scale, wobble) driven by a damped spring, about 6 Hz, damping ratio about 0.25 at default softness. Neighbouring cells are coupled, so an impulse travels through the stack as a visible **wave**, fading over 3 to 4 cells and about 1 s. Render with these offsets and scales (vertex shader or instance matrices); the grid positions never change.
- **Landing:** on lock the piece squashes (about 25% flatter and wider on hard drop, 10% on a soft landing), overshoots, and settles in about 0.6 s; the cells beneath dip and pass the wave down and sideways, strength scaled by drop speed.
- **Falling:** the falling piece stretches slightly along its fall speed, lags a little behind sideways moves and leans into them, and wobbles after a rotation (it rotates with a quick 90-degree ease of about 80 ms, overshooting a few degrees).
- **Weight:** the stack sags a little under its own height: the lowest rows are up to 6% squashed when the board is near full, so a tall stack looks heavy and tense.
- **Line clear:** the full rows **swell** (about 12%) and brighten for 0.15 s, then **pop**: each cell bursts into 6 to 10 jelly droplets in its colour (an instanced pool) that fly out, fall and splat on the glass front and the case floor, then fade. The rows above **drop** with gravity, land with a plop and send their own wave down. Multiple clears pop bottom to top in quick sequence (40 ms apart).
- **Tetris (4 lines):** a bigger pop, the whole remaining stack bounces like one dessert (one big squash wave from the bottom up), a short camera nudge, and a "TETRIS" toast in jelly letters that wobble in.
- **Poke:** tapping or clicking a settled cell dents it and sends a wave through its neighbours (Shift-click or a long press for a big squish). Pure delight, it never changes the game.
- **Never:** jitter at rest, a visual cell drifting away from its grid slot for more than a moment, gaps or overlaps between fused cells, or a wobble that makes it hard to see where a piece actually is.
## Controls
- Keyboard: `ArrowLeft` / `ArrowRight` move, `ArrowUp` or `X` rotate clockwise, `Z` rotate counter-clockwise, `ArrowDown` soft drop, `Space` hard drop, `Shift` or `C` hold, `P` or `Escape` pause, `R` restart, `V` Cinema (hide UI), `M` mute.
- Touch, on the board: drag sideways moves one column per cell-width dragged, tap rotates clockwise, drag down soft drops, a fast flick down hard drops. A tap on a settled cell of the stack (below the falling piece) pokes it instead of rotating. Plus a row of big on-screen buttons at phone width: Hold, Left, Rotate, Right, Drop.
- A press that ends within 6 px and 450 ms counts as a tap. Nothing is hover-only.
- On first load the game waits on a start card ("Press any key or tap to play"); any key or tap starts it.
## Easter egg
- After the first 20 pieces, one piece in about 40 is **golden jelly**: honey-gold, with tiny suspended glitter flakes. Clearing a line that contains a golden cell turns its droplets into sparkles, rings a little bell and doubles that clear's score. Never mention it in the UI.
## Sound
- Synthesize everything with WebAudio; create the `AudioContext` on the first gesture only; a mute button and `M`.
- Move: a tiny soft tick. Rotate: a short rubbery "bwip". Soft landing: a gentle "plop"; hard drop: a wet "splat" with a low thump, louder with height dropped. Hold: a soft slurp.
- Line clear: a juicy pop per row, pitched up a step for each row in the clear and for each combo step; Tetris: four rising pops and a big springy "boing". Poke: a small "blub".
- Level up: a two-note chime. Game over: a slow descending squelch.
- A master gain into a compressor, no clipping; cap simultaneous voices at about 12.
## Look
Aim for a glossy candy macro photograph, not a game asset.
- **Colour:** sRGB output, AgX tone mapping, exposure 1.0.
- **Lighting:** a procedural environment (`RoomEnvironment` through `PMREMGenerator`, no HDR files) plus a warm key from the upper left, a soft cool fill and a rim light from behind so every piece has a bright edge; soft shadows (2048, 1024 on phones) cast onto the back panel and floor of the case, and a contact shadow under the case.
- **Materials:** jelly is `MeshPhysicalNodeMaterial` with transmission, thickness, IOR about 1.35 and an attenuation colour per piece type (absorption deepens where it's thicker), a clearcoat for the wet sugar shine, and a faint procedural variation so no two cells look identical; a few tiny suspended air bubbles per cell. If full transmission is too slow, fake it convincingly (Fresnel rim, thickness-tinted colour, a refracted backdrop sample) rather than going opaque. The ghost piece is a frosted, low-opacity version of the same shape.
- **The case:** the board sits in an upright clear acrylic case (thin rounded frame, 10 x 20 cells inside) on a soft matte plinth; the back panel is frosted with a barely visible cell grid so positions read clearly.
- **Post:** one subtle bloom (strength ~0.4, threshold above 1), a light vignette. No heavy chromatic aberration or grain.
- **Backdrop:** a soft studio gradient in a **cool dusty slate blue** (around `#5b6b86` to `#3e4a60`), a mid tone that contrasts with the warm jellies. Never orange-on-orange, never pure white or pure black.
- **Framing:** the default camera is a gentle three-quarter view slightly above, the case filling about 80% of the height of a square frame, with Next / Hold floating beside it. The whole board must always be visible and readable while playing.
- **Motion:** everything eases, overshoots a little and settles; nothing pops or teleports (except the logic, which is always instant).
## Interface
- Calm, small and out of the way: **Score**, **Lines**, **Level** and the session **Best** on one side; **Hold** and **Next (3)** as small floating jelly previews in little glass trays beside the case. A status pill `WEBGPU · LIVE` or `WEBGL2 · LIVE`, a mute button, a pause button.
- Toasts top centre: "Double", "Triple", "TETRIS", "Combo x3", "Level 4". Pause shows a small card with Resume, Restart and the key list. Game over shows the score, lines and best with a big "Play again".
- At phone width everything collapses: stats in one line above the case, Hold / Next small at the top corners, the touch buttons at the bottom.
- If neither WebGPU nor WebGL2 works, a clear, designed message.
## Camera
- Fixed during play (a player needs a stable board). Drag on the backdrop outside the case, or two fingers, orbits a little (±25 degrees) and springs back on release. `Cinema` (`V`) hides all UI.
## Test hook
If the URL has `?test=1`, expose `window.__toy` with no visible change: `start()`, `press(key)`, `setBoard(rows)` (an array of 20 strings like `"IIII..OO.."`), `spawn(type)`, `tick(seconds)`, `poke(col, row)`, and `getState()` returning `{ renderer, fps, running, gameOver, score, lines, level, piece, hold, next, board, maxWobble, sleeping }`.
## Demo mode (required)
If the URL has `?demo=1`, auto-play a 4-second loop with no user input: the object drops in, wobbles, gets poked or sliced once, then settles.
For this toy that means:
- Hide all UI and the cursor (stats, Hold / Next, toasts, buttons, start card). Never create an `AudioContext`.
- One fixed quality preset, fixed seed, default camera.
- A deterministic loop restarting every 4.0 s of real time from the identical state. The start state: 4 full-width rows except a one-cell well in column 9, plus a few loose pieces on top (a T and an S on row 5-6, so something remains after the clear), all colours mixed:
- **0.0 to 0.3 s:** the stack at rest, light glowing through the jelly.
- **0.3 to 1.1 s:** a vertical I piece falls fast down the well, stretching with speed.
- **1.1 s:** it lands with a big squash; a wave rolls through the whole stack.
- **1.4 to 2.0 s:** the four rows swell, brighten and pop bottom to top into droplets that splat on the glass; the T and S drop to the floor of the case and plop with a wobble.
- **2.0 to 2.6 s:** droplets slide and fade; the T and S jiggle.
- **2.6 to 3.6 s:** the starting stack rebuilds: pieces rain in quickly from the top and land one after another with small plops (the T and S lift back to their spots).
- **3.6 to 4.0 s:** everything settles, identical to the state at 0.0 s.
- Start moving within 1 s of load; the drop and the pop must read clearly at 15 fps (the demo is recorded in a slow software renderer).
- Support `?demo=1&t=<seconds>` to freeze the demo at that time.
## Before you finish
Self-check against this list and fix anything that fails:
- Rules: every piece rotates both ways with correct wall kicks (including I and against the floor), the 7-bag never repeats a piece within a bag, lock delay and its 15-reset cap, DAS / ARR, hold once per piece, scoring for 1-4 lines and combos, levels, game over and restart.
- Feel: hard drop from the top on an empty board and onto a tall stack, soft landings, 1, 2, 3 and 4-line clears, and clears in the middle of a tall stack: the squash, the wave and the plop of the rows above all read clearly and settle with no jitter; at rest everything sleeps.
- Fast play: hammer keys at high levels; no input is ever lost or delayed by the jelly animation.
- Poke, Cinema, pause and resume, mute, and resizing to phone width with touch (drag, tap, flick, the on-screen buttons, poking the stack).
- Synthetic keys: `window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }))` followed by a `keyup` hard-drops, and the same works for `ArrowLeft`, `ArrowRight` and `ArrowUp`.
- Sound: every action has its effect, nothing plays before the first gesture, mute works, a Tetris never clips.
- It works inside `<iframe sandbox="allow-scripts allow-pointer-lock">` with storage blocked.
- `?demo=1` matches the timeline, loops cleanly, shows no UI and plays no sound; `?test=1` exposes `window.__toy`.
- No console errors or WebGPU validation errors on WebGPU **and** on WebGL2 (force the fallback to check), and it stays smooth on a mid-range phone.
Put exact, snappy controls first. The landing splat and the line-clear pop should be the best part.