16 lines
5.4 KiB
JavaScript
16 lines
5.4 KiB
JavaScript
import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';import {fileURLToPath} from 'node:url';import {solve,constraints,violations,assertStructure,canAppend,graph} from '../rules.mjs';
|
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');const pack=JSON.parse(fs.readFileSync(path.join(root,'levels.json'),'utf8')),notes=JSON.parse(fs.readFileSync(path.join(root,'design.json'),'utf8'));const plan=JSON.parse(fs.readFileSync(path.join(root,'../design/demo-50/demo-design.json'),'utf8'));let checks=0;const results=[];function check(ok,msg){checks++;if(!ok)throw Error(msg);}
|
|
check(pack.levels.length===53,'必须53关');check(new Set(pack.levels.map(l=>l.id)).size===53,'ID不唯一');
|
|
check(pack.playOrder.length===53&&new Set(pack.playOrder).size===53&&pack.playOrder.every(id=>pack.levels.some(l=>l.id===id)),'试玩顺序必须覆盖53个唯一ID');check(pack.playOrder[10]==='tutorial-straight'&&pack.playOrder[11]==='tutorial-corner'&&pack.playOrder[17]==='tutorial-colour','首次机制前的引导顺序');
|
|
function geometricallyValid(l,p){const g=graph(l);return p.length===l.cells.length&&new Set(p).size===l.cells.length&&p.every((id,i)=>g.ids.has(id)&&(!i||g.adj[g.ids.get(p[i-1])].includes(g.ids.get(id))));}
|
|
for(let i=0;i<pack.levels.length;i++){const l=pack.levels[i],spec=i<50?plan.levels[i]:{levelId:pack.levels[i].id,nodeCountTarget:pack.levels[i].cells.length,features:pack.levels[i].features,unknownStart:false},note=notes.find(n=>n.level.id===l.id);assertStructure(l);check(l.id===spec.levelId&&l.order===i+1,'关序或稳定ID不一致');check(l.cells.length===spec.nodeCountTarget,'节点数量不一致');check(JSON.stringify(l.features)===JSON.stringify(spec.features),'机制组合不一致');check(Boolean(l.cells.some(c=>c.clue===1))!==spec.unknownStart,'起点策略不一致');
|
|
const paths=solve(l,{limit:2,nodeLimit:5000000});check(paths.length===1,'非唯一解 '+l.order);check(paths[0].join()===note.author.join(),'作者解不一致');check(geometricallyValid(l,paths[0]),'作者几何不合法');for(let k=0;k<paths[0].length;k++)check(canAppend(l,paths[0].slice(0,k),paths[0][k]),'操作与求解不一致 '+l.order);
|
|
const ablations=[];for(const r of constraints(l)){const alts=solve(l,{limit:2,omit:r.key,nodeLimit:5000000});check(alts.length>=2,'冗余机制 '+l.order+' '+r.key);const w=note.witnesses.find(w=>w.constraint===r.key);check(Boolean(w)&&geometricallyValid(l,w.path)&&!violations(l,w.path,{omit:r.key}).length&&violations(l,w.path).some(v=>v.key===r.key),'反例无效 '+l.order+' '+r.key);ablations.push({key:r.key,solutionsAfterRemoval:'至少2',witness:w.path});}
|
|
const numeric=[];for(const c of l.cells.filter(c=>c.clue!==undefined&&c.clue!==1&&!(l.order===1&&c.clue===l.cells.length))){const copy=structuredClone(l);delete copy.cells.find(x=>x.id===c.id).clue;const count=solve(copy,{limit:2,nodeLimit:5000000}).length;check(count>=2,'冗余非教学数字 '+l.order);numeric.push({cell:c.id,step:c.clue,necessary:true});}
|
|
results.push({order:l.order,id:l.id,title:l.title,chapter:l.chapter,role:l.role,nodeCount:l.cells.length,features:l.features,unique:true,allMechanismsNecessary:true,ablations,numericClues:numeric,structure:note.metrics,targetDifficulty:l.targetDifficulty,humanDifficultyCalibrated:false});}
|
|
for(const filename of ['index.html','NumberSwipe-50关DEMO.html']){const h=fs.readFileSync(path.join(root,filename),'utf8');check(!/<script[^>]+src=|fetch\(|type="module"/.test(h),'不是离线单文件');check(!h.includes('"author":')&&!h.includes('"witnesses":'),'答案字段进入试玩包');}
|
|
const prototypes=JSON.parse(fs.readFileSync(path.join(root,'reports/prototype-before.json'),'utf8'));const changed=prototypes.filter(p=>!fs.existsSync(p.path)||crypto.createHash('sha256').update(fs.readFileSync(p.path)).digest('hex')!==p.sha256).map(p=>p.path);check(!changed.length,'旧40关文件发生修改');
|
|
const original=JSON.parse(fs.readFileSync(path.join(root,'../reports/original-before.json'),'utf8').replace(/^\uFEFF/,''));const oldChanged=original.filter(p=>!fs.existsSync(p.path)||crypto.createHash('sha256').update(fs.readFileSync(p.path)).digest('hex').toUpperCase()!==p.hash).map(p=>p.path);check(!oldChanged.length,'原项目历史文件发生变化');
|
|
const report={checkedAt:new Date().toISOString(),passed:pack.levels.length,total:pack.levels.length,checks,uniqueAll:true,mechanismsNecessaryAll:true,prototypesChecked:prototypes.length,prototypeFilesChanged:changed,originalHistoricalFilesChecked:original.length,originalHistoricalFilesChanged:oldChanged,scope:'关卡数学与操作规则验收;不代表真人难度、关卡意图或真机体验校准',results};fs.writeFileSync(path.join(root,'reports/validation.json'),JSON.stringify(report,null,2));
|
|
fs.writeFileSync(path.join(root,'reports/VALIDATION.md'),'# 53关DEMO自动验收\n\n'+`53/53关恰好一条完整有序解;每条新约束逐项必要;检查${checks}项。既有40关${prototypes.length}个文件校验值不变;原项目历史${original.length}文件不变。\n\n`+'|关|标题|格数|必要机制数|目标分|结构选择点|\n|---:|---|---:|---:|---:|---:|\n'+results.map(r=>`|${r.order}|${r.title}|${r.nodeCount}|${r.ablations.length}|${r.targetDifficulty}|${r.structure.noImmediateConflictChoices}|`).join('\n')+'\n\n目标分非实测,结构选择点不是人类难度。盘面自动通过不代表规划推理意图已由玩家验证。\n');console.log(`PASS 53/53; ${checks} checks; old40 ${prototypes.length} files unchanged.`);
|