Recognize group Guild shipment promises
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
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));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user