Files
Dune/tests/advanced-revival-order-engine.test.ts
T

214 lines
13 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import {
actAdvancedRevivalOrder, advanceAdvancedRevivalOrder, advancedRevivalOrderClean,
advancedRevivalOrderInventory, advancedRevivalOrderPlayer, advancedRevivalOrderStocks,
createAdvancedRevivalOrderFixture, finishAdvancedRevivalOrder, passAdvancedRevivalOrder,
reachAdvancedRevivalOrderActor,
} from './fixture-advanced-revival-order';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'A refused order action changes no original outcome');
}
function current(game: Game) {
const actor = game.revivalOrder?.remaining[0]; assert.ok(actor); return actor;
}
void test('public Advanced start earns the first Revival queue without replacing original physical state', () => {
const f = createAdvancedRevivalOrderFixture();
assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
assert.equal(f.afterSetup.status, 'playing'); assert.equal(f.game.turn, 1); assert.equal(f.game.phase, 4);
assert.equal(f.sourceActions.length, f.sourceActionCount); assert.ok(f.sourceActionCount > 15);
assert.deepEqual(f.staging, []);
assert.ok(f.game.revivalOrder);
assert.deepEqual(f.game.revivalOrder, { version: 1, turn: 1,
original: f.originalOrder, remaining: f.originalOrder, completed: [], started: false });
assert.equal(f.game.active, current(f.game));
assert.deepEqual(advancedRevivalOrderInventory(f.game), f.originalInventory);
for (const player of f.game.players) {
assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((sum, n) => sum + n, 0), 20);
assert.deepEqual(viewGame(f.game, player.id).revivalOrder, f.game.revivalOrder);
}
const view = viewGame(f.game, f.actor); assert.ok(view.revivalOrder);
view.revivalOrder.remaining.reverse();
assert.deepEqual(f.game.revivalOrder.remaining, f.originalOrder, 'Projected order is a separate public snapshot');
});
void test('native Advanced Revival rejects another participant return and finish before touching the source', () => {
const f = createAdvancedRevivalOrderFixture();
rejected(f.game, f.outOfTurn, { type: 'ready' });
rejected(f.game, f.outOfTurn, { type: 'revive', amount: 1, elite: 0 });
assert.equal(f.game.active, f.actor);
assert.deepEqual(f.game.revivalOrder?.completed, []);
});
void test('native Advanced participants finish once in original order before Shipment opens', () => {
const f = createAdvancedRevivalOrderFixture();
let game = reload(f.game);
for (let index = 0; index < f.originalOrder.length; index++) {
const actor = f.originalOrder[index];
assert.equal(current(game), actor); assert.equal(game.active, actor);
const before = advancedRevivalOrderStocks(game);
game = actAdvancedRevivalOrder(game, actor, { type: 'ready' });
assert.deepEqual(advancedRevivalOrderStocks(game), before);
if (index + 1 < f.originalOrder.length) {
assert.equal(game.phase, 4);
assert.deepEqual(game.revivalOrder?.original, f.originalOrder);
assert.deepEqual(game.revivalOrder?.completed, f.originalOrder.slice(0, index + 1));
assert.deepEqual(game.revivalOrder?.remaining, f.originalOrder.slice(index + 1));
rejected(game, actor, { type: 'ready' });
rejected(game, actor, { type: 'revive', amount: 1 });
}
}
game = advanceAdvancedRevivalOrder(game, state => state.phase === 5 && advancedRevivalOrderClean(state));
assert.equal(game.phase, 5);
assert.deepEqual(advancedRevivalOrderInventory(game), f.originalInventory);
});
void test('native paid shipment and next Storm earn an ordered force return and separate Tleilaxu income', () => {
const f = createAdvancedRevivalOrderFixture({ casualty: true });
assert.ok(f.shipment && f.storm); assert.equal(f.game.turn, 2);
assert.ok(f.shipment.steps.some(step => step.actor === f.owner && step.action.type === 'ship'));
assert.equal(f.storm.before.stormMovementSource?.kind, 'card');
assert.equal(f.storm.before.stormMovementSource?.turn, 2);
assert.equal(f.storm.before.stormMovementSource?.distance, f.storm.before.stormPending);
assert.ok(f.storm.steps.some(step => step.action.type === 'ready'));
assert.ok(f.storm.steps.every(step => step.action.type !== 'stormDial'));
assert.equal(advancedRevivalOrderPlayer(f.shipment.after, f.owner).forces[f.casualtyLocation], 1);
assert.equal(advancedRevivalOrderPlayer(f.storm.after, f.owner).tanks, 1);
let game = reachAdvancedRevivalOrderActor(f.game, f.owner);
const before = structuredClone(game), original = [...game.revivalOrder!.original];
const ownerBefore = advancedRevivalOrderPlayer(before, f.owner);
const responderBefore = advancedRevivalOrderPlayer(before, f.responder);
game = actAdvancedRevivalOrder(game, f.owner, f.revivalAction);
game = advanceAdvancedRevivalOrder(game, state => advancedRevivalOrderClean(state) || state.response?.kind === 'revivalIncome');
assert.equal(current(game), f.owner); assert.equal(game.active, f.owner);
assert.equal(game.revivalOrder?.started, true);
assert.notEqual(f.responder, current(game));
// With no possible cancellation the original income settles automatically;
// the test must not invent a mandatory acknowledgement or wait for it.
if (game.response) {
assert.equal(game.response.kind, 'revivalIncome');
assert.equal(game.response.owner, f.responder); assert.equal(game.response.amount, 1);
rejected(game, f.owner, { type: 'ready' });
game = actAdvancedRevivalOrder(game, f.responder, { type: 'passResponse' });
game = passAdvancedRevivalOrder(game);
}
game = advanceAdvancedRevivalOrder(game, advancedRevivalOrderClean);
const ownerAfter = advancedRevivalOrderPlayer(game, f.owner);
assert.equal(ownerAfter.tanks, 0); assert.equal(ownerAfter.reserves, ownerBefore.reserves + 1);
assert.equal(ownerAfter.revived, 1); assert.equal(ownerAfter.freeForcesRevived, 1);
assert.equal(ownerAfter.spice, ownerBefore.spice);
assert.deepEqual(ownerAfter.elites, ownerBefore.elites, 'An ordinary return does not invent Sardaukar');
assert.equal(advancedRevivalOrderPlayer(game, f.responder).spice, responderBefore.spice + 1);
assert.deepEqual(game.revivalOrder?.original, original); assert.equal(current(game), f.owner);
assert.deepEqual(advancedRevivalOrderInventory(game), f.originalInventory);
game = actAdvancedRevivalOrder(game, f.owner, { type: 'ready' });
if (game.phase === 4) assert.ok(!game.revivalOrder?.remaining.includes(f.owner));
game = finishAdvancedRevivalOrder(game);
assert.equal(game.phase, 5);
});
void test('native Tleilaxu grant remains legal outside its own ordered opportunity', () => {
const f = createAdvancedRevivalOrderFixture();
let game = reachAdvancedRevivalOrderActor(f.game, f.owner);
const before = structuredClone(game);
game = actAdvancedRevivalOrder(game, f.responder, { type: 'tleilaxuRevivalLimit', target: f.owner });
assert.ok(game.revivalRules?.expanded.includes(f.owner));
assert.deepEqual(game.revivalOrder, before.revivalOrder);
assert.equal(game.active, f.owner);
assert.deepEqual(advancedRevivalOrderStocks(game), advancedRevivalOrderStocks(before));
});
void test('public Basic Revival keeps concurrent readiness and has no Advanced order stamp', () => {
const f = createAdvancedRevivalOrderFixture({ advanced: false });
assert.equal(f.game.revivalOrder, undefined);
assert.equal(viewGame(f.game, f.actor).revivalOrder, null);
let game = actAdvancedRevivalOrder(f.game, f.outOfTurn, { type: 'ready' });
assert.ok(game.ready.includes(f.outOfTurn)); assert.equal(game.phase, 4);
game = finishAdvancedRevivalOrder(game);
assert.equal(game.phase, 5); assert.equal(game.revivalOrder, undefined);
});
void test('controlled legacy-envelope supplement keeps an already opened unstamped Advanced Revival concurrent', () => {
const f = createAdvancedRevivalOrderFixture();
// Controlled format supplement only: physical cards/forces/wallets and phase
// remain native. Remove the new envelope to model a pre-cutover open phase.
const legacy = reload(f.game); delete legacy.revivalOrder; legacy.active = null;
assert.equal(viewGame(legacy, f.outOfTurn).revivalOrder, null);
let game = actAdvancedRevivalOrder(legacy, f.outOfTurn, { type: 'ready' });
assert.equal(game.revivalOrder, undefined); assert.ok(game.ready.includes(f.outOfTurn));
game = finishAdvancedRevivalOrder(game);
assert.equal(game.phase, 5);
game = advanceAdvancedRevivalOrder(game, state => state.turn === 2 && state.phase === 4 && advancedRevivalOrderClean(state));
assert.equal(game.revivalOrder?.version, 1); assert.equal(game.revivalOrder?.turn, 2);
assert.deepEqual(game.revivalOrder?.original, game.order);
});
void test('four minimal policies offer the current native Finish and no foreign ordinary return or Finish', () => {
const f = createAdvancedRevivalOrderFixture();
const ownActions = new Set(['ready', 'revive', 'reviveLeader', 'reviveKwisatz',
'reviveForeignGhola', 'requestLeaderRevival', 'acceptLeaderRevival']);
for (const level of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
for (const player of f.game.players) {
const view = viewGame(f.game, player.id); view.players.find(seat => seat.id === player.id)!.bot = level;
const candidates = botActions(view);
if (player.id !== f.actor)
assert.ok(candidates.every(action => !ownActions.has(action.type)), `${level} waits for ${player.id}'s own opportunity`);
else {
const finish = candidates.find(action => action.type === 'ready'); assert.ok(finish);
assert.doesNotThrow(() => applyAction(f.game, player.id, finish));
}
}
}
});
void test('controlled dead-disc supplement keeps requester actions current and the Tleilaxu quote out of turn', () => {
const f = createAdvancedRevivalOrderFixture();
let game = reachAdvancedRevivalOrderActor(f.game, f.owner);
// Only this supplement stages prior disc deaths. No native death chronology
// is claimed; queue, physical card custody, wallets and clocks stay original.
const leader = advancedRevivalOrderPlayer(game, f.owner).leaders[0]; leader.dead = true; leader.deaths = 1;
const foreign = advancedRevivalOrderPlayer(game, 'atreides').leaders[0]; foreign.dead = true; foreign.deaths = 1;
rejected(game, 'atreides', { type: 'requestLeaderRevival', leader: foreign.id });
rejected(game, 'atreides', { type: 'reviveLeader', leader: foreign.id });
game = actAdvancedRevivalOrder(game, f.owner, { type: 'requestLeaderRevival', leader: leader.id });
assert.equal(current(game), f.owner); assert.notEqual(f.responder, f.owner);
game = actAdvancedRevivalOrder(game, f.responder, { type: 'quoteLeaderRevival', target: f.owner, amount: 0 });
assert.equal(game.revivalRequests?.[f.owner]?.price, 0);
game = actAdvancedRevivalOrder(game, f.owner, { type: 'acceptLeaderRevival' });
game = advanceAdvancedRevivalOrder(game, advancedRevivalOrderClean);
assert.equal(advancedRevivalOrderPlayer(game, f.owner).leaders.find(disc => disc.id === leader.id)?.dead, false);
assert.equal(current(game), f.owner);
});
void test('controlled prevention supplement retains responder quotes and lets a prevented current requester finish', () => {
const f = createAdvancedRevivalOrderFixture();
let game = reachAdvancedRevivalOrderActor(f.game,f.owner);
// Controlled prior death/prevention stamps supplement the played order;
// neither a native death nor a physical Karama-use chronology is claimed.
const leader = advancedRevivalOrderPlayer(game,f.owner).leaders[0];
leader.dead = true; leader.deaths = 1;
game = actAdvancedRevivalOrder(game,f.owner,{type:'requestLeaderRevival',leader:leader.id});
game.revivalPrevention = {player:f.responder,turn:game.turn};
for (const level of ['Easy','Medium','Hard','Brutal'] as const) {
const view = viewGame(game,f.responder); view.players.find(p=>p.id===f.responder)!.bot=level;
assert.equal(view.revival.prevented,true);
const quote = botActions(view).find(action=>action.type==='quoteLeaderRevival'); assert.ok(quote);
assert.equal(applyAction(game,f.responder,quote).active,f.owner);
}
game.revivalPrevention = {player:f.owner,turn:game.turn};
for (const level of ['Easy','Medium','Hard','Brutal'] as const) {
const view = viewGame(game,f.owner); view.players.find(p=>p.id===f.owner)!.bot=level;
const finish = botActions(view).find(action=>action.type==='ready'); assert.ok(finish);
const next=applyAction(game,f.owner,finish);
assert.equal(next.revivalRequests?.[f.owner],undefined);
assert.ok(next.phase!==4 || !next.revivalOrder?.remaining.includes(f.owner));
}
});