diff --git a/labs/route-gallery/index.html b/labs/route-gallery/index.html
new file mode 100644
index 0000000..0d85b41
--- /dev/null
+++ b/labs/route-gallery/index.html
@@ -0,0 +1,186 @@
+
+
+
+
+
路线画风 · 审查台
+
这里画的是轨道本身,不是关卡。先看线好不好看,再谈难度——因为线是玩家一眼就会看到的东西。
+
载入中…
+
+
+
「直穿填充」是那些满屏直线的来源。每个家族只铺自己的签名路线,盘面剩下的端口由填充逻辑配对,而它默认配成对穿的直线。一个格子里两条直线必定相交,所以直线多 = 交叉点多,两件事是同一个原因。
+
它还在压制反差。换成偏好弯道之后,花瓣的反差从 4.2 升到 14.0、双叶跨区从 4.8 升到 28.8、环绕从 10.6 升到 22.9 —— 前两个家族原本是被「反差 ≥12」这道闸门挡在关卡池之外的,其中花瓣正是「像花一样」的那个。
(反差要在盘面上摆好棋子、长成关卡之后才测得出来,所以不在下面这张表里;这几个数字来自六个种子的构造结果。)
+
+
+
+
+
+
+
+
+
六个家族的画风
+
同一个种子、同一个底板,只换填充方式。数字越低越好:直线占比是对穿轨道占全部轨道的比例,交叉/格是每个格子里互相穿过的轨道对数。
+
+
+
+
当前交付的 50 关
+
这是现在能玩到的那 50 关的盘面,用同一套指标标注。标红的是直线占比超过 60% 的。
+
+
+
+
+
+
+
+
diff --git a/scripts/prototype-d5.mjs b/scripts/prototype-d5.mjs
index 17baa53..b4db869 100644
--- a/scripts/prototype-d5.mjs
+++ b/scripts/prototype-d5.mjs
@@ -19,6 +19,9 @@ const MIN_PIECES = Number(args.minPieces ?? 8);
const DEPTHS = String(args.depths ?? '3,4,5,6').split(',').map(Number);
const PER_TURN = Number(args.perTurn ?? 2);
const SHOWCASE = Number(args.showcase ?? 2);
+// How leftover ports are paired. 'straight' was the only behaviour and it is
+// what made boards read as a grid of long lines crossing everywhere.
+const FILL = String(args.fill ?? 'curve');
// Random player: taps a random piece; after three blocked taps in a row, turns randomly.
function monkeyActionsPerPiece(level, runs = 8) {
@@ -44,7 +47,7 @@ for (const { id: family } of ROUTE_FAMILIES) for (const radius of RADII) {
const cellKey = `${family}/r${radius}`;
yieldTable[cellKey] = { locks: 0, grown: 0, seeds: SEEDS };
for (let seed = SEED_FROM; seed < SEED_FROM + SEEDS; seed++) {
- const base = createRouteBoard(seed, { family, radius });
+ const base = createRouteBoard(seed, { family, radius, fill: FILL });
const lock = createDeepLock(base, { seed, maxChecks: 600000 });
if (!lock) continue;
yieldTable[cellKey].locks++;
diff --git a/src/route-grammar.js b/src/route-grammar.js
index 608c2d0..1acac24 100644
--- a/src/route-grammar.js
+++ b/src/route-grammar.js
@@ -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} · 路线草图`,