126 lines
6.6 KiB
TypeScript
126 lines
6.6 KiB
TypeScript
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'));
|
||
});
|