// 共享 2048 核心引擎(前后端共用,保证回放一致) // 严格复刻 deploy/index.html 的 2048 规则(含锁定/撤销/洗牌/交换/锤子), // 所有随机来自种子化 PRNG(mulberry32),不再用 Math.random,从而后端可确定性复现。 (function (global, factory) { if (typeof module === "object" && module.exports) module.exports = factory(); else global.Core2048 = factory(); })(typeof self !== "undefined" ? self : this, function () { function mulberry32(a) { return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } class Game2048 { constructor(seed) { const s = typeof seed === "bigint" ? Number(seed & 0xffffffffn) : (seed >>> 0); this.rng = mulberry32(s); this.board = new Array(16).fill(0); this.score = 0; this.lockSet = new Set(); this.history = []; this.gameOver = false; this.hasReached2048 = false; // 双倍积分卡(道具6):激活后本局所有合并得分 ×2。 // 必须由 actionLog 的 {t:'double'} 驱动,保证前后端回放一致。 this.doubleScore = false; this._spawn(); this._spawn(); } _spawn() { const empty = []; for (let i = 0; i < 16; i++) if (this.board[i] === 0) empty.push(i); if (empty.length === 0) return; const idx = empty[Math.floor(this.rng() * empty.length)]; this.board[idx] = this.rng() < 0.9 ? 2 : 4; } _checkGameOver() { if (!this.hasReached2048) { for (let i = 0; i < 16; i++) if (this.board[i] >= 2048) this.hasReached2048 = true; } for (let i = 0; i < 16; i++) if (this.board[i] === 0) return; for (let r = 0; r < 4; r++) { for (let c = 0; c < 4; c++) { const v = this.board[r * 4 + c]; if (c < 3 && v === this.board[r * 4 + c + 1]) return; if (r < 3 && v === this.board[(r + 1) * 4 + c]) return; } } this.gameOver = true; } // 仅左右移动考虑 lockSet(与前端 moveLeft/moveRight 一致) _moveLeftRight(reverse) { let moved = false, inc = 0; for (let r = 0; r < 4; r++) { const lockedPositions = []; for (let c = 0; c < 4; c++) if (this.lockSet.has(r * 4 + c)) lockedPositions.push(c); const nonLocked = []; for (let c = reverse ? 3 : 0; reverse ? c >= 0 : c < 4; reverse ? c-- : c++) { if (!this.lockSet.has(r * 4 + c) && this.board[r * 4 + c] !== 0) nonLocked.push(this.board[r * 4 + c]); } const merged = []; for (let i = 0; i < nonLocked.length; i++) { if (i < nonLocked.length - 1 && nonLocked[i] === nonLocked[i + 1]) { const val = nonLocked[i] * 2; merged.push(val); inc += val; i++; } else merged.push(nonLocked[i]); } const newRow = [0, 0, 0, 0]; for (const c of lockedPositions) newRow[c] = this.board[r * 4 + c]; let mergeIdx = 0; for (let c = reverse ? 3 : 0; reverse ? c >= 0 : c < 4; reverse ? c-- : c++) { if (!this.lockSet.has(r * 4 + c)) { newRow[c] = mergeIdx < merged.length ? merged[mergeIdx++] : 0; } } for (let c = 0; c < 4; c++) { if (this.board[r * 4 + c] !== newRow[c]) moved = true; this.board[r * 4 + c] = newRow[c]; } } return [moved, inc]; } _moveUpDown(down) { let moved = false, inc = 0; for (let c = 0; c < 4; c++) { const lockedRows = []; for (let r = 0; r < 4; r++) if (this.lockSet.has(r * 4 + c)) lockedRows.push(r); const nonLocked = []; for (let r = down ? 3 : 0; down ? r >= 0 : r < 4; down ? r-- : r++) { if (!this.lockSet.has(r * 4 + c) && this.board[r * 4 + c] !== 0) nonLocked.push(this.board[r * 4 + c]); } const merged = []; for (let i = 0; i < nonLocked.length; i++) { if (i < nonLocked.length - 1 && nonLocked[i] === nonLocked[i + 1]) { const val = nonLocked[i] * 2; merged.push(val); inc += val; i++; } else merged.push(nonLocked[i]); } const newCol = [0, 0, 0, 0]; for (const r of lockedRows) newCol[r] = this.board[r * 4 + c]; let mergeIdx = 0; for (let r = down ? 3 : 0; down ? r >= 0 : r < 4; down ? r-- : r++) { if (!this.lockSet.has(r * 4 + c)) { newCol[r] = mergeIdx < merged.length ? merged[mergeIdx++] : 0; } } for (let r = down ? 3 : 0; down ? r >= 0 : r < 4; down ? r-- : r++) { const idx = r * 4 + c; if (this.board[idx] !== newCol[r]) moved = true; this.board[idx] = newCol[r]; } } return [moved, inc]; } move(dir) { if (this.gameOver) return false; let moved = false, inc = 0; if (dir === "left") [moved, inc] = this._moveLeftRight(false); else if (dir === "right") [moved, inc] = this._moveLeftRight(true); else if (dir === "up") [moved, inc] = this._moveUpDown(false); else if (dir === "down") [moved, inc] = this._moveUpDown(true); if (moved) { this.history.push({ board: [...this.board], score: this.score }); if (this.history.length > 3) this.history.shift(); this.score += this.doubleScore ? inc * 2 : inc; this._spawn(); this.lockSet.clear(); this._checkGameOver(); } return moved; } undo() { if (this.history.length === 0) return false; const prev = this.history.pop(); this.board = [...prev.board]; this.score = prev.score; return true; } shuffle() { const nums = this.board.filter((v) => v !== 0); for (let i = nums.length - 1; i > 0; i--) { const j = Math.floor(this.rng() * (i + 1)); [nums[i], nums[j]] = [nums[j], nums[i]]; } let ptr = 0; for (let i = 0; i < 16; i++) this.board[i] = ptr < nums.length ? nums[ptr++] : 0; } exchange(i, j) { if (i < 0 || i > 15 || j < 0 || j > 15 || i === j) return; const t = this.board[i]; this.board[i] = this.board[j]; this.board[j] = t; } hammer(i) { if (i < 0 || i > 15 || this.board[i] === 0) return; this.board[i] = 0; this.gameOver = false; } lock(cells) { for (const c of cells) this.lockSet.add(c); } // 双倍积分卡(道具6):激活后本局后续所有合并得分 ×2(一次性激活,持续到本局结束) double(on) { this.doubleScore = on === undefined ? true : !!on; } getState() { return { board: [...this.board], score: this.score, gameOver: this.gameOver, doubleScore: this.doubleScore, }; } } // 回放:seed + actions -> 最终状态。 // actions 元素:{t:'move',d} | {t:'undo'} | {t:'shuffle'} | {t:'exchange',i,j} // | {t:'hammer',i} | {t:'lock',cells} | {t:'double'} // 其中 {t:'double'} 为道具6「双倍积分卡」激活点:其后的合并得分 ×2。 // ⚠️ 前端 index.html 的 move() 必须与此处完全同构,否则回放分数不一致会被后端拒签。 function replay(seed, actions) { const g = new Game2048(seed); for (const a of actions) { if (a.t === "move") g.move(a.d); else if (a.t === "undo") g.undo(); else if (a.t === "shuffle") g.shuffle(); else if (a.t === "exchange") g.exchange(a.i, a.j); else if (a.t === "hammer") g.hammer(a.i); else if (a.t === "lock") g.lock(a.cells || []); else if (a.t === "double") g.double(true); } return g.getState(); } return { Game2048, replay, mulberry32 }; });