#!/usr/bin/env node // Merge a sharded prototype sweep into one candidate pool, and apply the gate // that only a wide sweep can afford. // // node scripts/build-pool.mjs --in= [--out=bench/pool.json] // [--maxTightShare=0.005] [--maxDeadTurns=1] // // prototype-d5 gates what a single sweep can pay for. Two more are left to this // step because together they cost nearly 60% of the yield. // // `maxDeadTurns` is the longest run of turns a player can make, from a stuck // state, before anything opens — the design brief's 无效转动就会很无聊, counted. // At 1 the worst case is one turn of suspense and then a release. Round 1 shipped // eleven levels at 2, and those are the ones the agent kept writing // 短暂以为转错了方向 about. // // It is deliberately NOT `doubleDead`, which was the first thing tried here. // That counts dead states rather than consecutive dead turns, so it scales with // the size of the state space: depth-3 levels have ~187 reachable stuck states // and depth-5 ones ~739, which made the raw count look eight times worse for // deep levels when the *rate* is 2% against 5%. Gating on it wiped out every // level past three turns — the opposite of what the brief asks for, since // turning is the game's signature and should not be rationed. import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { deadTurnAudit, deepLevelKey, trackShape } 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 OUT = resolve(args.out ?? 'bench/pool.json'); const MAX_TIGHT = Number(args.maxTightShare ?? 0.005); const MAX_DEAD = Number(args.maxDeadTurns ?? 1); const MAX_CROSS = Number(args.maxCrossPerCell ?? 1.0); // Shards from a sweep, or level files listed directly. const sources = String(args.in ?? 'bench/shards').split(',').map(s => resolve(s.trim())); const files = sources.flatMap(src => { try { return readdirSync(src).filter(f => f.endsWith('.json')).map(f => resolve(src, f)); } catch { return [src]; } // not a directory: a file path }); const all = files.flatMap(f => { try { const raw = JSON.parse(readFileSync(f, 'utf8')); return Array.isArray(raw) ? raw : raw.levels ?? []; } catch { return []; } }); const seen = new Set(); const unique = []; for (const level of all) { const key = deepLevelKey(level); if (seen.has(key)) continue; seen.add(key); unique.push(level); } // Levels harvested from the shipped set arrive without an audit. Being shipped // is not evidence of passing a gate that did not exist when they shipped. let audited = 0; for (const level of unique) { if (level.prototype?.audit) continue; level.prototype = { ...(level.prototype ?? {}), audit: deadTurnAudit(level) }; audited++; } const open = unique.filter(l => (l.prototype.audit.endgameTightShare ?? 1) <= MAX_TIGHT); const quiet = open.filter(l => (l.prototype.audit.maxDeadTurns ?? Infinity) <= MAX_DEAD); const kept = quiet.filter(l => (l.prototype.shape ?? trackShape(l)).crossPerCell <= MAX_CROSS); const tally = (rows, f) => rows.reduce((m, l) => { const k = f(l); m[k] = (m[k] || 0) + 1; return m; }, {}); const rot = l => l.prototype?.stats?.rotations ?? '?'; const fam = l => l.prototype?.family ?? (String(l.id).startsWith('trail-') ? 'v05' : '?'); console.log(`${files.length} 个来源合计 ${all.length} → 去重 ${unique.length}${audited ? `(其中 ${audited} 关现场补算了 audit)` : ''}`); console.log(` 终局紧局 ≤${(MAX_TIGHT*100).toFixed(1)}% → ${open.length}(${Math.round(100 * open.length / unique.length)}%)`); console.log(` 连续空转 ≤${MAX_DEAD} → ${quiet.length}(${Math.round(100 * quiet.length / unique.length)}%)`); console.log(` 交叉/格 ≤${MAX_CROSS} → ${kept.length}(${Math.round(100 * kept.length / unique.length)}%)`); console.log(`旋转深度 ${JSON.stringify(tally(kept, rot))}`); console.log(`家族 ${JSON.stringify(tally(kept, fam))}`); writeFileSync(OUT, JSON.stringify({ generated: 'build-pool', sources: files.length, swept: all.length, count: kept.length, gates: { maxTightShare: MAX_TIGHT, maxDeadTurns: MAX_DEAD }, levels: kept }, null, 1)); console.log(`已写入 ${OUT}`);