Taps can be unreliable inside this app's built-in browser. For the full experience, use its menu to open this page in your browser.
Skip to content

The 2048 Bug That Only Shows In The Score

A devlog by Neeraj Kotwani, who builds every game on Elipar.

2048 is four years of other people's clones away from being interesting to write, which is exactly why it is worth saying what actually goes into one. The rules fit in a sentence. The implementation has one trap in it, and the trap is quiet.

Here is the trap. Take a row of four twos. Swipe left. What should happen?

The answer is [4, 4, 0, 0]. Two separate merges, two tiles. The wrong answer — and it is very easy to write — is [8, 0, 0, 0]: the first pair merges into a 4, and then that brand-new 4 immediately merges with the 4 behind it. A tile that has just been created merges again in the same move, which the real game does not allow.

Why it survives a playtest

If this were a crash it would be fixed in an hour. It is not. An 8 is a perfectly ordinary tile to see on a 2048 board. The animation looks fine. The grid stays legal. You can play a hundred moves without noticing anything.

What is wrong is the score, and only the score. A merge pays out the value of the tile it creates, so the bad path pays 4 and then 8, twelve points, where the correct path pays 4 and 4, eight points. Every row of four matching tiles quietly overpays. Nobody is going to catch that by feel, and a leaderboard built on it would rank people by how often they happened to line up four of a kind.

So the merge rule is not a detail you get to be casual about. It is the thing the entire score rests on.

One function, and it moves past what it just made

The whole engine is a single pure function that collapses one line. It drops the zeroes, walks the remaining values, and whenever two neighbours match it doubles the first, removes the second, and then steps past the result before looking again. That step is the entire fix. A tile that has just been formed is never re-examined on the same pass, so it cannot merge twice.

Four twos therefore go: merge at the front, step forward, merge again, stop — two fours. And [2, 2, 4] gives [4, 4, 0] rather than collapsing all the way to an 8, which is the same bug wearing different clothes.

Only one direction is ever implemented. A row read left-to-right and a column read bottom-to-top are the same problem once you order the cell indices so that position zero is the edge the tiles are collapsing toward. Every swipe builds that ordering and hands it to the same function. Four directions, one piece of logic, so a bug cannot exist in "up" but not in "left".

The tiles genuinely move

The function returns three things: the new line, the points earned, and — the one that matters for animation — a record of which original positions ended up at each new position.

With that trace, the board is not repainted after a move. Each surviving tile element is the same DOM node it was before, moved to its destination with a transform. When two tiles merge, both are moved onto the same square and the one underneath is removed after the transition finishes. Only then does the new random tile appear.

Repainting would look almost identical and would be much less code, so there is a test that asserts the original element survives a move rather than asserting the board merely ends up correct. Without that, somebody could "simplify" this into a repaint and every other test would still pass.

Three test failures that were all mine

Worth recording, because in each case I was briefly convinced the game was broken.

I hand-wrote an expected result for a downward swipe and had two eights merging. They were in different columns. A vertical move can never merge them, and recomputing all four columns by hand showed the engine had been right the whole time.

I built a "dead board" to test game over, and it was not dead — two identical rows left a live vertical pair. Worse, since every move spawns a new tile, a genuinely dead board has to stay dead for both possible spawn values. I threw the construction away and reached game over by playing in circles until it happened, which is both more honest and shorter, and doubles as proof the fail state is reachable at all.

And a layout check skipped the mute button as "hidden", because I was testing a property that is always null for anything positioned fixed. It had passed in another suite that asked a different question, and that disagreement is what gave it away.

Banking, and what does not pay

Score reports each time you reach a new highest tile from 64 upward, rather than only at game over, so a session that ends early still records what it earned. The site keeps the highest figure it is ever sent, so a longer run can only ever raise your best.

Checked against play that should not pay: sliding around without ever merging anything reports nothing at all, and a game that ends on zero points reports nothing either.

One more thing, small but it was a real decision. On the win overlay the bright button used to be PLAY AGAIN — which throws the run away — while KEEP GOING, the thing somebody who just reached 2048 actually wants, sat underneath it in grey. They are swapped now. A screenshot caught that; no assertion ever would have.

2048 is free in a browser, no account, no download. Four twos make two fours.

We use cookies to keep games running and, once ads are live, to show relevant content. See our Privacy Policy for details.