Files
Dune/tests/semuta-tech-public-basic.test.ts
T
Quintine 7a083ac9aa
Verify and publish checkpoint container / container (push) Failing after 10m53s
Extend Truthtrance facts and Basic Semuta Tech
2026-10-29 05:15:04 +11:00

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));
}
});