Files
Dune/tests/coalition-fremen-bot-grant.test.ts
T
Quintine f9ca7ed9a9
Verify and publish checkpoint container / container (push) Canceled after 0s
Respect Fremen group revival grant for bots
2026-10-29 08:46:52 +11:00

164 lines
7.9 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 { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { FremenRevivalGrant } from '../components/fremen-revival-grant';
import { baseDeck } from '../game/cards';
import { botActions, runBots } from '../game/bots';
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
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 seat = (game:Game, id:string) => {
const player = game.players.find(candidate => candidate.id === id); assert.ok(player); return player;
};
/** Real Nexus proposals, acceptance and group admission. The original ordinary
* phase, force pools, wallets and holdings are staged at Revival, with tanks
* moved from reserves and the counter card taken from the physical deck. */
function fixture(difficulty:Difficulty, bot:'a'|'h' = 'a', grouped = true) {
let game = createGame('GROUPFREMENBOT',newPlayer('f','Fremen','fremen'),false);
game.players.push(newPlayer('a','Atreides','atreides'),newPlayer('h','Harkonnen','harkonnen'),
newPlayer('g','Guild','guild'));
Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,order:['f','a','h','g'],
ready:[],storm:18,deck:baseDeck(),response:null,decision:null,phaseOpening:null});
game.coalitionState = emptyCoalitionState();
game = act(game,'f',{type:'alliance',target:'a'});
game = act(game,'a',{type:'alliance',target:'f'});
if (grouped) {
const group = coalitionOf(game.players,game.coalitionState,'f'); assert.ok(group);
game = act(game,'f',{type:'coalitionPropose',groupId:group.id,entrant:'h',
expectedRevision:game.coalitionState!.revision});
for (const id of ['a','h']) {
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});
}
}
Object.assign(game,{phase:4,nexus:false,ready:[],response:null,decision:null});
for (const player of game.players) {
player.forces = {}; player.reserves = 20; player.tanks = 0; player.spice = 10;
player.hand = []; player.traitors = []; player.traitorChoices = [];
}
for (const id of ['a','h']) { seat(game,id).reserves = 16; seat(game,id).tanks = 4; }
const index = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
const [karama] = game.deck.splice(index,1); seat(game,'g').hand.push(karama);
game = act(game,bot,{type:'setAutopilot',difficulty});
return {game,bot,karama:karama.id};
}
function pass(game:Game):Game {
for (let count = 0; game.response && count < 24; count++) {
const player = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(player);
game = act(game,player.id,{type:'passResponse'});
}
assert.equal(game.response ?? null,null); return game;
}
for (const difficulty of DIFFICULTIES) for (const bot of ['a','h'] as const) void test(
`${difficulty} group ally ${bot} waits for human Fremen and uses the granted free revival`, () => {
let {game} = fixture(difficulty,bot);
const priorSpice = seat(game,bot).spice, priorTanks = seat(game,bot).tanks;
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
assert.equal(viewGame(game,'f').fremenGroupRevivalGrant?.find(row => row.recipient === bot)?.blocked,null);
game = runBots(game,1);
assert.equal(seat(game,bot).tanks,priorTanks);
assert.equal(seat(game,bot).spice,priorSpice);
assert.equal(seat(game,bot).revived,0);
assert.ok(!game.ready.includes(bot));
game = act(game,'f',{type:'grantRevival',target:bot});
assert.equal(game.response?.kind,'fremenRevivalGrant');
game = pass(game);
assert.equal(viewGame(game,bot).revival.freeRemaining,3);
assert.ok(botActions(viewGame(game,bot)).some(action => action.type === 'revive' && action.amount === 3));
game = runBots(game,1);
assert.equal(seat(game,bot).tanks,priorTanks - 3);
assert.equal(seat(game,bot).spice,priorSpice);
assert.equal(seat(game,bot).freeForcesRevived,3);
},
);
void test('Fremen Ready declines further group grants and releases the waiting ally', () => {
const {game: initial,bot} = fixture('Easy'); let game = initial;
game = runBots(game,1); assert.equal(seat(game,bot).revived,0);
game = act(game,'f',{type:'ready'}); assert.ok(game.ready.includes('f'));
game = runBots(game,1);
assert.ok(seat(game,bot).revived > 0 || game.ready.includes(bot));
});
void test('actual Karama prevents one group grant and releases that waiting ally', () => {
const {game: initial,bot,karama} = fixture('Medium'); let game = initial;
game = act(game,'h',{type:'setAutopilot',difficulty:'Hard'});
game = act(game,'f',{type:'grantRevival',target:bot});
assert.equal(game.response?.kind,'fremenRevivalGrant');
game = act(game,'g',{type:'card',card:karama,mode:'cancel'});
assert.equal(game.response ?? null,null);
assert.ok(game.revivalRules?.fremenGrantBlocked?.includes(bot));
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
game = runBots(game,1);
assert.ok(seat(game,bot).revived > 0 || game.ready.includes(bot));
assert.equal(seat(game,'h').revived,0);
assert.ok(!game.ready.includes('h'));
});
void test('an automated non-ally does not wait for a group grant it cannot receive', () => {
let {game} = fixture('Easy');
game = act(game,'g',{type:'setAutopilot',difficulty:'Hard'});
seat(game,'g').reserves = 16; seat(game,'g').tanks = 4;
game = runBots(game,1);
assert.ok(seat(game,'g').revived > 0 || game.ready.includes('g'));
assert.equal(seat(game,'a').revived,0);
});
void test('a group ally with only its two already-free forces in Tanks does not wait', () => {
const {game: initial,bot} = fixture('Easy'); let game = initial;
// Move surplus counters back to the same native reserve pool.
seat(game,bot).reserves += 2; seat(game,bot).tanks -= 2;
assert.equal(viewGame(game,bot).revival.freeRemaining,2);
game = runBots(game,1);
assert.equal(seat(game,bot).tanks,0);
assert.equal(seat(game,bot).revived,2);
});
void test('a normal pair in a coalition-enabled game also waits for the human grant', () => {
const {game: initial,bot} = fixture('Easy','a',false); let game = initial;
assert.ok(game.coalitionState);
assert.equal(seat(game,'f').ally,bot);
game = runBots(game,1);
assert.equal(seat(game,bot).revived,0);
assert.ok(!game.ready.includes(bot));
});
void test('two automated group allies both wait for the human decision', () => {
let {game} = fixture('Easy','a');
game = act(game,'h',{type:'setAutopilot',difficulty:'Hard'});
game = runBots(game,12);
for (const id of ['a','h']) {
assert.equal(seat(game,id).revived,0);
assert.equal(seat(game,id).tanks,4);
assert.ok(!game.ready.includes(id));
}
game = act(game,'f',{type:'grantRevival',target:'a'});
game = pass(game);
game = runBots(game,1);
assert.equal(seat(game,'a').revived,3);
assert.equal(seat(game,'h').revived,0);
assert.ok(!game.ready.includes('h'));
});
void test('group grant guidance is visible only to Fremen and does not disclose an ally’s Tanks', () => {
const {game} = fixture('Easy');
const markup = (id:string) => renderToStaticMarkup(createElement(FremenRevivalGrant, {
game:viewGame(game,id),busy:false,act() {},
}));
const before = markup('f');
assert.match(before,/When a grant would help an automated ally, it waits to revive forces/);
assert.match(before,/Grant Atreides free revival/);
assert.doesNotMatch(before,/Tanks|four forces|4 forces/);
seat(game,'a').reserves += 2; seat(game,'a').tanks -= 2;
assert.equal(markup('f'),before);
assert.equal(markup('a'),'');
assert.equal(markup('g'),'');
});