import { readFile } from 'node:fs/promises'; import { createServer } from 'vite'; const storage = new Map(); globalThis.window = { localStorage: { getItem(key) { return storage.has(key) ? storage.get(key) : null; }, setItem(key, value) { storage.set(key, String(value)); }, removeItem(key) { storage.delete(key); }, clear() { storage.clear(); } } }; const server = await createServer({ logLevel: 'error', server: { middlewareMode: true, hmr: false }, appType: 'custom' }); try { const memoryModule = await server.ssrLoadModule( '/src/game/data/firstPursuitScoutMemory.ts' ); const campaignModule = await server.ssrLoadModule( '/src/game/state/campaignState.ts' ); const { battleScenarios } = await server.ssrLoadModule( '/src/game/data/battles.ts' ); verifyCanonicalIds(memoryModule); verifyValidChoices(memoryModule, campaignModule, battleScenarios); verifyInvalidMarkers(memoryModule, campaignModule, battleScenarios); verifyTargetIsolation(memoryModule, campaignModule, battleScenarios); await verifyCampSceneLiteralSync(memoryModule); console.log( 'First-pursuit scout memory verification passed (canonical visit data, strict marker validation, target isolation, and CampScene literal sync).' ); } finally { await server.close(); } function verifyCanonicalIds(memoryModule) { assert( memoryModule.firstPursuitScoutVisitId === 'first-pursuit-scout-tent', 'The first-pursuit scout visit id must remain stable.' ); assert( memoryModule.firstPursuitScoutSourceBattleId === 'first-battle-zhuo-commandery' && memoryModule.firstPursuitScoutTargetBattleId === 'second-battle-yellow-turban-pursuit', 'The scout memory must remain scoped from the first victory to the second battle.' ); assert( JSON.stringify(memoryModule.firstPursuitScoutChoiceIds) === JSON.stringify(['trace-river-ambush', 'mark-village-relief']), 'The two canonical scout choice ids must remain stable and ordered.' ); } function verifyValidChoices(memoryModule, campaignModule, battleScenarios) { const memories = memoryModule.firstPursuitScoutChoiceIds.map((choiceId) => { const campaign = createCompletedScoutCampaign( memoryModule, campaignModule, battleScenarios, choiceId ); const memory = memoryModule.resolveFirstPursuitScoutMemory({ campaign, battleId: memoryModule.firstPursuitScoutTargetBattleId }); assert( memory?.visitId === memoryModule.firstPursuitScoutVisitId && memory.choiceId === choiceId && memory.choiceLabel.length > 0 && memory.deploymentIntentPreview === true && memory.campSummaryLine.startsWith('군영 준비 · ') && memory.openingLines.length === 2 && memory.openingLines[0] === memory.campSummaryLine, `A completed canonical choice must resolve a concise target-battle payoff: ${JSON.stringify(memory)}` ); return memory; }); assert( memories[0].campSummaryLine !== memories[1].campSummaryLine && memories[0].openingLines[1] !== memories[1].openingLines[1], 'Each scout choice must return distinct camp and opening guidance.' ); memories[0].openingLines[0] = 'mutated outside resolver'; const campaign = createCompletedScoutCampaign( memoryModule, campaignModule, battleScenarios, memoryModule.firstPursuitScoutChoiceIds[0] ); const resolvedAgain = memoryModule.resolveFirstPursuitScoutMemory({ campaign, battleId: memoryModule.firstPursuitScoutTargetBattleId }); assert( resolvedAgain?.openingLines[0] === resolvedAgain?.campSummaryLine, 'Resolved opening lines must be cloned instead of exposing canonical data.' ); } function verifyInvalidMarkers( memoryModule, campaignModule, battleScenarios ) { const valid = createCompletedScoutCampaign( memoryModule, campaignModule, battleScenarios, memoryModule.firstPursuitScoutChoiceIds[0] ); const sourceBattleId = memoryModule.firstPursuitScoutSourceBattleId; const targetBattleId = memoryModule.firstPursuitScoutTargetBattleId; const visitId = memoryModule.firstPursuitScoutVisitId; const cases = [ { label: 'missing source settlement', mutate(campaign) { delete campaign.battleHistory[sourceBattleId]; } }, { label: 'source defeat', mutate(campaign) { campaign.battleHistory[sourceBattleId].outcome = 'defeat'; } }, { label: 'mismatched source settlement id', mutate(campaign) { campaign.battleHistory[sourceBattleId].battleId = targetBattleId; } }, { label: 'missing visit completion', mutate(campaign) { campaign.completedCampVisits = []; } }, { label: 'lookalike visit completion', mutate(campaign) { campaign.completedCampVisits = [`${visitId}-forged`]; } }, { label: 'missing choice marker', mutate(campaign) { delete campaign.campVisitChoiceIds[visitId]; } }, { label: 'unknown choice marker', mutate(campaign) { campaign.campVisitChoiceIds[visitId] = 'reveal-all-enemies'; } }, { label: 'choice stored under a forged visit key', mutate(campaign) { campaign.campVisitChoiceIds[`${visitId}-forged`] = campaign.campVisitChoiceIds[visitId]; delete campaign.campVisitChoiceIds[visitId]; } } ]; cases.forEach(({ label, mutate }) => { const campaign = structuredClone(valid); mutate(campaign); const memory = memoryModule.resolveFirstPursuitScoutMemory({ campaign, battleId: targetBattleId }); assert( memory === undefined, `Invalid scout state (${label}) must not grant deployment intent preview: ${JSON.stringify(memory)}` ); }); } function verifyTargetIsolation( memoryModule, campaignModule, battleScenarios ) { const campaign = createCompletedScoutCampaign( memoryModule, campaignModule, battleScenarios, memoryModule.firstPursuitScoutChoiceIds[1] ); const unrelatedBattleIds = [ memoryModule.firstPursuitScoutSourceBattleId, 'third-battle-guangzong-road' ]; unrelatedBattleIds.forEach((battleId) => { const memory = memoryModule.resolveFirstPursuitScoutMemory({ campaign, battleId }); assert( memory === undefined, `Scout payoff must not leak from the second battle into ${battleId}.` ); }); } async function verifyCampSceneLiteralSync(memoryModule) { const source = await readFile( new URL('../src/game/scenes/CampScene.ts', import.meta.url), 'utf8' ); const visitsStart = source.indexOf( 'const campVisits: CampVisitDefinition[] = [' ); const visitStart = source.indexOf( `id: '${memoryModule.firstPursuitScoutVisitId}'`, visitsStart ); assert( visitsStart >= 0 && visitStart > visitsStart, 'CampScene must declare the canonical first-pursuit visit inside campVisits.' ); const visitSource = source.slice(visitStart, visitStart + 2600); assert( /availableAfterBattleIds:\s*\[\s*campBattleIds\.first\s*\]/.test( visitSource ), 'The canonical scout visit must be available immediately after the first battle.' ); memoryModule.firstPursuitScoutChoiceIds.forEach((choiceId) => { assert( visitSource.includes(`id: '${choiceId}'`), `CampScene scout visit is missing canonical choice id ${choiceId}.` ); }); const campaign = createLiteralSyncCampaign(memoryModule); memoryModule.firstPursuitScoutChoiceIds.forEach((choiceId) => { campaign.campVisitChoiceIds[memoryModule.firstPursuitScoutVisitId] = choiceId; const memory = memoryModule.resolveFirstPursuitScoutMemory({ campaign, battleId: memoryModule.firstPursuitScoutTargetBattleId }); assert( visitSource.includes(`label: '${memory.choiceLabel}'`), `CampScene label for ${choiceId} must match the canonical memory label.` ); }); } function createCompletedScoutCampaign( memoryModule, campaignModule, battleScenarios, choiceId ) { storage.clear(); campaignModule.resetCampaignState(); const scenario = battleScenarios[memoryModule.firstPursuitScoutSourceBattleId]; campaignModule.setFirstBattleReport({ battleId: scenario.id, battleTitle: scenario.title, outcome: 'victory', turnNumber: 6, rewardGold: scenario.baseVictoryGold, defeatedEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, totalEnemies: scenario.units.filter( (unit) => unit.faction === 'enemy' ).length, objectives: [], units: scenario.units, bonds: scenario.bonds.map((bond) => ({ ...bond, battleExp: 0 })), itemRewards: [], completedCampDialogues: [], completedCampVisits: [], createdAt: '2026-07-27T00:00:00.000Z' }); const updated = campaignModule.applyCampVisitReward( memoryModule.firstPursuitScoutVisitId, {}, choiceId ); assert( updated, `Campaign state must accept canonical scout choice ${choiceId}.` ); return campaignModule.getCampaignState(); } function createLiteralSyncCampaign(memoryModule) { return { battleHistory: { [memoryModule.firstPursuitScoutSourceBattleId]: { battleId: memoryModule.firstPursuitScoutSourceBattleId, outcome: 'victory' } }, completedCampVisits: [memoryModule.firstPursuitScoutVisitId], campVisitChoiceIds: {} }; } function assert(condition, message) { if (!condition) { throw new Error(message); } }