Files
heros_web/scripts/verify-first-pursuit-scout-memory.mjs

333 lines
9.5 KiB
JavaScript

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);
}
}