feat: add production campaigns, gameplay labs, and review documentation
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { campaignLaunch, campaignPage, levelIndexFromInput, productionPresentation, createAnalysisCache, featuredTrackEmphasis } from '../src/campaign-browser.js';
|
||||
import { hexagon, rotateBoard } from '../src/core.js';
|
||||
|
||||
const small={id:'old',chapterSize:8,chapters:['旧章'],levels:Array.from({length:8},(_,i)=>({id:`old-${i}`}))};
|
||||
const campaign={id:'v0.5',chapterSize:50,chapters:Array.from({length:20},(_,i)=>`旅程 ${i+1}`),levels:Array.from({length:1000},(_,i)=>({id:`new-${i}`}))};
|
||||
|
||||
test('campaign URLs locate an exact level without mixing progress or accepting invalid indices',()=>{
|
||||
const editions=[small,campaign];
|
||||
assert.deepEqual(campaignLaunch('?campaign=v0.5&level=1000',editions,small),{campaign,levelIndex:999});
|
||||
assert.deepEqual(campaignLaunch('?campaign=unknown&level=8',editions,small),{campaign:small,levelIndex:7});
|
||||
for(const level of ['0','1001','2.5','Infinity','-1','1e2',''])assert.equal(campaignLaunch(`?campaign=v0.5&level=${level}`,editions,small).levelIndex,null);
|
||||
assert.equal(campaignLaunch('',editions,small).campaign,small);
|
||||
});
|
||||
|
||||
test('a thousand-level campaign renders only the requested chapter, including its last level',()=>{
|
||||
const first=campaignPage(campaign),last=campaignPage(campaign,19);
|
||||
assert.deepEqual([first.start,first.end,first.levels.length],[0,50,50]);
|
||||
assert.deepEqual([last.start,last.end,last.levels.length],[950,1000,50]);
|
||||
assert.equal(last.levels.at(-1).index,999);
|
||||
assert.equal(campaignPage(campaign,999).chapter,19);
|
||||
assert.equal(campaignPage(campaign,-1).chapter,0);
|
||||
assert.equal(campaignPage(small).levels.length,8);
|
||||
});
|
||||
|
||||
test('direct level entry accepts whole level numbers within this campaign only',()=>{
|
||||
assert.equal(levelIndexFromInput(' 1000 ',1000),999);
|
||||
for(const value of ['0','1001','2.2','1e2','a',''])assert.equal(levelIndexFromInput(value,1000),null);
|
||||
});
|
||||
|
||||
test('production difficulty and journey rewards remain separate, with no old route-length score',()=>{
|
||||
const info=productionPresentation({beat:'花瓣巡游',lesson:'放出整片花瓣',production:{difficulty:{tier:1,label:'轻松',basis:'structural-estimate'},reward:{longestSteps:59,longestBends:37,longRoutes:4}}});
|
||||
assert.equal(info.label,'轻松');
|
||||
assert.equal(info.themeScore,12);
|
||||
assert.equal(info.reward,'最长巡游 59 格 · 37 次转弯 · 4 条长路');
|
||||
assert.match(info.estimate,/结构预估/);
|
||||
assert.equal(productionPresentation({}),null);
|
||||
});
|
||||
|
||||
test('analysis is reused for an immutable layout while distinct or edited layouts are reanalyzed',()=>{
|
||||
let calls=0;const getAnalysis=createAnalysisCache(level=>({tiles:level.tiles.length,call:++calls}));
|
||||
const level={tiles:[{}]},copy={tiles:[{}]};
|
||||
const first=getAnalysis(level);assert.equal(getAnalysis(level),first);assert.equal(calls,1);
|
||||
assert.equal(getAnalysis(copy).call,2);
|
||||
});
|
||||
|
||||
test('featured route drawing follows the board through all rotations and never highlights a different pair',()=>{
|
||||
const level={cells:hexagon(2),routeDesign:{featured:[{q:1,r:-1,a:0,b:3}]}};
|
||||
for(let turn=0;turn<6;turn++){
|
||||
const board=rotateBoard(level,turn),emphasis=featuredTrackEmphasis(level,turn);
|
||||
const highlighted=[];
|
||||
for(const cell of board.cells)for(let a=0;a<6;a++)if(a<cell.tracks[a]&&emphasis(cell,a,cell.tracks[a]))highlighted.push({cell,a,b:cell.tracks[a]});
|
||||
assert.equal(highlighted.length,1);
|
||||
const {cell,a,b}=highlighted[0];assert.equal(emphasis(cell,b,a),true);
|
||||
assert.equal(emphasis(cell,a,(b+1)%6),false);
|
||||
assert.equal(featuredTrackEmphasis(level,turn+6),emphasis);
|
||||
}
|
||||
assert.equal(featuredTrackEmphasis({}),null);
|
||||
});
|
||||
Reference in New Issue
Block a user