141 lines
7.0 KiB
TypeScript
141 lines
7.0 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { viewGame, type Game } from '../game/engine';
|
||
import { botActions, runBots } from '../game/bots';
|
||
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
|
||
import { actFremenRevivalGrant as act, advanceFremenRevivalGrant as advance,
|
||
createFremenRevivalGrantFixture, fremenRevivalGrantClean as clean,
|
||
fremenRevivalGrantPlayer as player, passFremenRevivalGrantResponses as pass } from './fixture-fremen-revival-grant';
|
||
|
||
let fixture: ReturnType<typeof createFremenRevivalGrantFixture> | undefined;
|
||
const source = () => fixture ??= createFremenRevivalGrantFixture();
|
||
|
||
/** The source uses real public Basic setup, an actual battle with six allied
|
||
* casualties and reciprocal Nexus consent. Handover uses the real seat action. */
|
||
function botAlly(difficulty: Difficulty) {
|
||
const f = source();
|
||
const game = act(structuredClone(f.game), f.ally, {type:'setAutopilot', difficulty});
|
||
assert.equal(game.advanced, false);
|
||
assert.equal(game.phase, 4);
|
||
assert.equal(player(game, f.fremen).ally, f.ally);
|
||
assert.equal(player(game, f.ally).ally, f.fremen);
|
||
assert.ok(player(game, f.ally).tanks >= 3);
|
||
assert.equal(viewGame(game, f.ally).revival.freeRemaining, 2);
|
||
assert.equal(viewGame(game, f.fremen).fremenRevivalGrant?.blocked, null);
|
||
return {f, game};
|
||
}
|
||
const ownChoices = (game: Game, id: string) => botActions(viewGame(game, id));
|
||
|
||
for (const difficulty of DIFFICULTIES) void test(
|
||
`Basic ${difficulty} ally waits for human Fremen, then uses the granted free revival`, () => {
|
||
const {f, game} = botAlly(difficulty);
|
||
const spice = player(game, f.ally).spice, tanks = player(game, f.ally).tanks;
|
||
const afterBot = runBots(game, 1);
|
||
assert.equal(player(afterBot, f.ally).spice, spice);
|
||
assert.equal(player(afterBot, f.ally).tanks, tanks);
|
||
assert.equal(player(afterBot, f.ally).revived, 0);
|
||
assert.ok(!afterBot.ready.includes(f.ally));
|
||
let next = act(afterBot, f.fremen, f.grant);
|
||
assert.equal(next.response?.kind, 'fremenRevivalGrant');
|
||
assert.equal(next.freeRevival.includes(f.ally), false);
|
||
next = pass(next);
|
||
assert.equal(viewGame(next, f.ally).revival.freeRemaining, 3);
|
||
const revival = ownChoices(next, f.ally).find(action => action.type === 'revive' && action.amount === 3);
|
||
assert.ok(revival, `${difficulty} should offer its legal three-force revival`);
|
||
next = advance(runBots(next, 1), clean);
|
||
assert.equal(player(next, f.ally).spice, spice);
|
||
assert.equal(player(next, f.ally).tanks, tanks - 3);
|
||
assert.equal(player(next, f.ally).freeForcesRevived, 3);
|
||
},
|
||
);
|
||
|
||
void test('Fremen’s normal Ready releases the waiting bot without granting the benefit', () => {
|
||
const {f, game} = botAlly('Easy');
|
||
const waiting = runBots(game, 1);
|
||
assert.equal(player(waiting, f.ally).revived, 0);
|
||
assert.ok(!waiting.ready.includes(f.ally));
|
||
const next = act(waiting, f.fremen, {type:'ready'});
|
||
assert.ok(next.ready.includes(f.fremen));
|
||
assert.equal(next.freeRevival.includes(f.ally), false);
|
||
const resumed = runBots(next, 1);
|
||
assert.ok(player(resumed, f.ally).revived > 0 || resumed.ready.includes(f.ally));
|
||
});
|
||
|
||
void test('a real Karama prevention releases the waiting bot with its intrinsic free rate', () => {
|
||
const {f, game} = botAlly('Medium');
|
||
let next = act(game, f.fremen, f.grant);
|
||
assert.equal(next.response?.kind, 'fremenRevivalGrant');
|
||
next = act(next, f.observer, {type:'card', card:f.karama, mode:'cancel'});
|
||
assert.equal(next.response, null);
|
||
assert.ok(next.revivalRules?.fremenGrantBlocked?.includes(f.ally));
|
||
assert.equal(viewGame(next, f.ally).revival.freeRemaining, 2);
|
||
const resumed = runBots(next, 1);
|
||
assert.ok(player(resumed, f.ally).revived > 0 || resumed.ready.includes(f.ally));
|
||
});
|
||
|
||
void test('the waiting ally can still pass its actual Karama response to the Fremen grant', () => {
|
||
const {f, game} = botAlly('Easy');
|
||
// Stage one conserved physical card exchange solely to make the allied
|
||
// bot, rather than the original Emperor, own the response card.
|
||
const ally = player(game, f.ally), observer = player(game, f.observer);
|
||
const karama = observer.hand.findIndex(card => card.id === f.karama);
|
||
assert.ok(karama >= 0 && ally.hand.length > 0);
|
||
[ally.hand[0], observer.hand[karama]] = [observer.hand[karama], ally.hand[0]];
|
||
let next = act(game, f.fremen, f.grant);
|
||
assert.equal(next.response?.kind, 'fremenRevivalGrant');
|
||
next = runBots(next, 1);
|
||
assert.equal(next.response, null);
|
||
assert.equal(next.freeRevival.includes(f.ally), true);
|
||
});
|
||
|
||
void test('a bot Fremen does not make its bot ally wait for a human grant', () => {
|
||
const {f, game} = botAlly('Easy');
|
||
const next = act(game, f.fremen, {type:'setAutopilot', difficulty:'Easy'});
|
||
// Staged actor iteration order puts the allied bot first; public storm order
|
||
// and the genuine prior game state are otherwise untouched.
|
||
next.players = [player(next, f.ally), player(next, f.fremen), player(next, f.observer)];
|
||
const resumed = runBots(next, 1);
|
||
assert.ok(player(resumed, f.ally).revived > 0 || resumed.ready.includes(f.ally));
|
||
});
|
||
|
||
void test('a completed normal revival does not hold the bot’s Ready for an unusable grant', () => {
|
||
// This fixture actually completes its paid/free three-force return before
|
||
// handover; the remaining ordinary revival allowance is genuinely zero.
|
||
const f = createFremenRevivalGrantFixture({priorRevival:true});
|
||
const game = act(f.game, f.ally, {type:'setAutopilot', difficulty:'Easy'});
|
||
assert.equal(viewGame(game, f.ally).revival.forcesRemaining, 0);
|
||
const resumed = runBots(game, 1);
|
||
assert.ok(resumed.ready.includes(f.ally));
|
||
});
|
||
|
||
void test('two remaining Tanks forces do not hold a bot that already has two free revivals', () => {
|
||
const {f, game} = botAlly('Easy');
|
||
const ally = player(game, f.ally);
|
||
// Labelled physical-custody staging: return the surplus ordinary Tanks
|
||
// counters to reserves, preserving the player's total force inventory.
|
||
const surplus = ally.tanks - 2;
|
||
assert.ok(surplus > 0);
|
||
ally.tanks -= surplus;
|
||
ally.reserves += surplus;
|
||
const reserves = ally.reserves, spice = ally.spice;
|
||
assert.equal(viewGame(game, f.ally).revival.freeRemaining, 2);
|
||
assert.equal(viewGame(game, f.fremen).fremenRevivalGrant?.blocked, null);
|
||
const resumed = runBots(game, 1);
|
||
assert.equal(player(resumed, f.ally).revived, 2);
|
||
assert.equal(player(resumed, f.ally).tanks, 0);
|
||
assert.equal(player(resumed, f.ally).reserves, reserves + 2);
|
||
assert.equal(player(resumed, f.ally).spice, spice);
|
||
});
|
||
|
||
void test('a bot barred from normal revival can Ready without waiting for a futile grant', () => {
|
||
const {f, game} = botAlly('Easy');
|
||
// Stage the existing current-turn native prevention receipt, without
|
||
// changing the genuine alliance, Tanks, wallet or Fremen grant state.
|
||
game.revivalPrevention = {player:f.ally, turn:game.turn};
|
||
assert.equal(viewGame(game, f.ally).revival.prevented, true);
|
||
const resumed = runBots(game, 1);
|
||
assert.ok(resumed.ready.includes(f.ally));
|
||
assert.equal(player(resumed, f.ally).revived, 0);
|
||
assert.equal(player(resumed, f.ally).tanks, player(game, f.ally).tanks);
|
||
});
|