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.
114 lines
5.7 KiB
TypeScript
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);
|
|
}
|
|
});
|