120 lines
6.2 KiB
TypeScript
120 lines
6.2 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { ownedTech, TECH_TOKENS } from '../game/tech-tokens';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { nextNativeTechFactionsStep } from './fixture-native-tech-factions';
|
|
import { basicSemutaAmbassadorInventory as inventory } from './fixture-basic-semuta-ambassador';
|
|
|
|
const seat = (game: Game, id: string) => {
|
|
const player = game.players.find(candidate => candidate.id === id);
|
|
assert.ok(player);
|
|
return player;
|
|
};
|
|
const clean = (game: Game) => !game.response && !game.phaseOpening && !game.decision && !game.pendingTreacheryDiscard;
|
|
|
|
/** Run original setup and first Storm; select only safe unopened Spice draws.
|
|
* Once the real movement opportunity is reached, relocate the physical Semuta
|
|
* and Hajr to their users' hands and return those hands' other cards to the
|
|
* deck. No Tech ownership, force, resource or turn state is staged. */
|
|
function fixture() {
|
|
let game = createGame('SEMUTATECHPUBLIC', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
|
|
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
|
joinGame(game, newPlayer('g', 'Guild', 'guild'));
|
|
game = applyAction(game, 'r', { type: 'techTokens', enabled: true });
|
|
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
|
game = applyAction(game, 'r', { type: 'start' });
|
|
assert.equal(game.status, 'setup');
|
|
assert.equal(game.semutaPreview, true);
|
|
const original = inventory(game);
|
|
for (let count = 0; game.status === 'setup' && count < 250; count++) {
|
|
const step = nextNativeTechFactionsStep(game); assert.ok(step);
|
|
game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.equal(game.status, 'playing');
|
|
for (let position = 0; position < 4; position++) {
|
|
const from = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card && !card.discovery);
|
|
assert.ok(from >= position);
|
|
const card = game.spiceDeck.splice(from, 1)[0];
|
|
game.spiceDeck.splice(position, 0, card);
|
|
}
|
|
for (let count = 0; !(game.phase === 5 && game.active === 'a' && clean(game)) && count < 1600; count++) {
|
|
assert.notEqual(game.status, 'finished');
|
|
const step = nextNativeTechFactionsStep(game); assert.ok(step);
|
|
game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.active, 'a');
|
|
assert.ok(clean(game));
|
|
const owners = TECH_TOKENS.map(token => game.techTokens![token.id].owner);
|
|
assert.equal(new Set(owners).size, 3);
|
|
assert.ok(owners.every(owner => game.players.some(player => player.id === owner)));
|
|
for (const id of ['r', 'a']) game.deck.push(...seat(game, id).hand.splice(0));
|
|
const zones = [game.deck, game.discard, game.richeseCache ?? [], game.richeseRemoved ?? [], ...game.players.map(player => player.hand)];
|
|
const take = (match: (card: Game['deck'][number]) => boolean) => {
|
|
for (const zone of zones) {
|
|
const index = zone.findIndex(match);
|
|
if (index >= 0) return zone.splice(index, 1)[0];
|
|
}
|
|
throw Error('The original physical card was not found');
|
|
};
|
|
const semuta = take(card => card.id === SEMUTA_DRUG_ID);
|
|
const hajr = take(card => card.effect === 'hajr');
|
|
seat(game, 'r').hand.push(semuta);
|
|
seat(game, 'a').hand.push(hajr);
|
|
assert.deepEqual(inventory(game), original);
|
|
return { game, original, hajr: hajr.id, tech: structuredClone(game.techTokens!) };
|
|
}
|
|
|
|
for (const claim of [false, true]) {
|
|
void test(`Basic public Tech Semuta ${claim ? 'claims' : 'passes'} a real Hajr discard and continues the shipment`, () => {
|
|
const f = fixture();
|
|
let game = applyAction(f.game, 'a', { type: 'card', card: f.hajr });
|
|
const pending = game.pendingTreacheryDiscard;
|
|
assert.ok(pending?.reaction?.stage === 'offer');
|
|
assert.equal(pending.continuation.kind, 'ordinaryCardDiscard');
|
|
assert.equal(viewGame(game, 'r').semutaReaction?.canCommit, true);
|
|
assert.deepEqual(game.techTokens, f.tech);
|
|
const event = pending.batch.event;
|
|
if (claim) game = applyAction(game, 'r', { type: 'semutaCommit', event });
|
|
else for (const player of game.players) game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select')
|
|
game = applyAction(game, 'r', { type: 'semutaSelect', event, card: f.hajr });
|
|
assert.equal(game.pendingTreacheryDiscard, null);
|
|
assert.ok(game.hajr.includes('a'));
|
|
assert.equal(seat(game, 'r').hand.some(card => card.id === f.hajr), claim);
|
|
assert.equal(game.discard.some(card => card.id === SEMUTA_DRUG_ID), claim);
|
|
const owner = game.techTokens!.heighliners.owner!;
|
|
const already = game.techTokens!.heighliners.triggeredTurn === game.turn;
|
|
const spice = already ? game.techTokens!.heighliners.spice : ownedTech(game.techTokens, owner).length;
|
|
const reserves = seat(game, 'a').reserves;
|
|
game = applyAction(game, 'a', { type: 'ship', territory: 'polar_sink', sector: 0, amount: 1 });
|
|
for (let count = 0; !clean(game) && count < 30; count++) {
|
|
const step = nextNativeTechFactionsStep(game); assert.ok(step);
|
|
game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.ok(clean(game));
|
|
assert.equal(seat(game, 'a').reserves, reserves - 1);
|
|
assert.equal(game.techTokens!.heighliners.owner, owner);
|
|
assert.equal(game.techTokens!.heighliners.triggeredTurn, game.turn);
|
|
assert.equal(game.techTokens!.heighliners.spice, spice);
|
|
assert.deepEqual(inventory(game), f.original);
|
|
});
|
|
}
|
|
|
|
void test('all four bot profiles can legally respond to the Basic Tech Semuta offer', () => {
|
|
const f = fixture();
|
|
const pending = applyAction(f.game, 'a', { type: 'card', card: f.hajr });
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(pending, 'r');
|
|
const player = view.players.find(candidate => candidate.id === 'r');
|
|
assert.ok(player);
|
|
player.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
assert.ok(action, difficulty);
|
|
assert.doesNotThrow(() => applyAction(pending, 'r', action));
|
|
}
|
|
});
|