fix(levels): stop rejecting crowded boards for an endgame nobody measured
No level in this project is more than a third pieces — 13% to 28% across the shipping fifty, and the thousand-level set that preceded it peaks at 32% with exactly one level above 30%. Clearing a full board is the satisfying part of a clearing game, and it has never been on offer. It was not a design choice. deadTurnAudit walks the reachable state space with a 20000-state budget, and the endgame gate read its answer. Past about eleven pieces that budget is gone long before the walk reaches any state with three pieces left, so the audit returned `endgameMinOpen: null` — and the gate, written `endgameMinOpen >= 2`, compared null and got false. Every denser level was thrown out for a bad endgame that was never looked at. `exhausted` was right there in the audit's return value and nothing consulted it. Endgames are now enumerated outright rather than walked to: C(26,3) is 2600 subsets, so it finishes on any board this game will ever have. The statistic is the SHARE of endgames that are tight, not the minimum over them, because a minimum over more subsets falls for arithmetic alone — the same confound that made doubleDead look eight times worse for deep levels when the rate was 2% against 5%. By share, crowded boards are the better ones: worst 0.33% against 1.73% for sparse. With the gate measuring something, density inverts. At eight pieces per orientation: 44% density, 18 of 23 candidates passing the whole chain, and not one failing the dead-turn gate. At two: 19% density, 9 of 23, with eleven failing on dead turns. perTurn becomes a swept dial, and the hard piece ceiling that truncated the placement schedule regardless of it is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,12 +161,15 @@ async function renderCampaign() {
|
||||
const levels = (await res.json()).levels;
|
||||
$('campaign').innerHTML = levels.map((l, i) =>
|
||||
card(String(i + 1).padStart(2, '0'), l.campaign?.family ?? '', l,
|
||||
`<span>棋子 / 格子</span><b>${l.tiles.length} / ${l.cells.length}</b>`)).join('');
|
||||
`<span>棋子密度</span><b class="${l.tiles.length / l.cells.length >= .35 ? 'ok' : l.tiles.length / l.cells.length < .22 ? 'hot' : ''}">${l.tiles.length} / ${l.cells.length} = ${Math.round(100 * l.tiles.length / l.cells.length)}%</b>`)).join('');
|
||||
[...$('campaign').querySelectorAll('canvas')].forEach((cv, i) => paint(cv, levels[i]));
|
||||
const s = levels.map(shape);
|
||||
const avg = k => s.reduce((n, x) => n + x[k], 0) / s.length;
|
||||
$('meta').textContent = `当前 50 关:直线占比均值 ${Math.round(avg('straight') * 100)}%、交叉每格 ${avg('cross').toFixed(2)}`
|
||||
+ ` · 超过 60% 直线的有 ${s.filter(x => x.straight > .6).length} 关`;
|
||||
const dens = levels.map(l => l.tiles.length / l.cells.length);
|
||||
const mean = a => a.reduce((n, x) => n + x, 0) / a.length;
|
||||
$('meta').textContent = `当前 50 关:直线占比均值 ${Math.round(avg('straight') * 100)}%(超过 60% 的 ${s.filter(x => x.straight > .6).length} 关)`
|
||||
+ ` · 交叉每格 ${avg('cross').toFixed(2)}`
|
||||
+ ` · 棋子密度均值 ${Math.round(mean(dens) * 100)}%,最高 ${Math.round(Math.max(...dens) * 100)}%,35% 以上 ${dens.filter(x => x >= .35).length} 关`;
|
||||
} catch (e) {
|
||||
$('campaign').innerHTML = `<p class="hint">读不到 levels/campaign-50.json:${e.message}</p>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user