feat(levels): c50c trades one good idea repeated for five that alternate
c50b is sealed where it stands, as a reference sample rather than a candidate. Its packed construction was judged good — clearing a board from the rim inward is the experience it proved out — but at 32 of 50 levels it played as one level fifty times: 体验太单一了,因为基本都是满盘. The note on why it is kept lives at the top of its module, because the lesson is the useful part. It was monotonous on a second axis nobody asked about. 环绕 and 花瓣, the two route families the brief singles out as the good-looking ones, were absent from it entirely — not by choice but by a crossing threshold. At 1.0 crossings per cell, 0% of orbits boards pass and 4% of spirals; at 1.2, 84% and 94% do. The gate meant to answer "交叉点太多" had quietly deleted the families most worth looking at, so it moves to 1.2 — still half the 2.36 that drew the complaint. c50c alternates five kinds of level every three to five: packed boards down to 32%, long-route departures to watch, deep locks to solve, small quick boards, and segments given over to orbits, petals and spirals by name. All six families appear, no family exceeds 24%, and the plan is 13 segments rather than 9. Selection learned to ask for variety outright — by family, piece count and longest drawn route — instead of hoping it falls out of a difficulty score. Rule bot across all three: 50/50 cleared and zero dead taps everywhere; c50c sits between the other two on blind rotations and on how much there is to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+129150
-3
File diff suppressed because it is too large
Load Diff
+625025
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+92
-10
@@ -1,11 +1,93 @@
|
|||||||
[
|
[
|
||||||
{ "name": "起步 · 小满盘", "count": 6, "packed": true, "maxCells": 37 },
|
{
|
||||||
{ "name": "深锁初见", "count": 4, "rotations": [3], "packed": false, "maxDensity": 0.28 },
|
"name": "开场 · 小满盘",
|
||||||
{ "name": "满盘 · 由外向内", "count": 7, "packed": true },
|
"count": 4,
|
||||||
{ "name": "变奏 · 深一点", "count": 5, "rotations": [4, 5], "packed": false },
|
"packed": true,
|
||||||
{ "name": "大满盘", "count": 7, "packed": true, "minCells": 61 },
|
"maxCells": 37
|
||||||
{ "name": "中段 · 稀而绕", "count": 4, "rotations": [3], "packed": false, "maxDensity": 0.30 },
|
},
|
||||||
{ "name": "满盘 · 转得更深", "count": 6, "packed": true, "minRotations": 3 },
|
{
|
||||||
{ "name": "收束前的深锁", "count": 5, "rotations": [4, 5], "packed": false },
|
"name": "长路线演出",
|
||||||
{ "name": "终章 · 最满的几盘", "count": 6, "packed": true, "minFill": 1 }
|
"count": 5,
|
||||||
]
|
"packed": false,
|
||||||
|
"maxPieces": 14,
|
||||||
|
"minLongestRoute": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "深锁初见",
|
||||||
|
"count": 4,
|
||||||
|
"rotations": [
|
||||||
|
3,
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"packed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "环绕 · 同心圈",
|
||||||
|
"count": 3,
|
||||||
|
"families": [
|
||||||
|
"orbits"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "花瓣 · 回到花心",
|
||||||
|
"count": 3,
|
||||||
|
"families": [
|
||||||
|
"petals"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "满盘 · 由外向内",
|
||||||
|
"count": 2,
|
||||||
|
"packed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "紧凑快节奏",
|
||||||
|
"count": 4,
|
||||||
|
"packed": false,
|
||||||
|
"maxPieces": 13,
|
||||||
|
"maxCells": 37
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "深锁 · 四转",
|
||||||
|
"count": 5,
|
||||||
|
"rotations": [
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"packed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "大盘长路线",
|
||||||
|
"count": 4,
|
||||||
|
"packed": false,
|
||||||
|
"minCells": 61,
|
||||||
|
"minLongestRoute": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "满盘 · 转得更深",
|
||||||
|
"count": 5,
|
||||||
|
"packed": true,
|
||||||
|
"minRotations": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "螺旋",
|
||||||
|
"count": 3,
|
||||||
|
"families": [
|
||||||
|
"spiral"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "交织收束",
|
||||||
|
"count": 3,
|
||||||
|
"families": [
|
||||||
|
"crossing",
|
||||||
|
"weave"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "终章 · 全满收束",
|
||||||
|
"count": 5,
|
||||||
|
"packed": true,
|
||||||
|
"minFill": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
// been played — --weights takes the refit, so a round is one flag, not an edit.
|
// been played — --weights takes the refit, so a round is one flag, not an edit.
|
||||||
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||||
import { dirname, resolve } from 'node:path';
|
import { dirname, resolve } from 'node:path';
|
||||||
|
import { trace } from '../src/core.js';
|
||||||
import { deepLevelKey, showStats } from '../src/deep-lock.js';
|
import { deepLevelKey, showStats } from '../src/deep-lock.js';
|
||||||
|
|
||||||
const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...v] = a.replace(/^--/, '').split('='); return [k, v.length ? v.join('=') : true]; }));
|
const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...v] = a.replace(/^--/, '').split('='); return [k, v.length ? v.join('=') : true]; }));
|
||||||
@@ -132,6 +133,16 @@ const spread = (rows, want) => {
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The longest line drawn on the board, which is what a level with few pieces and
|
||||||
|
// a winding route is selected for — the departure you watch rather than solve.
|
||||||
|
const routeCache = new WeakMap();
|
||||||
|
const longestRoute = level => {
|
||||||
|
if (routeCache.has(level)) return routeCache.get(level);
|
||||||
|
const n = Math.max(...level.tiles.map(t => trace(level, t, [], true).steps.length));
|
||||||
|
routeCache.set(level, n);
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
|
||||||
const shortfall = [];
|
const shortfall = [];
|
||||||
if (PLAN) {
|
if (PLAN) {
|
||||||
const density = row => row.level.tiles.length / row.level.cells.length;
|
const density = row => row.level.tiles.length / row.level.cells.length;
|
||||||
@@ -142,6 +153,14 @@ if (PLAN) {
|
|||||||
// difficulty curve from the outside in. A segment asks for one kind or the
|
// difficulty curve from the outside in. A segment asks for one kind or the
|
||||||
// other outright rather than inferring it from density.
|
// other outright rather than inferring it from density.
|
||||||
if (seg.packed != null && Boolean(p.packed) !== seg.packed) return false;
|
if (seg.packed != null && Boolean(p.packed) !== seg.packed) return false;
|
||||||
|
// Variety is a thing to ask for outright. An all-packed campaign reads as
|
||||||
|
// one level fifty times, and the families the brief singles out as the
|
||||||
|
// good-looking ones — 环绕 and 花瓣 — were absent from it entirely,
|
||||||
|
// filtered out upstream by a crossing threshold they sit just above.
|
||||||
|
if (seg.families && !seg.families.includes(p.family)) return false;
|
||||||
|
if (seg.maxPieces != null && r.level.tiles.length > seg.maxPieces) return false;
|
||||||
|
if (seg.minPieces != null && r.level.tiles.length < seg.minPieces) return false;
|
||||||
|
if (seg.minLongestRoute != null && longestRoute(r.level) < seg.minLongestRoute) return false;
|
||||||
if (seg.minFill != null && (p.fill ?? 0) < seg.minFill) return false;
|
if (seg.minFill != null && (p.fill ?? 0) < seg.minFill) return false;
|
||||||
if (seg.minRotations != null && (r.stats.rotations ?? 0) < seg.minRotations) return false;
|
if (seg.minRotations != null && (r.stats.rotations ?? 0) < seg.minRotations) return false;
|
||||||
if (seg.minCells != null && r.level.cells.length < seg.minCells) return false;
|
if (seg.minCells != null && r.level.cells.length < seg.minCells) return false;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { PRODUCTION_CAMPAIGN, PRODUCTION_LEVELS } from './levels-v05.js';
|
|||||||
import { TUTORIAL_LEVELS } from './tutorial-levels.js';
|
import { TUTORIAL_LEVELS } from './tutorial-levels.js';
|
||||||
import { C50_CAMPAIGN, C50_LEVELS } from './levels-c50.js';
|
import { C50_CAMPAIGN, C50_LEVELS } from './levels-c50.js';
|
||||||
import { C50B_CAMPAIGN, C50B_LEVELS } from './levels-c50b.js';
|
import { C50B_CAMPAIGN, C50B_LEVELS } from './levels-c50b.js';
|
||||||
|
import { C50C_CAMPAIGN, C50C_LEVELS } from './levels-c50c.js';
|
||||||
|
|
||||||
// Public layout versions are distinct from the level JSON schema's `version: 1`.
|
// Public layout versions are distinct from the level JSON schema's `version: 1`.
|
||||||
// Historical layouts stay intact; all editions share the current game rules.
|
// Historical layouts stay intact; all editions share the current game rules.
|
||||||
@@ -44,6 +45,11 @@ export const CAMPAIGN_VERSIONS = [
|
|||||||
chapters:C50B_CAMPAIGN.chapters, chapterSize:C50B_CAMPAIGN.chapterSize, levels:C50B_LEVELS,
|
chapters:C50B_CAMPAIGN.chapters, chapterSize:C50B_CAMPAIGN.chapterSize, levels:C50B_LEVELS,
|
||||||
legacyStorageKeys:[],
|
legacyStorageKeys:[],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id:C50C_CAMPAIGN.id, name:C50C_CAMPAIGN.name, description:C50C_CAMPAIGN.description,
|
||||||
|
chapters:C50C_CAMPAIGN.chapters, chapterSize:C50C_CAMPAIGN.chapterSize, levels:C50C_LEVELS,
|
||||||
|
legacyStorageKeys:[],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
export const DEFAULT_CAMPAIGN_VERSION = 'v0.5';
|
export const DEFAULT_CAMPAIGN_VERSION = 'v0.5';
|
||||||
export function campaignVersion(id) {
|
export function campaignVersion(id) {
|
||||||
|
|||||||
+13
-2
@@ -1,8 +1,19 @@
|
|||||||
// Generated by scripts/select-50.mjs --id=c50b — do not edit by hand.
|
// Generated by scripts/select-50.mjs --id=c50b — do not edit by hand.
|
||||||
|
//
|
||||||
|
// SEALED 2026-09-15, kept as a reference sample rather than as a candidate for
|
||||||
|
// shipping. 32 of its 50 levels are packed boards, which on playtest read as one
|
||||||
|
// level fifty times over: "体验太单一了,因为基本都是满盘". Two families the
|
||||||
|
// design brief calls out as the good-looking ones — 环绕 and 花瓣 — are absent
|
||||||
|
// from it entirely, filtered upstream by a crossing threshold of 1.0 that orbits
|
||||||
|
// sits just above (0% of orbits boards pass at 1.0, 84% pass at 1.2).
|
||||||
|
//
|
||||||
|
// It is worth keeping because the packed construction itself was judged good:
|
||||||
|
// the experience of clearing from the rim inward is what this set proved out.
|
||||||
|
// What it also proves is that one good idea at 64% is not a campaign.
|
||||||
export const C50B_CAMPAIGN = {
|
export const C50B_CAMPAIGN = {
|
||||||
"id": "c50b",
|
"id": "c50b",
|
||||||
"name": "五十关 · 满盘版",
|
"name": "五十关 · 满盘版(封存)",
|
||||||
"description": "外圈留空、内部填满的满盘为主,由外向内逐层清空;深锁关卡穿插其间做变奏。",
|
"description": "封存样本:满盘占 64%,实测体验过于单一。保留用于复盘满盘构造本身。",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
"起步",
|
"起步",
|
||||||
"转起来",
|
"转起来",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ test('every selectable edition retains its own level count, order and chapters',
|
|||||||
// Historical editions keep their exact shape; new ones are appended, because
|
// Historical editions keep their exact shape; new ones are appended, because
|
||||||
// other tests address the originals by position.
|
// other tests address the originals by position.
|
||||||
assert.deepEqual(CAMPAIGN_VERSIONS.map(c=>[c.id,c.levels.length,c.chapterSize,c.chapters.length]),[
|
assert.deepEqual(CAMPAIGN_VERSIONS.map(c=>[c.id,c.levels.length,c.chapterSize,c.chapters.length]),[
|
||||||
['v0.1',24,8,3],['v0.2',48,6,8],['v0.3',48,6,8],['v0.4',20,5,4],['v0.5',1000,50,20],['c50',50,10,5],['c50b',50,10,5],
|
['v0.1',24,8,3],['v0.2',48,6,8],['v0.3',48,6,8],['v0.4',20,5,4],['v0.5',1000,50,20],['c50',50,10,5],['c50b',50,10,5],['c50c',50,10,5],
|
||||||
]);
|
]);
|
||||||
assert.equal(campaignVersion('v0.1').levels[0].name,'转一转,路就通了');
|
assert.equal(campaignVersion('v0.1').levels[0].name,'转一转,路就通了');
|
||||||
assert.equal(campaignVersion('v0.1').levels.at(-1).name,'六角大满贯');
|
assert.equal(campaignVersion('v0.1').levels.at(-1).name,'六角大满贯');
|
||||||
|
|||||||
Reference in New Issue
Block a user