Classic 2048 puzzle, written in vanilla JavaScript with no build step —
just open index.html (or play the live demo) and you have a working game.
Live demo → https://alexdreamien.github.io/html5-2048/
Animated GIF of gameplay goes here — replace
docs/demo.gifwith a screen recording once the game is deployed.
- 4 x 4 board, classic 2048 rules. Two starting tiles, one new tile after every move (value 2 with 90% probability, 4 with 10%).
- Keyboard and touch input: arrow keys / WASD on desktop, swipe on mobile. Works equally well in either form factor.
- Configurable win target — 1024 by default, 2048 selectable. After reaching it you can either start a new game or keep playing for a higher score.
- Persistent best score stored in
localStorage. - Smooth animations — tiles slide via CSS transitions, merges pop, spawns scale in.
- Detects game over (no moves available) and game won (target reached).
- HTML5, ES modules, vanilla CSS
- No frameworks, no bundler, no build step for the game itself
- Vitest — used only to test the pure game-logic module; the game ships without it
index.html Markup and entry point
css/style.css Theme, layout, tile colours, animations
js/game.js Pure game logic (no DOM): move, slideRowLeft, canMove,
hasReachedTarget, spawnTile. Returns a `transitions` array
that drives slide animations in the renderer.
js/render.js Renderer class. Owns absolutely-positioned tile DOM
nodes, animates moves via CSS transition on transform,
merges via a "pop" keyframe, spawns via an "appear"
keyframe.
js/input.js Keyboard + touch -> direction callback (24-px swipe
threshold, ignores multi-touch).
js/main.js Wires the three layers together, persists state.
tests/ Vitest suite over game.js.
The split between pure logic and DOM is the key design choice: every non-trivial behaviour (slide-and-merge, no double-merging, win/lose detection, transition computation) is covered by unit tests because the logic doesn't touch the DOM. The renderer and input handler are thin adapters around it.
The game has no dependencies, but ES modules need to be served over HTTP (file:// blocks them). Any static server works:
# Python
python -m http.server 8080
# Node (one-off)
npx http-server -c-1 -p 8080 .Then open http://localhost:8080.
npm install
npm testVitest covers:
- Single-row slide & merge (gap closing, no chain-merging, no double-merge within a turn)
- Move correctness in all four directions;
movedflag false when nothing changes; no mutation of input - Merge coordinate and transition reporting (used by the renderer)
- Game over (full board, no equal neighbours) vs. still-playable
- Win-target detection
- Spawn probability split (2 / 4) and full-board no-op behaviour
CI runs the same suite on every push and pull request — see
.github/workflows/ci.yml. On push to main,
.github/workflows/pages.yml redeploys the
live demo, so the URL above always reflects the latest commit.
By design: no online leaderboard or backend, no accounts, no sound, no undo, no field sizes other than 4 x 4. The scope deliberately stays at "classic 2048 you can open in a browser".
MIT.
