Files
Dune/tests/richese-acquisition-bots.test.ts

188 lines
5.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
createGame,
newPlayer,
viewGame,
type Game,
type GameView,
} from '../game/engine';
import { baseDeck } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES, type Difficulty } from '../game/bot-profiles';
function fixture() {
const g = createGame('RICHESEACQUIRE', newPlayer('r', 'Richese', 'richese'));
g.players.push(
newPlayer('e', 'Emperor', 'emperor'),
newPlayer('f', 'Fremen', 'fremen'),
);
g.status = 'playing';
g.advanced = true;
g.phase = 2;
g.turn = 2;
g.order = ['r', 'e', 'f'];
g.active = null;
g.deck = baseDeck();
g.richeseCache = richeseCards();
for (const p of g.players) {
p.hand = [];
p.spice = 12;
p.forces = {};
p.traitors = [];
}
const index = g.deck.findIndex((card) => card.effect === 'karama');
g.players[0].hand.push(g.deck.splice(index, 1)[0]);
return g;
}
function projected(g: Game, difficulty: Difficulty, id = 'r') {
const v = viewGame(g, id);
v.players.find((p) => p.id === id)!.bot = difficulty;
return v;
}
function acquisition(v: GameView) {
return botActions(v).find(
(action) =>
action.type === 'card' &&
action.mode === 'special' &&
typeof action.acquire === 'string',
);
}
void test('all four profiles acquire only usable canonical Karama and settle once in the authoritative engine', () => {
for (const difficulty of DIFFICULTIES) {
const original = fixture();
const view = projected(original, difficulty);
const before = structuredClone(view);
const action = acquisition(view);
assert.deepEqual(view, before, 'policy does not mutate its view');
assert.ok(action, difficulty);
assert.equal(action.acquire, 'richese-karama');
assert.equal(action.card, original.players[0].hand[0].id);
let g = applyAction(original, 'r', action);
for (let step = 0; g.response && step < 8; step++) {
const responder = g.players
.map((p) => ({
id: p.id,
action: botActions(projected(g, difficulty, p.id))[0],
}))
.find((p) => p.action);
assert.ok(responder, 'income has a legal continuation');
g = applyAction(g, responder.id, responder.action);
}
assert.equal(g.response, null, 'income completes finitely');
assert.equal(g.players[0].spice, 9);
assert.equal(g.players[1].spice, 15);
assert.equal(g.players[0].specialKaramaUsed, true);
assert.deepEqual(
g.players[0].hand.map((c) => c.id),
['richese-karama'],
);
assert.equal(g.richeseCache!.length, 9);
assert.equal(g.discard.filter((c) => c.id === action.card).length, 1);
assert.equal(
acquisition(projected(JSON.parse(JSON.stringify(g)), difficulty)),
undefined,
);
assert.equal(original.players[0].spice, 12);
}
});
void test('all profiles decline unavailable, used, basic and low-budget purchases', () => {
for (const difficulty of DIFFICULTIES) {
for (const change of [
(g: Game) => {
g.advanced = false;
},
(g: Game) => {
g.players[0].specialKaramaUsed = true;
},
(g: Game) => {
g.players[0].spice = 3;
},
(g: Game) => {
g.players[0].hand = [];
},
(g: Game) => {
g.richeseCache = g.richeseCache!.filter((c) => c.effect !== 'karama');
},
]) {
const g = fixture();
change(g);
assert.equal(
acquisition(projected(g, difficulty)),
undefined,
difficulty,
);
}
const v = projected(fixture(), difficulty);
v.richeseSpecialKarama!.blocked = 'Resolve the pending operation first.';
assert.equal(acquisition(v), undefined);
}
});
void test('all profiles use only projected choices and reject a similarly named unsupported face', () => {
for (const difficulty of DIFFICULTIES) {
const g = fixture();
assert.equal(viewGame(g, 'e').richeseSpecialKarama, null);
const v = projected(g, difficulty);
const expected = acquisition(v);
v.players.find((p) => p.id === 'e')!.spice = 1000;
v.players.find((p) => p.id === 'f')!.hand = baseDeck();
assert.deepEqual(
acquisition(v),
expected,
'rival secret data does not inform selection',
);
v.richeseSpecialKarama!.cards = [
{ ...richeseCards().find((c) => c.effect === 'karama')!, id: 'impostor' },
];
assert.equal(acquisition(v), undefined);
}
});
void test('response, decision and Truthtrance controls retain priority over optional acquisition', () => {
for (const difficulty of DIFFICULTIES) {
const g = fixture();
g.players[2].hand.push(
g.deck.splice(
g.deck.findIndex((c) => c.effect === 'karama'),
1,
)[0],
);
const action = acquisition(projected(g, difficulty))!;
const purchased = applyAction(g, 'r', action);
assert.equal(purchased.response?.kind, 'richesePurchaseIncome');
assert.equal(acquisition(projected(purchased, difficulty)), undefined);
const blocker = projected(purchased, difficulty, 'f');
assert.ok(
botActions(blocker).some(
(a) => a.type === 'passResponse' || a.mode === 'cancel',
),
);
const v = projected(fixture(), difficulty);
v.decision = { kind: 'richeseDeclaration', player: 'r' };
v.richeseBidding = {
owner: 'r',
event: 'special-test-declaration',
stage: 'declaration',
position: null,
cache: [],
normalCount: null,
cacheTerms: null,
offerBlocked: null,
};
assert.equal(botActions(v)[0].type, 'decision');
assert.equal(acquisition(v), undefined);
v.decision = null;
v.truthtrance = {
stage: 'priority',
passed: [],
queue: [],
} as unknown as NonNullable<GameView['truthtrance']>;
assert.equal(botActions(v)[0].type, 'truthPass');
}
});