纠正仓库内容为衍生项目,标明 campaign-200-r2 最新版

This commit is contained in:
toshiocn
2026-10-19 16:56:31 +08:00
parent a492c5b80e
commit fdca4f67ea
1396 changed files with 392768 additions and 19530 deletions
+23
View File
@@ -0,0 +1,23 @@
import fs from 'node:fs';import {graph,solve,violations,constraints,directionAt,assertStructure,units} from '../rules.mjs';import {measure} from './score.mjs';import {signature} from './fingerprint.mjs';
let seed=2026101502;const rnd=()=>{seed=(seed*1664525+1013904223)>>>0;return seed/4294967296;},pick=a=>a[Math.floor(rnd()*a.length)],shuffle=a=>[...a].sort(()=>rnd()-.5);
function cellsFor(n,geometry,serial){if(geometry==='hex'){const width=n<=24?4:n<=30?5:6,height=Math.ceil(n/width);let pts=[];for(let q=0;q<width;q++)for(let r=0;r<height;r++)pts.push([q,r]);while(pts.length>n){const boundary=pts.filter(([q,r])=>q===0||r===0||q===width-1||r===height-1);const gone=pick(serial%4===0?pts:boundary);pts=pts.filter(p=>p!==gone);}return pts.map(([q,r],i)=>({id:'c'+i,q,r}));}
const shapes=geometry==='blocks'?(serial%3===0?[[[0,0],[1,0],[0,1]],[[0,0],[1,0]]]:[[[0,0],[1,0]],...(serial%2===0?[[[0,0],[0,1]]]:[])]):[];const extra=shapes.reduce((a,s)=>a+s.length-1,0),total=n+extra,width=total<=25?5:total<=36?6:7,height=Math.ceil(total/width);let cells=[];for(let y=0;y<height;y++)for(let x=0;x<width;x++)cells.push({x,y,w:1,h:1});while(cells.length>total){const pool=cells.filter(c=>serial%3===0||c.x===0||c.y===0||c.x===width-1||c.y===height-1);const gone=pick(pool);cells=cells.filter(c=>c!==gone);}for(const template of shapes){const matches=[];for(const a of cells)for(let rot=0;rot<2;rot++){const shape=template.map(([x,y])=>rot?[y,x]:[x,y]);const group=shape.map(([dx,dy])=>cells.find(c=>c.x===a.x+dx&&c.y===a.y+dy&&c.w===1&&c.h===1&&!c.shape));if(group.every(Boolean))matches.push({a,shape,group});}if(!matches.length)return null;const {a,shape,group}=pick(matches);a.w=Math.max(...shape.map(p=>p[0]))+1;a.h=Math.max(...shape.map(p=>p[1]))+1;if(shape.length!==a.w*a.h)a.shape=shape;cells=cells.filter(c=>c===a||!group.includes(c));}return cells.map((c,i)=>({...c,id:'c'+i}));}
function authorPath(l){let g;try{g=graph(l);assertStructure(l);}catch{return null;}const n=l.cells.length,seen=new Set(),p=[];let visits=0;function viable(cur){const left=new Set(Array.from({length:n},(_,i)=>i).filter(i=>!seen.has(i)));left.add(cur);const q=[cur],reached=new Set(q);let leaves=0;for(let k=0;k<q.length;k++){const i=q[k],next=g.adj[i].filter(j=>left.has(j));if(!next.length&&left.size>1)return false;if(i!==cur&&next.length===1&&++leaves>1)return false;for(const j of next)if(!reached.has(j)){reached.add(j);q.push(j);}}return reached.size===left.size;}
function walk(i){if(++visits>60000)return false;seen.add(i);p.push(i);if(p.length===n)return true;if(viable(i)){const choices=g.adj[i].filter(j=>!seen.has(j)).map(j=>({j,v:g.adj[j].filter(k=>!seen.has(k)).length+rnd()*3})).sort((a,b)=>a.v-b.v);for(const {j}of choices)if(walk(j))return true;}p.pop();seen.delete(i);return false;}for(const i of shuffle(Array.from({length:n},(_,i)=>i)).slice(0,8)){if(walk(i))return p.map(i=>g.cells[i].id);seen.clear();p.length=0;if(visits>60000)break;}return null;}
function spatial(l,a){let turns=0,maxRun=0,run=0,last=null,qrev=0,rrev=0,qsign=0,rsign=0;for(let i=1;i<a.length;i++){const x=l.cells.find(c=>c.id===a[i-1]),y=l.cells.find(c=>c.id===a[i]),dx=l.geometry==='hex'?y.q-x.q:Math.sign(y.x-x.x),dy=l.geometry==='hex'?y.r-x.r:Math.sign(y.y-x.y),key=[dx,dy].join();if(last&&last!==key)turns++;run=last===key?run+1:1;maxRun=Math.max(maxRun,run);last=key;if(dx){const s=Math.sign(dx);if(qsign&&qsign!==s)qrev++;qsign=s;}if(dy){const s=Math.sign(dy);if(rsign&&rsign!==s)rrev++;rsign=s;}}return {turnRate:turns/(a.length-2),maxRun,qReversals:qrev,rReversals:rrev};}
function configure(geometry,cells,a,features,serial){const l={mode:'campaign-200-r2',geometry,cells:structuredClone(cells),features:[...features],contentRevision:1};const at=id=>l.cells.find(c=>c.id===id),g=graph(l),pos=new Map(a.map((id,i)=>[id,i]));at(a[0]).clue=1;if(features.includes('alternation'))a.forEach((id,i)=>at(id).category=i%2?'triangle':'circle');
if(features.includes('direction')){const candidates=shuffle(a.slice(1,-1).filter(id=>units(at(id)).length===1&&directionAt(l,a,pos.get(id))));for(const id of candidates.slice(0,serial%3===0?3:2))at(id).direction=directionAt(l,a,pos.get(id));if(!l.cells.some(c=>c.direction))return null;}
if(features.includes('parity')){const pool=shuffle(a.slice(2,-2).filter(id=>!at(id).direction));for(const id of pool.slice(0,serial%4===0?2:1))at(id).parity=(pos.get(id)+1)%2?'odd':'even';}
if(features.includes('rhythm-door')){const pool=a.map((id,i)=>i).filter(i=>i>=4&&i<a.length-2&&i%2===0);if(!pool.length)return null;const i=pick(pool);l.doors=[{edge:[a[i],a[i+1]],initialOpen:false}];}
if(features.includes('walls')){const pool=[];for(let i=0;i<cells.length;i++)for(const j of g.adj[i])if(i<j){const x=cells[i].id,y=cells[j].id;if(Math.abs(pos.get(x)-pos.get(y))===1)continue;if(features.includes('alternation')&&at(x).category===at(y).category)continue;pool.push([x,y]);}if(!pool.length)return null;l.walls=shuffle(pool).slice(0,serial%3===0?3:2);}
return l;}
function fit(l,a){const pool=shuffle(a.slice(2,-2).filter(id=>{const c=l.cells.find(c=>c.id===id);return !c.direction&&!c.parity;}));const cap=Math.ceil(l.cells.length*.25);for(let round=0;round<=cap;round++){let sols;try{sols=solve(l,{limit:2,nodeLimit:250000});}catch{sols=null;}if(sols?.length===1){if(sols[0].join()!==a.join())return null;break;}if(sols?.length===0||round===cap)return null;const alternatives=(sols||[]).filter(p=>p.join()!==a.join());let choices=pool.filter(id=>l.cells.find(c=>c.id===id).clue===undefined);if(alternatives.length)choices=choices.filter(id=>alternatives.some(p=>p[a.indexOf(id)]!==id));if(!choices.length)return null;const id=pick(choices);l.cells.find(c=>c.id===id).clue=a.indexOf(id)+1;}
for(const c of shuffle(l.cells.filter(c=>c.clue!==undefined&&c.clue!==1))){const number=c.clue;delete c.clue;let count=2;try{count=solve(l,{limit:2,nodeLimit:200000}).length;}catch{}if(count!==1)c.clue=number;}
// Remove redundant atoms, but never silently drop an entire requested mechanism.
for(let pass=0;pass<2;pass++){let changed=false;for(const r of constraints(l)){let sols;try{sols=solve(l,{omit:r.key,limit:2,nodeLimit:250000});}catch{return null;}if(sols.length===1){if(r.kind==='direction')delete l.cells.find(c=>c.id===r.cell).direction;else if(r.kind==='parity')delete l.cells.find(c=>c.id===r.cell).parity;else if(r.kind==='wall')l.walls.splice(Number(r.key.split(':')[1]),1);else return null;changed=true;break;}}if(!changed)break;}
for(const feature of l.features){if(feature==='direction'&&!l.cells.some(c=>c.direction)||feature==='parity'&&!l.cells.some(c=>c.parity)||feature==='walls'&&!l.walls?.length)return null;}
const witnesses=[];for(const r of constraints(l)){let sols;try{sols=solve(l,{omit:r.key,limit:2,nodeLimit:350000});}catch{return null;}if(sols.length<2)return null;const p=sols.find(p=>p.join()!==a.join());if(!p||violations(l,p,{omit:r.key}).length)return null;witnesses.push({constraint:r.key,path:p});}return witnesses;}
const bankFile='reports/new-bank.json';let bank=fs.existsSync(bankFile)?JSON.parse(fs.readFileSync(bankFile)):[];const known=new Set(bank.map(n=>signature(n.level)));const configs=[['square',['direction','walls']],['square',['direction','walls','rhythm-door']],['blocks',['big','parity','walls']],['blocks',['big','direction','parity','walls']],['blocks',['big','direction','parity','rhythm-door']],['blocks',['big','alternation','walls']],['blocks',['big','direction','alternation','walls']],['blocks',['big','direction','alternation','rhythm-door']],['hex',['alternation','walls']],['hex',['parity','walls']],['hex',['alternation','walls','rhythm-door']],['hex',['parity','walls','rhythm-door']]];
const want=Number(process.argv[2]||150);for(let index=bank.length;index<want;index++){const sizeTier=index<25?0:index<60?1:2,n=(sizeTier===0?[20,22,24]:sizeTier===1?[24,26,28]:[28,30,32,36])[index%(sizeTier===2?4:3)],floor=sizeTier===0?35:sizeTier===1?48:60;let accepted=null;const geometryTarget=index%12;
for(let attempt=0;attempt<1800;attempt++){const [geometry,features]=configs[(geometryTarget+Math.floor(attempt/300))%configs.length];const serial=index*2003+attempt;seed=20261015+serial*73;const cells=cellsFor(n,geometry,serial);if(!cells)continue;const blank={geometry,features:[],cells},a=authorPath(blank);if(!a)continue;const space=spatial(blank,a);if(geometry==='hex'&&(space.qReversals<2||space.rReversals<2||space.maxRun>5||space.turnRate<.45))continue;const l=configure(geometry,cells,a,features,serial);if(!l)continue;const ws=fit(l,a);if(!ws)continue;const sig=signature(l);if(known.has(sig))continue;const m=measure(l,a);if(m.score<floor||m.decisions<3||m.nonlocalBranches<2)continue;accepted={level:l,author:a,witnesses:ws,objective:m,spatial:space,attempts:attempt+1,source:'r2新制作'};known.add(sig);break;}
if(!accepted)throw Error('无法生成难度层 '+index+' N='+n);bank.push(accepted);fs.writeFileSync(bankFile,JSON.stringify(bank,null,2));console.log('bank',bank.length,'N',n,accepted.level.geometry,accepted.level.features.join('+'),'score',accepted.objective.score,'decisions',accepted.objective.decisions,'tries',accepted.attempts);}