Files
Dune/tests/advisor-preparation-engine.test.ts
T
Quintine 660b0a0c25 Open native advisor preparation before the first shipment
Allow Advanced BG preparation in phases2–5 at quiet boundaries, preserving
original phase/auction/readiness and native eligibility. Close a current-turn
receipt only on committed shipments, including marker and Sneak Attack entry.
Retain the legacy phase5 queue and qualify the per-territory prevention policy.
Connect persistent controls, minimal bot support and focused native evidence.
Complete modes and browser/deployed acceptance remain open.
2026-10-24 18:36:55 +11:00

114 lines
5.7 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { viewGame } from '../game/engine';
import { isAdvisor } from '../game/advisors';
import {
createAdvisorPreparationFixture as fixture,
actAdvisorPreparation as act,
passAdvisorPreparationResponses as pass,
advisorPreparationPlayer as player,
advisorPreparationInventory as inventory,
shipAdvisorPreparation as ship,
} from './fixture-advisor-preparation';
for (const phase of [2, 3, 4] as const) {
void test(`native phase${phase} advisor preparation preserves the original phase and controls after allowance`, () => {
const f = fixture({ phase }), before = structuredClone(f.game);
assert.equal(isAdvisor(player(f.game, f.bg), 'arrakeen'), true);
assert.ok(viewGame(f.game, f.bg).advisorPreparation?.territories.includes('arrakeen'));
let g = act(f.game, f.bg, f.preparation);
assert.equal(g.response?.kind, 'advisorFlip');
assert.equal(g.response?.advisorResume, 'preparation');
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), true);
assert.ok(viewGame(g, f.observer).responseControls?.cancelCards.includes(f.karama));
g = pass(g);
assert.equal(g.phase, phase);
assert.equal(g.active, before.active);
assert.deepEqual(g.auction, before.auction);
assert.deepEqual(g.ready, before.ready);
assert.deepEqual(g.movementRemaining, before.movementRemaining);
assert.deepEqual(g.players.map(p => [p.id, p.forces, p.reserves, p.spice, p.shipped, p.moved]),
before.players.map(p => [p.id, p.forces, p.reserves, p.spice, p.shipped, p.moved]));
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), false);
assert.deepEqual(inventory(g), f.originalInventory);
assert.deepEqual(f.game, before);
});
void test(`native phase${phase} Karama prevents only that preparation without moving the interrupted phase`, () => {
const f = fixture({ phase });
let g = act(f.game, f.bg, f.preparation);
g = act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' });
assert.ok(!g.response);
assert.equal(g.phase, phase);
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), true);
assert.deepEqual(g.advisorPreparation?.deniedTerritories, ['arrakeen']);
assert.deepEqual(g.auction, f.game.auction);
assert.deepEqual(g.ready, f.game.ready);
assert.equal(g.discard.filter(c => c.id === f.karama).length, 1);
const before = structuredClone(g);
assert.throws(() => act(g, f.bg, f.preparation), /eligible|prevent/i);
assert.deepEqual(g, before);
assert.match(viewGame(g, f.bg).advisorPreparation?.blocked ?? '', /Karama/);
assert.deepEqual(inventory(g), f.originalInventory);
});
}
void test('declining the native phase5 chooser leaves the printed window open for a later declaration', () => {
const f = fixture({ phase: 5, declineOffer: true });
assert.equal(f.game.decision, null);
const active = f.game.active;
assert.ok(active);
const g = pass(act(f.game, f.bg, f.preparation));
assert.equal(g.phase, 5);
assert.equal(g.active, active);
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), false);
assert.equal(g.advisorPreparation?.firstShipment, null);
assert.deepEqual(g.movementRemaining, f.game.movementRemaining);
});
void test('the first actual native shipment closes preparation before its arrival followups', () => {
const f = fixture({ phase: 5, declineOffer: true });
const shipped = ship(f.game), g = shipped.after;
assert.deepEqual(g.advisorPreparation?.firstShipment, { player: f.game.active, producer: 'ordinary' });
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), true);
const before = structuredClone(g);
assert.throws(() => act(g, f.bg, f.preparation), /first shipment/);
assert.deepEqual(g, before);
assert.deepEqual(inventory(g), f.originalInventory);
});
void test('native canceled phase5 preparation cannot be bypassed through the new manual control', () => {
const f = fixture({ phase: 5 });
let g = act(f.game, f.bg, { type: 'decision', accept: true, territory: 'arrakeen' });
assert.ok(g.response?.advisorPreparationSource);
g = act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' });
assert.ok(!g.response);
assert.equal(g.phase, 5);
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), true);
assert.deepEqual(g.advisorPreparation?.deniedTerritories, ['arrakeen']);
assert.throws(() => act(g, f.bg, f.preparation), /eligible|prevent/i);
});
void test('ending an unshipped native turn leaves the printed preparation window open', () => {
const f = fixture({ phase: 5, declineOffer: true });
assert.ok(f.game.active);
let g = act(f.game, f.game.active, { type: 'endMovement' });
assert.equal(g.phase, 5);
assert.equal(g.movementRemaining!.includes(f.game.active), false);
assert.equal(g.advisorPreparation?.firstShipment, null);
const active = g.active;
g = pass(act(g, f.bg, f.preparation));
assert.equal(g.active, active);
assert.equal(isAdvisor(player(g, f.bg), 'arrakeen'), false);
});
void test('a changed preparation source rejects before a real Karama is spent', () => {
const f = fixture();
for (const change of ['turn', 'territory', 'stance']) {
const g = act(f.game, f.bg, f.preparation);
assert.ok(g.response?.advisorPreparationSource);
if (change === 'turn') g.response.advisorPreparationSource.turn++;
if (change === 'territory') g.response.advisorPreparationSource.territory = 'carthag';
if (change === 'stance') g.response.advisors = true;
const before = structuredClone(g);
assert.throws(() => act(g, f.observer, { type: 'card', card: f.karama, mode: 'cancel' }), /source|owner|territory/);
assert.deepEqual(g, before);
assert.ok(player(g, f.observer).hand.some(c => c.id === f.karama));
assert.equal(g.discard.some(c => c.id === f.karama), false);
}
});