A game with no server behind it.
This website is static: a set of files, with no database and no code running on a server. I wanted a game on it that people would actually compete on: three minutes on the clock, one score, and my record to beat. Competition normally needs a server that stores the scores and checks them. PolyFit gets the same result in the browser, from a single property of its engine.
The genre is familiar on purpose, so that anyone who has played a falling‑blocks puzzle feels at home at once. The interest is in how it is made: plain JavaScript modules loaded directly by the browser, drawing on a canvas, sounds and music synthesized on the spot, and no frameworks, no libraries and no build step.
Modern rules, no surprises.
- The 7‑bag. The seven pieces are shuffled in a bag and dealt one by one, then a new bag starts. Two copies of a piece are never more than 12 pieces apart, and after n pieces each one has been dealt ⌊n/7⌋ or ⌈n/7⌉ times: the Statistics panel never shows two counts more than one apart.
- Turns with wall kicks. A turn tries the rotated piece where it is, then four offsets of the Super Rotation System, and keeps the first one that fits.
- Hold, preview and ghost. One hold per piece, the next five pieces in view, and a dashed outline where the falling piece will land.
- Lock delay. Half a second on the ground, restarted by a move or a turn up to 15 times; reaching a new lowest row gives the 15 resets back.
- Gravity. Fifteen levels, one every 10 lines, on the curve t(ℓ) = (0.8 − 0.007(ℓ − 1))ℓ − 1 seconds per row: one row per second at level 1, more than two rows per tick at level 15.
| Move | Points | Notes |
|---|---|---|
| Single, Double, Triple | 100 · 300 · 500 | × level |
| PolyFit! (four lines) | 800 | × level; only the I piece can do it |
| T‑spin, 0 to 3 lines | 400 to 1,600 | × level; Mini 100 to 400 |
| Back‑to‑back | × 1.5 | a PolyFit! or a T‑spin with lines, after another with no plain clear in between |
| Combo | 50 × combo | × level, for pieces in a row that clear lines |
| Perfect clear | 800 to 3,200 | × level, when the board is left empty |
| Soft and hard drop | 1 · 2 per row | not multiplied |
T‑spins follow the 3‑corner rule: after a turn, at least three of the four cells diagonal to the center of the T are filled. Both corners on the side it points to make a full T‑spin; otherwise it is a Mini.
Same seed, same inputs, same game.
A game is fully determined by two things: the seed of its random generator and the list of its inputs, each one a tick, a button, and pressed or released. The rules run in fixed steps of 1/60 of a second, whatever the refresh rate of the screen. Gravity, the lock delay and the key repeat are whole numbers, never floating point; time exists only as a count of ticks; every random choice comes from Mulberry32, a seeded 32‑bit generator. So replay(seed, log) plays a game again and reaches the same board, score and statistics, to the last point.
export function replay(seed, log, seconds = GAME_SECONDS) { const game = new Game({ seed, seconds }); game.start(); let i = 0; while (game.state === 'playing') { while (i < log.length && log[i][0] === game.ticks) { const [, button, down] = log[i++]; if (down) game.press(button); else game.release(button); } game.tick(); game.events.length = 0; } return game;}
The engine imports nothing outside its folder and never touches the page, so the same code runs in the game, in the tests and in the browser of anyone who opens a challenge link. The demo at the top of this page uses it too: it decodes the link of my record game and replays it, live.
A whole game inside a link.
At the end of a game, “Challenge a friend” packs it into the part of the address after the #: a format byte, the version of the rules, the seed, the day, the player's name and the inputs. Each input becomes one number, ten times the ticks since the previous input plus a code from 0 to 9, written seven bits per byte. The bytes are compressed with DEFLATE and written in URL‑safe Base64. Browsers never send that part of an address to a server, so the game travels without ever reaching the website.
function pushNumber(out, n) { while (n >= 0x80) { out.push((n & 0x7f) | 0x80); n >>>= 7; } out.push(n);}/** The inputs as bytes: the part of a link that is the game itself. */function inputBytes(log) { const out = []; pushNumber(out, log.length); let last = 0; for (const [tick, button, down] of log) { pushNumber(out, (tick - last) * 10 + inputCode(button, down)); last = tick; } return out;}
| Game | Inputs | Packed bytes | Characters after #c= |
|---|---|---|---|
| My record, 27,638 points | 831 | 1,146 | 1,319 |
| Automatic player, five games | 2,626 – 3,736 | 3,021 – 4,287 | 787 – 1,063 |
A human presses keys at irregular moments, which compresses less than the steady rhythm of the automatic player of the tests.
A link holds no score. Whoever opens it replays the game and gets the score by themselves, then reads “Verified: replayed in your browser, move by move”. A score can be raised only by changing the inputs, and then the replay gives whatever those inputs really score. Decoding also refuses damaged or crafted links: at most 1 MiB once decompressed, 30,000 inputs and three minutes of ticks, names cleaned to letters, digits and a few signs, and the version of the rules checked, so that an old link is refused instead of replaying into a different game.
Racing my game, tick by tick.
Whoever opens a link plays the same pieces, in the same order, while my game is replayed in a small panel next to the board: at every frame, the replayer advances the recorded game to the player's tick. Under the score, a split time shows the gap at that very moment, like the intermediate times of a race. Without a challenge, the same mechanism races the player's own best game, and Watch plays any recorded game on the board at one, two or four times the speed.
Scores nobody can make up.
- Top 10 on this device. Like the high-score table of an arcade cabinet: the best ten games of the browser, with names, each stored with its game so that it can be watched again.
- Hall of Fame. The players who beat my record and send me their link. The links are part of the source code, and every visitor's browser replays them when the page opens, waiting whenever a game is running, and sorts them by the replayed score.
- What a replay proves. That a score is exactly what its inputs produce under the rules. It cannot prove that a human played: the automatic player of the tests scores up to 307,192 points in three minutes, more than eleven times my record. That is why the Hall of Fame is curated by hand.
Keyboard, touch, every screen.
On a desktop the board is as tall as the window allows, with the panels on both sides; on a phone the numbers become a strip above it and the Statistics a strip below the touch buttons. The keyboard is read by physical key, so the controls stay in place on every layout. On touch screens the board itself is the controller: a drag moves the piece, a tap turns it, a flick down drops it and a flick up holds it, told apart by distance, duration and speed. A mark inside every piece tells them apart without color, the game respects a request for less motion, a pause hides the board, and leaving the tab pauses at once: the clock never runs unseen.
Music without audio files.
Every sound is made when it plays, with the Web Audio API: short notes on a pentatonic scale, so that sounds which overlap never clash. The music is Korobeiniki, a Russian folk song of 1861 in the public domain, in an 8‑bit arrangement with a second part of my own: 24 bars at 150 beats per minute, on four voices. A pulse wave carries the melody, a quieter one follows it a third below, a triangle wave jumps between the root of each chord and its octave, and the drums are a falling triangle wave for the kick and filtered bursts of noise from a 15‑bit shift register, the way the old sound chips made it.
The notes are scheduled on the audio clock about 0.2 seconds ahead, so a late frame never makes the music stutter. It plays only while the clock of the game runs, stops at a pause and resumes where it was, and in the last 30 seconds it plays 15% faster. Measured in the browser, the sound effects stand about 6.8 dB above the music, so they always come through.
62 tests and a record to beat.
The tests run in the browser with a harness of 49 lines written for the project, on the very modules of the game. The long ones do not check a final result only: after every single tick of ten games played by an automatic player and three games of random key mashing, they check that no full row is left, the falling piece never overlaps the stack, the score equals the sum of its parts and never goes down, the level matches the lines, and every piece is counted once by the Statistics. The automatic player scores the board with six features introduced by Pierre Dellacherie, weighted as published by Islam El‑Ashi, and plays through the real inputs, exactly like a human.
| Test file | Tests | What they check |
|---|---|---|
| engine | 13 | generator, bag, rotations and kicks, line clears, gravity |
| game | 20 | spawning, key repeat, drops, lock delay, hold, T‑spins, the end |
| scoring | 10 | every row of the points table, chains and combos |
| share | 11 | links, fingerprints, damaged links, the replayer, the top 10 |
| music | 4 | the score, the bars, the second voice |
| stress | 4 | full games with every rule at every tick, replays identical |
My record game, 27,638 points, is public as a link. In 180 seconds it placed 121 pieces, 0.67 per second, with 831 inputs and six PolyFit!s. The level multiplies every clear, so the score grew faster as the game went on: 3,530 points in the first minute, 11,796 in the second and 12,312 in the third, where a single PolyFit! at level 4 was worth 3,200 points.
The page of the game is 31 files, 183 KB, about 64 KB compressed. A whole three-minute game replays in 1.0 ms, the median of 20 replays of the record game in Chrome on an Apple M5 Max: a Hall of Fame of a hundred games replays in about a tenth of a second.
Three minutes. One score.
The game, its 62 tests, the music and the 19‑page report are on GitHub, and my record is still waiting.

