Files
HexTrails/scripts/write-production-report.mjs

97 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFile, writeFile } from 'node:fs/promises';
const root = new URL('../', import.meta.url);
const read = async path => JSON.parse(await readFile(new URL(path, root), 'utf8'));
const [levels, manifest, report, validation] = await Promise.all(['levels/v0.5.json', 'levels/v0.5-recipes.json', 'levels/v0.5-analysis.json', 'levels/v0.5-validation.json'].map(read));
if (levels.length !== 1000 || validation.result !== 'passed' || validation.pipelineHash !== manifest.pipelineHash) throw new Error('Run levels:v05:verify against this exact edition first');
const rows = report.levels, sum = report.summary;
const count = values => values.reduce((result, value) => ((result[value] = (result[value] || 0) + 1), result), {});
const primary = (row, policy) => policy.paths.some(path => path.id === row.reward.heroId && path.bends >= manifest.recipes[row.level - 1].slot.minHeroBends);
const minLong = Math.min(...rows.map(row => row.policies.filter(p => p.longPaths > 0).length));
const minPrimary = Math.min(...rows.map(row => row.policies.filter(p => primary(row, p)).length));
const families = { petals: '花瓣', orbits: '环绕', spiral: '螺旋', switchback: '回折', weave: '编织', crossing: '双叶交叉' };
const kinds = { tour: '长路巡游', gate: '清挡放行', contrast: '轻巧反差', turn: '旋转换路', stages: '分段释放' };
const chapters = manifest.campaign.chapters.map((name, i) => {
const group = rows.slice(i * 50, i * 50 + 50), tiers = count(group.map(row => row.difficulty.tier));
return `| ${i + 1} · ${name} | ${i * 50 + 1}–${i * 50 + 50} | ${Array.from({ length: 5 }, (_, tier) => tiers[tier + 1] || 0).join(' / ')} | ${Math.min(...group.map(r => r.tiles))}–${Math.max(...group.map(r => r.tiles))} | ${group.filter(r => r.difficulty.requiredRotations > 0).length} | [总览](v0.5-review/contact-${String(i + 1).padStart(2, '0')}.png) |`;
});
const policies = Object.entries(sum.byPolicy).map(([name, value]) => `| ${name} | ${value.runs} | ${value.won} | ${value.longRoute} |`);
const text = `# v0.5「千段轨迹」1000 关制作报告
日期:2026-09-12。首版已经冻结 1000 个可玩布局,20 章 × 50 关,直接接入原游戏运行时。旧版布局与独立进度保留。当前交付是经过规则、策略和静态画面检查的试玩版本;没有把自动通过当作已证明千关长期体验优良。
- [从第 1 关开始](http://localhost:4173/?campaign=v0.5&level=1)
- [第 1000 关](http://localhost:4173/?campaign=v0.5&level=1000)
- [制作审核页](http://localhost:4173/labs/campaign-review/index.html)
## 实际产出与验收
| 项目 | 本批结果 |
| --- | ---: |
| 冻结布局 / 独立求解成功 | ${levels.length} / ${validation.independentlySolved} |
| 忽略颜色、名称并整体旋转/镜像归一后的不同布局 | ${validation.uniqueLayouts} |
| 完整回放(作者解 + 最少旋转解 + 20 种策略) | ${validation.completeReplays} |
| 通过回放的成功操作 | ${validation.successfulActions} |
| 需要旋转 / 不需要旋转 | ${sum.rotationRequired} / ${levels.length - sum.rotationRequired} |
| 最少旋转数的最高值 | ${sum.maxRequiredRotations} |
| 最少旋转解中有长路的关卡 | ${sum.longRouteLevels} |
| 最少旋转解中单次最长格步骤 / 最多转弯 | ${sum.longestSteps} / ${sum.longestBends} |
| 每关 20 策略中出现长路的最低次数 | ${minLong} |
| 每关 20 策略中出现指定主长路的最低次数 | ${minPrimary} |
长路的统计带是至少 6 次转弯;主目标还须满足该关制作卡的最低转弯数。格步骤允许一条路经过同一格的不同轨道,不等于不同格子的数量。以上是本引擎模型中的观察量,不是玩家通关率、观看率或平均时长。
## 形态、体验和难度
| 路线家族 | 关数 | 体验类型 | 关数 |
| --- | ---: | --- | ---: |
${Object.entries(families).map(([id, name], i) => { const k = Object.keys(kinds)[i]; return `| ${name} | ${sum.families[id] || 0} | ${k ? kinds[k] : '合计'} | ${k ? sum.experiences[k] || 0 : 1000} |`; }).join('\n')}
这六类是可变开口、可变规模和布子的路线家族,不是 1000 种独立美术外形。花瓣与编织使用棋子真正可以行驶的轨道配对。作者主棋子走过的配对中,属于强调造型的最低比例为 ${(sum.minHeroShapeRatio * 100).toFixed(1)}%;完整作者清盘覆盖全盘强调配对的最低比例为 ${(sum.minShapeUsage * 100).toFixed(1)}%。两个分母不同:主路确实属于造型,不代表每关会把整朵花的所有路线走遍。
结构难度 D1–D5 的实际数量为 ${Array.from({ length: 5 }, (_, i) => sum.difficultyTiers['D' + (i + 1)] || 0).join(' / ')}。分数使用必要旋转、放行深度、开局可走比例和扫掠阻挡,不加入长路转弯数;全部标记为未经玩家校准的结构预估。后期继续有直接巡游和小盘,不把关号、盘面尺寸或棋子数直接等同于难度。
| 章节 | 关号 | D1 / D2 / D3 / D4 / D5 | 棋子数范围 | 需旋转关数 | 画面 |
| --- | --- | --- | --- | ---: | --- |
${chapters.join('\n')}
## 制作逻辑与质量修正
先构造有形态的开放路线,再逆向布子并保存完整构造解。换路关与反差关包含旋转前互相阻挡、旋转后可放行的核心棋子;分段关使用经正式碰撞与求解器验证的两次换路锁组。每个槽位在同一家族和体验内比较合格候选,避免只追求一个总分。
每个清挡关的指定短路门在开局即可移走,移走后主棋子立刻获得长路;作者回放从这个关键动作开始。巡游关开局可连续点选。反差关主棋子朝内,旋转一次即可走上长路;分段关的最少解也必须有至少两次旋转。
独立审核曾发现旧候选中“另一枚长路替主目标过检”、关键长路过晚及随机探索大量绕过的问题。本批增加了指定主棋子的单独门槛:核心六策略至少四条走到主长路,20 条中至少 16 条有长路、至少 10 条走到主长路;清挡与反差另查实际兑现的放行事件。这个筛选阈值是制作约束,不是研究证明的最优比例。
视觉审核还退回了过于相似的 849/850 连排:849 改为中盘编织巡游,850 保留为章末重瓣分段释放。小盘布子受到实际容量限制,避免通过堆积棋子延长关卡。
## 操作顺序的证据边界
四种“先清所有可出棋子再转”的常规策略可能得到相同统计,不能视作四份独立玩家证据。表中每行只是一个确定的策略。14 条额外随机探索与核心策略分别记录;额外的未参与筛选种子由独立质量审核报告说明。
| 策略 | 回放数 | 清盘数 | 出现长路数 |
| --- | ---: | ---: | ---: |
${policies.join('\n')}
## 画面、浏览器与尚未验证的范围
- [前 500 关静态审阅](v0.5-visual-audit-early.md)、[后 500 关静态审阅](v0.5-visual-audit-late.md):20 张逐章联系表的全局目检,不冒充 1000 次手玩。
- [浏览器记录](v0.5-browser-review.md):快速连点、清挡即发、反差、分段解、第 1000 关结算、选关/版本/缩放的实际样本与尺寸。
- [独立质量审核](v0.5-quality-audit.md):全量复查、主奖励出现时机、未参与筛选的策略与后续复审项。
- \`levels/v0.5-reviews.json\` 单独保存 AI 辅助静态审阅与浏览器样本状态;布局分析中的 \`visual: pending\` 是生成时快照,审核页会并列显示后续记录。
客户端读取冻结关卡,不在进入每关时实时生成。当前实现按章显示 50 项,但关卡模块仍一次载入全部数据;本地加载验证不等同于移动网络性能验证。真人新手/熟练玩家的连续游玩、疲劳、图案辨识度及留存尚未测量。原生 Cocos/Android/iOS 运行与实机触控没有在本轮验收。
## 文件与复现
布局全集:\`levels/v0.5.json\`;分章:\`levels/v0.5/chapter-01.json\` 至 \`chapter-20.json\`;浏览器模块:\`src/levels-v05.js\`。逐关配方在 \`levels/v0.5-recipes.json\`,策略与实际路径在 \`levels/v0.5-analysis.json\`,本批独立验证在 \`levels/v0.5-validation.json\`。
流水线 SHA-256:\`${manifest.pipelineHash}\`。
\`npm run levels:v05\` 离线重新生产;\`npm run levels:v05:verify\` 重求解并回放冻结数据;\`node scripts/write-production-report.mjs\` 只对已通过对应哈希验证的数据更新本报告。源码指纹改变时不允许混用旧检查点。
方法与最初假设见[千关制作方案](1000-level-production-plan.md)、[设计准则](level-design-principles-v2.md)和[论文与竞品知识库](level-design-knowledge/README.md)。
`;
await writeFile(new URL('docs/v0.5-production-report.md', root), text);
console.log('Wrote docs/v0.5-production-report.md from verified frozen data.');