Respect Fremen group revival grant for bots
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
e439f3f7b6
commit
f9ca7ed9a9
6 files changed
+203
-7
No files matched your search
@@ -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'),'');
|
||||
});
|
||||
Reference in new issue
Block a user