fix(levels): let a packed board pay its opening as a cascade, not one long trip

The opening gate asks for a departure of at least 0.57s, which is how a sparse
board pays: one piece threading a long route while you watch. A packed board
cannot pay that way — the pieces free at the opening are the ones at the rim,
and their routes are short by construction. So the gate rejected 29 of 29
crowded candidates for lacking a payoff they deliver in a different currency:
the cascade of pieces leaving one after another, which is what the brief singles
out as 多个棋子连环出去,非常赏心悦目.

openingRunMs totals every departure available before a turn is needed, and the
gate takes either that or the single long show. Crowded boards come through at
46% density, 0.92 crossings per cell and 14% straight track.

Both thresholds scale with the motion dial the way the first one already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cabinw
2026-09-14 20:14:33 +08:00
co-authored by Claude Opus 5
parent 8d766b6f06
commit de485c0c34
2 changed files with 14 additions and 3 deletions
+7 -3
View File
@@ -112,6 +112,8 @@ const MIN_CONTRAST = Number(args.minContrast ?? 12);
// levels that were fine. See openingShowFloor in deep-lock.
const MIN_OPENING_SHOW = openingShowFloor(Number(args.minOpeningShow ?? 800));
const MIN_OPENING_CHOICES = Number(args.minOpeningChoices ?? 3);
// The cascade alternative to a single long show, in the same calibrated units.
const MIN_OPENING_RUN = openingShowFloor(Number(args.minOpeningRun ?? 2600));
const MIN_CRUISES = Number(args.minCruises ?? 3);
// Rotation count is deliberately not capped: turning is free and it is the
// point of the game. What has to hold is that every turn pays out (minRelease),
@@ -123,7 +125,9 @@ const playable = grownEnough.filter(r => r.audit.unsolvable === 0 && r.stats.fir
// The show is the payout. An opening that offers two rim pieces and a 0.3s
// hop gives a casual player nothing to enjoy before the thinking starts.
&& r.show.openingChoices >= MIN_OPENING_CHOICES
&& r.show.openingBestMs >= MIN_OPENING_SHOW
// One long departure, or a cascade of short ones — a packed board pays the
// other way round and the single-best test cannot see it.
&& (r.show.openingBestMs >= MIN_OPENING_SHOW || r.show.openingRunMs >= MIN_OPENING_RUN)
&& r.show.cruises >= MIN_CRUISES
&& r.shape.crossPerCell <= MAX_CROSS);
// Locked-share used to sort first and highest, which is backwards for a casual
@@ -142,9 +146,9 @@ const g5 = g4.filter(r => r.stats.minRelease >= MIN_RELEASE);
console.log(`\n【门槛】长成 ${grownEnough.length} → 首转双向 ${g1.length} → 锁住≤90% ${g2.length} → 无不可解态 ${g3.length} → 终局紧局≤${(MAX_TIGHT_SHARE*100).toFixed(1)}% ${g4.length} → 每转释放≥${MIN_RELEASE} ${g5.length} → 反差≥${MIN_CONTRAST} ${playable.length}`);
const g6 = g5.filter(r => r.contrast.contrast >= MIN_CONTRAST);
const g7 = g6.filter(r => r.show.openingChoices >= MIN_OPENING_CHOICES);
const g8 = g7.filter(r => r.show.openingBestMs >= MIN_OPENING_SHOW);
const g8 = g7.filter(r => r.show.openingBestMs >= MIN_OPENING_SHOW || r.show.openingRunMs >= MIN_OPENING_RUN);
const g9 = g8.filter(r => r.show.cruises >= MIN_CRUISES);
console.log(` → 开局可点≥${MIN_OPENING_CHOICES} ${g7.length} → 开局演出≥${(MIN_OPENING_SHOW / 1000).toFixed(2)}s ${g8.length} → 长巡游≥${MIN_CRUISES} ${g9.length} → 交叉/格≤${MAX_CROSS} ${playable.length}`);
console.log(` → 开局可点≥${MIN_OPENING_CHOICES} ${g7.length} → 开局演出≥${(MIN_OPENING_SHOW/1000).toFixed(2)}s 或连锁≥${(MIN_OPENING_RUN/1000).toFixed(1)}s ${g8.length} → 长巡游≥${MIN_CRUISES} ${g9.length} → 交叉/格≤${MAX_CROSS} ${playable.length}`);
console.log(`【候选池】旋转深度 ${JSON.stringify(playable.reduce((m, r) => (m[r.stats.rotations] = (m[r.stats.rotations] || 0) + 1, m), {}))}`);
console.log(` 交叉/格 中位 ${med(playable.map(r => r.shape.crossPerCell)).toFixed(2)} · 直线占比 中位 ${(med(playable.map(r => r.shape.straightShare))*100).toFixed(0)}% · 密度 中位 ${(med(playable.map(r => r.level.tiles.length/r.level.cells.length))*100).toFixed(0)}%`);
console.log(` 反差 中位 ${med(playable.map(r => r.contrast.contrast)).toFixed(1)} · 家族 ${JSON.stringify(playable.reduce((m, r) => (m[r.family] = (m[r.family] || 0) + 1, m), {}))}`);