194 lines
14 KiB
TypeScript
194 lines
14 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { isAdvisor } from '../game/advisors';
|
|
import {
|
|
createTruthtranceAdvancedShipmentPreparationFixture as fixture,
|
|
askTruthtranceAdvancedShipmentPreparation as ask, actTruthtranceAdvancedShipmentPreparation as act,
|
|
advanceTruthtranceAdvancedShipmentPreparation as advance, truthtranceAdvancedShipmentPreparationClean as clean,
|
|
truthtranceAdvancedShipmentPreparationPlayer as player, truthtranceAdvancedShipmentPreparationInventory as inventory,
|
|
type TruthtranceAdvancedShipmentPreparationFixture,
|
|
} from './fixture-truthtrance-advanced-shipment-preparation';
|
|
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before);
|
|
}
|
|
function nativeSource(f: TruthtranceAdvancedShipmentPreparationFixture) {
|
|
assert.equal(f.initial.advanced, false); assert.notEqual(f.initial.status, 'playing');
|
|
assert.deepEqual(f.rules.steps, [{ actor: f.asker, action: { type: 'rules', advanced: true } }]);
|
|
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, true); assert.equal(f.setup.advancedPreview, true);
|
|
assert.deepEqual(f.setup.expansions, []); assert.ok(f.beforeStartingDeal.players.every(p => p.hand.length === 0));
|
|
assert.deepEqual(f.afterSetup.players.flatMap(p => p.hand.map(card => card.id)), f.deal);
|
|
assert.equal(player(f.afterSetup, f.asker).hand[0].id, f.truthtrance);
|
|
assert.ok(player(f.afterSetup, f.emperor).hand.some(card => card.id === f.karama && card.effect === 'karama'));
|
|
assert.deepEqual(f.firstStorm.steps.filter(step => step.action.type === 'stormDial').map(step => step.action.amount), [11, 0]);
|
|
assert.equal(f.firstStorm.after.storm, 12); assert.equal(f.game.advanced, true);
|
|
assert.equal(f.game.truthtrance ?? null, null); assert.equal(f.game.shipmentPromises?.length ?? 0, 0);
|
|
if (f.kind === 'bgCharity') {
|
|
assert.ok(f.advisorSetup); assert.deepEqual(f.advisorSetup.steps[0].action, { type: 'advisorSetup', territory: 'arrakeen', sector: 10 });
|
|
assert.equal(f.game.turn, 1); assert.equal(f.game.phase, 2); assert.equal(f.counterHolder, f.emperor);
|
|
assert.equal(player(f.game, f.target).spice, 5); assert.equal(player(f.game, f.target).reserves, 19);
|
|
assert.equal(player(f.game, f.target).forces['arrakeen:10'], 1); assert.equal(isAdvisor(player(f.game, f.target), 'arrakeen'), true);
|
|
assert.equal(player(f.game, f.asker).forces['arrakeen:10'], 10); assert.equal(f.targetKarama, null);
|
|
assert.equal(player(f.game, f.target).hand.some(card => card.effect === 'karama'), false);
|
|
} else {
|
|
assert.ok(f.purchase && f.beforeBid && f.shipment && f.beforeStormMovement && f.stormLoss && f.guild);
|
|
assert.deepEqual(f.purchase.steps[0], { actor: f.asker, action: { type: 'bid', amount: 4 } });
|
|
assert.equal(player(f.purchase.before, f.emperor).spice, 10); assert.equal(player(f.purchase.after, f.emperor).spice, 14);
|
|
assert.equal(player(f.purchase.after, f.asker).spice, 6);
|
|
assert.ok(player(f.purchase.after, f.asker).hand.some(card => card.id === f.auctionCard));
|
|
assert.deepEqual(f.shipment.steps[0], { actor: f.target,
|
|
action: { type: 'ship', territory: 'hagga_basin', sector: 13, amount: 2, elite: 2, allyPayment: 0 } });
|
|
assert.equal(player(f.shipment.before, f.target).spice - player(f.shipment.after, f.target).spice, 4);
|
|
assert.equal(player(f.shipment.after, f.target).elites?.forces['hagga_basin:13'], 2);
|
|
assert.equal(f.beforeStormMovement.storm, 12); assert.equal(f.beforeStormMovement.stormPending, 1);
|
|
assert.equal(f.stormLoss.after.storm, 13); assert.equal(player(f.stormLoss.after, f.target).elites?.tanks, 2);
|
|
assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 4);
|
|
const p = player(f.game, f.target); assert.equal(p.spice, 10); assert.equal(p.reserves, 18); assert.equal(p.tanks, 2);
|
|
assert.equal(p.elites?.reserves, 3); assert.equal(p.elites?.tanks, 2); assert.equal(p.elites?.revived, 0);
|
|
assert.equal(p.revived, 0); assert.equal(p.freeForcesRevived, 0); assert.equal(f.targetKarama, f.karama);
|
|
}
|
|
assert.equal(f.originalInventory.length, 33); assert.deepEqual(inventory(f.game), f.originalInventory);
|
|
assert.equal(f.sourceActionCount, f.actions.length);
|
|
}
|
|
function bound(f: TruthtranceAdvancedShipmentPreparationFixture, game = structuredClone(f.game)) {
|
|
game = ask(game, f);
|
|
assert.ok(viewGame(game, f.asker).truthShipmentTargets.includes(f.target));
|
|
assert.deepEqual(viewGame(game, f.target).truthShipmentAnswers, ['yes', 'no']);
|
|
game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
assert.equal(game.truthtrance ?? null, null); assert.equal(game.shipmentPromises?.length, 1);
|
|
assert.equal(game.shipmentPromises![0].answer, true); assert.equal(game.shipmentPromises![0].released, undefined);
|
|
return game;
|
|
}
|
|
function prepare(game: Game, f: TruthtranceAdvancedShipmentPreparationFixture, action?: Action) {
|
|
const guide = viewGame(game, f.target).shipmentPreparation; assert.ok(guide?.actions.length);
|
|
assert.equal(guide.actions[0].type, f.kind === 'bgCharity' ? 'charity' : 'revive');
|
|
reject(game, f.target, { type: 'ready' });
|
|
if (f.kind === 'elite') {
|
|
assert.equal(guide.actions[0].amount, 1);
|
|
reject(game, f.target, { type: 'revive', amount: 2, elite: 2, freeElite: 1 });
|
|
}
|
|
game = act(game, f.target, action ?? guide.actions[0]);
|
|
if (f.kind === 'bgCharity') {
|
|
assert.equal(game.response?.kind, 'bgCharity'); assert.equal(player(game, f.target).spice, 5);
|
|
}
|
|
game = advance(game, clean);
|
|
assert.equal(viewGame(game, f.target).shipmentPreparation, null);
|
|
assert.equal(game.shipmentPromises![0].released, undefined);
|
|
if (f.kind === 'bgCharity') assert.equal(player(game, f.target).spice, 7);
|
|
else {
|
|
const p = player(game, f.target); assert.equal(p.spice, 10); assert.equal(p.reserves, 19); assert.equal(p.tanks, 1);
|
|
assert.equal(p.elites?.reserves, 4); assert.equal(p.elites?.tanks, 1); assert.equal(p.elites?.revived, 1);
|
|
assert.equal(p.revived, 1); assert.equal(p.freeForcesRevived, 1);
|
|
reject(game, f.target, { type: 'revive', amount: 1, elite: 1, freeElite: 0 });
|
|
}
|
|
return game;
|
|
}
|
|
function ship(game: Game, f: TruthtranceAdvancedShipmentPreparationFixture) {
|
|
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
|
|
assert.equal(game.shipmentPromises![0].released, undefined); assert.equal(game.shipmentPromises![0].fulfilled, undefined);
|
|
const before = structuredClone(game), guildBefore = f.guild ? player(game, f.guild).spice : null;
|
|
if (f.kind === 'elite') {
|
|
const rate = viewGame(game, f.target).shipmentCompletion?.actions[0]; assert.ok(rate);
|
|
assert.equal(rate.type, 'card'); assert.equal(rate.card, f.karama); assert.equal(rate.mode, 'shipment');
|
|
assert.ok(f.rateAction); game = act(game, f.target, f.rateAction);
|
|
assert.equal(player(game, f.target).spice, 10); assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
|
}
|
|
const completion = viewGame(game, f.target).shipmentCompletion; assert.ok(completion);
|
|
const shipment = completion.actions.find(action => action.type === 'ship'); assert.ok(shipment);
|
|
assert.equal(shipment.amount, f.minimum); assert.equal(shipment.territory, f.territory);
|
|
if (f.kind === 'elite') assert.equal(shipment.elite, 4);
|
|
reject(game, f.target, { ...f.shipmentAction, amount: f.minimum - 1 });
|
|
game = act(game, f.target, f.shipmentAction); game = advance(game, clean);
|
|
const p = player(game, f.target); assert.equal(p.spice, 0); assert.equal(p.forces[f.location], f.minimum);
|
|
assert.equal(player(before, f.target).spice - p.spice, f.kind === 'elite' ? 10 : 7);
|
|
assert.equal(p.shipped, true); assert.equal(p.moved, 0); assert.equal(game.shipmentPromises![0].fulfilled, true);
|
|
assert.equal(game.shipmentPromises![0].released, undefined);
|
|
assert.equal(p.reserves, player(before, f.target).reserves - f.minimum);
|
|
if (f.kind === 'elite') {
|
|
assert.equal(p.elites?.forces[f.location], 4); assert.equal(p.elites?.reserves, 0); assert.equal(p.elites?.tanks, 1);
|
|
assert.equal(player(game, f.guild!).spice, guildBefore, 'The real purchased rate sends its invoice to the bank');
|
|
} else {
|
|
assert.equal(p.forces['arrakeen:10'], 1); assert.equal(isAdvisor(p, 'arrakeen'), true); assert.equal(isAdvisor(p, 'carthag'), false);
|
|
assert.equal(p.reserves, 12);
|
|
}
|
|
assert.deepEqual(inventory(game), f.originalInventory); reject(game, f.target, f.shipmentAction); return game;
|
|
}
|
|
|
|
void test('native Advanced wealthy BG Charity prepares and pays the real seven-counter city commitment', () => {
|
|
const f = fixture({ kind: 'bgCharity' }); nativeSource(f); ship(prepare(bound(f), f), f);
|
|
});
|
|
|
|
void test('native Advanced original Emperor Karama prevents wealthy BG Charity and releases the otherwise impossible shipment once', () => {
|
|
const f = fixture({ kind: 'bgCharity' }); nativeSource(f);
|
|
let game = bound(f); reject(game, f.target, { type: 'ready' });
|
|
game = act(game, f.target, f.charityAction); assert.equal(game.response?.kind, 'bgCharity');
|
|
assert.equal(player(game, f.target).spice, 5); assert.ok(f.counterHolder);
|
|
game = act(game, f.counterHolder, { type: 'card', card: f.karama, mode: 'cancel' }); game = advance(game, clean);
|
|
assert.equal(player(game, f.target).spice, 5); assert.equal(game.shipmentPromises![0].released, true);
|
|
assert.equal(game.shipmentPromises![0].fulfilled, undefined); assert.equal(viewGame(game, f.target).shipmentPreparation, null);
|
|
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
|
|
const released = structuredClone(game.shipmentPromises); game = act(game, f.target, { type: 'ready' });
|
|
game = advance(game, state => state.phase === 5 && state.active === f.target && clean(state));
|
|
reject(game, f.target, f.shipmentAction);
|
|
game = act(game, f.target, { ...f.shipmentAction, amount: 5 }); game = advance(game, clean);
|
|
assert.equal(player(game, f.target).forces[f.location], 5); assert.equal(player(game, f.target).spice, 0);
|
|
assert.deepEqual(game.shipmentPromises, released); assert.deepEqual(inventory(game), f.originalInventory);
|
|
});
|
|
|
|
void test('native Advanced wealthy BG source cannot promise eight without an owned funding source', () => {
|
|
const f = fixture({ kind: 'bgCharity' }), game = ask(structuredClone(f.game), f, 8);
|
|
assert.deepEqual(viewGame(game, f.target).truthShipmentAnswers, ['no']);
|
|
reject(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
assert.doesNotThrow(() => act(game, f.target, { type: 'truthAnswer', answer: 'no' }));
|
|
});
|
|
|
|
void test('native Advanced elite-only Storm losses prepare exactly one Sardaukar then original Karama pays nineteen city counters', () => {
|
|
const f = fixture({ kind: 'elite' }); nativeSource(f); ship(prepare(bound(f), f), f);
|
|
});
|
|
|
|
void test('native Advanced elite-only source cannot promise twenty or revive its second Sardaukar this turn', () => {
|
|
const f = fixture({ kind: 'elite' }); nativeSource(f);
|
|
const game = ask(structuredClone(f.game), f, 20); assert.deepEqual(viewGame(game, f.target).truthShipmentAnswers, ['no']);
|
|
reject(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
reject(f.game, f.target, { type: 'revive', amount: 2, elite: 2, freeElite: 1 });
|
|
const prepared = prepare(bound(f), f); assert.equal(player(prepared, f.target).elites?.revived, 1);
|
|
reject(prepared, f.target, { type: 'revive', amount: 1, elite: 1 });
|
|
});
|
|
|
|
void test('a genuine earlier Advanced Ready is reopened by either required owned preparation answer', () => {
|
|
for (const kind of ['bgCharity', 'elite'] as const) {
|
|
const f = fixture({ kind }); let game = act(structuredClone(f.game), f.target, { type: 'ready' });
|
|
assert.equal(game.ready.includes(f.target), true); game = bound(f, game);
|
|
assert.equal(game.ready.includes(f.target), false); assert.ok(viewGame(game, f.target).shipmentPreparation?.actions.length);
|
|
prepare(game, f);
|
|
}
|
|
});
|
|
|
|
void test('native Advanced pending shipment questions reject foreign answers and premature shipment or phase skipping', () => {
|
|
for (const kind of ['bgCharity', 'elite'] as const) {
|
|
const f = fixture({ kind }), game = ask(structuredClone(f.game), f);
|
|
reject(game, f.asker, { type: 'truthAnswer', answer: 'yes' }); reject(game, f.target, f.shipmentAction);
|
|
reject(game, f.target, { type: 'ready' }); reject(game, f.asker, f.askStep.action);
|
|
const done = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
reject(done, f.target, { type: 'truthAnswer', answer: 'yes' }); assert.deepEqual(inventory(done), f.originalInventory);
|
|
}
|
|
});
|
|
|
|
void test('all four minimal profiles choose legal original Advanced answers and execute the required owned preparation', () => {
|
|
for (const kind of ['bgCharity', 'elite'] as const) {
|
|
const f = fixture({ kind }), asked = ask(structuredClone(f.game), f), promised = bound(f);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const answering = viewGame(asked, f.target); answering.players.find(p => p.id === f.target)!.bot = difficulty;
|
|
const answer = botActions(answering)[0]; assert.ok(answer); assert.equal(answer.type, 'truthAnswer');
|
|
assert.ok(answer.answer === 'yes' || answer.answer === 'no'); assert.doesNotThrow(() => act(asked, f.target, answer));
|
|
const own = viewGame(promised, f.target); own.players.find(p => p.id === f.target)!.bot = difficulty;
|
|
const proposed = botActions(own)[0]; assert.ok(proposed);
|
|
assert.equal(proposed.type, kind === 'bgCharity' ? 'charity' : 'revive');
|
|
ship(prepare(structuredClone(promised), f, proposed), f);
|
|
}
|
|
}
|
|
});
|