import fs from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {definitions,difficulties} from './definitions.mjs'; import {solve,violations,constraints,directionAt,assertStructure,graph} from './engine-template.mjs'; const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'); let seed=20261012;function random(){seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296;}function pick(a){return a[Math.floor(random()*a.length)];}function shuffle(a){return [...a].sort(()=>random()-.5);} function square(index){const dims=[[3,2],[3,3],[4,3],[4,3],[4,4],[4,4],[4,5],[4,5]][index],removed=[[],[[2,2]],[[0,2],[3,2]],[],[[0,3],[3,3]],[],[[0,4],[3,4]],[[3,4]]][index];let cells=[];for(let y=0;yp[0]===x&&p[1]===y))cells.push({id:'c'+cells.length,x,y,w:1,h:1});return cells;} function hex(index){const count=[6,8,11,12,14,16,18,19][index];let cells=[];const rows=index>=4?3:2;for(let q=0;cells.length[c.x+','+c.y,c])); const merges=index<4?1:2;for(let m=0;mc.w===1&&c.h===1&&!c.shape&&owner.has((c.x+1)+','+c.y)).filter(c=>{let b=owner.get((c.x+1)+','+c.y);return b&&b!==c&&b.w===1&&b.h===1&&!b.shape;});if(!choices.length)break; const a=choices[(variant+m*3)%choices.length],b=owner.get((a.x+1)+','+a.y);a.w=2;cells=cells.filter(c=>c!==b);owner.set(b.x+','+b.y,a); } if(index>=4){const a=cells.find(c=>c.w===2&&!c.shape&&cells.some(b=>b.x===c.x&&b.y===c.y+1&&b.w===1&&b.h===1&&!b.shape));if(a){const b=cells.find(b=>b.x===a.x&&b.y===a.y+1&&b.w===1&&b.h===1&&!b.shape);a.shape=[[0,0],[1,0],[0,1]];a.h=2;cells=cells.filter(c=>c!==b);}} return cells.map((c,i)=>({...c,id:'c'+i}));} function setup(def,index,cells,author){const level={id:String(index+1).padStart(2,'0'),mode:def.id,geometry:def.geometry,difficulty:difficulties[Math.floor(index/2)],title:def.goals[index],caption:index<2?def.rule:'',cells:structuredClone(cells)}; if(index!==7)level.cells.find(c=>c.id===author[0]).clue=1; const n=author.length;const at=i=>author[Math.max(0,Math.min(n-1,i))]; if(def.id==='relative'){ let pairs=index===0?[[1,4]]:index===1?[[1,5]]:index===2?[[2,n-3]]:index===3?[[1,Math.floor(n/2)],[Math.floor(n/2)+1,n-2]]:index===4?[[2,Math.floor(n/2)],[2,n-2]]:index===6?[[1,Math.floor(n/2)],[3,n-2],[Math.floor(n/2),n-1]]:[[2,Math.floor(n/2)+1],[Math.floor(n/2)-1,n-2]]; if(random()<.6&&index>1)pairs=pairs.map(([a,b])=>[Math.max(1,a+Math.floor(random()*2)),Math.min(n-1,b)]); if(index===1){const g=graph(level),near=[];for(let a=1;a({from:at(a),to:at(b),delta:b-a})); }else if(def.id==='order'){ const offsets=index===0?[1,4]:index===1?[1,n-2]:[1,Math.floor(n/2),n-2]; level.orderChains=index<3?[offsets.map(at)]:index===3?[[at(1),at(Math.floor(n/2))],[at(Math.floor(n/2)+1),at(n-2)]]:index===4?[[at(1),at(Math.floor(n/2))],[at(3),at(Math.floor(n/2))]]:index>=6?[[at(1),at(Math.floor(n/2)),at(n-2)],[at(3),at(Math.floor(n/2)+2),at(n-1)]]:[[at(2),at(Math.floor(n/2)),at(n-2)]]; if(index===1){const g=graph(level),near=author.map((id,i)=>i).filter(i=>i>=3&&g.adj[g.ids.get(at(0))].includes(g.ids.get(at(i))));if(!near.length)return null;const b=pick(near);level.orderChains=[[at(1+Math.floor(random()*(b-1))),at(b)]];}if(index>=6){const markers=shuffle(Array.from({length:n-2},(_,i)=>i+1)).slice(0,6).sort((a,b)=>a-b);level.orderChains=[[at(markers[0]),at(markers[2]),at(markers[4])],[at(markers[1]),at(markers[3]),at(markers[5])]];} }else if(def.id==='window'){ const count=index<3?1:index<6?2:3;let indices=shuffle(Array.from({length:n-3},(_,i)=>i+2)).slice(0,count); if(index===5)indices=[Math.floor(n/2)-1,Math.floor(n/2)+1]; level.windows=indices.map(i=>{const span=index<2?2:index===4?4:2+Math.floor(random()*3);const lo=Math.max(1,Math.min(n-span,i+1-Math.floor(random()*(span+1))));return {cell:at(i),min:lo,max:lo+span};}); }else if(def.id==='direction'){ const kinds=['lr','du','ru','dr','dl','lu'];const pool={};for(const kind of kinds)pool[kind]=[]; for(let i=1;ipool[k].length):[requested];if(!available.length)return null;const kind=pick(available);if(!pool[kind].length)return null;const id=pick(pool[kind]);pool[kind].splice(pool[kind].indexOf(id),1);level.cells.find(c=>c.id===id).direction=kind;selected.push(kind);} if(index===3&&selected[0]===selected[1])return null; }else if(def.id.includes('alternation')){for(let i=0;ic.id===author[i]).category=i%2?'triangle':'circle';if(def.geometry==='hex'){const g=graph(level);let edges=[];for(let a=0;a=2:i>=Math.floor(n/3))eligible.push(i);if(!eligible.length)return null;const e=pick(eligible);level.doors=[{edge:[at(e),at(e+1)],initialOpen:false}];} else if(def.id==='gates'){ let count=index<3?1:index<6?2:3;const gates=[];const used=new Set(); const crossed=[];for(let j=0;j0)low=crossed[0]+1;let high=e-1;if(index===3&&j>0)high=Math.min(high,crossed[0]-1);if(index===7&&j===2)high=Math.min(high,crossed[1]-1);let candidates=Array.from({length:Math.max(0,high-low+1)},(_,k)=>low+k).filter(k=>!used.has(k));if(!candidates.length)return null;let s=index===7&&j===2?pick(candidates.filter(k=>k>crossed[0]) .length?candidates.filter(k=>k>crossed[0]):candidates):pick(candidates);used.add(s);gates.push({edge:[at(e),at(e+1)],switchCell:at(s)});} level.gates=gates; } return level; } function selectClues(level,author,base,index){ let paths=base;if(index!==7)paths=paths.filter(p=>p[0]===author[0]); const atoms=constraints(level),keys=atoms.map(a=>a.key);if(!keys.length)return null; const masks=paths.map(p=>new Set(violations(level,p,{ignoreClues:true}).map(v=>v.key))); let survivors=paths.map((_,i)=>i).filter(i=>masks[i].size===0); let witnesses=keys.map(key=>paths.map((_,i)=>i).filter(i=>masks[i].size===1&&masks[i].has(key))); if(witnesses.some(w=>!w.length))return null;if(level.mode==='window'&&index===1){witnesses=witnesses.map((w,j)=>w.filter(i=>{const r=level.windows[j];return paths[i].indexOf(r.cell)+1!w.length))return null;} const forbidden=new Set([...(level.relations||[]).flatMap(r=>[r.from,r.to]),...(level.windows||[]).map(w=>w.cell)]); const candidates=shuffle(author.map((id,i)=>({id,step:i+1})).filter(c=>!forbidden.has(c.id)&&c.step!==1&&c.step!==author.length)); let chosen=[];while(survivors.length>1){let best=null; for(const c of candidates.filter(c=>!chosen.includes(c))){let matched=i=>paths[i][c.step-1]===c.id;const kept=survivors.filter(matched);if(kept.length===survivors.length)continue;const ws=witnesses.map(w=>w.filter(matched));if(ws.some(w=>!w.length))continue; if(!best||kept.length (index<2?3:index<6?4:5))return null; } for(const c of chosen)level.cells.find(x=>x.id===c.id).clue=c.step; if(survivors.length!==1||paths[survivors[0]].join()!==author.join())return null; // Remove individually redundant numerical clues, keeping entry start teaching. for(const c of shuffle(level.cells.filter(c=>c.clue!==undefined&&(c.clue!==1||index===7)))){const old=c.clue;delete c.clue;const remaining=paths.filter(p=>!violations(level,p).length);if(remaining.length!==1)c.clue=old;} const finalWitnesses=keys.map(key=>{const p=paths.find(p=>!violations(level,p,{omit:key}).length&&violations(level,p).length);return {constraint:key,path:p};}); if(finalWitnesses.some(w=>!w.path))return null; return finalWitnesses; } const cache=new Map();function basePaths(def,index,variant){const cells=def.geometry==='hex'?hex(index):def.geometry==='blocks'?blocks(index,variant):square(index);let key=JSON.stringify(cells);if(!cache.has(key)){const blank={mode:'base',geometry:def.geometry,cells};let paths;try{paths=solve(blank,{limit:60001,nodeLimit:8000000});}catch{return null;}if(paths.length>60000||!paths.length)return null;cache.set(key,{cells,paths});}return cache.get(key);} const archive=[]; for(const def of definitions){const folder=path.join(root,'modes',def.id);fs.mkdirSync(folder,{recursive:true});fs.copyFileSync(path.join(root,'tools','engine-template.mjs'),path.join(folder,'rules.mjs'));let levels=[],notes=[]; for(let index=0;index<8;index++){ let accepted=null;for(let attempt=0;attempt<2400;attempt++){ const base=basePaths(def,index,Math.floor(attempt/120));if(!base)continue; const author=pick(base.paths),level=setup(def,index,base.cells,author);if(!level)continue; const witnesses=selectClues(level,author,base.paths,index);if(!witnesses)continue; try{assertStructure(level);}catch{continue;} const solution=solve(level,{limit:2});if(solution.length!==1)throw Error('筛选与求解不一致'); accepted={level,author,witnesses,basePathCount:base.paths.length,attempt};break; } if(!accepted)throw Error('无法生成 '+def.id+' '+(index+1)); const {level,author,witnesses}=accepted;levels.push(level); const breakdown=constraints(level).map(r=>{if(r.kind==='relation')return `${r.from}→${r.to}:${r.delta} 次移动,作者步号 ${author.indexOf(r.from)+1}→${author.indexOf(r.to)+1}`;if(r.kind==='order')return `${r.from} 先于 ${r.to},作者步号 ${author.indexOf(r.from)+1}<${author.indexOf(r.to)+1}`;if(r.kind==='window')return `${r.cell} 窗口 ${r.min}–${r.max},作者第 ${author.indexOf(r.cell)+1} 步到达`;if(r.kind==='direction')return `${r.cell} 在第 ${author.indexOf(r.cell)+1} 步,必须${({lr:'左右直行',du:'上下直行',ru:'上与右',dr:'右与下',dl:'下与左',lu:'左与上'})[r.value]}`;if(r.kind==='wall')return `永久墙 ${r.edge.join('—')} 断开表面邻接,需绕行`;if(r.kind==='door'){const crossing=author.findIndex((id,i)=>i>0&&r.edge.includes(id)&&r.edge.includes(author[i-1]));return `第 ${crossing} 格后门开,从 ${author[crossing-1]} 走向 ${author[crossing]},落格后门关;空盘门关、起步也切换`;}if(r.kind==='gate')return `先在第 ${author.indexOf(r.switchCell)+1} 步踩 ${r.switchCell},第 ${Math.max(...r.edge.map(id=>author.indexOf(id)))+1} 步跨门 ${r.edge.join('—')}`;return '○/△每步交替,按节点数量推断端点类别';}); const keyWitness=witnesses[0],fork=keyWitness.path.findIndex((id,i)=>id!==author[i]); notes.push({...accepted,breakdown,keyFork:{step:fork+1,correct:author[fork],tempting:keyWitness.path[fork]},reason:violations(level,keyWitness.path).map(v=>v.message).join(';'),review:'自动验证通过;难度与人类推理顺序待真人试玩确认'}); console.log(def.id,level.id,level.difficulty,'N='+level.cells.length,'constraints='+constraints(level).length,'clues='+level.cells.filter(c=>c.clue!==undefined).length,'attempt='+accepted.attempt); } fs.writeFileSync(path.join(folder,'levels.json'),JSON.stringify({mode:def.id,title:def.title,rule:def.rule,levels},null,2)); fs.writeFileSync(path.join(folder,'design.json'),JSON.stringify(notes,null,2));archive.push({mode:def.id,title:def.title,count:levels.length}); let md=`# ${def.title}\n\n${def.rule}\n\n固定数字表示必须在该步到达;每格一次、共边移动、完整路线唯一。所有关卡开放。\n\n这些是机制验证候选关,难度标签按设计任务编排,尚未经真人校准。\n`; for(const item of notes){const l=item.level;md+=`\n## ${l.id} · ${l.difficulty} · ${l.title}\n\n格数 ${l.cells.length};基础完整路线 ${item.basePathCount} 条;新约束 ${constraints(l).length} 项。\n\n作者解:${item.author.join(' → ')}。\n\n推理检查点:\n\n${item.breakdown.map(s=>'- '+s).join('\n')}\n\n关键分叉:第 ${item.keyFork.step} 步应选 ${item.keyFork.correct},解除机制后的诱惑路线选择 ${item.keyFork.tempting}。排除依据:${item.reason}。\n\n诱惑路线:${item.witnesses[0].path.join(' → ')}。这是符合几何与辅助数字的完整路线,由新机制排除。\n\n逐项必要性证据:\n\n${item.witnesses.map(w=>'- 解除 '+w.constraint+' 后可走 '+w.path.join(' → ')).join('\n')}\n\n人工评审:请先隐藏作者解,依据盘面复述排除理由;记录是否主要依靠试错。\n`;} fs.writeFileSync(path.join(folder,'DESIGN.md'),md); } fs.writeFileSync(path.join(root,'reports','catalog.json'),JSON.stringify(archive,null,2));