feat(motion): raise the cruising speed, and make the show gates follow it

Pieces still read as slow. Cruising speed goes from 120ms per unit to 85: the
median departure across the shipped fifty drops 970ms → 763ms and a level's
whole animation budget 15.6s → 12.7s, about 18% off. The longest departures do
not move — the cap holds them at 3.46s — so if those still drag, the dial is the
cap, which was settled separately and is left alone.

The gates that guarantee a level opens with something worth watching are stated
in milliseconds, and they were picked while a piece crossed a unit in 120ms.
Held fixed while the speed changes, they stop meaning what they meant: this
change alone made 20 of the 50 shipped levels "fail" an opening gate every one
of them had passed, purely from the unscaled number. Scaled to the dial, none
fail — the set is unchanged, and the alarm was an artifact of the constant. So
the cruise threshold and the opening floor now derive from the speed, naming
the speed they were calibrated at.

exitTiming takes msPerUnit the way it already took knee and maxMs, and the
comparison lab gets a speed slider, reads the levels actually shipping rather
than the prototype sweep they were selected from, and totals every route in the
campaign instead of the four on screen — those four skew long, where the cap
flattens any speed change to 6%.

The test that pins constant cruising speed now reads the dial from the token
instead of hard-coding 120, so tuning motion no longer looks like a broken
guarantee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cabinw
2026-09-14 18:46:32 +08:00
co-authored by Claude Opus 5
parent 34e6e73016
commit 073b682234
9 changed files with 88 additions and 43 deletions
+6 -3
View File
@@ -6,7 +6,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { Game, random } from '../src/core.js';
import { createRouteBoard, ROUTE_FAMILIES } from '../src/route-grammar.js';
import { createDeepLock, growDeepLevel, deepLevelStats, deepLevelKey, deadTurnAudit, contrastStats, showStats } from '../src/deep-lock.js';
import { createDeepLock, growDeepLevel, deepLevelStats, deepLevelKey, deadTurnAudit, contrastStats, showStats, openingShowFloor } 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 SEEDS = Number(args.seeds ?? 8), TARGET = Number(args.target ?? 12), KEEP = Number(args.keep ?? 18);
@@ -84,7 +84,10 @@ const grownEnough = rows.filter(r => r.stats.pieces >= MIN_PIECES);
// an agent give up. It rejects exactly the levels every model stalled on.
const MIN_RELEASE = Number(args.minRelease ?? 2);
const MIN_CONTRAST = Number(args.minContrast ?? 12);
const MIN_OPENING_SHOW = Number(args.minOpeningShow ?? 800);
// Expressed at the speed it was calibrated at (120ms per unit) and scaled to
// whatever the speed dial is now, so re-tuning motion does not quietly reject
// levels that were fine. See openingShowFloor in deep-lock.
const MIN_OPENING_SHOW = openingShowFloor(Number(args.minOpeningShow ?? 800));
const MIN_OPENING_CHOICES = Number(args.minOpeningChoices ?? 3);
const MIN_CRUISES = Number(args.minCruises ?? 3);
// Rotation count is deliberately not capped: turning is free and it is the
@@ -116,7 +119,7 @@ console.log(`\n【门槛】长成 ${grownEnough.length} → 首转双向 ${g1.le
const g6 = g5.filter(r => r.contrast.contrast >= MIN_CONTRAST);
const g7 = g6.filter(r => r.show.openingChoices >= MIN_OPENING_CHOICES);
const g8 = g7.filter(r => r.show.openingBestMs >= MIN_OPENING_SHOW);
console.log(` → 开局可点≥${MIN_OPENING_CHOICES} ${g7.length} → 开局演出≥${(MIN_OPENING_SHOW / 1000).toFixed(1)}s ${g8.length} → 长巡游≥${MIN_CRUISES} ${playable.length}`);
console.log(` → 开局可点≥${MIN_OPENING_CHOICES} ${g7.length} → 开局演出≥${(MIN_OPENING_SHOW / 1000).toFixed(2)}s ${g8.length} → 长巡游≥${MIN_CRUISES} ${playable.length}`);
console.log(`【候选池】旋转深度 ${JSON.stringify(playable.reduce((m, r) => (m[r.stats.rotations] = (m[r.stats.rotations] || 0) + 1, m), {}))}`);
console.log(` 反差 中位 ${med(playable.map(r => r.contrast.contrast)).toFixed(1)} · 家族 ${JSON.stringify(playable.reduce((m, r) => (m[r.family] = (m[r.family] || 0) + 1, m), {}))}`);
const KEEP_ALL = String(args.keep) === 'all';