147 lines
7.8 KiB
TypeScript
147 lines
7.8 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { presenceAt } from '../game/force-presence';
|
|
import { nexusRicheseAllow, nexusRicheseFixture } from './fixture-nexus-richese';
|
|
|
|
function pair(g: Game, owner: string, revealedValue: 0 | 3 | 5 = 5): Action {
|
|
const tokens = g.players.find(player => player.id === owner)!.noField!.tokens;
|
|
const revealed = tokens.find(token => token.value === revealedValue)!;
|
|
const concealed = tokens.find(token => token.id !== revealed.id &&
|
|
token.id !== g.players.find(player => player.id === owner)!.noField!.lastShipped)!;
|
|
return {
|
|
type: 'ship', noField: concealed.id, revealedToken: revealed.id,
|
|
nexus: viewGame(g, owner).nexusRicheseCunning!.event,
|
|
event: g.players.find(player => player.id === owner)!.noFieldEvent,
|
|
territory: 'arrakeen', sector: 10, allyPayment: 0,
|
|
};
|
|
}
|
|
|
|
void test('Basic and Advanced Richese pair spends one card and price, materializes one public token and retains one concealed', () => {
|
|
for (const advanced of [false, true]) {
|
|
const { g: before, owner, target } = nexusRicheseFixture({ ownerFaction: 'richese', advanced });
|
|
const p = before.players.find(player => player.id === owner)!;
|
|
const original = JSON.stringify(before);
|
|
const request = pair(before, owner);
|
|
const concealed = String(request.noField);
|
|
const revealed = String(request.revealedToken);
|
|
const after = applyAction(before, owner, request);
|
|
const actual = after.players.find(player => player.id === owner)!;
|
|
assert.equal(JSON.stringify(before), original);
|
|
assert.equal(after.nexusCards!.cards!.hands[owner], null);
|
|
assert.equal(actual.spice, p.spice - 1);
|
|
assert.equal(actual.reserves, p.reserves - 5);
|
|
assert.equal(actual.forces['arrakeen:10'], 5);
|
|
assert.equal(actual.noField!.deployed!.tokenId, concealed);
|
|
assert.equal(actual.noField!.lastShipped, concealed);
|
|
assert.equal(presenceAt(actual, 'arrakeen'), 6);
|
|
assert.equal(after.nexusRicheseCunningLast?.stage, 'shipped');
|
|
assert.match(after.log.at(-1)!.text, /faceup 5 token placed 5 physical forces/);
|
|
const rival = viewGame(after, target);
|
|
assert.equal(rival.richeseNoField!.private, null);
|
|
assert.equal(rival.nexusRicheseCunning, null);
|
|
assert.equal(JSON.stringify(rival).includes(concealed), false);
|
|
assert.equal(JSON.stringify(rival).includes(revealed), false);
|
|
const restored = normalizeAutomaticGame(JSON.parse(JSON.stringify(after)));
|
|
assert.equal(restored.players.find(player => player.id === owner)!.reserves, p.reserves - 5);
|
|
assert.equal(viewGame(restored, target).richeseNoField!.public.deployed!.effectiveForces, 1);
|
|
assert.throws(() => applyAction(restored, owner, request), /ship once|unused shipment|stale/i);
|
|
}
|
|
});
|
|
|
|
void test('Karama cancels the two-token attempt without spending spice or tokens, while the Nexus stays spent', () => {
|
|
const { g: before, owner, target, karama } = nexusRicheseFixture({ ownerFaction: 'richese', karama: true });
|
|
const request = pair(before, owner);
|
|
const declared = applyAction(before, owner, request);
|
|
assert.equal(declared.response?.kind, 'richeseNoField');
|
|
assert.equal(declared.nexusCards!.cards!.hands[owner], null);
|
|
assert.equal(declared.players[0].noField!.deployed, null);
|
|
const canceled = applyAction(JSON.parse(JSON.stringify(declared)), target,
|
|
{ type: 'card', mode: 'cancel', card: karama! });
|
|
assert.equal(canceled.nexusRicheseCunningLast?.stage, 'stopped');
|
|
assert.equal(canceled.players[0].noField!.lastShipped, null);
|
|
assert.equal(canceled.players[0].reserves, before.players[0].reserves);
|
|
assert.equal(canceled.players[0].spice, before.players[0].spice);
|
|
assert.equal(canceled.players[0].shipped, false);
|
|
assert.equal(viewGame(canceled, owner).nexusRicheseCunning, null);
|
|
assert.throws(() => applyAction(canceled, owner, request), /unavailable|stale/i);
|
|
const ordinary = applyAction(canceled, owner,
|
|
{ type: 'ship', amount: 1, territory: 'arrakeen', sector: 10 });
|
|
assert.equal(ordinary.players[0].forces['arrakeen:10'], 1);
|
|
});
|
|
|
|
void test('Advanced Guild special prevention spends Cunning without moving a token or charging shipment price', () => {
|
|
const { g, owner, target, karama } = nexusRicheseFixture({
|
|
ownerFaction: 'richese', opponentFaction: 'guild', advanced: true, karama: true,
|
|
});
|
|
const request = pair(g, owner);
|
|
const pending = nexusRicheseAllow(applyAction(g, owner, request));
|
|
assert.equal(pending.decision?.kind, 'guildShipment');
|
|
assert.equal(pending.pendingShipment?.richesePair?.revealedTokenId, request.revealedToken);
|
|
const stopped = nexusRicheseAllow(applyAction(
|
|
JSON.parse(JSON.stringify(pending)), target,
|
|
{ type: 'card', mode: 'special', card: karama! },
|
|
));
|
|
assert.equal(stopped.nexusRicheseCunningLast?.stage, 'stopped');
|
|
assert.equal(stopped.players[0].spice, g.players[0].spice);
|
|
assert.equal(stopped.players[0].reserves, g.players[0].reserves);
|
|
assert.equal(stopped.players[0].noField!.deployed, null);
|
|
assert.equal(stopped.players[0].noField!.lastShipped, null);
|
|
assert.equal(stopped.nexusCards!.cards!.hands[owner], null);
|
|
assert.equal(stopped.players[0].shipped, true);
|
|
});
|
|
|
|
void test('restoration rejects an orphaned pair before it can lose its Karama or Guild continuation', () => {
|
|
const basic = nexusRicheseFixture({ ownerFaction: 'richese', karama: true });
|
|
const declared = applyAction(basic.g, basic.owner, pair(basic.g, basic.owner));
|
|
assert.equal(declared.response?.kind, 'richeseNoField');
|
|
const missingResponse = JSON.parse(JSON.stringify(declared)) as Game;
|
|
missingResponse.response = null;
|
|
assert.throws(() => normalizeAutomaticGame(missingResponse), /unique Karama, native Guild interception or funded invoice continuation/);
|
|
|
|
const advanced = nexusRicheseFixture({
|
|
ownerFaction: 'richese', opponentFaction: 'guild', advanced: true,
|
|
});
|
|
const pending = nexusRicheseAllow(applyAction(
|
|
advanced.g, advanced.owner, pair(advanced.g, advanced.owner),
|
|
));
|
|
assert.equal(pending.decision?.kind, 'guildShipment');
|
|
const missingDecision = JSON.parse(JSON.stringify(pending)) as Game;
|
|
missingDecision.decision = null;
|
|
assert.throws(() => viewGame(missingDecision, advanced.owner), /unique Karama, native Guild interception or funded invoice continuation/);
|
|
});
|
|
|
|
void test('pair rejects duplicate, prior-used and stale tokens without changing the original game', () => {
|
|
const { g, owner } = nexusRicheseFixture({ ownerFaction: 'richese' });
|
|
const request = pair(g, owner);
|
|
const original = JSON.stringify(g);
|
|
for (const altered of [
|
|
{ ...request, revealedToken: request.noField },
|
|
{ ...request, noField: 'foreign-token' },
|
|
{ ...request, nexus: 'stale' },
|
|
{ ...request, event: 'stale' },
|
|
{ ...request, extra: 'unbound' },
|
|
]) {
|
|
assert.throws(() => applyAction(g, owner, altered));
|
|
assert.equal(JSON.stringify(g), original);
|
|
}
|
|
const old = g.players[0].noField!;
|
|
old.lastShipped = old.tokens.find(token => token.value === 5)!.id;
|
|
assert.throws(() => applyAction(g, owner, request));
|
|
});
|
|
|
|
void test('all four profiles choose and complete a legal pair using their private own-token view', () => {
|
|
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const { g, owner } = nexusRicheseFixture({ ownerFaction: 'richese' });
|
|
const view = viewGame(g, owner);
|
|
view.players.find(player => player.id === owner)!.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
assert.equal(action?.type, 'ship');
|
|
assert.equal(action?.nexus, view.nexusRicheseCunning!.event, JSON.stringify(action));
|
|
assert.ok(action?.revealedToken && action.noField && action.revealedToken !== action.noField);
|
|
const after = applyAction(g, owner, action);
|
|
assert.equal(after.nexusRicheseCunningLast?.stage, 'shipped');
|
|
}
|
|
});
|