feat: carry camp scouting into battle deployment
This commit is contained in:
332
scripts/verify-first-pursuit-scout-memory.mjs
Normal file
332
scripts/verify-first-pursuit-scout-memory.mjs
Normal file
@@ -0,0 +1,332 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user