d9404a31-1970-4e79-ac9a-f4f9db60350b

This commit is contained in:
2026-06-23 12:47:49 +00:00
parent e193530f24
commit f73b96026c
101 changed files with 12685 additions and 2 deletions
+265
View File
@@ -0,0 +1,265 @@
// 回响星核 / 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<number>();
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<number, ResonanceColor>();
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);
}