feat: version campaigns and document original puzzle design
Restore v0.1 and v0.2 archives with separate progress, publish the curved v0.3 campaign, and document the original 24-level design with reproducible evidence. Refine board presentation and movement, and keep video lab tooling within its own directory.
This commit is contained in:
+32
-1
@@ -175,6 +175,37 @@ export function solveRotations(level, maxStates = 20000) {
|
||||
return { solvable: false, reachable: false, minimumMoves: null, minimumActions: null, rotations: 0, actions: [], order: [], waves: first.waves, stuck: first.remaining.map(t => t.id), states: visited, exhausted: visited >= maxStates };
|
||||
}
|
||||
|
||||
// Count the paths that successful pieces really travel in the reference plan.
|
||||
// Decorative bends in unused cells do not count as route-reading practice.
|
||||
export function solutionPathUsage(level, actions) {
|
||||
let board = level, remaining = [...level.tiles], straightRun = 0;
|
||||
const paths = [];
|
||||
for (const action of actions) {
|
||||
if (action.type === 'rotate') { board = rotateBoard(board, action.direction); continue; }
|
||||
const tile = remaining.find(t => t.id === action.id);
|
||||
if (!tile) continue;
|
||||
const route = trace(board, tile, remaining);
|
||||
if (route.status !== 'exit') continue;
|
||||
const bends = route.steps.filter(s => s.enter !== null && opposite(s.enter) !== s.exit).length;
|
||||
straightRun = bends ? 0 : straightRun + 1;
|
||||
paths.push({ id: tile.id, bends, cells: route.steps.length, straightRun });
|
||||
remaining = remaining.filter(t => t.id !== tile.id);
|
||||
}
|
||||
const curvedRemovals = paths.filter(p => p.bends > 0).length;
|
||||
return {
|
||||
removals: paths.length,
|
||||
curvedRemovals,
|
||||
straightRemovals: paths.length - curvedRemovals,
|
||||
curvedRatio: paths.length ? curvedRemovals / paths.length : 0,
|
||||
singleBendRemovals: paths.filter(p => p.bends === 1).length,
|
||||
multiBendRemovals: paths.filter(p => p.bends > 1).length,
|
||||
totalBends: paths.reduce((sum, p) => sum + p.bends, 0),
|
||||
maxBends: Math.max(0, ...paths.map(p => p.bends)),
|
||||
maxConsecutiveStraight: Math.max(0, ...paths.map(p => p.straightRun)),
|
||||
paths: paths.map(({ straightRun, ...path }) => path),
|
||||
};
|
||||
}
|
||||
|
||||
export function analyze(level) {
|
||||
const solution = solveRotations(level), n = level.tiles.length;
|
||||
const routes = level.tiles.map(t => trace(level, t, level.tiles, true));
|
||||
@@ -204,7 +235,7 @@ export function analyze(level) {
|
||||
// A pacing heuristic for comparing authored levels, not measured playtest time.
|
||||
estimatedSeconds: Math.round(n * 3 + totalTurns * 0.75 + solution.rotations * 4),
|
||||
};
|
||||
return { score, parts, meanTurns, maxTurns, free, freeRatio, sweptOnlyBlockers, dependencyRatio, workload, depth: solution.waves.length, ...solution, label: score < 30 ? '轻松' : score < 50 ? '进阶' : score < 70 ? '烧脑' : '挑战' };
|
||||
return { score, parts, meanTurns, maxTurns, free, freeRatio, sweptOnlyBlockers, dependencyRatio, workload, pathUsage: solutionPathUsage(level, solution.actions), depth: solution.waves.length, ...solution, label: score < 30 ? '轻松' : score < 50 ? '进阶' : score < 70 ? '烧脑' : '挑战' };
|
||||
}
|
||||
export function generateLevel(seed, radius = 3, target = 25, complexity = 0.7) {
|
||||
const rng = random(seed);
|
||||
|
||||
Reference in New Issue
Block a user