Files
Dune/tests/bot-guild-no-field-transport.test.ts

145 lines
7.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { guildNoFieldTransportActions as actions } from '../game/bot-guild-no-field-transport';
import { createGame, newPlayer, viewGame, type Action, type GameView } from '../game/engine';
import { territory } from '../game/board';
import { guildTransportQuote } from '../game/transport-quote';
import { botHomeworldShipmentPaymentAllowed } from '../game/homeworld-payment-options';
/** Public/owner-entitled projection fixtures for this bot adapter only.
* These are not played-state or physical card/force acquisition programmes.
*/
function fixture(mode: 'native' | 'secret' = 'native', value: 0 | 3 | 5 = 0): GameView {
const source = createGame('BOT-NF-GUILD', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
source.players.push(newPlayer(mode === 'native' ? 'g' : 'f', 'Partner', mode === 'native' ? 'guild' : 'fremen'),
newPlayer('a', 'Atreides', 'atreides'), newPlayer('e', 'Emperor', 'emperor'));
const view = viewGame(source, 'r');
Object.assign(view, { status: 'playing', phase: 5, turn: 2, active: 'r',
order: source.players.map(player => player.id),
storm: 18, response: null, decision: null, truthtrance: null, phaseOpening: null,
automaticContinuationPending: false });
for (const seat of view.players) {
seat.forces = {};
seat.ally = null;
seat.shipped = false;
}
const own = view.players[0];
own.spice = 2;
own.reserves = 0;
if (mode === 'native') {
own.ally = 'g';
view.players[1].ally = 'r';
}
view.aid.available = 0;
const location = { territory: 'imperial_basin', sector: 10 };
const tokenId = `nf-${value}`;
const publicMarker = { deployed: { controller: 'r', location: { ...location }, effectiveForces: 1 as const }, lastUsed: null };
own.noField = publicMarker;
view.richeseNoField = {
owner: 'r', event: 'owned-marker-view-source', public: publicMarker,
private: { tokens: [{ id: 'nf-0', value: 0 }, { id: 'nf-3', value: 3 }, { id: 'nf-5', value: 5 }],
lastShipped: tokenId, deployed: { tokenId, controller: 'r', location: { ...location } } },
canShip: false, homeworldForcesEnabled: false, shipBlock: 'A marker is already deployed.',
canReveal: true, revealBlock: null, canOfferAlly: false, allyOfferBlock: 'No ordinary allied offer.',
allyPricing: null, allyDeclined: false, allyOffer: null,
};
if (mode === 'secret') {
view.nexusCards = { card: 'guild', deckCount: 11, discardCount: 0,
held: Object.fromEntries(view.players.map(player => [player.id, player.id === 'r'])),
turn: null, choices: [], waiting: [] };
view.nexusGuildSecretAlly = { event: JSON.stringify(['nexusGuildSecretAlly', view.turn, view.me]), blocked: null };
}
return view;
}
function legal(view: GameView, choices: Action[]): void {
assert.ok(choices.length > 0);
for (const choice of choices) {
const quote = guildTransportQuote(view, choice);
assert.deepEqual(quote.unavailableReasons, [], JSON.stringify(choice));
assert.equal(botHomeworldShipmentPaymentAllowed(view, choice), true);
assert.ok(quote.quote);
assert.equal(quote.physicalForces, 0);
assert.equal(choice.type, 'guildShip');
assert.equal(choice.noField, view.richeseNoField!.private!.deployed!.tokenId);
assert.equal(choice.event, view.richeseNoField!.event);
assert.deepEqual(choice.forces, {});
assert.equal(choice.allyPayment, quote.quote.pledgedPayment);
assert.equal(choice.amount, undefined);
assert.equal(choice.from, undefined);
}
}
void test('a reciprocal Guild ally chooses quoted marker-only cross routes without revelation or native reserve return', () => {
const view = fixture(), before = structuredClone(view), choices = actions(view);
legal(view, choices);
assert.ok(choices.every(choice => choice.territory !== 'imperial_basin' && choice.territory !== 'reserves'));
assert.ok(choices.every(choice => choice.nexus === undefined));
assert.deepEqual(actions(view), choices, 'enumeration remains stable');
assert.deepEqual(view, before);
const variant = fixture('native', 5);
variant.players[0].reserves = 20;
assert.deepEqual(actions(variant).map(choice => ({ ...choice, noField: undefined })),
choices.map(choice => ({ ...choice, noField: undefined })), 'hidden value and reserves do not choose permission or destinations');
view.players[1].ally = null;
assert.deepEqual(actions(view), [], 'a one-sided alliance supplies no Guild permission');
});
void test('an actual absent-Guild Secret Ally adds its original Nexus source and permits a concealed reserve return', () => {
const view = fixture('secret'), choices = actions(view);
legal(view, choices);
const returned = choices.find(choice => choice.territory === 'reserves');
assert.ok(returned);
assert.equal(returned.sector, 0);
assert.ok(choices.every(choice => choice.nexus === view.nexusGuildSecretAlly!.event));
const wrongCard = structuredClone(view);
wrongCard.nexusCards!.card = 'ecaz';
assert.deepEqual(actions(wrongCard), []);
const allied = structuredClone(view);
allied.players[0].ally = 'f';
allied.players[1].ally = 'r';
assert.deepEqual(actions(allied), [], 'Secret Ally does not manufacture a real alliance');
});
void test('stale or missing owned sources and closed shipment windows produce no marker transport packet', () => {
const view = fixture(), old = actions(view);
legal(view, old);
view.richeseNoField!.event = 'renewed-owned-marker-source';
assert.ok(actions(view).every(choice => choice.event === view.richeseNoField!.event));
assert.ok(guildTransportQuote(view, old[0]).unavailableReasons.length > 0);
const absent = fixture();
absent.richeseNoField = null;
assert.deepEqual(actions(absent), []);
const foreign = fixture();
foreign.richeseNoField!.private!.deployed!.controller = 'g';
assert.deepEqual(actions(foreign), []);
const used = fixture();
used.players[0].shipped = true;
assert.deepEqual(actions(used), []);
const waiting = fixture();
waiting.phaseOpening = { passed: [] };
assert.deepEqual(actions(waiting), []);
const otherTurn = fixture();
otherTurn.active = 'g';
assert.deepEqual(actions(otherTurn), []);
});
void test('shared quotes exclude storm and occupied destinations and supply only the minimum pledged funding', () => {
const sourceStorm = fixture();
sourceStorm.storm = 10;
assert.deepEqual(actions(sourceStorm), []);
const view = fixture();
const carthag = territory('carthag').sectors[0], arrakeen = territory('arrakeen').sectors[0];
view.players[1].forces[`carthag:${carthag}`] = 1;
view.players[2].forces[`arrakeen:${arrakeen}`] = 1;
view.players[3].forces[`arrakeen:${arrakeen}`] = 1;
const choices = actions(view);
legal(view, choices);
assert.ok(choices.every(choice => choice.territory !== 'carthag' && choice.territory !== 'arrakeen'));
view.players[0].spice = 0;
assert.deepEqual(actions(view), []);
view.aid.available = 1;
const funded = actions(view);
legal(view, funded);
assert.ok(funded.every(choice => choice.allyPayment === 1));
});