feat(routes): let leftover track pair into a bend instead of straight across

Playing on a phone, the boards read as a grid of long straight lines crossing
everywhere — "一看到这个画面,我脑袋就大". The cause is one line in the filler.
Each family lays its signature routes over part of the board and leaves the rest
to completePorts, which paired every leftover port with the one straight across.
On a switchback board that made 76% of all track pairs diameters and left 43 of
61 cells containing nothing but diameters. Two diameters in one cell always
cross, so the straightness and the crossings were never two problems.

Preferring a bend halves both, in every family: switchback 76% → 40% straight
and 2.38 → 0.92 crossings per cell, crossing 86% → 10%, weave 37% → 5%.

It was also quietly suppressing 反差, the swing between a piece looking hopeless
and becoming trivial, which the design brief calls the point of the game.
Straight routes barely change between orientations. With bends, petals goes from
4.2 to 14.0 and crossing from 4.8 to 28.8 — both had been failing the ≥12 gate
and were absent from the level pool entirely. Petals is the family the user
singles out as the good-looking one. Generation yield rises with it: four seeds
now grow 24 playable levels against 3, at greater rotation depth.

createRouteBoard keeps 'straight' as its default so nothing already built moves;
the sweep opts in. labs/route-gallery draws both, with the real renderer, so the
choice is made by looking rather than by reading a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cabinw
2026-09-14 18:52:37 +08:00
co-authored by Claude Opus 5
parent 073b682234
commit f846a342d1
3 changed files with 203 additions and 5 deletions
+13 -4
View File
@@ -228,14 +228,23 @@ function crossing(design, radius, variant) {
design.ray(gate, 0); design.ray(gate, 1);
}
function completePorts(design, family, variant) {
// Every family lays its signature routes over part of the board and leaves the
// rest to be filled in. The filler used to pair a leftover port with the one
// straight across, which is why a board reads as a grid of long straight lines
// crossing everywhere: on a switchback board 76% of all track pairs were
// diameters and 43 of 61 cells were nothing but diameters. Two diameters in one
// cell always cross, so straight filler buys crossings as well as straightness.
// `fill` picks what a leftover port prefers instead; 'curve' asks for a bend.
function completePorts(design, family, variant, fill = 'straight') {
for (const cell of design.cells) {
const free = cell.tracks.flatMap((value, port) => value === null ? [port] : []);
// Periodic completion keeps secondary rails ordered instead of random noise.
const phase = mod(cell.q - cell.r + variant, 3);
while (free.length) {
const a = free.shift();
const preferred = family === 'weave' && phase === 0 ? mod(a + 1) : opposite(a);
const bend = mod(a + 1 + phase % 2); // 60° or 120°, varied by phase
const preferred = fill === 'curve' ? bend
: family === 'weave' && phase === 0 ? mod(a + 1) : opposite(a);
const b = free.splice(free.includes(preferred) ? free.indexOf(preferred) : mod(phase, free.length), 1)[0];
cell.tracks[a] = b; cell.tracks[b] = a;
}
@@ -290,7 +299,7 @@ function describeRoutes(design) {
return { featured, heroStarts: selected.map(({ pairs, ...candidate }) => candidate) };
}
export function createRouteBoard(seed, { family = 'petals', radius = 3, variant = 0 } = {}) {
export function createRouteBoard(seed, { family = 'petals', radius = 3, variant = 0, fill = 'straight' } = {}) {
const spec = ROUTE_FAMILIES.find(candidate => candidate.id === family);
if (!spec) throw new RangeError(`Unknown route family: ${family}`);
if (!Number.isInteger(radius) || radius < 2 || radius > 5) throw new RangeError('Route radius must be 2–5');
@@ -303,7 +312,7 @@ export function createRouteBoard(seed, { family = 'petals', radius = 3, variant
if (family === 'switchback') switchback(design, radius, rng, gateVariant);
if (family === 'weave') weave(design, radius, rng, gateVariant);
if (family === 'crossing') crossing(design, radius, gateVariant);
completePorts(design, family, gateVariant);
completePorts(design, family, gateVariant, fill);
const { featured, heroStarts } = describeRoutes(design);
return {
version: 1, id: `route-${family}-${radius}-${seed}-${variant}`, name: `${spec.name} · 路线草图`,