100 lines
5.9 KiB
TypeScript
100 lines
5.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { actTruthMovement as act, truthMovementClean as clean, truthMovementInventory as inventory,
|
|
truthMovementPlayer as player } from './fixture-truth-movement';
|
|
import { nextStrongholdFactionsNativeStep as next } from './fixture-stronghold-factions';
|
|
|
|
const proposer = 'atreides', traveler = 'emperor', first = 'harkonnen', outsider = 'fremen';
|
|
const shipment: Action = { type:'ship', territory:'polar_sink', sector:0, amount:1, elite:0, allyPayment:0 };
|
|
const travel = { actor:traveler, kind:'ship', territory:'polar_sink', minimum:1, answer:true } as const;
|
|
function reach(game: Game, until: (game: Game) => boolean): Game {
|
|
for (let i = 0; i < 1200; i++) {
|
|
if (until(game)) return game;
|
|
assert.notEqual(game.status, 'finished');
|
|
const step = next(game); assert.ok(step);
|
|
game = act(game, step.actor, step.action);
|
|
}
|
|
throw Error('The native shipment opportunity was not reached.');
|
|
}
|
|
function nativeSource(): Game {
|
|
let game = createGame('GROUPPAIDTRAVEL', newPlayer(proposer, 'Atreides', 'atreides'), false, []);
|
|
for (const [id, faction] of [[traveler, 'emperor'], [first, 'harkonnen'], [outsider, 'fremen']] as const)
|
|
joinGame(game, newPlayer(id, id, faction));
|
|
for (const seat of game.players) game = act(game, seat.id, { type:'ready' });
|
|
game = act(game, proposer, { type:'start' });
|
|
const cards = game.deck.filter(card => ['projectile', 'poison', 'shield', 'snooper'].includes(card.kind)).slice(0, 4);
|
|
assert.equal(cards.length, 4);
|
|
for (const [position, card] of cards.entries()) {
|
|
const from = game.deck.findIndex(row => row.id === card.id); assert.ok(from >= position);
|
|
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
|
|
}
|
|
game = reach(game, state => state.status === 'playing');
|
|
const land = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(land >= 0);
|
|
game.spiceDeck.unshift(game.spiceDeck.splice(land, 1)[0]);
|
|
game = reach(game, state => state.turn === 1 && state.phase === 5 && state.active === traveler && clean(state));
|
|
assert.equal(player(game, traveler).shipped, false);
|
|
assert.ok(player(game, traveler).spice >= 2);
|
|
return game;
|
|
}
|
|
let source: Game | undefined;
|
|
const fixture = () => structuredClone(source ??= nativeSource());
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => act(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function offer(game: Game, payer: string, recipient: string, amount: number) {
|
|
const result = act(game, proposer, { type:'dealOffer', targets:[first, traveler], terms:'Travel and payment.',
|
|
payment:{ kind:'immediateBribe', payer, recipient, amount }, travel });
|
|
const deal = result.deals?.at(-1); assert.ok(deal);
|
|
return { game:result, id:deal.id };
|
|
}
|
|
const accept = (game: Game, actor: string, id: string) => act(game, actor, { type:'dealRespond', deal:id, accept:true });
|
|
|
|
for (const payer of [traveler, first]) void test(`group paid travel pays ${payer} only after the traveler gives final consent`, () => {
|
|
const initial = fixture(), cash = player(initial, payer).spice, escrow = player(initial, proposer).bribes;
|
|
assert.ok(cash >= 1);
|
|
const cards = inventory(initial), offered = offer(initial, payer, proposer, 1);
|
|
let game = offered.game;
|
|
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
|
|
assert.equal(player(game, payer).spice, cash);
|
|
game = accept(game, first, offered.id);
|
|
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
|
assert.equal(player(game, payer).spice, cash);
|
|
assert.equal(player(game, proposer).bribes, escrow);
|
|
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
|
game = accept(game, traveler, offered.id);
|
|
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'active');
|
|
assert.equal(player(game, payer).spice, cash - 1);
|
|
assert.equal(player(game, proposer).bribes, escrow + 1);
|
|
assert.equal(viewGame(game, traveler).dealTravelStatus[offered.id], 'pending');
|
|
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
|
|
game = reach(act(game, traveler, shipment), state => clean(state) && !state.pendingShipment);
|
|
assert.equal(viewGame(game, traveler).dealTravelStatus[offered.id], 'fulfilled');
|
|
assert.deepEqual(inventory(game), cards);
|
|
});
|
|
|
|
void test('traveler payment that consumes shipment funds rolls back only final consent', () => {
|
|
const initial = fixture(), cash = player(initial, traveler).spice, escrow = player(initial, proposer).bribes;
|
|
const offered = offer(initial, traveler, proposer, cash);
|
|
const waiting = accept(offered.game, first, offered.id);
|
|
reject(waiting, traveler, { type:'dealRespond', deal:offered.id, accept:true });
|
|
assert.equal(waiting.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
|
assert.equal(player(waiting, traveler).spice, cash);
|
|
assert.equal(player(waiting, proposer).bribes, escrow);
|
|
assert.equal(waiting.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
|
});
|
|
|
|
void test('a prior consenting payer with insufficient funds cannot make the traveler partly activate the group deal', () => {
|
|
const initial = fixture(), cash = player(initial, first).spice; assert.ok(cash >= 1);
|
|
const offered = offer(initial, first, proposer, cash);
|
|
let game = accept(offered.game, first, offered.id);
|
|
game = act(game, first, { type:'bribe', target:proposer, amount:1 });
|
|
const before = structuredClone(game);
|
|
reject(game, traveler, { type:'dealRespond', deal:offered.id, accept:true });
|
|
assert.deepEqual(game, before);
|
|
assert.equal(game.deals!.find(deal => deal.id === offered.id)!.status, 'offered');
|
|
assert.equal(game.travelPromises?.some(row => row.sourceDeal === offered.id) ?? false, false);
|
|
});
|