Files
NumberSwipe_New/campaign-200/tools/validate.mjs
T

6 lines
4.3 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,units,sideBetween} from '../rules.mjs';import {signature} from './fingerprint.mjs';
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),read=name=>JSON.parse(fs.readFileSync(path.join(root,name))),pack=read('levels.json'),notes=read('design.json'),plan=read('plan.json');let checks=0;const rows=[],fingerprints=new Set(),introduced=new Set();function check(ok,message){checks++;if(!ok)throw Error(message);}
check(pack.levels.length===200,'必须200关');check(new Set(pack.levels.map(l=>l.id)).size===200,'ID重复');check(pack.playOrder.join()===pack.levels.map(l=>l.id).join(),'试玩顺序不一致');for(const l of pack.levels){const n=notes.find(n=>n.level.id===l.id),s=plan.levels[l.order-1];check(l.order===s.displayOrder&&l.id===s.levelId,'编号不一致');assertStructure(l);check(l.cells.length===s.nodeCountTarget,'格数错误 '+l.order);const relevant=[...l.features,...(l.geometry==='hex'?['hex']:[]),...(l.cells.some(c=>c.shape)?['irregular']:[]),...(!l.cells.some(c=>c.clue===1)?['unknown-start']:[])];for(const f of relevant){if(!introduced.has(f))check(Boolean(l.tutorial),'首次机制缺少教学 '+l.order+' '+f);introduced.add(f);}if(l.tutorial)check(l.cells.length<=6,'教学不够极简');else{const fp=signature(l);check(!fingerprints.has(fp),'镜像/旋转重复 '+l.order);fingerprints.add(fp);}
const answers=solve(l,{limit:2,nodeLimit:7000000});check(answers.length===1,'非唯一 '+l.order);check(answers[0].join()===n.author.join(),'作者解不匹配');for(let i=0;i<n.author.length;i++)check(canAppend(l,n.author.slice(0,i),n.author[i]),'不能按作者解操作');const ablations=[];for(const c of constraints(l)){const alt=solve(l,{limit:2,omit:c.key,nodeLimit:7000000});check(alt.length>=2,'冗余机制 '+l.order+' '+c.key);const w=n.witnesses.find(w=>w.constraint===c.key);check(w&&!violations(l,w.path,{omit:c.key}).length&&violations(l,w.path).some(v=>v.key===c.key),'必要性反例错误');ablations.push(c.key);}for(const cell of l.cells.filter(c=>c.clue!==undefined&&c.clue!==1)){const copy=structuredClone(l);delete copy.cells.find(c=>c.id===cell.id).clue;check(solve(copy,{limit:2,nodeLimit:7000000}).length>=2,'冗余辅助数字 '+l.order+' '+cell.id);}if(l.doors&&l.cells.length<=12)check(JSON.stringify(solve(l,{prune:false}))===JSON.stringify(answers),'门剪枝错误');rows.push({order:l.order,title:l.title,nodeCount:l.cells.length,geometry:l.geometry,features:l.features,tutorial:l.tutorial||null,unique:true,necessary:ablations.length,targetDifficulty:l.targetDifficulty,role:l.role});}
const before=read('reports/demo53-before.json');const changed=before.filter(b=>!fs.existsSync(b.path)||crypto.createHash('sha256').update(fs.readFileSync(b.path)).digest('hex')!==b.sha256);check(!changed.length,'53关DEMO发生修改 '+changed.map(x=>x.path));const old=JSON.parse(fs.readFileSync(path.join(root,'../reports/original-before.json'),'utf8').replace(/^\uFEFF/,''));check(old.every(b=>fs.existsSync(b.path)&&crypto.createHash('sha256').update(fs.readFileSync(b.path)).digest('hex').toUpperCase()===b.hash),'原项目历史文件被修改');const html=fs.readFileSync(path.join(root,'index.html'),'utf8');check(!/<script[^>]+src=|fetch\(|type="module"/.test(html),'不是离线单文件');check(!html.includes('"author":')&&!html.includes('"witnesses":'),'作者解被编入试玩包');check(html.includes('variant:campaign-200:v1'),'存档未隔离');const report={checkedAt:new Date().toISOString(),passed:200,total:200,checks,allUnique:true,allMechanismsNecessary:true,originalDemoFilesChecked:before.length,originalDemoFilesChanged:changed,scope:'数学、教学时机、去重与隔离;目标难度仍待真人校准',rows};fs.writeFileSync(path.join(root,'reports/validation.json'),JSON.stringify(report,null,2));fs.writeFileSync(path.join(root,'reports/VALIDATION.md'),'# 200关验收\n\n全部200关唯一解,每项机制必要,首次新机制均有极简教学。\n\n检查 '+checks+' 项;原53关DEMO '+before.length+' 文件未变;原项目历史 '+old.length+' 文件未变。\n');console.log('PASS 200/200;',checks,'checks; DEMO53 unchanged',before.length,'files');