---
title: "2048 — ADI Store"
description: "The classic 2048 sliding-tile puzzle, frontend-only, with slide/merge animations and a best score kept in localStorage."
url: https://withadi.dev/store/game-2048/
---

[All apps](https://withadi.dev/store/)

![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'><rect width='48' height='48' rx='10' fill='%23161616'/><rect x='6' y='6' width='16' height='16' rx='3' fill='%23EEE4DA'/><rect x='26' y='6' width='16' height='16' rx='3' fill='%23F2B179'/><rect x='6' y='26' width='16' height='16' rx='3' fill='%23F67C5F'/><rect x='26' y='26' width='16' height='16' rx='3' fill='%23E8532A'/></svg>)

# 2048

`1.0.1`

The classic 2048 sliding-tile puzzle, frontend-only, with slide/merge animations and a best score kept in localStorage.

game 2048 puzzle

[Install](http://app.adi/marketplace/store/game-2048?install=1)[Download ADI](https://withadi.dev/#download)Opens in your panel, which asks where it goes Free — then this installs in one click

Already have ADI? [Install it from your panel](http://app.adi/marketplace/store/game-2048?install=1).

## What's included

As published. What actually installs is read off the pinned commit's own tree.

- **Dashboards**

  - game-2048 — The game itself.

## About

## 2048

The classic 2048, as one dashboard — a 4×4 sliding-tile puzzle with real slide/merge animations, a best score that survives a reload, and nothing on the backend: every rule lives in the browser.

This bundle carries one element:

| kind | name | what it is |
| --- | --- | --- |
| dashboard | `game-2048` | the game itself — frontend only, a one-route backend stub for the dashboard contract |

Install it:

```
adi-mono marketplace install <marketplace>/game-2048
```

It arrives archived, same as every dashboard — start it from the Dashboards page, or:

```
adi-mono marketplace start <marketplace>/game-2048/dashboards/game-2048
```

Then open it by its hostname. No secret, no project, nothing else to configure.

### Why no backend logic

2048's entire rulebook — sliding, merging, spawning, win/lose detection — runs client-side; there is nothing for a server to compute or to keep private. The backend half of this dashboard exists only because the dashboard contract (`guides/dashboards.md`) needs one: it is the template's own unmodified `backend/index.ts`, answering `/api/health` so the page's status dot has something to poll, and nothing more. No route file was added.

### How it's built

`frontend/modules/game.ts` is the panel; `frontend/modules/.logic.ts` is the whole rulebook — pure functions over a plain 4×4 number grid, no DOM, no randomness that isn't passed in as an argument. The dot in its name keeps it off the shell's auto-mounted panel list (`guides/ dashboards.md`'s module discovery lists every `modules/*.ts` that doesn't start with `.`); the panel reaches it at runtime as a same-origin `./.logic.js` import, which the frontend server resolves straight back to this file.

The rules:

- **merge once per move**: scanned left-to-right (or the equivalent for the direction moved), a

tile that has already merged this move never merges again — `[2,2,2,0]` slides to `[4,2,0,0]`, not `[4,4,0,0]`; `[2,2,2,2]` merges as two separate pairs, `[4,4,0,0]`.

- **spawn**: after every move that actually changes the board, one new tile lands on a random

empty cell — 2 nine times out of ten, 4 the rest.

- **win**: reaching 2048 offers *Keep going* as well as *New game* — the board keeps playing, and

nothing about reaching 2048 a second time re-fires the banner.

- **game over**: every one of the four directions is checked against the current board; only when

none of them would change anything is it actually over, not merely "no empty cell left" (a full board that can still merge is not over).

`frontend/modules/game.ts` renders this onto a board of real DOM tiles kept alive across moves — a tile that slides keeps its element and just gets new `--x`/`--y` custom properties (a CSS `transition` on `left`/`top` does the sliding); a tile consumed by a merge is removed once the slide finishes and a freshly minted tile appears at the destination with a short "pop" animation; a spawned tile gets a "scale in" animation. The board is drawn as CSS percentages off one `SIZE` constant, so it holds together at any panel width, and the same palette is used in light and dark (the template's own ADI tokens already cover both — see `[data-theme="light"]` in `frontend/ index.html`).

Input: `ArrowLeft/Right/Up/Down` and `a`/`d`/`w`/`s` (case-insensitive, ignored while any text input or textarea elsewhere on the page has focus), plus a touch swipe (24px minimum, whichever axis moved further wins ties). Best score is `localStorage["adi.game2048.best"]`, scoped to the browser, not the backend — there is nothing server-side to lose it with.

### Checking it works

```
bun test test/game-logic.test.ts
```

19 checks against `frontend/modules/.logic.ts` directly — no DOM, no server: every merge shape (`mergeLine`), all four directions (`move`), the spawn distribution with an injected `rng` (`addRandomTile`, `randomTileValue`), and game-over detection against both a mid-game and a fully-deadlocked board (`isGameOver`).

A second, optional check covers what the logic tests structurally can't: whether the win/game-over overlay is actually invisible when it should be. `.g2048__overlay` sets `display: flex` (to centre its card), and an author stylesheet rule always outranks the UA's own `[hidden] { display: none }` at equal specificity — so a missing `.g2048__overlay[hidden]` override renders the overlay (and its "New game" button) permanently, over the board, regardless of the `hidden` attribute. Neither `bun:test`'s DOM nor happy-dom compute CSS cascade, so this needs a real browser:

```
bun add -d playwright && bunx playwright install chromium # one-time, not needed to play the game
bun test test/overlay-visibility.test.ts
```

It skips (with a warning, not a failure) if Playwright isn't installed — the dashboard itself has no dependency on it.

### Licence

MIT.

## Where it comes from

Installing clones this repository at this commit. A publisher who pushes something else afterwards changes nothing about what you already read.

- **Repository** — [github.com/adi-family/mp-game-2048](https://github.com/adi-family/mp-game-2048.git)
- **Commit** — `dec70b6ddee3cdf3c3c453395e0cb09f957829a0`
- **Branch** — `main`
- **Address** — `store/game-2048`
- **Contains** — dashboard
- **Manifest** — [raw.githubusercontent.com/adi-family/marketplace/main/apps/marketplace.json](https://raw.githubusercontent.com/adi-family/marketplace/main/apps/marketplace.json)

---
[ADI](https://withadi.dev/)

- [Download](https://withadi.dev/#download)
- [Store](https://withadi.dev/store/)
- [Docs](https://docs.withadi.dev/)
- [Source](https://github.com/adi-family/mono)
- [Licence](https://github.com/adi-family/mono/blob/main/LICENSE)
- [Changelog](https://github.com/adi-family/mono/blob/main/CHANGELOG.md)
- [Security](https://withadi.dev/security)
- [Privacy](https://withadi.dev/privacy)
- [Red team](https://withadi.dev/red-team-engagements)
- [Terms](https://withadi.dev/terms)
- [Support](https://withadi.dev/support)
- [Startups](https://withadi.dev/startups)
- [Partners](https://withadi.dev/#partners)
- [Partner portal](https://partners.withadi.dev/)
- [info@withadi.dev](mailto:info@withadi.dev)
- [Contact](mailto:info@withadi.dev)
