Files
Dune/tests/coalition-admission-joint-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

67 lines
3.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { applyAction, createGame, newPlayer, type Action, type Game } from '../game/engine';
const act = (game: Game, actor: string, action: Action) => applyAction(game, actor, action);
/** A player's retained partner and unrelated bribe remain jointly feasible.
* The empty joint ledger is significant: it delegates accepted-order
* reconciliation to the scalar alliance planner. */
function fixture(): Game {
let game = createGame('ADMISSIONJOINT', newPlayer('a', 'Atreides', 'atreides'), false);
game.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'),
newPlayer('d', 'Harkonnen', 'harkonnen'), newPlayer('e', 'Guild', 'guild'));
Object.assign(game, {status:'playing', phase:1, turn:2, nexus:true,
order:['a','b','c','d','e'], ready:[]});
game.coalitionState = emptyCoalitionState('unlimited');
game.players.find(player => player.id === 'a')!.spice = 8;
game = act(game, 'a', {type:'alliance', target:'b'});
game = act(game, 'b', {type:'alliance', target:'a'});
const source = JSON.stringify(['ordinaryNexus', game.turn, game.spiceDiscard.map(pile => pile.length),
game.spiceSequence?.pile ?? null, game.spiceSequence?.skipped?.length ?? 0]);
game.alliancePromises = [{event:'alliance', source, player:'a', asker:'e', turn:game.turn,
partner:'b', answer:true}];
game.bribePromises = [{event:'bribe', player:'a', asker:'e', turn:game.turn,
recipient:'e', minimum:1, answer:true, progress:0}];
game.turnActionPromises = [];
return game;
}
function proposal(game: Game) {
const group = coalitionOf(game.players, game.coalitionState, 'a'); assert.ok(group);
return {type:'coalitionPropose', groupId:group.id, entrant:'c',
expectedRevision:game.coalitionState!.revision};
}
function answer(game: Game, accept: boolean) {
const offer = game.coalitionState!.proposal; assert.ok(offer);
return {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus, turn:offer.turn,
accept, expectedRevision:game.coalitionState!.revision};
}
function commitmentsLive(game: Game) {
assert.equal(game.bribePromises![0].released ?? false, false);
assert.equal(game.alliancePromises![0].released ?? false, false);
}
void test('joint accepted-order promises cannot be released by group proposal or approval', () => {
let game = fixture();
const before = structuredClone(game);
assert.throws(() => act(game, 'a', proposal(game)));
assert.deepEqual(game, before);
commitmentsLive(game);
// Another member may propose; the affected player can still decline, but
// cannot approve a group while the scalar reconciliation is unsupported.
game = act(game, 'b', proposal(game));
commitmentsLive(game);
game = act(game, 'c', answer(game, true));
commitmentsLive(game);
const pending = structuredClone(game);
assert.throws(() => act(game, 'a', answer(game, true)));
assert.deepEqual(game, pending);
commitmentsLive(game);
game = act(game, 'a', answer(game, false));
commitmentsLive(game);
assert.equal(game.coalitionState!.proposal, null);
});