feat(levels): c50c trades one good idea repeated for five that alternate
c50b is sealed where it stands, as a reference sample rather than a candidate. Its packed construction was judged good — clearing a board from the rim inward is the experience it proved out — but at 32 of 50 levels it played as one level fifty times: 体验太单一了,因为基本都是满盘. The note on why it is kept lives at the top of its module, because the lesson is the useful part. It was monotonous on a second axis nobody asked about. 环绕 and 花瓣, the two route families the brief singles out as the good-looking ones, were absent from it entirely — not by choice but by a crossing threshold. At 1.0 crossings per cell, 0% of orbits boards pass and 4% of spirals; at 1.2, 84% and 94% do. The gate meant to answer "交叉点太多" had quietly deleted the families most worth looking at, so it moves to 1.2 — still half the 2.36 that drew the complaint. c50c alternates five kinds of level every three to five: packed boards down to 32%, long-route departures to watch, deep locks to solve, small quick boards, and segments given over to orbits, petals and spirals by name. All six families appear, no family exceeds 24%, and the plan is 13 segments rather than 9. Selection learned to ask for variety outright — by family, piece count and longest drawn route — instead of hoping it falls out of a difficulty score. Rule bot across all three: 50/50 cleared and zero dead taps everywhere; c50c sits between the other two on blind rotations and on how much there is to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
// been played — --weights takes the refit, so a round is one flag, not an edit.
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { trace } from '../src/core.js';
|
||||
import { deepLevelKey, showStats } from '../src/deep-lock.js';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...v] = a.replace(/^--/, '').split('='); return [k, v.length ? v.join('=') : true]; }));
|
||||
@@ -132,6 +133,16 @@ const spread = (rows, want) => {
|
||||
return out;
|
||||
};
|
||||
|
||||
// The longest line drawn on the board, which is what a level with few pieces and
|
||||
// a winding route is selected for — the departure you watch rather than solve.
|
||||
const routeCache = new WeakMap();
|
||||
const longestRoute = level => {
|
||||
if (routeCache.has(level)) return routeCache.get(level);
|
||||
const n = Math.max(...level.tiles.map(t => trace(level, t, [], true).steps.length));
|
||||
routeCache.set(level, n);
|
||||
return n;
|
||||
};
|
||||
|
||||
const shortfall = [];
|
||||
if (PLAN) {
|
||||
const density = row => row.level.tiles.length / row.level.cells.length;
|
||||
@@ -142,6 +153,14 @@ if (PLAN) {
|
||||
// difficulty curve from the outside in. A segment asks for one kind or the
|
||||
// other outright rather than inferring it from density.
|
||||
if (seg.packed != null && Boolean(p.packed) !== seg.packed) return false;
|
||||
// Variety is a thing to ask for outright. An all-packed campaign reads as
|
||||
// one level fifty times, and the families the brief singles out as the
|
||||
// good-looking ones — 环绕 and 花瓣 — were absent from it entirely,
|
||||
// filtered out upstream by a crossing threshold they sit just above.
|
||||
if (seg.families && !seg.families.includes(p.family)) return false;
|
||||
if (seg.maxPieces != null && r.level.tiles.length > seg.maxPieces) return false;
|
||||
if (seg.minPieces != null && r.level.tiles.length < seg.minPieces) return false;
|
||||
if (seg.minLongestRoute != null && longestRoute(r.level) < seg.minLongestRoute) return false;
|
||||
if (seg.minFill != null && (p.fill ?? 0) < seg.minFill) return false;
|
||||
if (seg.minRotations != null && (r.stats.rotations ?? 0) < seg.minRotations) return false;
|
||||
if (seg.minCells != null && r.level.cells.length < seg.minCells) return false;
|
||||
|
||||
Reference in New Issue
Block a user