92 lines
5.1 KiB
TypeScript
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));
|
|
}
|
|
});
|