#!/usr/bin/env node // Harvest the shallow end of the campaign from the shipped v0.5 set. // // node scripts/harvest-easy.mjs [--set=levels/v0.5.json] [--out=bench/pool-easy.json] // // The deep-lock construction always needs three turns, so it cannot supply the // opening dozen levels. v0.5 can — but only the small fraction of it that holds // up: most of that set is clearable by tapping at random, which is the failure // the monkey threshold exists to catch. import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { Game, random, solveRotations } from '../src/core.js'; import { showStats, contrastStats } from '../src/deep-lock.js'; const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...v] = a.replace(/^--/, '').split('='); return [k, v ?? true]; })); const SET = resolve(args.set ?? 'levels/v0.5.json'); const OUT = resolve(args.out ?? 'bench/pool-easy.json'); const MIN_APT = Number(args.minAPT ?? 3), MIN_CONTRAST = Number(args.minContrast ?? 10); const MIN_OPENING_SHOW = Number(args.minOpeningShow ?? 900), MIN_OPENING_CHOICES = Number(args.minOpeningChoices ?? 3); const MIN_CRUISES = Number(args.minCruises ?? 3); function monkeyActionsPerPiece(level, runs = 4) { let total = 0; for (let run = 0; run < runs; run++) { const rng = random(4242 + run * 31), game = new Game(level); let actions = 0, streak = 0; while (!game.won && !game.lost && actions < 800) { if (streak >= 3) { game.rotate(rng() < .5 ? 1 : -1); actions++; streak = 0; continue; } const tile = game.tiles[Math.floor(rng() * game.tiles.length)]; actions++; if (game.tap(tile.id).status === 'exit') streak = 0; else streak++; } total += game.won ? actions / level.tiles.length : 999; } return total / runs; } const raw = JSON.parse(readFileSync(SET, 'utf8')); const levels = Array.isArray(raw) ? raw : raw.levels; const kept = []; const rejected = { unsolvable: 0, zeroRotation: 0, show: 0, contrast: 0, monkey: 0 }; for (const level of levels) { const solution = solveRotations(level); if (!solution.solvable) { rejected.unsolvable++; continue; } if (solution.rotations < 1) { rejected.zeroRotation++; continue; } const show = showStats(level); if (!show || show.openingChoices < MIN_OPENING_CHOICES || show.openingBestMs < MIN_OPENING_SHOW || show.cruises < MIN_CRUISES) { rejected.show++; continue; } const contrast = contrastStats(level); if (contrast.contrast < MIN_CONTRAST) { rejected.contrast++; continue; } const apt = monkeyActionsPerPiece(level); if (apt < MIN_APT) { rejected.monkey++; continue; } const copy = JSON.parse(JSON.stringify(level)); copy.prototype = { family: 'v05', radius: null, source: 'v0.5', stats: { rotations: solution.rotations, pieces: level.tiles.length, lockedShare: 0, minRelease: 1 }, contrast, show, monkeyActionsPerPiece: apt, }; kept.push(copy); } mkdirSync(dirname(OUT), { recursive: true }); writeFileSync(OUT, JSON.stringify({ generated: 'harvest-easy', source: SET, count: kept.length, levels: kept }, null, 1)); console.log(`采集 ${kept.length} / ${levels.length} 关 → ${OUT}`); console.log(` 刷掉:0 转 ${rejected.zeroRotation} · 演出 ${rejected.show} · 反差 ${rejected.contrast} · 乱点即过 ${rejected.monkey}`); console.log(` 旋转分布 ${JSON.stringify(kept.reduce((m, l) => (m[l.prototype.stats.rotations] = (m[l.prototype.stats.rotations] || 0) + 1, m), {}))}`);