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} · 路线草图`,