初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
// 关卡设计辅助:
|
||||
// npm run design -- 1-7 打印盘面、无线索路径数、当前解
|
||||
// npm run design -- 1-7 --minimize 当前线索里哪些可删(删掉仍唯一)
|
||||
// npm run design -- 1-7 --walls 哪些墙可删(删掉仍唯一)
|
||||
// npm run design -- 1-7 --suggest 3 --path a,b,c,... 给作者解找最少线索(≤3 个)
|
||||
// npm run design -- /path/draft.json --level 1-7 ... 也可以指定临时关卡文件
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { HexBoard } from '../src/board';
|
||||
import { loadPack, parsePack, type LoadedLevel } from '../src/levels';
|
||||
import { redundantWalls, solve, spinnerReport, wallsMatter } from '../src/solver';
|
||||
import type { LevelDef } from '../src/types';
|
||||
import { pathToIds, renderAscii } from './ascii';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const argv = process.argv.slice(2);
|
||||
const flag = (name: string): string | undefined => {
|
||||
const i = argv.indexOf(name);
|
||||
return i >= 0 ? argv[i + 1] : undefined;
|
||||
};
|
||||
|
||||
function findLevel(): LoadedLevel {
|
||||
const target = argv[0];
|
||||
if (!target) throw new Error('用法: npm run design -- <关卡id | 文件> [--level id] [--minimize|--walls|--suggest k --path ...]');
|
||||
const files = target.endsWith('.json')
|
||||
? [target]
|
||||
: readdirSync(join(here, '..', 'levels'))
|
||||
.filter((f) => f.endsWith('.json'))
|
||||
.map((f) => join(here, '..', 'levels', f));
|
||||
const wantId = target.endsWith('.json') ? flag('--level') : target;
|
||||
for (const f of files) {
|
||||
const pack = parsePack(JSON.parse(readFileSync(f, 'utf8')));
|
||||
const levels = loadPack(pack);
|
||||
const lv = wantId ? levels.find((l) => l.def.id === wantId) : levels[0];
|
||||
if (lv) return lv;
|
||||
}
|
||||
throw new Error(`找不到关卡 ${wantId ?? target}`);
|
||||
}
|
||||
|
||||
function withClues(def: LevelDef, clueOf: Map<string, number>): HexBoard {
|
||||
return HexBoard.fromCells(
|
||||
def.cells.map((c) => ({ id: c.id, q: c.q, r: c.r, clue: clueOf.get(c.id) })),
|
||||
def.walls ?? [],
|
||||
def.spinners ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
const lv = findLevel();
|
||||
const b = lv.board;
|
||||
console.log(`\n${lv.def.id} N=${b.n} 墙=${b.walls.length}`);
|
||||
console.log(renderAscii(b).replace(/^/gm, ' '));
|
||||
const free = solve(b, { ignoreClues: true, limit: 2000 });
|
||||
console.log(`无线索哈密顿路径: ${free.length >= 2000 ? '2000+' : free.length}`);
|
||||
const cur = solve(b, { limit: 3 });
|
||||
console.log(`当前线索下的解: ${cur.length}${cur.length === 1 ? '(唯一)' : ''}`);
|
||||
for (const p of cur) console.log(' ' + pathToIds(b, p));
|
||||
if (cur.length === 1) {
|
||||
console.log(renderAscii(b, cur[0]).replace(/^/gm, ' '));
|
||||
console.log(`去墙后解: ${solve(b.withoutWalls(), { limit: 100 }).length} 墙参与唯一性: ${wallsMatter(b) ? '是' : '否'}`);
|
||||
for (const r of spinnerReport(b)) {
|
||||
console.log(`转墙 ${r.index}: ${r.removable ? '冗余' : '必要'};${r.freezable.length ? `可换成静态墙(边 ${r.freezable.join('/')})` : '不能用静态墙替代'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (argv.includes('--minimize') && cur.length === 1) {
|
||||
const clues = b.cells.filter((c) => c.clue !== null);
|
||||
for (const c of clues) {
|
||||
const m = new Map(clues.filter((x) => x !== c).map((x) => [x.id, x.clue!]));
|
||||
const n = solve(withClues(lv.def, m), { limit: 2 }).length;
|
||||
console.log(` 删掉 ${c.id}=${c.clue}: ${n === 1 ? '仍唯一(可删)' : n === 0 ? '无解' : '多解(必须留)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (argv.includes('--walls') && cur.length === 1) {
|
||||
const r = redundantWalls(b);
|
||||
console.log(r.length ? `可删的墙: ${r.map((i) => `${b.cells[b.walls[i].a].id}|${b.cells[b.walls[i].b].id}`).join(', ')}` : '每堵墙都必要');
|
||||
}
|
||||
|
||||
const suggestK = flag('--suggest');
|
||||
if (suggestK) {
|
||||
const pathIds = (flag('--path') ?? '').split(',').filter(Boolean);
|
||||
const path = pathIds.length ? pathIds.map((id) => b.cellById(id).index) : cur[0];
|
||||
if (!path || path.length !== b.n) throw new Error('--path 必须是覆盖全部格的作者解');
|
||||
const k = Number(suggestK);
|
||||
const stepOf = new Map<number, number>();
|
||||
path.forEach((c, i) => stepOf.set(c, i + 1));
|
||||
const cells = b.cells.map((c) => c.index);
|
||||
const results: Array<{ ids: string[]; score: number }> = [];
|
||||
const rec = (start: number, chosen: number[]): void => {
|
||||
if (chosen.length > 0) {
|
||||
const m = new Map(chosen.map((c) => [b.cells[c].id, stepOf.get(c)!]));
|
||||
const n = solve(withClues(lv.def, m), { limit: 2 }).length;
|
||||
if (n === 1) {
|
||||
const steps = chosen.map((c) => stepOf.get(c)!).sort((x, y) => x - y);
|
||||
// 偏好:线索少、少用端点、分布均匀
|
||||
let score = chosen.length * 10;
|
||||
for (const s of steps) if (s === 1 || s === b.n) score += 1;
|
||||
for (let i = 1; i < steps.length; i++) score += Math.abs(steps[i] - steps[i - 1] - b.n / (steps.length + 1)) * 0.1;
|
||||
results.push({ ids: chosen.map((c) => `${b.cells[c].id}=${stepOf.get(c)}`), score });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (chosen.length === k) return;
|
||||
for (let i = start; i < cells.length; i++) rec(i + 1, [...chosen, cells[i]]);
|
||||
};
|
||||
rec(0, []);
|
||||
results.sort((x, y) => x.score - y.score);
|
||||
console.log(`\n作者解: ${pathToIds(b, path)}`);
|
||||
console.log(`≤${k} 个线索可唯一的组合(前 12):`);
|
||||
for (const r of results.slice(0, 12)) console.log(' ' + r.ids.join(' '));
|
||||
if (!results.length) console.log(' 没有');
|
||||
}
|
||||
Reference in New Issue
Block a user