36 lines
9.2 KiB
JavaScript
36 lines
9.2 KiB
JavaScript
import fs from 'node:fs';import path from 'node:path';import {fileURLToPath} from 'node:url';import {solve,violations,constraints,directionAt,assertStructure,graph,canAppend} from '../rules.mjs';
|
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..'),plan=JSON.parse(fs.readFileSync(path.join(root,'plan.json'),'utf8'));let seed=1;const rand=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296;},pick=a=>a[Math.floor(rand()*a.length)];function shuffle(a){const b=[...a];for(let i=b.length-1;i>0;i--){const j=Math.floor(rand()*(i+1));[b[i],b[j]]=[b[j],b[i]];}return b;}
|
|
function patch(n,variant){const width=n<=6?3:n<=12?(variant%3===1?4:3):4,height=Math.ceil(n/width);let units=[];for(let y=0;y<height;y++)for(let x=0;x<width;x++)units.push([x,y]);const remove=units.length-n;
|
|
let priority=variant%4===0?[...units].reverse():shuffle(units.filter(([x,y])=>x===0||x===width-1||y===0||y===height-1));if(variant%4===3&&n>12)priority=shuffle(units);for(const p of priority.slice(0,remove))units=units.filter(u=>u!==p);
|
|
if(variant%2)units=units.map(([x,y])=>[width-1-x,y]);if(variant%5===2)units=units.map(([x,y])=>[y,x]);return units.map(([x,y],i)=>({id:'c'+i,x,y,w:1,h:1}));}
|
|
function makeCells(spec,variant){const n=spec.nodeCountTarget,num=spec.displayOrder;if(spec.geometry==='hex'){const width=variant%4===0?2:variant%4===1?3:n<=12?2:3;let coords=[];for(let q=0;coords.length<n;q++)for(let r=0;r<width&&coords.length<n;r++)coords.push({id:'c'+coords.length,q,r});if(variant%3===1)coords=coords.map(c=>({...c,q:c.q+Math.floor(c.r/2)}));if(variant%3===2)coords=coords.map(c=>({...c,q:-c.q,r:-c.r}));return coords;}
|
|
if(spec.geometry==='square')return patch(n,variant);
|
|
const specialL=spec.irregular||false;const two=spec.doubleBlock||false;const templates=specialL?[[[0,0],[1,0],[0,1]],...(two?[[[0,0],[1,0]]]:[])]:[[[0,0],[1,0]],...(two?[[[0,0],[0,1]]]:[])];
|
|
const extra=templates.reduce((s,t)=>s+t.length-1,0);let cells=patch(n+extra,variant),taken=new Set();for(const shape of templates){let choices=[];for(const a of cells){if(taken.has(a.id)||a.shape||a.w!==1||a.h!==1)continue;for(const rotation of [0,1]){const rotated=shape.map(([x,y])=>rotation?[y,x]:[x,y]);const matches=rotated.map(([x,y])=>cells.find(c=>!taken.has(c.id)&&c.x===a.x+x&&c.y===a.y+y&&c.w===1&&c.h===1&&!c.shape));if(matches.every(Boolean))choices.push({a,rotated,matches});}}
|
|
if(!choices.length)return null;const {a,rotated,matches}=pick(choices);const xs=rotated.map(p=>p[0]),ys=rotated.map(p=>p[1]);a.w=Math.max(...xs)+1;a.h=Math.max(...ys)+1;if(rotated.length!==a.w*a.h)a.shape=rotated;for(const c of matches)if(c!==a)taken.add(c.id);cells=cells.filter(c=>!taken.has(c.id));}
|
|
return cells.map((c,i)=>({...c,id:'c'+i}));}
|
|
const baseCache=new Map();function base(spec,variant){const cells=makeCells(spec,variant);if(!cells)return null;const key=JSON.stringify(cells);if(baseCache.has(key))return baseCache.get(key);const l={mode:'base',features:[],geometry:spec.geometry,cells};try{assertStructure(l);const paths=solve(l,{limit:8001,nodeLimit:1800000});if(!paths.length){baseCache.set(key,null);return null;}const item={cells,paths,truncated:paths.length>=8001};baseCache.set(key,item);return item;}catch{baseCache.set(key,null);return null;}}
|
|
const wallCounts={8:2,18:2,24:2,32:2,34:2,38:2,42:2,43:2,44:3,47:2,48:2,50:2};
|
|
function setup(spec,cells,author){const num=spec.displayOrder,n=cells.length,l={id:spec.levelId,contentRevision:1,order:num,mode:'campaign-200',geometry:spec.geometry,features:[...spec.features],title:spec.title,chapter:spec.chapter,chapterTitle:plan.chapters[spec.chapter-1].title,role:spec.role,targetDifficulty:spec.targetDifficulty,caption:caption(spec),cells:structuredClone(cells)};if(!spec.unknownStart)l.cells.find(c=>c.id===author[0]).clue=1;if(num===1)l.cells.find(c=>c.id===author.at(-1)).clue=n;
|
|
const g=graph(l),positions=new Map(author.map((id,i)=>[id,i]));
|
|
if(l.features.includes('alternation'))for(let i=0;i<n;i++)l.cells.find(c=>c.id===author[i]).category=i%2?'triangle':'circle';
|
|
if(l.features.includes('direction')){const pool={lr:[],du:[],ru:[],dr:[],dl:[],lu:[]};for(let i=1;i<n-1;i++){const c=l.cells.find(c=>c.id===author[i]),kind=directionAt(l,author,i);if((c.w||1)*(c.h||1)===1&&!c.shape&&kind&&pool[kind])pool[kind].push(author[i]);};const pattern=spec.directionPattern|| (num===11?['lr']:num===12?['du']:num===13?['corner']:num===14?['corner','corner']:num===47?['lr','du','corner']:['lr','corner']);const selected=[];for(const want of pattern){let kinds=want==='corner'?['ru','dr','dl','lu'].filter(k=>pool[k].length):[want];if(!kinds.length)return null;const kind=pick(kinds);if(!pool[kind]?.length)return null;const id=pick(pool[kind]);pool[kind].splice(pool[kind].indexOf(id),1);l.cells.find(c=>c.id===id).direction=kind;selected.push(kind);}}
|
|
if(l.features.includes('rhythm-door')){const options=[];for(let i=2;i<n-1;i+=2)if(num<28||i>=Math.floor(n/3))options.push(i);if(!options.length)return null;const i=pick(options);l.doors=[{edge:[author[i],author[i+1]],initialOpen:false}];}
|
|
if(l.features.includes('parity')){const count=spec.parityCount||1;const pool=shuffle(author.slice(spec.unknownStart?0:2,-1).filter(id=>!(l.doors||[]).some(d=>d.edge.includes(id))));if(pool.length<count)return null;let selected=pool.slice(0,count);if(num===21){const even=pool.filter(id=>(positions.get(id)+1)%2===0);if(!even.length)return null;selected=[pick(even)];}if(num===22){const odd=pool.filter(id=>(positions.get(id)+1)%2===1);if(!odd.length)return null;selected=[pick(odd)];}for(const id of selected)l.cells.find(c=>c.id===id).parity=(positions.get(id)+1)%2?'odd':'even';if(count===2&&num===24&&new Set(selected.map(id=>l.cells.find(c=>c.id===id).parity)).size<2)return null;}
|
|
if(l.features.includes('walls')){const options=[];for(let a=0;a<n;a++)for(const b of g.adj[a])if(a<b){const x=g.cells[a].id,y=g.cells[b].id;if(Math.abs(positions.get(x)-positions.get(y))===1)continue;if(l.features.includes('alternation')&&l.cells[a].category===l.cells[b].category)continue;options.push([x,y]);}const count=spec.wallCount||1;if(options.length<count)return null;l.walls=shuffle(options).slice(0,count);}
|
|
if(l.geometry==='blocks'){const large=l.cells.filter(c=>c.w*c.h>1);if(large.some(c=>positions.get(c.id)===0||positions.get(c.id)===n-1))return null;}
|
|
return l;}
|
|
function caption(s){return s.caption||'先观察出口,再按本关规则走遍每一格。';}
|
|
function chooseClues(l,author,basePaths,spec={}){let paths=basePaths.filter(p=>l.cells.every(c=>c.clue===undefined||p[c.clue-1]===c.id)),atoms=constraints(l),keys=atoms.map(r=>r.key);const masks=paths.map(p=>new Set(violations(l,p,{ignoreClues:true}).map(x=>x.key)));let survivors=paths.map((_,i)=>i).filter(i=>!masks[i].size),witnesses=keys.map(key=>paths.map((_,i)=>i).filter(i=>masks[i].size===1&&masks[i].has(key)));if(witnesses.some(w=>!w.length)||!survivors.length)return null;
|
|
const candidates=spec.noExtraNumbers?[]:shuffle(author.map((id,i)=>({id,step:i+1})).filter(c=>c.step!==1&&c.step!==author.length&&!l.cells.find(x=>x.id===c.id).parity&&!l.cells.find(x=>x.id===c.id).direction));let added=[];
|
|
function addOne(contenders){let best=null;for(const c of candidates.filter(c=>!added.includes(c))){const matches=i=>paths[i][c.step-1]===c.id,kept=survivors.filter(matches),ws=witnesses.map(w=>w.filter(matches));if(ws.some(w=>!w.length))continue;const rejected=contenders.filter(p=>p[c.step-1]!==c.id).length;if(!rejected)continue;if(!best||rejected>best.rejected||rejected===best.rejected&&kept.length<best.kept.length)best={c,kept,ws,rejected};}if(!best)return false;added.push(best.c);l.cells.find(c=>c.id===best.c.id).clue=best.c.step;survivors=best.kept;witnesses=best.ws;return true;}
|
|
while(survivors.length>1){if(!addOne(survivors.map(i=>paths[i]))||added.length>4)return null;}
|
|
// Geometry enumeration may be sampled; every accepted puzzle is fully solved afterwards.
|
|
for(let pass=0;pass<6;pass++){let full;try{full=solve(l,{limit:2,nodeLimit:1600000});}catch{return null;}if(full.length===1&&full[0].join()===author.join())break;if(!full.length||!addOne(full.filter(p=>p.join()!==author.join()))||added.length>4)return null;if(pass===5)return null;}
|
|
for(const c of shuffle(l.cells.filter(c=>c.clue!==undefined&&c.clue!==1&&!(l.order===1&&c.clue===l.cells.length)))){const old=c.clue;delete c.clue;let count;try{count=solve(l,{limit:2,nodeLimit:1600000}).length;}catch{count=0;}if(count!==1)c.clue=old;}
|
|
const saved=keys.map((key,i)=>({constraint:key,path:paths[witnesses[i][0]]}));if(saved.some(w=>!w.path||violations(l,w.path,{omit:w.constraint}).length||!violations(l,w.path).length))return null;return saved;}
|
|
function metrics(l,p){const g=graph(l);let choices=0,maxRun=0,run=0,last=null;for(let i=0;i<p.length-1;i++){const prefix=p.slice(0,i+1);const options=g.adj[g.ids.get(p[i])].map(j=>g.cells[j].id).filter(id=>canAppend(l,prefix,id)&&!violations(l,[...prefix,id]).length);if(options.length>1)choices++;const a=g.cells[g.ids.get(p[i])],b=g.cells[g.ids.get(p[i+1])];const key=l.geometry==='hex'?`${Math.sign(b.q-a.q)},${Math.sign(b.r-a.r)}`:`${Math.sign(b.x-a.x)},${Math.sign(b.y-a.y)}`;run=key===last?run+1:1;last=key;maxRun=Math.max(maxRun,run);}return {noImmediateConflictChoices:choices,longestSameDirectionTransfers:maxRun,scope:'结构代理,不是人类难度或完整规则推理的有效分叉'};}
|
|
|
|
export {base,setup,chooseClues,metrics};
|
|
export function setSeed(value){seed=value;}
|