159 lines
11 KiB
TypeScript
159 lines
11 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 {
|
|
createTruthtranceEarlyAdvancedShipmentFixture as fixture, actTruthtranceEarlyAdvancedShipment as act,
|
|
advanceTruthtranceEarlyAdvancedShipment as advance, finishTruthtranceEarlyAdvancedShipment as finish,
|
|
truthtranceEarlyAdvancedShipmentClean as clean, truthtranceEarlyAdvancedShipmentPlayer as player,
|
|
truthtranceEarlyAdvancedShipmentInventory as inventory, truthtranceEarlyAdvancedShipmentPhysical as physical,
|
|
type TruthtranceEarlyAdvancedShipmentFixture,
|
|
} from './fixture-truthtrance-early-advanced-shipment';
|
|
|
|
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: TruthtranceEarlyAdvancedShipmentFixture) {
|
|
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.advanced, true); assert.deepEqual(f.setup.expansions, []);
|
|
assert.ok(!f.setup.homeworlds && !f.setup.nexusCards && !f.setup.leaderSkills && !f.setup.techTokens &&
|
|
!f.setup.strongholdCards && !f.setup.discoveryEnabled && !f.setup.ecazTreachery);
|
|
assert.ok(f.actions.some(step => step.actor === f.lobby.host && step.action.type === 'start' && step.action.advancedPreview === true));
|
|
assert.ok(f.beforeStartingDeal.players.every(player => player.hand.length === 0));
|
|
assert.deepEqual(f.afterSetup.players.flatMap(player => player.hand.map(card => card.id)), f.deal);
|
|
assert.ok(player(f.afterSetup, f.asker).hand.some(card => card.id === f.card && card.effect === 'truthtrance'));
|
|
assert.ok(player(f.afterSetup, f.counterHolder).hand.some(card => card.id === f.karama && card.effect === 'karama'));
|
|
assert.ok(player(f.afterSetup, f.target).hand.some(card => card.id === f.baliset && card.name === 'Baliset'));
|
|
assert.equal(player(f.game, f.target).spice, 10); assert.equal(player(f.game, f.target).reserves, 20);
|
|
assert.deepEqual(player(f.game, f.target).forces, {}); assert.equal(player(f.game, f.target).tanks, 0);
|
|
assert.equal(player(f.game, f.target).shipped, false); assert.equal(player(f.game, f.target).moved, 0);
|
|
assert.equal(f.game.turn, 1); assert.equal(f.game.phase, f.phaseNumber); assert.ok(clean(f.game));
|
|
assert.equal(f.game.truthtrance ?? null, null); assert.equal(f.game.shipmentPromises, undefined);
|
|
assert.equal(f.originalInventory.length, 33); assert.deepEqual(inventory(f.game), f.originalInventory);
|
|
assert.deepEqual(physical(f.game), f.originalPhysical); assert.equal(f.sourceActionCount, f.actions.length);
|
|
if (f.phase === 'storm') assert.deepEqual(f.game.stormDials, {});
|
|
else {
|
|
assert.equal(f.actions.filter(step => step.action.type === 'stormDial').length, 2);
|
|
assert.equal(f.game.spiceSequence ?? null, null);
|
|
assert.ok(f.game.spiceDiscard[0].some(card => 'territory' in card));
|
|
assert.ok(f.game.spiceDiscard[1].some(card => 'territory' in card));
|
|
}
|
|
}
|
|
function asked(f: TruthtranceEarlyAdvancedShipmentFixture) {
|
|
const actions: { actor: string; action: Action }[] = [];
|
|
let game = act(structuredClone(f.game), f.declarationStep.actor, f.declarationStep.action, actions);
|
|
const declared = structuredClone(game);
|
|
game = advance(game, state => state.truthtrance?.stage === 'ask', actions);
|
|
assert.ok(viewGame(game, f.asker).truthShipmentTargets.includes(f.target));
|
|
const beforeQuestion = structuredClone(game);
|
|
game = act(game, f.askStep.actor, f.askStep.action, actions);
|
|
assert.equal(game.truthtrance?.stage, 'answer');
|
|
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('yes'));
|
|
assert.ok(viewGame(game, f.target).truthShipmentAnswers?.includes('no'));
|
|
return { game, declared, beforeQuestion, actions };
|
|
}
|
|
function bound(f: TruthtranceEarlyAdvancedShipmentFixture, answer: 'yes' | 'no' = 'yes') {
|
|
const question = asked(f), game = act(question.game, f.target, { type: 'truthAnswer', answer }, question.actions);
|
|
assert.equal(game.truthtrance ?? null, null); assert.equal(game.shipmentPromises?.length, 1);
|
|
assert.equal(game.shipmentPromises![0].player, f.target);
|
|
assert.equal(game.shipmentPromises![0].answer, answer === 'yes');
|
|
assert.equal(game.shipmentPromises![0].released, undefined);
|
|
assert.equal(game.shipmentPromises![0].fulfilled, undefined);
|
|
assert.equal(game.phase, f.phaseNumber); assert.deepEqual(physical(game), f.originalPhysical);
|
|
assert.deepEqual(inventory(game), f.originalInventory); return game;
|
|
}
|
|
|
|
for (const phase of ['storm', 'charity', 'bidding'] as const) void test(
|
|
`public Advanced ${phase} Truthtrance binds an eight-force future shipment without granting an early action`, () => {
|
|
const f = fixture({ phase }); nativeSource(f); const game = bound(f);
|
|
const view = viewGame(game, f.target);
|
|
assert.equal(view.shipmentCompletion, null); assert.equal(view.shipmentPreparation, null);
|
|
reject(game, f.target, f.shipment); reject(game, f.target, { type: 'endMovement' });
|
|
reject(game, f.target, f.bigBribe);
|
|
assert.equal(game.shipmentPromises![0].released, undefined);
|
|
});
|
|
|
|
for (const guildTake of [false, true]) void test(
|
|
`public Advanced early shipment Yes retains purchase2 and native Guild ${guildTake ? 'TAKE' : 'WAIT'} through actual eight-force delivery`, () => {
|
|
const f = fixture(); nativeSource(f); const game = bound(f);
|
|
const completed = finish(f, game, { guildTake });
|
|
reject(completed.purchase.before, f.target, f.bigBid);
|
|
assert.equal(player(completed.purchase.before, f.target).spice, 10);
|
|
assert.equal(player(completed.purchase.after, f.target).spice, 8);
|
|
assert.ok(player(completed.purchase.after, f.target).hand.some(card => card.id === f.purchaseCard));
|
|
assert.equal(completed.purchase.after.shipmentPromises![0].released, undefined);
|
|
assert.ok(completed.timing.some(record => record.steps.some(step => step.actor === f.guild &&
|
|
step.action.type === 'decision' && step.action.take === guildTake)));
|
|
if (guildTake) {
|
|
assert.equal(completed.guildTurn.length, 1);
|
|
assert.deepEqual(completed.guildTurn[0].steps, [{ actor: f.guild, action: { type: 'endMovement' } }]);
|
|
} else assert.equal(completed.guildTurn.length, 0);
|
|
assert.equal(completed.beforeShipment.active, f.target);
|
|
assert.equal(viewGame(completed.beforeShipment, f.target).shipmentCompletion?.actions.at(-1)?.amount, 8);
|
|
assert.ok(completed.shipment.controls.some(control => control.decision?.kind === 'guildShipment'));
|
|
assert.ok(completed.shipment.controls.some(control => control.response?.kind === 'guildIncome'));
|
|
assert.equal(player(completed.delivered, f.guild).spice, player(completed.beforeShipment, f.guild).spice + 8);
|
|
assert.equal(player(completed.delivered, f.target).spice, 0);
|
|
assert.equal(player(completed.delivered, f.target).forces[f.location], 8);
|
|
assert.equal(player(completed.delivered, f.target).reserves, 12);
|
|
assert.equal(completed.delivered.shipmentPromises![0].fulfilled, true);
|
|
assert.equal(completed.delivered.shipmentPromises![0].released, undefined);
|
|
assert.ok(player(completed.game, f.counterHolder).hand.some(card => card.id === f.karama));
|
|
assert.equal(completed.game.discard.filter(card => card.id === f.card).length, 1);
|
|
reject(completed.delivered, f.target, f.shipment);
|
|
assert.deepEqual(inventory(completed.game), f.originalInventory);
|
|
});
|
|
|
|
void test('public Advanced early shipment No allows voluntary costs and a real smaller Carthag shipment', () => {
|
|
const f = fixture({ phase: 'bidding' }); nativeSource(f); const game = bound(f, 'no');
|
|
const voluntary = act(structuredClone(game), f.target, f.bigBribe);
|
|
assert.equal(player(voluntary, f.target).spice, 7); assert.equal(voluntary.shipmentPromises![0].released, undefined);
|
|
const completed = finish(f, game, { amount: 7, guildTake: true });
|
|
reject(completed.beforeShipment, f.target, f.shipment);
|
|
assert.equal(player(completed.delivered, f.target).forces[f.location], 7);
|
|
assert.equal(player(completed.delivered, f.target).spice, 1);
|
|
assert.equal(completed.delivered.shipmentPromises![0].fulfilled, true);
|
|
assert.equal(completed.delivered.shipmentPromises![0].released, undefined);
|
|
assert.deepEqual(inventory(completed.game), f.originalInventory);
|
|
});
|
|
|
|
void test('an actual first Storm releases an early Advanced shipment only after its current-board route becomes unavailable', () => {
|
|
const f = fixture({ phase: 'storm' }); nativeSource(f); let game = bound(f);
|
|
const initialStorm = game.storm, distance = (f.sector - initialStorm + 18) % 18;
|
|
assert.ok(distance > 0 && distance <= 20); assert.equal(game.stormDialers.length, 2);
|
|
for (const [index, actor] of game.stormDialers.entries())
|
|
game = act(game, actor, { type: 'stormDial', amount: index === 0 ? distance : 0 });
|
|
assert.equal(game.storm, initialStorm); assert.equal(game.stormPending, distance);
|
|
assert.equal(game.shipmentPromises![0].released, undefined, 'Recorded future distance is not a future-board promise prediction');
|
|
game = advance(game, state => state.phase === 2 && clean(state));
|
|
assert.equal(game.storm, f.sector); assert.equal(game.shipmentPromises![0].released, true);
|
|
const spent = act(game, f.target, f.bigBribe);
|
|
assert.equal(player(spent, f.target).spice, 7);
|
|
assert.deepEqual(inventory(spent), f.originalInventory);
|
|
});
|
|
|
|
void test('the genuine early Advanced Truth question rejects foreign and underlying replies before its native answer', () => {
|
|
const f = fixture(), question = asked(f), game = question.game;
|
|
reject(game, f.asker, { type: 'truthAnswer', answer: 'yes' });
|
|
reject(game, f.target, { type: 'ready' }); reject(game, f.target, f.shipment);
|
|
reject(game, f.target, f.bigBribe); 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' });
|
|
reject(done, f.asker, f.askStep.action);
|
|
assert.deepEqual(physical(done), f.originalPhysical); assert.deepEqual(inventory(done), f.originalInventory);
|
|
});
|
|
|
|
void test('all four minimal policies participate legally in the actual early Advanced bid while retaining shipment funds', () => {
|
|
const f = fixture(), boundGame = bound(f);
|
|
const game = advance(boundGame, state => state.phase === 3 && clean(state) && state.auction?.active === f.target);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game, f.target); view.players.find(player => player.id === f.target)!.bot = difficulty;
|
|
const before = structuredClone(view), action = botActions(view)[0]; assert.ok(action, difficulty);
|
|
assert.deepEqual(view, before);
|
|
const done = act(structuredClone(game), f.target, action);
|
|
assert.equal(done.shipmentPromises![0].released, undefined);
|
|
assert.ok(player(done, f.target).spice >= 8);
|
|
assert.deepEqual(inventory(done), f.originalInventory);
|
|
}
|
|
});
|