diff --git a/scripts/build-board.mjs b/scripts/build-board.mjs index cbe2a61..0c58738 100644 --- a/scripts/build-board.mjs +++ b/scripts/build-board.mjs @@ -20,7 +20,10 @@ const f2 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 100) / 10 const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null; // ---------- live measurement ---------- -const SET = resolve('levels/prototypes/d5.json'); +// The shipping set once it exists, the prototype output before that. Reading +// the prototype after the campaign was assembled showed the tile as "18 / 50" +// while fifty levels were sitting in levels/campaign-50.json. +const SET = [resolve('levels/campaign-50.json'), resolve('levels/prototypes/d5.json')].find(existsSync); let pool = { n: 0, rot: {}, apt: null, families: {}, blind: null, wrong: null, endgame: {} }; if (existsSync(SET)) { const raw = JSON.parse(readFileSync(SET, 'utf8')); @@ -29,7 +32,7 @@ if (existsSync(SET)) { for (const l of levels) { const r = l.prototype?.stats?.rotations ?? solveRotations(l).rotations; pool.rot[r] = (pool.rot[r] || 0) + 1; - const fam = l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, ''); + const fam = l.campaign?.family ?? l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, ''); pool.families[fam] = (pool.families[fam] || 0) + 1; const a = l.prototype?.audit ?? deadTurnAudit(l); pool.endgame[a.endgameMinOpen] = (pool.endgame[a.endgameMinOpen] || 0) + 1; diff --git a/src/campaign-versions.js b/src/campaign-versions.js index b933e75..d5dacbe 100644 --- a/src/campaign-versions.js +++ b/src/campaign-versions.js @@ -36,6 +36,7 @@ export const CAMPAIGN_VERSIONS = [ { id:C50_CAMPAIGN.id, name:C50_CAMPAIGN.name, description:C50_CAMPAIGN.description, chapters:C50_CAMPAIGN.chapters, chapterSize:C50_CAMPAIGN.chapterSize, levels:C50_LEVELS, + legacyStorageKeys:[], }, ]; export const DEFAULT_CAMPAIGN_VERSION = 'v0.5'; diff --git a/tests/campaign-versions.test.js b/tests/campaign-versions.test.js index 0a5c0f1..94bb798 100644 --- a/tests/campaign-versions.test.js +++ b/tests/campaign-versions.test.js @@ -118,3 +118,20 @@ test('the production campaign starts independently and retains its thousandth le assert.equal(s.getItem(campaignProgressKey('v0.4')),old); assert.equal(selectedCampaign(s).id,'v0.5'); }); + +// A campaign edition is registered by hand, and app.js spreads several of its +// fields on load. Leaving one out registers an edition that looks fine in every +// other test and blanks the board the moment somebody opens it — which is how +// c50 shipped its first build. +test('every campaign edition carries the fields the app reads on load', () => { + for (const edition of CAMPAIGN_VERSIONS) { + assert.ok(edition.id && edition.name, `${edition.id}: 缺 id/name`); + assert.ok(Array.isArray(edition.levels) && edition.levels.length, `${edition.id}: 缺关卡`); + assert.ok(Array.isArray(edition.chapters) && edition.chapters.length, `${edition.id}: 缺章节`); + assert.ok(Number.isFinite(edition.chapterSize) && edition.chapterSize > 0, `${edition.id}: chapterSize 非法`); + // app.js iterates this one directly, so a missing array is a TypeError. + assert.ok(Array.isArray(edition.legacyStorageKeys), `${edition.id}: legacyStorageKeys 必须是数组`); + assert.ok(edition.chapters.length >= Math.ceil(edition.levels.length / edition.chapterSize), + `${edition.id}: 章节数不够放下 ${edition.levels.length} 关`); + } +});