RedoHub's Tetris is the densest piece of code on the whole site -- the entire game logic fits in one compact <script> block, and almost every line in it is doing real work. A few of the decisions in there are worth pulling apart on their own.

Pieces Come From a Shuffled Bag, Not Math.random() Directly

If you pick each new piece with a plain random roll, you can go a genuinely long stretch without seeing a piece you need -- or get four S pieces in a row, which feels awful to play against. Modern Tetris games solve this with a "7-bag" system, and I implemented the same idea:

function shuffle(a){for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;} function newBag(){return shuffle([...KEYS]);} function nextFromBag(){ if(!bag.length){bag=nextBag.length?nextBag:newBag();nextBag=newBag();} if(!nextBag.length)nextBag=newBag(); return bag.shift(); }

All seven piece types go into a bag, get shuffled once (a Fisher-Yates shuffle, the standard unbiased way to shuffle an array), and get handed out one at a time until the bag is empty, at which point a freshly shuffled bag replaces it. This guarantees you'll see every piece exactly once every seven pieces -- no droughts, no floods -- while the shuffle order still keeps the sequence unpredictable. The nextBag variable exists so the "next piece" preview shown in the sidebar is always accurate even right at the boundary between one bag emptying and the next one starting.

Rotation Uses a Short Wall-Kick List, Not the Full SRS Tables

Real competitive Tetris implementations use the Super Rotation System, which defines a specific table of kick offsets for every piece type and every rotation transition. I didn't implement that table. Instead, rotation tries a small fixed set of horizontal offsets and takes the first one that fits:

function doRotate(){ const r=rotate(piece.matrix); for(const k of[0,-1,1,-2,2]) if(valid(piece,k,0,r)){piece.matrix=r;piece.x+=k;return;} }

Try rotating in place first; if that overlaps something, try shifting one column left, then one right, then two left, then two right, and take whichever first produces a valid position. It's a deliberate simplification -- full SRS kick tables are large and piece-specific, and getting them exactly right matters enormously for competitive play but far less for a casual browser game. This shorter list still lets pieces rotate sensibly next to walls and most stacked blocks, which is the behavior that actually matters for someone playing casually; it just won't match frame-perfect SRS behavior in every edge case a tournament player would notice.

Drawing and Dropping Are Two Separate Animation Loops

There isn't one game loop -- there are two independent requestAnimationFrame chains running at the same time. draw() redraws the board, the falling piece, and the ghost piece on every single frame, forever, once started. Separately, gameLoop() only checks whether enough time has passed to drop the piece one row:

function gameLoop(ts){ if(!gameRunning||gamePaused)return; if(ts-lastDropTime>dropInterval){lastDropTime=ts;if(valid(piece,0,1))piece.y++;else place();} dropTimer=requestAnimationFrame(gameLoop); }

Splitting these apart means the visuals -- including the ghost piece, which recalculates its landing position every frame based on the piece's current column -- stay perfectly smooth and responsive to left/right movement even though the piece itself only moves down once every dropInterval milliseconds, which shrinks as the level increases. If drawing were tied to the same timer as dropping, moving the piece sideways would only visually update once per drop tick, which would feel sluggish and unresponsive compared to how instant it actually needs to feel.

Touch Buttons Fake a Keyboard's Key Repeat

Holding an arrow key down on a real keyboard moves a piece once immediately, pauses briefly, then repeats rapidly -- that's OS-level key repeat, and it's what makes holding "left" feel natural. Touch buttons don't get that behavior for free, so I built it by hand:

function btnStart(fn,noRepeat){ fn(); if(!noRepeat){ clearTimeout(rptDelay);clearInterval(rptTimer); rptDelay=setTimeout(()=>{rptTimer=setInterval(fn,75);},180); } }

The function fires immediately on press, then waits 180ms before switching into a fast 75ms repeat interval -- deliberately mirroring the "short delay, then rapid repeat" rhythm of a real keyboard, rather than either firing once per tap or repeating instantly from the first press (which would make small, precise taps impossible). Rotation and hard-drop buttons pass noRepeat=true because those are one-shot actions -- repeating a hard drop 13 times a second the instant you touch the button would be actively harmful, not helpful.

Block Size Is Computed, Not Fixed

computeLayout() derives the pixel size of a single block from the actual viewport, then floors it to a whole number before anything gets drawn: Math.floor(availH/ROWS). A fractional block size -- say 23.7px -- would leave faint sub-pixel gaps between some grid cells and not others, since different browsers round fractional pixel boundaries slightly differently. Flooring to a whole pixel before the grid is ever drawn means every block is guaranteed to tile perfectly against its neighbors, on every screen size the layout function runs on.

Scoring Rewards Skill, Not Just Line Clears

The point values for clearing lines aren't linear -- clearing four lines at once (a "Tetris") is worth far more than four separate single-line clears would be:

const LS=[0,100,300,500,800]; score+=(LS[full.length]||1200)*level+(combo>1?(combo-1)*50*level:0);

One line is 100 points, but four lines is 800 -- twice the per-line rate of a single clear -- which rewards the harder, riskier play of stacking up and waiting for a big clear instead of always taking the easy single line. On top of that, consecutive clears without a "miss" build a combo multiplier, and everything scales with the current level, so late-game play (higher level, harder to set up big clears) pays proportionally more per success. Hard-dropping a piece also adds two points per row it fell, and manually soft-dropping adds one point per row -- a small nudge toward playing faster and more decisively rather than always slow-dropping every piece to think it over.