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