Files
Dune/tests/semuta-independent-ordinary.test.ts
Quintine 4d769af8c7
Verify and publish checkpoint container / container (push) Successful in 10m7s
Expand Semuta and Richese Homeworld shipments
2026-10-28 22:26:47 +11:00

92 lines
5.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { type Game, viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { stormSectorAfter } from '../game/disaster-rules';
import { actIndependentOrdinary as act, createIndependentOrdinaryFixture as fixture,
advanceIndependentOrdinary as advance, independentOrdinaryInventory as inventory,
independentOrdinaryPlayer as player } from './fixture-semuta-independent-ordinary';
function react(game: Game, holder: string, event: string, claim: boolean) {
if (claim) return act(game, holder, {type: 'semutaCommit', event});
for (const seat of game.players) game = act(game, seat.id, {type: 'semutaPass', event});
return game;
}
for (const effect of ['hajr', 'ghola', 'weather', 'harvester', 'atomics'] as const) for (const claim of [false, true]) {
void test(`Basic independent cards: ${effect} offers Semuta ${claim ? 'claim' : 'pass'} without replay`, () => {
const f = fixture(effect), before = structuredClone(f.game);
const oldPlayer = player(f.game, f.owner);
const pending = act(f.game, f.owner, f.play);
assert.deepEqual(f.game, before);
const frame = pending.pendingTreacheryDiscard; assert.ok(frame);
assert.equal(frame.reaction?.stage, 'offer');
assert.equal(frame.continuation.kind, 'ordinaryCardDiscard');
assert.deepEqual(frame.batch.entries.map(entry => entry.card.id), [f.card]);
assert.ok(viewGame(pending, f.holder).semutaReaction?.canCommit);
if (effect === 'hajr') assert.ok(pending.hajr.includes(f.owner));
if (effect === 'ghola') {
assert.equal(player(pending, f.owner).reserves, oldPlayer.reserves + 3);
assert.equal(player(pending, f.owner).tanks, oldPlayer.tanks - 3);
}
if (effect === 'weather') assert.equal(pending.stormPending, 1);
if (effect === 'atomics') {
assert.equal(pending.shieldWallDestroyed, true);
assert.equal(player(pending, f.owner).forces[f.casualtyLocation!], undefined);
assert.equal(player(pending, f.owner).tanks, oldPlayer.tanks + 3);
}
if (effect === 'harvester') {
assert.equal(pending.spiceWindow!.amount, f.game.spiceWindow!.amount * 2);
assert.equal(pending.spiceWindow!.harvested, true);
}
const committed = {phase: pending.phase, turn: pending.turn, storm: pending.storm,
stormPending: pending.stormPending, hajr: pending.hajr, spice: pending.spice,
spiceWindow: pending.spiceWindow, shieldWallDestroyed: pending.shieldWallDestroyed,
forces: player(pending, f.owner).forces,
reserves: player(pending, f.owner).reserves, tanks: player(pending, f.owner).tanks};
const done = react(pending, f.holder, frame.batch.event, claim);
assert.equal(done.pendingTreacheryDiscard, null);
assert.deepEqual({phase: done.phase, turn: done.turn, storm: done.storm,
stormPending: done.stormPending, hajr: done.hajr, spice: done.spice,
spiceWindow: done.spiceWindow, shieldWallDestroyed: done.shieldWallDestroyed,
forces: player(done, f.owner).forces,
reserves: player(done, f.owner).reserves, tanks: player(done, f.owner).tanks}, committed);
assert.equal(player(done, f.holder).hand.some(card => card.id === f.card), claim);
assert.equal(done.discard.some(card => card.id === f.card), !claim);
assert.equal(done.discard.some(card => card.id === SEMUTA_DRUG_ID), claim);
assert.deepEqual(inventory(done), f.originalInventory);
assert.throws(() => act(done, f.holder, {type: 'semutaCommit', event: frame.batch.event}));
if (effect === 'hajr') {
const next = act(done, f.owner, {type: 'endMovement'});
assert.ok(!next.movementRemaining?.includes(f.owner));
} else if (effect === 'weather' || effect === 'atomics') {
const next = advance(done, state => state.turn === done.turn && state.phase === 1);
assert.equal(next.storm, stormSectorAfter(done.storm, done.stormPending!));
if (effect === 'atomics') assert.equal(next.shieldWallDestroyed, true);
} else if (effect === 'harvester') {
const next = advance(done, state => state.turn === done.turn && state.phase === 2);
const blow = pending.spiceWindow!;
const key = `${blow.territory}:${blow.sector}`;
assert.equal(next.spice[key] ?? 0, committed.spice[key] ?? 0);
} else {
const next = advance(done, state => state.turn === done.turn && state.phase === 5);
assert.equal(player(next, f.owner).reserves, committed.reserves);
assert.equal(player(next, f.owner).tanks, committed.tanks);
}
});
}
void test('all AI profiles can legally respond to an independent-card Hajr discard', () => {
const f = fixture('hajr'), pending = act(f.game, f.owner, f.play);
for (const difficulty of DIFFICULTIES) {
const game = structuredClone(pending);
player(game, f.holder).bot = difficulty;
const response = botActions(viewGame(game, f.holder)).find(action =>
action.type === 'semutaCommit' || action.type === 'semutaPass');
assert.ok(response);
assert.doesNotThrow(() => act(game, f.holder, response));
}
});