From f846a342d18c8bb89f96c6101cc7e704e6bfcb00 Mon Sep 17 00:00:00 2001 From: cabinw Date: Mon, 14 Sep 2026 18:52:37 +0800 Subject: [PATCH] feat(routes): let leftover track pair into a bend instead of straight across MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- labs/route-gallery/index.html | 186 ++++++++++++++++++++++++++++++++++ scripts/prototype-d5.mjs | 5 +- src/route-grammar.js | 17 +++- 3 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 labs/route-gallery/index.html 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% 的。

+
+ +
+

labs/route-gallery · 轨道由 src/renderer.js 实际绘制,和游戏里看到的是同一套画法

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