Files
heros_web/scripts/verify-xuzhou-stay-battle-payoff.mjs

576 lines
16 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 payoff = await server.ssrLoadModule(
'/src/game/data/xuzhouStayBattlePayoff.ts'
);
const cityStays = await server.ssrLoadModule(
'/src/game/data/cityStays.ts'
);
const battleUsables = await server.ssrLoadModule(
'/src/game/data/battleUsables.ts'
);
const scenario = await server.ssrLoadModule(
'/src/game/data/scenario.ts'
);
verifyCanonicalDefinition({
payoff,
cityStays,
battleUsables,
scenario
});
const memories = verifyCampaignResolution(payoff);
verifyRuntimeAndSnapshotNormalization(payoff, memories);
console.log(
'Xuzhou stay battle payoff verification passed (canonical city provenance, source-victory precedence, independent information/dialogue resolution, target-battle isolation, first-three-turn aid/encourage effects, counterplay bounds, Mi Zhu deployment neutralization, choice-exclusive counters, snapshot cloning, and forged-state rejection).'
);
} finally {
await server.close();
}
function verifyCanonicalDefinition({
payoff,
cityStays,
battleUsables,
scenario
}) {
const city = cityStays.getCityStayDefinition('xuzhou');
const definition = payoff.xuzhouStayBattlePayoffDefinition;
assert.equal(definition.version, 1);
assert.equal(definition.recordId, 'xuzhou-stay-battle-payoff');
assert.equal(definition.cityStayId, city.id);
assert.equal(definition.sourceBattleId, city.afterBattleId);
assert.equal(definition.targetBattleId, city.nextBattleId);
assert.equal(definition.information.visitId, city.information.id);
assert.equal(definition.dialogue.dialogueId, city.dialogue.id);
assert.deepEqual(
Object.keys(definition.dialogue.choices),
city.dialogue.choices.map((choice) => choice.id),
'Both canonical Xuzhou dialogue choices must have a battle payoff.'
);
assert.deepEqual(payoff.xuzhouStayBattlePayoffChoiceIds, [
'city-xuzhou-share-storehouse-duty',
'city-xuzhou-trust-mi-zhu-ledger'
]);
assert.deepEqual(definition.information.effect, {
kind: 'deployment-intent-preview',
enemyUnitIds: ['xiaopei-guard-a', 'xiaopei-guard-b'],
previewCount: 2
});
const eighthBattleEnemyIds = new Set(
scenario.eighthBattleUnits
.filter((unit) => unit.faction === 'enemy')
.map((unit) => unit.id)
);
definition.information.effect.enemyUnitIds.forEach((unitId) => {
assert(
eighthBattleEnemyIds.has(unitId),
`Information preview target is missing from battle 8: ${unitId}`
);
});
const aid = payoff.getXuzhouStayResonanceEffect(
'city-xuzhou-share-storehouse-duty'
);
assert.deepEqual(aid, {
kind: 'mi-zhu-first-aid-healing',
actorUnitId: 'mi-zhu',
usableId: 'aid',
activeThroughTurn: 3,
bonusHealing: 4,
maxBattleUses: 1
});
const encourage = payoff.getXuzhouStayResonanceEffect(
'city-xuzhou-trust-mi-zhu-ledger'
);
assert.deepEqual(encourage, {
kind: 'mi-zhu-first-encourage-duration',
actorUnitId: 'mi-zhu',
usableId: 'encourage',
activeThroughTurn: 3,
bonusBuffTurns: 1,
maxBattleUses: 1
});
assert.equal(
payoff.getXuzhouStayResonanceEffect('forged-choice'),
undefined
);
assert(
battleUsables.unitStrategyIds['mi-zhu'].includes(aid.usableId),
'Mi Zhu must know aid before his aid payoff can be applied.'
);
assert(
battleUsables.unitStrategyIds['mi-zhu'].includes(
encourage.usableId
),
'Mi Zhu must know encourage before his encourage payoff can be applied.'
);
assert.equal(
battleUsables.usableCatalog.encourage.duration + encourage.bonusBuffTurns,
3,
'The entrusted ledger choice should extend encourage from two to three turns.'
);
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(1), true);
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(3), true);
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(0), false);
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(4), false);
assert.equal(payoff.isXuzhouStayBattlePayoffActiveTurn(2.5), false);
}
function verifyCampaignResolution(payoff) {
const sourceBattleId =
payoff.xuzhouStayBattlePayoffSourceBattleId;
const targetBattleId =
payoff.xuzhouStayBattlePayoffTargetBattleId;
const victory = {
battleId: sourceBattleId,
outcome: 'victory'
};
const baseCampaign = {
battleHistory: {
[sourceBattleId]: victory
},
firstBattleReport: null,
completedCampVisits: [],
completedCampDialogues: [],
campDialogueChoiceIds: {}
};
assert.equal(
payoff.resolveXuzhouStayBattlePayoff(baseCampaign),
undefined,
'A victory without either city activity must not create a payoff.'
);
assert.equal(
payoff.resolveXuzhouStayBattlePayoff(
{
...baseCampaign,
completedCampVisits: [payoff.xuzhouStayInformationVisitId]
},
'ninth-battle-xuzhou-gate-night-raid'
),
undefined,
'Xuzhou preparation must not leak beyond battle 8.'
);
const informationOnly =
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampVisits: [
'unrelated-visit',
payoff.xuzhouStayInformationVisitId
]
});
assert(informationOnly);
assert.equal(informationOnly.recordSource, 'battle-history');
assert.equal(informationOnly.informationCompleted, true);
assert.equal(informationOnly.choiceId, undefined);
assert.deepEqual(informationOnly.informationEffect, {
kind: 'deployment-intent-preview',
enemyUnitIds: ['xiaopei-guard-a', 'xiaopei-guard-b'],
previewCount: 2
});
assert.notEqual(
informationOnly.informationEffect.enemyUnitIds,
payoff.xuzhouStayInformationEnemyUnitIds,
'Resolved memories must not expose the canonical tuple by reference.'
);
const aidOnly = payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampDialogues: [
payoff.xuzhouStayResonanceDialogueId
],
campDialogueChoiceIds: {
[payoff.xuzhouStayResonanceDialogueId]:
payoff.xuzhouStayShareStorehouseDutyChoiceId
}
});
assert(aidOnly);
assert.equal(aidOnly.informationCompleted, false);
assert.equal(
aidOnly.choiceId,
payoff.xuzhouStayShareStorehouseDutyChoiceId
);
assert.equal(
aidOnly.resonanceEffect.kind,
'mi-zhu-first-aid-healing'
);
const bothWithEncourage =
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampVisits: [
payoff.xuzhouStayInformationVisitId
],
completedCampDialogues: [
payoff.xuzhouStayResonanceDialogueId
],
campDialogueChoiceIds: {
[payoff.xuzhouStayResonanceDialogueId]:
payoff.xuzhouStayTrustMiZhuLedgerChoiceId
}
});
assert(bothWithEncourage);
assert.equal(bothWithEncourage.informationCompleted, true);
assert.equal(
bothWithEncourage.choiceId,
payoff.xuzhouStayTrustMiZhuLedgerChoiceId
);
assert.equal(
bothWithEncourage.resonanceEffect.kind,
'mi-zhu-first-encourage-duration'
);
assert.equal(
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampDialogues: [],
campDialogueChoiceIds: {
[payoff.xuzhouStayResonanceDialogueId]:
payoff.xuzhouStayShareStorehouseDutyChoiceId
}
}),
undefined,
'A remembered choice without completed dialogue is not evidence.'
);
assert.equal(
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampDialogues: [
payoff.xuzhouStayResonanceDialogueId
],
campDialogueChoiceIds: {
[payoff.xuzhouStayResonanceDialogueId]: 'forged-choice'
}
}),
undefined,
'A completed dialogue with a forged choice must stay neutral.'
);
const informationSurvivesForgedDialogue =
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
completedCampVisits: [
payoff.xuzhouStayInformationVisitId
],
completedCampDialogues: [
payoff.xuzhouStayResonanceDialogueId
],
campDialogueChoiceIds: {
[payoff.xuzhouStayResonanceDialogueId]: 'forged-choice'
}
});
assert(informationSurvivesForgedDialogue);
assert.equal(
informationSurvivesForgedDialogue.informationCompleted,
true
);
assert.equal(
informationSurvivesForgedDialogue.choiceId,
undefined,
'Invalid dialogue data must not erase independently earned information.'
);
const legacyInformation =
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
battleHistory: {},
firstBattleReport: victory,
completedCampVisits: [
payoff.xuzhouStayInformationVisitId
]
});
assert(legacyInformation);
assert.equal(legacyInformation.recordSource, 'legacy-report');
assert.equal(
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
battleHistory: {
[sourceBattleId]: {
battleId: sourceBattleId,
outcome: 'defeat'
}
},
firstBattleReport: victory,
completedCampVisits: [
payoff.xuzhouStayInformationVisitId
]
}),
undefined,
'An explicit invalid history entry must shadow a stale legacy report.'
);
assert.equal(
payoff.resolveXuzhouStayBattlePayoff({
...baseCampaign,
battleHistory: {},
firstBattleReport: {
battleId: targetBattleId,
outcome: 'victory'
},
completedCampVisits: [
payoff.xuzhouStayInformationVisitId
]
}),
undefined,
'A victory from another battle must not authorize this payoff.'
);
return {
informationOnly,
aidOnly,
bothWithEncourage
};
}
function verifyRuntimeAndSnapshotNormalization(payoff, memories) {
const aidRuntime =
payoff.createXuzhouStayBattlePayoffRuntime(
memories.aidOnly,
{
miZhuDeployed: true,
expectedRosterUnitIds: ['liu-bei', 'mi-zhu']
}
);
assert.deepEqual(aidRuntime, {
version: 1,
recordId: 'xuzhou-stay-battle-payoff',
cityStayId: 'xuzhou',
sourceBattleId: 'seventh-battle-xuzhou-rescue',
targetBattleId: 'eighth-battle-xiaopei-supply-road',
informationVisitId: 'city-xuzhou-xiaopei-ledger',
dialogueId: 'city-xuzhou-liu-mi-supply-trust',
informationCompleted: false,
choiceId: 'city-xuzhou-share-storehouse-duty',
informationCounterplays: 0,
miZhuDeployed: true,
supportAttempts: 0,
supportUses: 0,
bonusHealing: 0,
bonusBuffTurns: 0
});
const saturatedAid =
payoff.normalizeXuzhouStayBattlePayoffRuntime(
{
...aidRuntime,
informationCounterplays: 99,
supportAttempts: 4.9,
supportUses: 8,
bonusHealing: 99,
bonusBuffTurns: 99
},
{
expectedBattleId:
payoff.xuzhouStayBattlePayoffTargetBattleId,
expectedRosterUnitIds: new Set(['mi-zhu']),
expectedMemory: memories.aidOnly
}
);
assert.deepEqual(saturatedAid, {
...aidRuntime,
supportAttempts: 4,
supportUses: 1,
bonusHealing: 4
});
const zeroBenefitAid =
payoff.normalizeXuzhouStayBattlePayoffRuntime({
...aidRuntime,
supportAttempts: 1,
supportUses: 1,
bonusHealing: 0
});
assert.equal(
zeroBenefitAid.supportUses,
0,
'Aid cannot be consumed unless it provided at least one point of real bonus healing.'
);
assert.equal(zeroBenefitAid.bonusHealing, 0);
const informationRuntime =
payoff.createXuzhouStayBattlePayoffRuntime(
memories.informationOnly,
{
miZhuDeployed: false,
expectedRosterUnitIds: ['liu-bei']
}
);
const boundedInformation =
payoff.normalizeXuzhouStayBattlePayoffRuntime({
...informationRuntime,
informationCounterplays: 500,
supportAttempts: 3,
supportUses: 1,
bonusHealing: 4,
bonusBuffTurns: 1
});
assert.equal(boundedInformation.informationCounterplays, 2);
assert.equal(boundedInformation.supportAttempts, 0);
assert.equal(boundedInformation.supportUses, 0);
assert.equal(boundedInformation.bonusHealing, 0);
assert.equal(boundedInformation.bonusBuffTurns, 0);
const neutralizedGhost =
payoff.normalizeXuzhouStayBattlePayoffRuntime(
{
...saturatedAid,
informationCompleted: true,
informationCounterplays: 2,
miZhuDeployed: true
},
{
expectedRosterUnitIds: ['liu-bei']
}
);
assert.equal(neutralizedGhost.miZhuDeployed, false);
assert.equal(neutralizedGhost.supportAttempts, 0);
assert.equal(neutralizedGhost.supportUses, 0);
assert.equal(neutralizedGhost.bonusHealing, 0);
assert.equal(neutralizedGhost.bonusBuffTurns, 0);
assert.equal(
neutralizedGhost.informationCounterplays,
2,
'Mi Zhu deployment must not erase the independent information payoff.'
);
const rosterRepairsDeployment =
payoff.normalizeXuzhouStayBattlePayoffRuntime(
{
...aidRuntime,
miZhuDeployed: false,
supportAttempts: 2,
supportUses: 1,
bonusHealing: 3
},
{
expectedRosterUnitIds: ['mi-zhu']
}
);
assert.equal(rosterRepairsDeployment.miZhuDeployed, true);
assert.equal(rosterRepairsDeployment.supportAttempts, 2);
assert.equal(rosterRepairsDeployment.supportUses, 1);
assert.equal(rosterRepairsDeployment.bonusHealing, 3);
const encourageRuntime =
payoff.createXuzhouStayBattlePayoffRuntime(
memories.bothWithEncourage,
{
miZhuDeployed: true,
expectedRosterUnitIds: ['mi-zhu']
}
);
const appliedEncourage =
payoff.normalizeXuzhouStayBattlePayoffRuntime(
{
...encourageRuntime,
informationCounterplays: 2,
supportAttempts: 3,
supportUses: 7,
bonusHealing: 50,
bonusBuffTurns: 50
},
{
expectedMemory: memories.bothWithEncourage
}
);
assert.equal(appliedEncourage.informationCounterplays, 2);
assert.equal(appliedEncourage.supportAttempts, 3);
assert.equal(appliedEncourage.supportUses, 1);
assert.equal(appliedEncourage.bonusHealing, 0);
assert.equal(appliedEncourage.bonusBuffTurns, 1);
const noEncourageUse =
payoff.normalizeXuzhouStayBattlePayoffRuntime({
...encourageRuntime,
supportAttempts: 1,
supportUses: 0,
bonusBuffTurns: 99
});
assert.equal(noEncourageUse.supportAttempts, 1);
assert.equal(noEncourageUse.supportUses, 0);
assert.equal(noEncourageUse.bonusBuffTurns, 0);
const snapshot =
payoff.createXuzhouStayBattlePayoffSnapshot(
appliedEncourage,
{
expectedBattleId:
payoff.xuzhouStayBattlePayoffTargetBattleId,
expectedRosterUnitIds: ['mi-zhu']
}
);
assert.deepEqual(snapshot, appliedEncourage);
assert.notEqual(snapshot, appliedEncourage);
const snapshotClone =
payoff.cloneXuzhouStayBattlePayoffSnapshot(snapshot);
const runtimeClone =
payoff.cloneXuzhouStayBattlePayoffRuntime(appliedEncourage);
assert.deepEqual(snapshotClone, snapshot);
assert.deepEqual(runtimeClone, appliedEncourage);
assert.notEqual(snapshotClone, snapshot);
assert.notEqual(runtimeClone, appliedEncourage);
assert.equal(
payoff.normalizeXuzhouStayBattlePayoffSnapshot(snapshot, {
expectedBattleId: 'ninth-battle-xuzhou-gate-night-raid'
}),
undefined,
'A containing report from another battle must reject the snapshot.'
);
assert.equal(
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
{
...snapshot,
targetBattleId: 'forged-battle'
}
),
undefined
);
assert.equal(
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
{
...snapshot,
choiceId: 'forged-choice'
}
),
undefined
);
assert.equal(
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
{
...snapshot,
informationCompleted: false
},
{
expectedMemory: memories.bothWithEncourage
}
),
undefined,
'Loaded runtime evidence must match the campaign-derived memory.'
);
assert.equal(
payoff.normalizeXuzhouStayBattlePayoffSnapshot(
{
...snapshot,
choiceId: payoff.xuzhouStayShareStorehouseDutyChoiceId
},
{
expectedMemory: memories.bothWithEncourage
}
),
undefined,
'A save cannot exchange one completed resonance choice for another.'
);
}