Files
heros_web/scripts/verify-xuzhou-stay-narrative-memory.mjs

565 lines
14 KiB
JavaScript

import assert from 'node:assert/strict';
import { createServer } from 'vite';
const server = await createServer({
logLevel: 'error',
server: { middlewareMode: true, hmr: false },
appType: 'custom'
});
try {
const narrative = await server.ssrLoadModule(
'/src/game/data/xuzhouStayNarrativeMemory.ts'
);
const payoff = await server.ssrLoadModule(
'/src/game/data/xuzhouStayBattlePayoff.ts'
);
const scenario = await server.ssrLoadModule(
'/src/game/data/scenario.ts'
);
verifyIntroChoices(narrative, payoff, scenario);
verifyInformationOnlyIntro(narrative, payoff, scenario);
verifyHistoricalInformationAndSupport(
narrative,
payoff,
scenario
);
verifyUnusedNoDeploymentAndRetreat(
narrative,
payoff,
scenario
);
verifyIsolationAndMalformedInputs(
narrative,
payoff,
scenario
);
verifyInputImmutability(narrative, payoff, scenario);
console.log(
'Xuzhou stay narrative memory verification passed (two intro choices, information-only memory, historical counterplays, support used/unused, Mi Zhu no-deployment/retreat, malformed and other-battle isolation, old-save fallback, historical independence, exact portrait/page placement, and input immutability).'
);
} finally {
await server.close();
}
function verifyIntroChoices(narrative, payoff, scenario) {
const aid = resolveIntro(
narrative,
scenario,
campaignMemory(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId
})
);
assert.equal(
aid.memory.source,
'campaign'
);
assert.equal(aid.memory.stage, 'story');
assert.equal(
aid.memory.choiceId,
payoff.xuzhouStayShareStorehouseDutyChoiceId
);
assert.equal(aid.memory.informationCompleted, true);
assert.deepEqual(aid.memory.appliedPageIds, [
narrative.xuzhouStayNarrativeIntroPageId
]);
const aidPage = insertedPage(
aid,
narrative.xuzhouStayNarrativeIntroPageId
);
assert.equal(aidPage.portrait, 'mi-zhu');
assert.equal(aidPage.speaker, '미축');
assert.match(aidPage.text, /장부/);
assert.match(aidPage.text, /창고 당번/);
assertInsertedBetween(
aid.pages,
'eighth-xiaopei-road',
narrative.xuzhouStayNarrativeIntroPageId,
'eighth-sortie-choice'
);
const encourage = resolveIntro(
narrative,
scenario,
campaignMemory(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId
})
);
assert.equal(
encourage.memory.choiceId,
payoff.xuzhouStayTrustMiZhuLedgerChoiceId
);
const encouragePage = insertedPage(
encourage,
narrative.xuzhouStayNarrativeIntroPageId
);
assert.match(encouragePage.text, /장부를 제게 맡겨/);
assert.match(encouragePage.text, /독려/);
}
function verifyInformationOnlyIntro(
narrative,
payoff,
scenario
) {
const result = resolveIntro(
narrative,
scenario,
campaignMemory(payoff, {
informationCompleted: true
})
);
assert.equal(result.memory.choiceId, null);
assert.equal(result.memory.informationCompleted, true);
const page = insertedPage(
result,
narrative.xuzhouStayNarrativeIntroPageId
);
assert.match(page.text, /장부/);
assert.match(page.text, /출진 장수/);
}
function verifyHistoricalInformationAndSupport(
narrative,
payoff,
scenario
) {
const aidReport = historicalReport(
payoff,
payoffSnapshot(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId,
informationCounterplays: 2,
miZhuDeployed: true,
supportAttempts: 1,
supportUses: 1,
bonusHealing: 4
}),
14
);
const aid = resolveAftermath(
narrative,
scenario,
aidReport,
campaignMemory(payoff, {
informationCompleted: false,
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId
})
);
assert.equal(aid.memory.source, 'historical-payoff');
assert.equal(aid.memory.informationCounterplays, 2);
assert.equal(aid.memory.supportOutcome, 'used');
assert.equal(aid.memory.miZhuStatus, 'deployed');
assert.equal(aid.memory.bonusHealing, 4);
const aidPage = insertedPage(
aid,
narrative.xuzhouStayNarrativeAftermathPageId
);
assert.equal(aidPage.portrait, 'mi-zhu');
assert.match(aidPage.text, /2곳/);
assert.match(aidPage.text, /응급 회복을 4만큼/);
assert.doesNotMatch(aidPage.text, /격려의 기세/);
assertInsertedBetween(
aid.pages,
'eighth-victory-supply-road',
narrative.xuzhouStayNarrativeAftermathPageId,
'eighth-lu-bu-arrival'
);
const changedCampaign = campaignMemory(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId
});
const replayed = resolveAftermath(
narrative,
scenario,
aidReport,
changedCampaign
);
assert.deepEqual(
replayed,
aid,
'Historical aftermath text must not change with current campaign memory.'
);
const encourage = resolveAftermath(
narrative,
scenario,
historicalReport(
payoff,
payoffSnapshot(payoff, {
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId,
miZhuDeployed: true,
supportAttempts: 1,
supportUses: 1,
bonusBuffTurns: 1
}),
9
)
);
assert.equal(encourage.memory.supportOutcome, 'used');
assert.equal(encourage.memory.bonusBuffTurns, 1);
assert.match(
insertedPage(
encourage,
narrative.xuzhouStayNarrativeAftermathPageId
).text,
/격려의 기세를 1턴/
);
const informationOnly = resolveAftermath(
narrative,
scenario,
historicalReport(
payoff,
payoffSnapshot(payoff, {
informationCompleted: true,
informationCounterplays: 1
})
)
);
assert.equal(informationOnly.memory.choiceId, null);
assert.equal(informationOnly.memory.supportOutcome, null);
assert.match(
insertedPage(
informationOnly,
narrative.xuzhouStayNarrativeAftermathPageId
).text,
/1곳/
);
}
function verifyUnusedNoDeploymentAndRetreat(
narrative,
payoff,
scenario
) {
const unused = resolveAftermath(
narrative,
scenario,
historicalReport(
payoff,
payoffSnapshot(payoff, {
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId,
miZhuDeployed: true
}),
11
)
);
assert.equal(unused.memory.miZhuStatus, 'deployed');
assert.equal(unused.memory.supportOutcome, 'unused');
assert.match(
insertedPage(
unused,
narrative.xuzhouStayNarrativeAftermathPageId
).text,
/쓸 기회는 없었지만/
);
const notDeployed = resolveAftermath(
narrative,
scenario,
historicalReport(
payoff,
payoffSnapshot(payoff, {
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId,
miZhuDeployed: false
}),
null
)
);
assert.equal(
notDeployed.memory.miZhuStatus,
'not-deployed'
);
assert.equal(
notDeployed.memory.supportOutcome,
'unavailable'
);
assert.match(
insertedPage(
notDeployed,
narrative.xuzhouStayNarrativeAftermathPageId
).text,
/출전에 함께하지 못해/
);
const retreated = resolveAftermath(
narrative,
scenario,
historicalReport(
payoff,
payoffSnapshot(payoff, {
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId,
miZhuDeployed: true
}),
0
)
);
assert.equal(retreated.memory.miZhuStatus, 'retreated');
assert.equal(retreated.memory.supportOutcome, 'unused');
assert.match(
insertedPage(
retreated,
narrative.xuzhouStayNarrativeAftermathPageId
).text,
/전장에서 물러나는 바람에/
);
}
function verifyIsolationAndMalformedInputs(
narrative,
payoff,
scenario
) {
const noIntroMemory = resolveIntro(
narrative,
scenario,
campaignMemory(payoff, {})
);
assert.equal(noIntroMemory.memory, undefined);
assert.equal(
noIntroMemory.pages.length,
scenario.eighthBattleIntroPages.length
);
const noHistoricalMemory = resolveAftermath(
narrative,
scenario,
{
battleId: payoff.xuzhouStayBattlePayoffTargetBattleId,
outcome: 'victory',
units: []
}
);
assert.equal(noHistoricalMemory.memory, undefined);
assert.equal(
noHistoricalMemory.pages.length,
scenario.eighthBattleVictoryPages.length
);
const malformed = resolveAftermath(
narrative,
scenario,
{
battleId: payoff.xuzhouStayBattlePayoffTargetBattleId,
outcome: 'victory',
units: [{ unitId: 'mi-zhu', hp: 10 }],
xuzhouStayBattlePayoff: {
...payoffSnapshot(payoff, {
informationCompleted: true
}),
recordId: 'forged-record'
}
}
);
assert.equal(malformed.memory, undefined);
const otherBattle = narrative.resolveXuzhouStayNarrativeMemory(
scenario.eighthBattleIntroPages,
{
battleId: 'ninth-battle-xuzhou-gate-night-raid',
stage: 'story',
campaign: campaignMemory(payoff, {
informationCompleted: true
})
}
);
assert.equal(otherBattle.memory, undefined);
assert.equal(
otherBattle.pages.some((page) =>
page.id.startsWith('eighth-xuzhou-stay-memory')
),
false
);
const defeat = resolveAftermath(
narrative,
scenario,
{
...historicalReport(
payoff,
payoffSnapshot(payoff, {
informationCompleted: true
})
),
outcome: 'defeat'
}
);
assert.equal(defeat.memory, undefined);
}
function verifyInputImmutability(narrative, payoff, scenario) {
const pages = structuredClone(
scenario.eighthBattleVictoryPages
);
const campaign = campaignMemory(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId
});
const report = historicalReport(
payoff,
payoffSnapshot(payoff, {
informationCompleted: true,
choiceId: payoff.xuzhouStayTrustMiZhuLedgerChoiceId,
informationCounterplays: 2,
miZhuDeployed: true,
supportAttempts: 1,
supportUses: 1,
bonusBuffTurns: 1
}),
8
);
const beforePages = structuredClone(pages);
const beforeCampaign = structuredClone(campaign);
const beforeReport = structuredClone(report);
const result = narrative.resolveXuzhouStayNarrativeMemory(
pages,
{
battleId: payoff.xuzhouStayBattlePayoffTargetBattleId,
stage: 'aftermath',
campaign,
historicalReport: report
}
);
assert.deepEqual(pages, beforePages);
assert.deepEqual(campaign, beforeCampaign);
assert.deepEqual(report, beforeReport);
assert.notEqual(result.pages, pages);
assert.notEqual(result.pages[0], pages[0]);
result.pages[0].text = 'mutated output';
assert.deepEqual(pages, beforePages);
}
function resolveIntro(narrative, scenario, campaign) {
return narrative.resolveXuzhouStayNarrativeMemory(
scenario.eighthBattleIntroPages,
{
battleId: 'eighth-battle-xiaopei-supply-road',
stage: 'story',
campaign
}
);
}
function resolveAftermath(
narrative,
scenario,
historicalReport,
campaign
) {
return narrative.resolveXuzhouStayNarrativeMemory(
scenario.eighthBattleVictoryPages,
{
battleId: 'eighth-battle-xiaopei-supply-road',
stage: 'aftermath',
campaign,
historicalReport
}
);
}
function campaignMemory(
payoff,
{ informationCompleted = false, choiceId } = {}
) {
return {
battleHistory: {
[payoff.xuzhouStayBattlePayoffSourceBattleId]: {
battleId: payoff.xuzhouStayBattlePayoffSourceBattleId,
outcome: 'victory'
}
},
completedCampVisits: informationCompleted
? [payoff.xuzhouStayInformationVisitId]
: [],
completedCampDialogues: choiceId
? [payoff.xuzhouStayResonanceDialogueId]
: [],
campDialogueChoiceIds: choiceId
? {
[payoff.xuzhouStayResonanceDialogueId]: choiceId
}
: {}
};
}
function payoffSnapshot(
payoff,
{
informationCompleted = false,
choiceId,
informationCounterplays = 0,
miZhuDeployed = false,
supportAttempts = 0,
supportUses = 0,
bonusHealing = 0,
bonusBuffTurns = 0
} = {}
) {
return {
version: payoff.xuzhouStayBattlePayoffVersion,
recordId: payoff.xuzhouStayBattlePayoffRecordId,
cityStayId: payoff.xuzhouStayId,
sourceBattleId:
payoff.xuzhouStayBattlePayoffSourceBattleId,
targetBattleId:
payoff.xuzhouStayBattlePayoffTargetBattleId,
informationVisitId: payoff.xuzhouStayInformationVisitId,
dialogueId: payoff.xuzhouStayResonanceDialogueId,
informationCompleted,
...(choiceId ? { choiceId } : {}),
informationCounterplays,
miZhuDeployed,
supportAttempts,
supportUses,
bonusHealing,
bonusBuffTurns
};
}
function historicalReport(payoff, snapshot, miZhuHp = null) {
return {
battleId: payoff.xuzhouStayBattlePayoffTargetBattleId,
outcome: 'victory',
units:
miZhuHp === null
? []
: [
{
unitId: payoff.xuzhouStayMiZhuUnitId,
hp: miZhuHp
}
],
xuzhouStayBattlePayoff: snapshot
};
}
function insertedPage(result, pageId) {
assert(result.memory, `Missing narrative memory for ${pageId}.`);
const page = result.pages.find(
(candidate) => candidate.id === pageId
);
assert(page, `Missing inserted page: ${pageId}`);
return page;
}
function assertInsertedBetween(
pages,
previousId,
insertedId,
nextId
) {
const ids = pages.map((page) => page.id);
const insertedIndex = ids.indexOf(insertedId);
assert(insertedIndex > 0, `Missing inserted page: ${insertedId}`);
assert.equal(ids[insertedIndex - 1], previousId);
assert.equal(ids[insertedIndex + 1], nextId);
}