149 lines
4.2 KiB
TypeScript
149 lines
4.2 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
newPlayer,
|
|
viewGame,
|
|
type Action,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import { baseDeck } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
|
|
const card = 'richese-juice-of-sapho';
|
|
function fixture() {
|
|
const g = createGame(
|
|
'SAPHOREVIEW',
|
|
newPlayer('r', 'Richese', 'richese'),
|
|
false,
|
|
['choam'],
|
|
);
|
|
g.players.push(
|
|
newPlayer('a', 'Atreides', 'atreides'),
|
|
newPlayer('e', 'Emperor', 'emperor'),
|
|
);
|
|
Object.assign(g, {
|
|
status: 'playing',
|
|
phase: 5,
|
|
turn: 2,
|
|
storm: 18,
|
|
active: 'a',
|
|
order: ['a', 'r', 'e'],
|
|
movementRemaining: ['a', 'r', 'e'],
|
|
deck: baseDeck(),
|
|
richeseCache: richeseCards(),
|
|
});
|
|
for (const p of g.players) {
|
|
p.hand = [];
|
|
p.forces = {};
|
|
p.reserves = 20;
|
|
p.spice = 20;
|
|
}
|
|
const index = g.richeseCache!.findIndex((c) => c.id === card);
|
|
g.players[0].hand.push(g.richeseCache!.splice(index, 1)[0]);
|
|
return g;
|
|
}
|
|
function hold(g: Game, owner: string, effect: string) {
|
|
const index = g.deck.findIndex((c) => c.effect === effect);
|
|
assert.ok(index >= 0, effect);
|
|
const c = g.deck.splice(index, 1)[0];
|
|
g.players.find((p) => p.id === owner)!.hand.push(c);
|
|
return c.id;
|
|
}
|
|
function play(g: Game): Action {
|
|
return {
|
|
type: 'card',
|
|
card,
|
|
mode: 'last',
|
|
scope: 'movement',
|
|
event: `movement:${g.turn}`,
|
|
};
|
|
}
|
|
function blocked(g: Game) {
|
|
assert.deepEqual(viewGame(g, 'r').saphoOptions, []);
|
|
const before = JSON.stringify(g);
|
|
assert.throws(() => applyAction(g, 'r', play(g)));
|
|
assert.equal(JSON.stringify(g), before);
|
|
}
|
|
|
|
void test('an actual reserved Richese gift suppresses Sapho and cannot discard its pending physical card', () => {
|
|
let g = fixture();
|
|
g.players[0].ally = 'a';
|
|
g.players[1].ally = 'r';
|
|
hold(g, 'e', 'karama');
|
|
g = applyAction(g, 'r', { type: 'richeseGift', card });
|
|
assert.equal(g.response?.kind, 'richeseGift');
|
|
assert.equal(g.pendingRicheseGift!.intent.cardId, card);
|
|
blocked(g);
|
|
g = applyAction(JSON.parse(JSON.stringify(g)), 'e', { type: 'passResponse' });
|
|
assert.equal(g.pendingRicheseGift, null);
|
|
assert.ok(g.players[1].hand.some((c) => c.id === card));
|
|
assert.equal(
|
|
g.discard.some((c) => c.id === card),
|
|
false,
|
|
);
|
|
assert.deepEqual(viewGame(g, 'r').saphoOptions, []);
|
|
});
|
|
|
|
void test('actual Truthtrance priority and answer overlays block Sapho without altering future movement opportunity', () => {
|
|
let g = fixture();
|
|
const truth = hold(g, 'e', 'truthtrance');
|
|
g = applyAction(g, 'e', { type: 'card', card: truth });
|
|
blocked(g);
|
|
while (g.truthtrance?.stage === 'priority') {
|
|
const actor = g.players.find(
|
|
(p) => !g.truthtrance!.passed.includes(p.id),
|
|
)!.id;
|
|
g = applyAction(g, actor, { type: 'truthPass' });
|
|
}
|
|
blocked(g);
|
|
g = applyAction(g, 'e', {
|
|
type: 'truthAsk',
|
|
question: {
|
|
kind: 'fact',
|
|
target: 'r',
|
|
fact: { kind: 'spice', compare: 'gte', value: 0 },
|
|
},
|
|
});
|
|
blocked(g);
|
|
g = applyAction(g, 'r', { type: 'truthAnswer', answer: 'yes' });
|
|
assert.ok(viewGame(g, 'r').saphoOptions.some((o) => o.mode === 'last'));
|
|
g = applyAction(g, 'r', play(g));
|
|
assert.deepEqual(g.movementRemaining, ['a', 'e', 'r']);
|
|
});
|
|
|
|
void test('a real Karama response to shipment retains the suspended declaration when Sapho is rejected', () => {
|
|
let g = fixture();
|
|
g.advanced = true;
|
|
g.players[2].faction = 'guild';
|
|
hold(g, 'e', 'karama');
|
|
g = applyAction(g, 'a', {
|
|
type: 'ship',
|
|
territory: 'polar_sink',
|
|
sector: 0,
|
|
amount: 1,
|
|
});
|
|
assert.ok(g.decision || g.response || g.pendingShipment);
|
|
blocked(g);
|
|
});
|
|
|
|
void test('unsupported scopes and malformed requests are rejected atomically despite a valid physical holder', () => {
|
|
const g = fixture();
|
|
for (const extra of [
|
|
{ scope: 'normalAuction' },
|
|
{ scope: 'silent' },
|
|
{ scope: 'battle' },
|
|
{ mode: 'aggressor' },
|
|
{ mode: 'next' },
|
|
{ event: 'movement:1' },
|
|
{ event: undefined },
|
|
{ scope: undefined },
|
|
{ mode: undefined },
|
|
]) {
|
|
const before = JSON.stringify(g);
|
|
assert.throws(() => applyAction(g, 'r', { ...play(g), ...extra }));
|
|
assert.equal(JSON.stringify(g), before);
|
|
}
|
|
});
|