565 lines
14 KiB
JavaScript
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);
|
|
}
|