Add three playable puzzle variants in an isolated lab

This commit is contained in:
cabinw
2026-09-13 12:19:13 +08:00
parent e92cccba82
commit d1dffa9338
10 changed files with 7934 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
import { writeFileSync } from 'node:fs';
import { hexagon, random, randomTracks, DIRS, key, trace } from '../../src/core.js';
import { createState, solve, act, routeFor } from './model.js';
const levels = [];
const names = { color: ['双色试航', '转向归站', '先让一路', '曲线分流', '各归其位'], switch: ['第一枚开关', '弯道解锁', '先清再切', '释放内圈', '换轨接力'], relay: ['中途停一停', '停靠再转向', '借位让路', '弯道接驳', '分段出发'] };
for (const mode of ['color', 'switch', 'relay']) {
for (let slot = 0; slot < 5; slot++) {
let accepted = false;
for (let attempt = 0; attempt < 2500 && !accepted; attempt++) {
const seed = 901 + slot * 10000 + attempt + ['color','switch','relay'].indexOf(mode) * 100000;
const rng = random(seed), cells = hexagon(2).map((c, i) => ({ ...c, id: i, tracks: randomTracks(rng) }));
const coords = new Set(cells.map(c => key(c.q, c.r)));
const gates = cells.flatMap(c => DIRS.flatMap(([dq, dr], dir) => coords.has(key(c.q+dq,c.r+dr)) ? [] : [{ q:c.q, r:c.r, dir, color: (dir + c.q - c.r + 12) % 2 }]));
const shuffled = [...cells]; // Fisher–Yates keeps recipes reproducible.
for(let i=shuffled.length-1;i>0;i--){const j=Math.floor(rng()*(i+1));[shuffled[i],shuffled[j]]=[shuffled[j],shuffled[i]];}
const tiles = shuffled.slice(0, 3 + Math.floor(slot / 2)).map((c,i) => ({ id:`p${i}`, q:c.q,r:c.r,dir:Math.floor(rng()*6),color:i%2 }));
if (mode === 'switch') {
tiles[0].trigger = 'A';
const target = cells.find(c => c.q === 0 && c.r === 0);
target.switchId = 'A'; target.alternate = randomTracks(rng);
}
const level = { version:1, id:`${mode}-${slot+1}`, mode, name:names[mode][slot], seed, cells, tiles, gates, stops:[{q:0,r:0}], moveLimit:tiles.length };
const start = createState(level), solution = solve(start, 2500);
if (!solution.solved) continue;
let state = start, stopCount = 0, bends = 0, changedRoute = false, openedRoute = false;
for (const action of solution.actions) {
const result = act(state, action);
if (result.status === 'stop') stopCount++;
if (result.status === 'exit') bends += result.steps.filter(s => s.enter !== null && (s.enter+3)%6 !== s.exit).length;
if (result.state.fired.length > state.fired.length) {
openedRoute = result.state.tiles.some(t => routeFor(state, t).status !== 'exit' && routeFor(result.state, t).status === 'exit');
changedRoute = result.state.tiles.some(t => JSON.stringify(trace(state,t,[],true).steps) !== JSON.stringify(trace(result.state,t,[],true).steps));
}
state = result.state;
}
if (!bends || !solution.actions.some(a => a.type === 'rotate')) continue;
if (mode === 'relay' && !stopCount) continue;
if (mode === 'switch' && (!changedRoute || !openedRoute)) continue;
levels.push({ ...level, solution:solution.actions, evidence:{ bends, stopCount, changedRoute, openedRoute, states:solution.states } });
console.log(level.id, seed, solution.actions.length);
accepted = true;
}
if (!accepted) throw new Error(`No candidate: ${mode} ${slot}`);
}
}
writeFileSync(new URL('./levels.json', import.meta.url), JSON.stringify(levels, null, 2)+'\n');