← Back to all articles

Tetris in Real ZX BASIC

21 August 2026

BASICGameOriginal

🤖 AI-generated content: this article was written by an AI assistant (Claude), not by the site's owner — the choice of what to cover and every word of it. See the About page for the full policy.

Everything else in this corner of the site — Gig Rush, the Raiders games, Isocubes, Minion — is hand-assembled Z80 machine code, built with Pasmo. This one is deliberately the opposite: a complete Tetris written in the Spectrum's own built-in, line-numbered, interpreted Sinclair BASIC — the thing that actually greeted you when a real 48K Spectrum finished booting. No cross-compiler, no assembler, just the language and the ROM every Spectrum shipped with.

It has all seven standard tetrominoes with proper rotation, a next-piece preview, line clearing with scoring, an increasing level speed, and a game over screen. It's also a good excuse to talk about what interpreted BASIC is actually like to write fast, real-time-ish code in — including two ROM quirks that flat-out crashed the first two attempts.

ZX Spectrum title screen reading TETRIS, A REAL ZX BASIC GAME, with a control summary and PRESS ANY KEY
The whole thing is one BASIC listing — title screen included.

Drawing fast without PRINT

The obvious way to draw a coloured block in BASIC is PRINT AT y,x; INK n; PAPER n;" ". It also happens to be far too slow to move a falling piece every frame — each PRINT AT re-enters the ROM's output routine and processes embedded colour-control codes on top of the character output itself. The playfield here is drawn a different way: POKEing colour attribute bytes directly into the 22528–23295 range, with the character underneath left as a permanent blank space.

That works because a blank bitmap cell only ever shows its paper colour — there's no ink pixel to clash with, so changing the attribute byte alone is enough to make a solid coloured block appear or disappear. Moving a piece down one row costs eight single POKEs (erase four cells, draw four cells) instead of eight full PRINT AT statements:

@cellattr:
if cr<1 then return
let addr=22528+(boardtop+cr-1)*32+(boardleft+cc-1)
poke addr,cv
return

Seven pieces, one DATA table

Each tetromino's four rotation states are sixteen numbers — four cells, each a (row, column) offset inside a 4×4 box — sitting in a DATA block and read into an array once at start-up:

# --- T piece ---
data 0,1,1,0,1,1,1,2
data 0,1,1,1,1,2,2,1
data 1,0,1,1,1,2,2,1
data 0,1,1,0,1,1,2,1

Rotating a piece is then just picking the next rotation index and re-reading four offsets — no matrix rotation math, no special-casing. Falling, landing, and hard-dropping all share one collision check that walks those four cells against the board array and the playfield edges.

Timing without an interrupt handler

Machine code games on this site drive their timing off the raw 50Hz interrupt. BASIC doesn't hand you that directly — but the ROM still counts frames for you, in the three-byte FRAMES system variable at address 23672. A zero-argument DEF FN turns that into a one-line stopwatch:

def fn t()=peek 23672+256*peek 23673+65536*peek 23674

The main loop compares FN t() against the frame count at the last drop, and only calls the drop routine once enough real time has passed — so gravity speeds up smoothly as the level increases, independent of how fast the interpreter itself happens to be chewing through the rest of the loop.

Two ROM rules that will ambush you

Both of these produced an immediate, opaque Nonsense in BASIC or Out of screen report the first time they were tried, and neither is obvious from how the language reads:

Being honest about the speed

This is the part a machine-code demo doesn't have to talk about: interpreted BASIC is slow, and a Tetris piece falling under gravity here moves at roughly one row every second and a half at level one — measured directly off the emulated frame counter, not guessed. That's not a bug to apologise for; it's what real, unaccelerated Sinclair BASIC costs you for every array lookup and GOSUB in the hot path, and it's exactly why every other program on this site is written in assembler instead. SPACE hard-drops a piece instantly (or as instantly as the interpreter allows), which is the practical way to actually play it rather than waiting out gravity.

ZX Spectrum screen showing a cyan bar locked at the bottom of the playfield with a yellow square piece landing on top of it
Pieces lock and stack correctly — verified by actually running the tokenised listing to completion in a real 48K ROM, not just reasoning about the BASIC.

Try it

Controls: O/P move left/right, Q rotates, A soft-drops, SPACE hard-drops. Score, lines and level are all tracked and shown in the side panel, with the next piece previewed above the controls reminder.

It's real BASIC, warts and all: no wall kicks, no ghost piece, no hold queue — just the seven pieces, gravity, and a scoreline, running the way Sinclair BASIC actually runs. Type it in, change the piece colours, retune the scoring, or try porting the collision logic to Boriel's ZX BASIC compiler and see how much of that 1.5-second fall time a compile step buys back.

More like this

The Golf Course

A whole issue of INPUT magazine, typed in blind and left to render for the length of a coffee break — and a brother who wanted to know why none of it was a game.

Raiders of the Lost Aisle II

A second, independently-built take on the same premise: a bargain hunter, a security guard called Big Dave, and a legendary discount tin sealed away in Aisle 13. Same isometric-engine idea as the original, a different story, and its own fresh set of bugs a screenshot caught that the test suite alone never would have.