119 lines
5.7 KiB
TypeScript
119 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { territory } from '../game/board';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { createTechTokens } from '../game/tech-tokens';
|
|
import { bindTruthBribe, createTruthBribeFixture, truthBribePlayer } from './fixture-truth-bribe';
|
|
|
|
/** Original Advanced Harkonnen/Atreides/Emperor setup and native actions reach
|
|
* Collection. Only the conserved force locations are staged to expose a rare
|
|
* victory board; the agreement case also stages a complete physical Tech set,
|
|
* and the final-turn case stages turn ten. This is not a naturally earned
|
|
* late-game board, a save/recovery test, or full module acceptance. */
|
|
function position(options: { holds?: number; simultaneous?: boolean; finalTurn?: boolean } = {}) {
|
|
const source = createTruthBribeFixture({ advanced: true, phase: 7 });
|
|
const game = structuredClone(source.game);
|
|
for (const player of game.players) {
|
|
player.reserves += Object.values(player.forces).reduce((sum, count) => sum + count, 0);
|
|
player.forces = {};
|
|
if (player.elites) {
|
|
player.elites.reserves += Object.values(player.elites.forces).reduce((sum, count) => sum + count, 0);
|
|
player.elites.forces = {};
|
|
}
|
|
}
|
|
const assign = (id: string, names: string[]) => {
|
|
const player = truthBribePlayer(game, id);
|
|
for (const name of names) player.forces[`${name}:${territory(name).sectors[0]}`] = 1;
|
|
player.reserves -= names.length;
|
|
};
|
|
const holds = options.holds ?? 3;
|
|
assign('atreides', ['arrakeen', 'carthag', 'sietch_tabr'].slice(0, holds));
|
|
if (options.simultaneous) {
|
|
assign('emperor', ['tueks_sietch', 'habbanya_ridge_sietch']);
|
|
game.techTokens = createTechTokens();
|
|
for (const token of Object.values(game.techTokens)) token.owner = 'emperor';
|
|
}
|
|
if (options.finalTurn) game.turn = 10;
|
|
return { source, game };
|
|
}
|
|
|
|
function enterMentat(initial: Game) {
|
|
let game = initial;
|
|
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
|
assert.equal(game.phase, 8);
|
|
assert.equal(game.status, 'playing');
|
|
assert.deepEqual(game.winner, []);
|
|
assert.equal(game.victoryContinuation, undefined);
|
|
return game;
|
|
}
|
|
function finishMentat(initial: Game) {
|
|
let game = initial;
|
|
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
|
return game;
|
|
}
|
|
|
|
void test('Advanced winner waits through a real Truthtrance and its required Mentat payment', () => {
|
|
const f = position();
|
|
let game = enterMentat(f.game);
|
|
const bound = bindTruthBribe(f.source, 'yes', 3, f.source.recipient, game);
|
|
game = bound.game;
|
|
assert.throws(() => applyAction(game, f.source.payer, { type: 'ready' }), /Truthtrance|bribe|payment/i);
|
|
const guide = viewGame(game, f.source.payer).bribePromiseGuide;
|
|
assert.ok(guide);
|
|
const payerSpice = truthBribePlayer(game, f.source.payer).spice;
|
|
game = applyAction(game, f.source.payer, guide.action);
|
|
assert.equal(truthBribePlayer(game, f.source.payer).spice, payerSpice - 3);
|
|
assert.equal(truthBribePlayer(game, f.source.recipient).bribes, 3);
|
|
game = finishMentat(game);
|
|
assert.equal(game.status, 'finished');
|
|
assert.deepEqual(game.winner, ['atreides']);
|
|
});
|
|
|
|
void test('Advanced Mentat with no winner advances once only after every player is ready', () => {
|
|
let game = enterMentat(position({ holds: 2 }).game);
|
|
const turn = game.turn;
|
|
for (const player of game.players.slice(0, -1)) {
|
|
game = applyAction(game, player.id, { type: 'ready' });
|
|
assert.equal(game.phase, 8); assert.equal(game.turn, turn);
|
|
}
|
|
game = applyAction(game, game.players.at(-1)!.id, { type: 'ready' });
|
|
assert.equal(game.status, 'playing'); assert.equal(game.turn, turn + 1); assert.equal(game.phase, 0);
|
|
assert.deepEqual(game.winner, []);
|
|
});
|
|
|
|
void test('Advanced final-turn fallback is checked after Mentat opportunities', () => {
|
|
const game = enterMentat(position({ holds: 2, finalTurn: true }).game);
|
|
const settled = finishMentat(game);
|
|
assert.equal(settled.turn, 10); assert.equal(settled.status, 'finished');
|
|
assert.deepEqual(settled.winner, ['atreides']);
|
|
});
|
|
|
|
void test('Advanced simultaneous qualifiers vote after Mentat and a unanimous agreement advances once', () => {
|
|
const game = finishMentat(enterMentat(position({ simultaneous: true }).game));
|
|
const offer = game.victoryContinuation;
|
|
assert.ok(offer); assert.equal(offer.resume, 'advance');
|
|
assert.deepEqual(new Set(offer.voters), new Set(['atreides', 'emperor']));
|
|
let next = applyAction(game, offer.voters[0], { type: 'victoryContinuation', event: offer.event, mode: 'continue', extra: 1 });
|
|
for (const voter of offer.voters.slice(1))
|
|
next = applyAction(next, voter, { type: 'victoryContinuation', event: offer.event, mode: 'accept' });
|
|
assert.equal(next.status, 'playing'); assert.equal(next.turn, game.turn + 1); assert.equal(next.phase, 0);
|
|
assert.equal(next.victoryExtraStrongholds, 1); assert.ok(!next.victoryContinuation);
|
|
});
|
|
|
|
void test('all four minimal bot profiles can finish Advanced Mentat before victory', () => {
|
|
const initial = enterMentat(position().game);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
let game = structuredClone(initial);
|
|
for (const player of game.players) {
|
|
const view = viewGame(game, player.id);
|
|
view.players.find(seat => seat.id === player.id)!.bot = difficulty;
|
|
const action = botActions(view).find(candidate => candidate.type === 'ready');
|
|
assert.ok(action, `${difficulty} can finish ${player.id}'s Mentat opportunity`);
|
|
game = applyAction(game, player.id, action);
|
|
}
|
|
assert.equal(game.status, 'finished'); assert.deepEqual(game.winner, ['atreides']);
|
|
}
|
|
});
|