Files
Dune/tests/coalition-alliance-admission-commitments.test.ts
T
Quintine 6524ed3bf2
Verify and publish checkpoint container / container (push) Canceled after 0s
Allow group admission with compatible commitments
2026-10-29 00:54:36 +11:00

126 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
const send = (game:Game, actor:string, action:Action) => applyAction(game,actor,action);
const seat = (game:Game, id:string) => game.players.find(player=>player.id===id)!;
/** Only the quiet Basic Nexus and two physical Truthtrance cards are staged.
* Pair/group formation, each question, consent and departure use real actions. */
function group(): {game:Game; cards:string[]} {
let game = createGame('GROUPALLIANCEADMIT',newPlayer('a','Atreides','atreides'),false);
game.players.push(newPlayer('b','Emperor','emperor'),newPlayer('c','Fremen','fremen'),
newPlayer('d','Harkonnen','harkonnen'),newPlayer('e','Guild','guild'),
newPlayer('f','Bene Gesserit','beneGesserit'));
Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,
order:['a','b','c','d','e','f'],ready:[],deck:baseDeck()});
game.coalitionState = emptyCoalitionState('unlimited');
game = send(game,'a',{type:'alliance',target:'b'});
game = send(game,'b',{type:'alliance',target:'a'});
const pair = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(pair);
game = send(game,'a',{type:'coalitionPropose',groupId:pair.id,entrant:'c',
expectedRevision:game.coalitionState!.revision});
for (const actor of ['b','c']) game = answer(game,actor,true);
const cards:string[] = [];
for (let count=0;count<2;count++) {
const index = game.deck.findIndex(card=>card.effect==='truthtrance'); assert.ok(index>=0);
const [card] = game.deck.splice(index,1); cards.push(card.id); seat(game,'f').hand.push(card);
}
return {game,cards};
}
function ask(game:Game, card:string, partner:string, choice:'yes'|'no'):Game {
game = send(game,'f',{type:'card',card});
while (game.truthtrance?.stage==='priority') {
const next = game.players.find(player=>!game.truthtrance!.passed.includes(player.id)); assert.ok(next);
game = send(game,next.id,{type:'truthPass'});
}
game = send(game,'f',{type:'truthAsk',question:{kind:'alliance',target:'a',turn:game.turn,partner}});
assert.ok(viewGame(game,'a').truthAllianceAnswers?.includes(choice));
return send(game,'a',{type:'truthAnswer',answer:choice});
}
function propose(game:Game, proposer='c'):Game {
const current = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(current);
return send(game,proposer,{type:'coalitionPropose',groupId:current.id,entrant:'d',
expectedRevision:game.coalitionState!.revision});
}
function answer(game:Game, actor:string, accept:boolean):Game {
const offer = game.coalitionState!.proposal; assert.ok(offer);
return send(game,actor,{type:'coalitionAnswer',event:offer.event,nexus:offer.nexus,
turn:offer.turn,accept,expectedRevision:game.coalitionState!.revision});
}
function rejectedAcceptance(game:Game, actor:string) {
const offer = game.coalitionState!.proposal; assert.ok(offer);
const before = structuredClone(game);
assert.throws(()=>send(game,actor,{type:'coalitionAnswer',event:offer.event,nexus:offer.nexus,
turn:offer.turn,accept:true,expectedRevision:game.coalitionState!.revision}));
assert.deepEqual(game,before,'Rejected consent must preserve the offer and every answer.');
}
function botDeclines(game:Game, actor:string) {
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game,actor);
view.players.find(player=>player.id===actor)!.bot = difficulty;
const action = botActions(view)[0]; assert.ok(action,difficulty);
assert.equal(action.type,'coalitionAnswer',difficulty);
assert.equal(action.accept,false,difficulty);
assert.doesNotThrow(()=>send(game,actor,action),difficulty);
}
}
void test('an unmet group No survives a pending invitation the respondent can decline',()=>{
const fixture=group();
let game=ask(fixture.game,fixture.cards[0],'b','no');
const event=game.alliancePromises!.find(row=>row.player==='a')!.event;
assert.equal(viewGame(game,'a').alliancePromiseGuide?.action.type,'coalitionLeave');
game=propose(game,'c');
assert.ok(game.coalitionState!.proposal);
assert.ok(!game.alliancePromises!.find(row=>row.event===event)!.released,
'The respondent can decline the invitation and then leave.');
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game,'a');
view.players.find(player=>player.id==='a')!.bot = difficulty;
const action = botActions(view)[0]; assert.ok(action,difficulty);
assert.equal(action.type,'coalitionAnswer',difficulty);
assert.equal(action.accept,true,difficulty);
assert.doesNotThrow(()=>send(game,'a',action),difficulty);
}
game=answer(game,'a',false);
assert.equal(game.coalitionState!.proposal,null);
assert.ok(!game.alliancePromises!.find(row=>row.event===event)!.released);
const guide=viewGame(game,'a').alliancePromiseGuide; assert.ok(guide);
assert.equal(guide.action.type,'coalitionLeave');
game=send(game,'a',guide.action);
assert.equal(coalitionOf(game.players,game.coalitionState,'a'),null);
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'b')!.members,['b','c']);
assert.ok(!game.alliancePromises!.find(row=>row.event===event)!.released);
game=send(game,'a',{type:'ready'});
assert.ok(game.ready.includes('a'));
});
void test('Yes to an existing ally and No to the entrant constrain only their respondent’s consent',()=>{
const fixture=group();
let game=ask(fixture.game,fixture.cards[0],'b','yes');
game=ask(game,fixture.cards[1],'d','no');
const events=game.alliancePromises!.filter(row=>row.player==='a').map(row=>row.event);
assert.equal(events.length,2);
game=propose(game,'c');
rejectedAcceptance(game,'a');
// Other participants may approve: the promise is A's, and those approvals
// neither constitute A's consent nor complete the larger alliance.
game=answer(game,'b',true);
game=answer(game,'d',true);
assert.ok(game.coalitionState!.proposal);
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'a')!.members,['a','b','c']);
rejectedAcceptance(game,'a');
assert.ok(events.every(event=>!game.alliancePromises!.find(row=>row.event===event)!.released));
botDeclines(game,'a');
game=answer(game,'a',false);
assert.equal(game.coalitionState!.proposal,null);
assert.ok(events.every(event=>!game.alliancePromises!.find(row=>row.event===event)!.released));
game=send(game,'a',{type:'ready'});
assert.ok(game.ready.includes('a'));
});