Respect Fremen group revival grant for bots
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 08:46:52 +11:00
1 parent e439f3f7b6
commit f9ca7ed9a9
6 files changed
+203 -7

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
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'),'');
});