2048: collapsing four directions into one operation

Sliding, merging and scoring only need to be written once; the other three directions reuse it through transpose and reverse. The hard part is that each tile may merge at most once per move.

Normalise first, implement once

Writing slide logic four times produces four nearly identical implementations with slightly different details. The cheaper and more reliable approach is to implement only “slide and merge one row to the left”, and for the other directions transform the grid, call that same code, and transform back.

Left and right differ by reversing a row; up and down transpose first and then reuse the horizontal path. Transformations are pure array operations with no game rules in them, so they can be verified on their own.

Merging is harder than sliding

The first step of sliding is dropping zeros so the remaining numbers pack to the left. The part that goes wrong is merging: each tile may take part in at most one merge per move.

So [2, 2, 4] must produce [4, 4], never [8], because the second 2 was already consumed by the first and cannot merge again with the freshly formed 4. Likewise [2, 2, 2, 2] is [4, 4], not [8].

In code, scan for an equal pair, merge it, and advance the index one extra step to skip the tile that was eaten:

if (i + 1 < nums.length && nums[i] === nums[i + 1]) {
  const merged = nums[i] * 2;
  out.push(merged);
  gained += merged;
  i += 1; // skip the one that was consumed
}

That single increment is the one line this game cannot lose. Remove it and the game still runs, but on certain boards it produces 8s and 16s that should not exist, and the player will struggle to say which move was wrong.

Spawn only after the board actually moved

Another easily missed rule: if a slide changes nothing, because every tile is already packed against that edge, no new tile should spawn and the move should not count as a turn. Without that check, holding a direction key farms fresh tiles indefinitely.

New tiles only appear in empty cells, and the choice must be reproducible. Randomness comes from a seeded mulberry32, so pinning the seed in the test suite pins the spawn order too.

Game over

The game ends when there are no empty cells and no direction can produce a merge. Neither condition alone is enough: a full board with adjacent equal tiles is still playable.

Why the test suite catches it

All of these rules live in pure functions over plain arrays with no DOM. The assertions target exactly those boundaries: [2, 2, 4] must give [4, 4], four 2s must give two 4s, an invalid move must not spawn a tile, and a fixed seed must produce a fixed spawn order. On screen these bugs only look like odd numbers; an assertion pins them down.

← Back to all posts

Comments

…