119 lines
6.6 KiB
TypeScript
119 lines
6.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { leaders } from '../game/cards';
|
|
import { viewGame, type Action } from '../game/engine';
|
|
import {
|
|
assertMixedE3MoritaniPhysical, assertMixedE3MoritaniRejects, mixedE3MoritaniPlayer,
|
|
quoteMixedE3MoritaniBattle,
|
|
} from './fixture-mixed-e3-moritani-nexus';
|
|
import {
|
|
createMoritaniReplacementExchangeFixture, createMoritaniReplacementRedrawProgramme,
|
|
finishMoritaniReplacementExchange, moritaniReplacementDrawStep,
|
|
} from './fixture-moritani-replacement-exchange';
|
|
|
|
void test('an original Harkonnen Secret Ally return gives the historical Mentat replacement back to the physical deck without disturbing its public receipt', () => {
|
|
const f = createMoritaniReplacementExchangeFixture();
|
|
assertMixedE3MoritaniPhysical(f.ready);
|
|
assert.deepEqual(mixedE3MoritaniPlayer(f.ready, f.moritani).traitors, [f.historicalReplacement]);
|
|
const own = viewGame(f.ready, f.moritani);
|
|
assert.equal(own.nexusCards!.card, 'harkonnen');
|
|
assert.deepEqual(own.nexusTraitors!.pending, null);
|
|
assert.equal(own.nexusTraitors!.offer!.draw, 2);
|
|
assert.equal(own.nexusTraitors!.offer!.blocked, null);
|
|
|
|
const result = finishMoritaniReplacementExchange(f);
|
|
const exchange = result.exchanged.nexusTraitorExchanges!.at(-1)!;
|
|
assert.equal(exchange.stage, 'complete');
|
|
assert.deepEqual(exchange.drawn, [...f.drawn]);
|
|
assert.deepEqual([...exchange.returned].sort(), [...result.returnCards].sort());
|
|
assert.ok(result.returnCards.includes(f.historicalReplacement),
|
|
'The owner really gave the historical replacement back.');
|
|
assert.deepEqual(exchange.before, [f.historicalReplacement]);
|
|
assert.equal(result.exchanged.traitorReserve![0], f.historicalReplacement,
|
|
'The returned historical replacement is a physical reserve card again.');
|
|
assert.deepEqual(mixedE3MoritaniPlayer(result.exchanged, f.moritani).traitors, [f.guildTraitor],
|
|
'Only the real newly drawn Traitor Card stays in the owner hand.');
|
|
|
|
assert.equal(result.exchanged.traitorReserve!.includes(f.retiredVictim), false);
|
|
for (const player of result.exchanged.players)
|
|
assert.equal(player.traitors.includes(f.retiredVictim), false);
|
|
const receipt = result.exchanged.moritaniAssassinate!.opportunities.at(-1)!;
|
|
assert.equal(receipt.stage, 'replaced');
|
|
assert.equal(receipt.replacement, f.historicalReplacement);
|
|
assert.deepEqual({ event: receipt.event, card: receipt.card, replacement: receipt.replacement,
|
|
signature: receipt.signature }, f.history,
|
|
'The recorded public victim, its private replacement and the receipt signature must not be rewritten.');
|
|
|
|
assert.equal(result.exchanged.turn, f.ready.turn);
|
|
assert.equal(result.exchanged.phase, 8);
|
|
assert.equal(result.exchanged.nexusTraitorPending ?? null, null);
|
|
assert.deepEqual(result.exchanged.decision, f.ready.decision,
|
|
'The owner keeps its suspended original Mentat placement after returning physical cards.');
|
|
assert.equal(result.exchanged.nexusCards!.cards!.discard.filter(card => card === 'harkonnen').length, 1);
|
|
assertMixedE3MoritaniPhysical(result.exchanged);
|
|
});
|
|
|
|
void test('the physical return stays owner-only and cannot be replayed against the real custody', () => {
|
|
const f = createMoritaniReplacementExchangeFixture();
|
|
const guild = f.seats.guild!;
|
|
const rival = viewGame(f.ready, guild).nexusTraitors!;
|
|
assert.equal(rival.offer, null);
|
|
assert.equal(rival.pending, null);
|
|
assertMixedE3MoritaniRejects(f.ready, guild, f.draw);
|
|
|
|
const drawn = moritaniReplacementDrawStep(f);
|
|
const pending = viewGame(drawn.after, f.moritani).nexusTraitors!.pending!;
|
|
assert.deepEqual(pending.choices.map(choice => choice.id), [f.historicalReplacement, ...f.drawn]);
|
|
assert.deepEqual(mixedE3MoritaniPlayer(drawn.after, f.moritani).traitors,
|
|
[f.historicalReplacement, ...f.drawn]);
|
|
const rivalPending = viewGame(drawn.after, guild).nexusTraitors!.pending!;
|
|
assert.equal(rivalPending.event, pending.event);
|
|
assert.deepEqual(rivalPending.choices, []);
|
|
const giveBack: Action = { type: 'nexusTraitorReturn', event: pending.event,
|
|
cards: [f.historicalReplacement, f.guildTraitor] };
|
|
assertMixedE3MoritaniRejects(drawn.after, guild, giveBack);
|
|
assertMixedE3MoritaniRejects(drawn.after, f.moritani,
|
|
{ type: 'nexusTraitorReturn', event: pending.event,
|
|
cards: [f.historicalReplacement, f.historicalReplacement] });
|
|
assertMixedE3MoritaniRejects(drawn.after, f.moritani, f.draw);
|
|
});
|
|
|
|
void test('a later original battle loss lets the next Mentat draw the same physical card again while the older Nexus history stays unchanged', () => {
|
|
const programme = createMoritaniReplacementRedrawProgramme();
|
|
const f = programme.fixture;
|
|
const guild = f.seats.guild!;
|
|
assertMixedE3MoritaniPhysical(programme.exchanged);
|
|
const historical = structuredClone(programme.exchanged.nexusTraitorExchanges);
|
|
|
|
assert.equal(quoteMixedE3MoritaniBattle(programme.revealed).winner, guild,
|
|
'The real second loss must be a surviving Guild win.');
|
|
const receipts = programme.redrawn.moritaniAssassinate!.opportunities;
|
|
assert.equal(receipts.length, 2);
|
|
const [first, second] = receipts;
|
|
assert.equal(first.stage, 'replaced');
|
|
assert.equal(second.stage, 'replaced');
|
|
assert.equal(second.turn, 3);
|
|
assert.equal(second.card, f.guildTraitor, 'The second public marker retires the held Guild Traitor.');
|
|
assert.equal(second.replacement, first.replacement,
|
|
'The later original Mentat replacement may be the very card the older Nexus history returned.');
|
|
assert.equal(second.replacement, f.historicalReplacement);
|
|
assert.equal(second.bounty, leaders('guild').find(leader => leader.id === f.guildTraitor)!.strength);
|
|
assert.equal(mixedE3MoritaniPlayer(programme.redrawn, guild).leaders
|
|
.find(leader => leader.id === f.guildTraitor)!.dead, true);
|
|
|
|
assert.deepEqual(mixedE3MoritaniPlayer(programme.redrawn, f.moritani).traitors, [f.historicalReplacement],
|
|
'The same physical card is really held again.');
|
|
assert.equal(programme.redrawn.traitorReserve!.includes(f.historicalReplacement), false);
|
|
assert.equal(programme.redrawn.traitorReserve!.includes(f.guildTraitor), false);
|
|
assert.equal(programme.redrawn.traitorReserve!.includes(f.retiredVictim), false);
|
|
for (const player of programme.redrawn.players)
|
|
for (const retired of [f.retiredVictim, f.guildTraitor])
|
|
assert.equal(player.traitors.includes(retired), false);
|
|
|
|
assert.deepEqual(programme.redrawn.nexusTraitorExchanges, historical,
|
|
'A later public marker must not rewrite or reinterpret the earlier Nexus exchange history.');
|
|
assert.deepEqual({ event: first.event, card: first.card, replacement: first.replacement,
|
|
signature: first.signature }, f.history);
|
|
assertMixedE3MoritaniPhysical(programme.redrawn);
|
|
});
|