Recognize group Guild shipment promises
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 03:50:33 +11:00
1 parent 6eb417ff32
commit 44e7cf8001
4 files changed
+145 -7

No files matched your search

@@ -0,0 +1,116 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
const act = (game: Game, id: string, action: Action) => applyAction(game, id, action);
const player = (game: Game, id: string) => game.players.find(seat => seat.id === id)!;
const shipment: Action = {type:'guildShip', from:'reserves', territory:'carthag', sector:11, amount:3, elite:0, allyPayment:0};
/** The unexposed group mode requires a staged empty ledger and later Movement
* phase; the pair and third member join using actual Nexus actions. Forces and
* wallets are staged with physical totals intact, and the actual Truthtrance
* card is transferred from the undealt deck. No saved room is touched. */
function fixture() {
let game = createGame('GROUPFREMENSHIP', newPlayer('g', 'Guild', 'guild'), false);
game.players.push(newPlayer('e', 'Emperor', 'emperor'), newPlayer('f', 'Fremen', 'fremen'),
newPlayer('h', 'Harkonnen', 'harkonnen'));
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true, order:['g','e','f','h'],
ready:[], storm:18, deck:baseDeck(), response:null, decision:null, phaseOpening:null});
game.coalitionState = emptyCoalitionState();
game = act(game, 'g', {type:'alliance', target:'e'});
game = act(game, 'e', {type:'alliance', target:'g'});
const pair = coalitionOf(game.players, game.coalitionState, 'g'); assert.ok(pair);
game = act(game, 'g', {type:'coalitionPropose', groupId:pair.id, entrant:'f',
expectedRevision:game.coalitionState!.revision});
for (const id of ['e','f']) {
const proposal = game.coalitionState!.proposal!;
game = act(game, id, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus,
turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision});
}
assert.equal(player(game,'f').ally, null, 'A three-member group does not use the pair-only scalar.');
Object.assign(game, {phase:5, nexus:false, active:'f', ready:[],
movementRemaining:['f','g','e','h'], guildTimingGranted:false, guildTimingLocked:false});
for (const seat of game.players) {
seat.forces = {}; seat.reserves = 20; seat.tanks = 0; seat.spice = 10;
seat.hand = []; seat.traitors = []; seat.traitorChoices = []; seat.shipped = false; seat.moved = 0;
}
const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0);
const [truth] = game.deck.splice(index,1); player(game,'h').hand.push(truth);
// A distinct undealt Karama gives the outside player a real opportunity to
// counter the native permission and rate; it stays held when passed.
const karama = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(karama >= 0);
player(game,'h').hand.push(...game.deck.splice(karama,1));
return {game, truth};
}
function settle(game: Game): Game {
for (let n = 0; game.response && n < 32; n++) {
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(seat);
game = act(game,seat.id,{type:'passResponse'});
}
assert.equal(game.response ?? null,null);
return game;
}
function ask() {
const {game:before, truth} = fixture();
let game = act(before,'h',{type:'card',card:truth.id});
while (game.truthtrance?.stage === 'priority') {
const seat = game.players.find(candidate => !game.truthtrance!.passed.includes(candidate.id)); assert.ok(seat);
game = act(game,seat.id,{type:'truthPass'});
}
game = act(game,'h',{type:'truthAsk',question:{kind:'shipment',target:'f',turn:game.turn,
territory:'carthag',minimum:3}});
assert.equal(game.truthtrance?.stage,'answer');
return {before,game,truth};
}
void test('group-allied Fremen can actually ship from reserves to Carthag with native Guild permission', () => {
let {game} = fixture();
// Carthag is outside the Fremen southern reinforcement area. The native
// group-aware Guild path is the real legal transport for this location.
assert.throws(() => act(game,'f',{type:'ship',territory:'carthag',sector:11,amount:3,elite:0}));
game = act(game,'f',shipment);
assert.equal(game.response?.kind,'guildTransportPermission');
assert.equal(player(game,'f').reserves,20);
game = settle(game);
assert.equal(player(game,'f').forces['carthag:11'],3);
assert.equal(player(game,'f').reserves,17);
assert.equal(player(game,'f').spice,8);
assert.equal(player(game,'g').spice,12);
});
void test('a group-allied Fremen can answer Yes to the real Carthag reserve-shipment question and fulfill it', () => {
const {game:asked,truth} = ask();
assert.ok(viewGame(asked,'f').truthShipmentAnswers?.includes('yes'));
let game = act(asked,'f',{type:'truthAnswer',answer:'yes'});
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.answer && !promise.released));
assert.equal(game.discard.filter(card => card.id === truth.id).length,1);
assert.throws(() => act(game,'f',{type:'endMovement'}),/Truthtrance/i);
game = act(game,'f',shipment);
assert.equal(game.response?.kind,'guildTransportPermission');
game = settle(game);
assert.equal(player(game,'f').forces['carthag:11'],3);
assert.equal(player(game,'f').spice,8);
assert.equal(player(game,'g').spice,12);
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released));
});
void test('all four bot profiles can answer and follow the original group Guild shipment witness', () => {
const {game:asked} = ask();
for (const difficulty of ['Easy','Medium','Hard','Brutal'] as const) {
const answerView = viewGame(asked,'f');
answerView.players.find(seat => seat.id === 'f')!.bot = difficulty;
const answer = botActions(answerView).find(action => action.type === 'truthAnswer'); assert.ok(answer);
assert.equal(answer.answer,'yes');
let game = act(structuredClone(asked),'f',answer);
const movementView = viewGame(game,'f');
movementView.players.find(seat => seat.id === 'f')!.bot = difficulty;
const next = botActions(movementView)[0]; assert.equal(next?.type,'guildShip');
assert.equal(next.from,'reserves'); assert.equal(next.territory,'carthag');
game = act(game,'f',next);
game = settle(game);
assert.ok(game.shipmentPromises?.some(promise => promise.player === 'f' && promise.fulfilled && !promise.released));
}
});