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.
106 lines
5.4 KiB
JavaScript
106 lines
5.4 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { createHash } from 'node:crypto';
|
|
import { Game, solveRotations, validateLevel } from '../src/core.js';
|
|
import { LEVELS } from '../src/levels.js';
|
|
import { CAMPAIGN_VERSIONS, campaignVersion } from '../src/campaign-versions.js';
|
|
import { CAMPAIGN_SELECTION_KEY, campaignProgressKey, selectedCampaign, normalizeProgress, readCampaignProgress, writeCampaignProgress } from '../src/campaign-progress.js';
|
|
|
|
const storage=entries=>{
|
|
const items=new Map(Object.entries(entries||{}));
|
|
return {getItem:key=>items.get(key)??null,setItem:(key,value)=>items.set(key,value)};
|
|
};
|
|
|
|
test('three selectable editions retain their own level counts, order and chapters',()=>{
|
|
assert.deepEqual(CAMPAIGN_VERSIONS.map(c=>[c.id,c.levels.length,c.chapterSize,c.chapters.length]),[
|
|
['v0.1',24,8,3],['v0.2',48,6,8],['v0.3',48,6,8],
|
|
]);
|
|
assert.equal(campaignVersion('v0.1').levels[0].name,'转一转,路就通了');
|
|
assert.equal(campaignVersion('v0.1').levels.at(-1).name,'六角大满贯');
|
|
assert.equal(campaignVersion('v0.2').levels[0].name,'一眼看见出口');
|
|
assert.equal(campaignVersion('v0.3').levels,LEVELS);
|
|
assert.equal(campaignVersion('v0.3').levels[0].name,'跟着弯道走');
|
|
for(const c of CAMPAIGN_VERSIONS)assert.equal(c.chapterSize*c.chapters.length,c.levels.length);
|
|
});
|
|
|
|
test('browser archive module matches the exact historical JSON bytes and provenance',async()=>{
|
|
const manifest=JSON.parse(await readFile(new URL('../levels/versions/provenance.json',import.meta.url),'utf8'));
|
|
for(const source of manifest.versions){
|
|
const bytes=await readFile(new URL('../'+source.archivePath,import.meta.url));
|
|
assert.equal(bytes.length,source.bytes);
|
|
assert.equal(createHash('sha256').update(bytes).digest('hex'),source.sha256);
|
|
assert.deepEqual(campaignVersion(source.version).levels,JSON.parse(bytes.toString('utf8')));
|
|
}
|
|
});
|
|
|
|
test('all 72 historical layouts solve and replay with the current free rotation rules',()=>{
|
|
for(const c of CAMPAIGN_VERSIONS.slice(0,2))for(const [i,level] of c.levels.entries()){
|
|
const label=`${c.id} level ${i+1}`;
|
|
assert.deepEqual(validateLevel(level),[],label);
|
|
const plan=solveRotations(level),game=new Game(level);
|
|
assert.ok(plan.solvable&&!plan.exhausted,label);
|
|
for(const action of plan.actions){
|
|
const before=game.moves;
|
|
if(action.type==='rotate'){
|
|
assert.equal(game.rotate(action.direction).status,'rotated',label);
|
|
assert.equal(game.moves,before,label);
|
|
}else assert.equal(game.tap(action.id).status,'exit',label);
|
|
}
|
|
assert.ok(game.won,label);
|
|
}
|
|
});
|
|
|
|
test('new players default to v0.3 while an explicit version choice survives reload',()=>{
|
|
const s=storage();assert.equal(selectedCampaign(s).id,'v0.3');
|
|
assert.ok(writeCampaignProgress(s,campaignVersion('v0.1'),{current:23,completed:[0,22]}));
|
|
assert.equal(selectedCampaign(s).id,'v0.1');
|
|
assert.deepEqual(readCampaignProgress(s,selectedCampaign(s)),{current:23,completed:[0,22]});
|
|
s.setItem(CAMPAIGN_SELECTION_KEY,'v9.9');assert.equal(selectedCampaign(s).id,'v0.3');
|
|
});
|
|
|
|
test('versions keep progress separate even when v0.2 and v0.3 reuse level IDs',()=>{
|
|
const s=storage(),v2=campaignVersion('v0.2'),v3=campaignVersion('v0.3');
|
|
assert.equal(v2.levels[0].id,v3.levels[0].id);
|
|
writeCampaignProgress(s,v2,{current:47,completed:[0,1,47]});
|
|
assert.deepEqual(readCampaignProgress(s,v3),{current:0,completed:[]});
|
|
writeCampaignProgress(s,v3,{current:7,completed:[6]});
|
|
assert.deepEqual(readCampaignProgress(s,v2),{current:47,completed:[0,1,47]});
|
|
assert.deepEqual(readCampaignProgress(s,v3),{current:7,completed:[6]});
|
|
assert.deepEqual(readCampaignProgress(s,campaignVersion('v0.1')),{current:0,completed:[]});
|
|
});
|
|
|
|
test('each edition migrates only its own legacy slot without changing the old data',()=>{
|
|
const old={
|
|
'hex-trails-v2':JSON.stringify({current:20,completed:[0,20]}),
|
|
'hex-trails-observe-v3':JSON.stringify({current:45,completed:[0,45]}),
|
|
'hex-trails-observe-v3-r2':JSON.stringify({current:6,completed:[6]}),
|
|
};
|
|
const s=storage(old);
|
|
for(const [i,c] of CAMPAIGN_VERSIONS.entries()){
|
|
const key=Object.keys(old)[i],expected=JSON.parse(old[key]);
|
|
assert.deepEqual(readCampaignProgress(s,c),expected);
|
|
writeCampaignProgress(s,c,{current:1,completed:[1]});
|
|
assert.deepEqual(readCampaignProgress(s,c),{current:1,completed:[1]});
|
|
assert.equal(s.getItem(key),old[key]);
|
|
}
|
|
});
|
|
|
|
test('invalid saves are bounded to each edition and do not resurrect legacy progress',()=>{
|
|
assert.deepEqual(normalizeProgress({current:47,completed:[-1,0,0,23,24,47,1.5,'2',null]},24),{current:0,completed:[0,23]});
|
|
const c=campaignVersion('v0.1');
|
|
for(const raw of ['{','null','[]','true','"text"']){
|
|
const s=storage({[campaignProgressKey(c.id)]:raw,'hex-trails-v2':JSON.stringify({current:20,completed:[20]})});
|
|
assert.deepEqual(readCampaignProgress(s,c),{current:0,completed:[]});
|
|
}
|
|
});
|
|
|
|
test('unavailable browser storage still permits loading a campaign',()=>{
|
|
const denied={getItem(){throw new Error('denied');},setItem(){throw new Error('denied');}};
|
|
for(const s of [undefined,denied]){
|
|
assert.equal(selectedCampaign(s).id,'v0.3');
|
|
assert.deepEqual(readCampaignProgress(s,campaignVersion('v0.1')),{current:0,completed:[]});
|
|
assert.equal(writeCampaignProgress(s,campaignVersion('v0.1'),{current:1,completed:[0]}),false);
|
|
}
|
|
});
|