Preserve permanent Traitor markers through legal replacement recycling
This commit is contained in:
1 parent
1cd7db7d0b
commit
e265af3cb3
29 files changed
+1273
-68
No files matched your search
@@ -0,0 +1,118 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user