2048看似简明:同值之块相合,击碎至2048。然一经着手实现,方知滑动合并之术自有隐微之规,动画之效非易以块之更替而仿。是文详述约三百行之纯JS实现:一为纯板引擎,二为无链合并之恒常,三为CSS过渡动画,不假
requestAnimationFrame。
🌐 Demo: https://sen.ltd/portfolio/2048/
📦 GitHub: https://github.com/sen-ltd/2048
滑动一行
其要义在于“左移一行,合并相邻相等。”此乃全貌:
export function slideRow(input) {
// 1. drop zeros (compact)
const filtered = input.filter((v) => v !== 0);
// 2. walk left-to-right, merging adjacent equal pairs
const out = [];
let i = 0;
while (i < filtered.length) {
if (i + 1 < filtered.length && filtered[i] === filtered[i + 1]) {
out.push(filtered[i] * 2); // merge
i += 2; // consume both
} else {
out.push(filtered[i]);
i++;
}
}
// 3. pad right with zeros
while (out.length < 4) out.push(0);
return out;
}
十五行。兼理[2, 2, 0, 0] → [4, 0, 0, 0]与[2, 0, 0, 2] → [4, 0, 0, 0]。要义在于先于扫描——若就地扫描,必处隙于所至.
"无链合并"之规
此乃制衡愚拙之规.
[2, 2, 2, 0] → ?
一无所知的“边寻边合并”之法得[4, 2, 0, 0] — 确然无误。然若任其不断合并:
[4, 2, 2, 0] → [4, 4, 0, 0] // correct
→ [8, 0, 0, 0] // WRONG, this is the chain-merge bug
已于此回合合并之瓦片不可再合,须待下一回合。此乃2048之实规.
吾slideRow()偶得其当,盖因i += 2绕过合并之对,直指未触之瓦。然尔当以显验定之,盖此类规易在重构之际破也:
test("a tile cannot merge twice in one move", () => {
// [4, 2, 2, 0] → [4, 4, 0, 0], not [8, 0, 0, 0]
assert.deepEqual(slideRow([4, 2, 2, 0]).row, [4, 4, 0, 0]);
});
test("three same tiles merge only the left pair (no chain)", () => {
// [2, 2, 2, 0] → [4, 2, 0, 0], not [8, 0, 0, 0]
assert.deepEqual(slideRow([2, 2, 2, 0]).row, [4, 2, 0, 0]);
});
此规之所以重者:三连四连于实棋常见。若误行连并,数进"真2048"之速,倍之而数超,玩家觉有异,然莫知其故。
四向之通
若"左移"为唯一诚施之向,余皆复用:
- 右:逆行,滑行,复逆行
- 上:同左,然施于列而非行
- 下:列,逆之
export function slide(board, direction) {
if (direction === "left" || direction === "right") {
for (let r = 0; r < SIZE; r++) {
let cells = row(board, r);
const reversed = direction === "right";
if (reversed) cells = [...cells].reverse();
const { row: out, gained: g } = slideRow(cells);
const final = reversed ? [...out].reverse() : out;
setRow(next, r, final);
gained += g;
}
} else if (direction === "up" || direction === "down") {
// same shape, columns
}
}
:四四之板,取列,行滑,复贴之费,无足论。勿优之。
以RNG为引
经典之2048生成法,九十之机出二,十之机出四于随机空格。勿直唤Math.random:
export function spawnTile(board, rng) {
const empties = [];
for (let i = 0; i < board.length; i++) {
if (board[i] === 0) empties.push(i);
}
if (empties.length === 0) return -1;
const idx = empties[Math.floor(rng() * empties.length)];
board[idx] = rng() < 0.9 ? 2 : 4;
return idx;
}
测试得注入定数之RNG:
function mockRng(values) {
let i = 0;
return () => values[i++ % values.length];
}
test("places a 4 when rng() >= 0.9", () => {
const b = emptyBoard();
spawnTile(b, mockRng([0, 0.95])); // [position, value-roll]
assert.equal(b[0], 4);
});
制作者仅作const rng = Math.random;。一法依赖注入,测试可预见性无穷增益。
动画以CSS过渡,非requestAnimationFrame。
游戏动画之典法,乃"用requestAnimationFrame,逐帧插值位置。"至2048之量级,可容纯CSS过渡。transform/left/top,竟可全然越rAF之循环。
.tile {
position: absolute;
width: calc((100% - 3 * var(--gap)) / 4);
height: calc((100% - 3 * var(--gap)) / 4);
left: calc(var(--col) * ((100% - 3 * var(--gap)) / 4 + var(--gap)));
top: calc(var(--row) * ((100% - 3 * var(--gap)) / 4 + var(--gap)));
transition: left 130ms ease, top 130ms ease, transform 130ms ease;
}
位之驱动,本乎CSS之变数--row与--col。JS惟更此二者耳:
function placeAt(el, cellIdx) {
const r = Math.floor(cellIdx / SIZE);
const c = cellIdx % SIZE;
el.style.setProperty("--row", String(r));
el.style.setProperty("--col", String(c));
}
于幻动之际,瓦片DOM之元素为未毁 —吾等惟易其--row / --col之值,而浏览器自为之帧插值。JS主线程得闲息于全部130毫秒之期。
至于合并之术,其要在于:
- 使双源瓦片俱移至目标格(浏览器自为之动画,无偿)。
-
await new Promise(r => setTimeout(r, 130))俟过渡之毕。 - 除二源元
- 增新瓦,值倍之,
pop动效随之
.tile.pop {
animation: pop 160ms ease;
}
@keyframes pop {
0% { transform: scale(0.85); }
50% { transform: scale(1.12); }
100% { transform: scale(1); }
}
六行CSS,得二瓦相撞而合之悦
建筑之艺
board.js ← board logic (no DOM, no audio, no RNG dependency — Node-testable)
render.js ← DOM + animation
app.js ← UI glue (keyboard, touch, score, theme)
board.js 既无document,亦无window,更无Math.random。故Node之内置测试运行器,不假浏览器而验其二十一例:
npm test # 21 tests, all pass
所系之箭,惟向app.js → render.js → board.js。尔若增一变体(如六六之棋,负值之瓦,戏链合并之式,凡此种种),则尔更变之。board.js與航測。動畫層固若金湯,毫釐不動。
結論
- 2048計算尺,體積緊湊 + 自左至右掃描 + 跳過合併之對 — 約十五行。
- 每回合無瓦片合併二次乃制勝之鋒,凡愚鈍之實施,皆可窺破;須明確測試三連及四連之境。
- 推及四方 = 复用"左移",兼行行列抽提,可择
reverse(). - RNG为论据使产卵之行于试中定然,而无所费。
- 为棋盘动画,CSS过渡与CSS变量用于定位较之JS动画之循环,其更纤小而滑润。
此乃SEN LLC(东京)之OSS组合第241号。吾辈持续运微利刃:https://sen.ltd/portfolio/













