Files
Dune/tests/nexus-choam-engine.test.ts
T

270 lines
10 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Game, type Action } from '../game/engine';
import {
NEXUS_CHOAM_EFFECTS,
nexusChoamFixture,
holdNexusChoamCard,
nexusChoamRequest,
nexusChoamAllow,
nexusChoamInventory,
nexusChoamReload,
} from './fixture-nexus-choam';
function reject(g: Game, owner: string, action: Action) {
const before = JSON.stringify(g);
assert.throws(() => applyAction(g, owner, action));
assert.equal(JSON.stringify(g), before);
}
for (const effect of NEXUS_CHOAM_EFFECTS) {
void test(`CHOAM Nexus ${effect} spends Nexus before the response and applies its real effect once`, () => {
const f = nexusChoamFixture(effect),
before = structuredClone(f.g);
const offer = viewGame(f.g, f.owner).choamWorthless!.plays.find(
(play) =>
play.source === 'nexus' &&
play.effect === effect &&
play.card.id === f.cost.id,
)!;
assert.ok(offer && offer.source === 'nexus');
assert.equal(offer.blocked, null);
assert.equal(offer.event, nexusChoamRequest(f).nexus);
let g = applyAction(f.g, f.owner, nexusChoamRequest(f));
assert.equal(g.response?.kind, 'choamWorthless');
assert.equal(g.nexusCards!.cards!.hands[f.owner], null);
assert.equal(
g.nexusCards!.cards!.discard.filter((card) => card === 'choam').length,
1,
);
assert.deepEqual(
g.players[0].hand.find((card) => card.id === f.cost.id),
f.cost,
);
assert.deepEqual(
g.players.map((p) => p.forces),
before.players.map((p) => p.forces),
);
g = nexusChoamAllow(nexusChoamReload(g));
assert.equal(g.nexusChoamLast?.stage, 'complete');
assert.deepEqual(
g.discard.find((card) => card.id === f.cost.id),
f.cost,
);
assert.equal(g.discard.filter((card) => card.id === f.cost.id).length, 1);
assert.equal(
g.players[0].hand.some((card) => card.id === f.cost.id),
false,
);
if (effect === 'kulon')
assert.deepEqual(g.choamMovement, { turn: g.turn, bonus: 1 });
if (effect === 'laLaLa') {
assert.equal(g.players[1].tanks, 4);
assert.equal(g.players[1].spice, 20);
assert.equal(g.pendingRevival, null);
}
if (effect === 'gamont') {
assert.equal(g.players[1].forces['arrakeen:10'], 1);
assert.equal(g.players[1].reserves, 19);
assert.equal(g.decision?.kind, 'choamMentat');
}
if (effect === 'baliset') {
assert.deepEqual(g.players[1].forces, before.players[1].forces);
assert.equal(g.players[1].moved, 0);
assert.equal(g.pendingChoamMove, null);
}
if (effect === 'jubba') {
assert.equal(g.players[0].forces['red_chasm:7'], 4);
assert.equal(g.players[1].tanks, 3);
assert.equal(g.spice['red_chasm:7'], undefined);
}
reject(g, f.owner, nexusChoamRequest(f));
nexusChoamInventory(g);
for (const p of g.players)
assert.deepEqual(viewGame(nexusChoamReload(g), p.id), viewGame(g, p.id));
});
void test(`CHOAM Nexus ${effect} Karama retains the actual cost and resumes the original public context`, () => {
const f = nexusChoamFixture(effect);
let g = applyAction(f.g, f.owner, nexusChoamRequest(f));
g = applyAction(nexusChoamReload(g), f.target, {
type: 'card',
mode: 'cancel',
card: f.karama,
});
assert.equal(g.nexusChoamLast?.stage, 'canceled');
assert.deepEqual(
g.players[0].hand.find((card) => card.id === f.cost.id),
f.cost,
);
assert.equal(
g.discard.some((card) => card.id === f.cost.id),
false,
);
assert.equal(g.discard.filter((card) => card.id === f.karama).length, 1);
assert.equal(g.nexusCards!.cards!.hands[f.owner], null);
if (effect === 'kulon') assert.equal(g.choamMovement, undefined);
if (effect === 'laLaLa') {
assert.equal(g.players[1].reserves, 19);
assert.equal(g.players[1].tanks, 1);
assert.equal(g.players[1].spice, 16);
}
if (effect === 'gamont') {
assert.equal(g.players[1].forces['arrakeen:10'], 2);
assert.equal(g.decision?.kind, 'choamMentat');
}
if (effect === 'baliset') {
assert.equal(g.players[1].forces['red_chasm:7'], 1);
assert.equal(g.players[1].moved, 1);
}
if (effect === 'jubba') {
assert.equal(g.decision?.kind, 'choamStorm');
g = applyAction(g, f.owner, { type: 'decision', decline: true });
assert.equal(g.players[0].tanks, 4);
assert.equal(g.players[1].tanks, 3);
}
reject(g, f.owner, nexusChoamRequest(f));
nexusChoamInventory(g);
});
}
void test('CHOAM Nexus rejects stale events, wrong owner/effect, unsupported Kull and malformed costs immutably', () => {
const f = nexusChoamFixture('kulon'),
action = nexusChoamRequest(f);
for (const bad of [
{ ...action, nexus: 'stale' },
{ ...action, effect: 'kull' },
{ ...action, card: 'unknown' },
{ ...action, effect: 'gamont' },
])
reject(f.g, f.owner, bad);
reject(f.g, f.target, action);
const unsupported = viewGame(f.g, f.owner).choamWorthless!.plays.find(
(play) => play.effect === 'kull',
);
assert.ok(unsupported?.blocked);
});
void test('CHOAM Nexus hidden physical cost kinds have identical opponent views until actual discard', () => {
const f = nexusChoamFixture('kulon'),
other = structuredClone(f.g);
const index = other.deck.findIndex((card) => card.kind === 'poison');
assert.ok(index >= 0);
const cost = other.deck[index];
other.deck[index] = other.players[0].hand[0];
other.players[0].hand[0] = cost;
const alternate = { ...f, g: other, cost };
for (const id of [f.target, f.observer])
assert.deepEqual(viewGame(f.g, id), viewGame(other, id));
const a = applyAction(f.g, f.owner, nexusChoamRequest(f)),
b = applyAction(other, f.owner, nexusChoamRequest(alternate));
for (const id of [f.target, f.observer])
assert.deepEqual(viewGame(a, id), viewGame(b, id));
for (const g of [a, b])
for (const id of [f.target, f.observer]) {
assert.equal(Object.hasOwn(viewGame(g, id), 'nexusChoamHistory'), false);
assert.equal(viewGame(g, id).choamWorthless, null);
}
});
void test('CHOAM Nexus can discard a real Ghola as payment without activating its printed revival effect in Basic or Advanced', () => {
for (const advanced of [false, true]) {
const f = nexusChoamFixture('kulon', { advanced, costKind: 'ghola' });
const before = f.g.players.map((p) => ({
reserves: p.reserves,
tanks: p.tanks,
revived: p.revived,
}));
const g = nexusChoamAllow(applyAction(f.g, f.owner, nexusChoamRequest(f)));
assert.deepEqual(
g.players.map((p) => ({
reserves: p.reserves,
tanks: p.tanks,
revived: p.revived,
})),
before,
);
assert.equal(g.discard.find((c) => c.id === f.cost.id)?.effect, 'ghola');
assert.equal(g.choamMovement?.bonus, 1);
nexusChoamInventory(g);
}
});
void test('CHOAM Nexus Baliset survives a real paid Nullentropy search and restores its original movement response before allow or cancellation', () => {
for (const cancel of [false, true]) {
const f = nexusChoamFixture('baliset', { opponentFaction: 'richese' });
let g = f.g;
const index = g.richeseCache!.findIndex(
(card) => card.effect === 'nullentropyBox',
);
assert.ok(index >= 0);
const box = g.richeseCache!.splice(index, 1)[0];
g.players[1].hand.push(box);
for (const kind of ['shield', 'projectile']) {
const i = g.deck.findIndex((card) => card.kind === kind);
assert.ok(i >= 0);
g.discard.push(g.deck.splice(i, 1)[0]);
}
g = applyAction(g, f.owner, nexusChoamRequest(f));
const response = structuredClone(g.response),
move = structuredClone(g.pendingChoamMove);
g = applyAction(g, f.target, { type: 'card', card: box.id });
assert.equal(g.decision?.kind, 'nullentropy');
assert.deepEqual(g.pendingNullentropy!.resume.response, response);
assert.deepEqual(g.pendingChoamMove, move);
assert.equal(g.players[1].spice, 18);
nexusChoamInventory(g);
for (const p of g.players)
assert.deepEqual(viewGame(nexusChoamReload(g), p.id), viewGame(g, p.id));
const selected = g.discard[0].id;
g = applyAction(nexusChoamReload(g), f.target, {
type: 'decision',
event: g.pendingNullentropy!.event,
card: selected,
});
assert.deepEqual(g.response, response);
g = cancel
? applyAction(g, f.target, {
type: 'card',
mode: 'cancel',
card: f.karama,
})
: nexusChoamAllow(g);
assert.equal(g.nexusChoamLast?.stage, cancel ? 'canceled' : 'complete');
assert.equal(g.players[1].forces['red_chasm:7'], cancel ? 1 : 3);
assert.equal(g.players[1].spice, 18);
assert.equal(g.discard.filter((card) => card.id === box.id).length, 1);
assert.ok(g.players[1].hand.some((card) => card.id === selected));
nexusChoamInventory(g);
}
});
void test('CHOAM Nexus legitimately fizzles after native Karama cash-in spends its payment, resuming movement or storm once', () => {
for (const effect of ['baliset', 'jubba'] as const) {
const f = nexusChoamFixture(effect);
const karama = holdNexusChoamCard(f.g, f.owner, 'karama');
let g = applyAction(f.g, f.owner, nexusChoamRequest(f));
g = applyAction(g, f.owner, {
type: 'card',
mode: 'special',
card: karama.id,
cards: [f.cost.id],
});
g = nexusChoamAllow(nexusChoamReload(g));
assert.equal(g.nexusChoamLast?.stage, 'fizzled');
assert.equal(
g.nexusCards!.cards!.discard.filter((card) => card === 'choam').length,
1,
);
assert.equal(g.discard.filter((card) => card.id === f.cost.id).length, 1);
assert.equal(g.discard.filter((card) => card.id === karama.id).length, 1);
assert.equal(g.players[0].spice, 23);
if (effect === 'baliset') {
assert.equal(g.players[1].forces['red_chasm:7'], 1);
assert.equal(g.players[1].moved, 1);
} else {
assert.equal(g.decision?.kind, 'choamStorm');
g = applyAction(g, f.owner, { type: 'decision', decline: true });
assert.equal(g.players[0].tanks, 4);
assert.equal(g.players[1].tanks, 3);
}
reject(g, f.owner, nexusChoamRequest(f));
nexusChoamInventory(g);
}
});