Files
Dune/tests/guild-ambassador-no-field-engine.test.ts
T
Quintine 659e2a4a9a Support Basic No-Field Guild Ambassador shipments
Connect native concealment and allied supplier/recipient consent to the independent grant, with explicit provisional cap and transport-fee policy. Preserve actual tokens, ordinary allowances, source-scoped Karama prevention and original income/arrival continuation.

Add genuine three/four-seat sources, focused quote/control/engine cases and bounded runtime/minimal-bot proof. Advanced and optional profiles retain their existing guards.
2026-10-24 17:36:12 +11:00

318 lines
20 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { viewGame, type Action, type Game } from '../game/engine';
import {
actGuildAmbassadorNoField as act, advanceGuildAmbassadorNoField as advance,
createGuildAmbassadorNoFieldFixture as fixture, guildAmbassadorNoFieldClean as clean,
guildAmbassadorNoFieldInventory as inventory, guildAmbassadorNoFieldPlayer as player,
passGuildAmbassadorNoFieldResponses as passResponses,
type GuildAmbassadorNoFieldFixture,
} from './fixture-guild-ambassador-no-field';
function allowances(game: Game) {
return { active: game.active, order: game.order, remaining: game.movementRemaining,
players: game.players.map(p => ({ id: p.id, shipped: p.shipped, moved: p.moved })) };
}
function material(game: Game) {
return game.players.map(p => ({ id: p.id, spice: p.spice, reserves: p.reserves, forces: p.forces,
noField: p.noField, noFieldEvent: p.noFieldEvent, noFieldBlockedTurn: p.noFieldBlockedTurn }));
}
function nativeAction(f: GuildAmbassadorNoFieldFixture, value: 0 | 3 | 5, extra: Partial<Action> = {}): Action {
const token = f.tokens.find(token => token.value === value)!;
return { type: 'decision', event: f.event, amount: 1, noField: token.id,
noFieldEvent: player(f.game, f.richese).noFieldEvent,
territory: f.territory, sector: f.sector, ...extra };
}
function reject(game: Game, actor: string, action: Action, reason?: RegExp) {
const before = structuredClone(game), submitted = structuredClone(action);
if (reason) assert.throws(() => act(game, actor, action), reason);
else assert.throws(() => act(game, actor, action));
assert.deepEqual(game, before); assert.deepEqual(action, submitted);
}
function finishEntrant(f: GuildAmbassadorNoFieldFixture, game: Game) {
game = advance(game, clean);
assert.equal(game.pendingAmbassador, null);
assert.equal(game.active, f.entrantHark);
assert.equal(player(game, f.entrantHark).forces['habbanya_ridge_sietch:17'], 1);
game = act(game, f.entrantHark, { type: 'move', from: 'habbanya_ridge_sietch:17', amount: 1,
territory: f.desert, sector: f.desertSector });
game = advance(game, clean);
assert.equal(player(game, f.entrantHark).forces[`${f.desert}:${f.desertSector}`], 1);
game = act(game, f.entrantHark, { type: 'endMovement' });
game = advance(game, state => state.active === f.guild && clean(state));
assert.deepEqual(inventory(game), f.originalInventory);
return game;
}
void test('genuine Basic Guild marker source uses native deal, paid placements, accepted Ecaz alliance and paid entrant', () => {
for (const beneficiary of ['own', 'allied'] as const) {
const f = fixture({ beneficiary }), g = f.game;
assert.deepEqual(f.staging, []);
assert.equal(g.advanced, false); assert.deepEqual(g.expansions, ['ecaz', 'choam']);
assert.ok(!g.homeworlds && !g.nexusCards && !g.leaderSkills && !g.techTokens &&
!g.strongholdCards && !g.discoveryEnabled && !g.ecazTreachery);
assert.equal(g.status, 'playing'); assert.equal(g.turn, 1); assert.equal(g.phase, 5);
assert.ok(g.pendingAmbassador); assert.equal(g.pendingAmbassador.stage, 'ship');
assert.equal(g.pendingAmbassador.effect, 'guild');
assert.equal(g.pendingAmbassador.entrant, f.entrantHark);
assert.equal(g.pendingAmbassador.beneficiary, f.beneficiaryID);
assert.equal(g.decision?.player, f.beneficiaryID);
assert.deepEqual(g.order, [f.ownerEcaz, f.richese, f.entrantHark, f.guild]);
assert.equal(player(g, f.ownerEcaz).ally, f.richese); assert.equal(player(g, f.richese).ally, f.ownerEcaz);
assert.equal(player(g, f.ownerEcaz).shipped, true); assert.equal(player(g, f.richese).shipped, true);
assert.equal(player(g, f.entrantHark).shipped, true); assert.equal(player(g, f.guild).shipped, false);
assert.equal(player(g, f.richese).forces['carthag:11'], 1);
assert.equal(player(g, f.ownerEcaz).forces['polar_sink:0'], 1);
assert.equal(player(g, f.entrantHark).forces['habbanya_ridge_sietch:17'], 1);
assert.equal(g.ecazAmbassadors!.tokens.find(token => token.effect === 'ecaz')!.zone, 'supply');
assert.equal(g.ecazAmbassadors!.tokens.find(token => token.effect === 'guild')!.zone, 'used');
assert.ok(f.alliance.steps.some(step => step.actor === f.richese && step.action.accept === true));
assert.ok(f.alliance.steps.some(step => step.actor === f.ownerEcaz && step.action.loan === false));
assert.deepEqual(inventory(g), f.originalInventory);
}
});
for (const value of [0, 3, 5] as const) {
void test(`native Basic Richese Guild grant conceals original ${value} for free without spending its ordinary allowance`, () => {
const f = fixture({ beneficiary: 'own', karama: true }), before = structuredClone(f.game);
const token = f.tokens.find(token => token.value === value)!;
let game = act(f.game, f.richese, nativeAction(f, value));
assert.equal(game.response?.kind, 'richeseNoField');
assert.deepEqual(material(game), material(before));
game = passResponses(game);
assert.deepEqual(player(game, f.richese).noField!.deployed, {
tokenId: token.id, controller: f.richese, location: { territory: f.territory, sector: f.sector },
});
assert.equal(player(game, f.richese).noField!.lastShipped, token.id);
assert.equal(player(game, f.richese).reserves, player(before, f.richese).reserves);
assert.equal(player(game, f.richese).forces[`${f.territory}:${f.sector}`] ?? 0, 0);
for (const p of game.players) assert.equal(p.spice, player(before, p.id).spice);
assert.deepEqual(allowances(game), allowances(before));
const publicMarker = viewGame(game, f.entrantHark).players.find(p => p.id === f.richese)!.noField!.deployed!;
assert.equal(publicMarker.effectiveForces, 1);
assert.deepEqual(inventory(game), f.originalInventory);
finishEntrant(f, game);
});
}
function requestAllied(f: GuildAmbassadorNoFieldFixture, game = f.game, desert = false) {
return act(game, f.beneficiaryID, { type: 'decision', event: f.event, requestNoField: true,
territory: desert ? f.desert : f.territory, sector: desert ? f.desertSector : f.sector });
}
function offerAllied(f: GuildAmbassadorNoFieldFixture, game: Game, value: 0 | 3 | 5,
payer = f.richese) {
return act(game, f.richese, { type: 'decision', event: f.event,
offerNoField: f.tokens.find(token => token.value === value)!.id,
noFieldEvent: player(game, f.richese).noFieldEvent, payer, elite: 0 });
}
void test('a five-marker materializes four available allied counters without inventing the missing fifth', () => {
const f = fixture({ beneficiary: 'allied' });
// Controlled prior reserve shortage after the genuine grant: move existing
// Ecaz counters to its original setup location, without changing total custody.
const source = structuredClone(f.game), recipient = player(source,f.beneficiaryID);
recipient.forces['imperial_basin:9'] += recipient.reserves - 4;
recipient.reserves = 4;
const before = structuredClone(source);
let game = requestAllied(f,source);
game = offerAllied(f,game,5,f.richese);
game = act(game,f.beneficiaryID,{type:'decision',event:f.event,acceptNoField:true});
game = passResponses(game);
assert.equal(player(game,f.beneficiaryID).reserves,0);
assert.equal(player(game,f.beneficiaryID).forces[`${f.territory}:${f.sector}`],4);
assert.equal(player(game,f.richese).noField!.lastShipped,f.tokens.find(token => token.value === 5)!.id);
assert.deepEqual(allowances(game),allowances(before));
for (const p of game.players) assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum,n) => sum+n,0),20);
finishEntrant(f,game);
});
void test('native No-Field remains available when the physical four-force grant has no reserve counter', () => {
const f = fixture({ beneficiary: 'own' });
// Controlled prior deployment conserves all original counters; the new
// concealed marker itself withdraws none and may later reveal zero.
const source = structuredClone(f.game), richese = player(source,f.richese);
richese.forces['carthag:11'] += richese.reserves;
richese.reserves = 0;
const view = viewGame(source,f.richese);
assert.equal(view.ambassadorEntry?.shipment?.maximum,0);
assert.ok(view.ambassadorEntry?.noField?.options.some(option => option.action.noField === f.tokens.find(token => token.value === 5)!.id));
let game = act(source,f.richese,nativeAction(f,5));
game = passResponses(game);
assert.equal(player(game,f.richese).reserves,0);
assert.equal(player(game,f.richese).forces['carthag:11'],20);
assert.equal(player(game,f.richese).noField!.deployed!.tokenId,f.tokens.find(token => token.value === 5)!.id);
finishEntrant(f,game);
});
void test('original ordinary starting hands automatically allow both Guild marker declarations without a pending Karama responder', () => {
for (const beneficiary of ['own', 'allied'] as const) {
const f = fixture({ beneficiary }), before = structuredClone(f.game);
assert.ok(f.game.players.every(p => p.hand.every(card => card.effect !== 'karama')));
let game = beneficiary === 'own' ? act(f.game, f.richese, nativeAction(f, 3)) : requestAllied(f);
if (beneficiary === 'allied') {
game = offerAllied(f, game, 3, f.beneficiaryID);
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, acceptNoField: true });
assert.equal(player(game, f.beneficiaryID).forces[`${f.territory}:${f.sector}`], 3);
assert.equal(player(game, f.beneficiaryID).reserves, player(before, f.beneficiaryID).reserves - 3);
assert.equal(player(game, f.richese).noField!.deployed, null);
} else {
assert.equal(player(game, f.richese).noField!.deployed!.tokenId, f.tokens.find(token => token.value === 3)!.id);
assert.equal(player(game, f.richese).reserves, player(before, f.richese).reserves);
}
assert.equal(game.response, null);
assert.deepEqual(allowances(game), allowances(before));
finishEntrant(f, game);
}
});
for (const desert of [false, true]) {
for (const payee of ['supplier', 'recipient', ...(desert ? ['both'] : [])]) {
void test(`allied Basic three-marker ${desert ? 'desert' : 'stronghold'} grant charges the printed fee to ${payee}`, () => {
const f = fixture({ beneficiary: 'allied', karama: true }), before = structuredClone(f.game);
const payer = payee === 'supplier' ? f.richese : payee === 'recipient' ? f.beneficiaryID : 'both';
let game = requestAllied(f, f.game, desert);
assert.equal(game.decision?.player, f.richese);
assert.deepEqual(material(game), material(before));
game = offerAllied(f, game, 3, payer);
assert.equal(game.decision?.player, f.beneficiaryID);
assert.deepEqual(material(game), material(before));
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, acceptNoField: true });
assert.equal(game.response?.kind, 'richeseNoField');
assert.deepEqual(material(game), material(before));
game = passResponses(game);
const cost = desert ? 2 : 1, ownerPayment = payee === 'supplier' ? cost : payee === 'both' ? 1 : 0;
const recipientPayment = cost - ownerPayment;
const target = desert ? `${f.desert}:${f.desertSector}` : `${f.territory}:${f.sector}`;
assert.equal(player(game, f.beneficiaryID).forces[target], 3);
assert.equal(player(game, f.beneficiaryID).reserves, player(before, f.beneficiaryID).reserves - 3);
assert.equal(player(game, f.richese).reserves, player(before, f.richese).reserves);
assert.equal(player(game, f.richese).noField!.deployed, null);
assert.equal(player(game, f.richese).noField!.lastShipped, f.tokens.find(token => token.value === 3)!.id);
assert.equal(player(game, f.richese).spice, player(before, f.richese).spice - ownerPayment);
assert.equal(player(game, f.beneficiaryID).spice, player(before, f.beneficiaryID).spice - recipientPayment);
assert.equal(player(game, f.guild).spice, player(before, f.guild).spice + cost);
assert.deepEqual(allowances(game), allowances(before));
assert.deepEqual(inventory(game), f.originalInventory);
finishEntrant(f, game);
});
}
}
void test('actual prior native three-marker must be explicitly revealed before a different Guild marker is declared', () => {
const f = fixture({ beneficiary: 'own', priorMarker: true });
const original = structuredClone(f.game), richese = player(original, f.richese);
assert.deepEqual(richese.noField!.deployed, {
tokenId: f.priorToken.id, controller: f.richese, location: { territory: 'carthag', sector: 11 },
});
reject(f.game, f.richese, nativeAction(f, 5), /reveal/i);
let game = act(f.game, f.richese, { type: 'decision', event: f.event, revealNoField: true });
assert.equal(game.pendingAmbassador?.stage, 'ship'); assert.equal(game.decision?.player, f.richese);
assert.equal(player(game, f.richese).noField!.deployed, null);
assert.equal(player(game, f.richese).forces['carthag:11'], 3);
assert.equal(player(game, f.richese).reserves, richese.reserves - 3);
assert.equal(player(game, f.richese).noField!.lastShipped, f.priorToken.id);
assert.notEqual(player(game, f.richese).noFieldEvent, richese.noFieldEvent);
assert.deepEqual(allowances(game), allowances(original));
reject(game, f.richese, nativeAction(f, 5), /event|stale/i);
reject(game, f.richese, nativeAction(f, 3, { noFieldEvent: player(game, f.richese).noFieldEvent }), /twice|same|row/i);
const revealed = structuredClone(game);
game = act(game, f.richese, nativeAction(f, 5, { noFieldEvent: player(game, f.richese).noFieldEvent }));
game = passResponses(game);
assert.equal(player(game, f.richese).noField!.deployed!.tokenId, f.tokens.find(token => token.value === 5)!.id);
assert.equal(player(game, f.richese).reserves, player(revealed, f.richese).reserves);
assert.equal(player(game, f.richese).spice, richese.spice);
assert.deepEqual(allowances(game), allowances(original));
finishEntrant(f, game);
});
void test('allied zero grant reveals the actual older supplier marker only after recipient consent and neutral prevention passes', () => {
const f = fixture({ beneficiary: 'allied', priorMarker: true, karama: true }), before = structuredClone(f.game);
let game = requestAllied(f);
game = offerAllied(f, game, 0, f.beneficiaryID);
assert.deepEqual(material(game), material(before));
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, acceptNoField: true });
assert.equal(game.response?.kind, 'richeseNoField');
assert.deepEqual(material(game), material(before));
game = passResponses(game);
assert.equal(player(game, f.richese).forces['carthag:11'], 3);
assert.equal(player(game, f.richese).reserves, player(before, f.richese).reserves - 3);
assert.equal(player(game, f.richese).noField!.deployed, null);
assert.equal(player(game, f.richese).noField!.lastShipped, f.tokens.find(token => token.value === 0)!.id);
assert.equal(player(game, f.beneficiaryID).reserves, player(before, f.beneficiaryID).reserves);
assert.equal(player(game, f.beneficiaryID).forces[`${f.territory}:${f.sector}`] ?? 0, 0);
assert.equal(player(game, f.beneficiaryID).spice, player(before, f.beneficiaryID).spice - 1);
assert.equal(player(game, f.guild).spice, player(before, f.guild).spice + 1);
assert.deepEqual(allowances(game), allowances(before));
finishEntrant(f, game);
});
for (const who of ['supplier', 'recipient'] as const) {
void test(`allied Guild substitution ${who} refusal returns the same free physical grant without payment or token changes`, () => {
const f = fixture({ beneficiary: 'allied' }), before = structuredClone(f.game);
let game = requestAllied(f);
if (who === 'recipient') game = offerAllied(f, game, 3);
game = act(game, who === 'supplier' ? f.richese : f.beneficiaryID,
{ type: 'decision', event: f.event, declineNoField: true });
assert.equal(game.decision?.player, f.beneficiaryID);
assert.ok(game.pendingAmbassador); assert.equal(game.pendingAmbassador.event, f.event);
assert.equal(game.pendingAmbassador.stage, 'ship');
assert.deepEqual(material(game), material(before)); assert.deepEqual(allowances(game), allowances(before));
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, amount: 4,
territory: f.territory, sector: f.sector, elite: 0 });
game = advance(game, clean);
assert.equal(player(game, f.beneficiaryID).forces[`${f.territory}:${f.sector}`], 4);
assert.equal(player(game, f.beneficiaryID).reserves, player(before, f.beneficiaryID).reserves - 4);
for (const p of game.players) assert.equal(p.spice, player(before, p.id).spice);
assert.deepEqual(player(game, f.richese).noField, player(before, f.richese).noField);
assert.deepEqual(allowances(game), allowances(before)); finishEntrant(f, game);
});
}
void test('allied five cannot enlarge the four-force grant and malformed native mixtures preserve the live opportunity', () => {
const allied = fixture({ beneficiary: 'allied' });
const requested = requestAllied(allied);
assert.ok(player(requested, allied.beneficiaryID).reserves > 4);
reject(requested, allied.richese, { type: 'decision', event: allied.event,
offerNoField: allied.tokens.find(token => token.value === 5)!.id,
noFieldEvent: player(requested, allied.richese).noFieldEvent, payer: allied.richese, elite: 0 }, /four|4/i);
const own = fixture({ beneficiary: 'own' });
for (const extra of [{ amount: 4 }, { elite: 1 }, { allyPayment: 1 }, { noFieldEvent: 'stale-selection' },
{ event: 'stale-parent' }, { alliedNoField: true }])
reject(own.game, own.richese, nativeAction(own, 3, extra));
reject(own.game, own.entrantHark, nativeAction(own, 3));
let game = act(own.game, own.richese, nativeAction(own, 3));
game = passResponses(game); finishEntrant(own, game);
});
for (const beneficiary of ['own', 'allied'] as const) {
void test(`actual held Karama prevents the ${beneficiary} Guild marker only, preserving ordinary shipment state and physical fallback`, () => {
const f = fixture({ beneficiary, karama: true }), before = structuredClone(f.game);
assert.ok(f.karama); assert.ok(player(f.game, f.entrantHark).hand.some(card => card.id === f.karama));
let game = beneficiary === 'own' ? act(f.game, f.richese, nativeAction(f, 3)) : requestAllied(f);
if (beneficiary === 'allied') {
game = offerAllied(f, game, 3, f.beneficiaryID);
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, acceptNoField: true });
}
assert.equal(game.response?.kind, 'richeseNoField');
assert.deepEqual(material(game), material(before));
game = act(game, f.entrantHark, { type: 'card', mode: 'cancel', card: f.karama });
game = advance(game, state => !state.response && !state.pendingTreacheryDiscard);
assert.deepEqual(material(game), material(before)); assert.deepEqual(allowances(game), allowances(before));
assert.ok(game.pendingAmbassador); assert.equal(game.pendingAmbassador.event, f.event);
assert.equal(game.pendingAmbassador.stage, 'ship');
assert.equal(game.decision?.player, f.beneficiaryID);
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
assert.equal(player(game, f.richese).noFieldBlockedTurn, player(before, f.richese).noFieldBlockedTurn);
assert.deepEqual(game.richeseAllyBlocked, before.richeseAllyBlocked);
if (beneficiary === 'own') reject(game, f.richese, nativeAction(f, 3));
else reject(game, f.beneficiaryID, { type: 'decision', event: f.event, requestNoField: true,
territory: f.territory, sector: f.sector });
game = act(game, f.beneficiaryID, { type: 'decision', event: f.event, amount: 1,
territory: f.territory, sector: f.sector, elite: 0 });
game = advance(game, clean);
assert.equal(player(game, f.beneficiaryID).forces[`${f.territory}:${f.sector}`], 1);
assert.equal(player(game, f.beneficiaryID).reserves, player(before, f.beneficiaryID).reserves - 1);
assert.deepEqual(allowances(game), allowances(before)); finishEntrant(f, game);
});
}