TinyGames · how it works
Three pieces are offered. Drop them anywhere they fit on the 8×8 board, and any row or column you complete clears. There is no rotation and no gravity, so the only decision is where to put things. It ends when none of the three will go anywhere.
Open index.html. No build step, no dependencies.
tessera/
index.html markup
style.css the frame around the canvas
js/engine.js copied unchanged from Overdrive - the seventh game on it
js/blocks.js the board and the pieces - no pixels, no engine, no DOM
js/tessera.js the playable layer: dragging, drawing, scoring
test-blocks.js node test-blocks.js
assets/ Kenney Puzzle Pack II, CC0
Work out the full rows, empty them, then look for full columns — and every column that happened to cross a cleared row is no longer full, because you just emptied a cell in it. The player sees fewer lines go than they can plainly see are complete, and there is nothing on screen to explain it. It reads as a rendering glitch rather than a rules error, which is what makes it hard to chase.
The test is one board:
row 3 full, column 5 full, crossing at (3,5)
That is 8 + 8 − 1 = 15 cells. Done in two passes it is 8. The implementation collects both
lists before emptying anything, and the test asserts 15.
It is can any of these three pieces still go anywhere. Get it wrong in the safe direction and the game ends with a legal move on the table; get it wrong the other way and it never ends at all.
anyMove is cross-checked against an independently written placer — one that builds the
piece into a copy of the board by hand and checks nothing was overwritten — over 3,000
random boards at every density from empty to full:
$ node test-blocks.js
49 passed, 0 failed
No disagreements, and the sample is asserted to contain both live and dead boards, so it cannot pass by only ever seeing one answer.
A hand of three that cannot be played at all is an instant, unearned loss, so the deal gets six attempts at producing one that fits somewhere.
Deliberately only six. Rigging the deal until the game is always survivable would remove the losing condition altogether, and the point of the board filling up is that eventually nothing fits. On a roomy board the tests confirm a dealt hand is always playable; on a board with one square left the deal is allowed to give up rather than hang.
The bag leans hard towards small pieces — a domino is drawn about nine times as often as the 3×3 block — and the test asserts that ratio, because a bag weighted evenly jams the board almost immediately.
While you drag, the piece shows the exact cells it will occupy, and if those cells would complete a row or column, the whole of that line lights up too. A clear becomes something you can see coming rather than something you find out about afterwards. Illegal positions go red rather than silently refusing on release.
Two details that only matter on a phone:
Two simulated players, 25 games each, differing only in how they choose a square:
| pieces placed | score | |
|---|---|---|
| drops anywhere legal | 14 | 131 |
| packs tightly and takes clears | 47 | 1,493 |
Both are medians. The thinking player prefers placements that complete lines, then keeps the board empty and avoids leaving single-cell holes — which is roughly the advice you would give a person.
1 a cell placed, 10 a cell cleared, and consecutive clearing moves add 50% each to the
multiplier. Breaking the chain resets it. Best score is kept in localStorage.
Colour carries no meaning here — every colour clears the same way — so unlike the other games in this set nothing is hidden from anyone who cannot tell the tiles apart.
| Drag a piece from the tray onto the board | place it |
| Release outside the board | put it back |
| R | new board |
A piece that has nowhere left to go is dimmed in the tray.
placements() already returns every
legal square for a piece.Board takes a size and the rules do not care.Advertisement