Files
Dune/tests/diplomat-late-journey.test.ts
T

119 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { advancedNativePlayer, assertAdvancedNativeCustody } from './fixture-advanced-native-skills';
import {
createDiplomatLateJourney, finishDiplomatLateJourney, settleDiplomatLateJourney,
type DiplomatLateJourney, type DiplomatLateJourneyStep,
} from './fixture-diplomat-late-journey';
function assertOutcome(game: Game, fixture: DiplomatLateJourney, branch: 'copy' | 'decline') {
const expected = fixture.expected[branch];
const own = advancedNativePlayer(game, 'emperor'), opposing = advancedNativePlayer(game, 'guild');
assert.equal(own.leaders.find(l => l.id === fixture.trainer)!.dead, expected.trainerDead);
assert.equal(opposing.leaders.find(l => l.id === fixture.opposingLeader)!.dead, false,
'The actual Portable protects its original holder in either branch.');
assert.equal(own.forces[fixture.key] ?? 0, expected.ownForces);
assert.equal(opposing.forces[fixture.key] ?? 0, expected.opposingForces);
assert.equal(own.tanks, expected.ownTanks);
assert.equal(opposing.tanks, expected.opposingTanks);
assert.equal(game.lastBattleContext!.winner, expected.winner);
assert.equal(game.lastBattleContext!.event, fixture.event);
for (const [id, count] of [
[fixture.ownPoison, expected.ownPoisonDiscards],
[fixture.opposingPoison, expected.opposingPoisonDiscards],
[fixture.portable, expected.portableDiscards],
[fixture.worthless, expected.worthlessDiscards],
] as const) {
assert.equal(game.discard.filter(c => c.id === id).length, count, id);
const physical = [...game.deck, ...game.discard, ...(game.richeseCache ?? []),
...(game.richeseRemoved ?? []), ...game.players.flatMap(p => p.hand)];
assert.equal(physical.filter(c => c.id === id).length, 1, 'Each original played card keeps exactly one physical custodian.');
}
assertAdvancedNativeCustody(game);
}
void test('genuine paid Richese programme offers the used late Portable after Emperor already declined traitor, then mandatory Worthless copy survives', () => {
const f = createDiplomatLateJourney();
for (const shipment of f.shipments) {
const before = shipment.before.players.find(p => p.id === shipment.step.actor)!;
const after = shipment.after.players.find(p => p.id === shipment.step.actor)!;
assert.equal(before.spice - after.spice, shipment.step.actor === f.owner ? 4 : 2);
assert.equal(before.reserves - after.reserves, 2);
assert.equal(after.forces['south_mesa:5'], 2);
}
for (const movement of f.movements) {
const after = movement.after.players.find(p => p.id === movement.step.actor)!;
assert.equal(after.forces[f.key], 2);
assert.equal(after.forces['south_mesa:5'] ?? 0, 0);
}
assert.equal(f.game.spice[f.key], 8);
assert.ok(!f.revealed.battle!.diplomatDefense);
assert.notEqual(f.revealed.decision?.kind, 'diplomatDefense');
assert.equal(f.revealed.battle!.traitorCalls[f.owner], undefined);
assert.equal(viewGame(f.revealed, f.owner).battle!.cards.some(c => c.id === f.portable), false);
assert.deepEqual(f.beforeLate.battle!.traitorCalls, { [f.owner]: false });
assert.equal(viewGame(f.beforeLate, f.opponent).portableSnooper!.blocked, null);
const offered = f.offered;
assert.ok(offered.decision?.kind === 'diplomatDefense');
assert.equal(offered.decision.player, f.owner);
assert.equal(offered.decision.source, f.portable);
assert.deepEqual(offered.decision.cards, [f.worthless]);
assert.equal(offered.battle!.diplomatDefense!.kind, 'snooper');
assert.equal(offered.battle!.lateDefense![f.opponent], f.portable);
assert.deepEqual(offered.battle!.plans, f.revealed.battle!.plans);
assert.equal(offered.battle!.event, f.event);
assert.deepEqual(offered.battle!.traitorCalls, { [f.owner]: false });
for (const actor of [f.owner, f.opponent, f.observer])
assert.ok(viewGame(offered, actor).battle!.cards.some(c => c.id === f.portable));
const copied = applyAction(offered, f.copyAction.actor, f.copyAction.action);
assert.equal(copied.battle!.diplomatDefense!.stage, 'copied');
assert.deepEqual(copied.battle!.plans, f.revealed.battle!.plans);
assert.deepEqual(copied.battle!.traitorCalls, { [f.owner]: false });
assert.deepEqual(viewGame(copied, f.owner).battle!.traitorVoters.filter(id => copied.battle!.traitorCalls[id] === undefined),
f.expected.copy.remainingVoters);
assert.ok(advancedNativePlayer(copied, 'emperor').hand.some(c => c.id === f.worthless),
'Copy selection reserves the original Worthless until real resolution.');
const steps: DiplomatLateJourneyStep[] = [];
const { beforeResolution, cleanup } = settleDiplomatLateJourney(copied, f, steps);
assert.deepEqual(beforeResolution.battle!.traitorCalls, { [f.owner]: false });
assert.equal(cleanup.decision?.kind, 'battleCards');
assert.ok(cleanup.decision?.kind === 'battleCards');
assert.equal(cleanup.decision.player, f.owner);
assert.ok(cleanup.decision.cards.includes(f.ownPoison));
assert.ok(!cleanup.decision.cards.includes(f.worthless), 'The selected Worthless is mandatory, not optional winner cleanup.');
assertOutcome(cleanup, f, 'copy');
const collected = finishDiplomatLateJourney(cleanup, steps);
assert.equal(collected.spice[f.key], 8 - f.expected.copy.collection);
assert.equal(advancedNativePlayer(collected, 'emperor').spice - advancedNativePlayer(cleanup, 'emperor').spice,
f.expected.copy.collection);
assert.ok(advancedNativePlayer(collected, 'emperor').hand.some(c => c.id === f.ownPoison));
assertOutcome(collected, f, 'copy');
});
void test('declining that same genuine late offer leaves poison lethal and Guild retains its original Portable through winner cleanup and Collection', () => {
const f = createDiplomatLateJourney();
assert.ok(f.offered.decision?.kind === 'diplomatDefense');
const declined = applyAction(f.offered, f.declineAction.actor, f.declineAction.action);
assert.equal(declined.battle!.diplomatDefense!.stage, 'declined');
assert.equal(declined.battle!.diplomatDefense!.card, null);
assert.deepEqual(declined.battle!.traitorCalls, { [f.owner]: false });
assert.deepEqual(declined.battle!.plans, f.revealed.battle!.plans);
assert.equal(declined.battle!.event, f.event);
assert.deepEqual(viewGame(declined, f.owner).battle!.traitorVoters.filter(id => declined.battle!.traitorCalls[id] === undefined),
f.expected.decline.remainingVoters);
assert.ok(advancedNativePlayer(declined, 'emperor').hand.some(c => c.id === f.worthless));
const steps: DiplomatLateJourneyStep[] = [];
const { cleanup } = settleDiplomatLateJourney(declined, f, steps);
assert.ok(cleanup.decision?.kind === 'battleCards');
assert.equal(cleanup.decision.player, f.opponent);
assert.ok(cleanup.decision.cards.includes(f.portable));
assert.ok(cleanup.decision.cards.includes(f.opposingPoison));
assertOutcome(cleanup, f, 'decline');
const collected = finishDiplomatLateJourney(cleanup, steps);
assert.equal(collected.spice[f.key], 8 - f.expected.decline.collection);
assert.ok(advancedNativePlayer(collected, 'guild').hand.some(c => c.id === f.portable));
assert.ok(advancedNativePlayer(collected, 'guild').hand.some(c => c.id === f.opposingPoison));
assertOutcome(collected, f, 'decline');
});