142 lines
5.2 KiB
TypeScript
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);
|
|
}
|
|
});
|