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