5 min read

Tetris Web Game

A Tetris clone in HTML5 canvas, jQuery 2.1.0, and a single script.js. Weekend project from 2014 — coding to scratch the tech itch while grinding quant finance. Twelve years on, the demo URL still boots, which is its own quiet achievement (most side projects from 2014 are dead links by now: GitHub user pivoted, gh-pages branch rotted, jQuery’s CDN disappeared, something).

What it is, looked at fresh

A perfect artifact of its exact moment in web dev history. jQuery as the default assumption (right before React/Angular/Vue properly took over and killed it). Canvas 2D with no WebGL, no pixi.js, no phaser — just “I’ll draw rectangles myself” because that’s how every tutorial framed it. Table-based layout for the score panel — <table align="center"> with nested tables for the stat grid, because Flexbox wasn’t widely trusted until ~2015 and CSS Grid was years away.

The HTML5 shim-era CSS is still in there — the article, aside, details, figcaption... display:block rule at the top of styles.css is the ghost of we need to tell old browsers these new semantic tags are blocks. I wasn’t even using any of those elements. Vestigial CSS from a tutorial I copied. Beautiful.

What past-me actually did right

Reading the code now, with names for the patterns I didn’t have then:

  • Tetrimino factory + Piece.prototype.* methods — basically pre-class-syntax idiomatic JS. Separation of concerns is reasonable: game as the state/rendering namespace, Tetrimino as piece behavior, keys as input config.
  • Try-then-commit collision pattern. rotateCW / moveLeft build a candidate tPiece, convert to world coordinates, check occupiedQ on each cell, only commit if valid. That’s the textbook approach. I reached for it not because a blog post told me to, but because mutating-then-undoing felt annoying.
  • Coordinate abstraction. Piece cells stored as unit offsets (-1, 0, 1) around a center, multiplied by pSize at render time via convertXYs. Rotation becomes (x, y) → (-y, x). Clean.

Where the 2014 weirdness shows

Things current-me would wince at:

  • tPiece, lastKey, keys, converted — all declared without var/let/const. Accidental globals everywhere. JS’s silent-global behavior was a footgun I clearly hadn’t been bitten by yet. 'use strict' would’ve caught it all.
  • setTimeout(loop, 1000/fps) recursion instead of requestAnimationFrame (which I imported via comment at the bottom but never used). Game loop runs on a timer rather than synced to the display, inputs don’t feel crisp.
  • Rotation has no wall-kicks. Try to rotate an I-piece next to the wall and it silently fails instead of nudging it in. Modern Tetris guideline has SRS kick tables for this.
  • Multi-line clear logic is a bit off — it shifts everything above the last cleared line down per iteration, which collapses doubles/triples/tetrises rather than respecting gaps. Plays fine, isn’t quite right.
  • An Array.prototype.indexesOf monkey-patch that’s never actually called anywhere. Classic.

The “function as first-class citizen” thing

Coming from C++/Java where functions are just not values, the idea that you pass function() { loop(); } to setTimeout as data is a paradigm shift. I half-got it: I wrote setTimeout(function() { loop(); }, ...) instead of setTimeout(loop, ...). Functionally equivalent, but the wrapper suggests I was still thinking of loop as “a thing to be invoked” rather than “a reference I can hand over.” Baby’s first closure.

The funny thing is — that exact instinct (functions as values, closures, composition) is what makes modern ML and data-pipeline work feel natural. map, filter, higher-order transforms, decorators, partial application. All the things that felt weird in 2014. Came around eventually.

Why I’m leaving it as-is

This is the kind of code that would benefit enormously from types. Every converted.x[i] access, every game.inactives[i].x === piece.x && game.inactives[i].y === piece.y, every “is this a piece object or a cell object or a coordinate pair” ambiguity — TypeScript would clean it up and catch the accidental globals at the same time.

But I’m not going to rewrite it. It’s a monument to a specific mode I used to be in: a quant by day who missed making things, sitting with a JS book on a weekend, brute-forcing through a problem with no autocomplete, no LLM, no hints at the next token — until reasonable patterns fell out because the problem demanded them. The yearning to go back to building, and the kind of muscle you only build when nothing’s whispering in your ear. Worth keeping a record of.