I wanted a lightweight version of 2048 that runs from a single index.html with zero build tools. The result is a compact, modular implementation in plain HTML/CSS/JS — easy to read, easy to tweak, and fun to extend.
👉 Live demo: https://seryogakovalyov.github.io/2048-js-classic/
👉 GitHub: https://github.com/seryogakovalyov/2048-js-classic
📦 Project structure
index.htmlcontains the HUD (score/best, restart) and the board container.js/main.jsis the entry point: initialize state → render → bind input.- Core mechanics live in
js/core/*, rendering helpers injs/components/*, and matrix utilities injs/utils/*.
Simple, modular, dependency-free.
🔢 Representing the board
The game state is a 4×4 matrix plus meta fields:
export const state = {
grid: [],
score: 0,
best: 0,
status: "playing",
animations: [],
lastAdded: null
};
A new game resets the grid and places two random tiles.
↔️ Movement and merging
The core logic implements “move left.”
Other directions are rotations/transposes of the same process.
Here’s the main pure function:
export function moveRowLeft(row) {
let nonZero = row
.map((value, index) => ({ value, index }))
.filter(({ value }) => value !== 0);
let result = [];
let scoreGain = 0;
let mergedTo2048 = false;
let moves = [];
for (let i = 0; i < nonZero.length; i++) {
const current = nonZero[i];
const next = nonZero[i + 1];
if (next && current.value === next.value) {
const merged = current.value * 2;
const toCol = result.length;
const fromCol = next.index;
result.push(merged);
scoreGain += merged;
if (merged === 2048) mergedTo2048 = true;
moves.push({
from: fromCol,
to: toCol,
value: merged,
merged: true
});
i++;
} else {
result.push(current.value);
moves.push({
from: current.index,
to: result.length - 1,
value: current.value,
merged: false
});
}
}
while (result.length < 4) result.push(0);
return { row: result, scoreGain, mergedTo2048, moves };
}
At the grid level, this runs on each row, applies rotations for up/down/right, and detects win/lose states.
🎨 Rendering & animations
- Background consists of 16 static cells.
- Actual tiles render in a separate
.tileslayer with absolute positioning. - Each tile gets CSS variables
--xand--yfor its start position, thenrequestAnimationFrameupdates them to trigger smooth transitions. - Newly spawned and merged tiles use
new/mergeclasses for pop animations.
Smooth motion with minimal JS.
🎮 Input: keyboard + touch
initInput handles:
- Arrow keys and WASD
- Swipes (simple dx/dy measurement from
touchstart→touchend)
All inputs call move(direction) followed by render().
💾 Persistence
Every state mutation is saved in localStorage:
gridscorebeststatus
Reloading the page restores the session automatically.
💅 Styling
css/styles.css defines:
- the grid using CSS Grid,
- tile colors per value,
- absolute-positioned animated tiles,
- win/lose overlays.
🚀 Running the project
Open index.html directly, or run a tiny local server:
npx serve .
# or
python -m http.server 8000
Visit http://localhost:8000.
🔧 Possible upgrades
- Undo or move history
- Reset best score button
- Tests for
moveRowLeftand rotation helpers - Additional themes or larger board sizes
Leave a Reply