feat: version campaigns and document original puzzle design
Restore v0.1 and v0.2 archives with separate progress, publish the curved v0.3 campaign, and document the original 24-level design with reproducible evidence. Refine board presentation and movement, and keep video lab tooling within its own directory.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { hexagon, random, randomTracks, rotateBoard, trace, available, analyze, key, opposite, Game } from './core.js';
|
||||
|
||||
export const CAMPAIGN_PLAN = {
|
||||
id:'observe-v3', chapterSize:6,
|
||||
id:'observe-v3', revision:2, chapterSize:6, minimumCurvedRatio:.65,
|
||||
chapters:['看见出口','转出新路','连成节奏','轻巧解结','展开天地','从容变换','长路常新','自在回响'],
|
||||
};
|
||||
const STRAIGHT=[3,4,5,0,1,2];
|
||||
@@ -26,7 +26,9 @@ export function generateLaneLevel(seed,{radius=3,target=18,axis=seed%3}={}) {
|
||||
// Independent board size, route vocabulary and piece count. Reverse insertion
|
||||
// makes the unrotated board solvable; a selected starting rotation may add a
|
||||
// small routing decision. No route is allowed to hide a solitary-piece loop.
|
||||
export function generateFlowLevel(seed,{radius=3,target=18,bendRate=.12,maxTurns=4,startRotation=0,allFree=false,clearBlockers=false}={}) {
|
||||
// min/maxBaseTurns constrain the construction orientation, before startRotation.
|
||||
// Published route usage is measured again in the final reference solution.
|
||||
export function generateFlowLevel(seed,{radius=3,target=18,bendRate=.12,maxTurns=4,startRotation=0,allFree=false,clearBlockers=false,minBaseTurns=0,maxBaseTurns=maxTurns,curvePreference=0}={}) {
|
||||
const rng=random(seed);
|
||||
let level={version:1,id:`flow-${seed}`,name:'流动的轨迹',seed,rotationEnabled:true,cells:hexagon(radius),tiles:[],moveLimit:target};
|
||||
level.cells.forEach(c=>{c.tracks=rng()<bendRate?randomTracks(rng):[...STRAIGHT];});
|
||||
@@ -35,6 +37,7 @@ export function generateFlowLevel(seed,{radius=3,target=18,bendRate=.12,maxTurns
|
||||
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])<minBaseTurns||turnCount(routes[0])>maxBaseTurns)continue;
|
||||
candidates.push({t,steps:routes[0].steps.length,turns:turnCount(routes[0])});
|
||||
}
|
||||
for(let n=0;n<target;n++){
|
||||
@@ -50,7 +53,7 @@ export function generateFlowLevel(seed,{radius=3,target=18,bendRate=.12,maxTurns
|
||||
})))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;
|
||||
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;
|
||||
@@ -68,26 +71,28 @@ 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,states=0;
|
||||
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<routes.length;i++)if(routes[i].status==='blocked'){
|
||||
blockedStates++;const base=trace(game.level,game.tiles[i],game.tiles,true);
|
||||
if(!base.blockers.includes(routes[i].blocker))hiddenBlockerStates++;
|
||||
}
|
||||
if(game.tiles.length>=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,rotationDistance:distance});
|
||||
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,states,samples,won:game.won};
|
||||
return {analysis:a,independentLoops,maxSoloTurns,maxDryRotations,maxUnlockDistance,hiddenBlockerStates,blockedStates,lowChoiceStates,noCurvedChoiceStates,states,samples,won:game.won};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user