Files
NumberSwipe_New/campaign-50/tools/build.mjs
T

17 lines
7.3 KiB
JavaScript
Raw 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 fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {constraints,directionNames,violations} from '../rules.mjs';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),pack=JSON.parse(fs.readFileSync(path.join(root,'levels.json'),'utf8'));const rules=fs.readFileSync(path.join(root,'rules.mjs'),'utf8').replaceAll('export ',''),ui=fs.readFileSync(path.join(root,'ui.mjs'),'utf8').replace(/^import[^\n]+\n/gm,''),css=fs.readFileSync(path.join(root,'style.css'),'utf8');
const audio=fs.readFileSync(path.join(root,'audio.mjs'),'utf8').replaceAll('export ','');
const html=`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#ffffff"><title>数字一笔 · 53关DEMO</title><style>${css}</style></head><body><div id="app">
<section id="screen-select" class="screen"><header class="select-header"><h1 id="title">数字一笔</h1><p class="sub">53关DEMO · 一笔走遍每一格</p><div class="overall"><div class="overall-bar"><i id="overall-fill"></i></div><span id="progress" class="overall-text">0 / 53</span></div></header><div id="pack-list" class="pack-list"></div><button id="btn-continue" class="primary">继续</button><footer class="select-footer"><a href="../index.html" class="chip">原型入口</a><button id="btn-feedback" class="chip">试玩反馈</button></footer></section>
<section id="screen-play" class="screen hidden"><header class="topbar"><button id="btn-back" class="icon" aria-label="返回选关"><svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true"><path d="M15 5l-7 7 7 7" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button><div class="level-info"><div id="position" class="level-title"></div><div id="level-sub" class="level-sub"></div></div><button id="btn-sound" class="sound-button" aria-label="切换音效" aria-pressed="true">音效开</button><button id="btn-rules" class="rules-button" aria-label="查看本关规则">规则</button><div id="remaining" class="remaining" aria-live="polite"><svg class="remaining-icon tile" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="4.5" fill="none" stroke="currentColor" stroke-width="2.4"/></svg><svg class="remaining-icon check" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg><span id="remaining-n">0</span></div></header><div id="level-progress" class="level-progress" aria-hidden="true"></div><div id="caption" class="caption"></div><div id="constraints" aria-label="当前规则"></div><div id="board-wrap" class="board-wrap"><canvas id="board" aria-label="拖动或点按共边格画路"></canvas></div><div class="feedback-row" aria-label="操作反馈与下一关"><div id="status" class="toast" role="status" aria-live="polite"></div><button id="next" class="primary next-overlay" hidden>下一关 →</button></div><h2 id="goal" class="sr-only"></h2><p class="gesture-help">按住滑动连线 · 沿来时路逐格回退</p><footer class="bottombar"><button id="restart" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M4 12a8 8 0 1 0 2.5-5.8M4 4v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>重开</span></button><button id="undo" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M9 5l-5 5 5 5M4 10h10a6 6 0 0 1 0 12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>撤销</span></button><button id="hint" class="action"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M5 19v-7a4 4 0 0 1 4-4h10m-4-4 4 4-4 4" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>指路</span></button></footer></section>
<div id="rules-sheet" class="sheet-backdrop" hidden><section class="sheet" role="dialog" aria-label="本关规则"><h2>本关规则</h2><p id="rule-text"></p><div class="sheet-actions"><button id="close-rules" class="action">返回游戏</button></div></section></div>
<div id="feedback-sheet" class="sheet-backdrop" hidden><section class="sheet" role="dialog" aria-label="试玩反馈"><h2>试玩反馈</h2><p>记下有趣、枯燥、太难或规则不清楚的关卡,导出记录后用于调整。</p><textarea id="reasoning" aria-label="本关反馈" placeholder="例如:第18关有趣;第24关太难……"></textarea><div class="sheet-actions"><button id="close-feedback" class="action">关闭</button><button id="export" class="primary">导出全部记录</button></div></section></div>
</div><script>const PACK=${JSON.stringify(pack).replaceAll('<','\\u003c')};\n${rules}\n${audio}\n${ui}</script></body></html>`;
fs.writeFileSync(path.join(root,'index.html'),html);
// Offline release has no runtime file dependencies; the return link needs the original folder.
fs.writeFileSync(path.join(root,'NumberSwipe-53关DEMO.html'),html);fs.writeFileSync(path.join(root,'NumberSwipe-50关DEMO.html'),html);
let notes=JSON.parse(fs.readFileSync(path.join(root,'design.json'),'utf8'));for(const item of notes){const l=item.level,author=item.author;item.breakdown=constraints(l).map(r=>r.kind==='parity'?`${r.cell} 为第${author.indexOf(r.cell)+1}步,要求${r.value==='odd'?'奇':'偶'}`:r.kind==='direction'?`${r.cell} 必须${directionNames[r.value]}`:r.kind==='wall'?`墙 ${r.edge.join('—')} 封闭该邻接`:r.kind==='door'?`门 ${r.edge.join('—')} 通过前按已走格数切换,通过后保持开启;正确解从第${author.findIndex((id,i)=>i&&r.edge.includes(id)&&r.edge.includes(author[i-1]))}步末端通过`:'每步蓝橙交替');const first=item.witnesses[0];if(first){const fork=first.path.findIndex((id,i)=>id!==author[i]);item.keyFork={step:fork+1,correct:author[fork],tempting:first.path[fork]};item.reason=violations(l,first.path).map(v=>v.message).join(';');}else{item.keyFork={step:0,correct:author[0],tempting:'数字不符的候选路线'};item.reason='基础数字约束';}}
fs.writeFileSync(path.join(root,'design.json'),JSON.stringify(notes,null,2));
const md=['# 53关DEMO制作记录','', '53关均已制作具体坐标。目标难度来自策划,实际难度等待试玩;推理任务不因自动唯一性而被视为人解质量已通过。',''];for(const item of notes){const l=item.level;md.push(`## ${l.order} · ${l.title}`, '',`章节:${l.chapterTitle};节点${l.cells.length};角色${l.role};策划目标${l.targetDifficulty}。`, '',`作者解:${item.author.join(' → ')}。`, '',`原规划任务:${item.planningIntent}。`, '',`结构记录:无即时冲突选择点${item.metrics.noImmediateConflictChoices};最多连续同向${item.metrics.longestSameDirectionTransfers}次转移。此指标不是人类难度。`, '',...item.breakdown.map(s=>'- '+s), '',...item.witnesses.map(w=>`- 解除 ${w.constraint} 后的完整反例:${w.path.join(' → ')}`),'');}fs.writeFileSync(path.join(root,'DESIGN.md'),md.join('\n'));
console.log('Built index.html + standalone DEMO, and author/ablation design record.');