Allow native Advanced use after declining the first offer, including a sealed target. Reuse projected card/target permission in persistent controls and minimal bots; retain sealed plans and finish target-last public reveal once. Source: authorized supplied physical27 F. Ordinary preparation priority and target-first ordering remain provisional; no new reading acknowledgement. Verified: 11 affected offline files126/126, types/lint, production build, five actual control/gameplay/GameTable SSR journeys and eight legal profile paths. No saved-room, D1, reset, restart or deployment operation.
128 lines
6.4 KiB
TypeScript
128 lines
6.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import {
|
|
createAtreidesFullPlanWindowFixture as fixture,
|
|
actAtreidesFullPlanWindow as act,
|
|
atreidesFullPlanWindowPlayer as player,
|
|
atreidesFullPlanWindowInventory as inventory,
|
|
finishAtreidesFullPlanWindow as finish,
|
|
} from './fixture-atreides-full-plan-window';
|
|
|
|
function rejected(game: Game, actor: string, action: Action, reason?: RegExp) {
|
|
const before = structuredClone(game);
|
|
if (reason) assert.throws(() => applyAction(game, actor, action), reason);
|
|
else assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
|
|
void test('actual paid setup and battle retain the special window after declining its first offer', () => {
|
|
const f = fixture();
|
|
assert.equal(f.offer.decision?.kind, 'fullPlanOffer');
|
|
assert.equal(f.game.decision, null);
|
|
assert.equal(f.game.battle?.revealed, false);
|
|
assert.deepEqual(f.game.battle?.plans, {});
|
|
assert.equal(viewGame(f.game, f.owner).atreidesFullPlan?.blocked, null);
|
|
assert.deepEqual(viewGame(f.game, f.owner).atreidesFullPlan?.cards, [f.karama]);
|
|
assert.equal(viewGame(f.game, f.guild).atreidesFullPlan, null);
|
|
assert.deepEqual(inventory(f.game), f.originalInventory);
|
|
const inspecting = act(f.game, f.owner, f.inspect);
|
|
assert.equal(inspecting.decision, null);
|
|
rejected(inspecting, f.owner, f.ownPlan, /requested by special prescience/);
|
|
const read = act(inspecting, f.opponent, f.opponentPlan);
|
|
assert.equal(read.decision, null);
|
|
assert.equal(read.battle?.revealed, false);
|
|
assert.ok(viewGame(read, f.owner).battle?.fullPlanInsight);
|
|
assert.equal(finish(read, f).phase, 7);
|
|
});
|
|
|
|
void test('a target sealed before activation is immediately readable without changing that plan or spending support early', () => {
|
|
const f = fixture();
|
|
const sealed = act(f.game, f.opponent, f.opponentPlan);
|
|
const before = structuredClone(player(sealed, f.opponent));
|
|
assert.equal(viewGame(sealed, f.owner).battle?.plans[f.opponent], undefined);
|
|
const read = act(sealed, f.owner, f.inspect);
|
|
assert.deepEqual(read.battle?.plans, sealed.battle?.plans);
|
|
assert.deepEqual(player(read, f.opponent), before);
|
|
assert.equal(read.response, null);
|
|
assert.equal(read.decision, null);
|
|
const insight = viewGame(read, f.owner).battle?.fullPlanInsight;
|
|
assert.deepEqual(insight?.plan, sealed.battle!.plans[f.opponent]);
|
|
assert.deepEqual(insight?.cards.map(card => card.id), [f.weapon]);
|
|
assert.equal(player(read, f.owner).specialKaramaUsed, true);
|
|
assert.equal(read.discard.filter(card => card.id === f.karama).length, 1);
|
|
assert.deepEqual(inventory(read), f.originalInventory);
|
|
for (const actor of [f.opponent, f.guild]) {
|
|
assert.equal(viewGame(read, actor).battle?.fullPlanInsight, null);
|
|
assert.equal(viewGame(read, actor).players.find(p => p.id === f.owner)?.hand, undefined);
|
|
}
|
|
rejected(read, f.opponent, { ...f.opponentPlan, dial: 0 }, /not available/);
|
|
rejected(read, f.owner, f.inspect, /already been used/);
|
|
const revealed = act(read, f.owner, f.ownPlan);
|
|
assert.equal(revealed.battle?.revealed, true);
|
|
assert.equal(viewGame(revealed, f.owner).battle?.fullPlanInsight, null);
|
|
assert.equal(finish(revealed, f).phase, 7);
|
|
});
|
|
|
|
void test('own seal then special activation and target-last commitment reveals both once without a read deadlock', () => {
|
|
const f = fixture();
|
|
const sealed = act(f.game, f.owner, f.ownPlan);
|
|
const inspecting = act(sealed, f.owner, f.inspect);
|
|
assert.deepEqual(inspecting.battle?.plans, sealed.battle?.plans);
|
|
assert.equal(inspecting.battle?.revealed, false);
|
|
assert.equal(inspecting.decision, null);
|
|
const revealed = act(inspecting, f.opponent, f.opponentPlan);
|
|
assert.equal(revealed.battle?.revealed, true);
|
|
assert.deepEqual(revealed.battle?.plans[f.owner], sealed.battle?.plans[f.owner]);
|
|
assert.equal(revealed.log.filter(row => row.text === 'Both battle plans are revealed.').length, 1);
|
|
assert.ok(revealed.log.some(row => row.text.includes('Both plans reveal immediately')));
|
|
assert.notEqual(revealed.decision?.kind, 'fullPlanRead');
|
|
rejected(revealed, f.owner, { ...f.ownPlan, dial: 0 }, /not available/);
|
|
const done = finish(revealed, f);
|
|
assert.equal(done.phase, 7);
|
|
assert.equal(done.battle, null);
|
|
assert.deepEqual(inventory(done), f.originalInventory);
|
|
});
|
|
|
|
void test('any one player includes the already sealed own plan without reopening it', () => {
|
|
const f = fixture();
|
|
const sealed = act(f.game, f.owner, f.ownPlan);
|
|
const read = act(sealed, f.owner, { ...f.inspect, target: f.owner });
|
|
assert.deepEqual(viewGame(read, f.owner).battle?.fullPlanInsight?.plan, sealed.battle?.plans[f.owner]);
|
|
assert.deepEqual(read.battle?.plans, sealed.battle?.plans);
|
|
assert.equal(viewGame(read, f.guild).battle?.fullPlanInsight, null);
|
|
assert.equal(finish(read, f).phase, 7);
|
|
});
|
|
|
|
void test('actual preparation, foreign target and public reveal retain their timing and card boundaries', () => {
|
|
const f = fixture();
|
|
const before = viewGame(f.beforePreparation, f.owner).atreidesFullPlan;
|
|
assert.ok(before?.blocked);
|
|
rejected(f.beforePreparation, f.owner, f.inspect);
|
|
rejected(f.game, f.owner, { ...f.inspect, target: f.guild }, /in this battle/);
|
|
rejected(f.game, f.owner, { ...f.inspect, card: f.defense }, /Karama card/);
|
|
let revealed = act(f.game, f.opponent, f.opponentPlan);
|
|
revealed = act(revealed, f.owner, f.ownPlan);
|
|
assert.equal(revealed.battle?.revealed, true);
|
|
assert.match(viewGame(revealed, f.owner).atreidesFullPlan!.blocked!, /already been revealed/);
|
|
rejected(revealed, f.owner, f.inspect, /already been revealed/);
|
|
assert.equal(player(revealed, f.owner).specialKaramaUsed, undefined);
|
|
assert.ok(player(revealed, f.owner).hand.some(card => card.id === f.karama));
|
|
});
|
|
|
|
for (const profile of DIFFICULTIES) {
|
|
void test(`${profile} uses the same legal private late-inspection offer and continues to Collection`, () => {
|
|
const f = fixture();
|
|
const sealed = act(f.game, f.opponent, f.opponentPlan);
|
|
const view = viewGame(sealed, f.owner);
|
|
view.players.find(p => p.id === f.owner)!.bot = profile;
|
|
const action = botActions(view)[0];
|
|
assert.deepEqual(action, f.inspect);
|
|
const read = act(sealed, f.owner, action);
|
|
assert.ok(viewGame(read, f.owner).battle?.fullPlanInsight);
|
|
assert.equal(finish(read, f).phase, 7);
|
|
});
|
|
}
|