180 lines
10 KiB
TypeScript
180 lines
10 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import {
|
|
actNexusFremenReactiveBetrayal, createNexusFremenReactiveBetrayalFixture,
|
|
finishNexusFremenReactiveBetrayal, nexusFremenReactiveBetrayalInventory,
|
|
nexusFremenReactiveBetrayalPhysical, nexusFremenReactiveBetrayalPlayer as player,
|
|
passNexusFremenReactiveBetrayal, rideNexusFremenReactiveBetrayal,
|
|
useNexusFremenReactiveBetrayal, type NexusFremenReactiveBetrayalFixture,
|
|
} from './fixture-nexus-fremen-reactive-betrayal';
|
|
|
|
function rejected(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => actNexusFremenReactiveBetrayal(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function completedReceipt(game: Game, f: NexusFremenReactiveBetrayalFixture, usedBy: string | null) {
|
|
const receipt = game.nexusFremenRideBetrayal;
|
|
assert.ok(receipt);
|
|
assert.equal(receipt.stage, 'complete');
|
|
assert.equal(receipt.usedBy, usedBy);
|
|
assert.equal(receipt.event, f.originalFrame.event);
|
|
assert.equal(receipt.turn, f.originalFrame.turn);
|
|
assert.deepEqual(receipt.source, f.originalDecision);
|
|
assert.deepEqual(receipt.order, f.originalFrame.order);
|
|
assert.ok(receipt.signature);
|
|
assert.notEqual(receipt.signature, f.originalFrame.signature);
|
|
if (usedBy === null) assert.deepEqual(receipt.passed, receipt.order);
|
|
for (const seat of game.players)
|
|
assert.equal(viewGame(game, seat.id).nexusFremenRideBetrayal ?? null, null);
|
|
}
|
|
function source(f: NexusFremenReactiveBetrayalFixture) {
|
|
assert.equal(f.game.status, 'playing');
|
|
assert.equal(f.game.turn, 3);
|
|
assert.equal(f.game.phase, 1);
|
|
assert.equal(f.game.response ?? null, null);
|
|
assert.equal(f.game.phaseOpening ?? null, null);
|
|
assert.ok(f.game.decision?.kind === 'nexusFremenRideBetrayal');
|
|
assert.equal(f.game.decision.player, f.holder);
|
|
assert.equal(f.beforeDraw.nexusCards!.cards!.hands[f.holder], null);
|
|
assert.equal(f.afterDraw.nexusCards!.cards!.hands[f.holder], 'fremen');
|
|
assert.equal(f.game.nexusCards!.cards!.hands[f.holder], 'fremen');
|
|
assert.equal(player(f.game, f.holder).ally, null);
|
|
assert.equal(player(f.game, f.rider).ally, 'emperor');
|
|
assert.equal(player(f.game, 'emperor').ally, f.rider);
|
|
assert.equal(f.draw.action.type, 'nexusCardChoice');
|
|
assert.equal(f.shipment.action.type, 'ship');
|
|
assert.equal(f.shipment.action.amount, 2);
|
|
assert.equal(player(f.afterShipment, f.rider).reserves, player(f.beforeShipment, f.rider).reserves - 2);
|
|
assert.equal(player(f.afterShipment, f.rider).spice, player(f.beforeShipment, f.rider).spice);
|
|
assert.equal(player(f.game, f.rider).forces['hagga_basin:12'], 2);
|
|
assert.deepEqual(player(f.game, f.rider).forces, player(f.beforeAppearance, f.rider).forces,
|
|
'The real worm and neutral counter window have not transported the native survivors');
|
|
assert.equal(f.game.nexusCards!.phase!.occurred, true);
|
|
assert.ok(f.actions.some(step => step.actor === f.rider && step.action.type === 'alliance'));
|
|
assert.ok(f.actions.some(step => step.actor === f.holder && step.action.type === 'nexusCardChoice' && step.action.choice === 'draw'));
|
|
assert.ok(!f.actions.some(step => step.action.type === 'nexusFremenBetrayal'), 'No advance Betrayal substitutes for the reactive source');
|
|
assert.equal(f.sourceActionCount, f.actions.length);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalInventory(f.game), f.originalInventory);
|
|
assert.equal(f.game.homeworlds ?? null, null);
|
|
assert.equal(f.game.leaderSkills ?? null, null);
|
|
assert.equal(f.game.techTokens ?? null, null);
|
|
if (f.kind === 'greatMaker') {
|
|
assert.ok(f.game.discoveries);
|
|
assert.equal(f.originalDecision.kind, 'greatMakerRide');
|
|
assert.equal(f.game.greatMaker!.stage, 'ride');
|
|
assert.deepEqual(f.game.greatMaker!.votes.map(vote => vote.yes), f.game.order.map(() => true));
|
|
} else {
|
|
assert.equal(f.game.discoveries ?? null, null);
|
|
assert.deepEqual(f.originalDecision, { kind: 'wormRide', player: f.rider, territory: 'hagga_basin' });
|
|
assert.ok(f.actions.some(step => step.action.type === 'ready'), 'Actual native readiness completes the Nexus before its pending ride');
|
|
}
|
|
}
|
|
|
|
const variants = [
|
|
{ kind: 'ordinary', advanced: false, label: 'Basic ordinary' },
|
|
{ kind: 'ordinary', advanced: true, label: 'Advanced ordinary' },
|
|
{ kind: 'greatMaker', advanced: true, label: 'Advanced Great Maker' },
|
|
] as const;
|
|
|
|
for (const variant of variants) {
|
|
void test(`genuine ${variant.label} reactive Fremen Betrayal spends the native draw and stops only the unmade ride`, () => {
|
|
const f = createNexusFremenReactiveBetrayalFixture(variant);
|
|
source(f);
|
|
const physical = nexusFremenReactiveBetrayalPhysical(f.game);
|
|
let game = useNexusFremenReactiveBetrayal(f);
|
|
completedReceipt(game, f, f.holder);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.holder], null);
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'fremen').length, 1);
|
|
assert.equal(player(game, f.rider).fremenNexusWormBlockedTurn, game.turn);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalPhysical(game), physical,
|
|
'The response spends its original Nexus card without moving or killing surviving riders');
|
|
assert.equal(game.nexusFremenBetrayalHistory!.at(-1)!.mode, 'worm');
|
|
assert.equal(game.nexusFremenBetrayalHistory!.at(-1)!.owner, f.holder);
|
|
assert.deepEqual(game.wormRides, []);
|
|
rejected(game, f.holder, f.action);
|
|
game = finishNexusFremenReactiveBetrayal(f, game);
|
|
assert.equal(game.phase, 2);
|
|
assert.equal(game.nexusCards!.phase!.stage, 'complete');
|
|
assert.equal(player(game, f.rider).forces['hagga_basin:12'], 2);
|
|
assert.equal(player(game, f.rider).forces['polar_sink:0'] ?? 0, 0);
|
|
assert.equal(player(game, f.rider).reserves, player(f.game, f.rider).reserves);
|
|
assert.equal(game.decision?.kind === 'wormRide' || game.decision?.kind === 'greatMakerRide', false);
|
|
assert.equal(game.nexusFremenBetrayalHistory!.filter(row => row.turn === f.game.turn).length, 1);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalInventory(game), f.originalInventory);
|
|
});
|
|
|
|
void test(`genuine ${variant.label} all-pass restores the exact native ride and actually transports its original counters`, () => {
|
|
const f = createNexusFremenReactiveBetrayalFixture(variant);
|
|
source(f);
|
|
const queue = structuredClone(f.game.wormRides), maker = structuredClone(f.game.greatMaker);
|
|
const passed = passNexusFremenReactiveBetrayal(structuredClone(f.game));
|
|
completedReceipt(passed, f, null);
|
|
assert.deepEqual(passed.decision, f.originalDecision);
|
|
assert.deepEqual(passed.wormRides, queue);
|
|
assert.deepEqual(passed.greatMaker, maker);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalPhysical(passed), f.originalPhysical);
|
|
assert.equal(passed.nexusCards!.cards!.hands[f.holder], 'fremen');
|
|
assert.equal(passed.nexusFremenBetrayalHistory?.length ?? 0, 0);
|
|
const ride = rideNexusFremenReactiveBetrayal(f, passed);
|
|
const before = player(ride.before, f.rider), after = player(ride.after, f.rider);
|
|
const amount = f.kind === 'greatMaker' ? 1 : 2;
|
|
assert.equal(after.forces['polar_sink:0'], amount);
|
|
assert.equal(after.spice, before.spice);
|
|
assert.equal(after.shipped, before.shipped);
|
|
assert.equal(after.moved, before.moved);
|
|
if (f.kind === 'greatMaker') {
|
|
assert.equal(after.reserves, before.reserves - 1);
|
|
assert.equal(after.elites!.reserves, before.elites!.reserves - 1);
|
|
assert.equal(after.forces['hagga_basin:12'], 2);
|
|
} else {
|
|
assert.equal(after.reserves, before.reserves);
|
|
assert.equal(after.forces['hagga_basin:12'] ?? 0, 0);
|
|
}
|
|
if (f.advanced) assert.equal(after.elites!.forces['polar_sink:0'], 1);
|
|
const game = finishNexusFremenReactiveBetrayal(f, ride.game);
|
|
assert.equal(game.phase, 2);
|
|
assert.equal(game.nexusCards!.phase!.stage, 'complete');
|
|
assert.equal(game.nexusCards!.cards!.hands[f.holder], 'fremen');
|
|
assert.equal(game.nexusFremenBetrayalHistory?.length ?? 0, 0);
|
|
assert.equal(player(game, f.rider).forces['polar_sink:0'], amount, 'Only the original accepted ride moved these counters');
|
|
assert.deepEqual(nexusFremenReactiveBetrayalInventory(game), f.originalInventory);
|
|
});
|
|
|
|
void test(`genuine ${variant.label} reactive window rejects foreign, stale and underlying ride replies without spending`, () => {
|
|
const f = createNexusFremenReactiveBetrayalFixture(variant);
|
|
rejected(f.game, f.rider, f.action);
|
|
rejected(f.game, 'emperor', f.action);
|
|
rejected(f.game, f.holder, { ...f.action, event: 'stale' });
|
|
rejected(f.game, f.holder, { type: 'decision', event: f.action.event, use: 'yes' });
|
|
rejected(f.game, f.rider, f.kind === 'greatMaker'
|
|
? { type: 'decision', event: f.game.greatMaker!.event, accept: false }
|
|
: { type: 'decision', accept: false });
|
|
assert.equal(f.game.nexusCards!.cards!.hands[f.holder], 'fremen');
|
|
assert.deepEqual(nexusFremenReactiveBetrayalPhysical(f.game), f.originalPhysical);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalInventory(f.game), f.originalInventory);
|
|
});
|
|
}
|
|
|
|
void test('four minimal profiles answer the genuine reactive Fremen ride window with an exact legal native decision', () => {
|
|
for (const variant of variants) {
|
|
const f = createNexusFremenReactiveBetrayalFixture(variant);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(f.game, f.holder);
|
|
view.players.find(seat => seat.id === f.holder)!.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
assert.ok(action, `${variant.label}/${difficulty}`);
|
|
assert.equal(action.type, 'decision');
|
|
assert.equal(action.event, f.action.event);
|
|
assert.equal(typeof action.use, 'boolean');
|
|
const game = actNexusFremenReactiveBetrayal(structuredClone(f.game), f.holder, action);
|
|
assert.notEqual(game.decision?.kind, 'nexusFremenRideBetrayal');
|
|
assert.deepEqual(nexusFremenReactiveBetrayalPhysical(game), f.originalPhysical);
|
|
assert.deepEqual(nexusFremenReactiveBetrayalInventory(game), f.originalInventory);
|
|
}
|
|
}
|
|
});
|