import { hexagon, random, randomTracks, rotateBoard, trace, available, analyze, key, opposite, Game } from './core.js'; export const CAMPAIGN_PLAN = { id:'observe-v3', revision:2, chapterSize:6, minimumCurvedRatio:.65, chapters:['看见出口','转出新路','连成节奏','轻巧解结','展开天地','从容变换','长路常新','自在回响'], }; const STRAIGHT=[3,4,5,0,1,2]; const turnCount=route=>route.steps.filter(s=>s.enter!==null&&opposite(s.enter)!==s.exit).length; // Spacious parallel lanes create long, readable release levels. Alternate // columns leave enough physical clearance to avoid off-route grazing blockers; // each lane splits outwards, so several independent choices stay available. export function generateLaneLevel(seed,{radius=3,target=18,axis=seed%3}={}) { const rng=random(seed),cells=hexagon(radius); const sets=[0,1].map(parity=>cells.filter(c=>Math.abs(c.q)%2===parity)); let pool=sets[seed%2];if(pool.length({c,w:rng()})).sort((a,b)=>a.w-b.w).slice(0,target).map(({c})=>c); const tiles=selected.map((c,i)=>{ let q=c.q,r=c.r,dir=c.r+c.q/2<0?0:3; for(let n=0;n{c.tracks=rng()rotateBoard(level,i)),candidates=[]; for(const cell of level.cells)for(let dir=0;dir<6;dir++){ const t={id:'candidate',q:cell.q,r:cell.r,dir,color:0}; const routes=boards.map(b=>trace(b,t,[t],true)); if(routes.some(r=>r.status!=='exit'||turnCount(r)>maxTurns))continue; if(turnCount(routes[0])maxBaseTurns)continue; candidates.push({t,steps:routes[0].steps.length,turns:turnCount(routes[0])}); } for(let n=0;nkey(t.q,t.r))),choices=[]; for(const c of candidates){ if(occupied.has(key(c.t.q,c.t.r)))continue; const tile={...c.t,id:`t${n}`,color:n%4},tiles=[...level.tiles,tile]; if(trace(level,tile,tiles).status!=='exit')continue; if(allFree&&tiles.some(t=>trace(level,t,tiles).status!=='exit'))continue; if(clearBlockers&&level.tiles.some(other=>[tile,other].some((t,i)=>{ const r=trace(level,t,[tile,other]); return r.status==='blocked'&&!trace(level,t,[tile,other],true).blockers.includes((i?tile:other).id); })))continue; // Avoid filling the board with one-step edge exits; give readable routes // a small preference, while random tie-breaking varies spatial patterns. const weight=(allFree?rng()*2-c.steps*2:rng()*3+Math.min(c.steps,5)*.35-c.turns*.25)+Math.min(c.turns,2)*curvePreference; choices.push({tile,weight}); } if(!choices.length)break; choices.sort((a,b)=>b.weight-a.weight); level.tiles.push(choices[0].tile); } level=rotateBoard(level,startRotation); level.moveLimit=level.tiles.length; return level; } // These are structural checks, not playtest claims. Observe every solver state // in all six orientations to find long no-progress stretches before shipping. export function inspectFlowLevel(level) { const a=analyze(level),boards=Array.from({length:6},(_,i)=>rotateBoard(level,i)); let independentLoops=0,maxSoloTurns=0; for(const board of boards)for(const tile of level.tiles){const r=trace(board,tile,[tile],true);if(r.status!=='exit')independentLoops++;maxSoloTurns=Math.max(maxSoloTurns,turnCount(r));} const game=new Game(level);let maxDryRotations=0,dry=0,maxUnlockDistance=0,hiddenBlockerStates=0,blockedStates=0,lowChoiceStates=0,noCurvedChoiceStates=0,states=0; const samples=[]; function inspectState(){ if(!game.tiles.length)return; const routes=game.tiles.map(t=>trace(game.level,t,game.tiles)); const free=routes.filter(r=>r.status==='exit').length; const freeCurved=routes.filter(r=>r.status==='exit'&&turnCount(r)>0).length; for(let i=0;i=6&&free<2)lowChoiceStates++; if(game.tiles.length>=4&&free>0&&!freeCurved)noCurvedChoiceStates++; let distance=0; if(!free){distance=3;for(let turns=1;turns<=3;turns++)if(available(rotateBoard(game.level,turns),game.tiles).length||available(rotateBoard(game.level,-turns),game.tiles).length){distance=turns;break;}} maxUnlockDistance=Math.max(maxUnlockDistance,distance);states++; samples.push({remaining:game.tiles.length,free,freeCurved,rotationDistance:distance}); } for(const action of a.actions){ inspectState(); if(action.type==='rotate'){dry++;maxDryRotations=Math.max(maxDryRotations,dry);game.rotate(action.direction);} else{dry=0;game.tap(action.id);} } return {analysis:a,independentLoops,maxSoloTurns,maxDryRotations,maxUnlockDistance,hiddenBlockerStates,blockedStates,lowChoiceStates,noCurvedChoiceStates,states,samples,won:game.won}; }