feat(levels): gate on crossing points, and lay the campaign out as segments
trackShape moves the two drawing measurements into src so the sweep, the pool build, the selector and the gallery share one definition. They are one property seen twice: a chord joining opposite ports is a straight line, and two of those in a cell always cross, which is why the filler that paired leftovers straight across produced a board that is both all-straight and all-crossings. Crossings per cell is now a gate at 1.0. Bending the leftover ports alone takes the median from 2.36 to 0.93, so the gate costs a third of the candidates rather than most of them, and it is the difference the boards were described by. Selection gains --plan, an ordered list of segments each naming how many levels it wants and of what kind. Depth quotas cannot express the shape this campaign needs, because the two good things pull against each other: a crowded board clears beautifully and caps at three turns, while a deep lock needs room and so stays sparse — at five turns and eight pieces per orientation, 16 of 18 candidates fail the release gate. Sorting by depth would file every crowded level first and every deep one last; segments alternate them deliberately, and a plan is left in the order it was written rather than re-sorted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -344,6 +344,42 @@ export function deepLevelStats(level) {
|
||||
// A level is judged on its endgame with this many pieces or fewer left.
|
||||
const ENDGAME_PIECES = 3;
|
||||
|
||||
// How busy the drawing is, as two numbers taken from the same data. A cell's
|
||||
// six ports are paired into three chords; a chord joining opposite ports is a
|
||||
// diameter, drawn as a straight line, and two diameters in one cell always
|
||||
// cross. So "the board is all straight lines" and "there are too many crossing
|
||||
// points" are one property seen twice — which is why the filler that paired
|
||||
// leftover ports straight across produced both at once.
|
||||
export function trackShape(level) {
|
||||
let pairs = 0, straight = 0, crossings = 0, busiest = 0;
|
||||
for (const cell of level.cells) {
|
||||
const seen = new Set(), chords = [];
|
||||
for (let port = 0; port < 6; port++) {
|
||||
if (seen.has(port)) continue;
|
||||
const other = cell.tracks[port];
|
||||
seen.add(port); seen.add(other);
|
||||
chords.push([port, other]);
|
||||
}
|
||||
const between = (x, a, b) => { for (let k = (a + 1) % 6; k !== b; k = (k + 1) % 6) if (k === x) return true; return false; };
|
||||
let here = 0;
|
||||
for (let i = 0; i < chords.length; i++) {
|
||||
pairs++;
|
||||
if ((chords[i][0] + 3) % 6 === chords[i][1]) straight++;
|
||||
for (let j = i + 1; j < chords.length; j++) {
|
||||
const [a, b] = chords[i], [c, d] = chords[j];
|
||||
if (between(c, a, b) !== between(d, a, b)) here++;
|
||||
}
|
||||
}
|
||||
crossings += here;
|
||||
busiest = Math.max(busiest, here);
|
||||
}
|
||||
return {
|
||||
straightShare: pairs ? straight / pairs : 0,
|
||||
crossPerCell: level.cells.length ? crossings / level.cells.length : 0,
|
||||
crossings, busiestCell: busiest,
|
||||
};
|
||||
}
|
||||
|
||||
// The endgame question — with a handful of pieces left, how many of the six
|
||||
// orientations release anything — used to be answered from the same breadth
|
||||
// search that counts dead turns. On a dense board that search hits its state
|
||||
|
||||
Reference in New Issue
Block a user