Files
Dune/tests/bot-revival-timing.test.ts
2026-09-08 07:37:52 +10:00

142 lines
5.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
const player = (g: Game, id: string) => g.players.find((p) => p.id === id)!;
function fixture(emperorTanks: number, tleilaxu = false) {
const g = createGame(
'REVIVALBOTORDER',
newPlayer('c', 'CHOAM', 'choam'),
true,
);
g.players.push(newPlayer('e', 'Emperor', 'emperor'));
if (tleilaxu) g.players.push(newPlayer('t', 'Tleilaxu', 'tleilaxu'));
Object.assign(g, {
status: 'playing',
phase: 4,
turn: 3,
order: g.players.map((p) => p.id),
storm: 18,
deck: baseDeck(),
});
for (const p of g.players) p.spice = 20;
player(g, 'c').tanks = 1;
player(g, 'c').reserves = 19;
player(g, 'e').tanks = emperorTanks;
player(g, 'e').reserves = 20 - emperorTanks;
player(g, 'e').elites = { reserves: 5, tanks: 0, forces: {}, revived: 0 };
const card = g.deck.splice(
g.deck.findIndex((c) => c.effect === 'karama'),
1,
)[0];
player(g, tleilaxu ? 't' : 'e').hand.push(card);
return { g, card };
}
function proposals(g: Game, id: string, difficulty: Difficulty) {
const view = viewGame(g, id);
view.players.find((p) => p.id === id)!.bot = difficulty;
const before = structuredClone(view);
const actions = botActions(view);
assert.deepEqual(view, before);
return actions;
}
function allow(g: Game) {
for (let i = 0; i < 30 && g.response; i++)
g = applyAction(
g,
g.players.find((p) => !g.response!.passed.includes(p.id))!.id,
{ type: 'passResponse' },
);
assert.equal(g.response, null);
return g;
}
void test('pending CHOAM revival suppresses Emperor special Karama until settlement at all four AI levels', () => {
const cases: [Difficulty, number][] = [
['Easy', 1],
...DIFFICULTIES.map((d): [Difficulty, number] => [d, 3]),
];
for (const [difficulty, count] of cases) {
const { g: start, card } = fixture(count);
const g = applyAction(start, 'c', { type: 'revive', amount: 1 });
assert.equal(g.response?.kind, 'choamRevival');
assert.equal(viewGame(g, 'e').revival.pending, true);
assert.equal('pendingRevival' in viewGame(g, 'e'), false);
const actions = proposals(g, 'e', difficulty);
assert.ok(actions.length);
assert.ok(
actions.every((a) => !(a.type === 'card' && a.mode === 'special')),
);
for (const action of actions)
applyAction(JSON.parse(JSON.stringify(g)), 'e', action);
assert.throws(
() =>
applyAction(g, 'e', {
type: 'card',
mode: 'special',
card: card.id,
amount: count,
elite: 0,
}),
/pending revival/,
);
const settled = allow(g);
assert.equal(viewGame(settled, 'e').revival.pending, false);
const resumed = proposals(settled, 'e', difficulty);
const special = resumed.find(
(a) => a.type === 'card' && a.mode === 'special',
);
assert.ok(special, difficulty + '/' + count);
const next = applyAction(settled, 'e', special);
assert.equal(player(next, 'e').tanks, 0);
assert.equal(player(next, 'e').specialKaramaUsed, true);
assert.equal(next.discard.filter((c) => c.id === card.id).length, 1);
}
});
void test('the pending marker preserves ordinary Karama cancellation and its distinct current price outcome', () => {
const { g: start, card } = fixture(3);
const g = applyAction(start, 'c', { type: 'revive', amount: 1 });
assert.ok(viewGame(g, 'e').responseControls?.cancelCards.includes(card.id));
const canceled = allow(
applyAction(g, 'e', { type: 'card', card: card.id, mode: 'cancel' }),
);
assert.equal(player(canceled, 'c').tanks, 0);
assert.equal(player(canceled, 'c').spice, 18);
assert.equal(player(canceled, 'e').tanks, 3);
assert.equal(player(canceled, 'e').specialKaramaUsed, undefined);
assert.equal(viewGame(canceled, 'e').revival.pending, false);
assert.equal(canceled.discard.filter((c) => c.id === card.id).length, 1);
});
void test('Tleilaxu retains its authorized special Karama during the actual revival-stop decision at all levels', () => {
for (const difficulty of DIFFICULTIES) {
const { g: start, card } = fixture(3, true);
player(start, 'c').tanks = 12;
player(start, 'c').reserves = 8;
const g = applyAction(start, 'c', { type: 'revive', amount: 1 });
assert.equal(g.decision?.kind, 'revivalStop');
assert.equal(viewGame(g, 't').revival.pending, true);
const actions = proposals(g, 't', difficulty);
assert.equal(actions.length, 1);
assert.equal(actions[0].type, 'card');
assert.equal(actions[0].mode, 'special');
const next = applyAction(JSON.parse(JSON.stringify(g)), 't', actions[0]);
assert.equal(player(next, 't').specialKaramaUsed, true);
assert.equal(player(next, 'c').tanks, 12);
assert.equal(player(next, 'c').spice, 20);
assert.equal(viewGame(next, 'c').revival.prevented, true);
assert.equal(viewGame(next, 't').revival.pending, false);
assert.equal(next.discard.filter((c) => c.id === card.id).length, 1);
}
});