// 回响星核 / Echo Nexus — 解码共振谜题逻辑(原创核心玩法) // // 谜题规则「谐振序列 / Harmonic Sequence」: // 1. 一个 rows×cols 的发光节点阵列,每个节点带一种共振色(4 色全息色谱)。 // 2. 中央给出「目标谐振序列」target: ResonanceColor[](长度由 tier 决定)。 // 3. 玩家依次点击节点,重建该序列: // - 第 1 次点击可任意节点,但该节点颜色须 === target[0]。 // - 第 k 次点击的节点颜色须 === target[k-1],且与上一次点击节点**四邻接**(上下左右)。 // - 每个节点只能用一次(点击后变暗)。 // 4. 步数上限 = stepLimitBase + 解码技术加成;超出则谐振崩塌,可重试(晶体不损耗)。 // 5. 成功 → 释放晶体奖励 + 洞见 + 接触进度 + 可能触发叙事碎片。 // // 设计意图:把放置游戏的「等待」变成有节奏的空间规划小谜题,兼具策略与爽快感。 import type { CrystalTier, DecodeNode, DecodePuzzle, ResonanceColor, } from "./types"; import { DECODE_CONFIG, RESONANCE_COLORS } from "./config"; /** 简单可复现随机(mulberry32) */ function makeRng(seed: number) { let a = seed >>> 0; return () => { 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; }; } /** * 生成一个**保证有解**的解码谜题。 * * 算法(路径构造法): * 1. 先随机生成目标序列 target。 * 2. 在网格上**主动构造一条合法路径**:随机起点,每步走向一个未使用的四邻接节点, * 长度 = target.length。构造成功后,把路径上第 i 个节点颜色**强制设为 target[i]**。 * 3. 其余非路径节点随机填色(偏向目标色,提升视觉丰富度与多解性)。 * * 这样至少存在一条解(即构造的路径),玩家不会遇到「怎么都过不了」的死局。 * 同时由于其余节点随机,玩家可能发现别的解路径,保留探索乐趣。 */ export function generatePuzzle(tier: CrystalTier, seed?: number): DecodePuzzle { const cfg = DECODE_CONFIG[tier]; const s = seed ?? Math.floor(Math.random() * 1e9); const rng = makeRng(s); const { rows, cols, targetLen } = cfg; const total = rows * cols; // 目标序列:从 4 色中随机选 targetLen 个(允许重复,但保证至少 2 种色以增加策略) const target: ResonanceColor[] = []; for (let i = 0; i < targetLen; i++) { target.push(RESONANCE_COLORS[Math.floor(rng() * RESONANCE_COLORS.length)]); } if (new Set(target).size < 2) { target[target.length - 1] = RESONANCE_COLORS[(RESONANCE_COLORS.indexOf(target[0]) + 1) % RESONANCE_COLORS.length]; } // 邻居计算 const neighborsOf = (id: number): number[] => { const r = Math.floor(id / cols); const c = id % cols; const ns: number[] = []; if (r > 0) ns.push((r - 1) * cols + c); if (r < rows - 1) ns.push((r + 1) * cols + c); if (c > 0) ns.push(r * cols + (c - 1)); if (c < cols - 1) ns.push(r * cols + (c + 1)); return ns; }; // 构造一条保证可解的路径(多次尝试) let pathIds: number[] | null = null; for (let attempt = 0; attempt < 300 && !pathIds; attempt++) { const used = new Set(); const path: number[] = []; const start = Math.floor(rng() * total); used.add(start); path.push(start); let cur = start; let ok = true; for (let i = 1; i < target.length; i++) { const ns = neighborsOf(cur).filter((n) => !used.has(n)); if (ns.length === 0) { ok = false; break; } cur = ns[Math.floor(rng() * ns.length)]; used.add(cur); path.push(cur); } if (ok) pathIds = path; } // 极端兜底(理论不会触发):线性路径 if (!pathIds) { pathIds = Array.from({ length: target.length }, (_, i) => i); } // 路径节点颜色映射 const pathColor = new Map(); pathIds.forEach((id, i) => pathColor.set(id, target[i])); // 生成网格 const grid: DecodeNode[] = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const id = r * cols + c; let color: ResonanceColor; if (pathColor.has(id)) { color = pathColor.get(id)!; } else { // 非路径节点:60% 偏向目标色(提升多解性),40% 随机全色 if (rng() < 0.6) { color = target[Math.floor(rng() * target.length)]; } else { color = RESONANCE_COLORS[Math.floor(rng() * RESONANCE_COLORS.length)]; } } grid.push({ id, row: r, col: c, color, used: false }); } } return { tier, grid, rows, cols, target, path: [], stepLimit: cfg.stepLimitBase, seed: s, }; } /** * 精确判断当前局面是否仍可解。 * 从当前路径末端出发,DFS 搜索是否存在一条按 target 剩余顺序、四邻接、不重复的完成路径。 * 用于玩家点击后检测「此路不通」,提示撤销。 */ export function isSolvable(puzzle: DecodePuzzle): boolean { const remaining = puzzle.target.slice(puzzle.path.length); if (remaining.length === 0) return true; const used = new Set(puzzle.path); const neighborsOf = (id: number): number[] => { const node = puzzle.grid.find((n) => n.id === id); if (!node) return []; const ns: number[] = []; if (node.row > 0) ns.push((node.row - 1) * puzzle.cols + node.col); if (node.row < puzzle.rows - 1) ns.push((node.row + 1) * puzzle.cols + node.col); if (node.col > 0) ns.push(node.row * puzzle.cols + (node.col - 1)); if (node.col < puzzle.cols - 1) ns.push(node.row * puzzle.cols + (node.col + 1)); return ns; }; // 若尚未落子,任意颜色匹配 remaining[0] 的未使用节点都可作为起点 const starts: number[] = puzzle.path.length === 0 ? puzzle.grid.filter((n) => !used.has(n.id) && n.color === remaining[0]).map((n) => n.id) : [puzzle.path[puzzle.path.length - 1]]; const dfs = (curId: number, idx: number): boolean => { if (idx >= remaining.length) return true; const nextColor = remaining[idx]; for (const nid of neighborsOf(curId)) { if (used.has(nid)) continue; const n = puzzle.grid.find((x) => x.id === nid); if (!n || n.color !== nextColor) continue; // 起点情况:第一步 idx=0 时,curId 本身要匹配 nextColor if (idx === 0 && puzzle.path.length === 0) { const cur = puzzle.grid.find((x) => x.id === curId); if (!cur || cur.color !== nextColor) continue; } used.add(nid); if (dfs(nid, idx + 1)) return true; used.delete(nid); } return false; }; for (const st of starts) { if (puzzle.path.length === 0) { // 起点自身需匹配 remaining[0] const cur = puzzle.grid.find((n) => n.id === st); if (!cur || cur.color !== remaining[0]) continue; used.add(st); if (dfs(st, 1)) return true; used.delete(st); } else { if (dfs(st, 0)) return true; } } return false; } /** 两节点是否四邻接 */ export function isAdjacent(a: DecodeNode, b: DecodeNode): boolean { return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1; } /** 尝试点击节点。返回 { ok, finished, failReason } */ export function tryClickNode( puzzle: DecodePuzzle, nodeId: number ): { ok: boolean; finished: boolean; failReason?: string } { const node = puzzle.grid.find((n) => n.id === nodeId); if (!node) return { ok: false, finished: false, failReason: "无效节点" }; if (node.used) return { ok: false, finished: false, failReason: "节点已使用" }; const stepIdx = puzzle.path.length; // 颜色须匹配目标序列当前位置 if (node.color !== puzzle.target[stepIdx]) { return { ok: false, finished: false, failReason: "谐振不匹配" }; } // 非首步须邻接 if (stepIdx > 0) { const prev = puzzle.grid.find((n) => n.id === puzzle.path[stepIdx - 1])!; if (!isAdjacent(prev, node)) { return { ok: false, finished: false, failReason: "节点不相邻" }; } } // 步数上限 if (stepIdx >= puzzle.stepLimit) { return { ok: false, finished: false, failReason: "步数耗尽" }; } node.used = true; puzzle.path.push(nodeId); const finished = puzzle.path.length >= puzzle.target.length; return { ok: true, finished }; } /** 是否已无解 — 复用精确的 isSolvable 判定 */ export function isStuck(puzzle: DecodePuzzle): boolean { if (puzzle.path.length >= puzzle.target.length) return false; if (puzzle.path.length >= puzzle.stepLimit) return true; return !isSolvable(puzzle); } /** * 假设从某节点作为第一步落子,局面是否仍可解。 * 用于在选起点阶段高亮「可行起点」,避免玩家选到死起点。 */ export function canStartFrom(puzzle: DecodePuzzle, nodeId: number): boolean { if (puzzle.path.length !== 0) return false; const node = puzzle.grid.find((n) => n.id === nodeId); if (!node || node.used) return false; if (node.color !== puzzle.target[0]) return false; const cp: DecodePuzzle = JSON.parse(JSON.stringify(puzzle)); const n = cp.grid.find((x) => x.id === nodeId)!; n.used = true; cp.path.push(nodeId); return isSolvable(cp); } /** 重置谜题(保留同一晶体,重新挑战) */ export function resetPuzzle(puzzle: DecodePuzzle): DecodePuzzle { return generatePuzzle(puzzle.tier, puzzle.seed + 1); }