270 lines
10 KiB
TypeScript
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);
|
|
}
|
|
});
|