Complete concealed No-Field allied separation endings

This commit is contained in:
Quintine committed 2026-10-27 12:11:45 +11:00
1 parent 57a59c1663
commit e41abdaf2e
17 files changed
+569 -30

No files matched your search

+2
View File
@@ -72,6 +72,8 @@ function nativeStep(game: Game): Game {
}
if (game.phase === 3 && game.auction) return applyAction(game, game.auction.active, {type: 'passBid'});
if (game.phase === 5) return applyAction(game, game.active!, {type: 'endMovement'});
if (game.phase === 2 && game.charityOrder?.remaining[0])
return applyAction(game,game.charityOrder.remaining[0],{type:'ready'});
if (game.phase === 4 && game.revivalOrder?.remaining[0])
return applyAction(game,game.revivalOrder.remaining[0],{type:'ready'});
// A native Richese lot can own Bidding without an ordinary auction row.
+173
View File
@@ -0,0 +1,173 @@
import assert from 'node:assert/strict';
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import type { NoFieldValue } from '../game/richese-no-field';
import { actFamilyAtomicsNoField, familyAtomicsNoFieldPlayer, familyAtomicsNoFieldReload,
familyAtomicsNoFieldInventory, familyAtomicsNoFieldClean, nextFamilyAtomicsNoFieldStep,
type FamilyAtomicsNoFieldStep } from './fixture-family-atomics-no-field';
export type NoFieldAlliedSeparationStep = FamilyAtomicsNoFieldStep;
export const actNoFieldAlliedSeparation = actFamilyAtomicsNoField;
export const noFieldAlliedSeparationPlayer = familyAtomicsNoFieldPlayer;
export const noFieldAlliedSeparationReload = familyAtomicsNoFieldReload;
export const noFieldAlliedSeparationInventory = familyAtomicsNoFieldInventory;
export const noFieldAlliedSeparationClean = familyAtomicsNoFieldClean;
/** Consume the native optional ally-shipment offer by passing. The source
* never starts a new token shipment after the measured separation ending. */
export function nextNoFieldAlliedSeparation(game: Game): NoFieldAlliedSeparationStep | null {
if (!game.response && !game.phaseOpening && game.decision?.kind === 'richeseAllyOpportunity')
return { actor: game.decision.player, action: { type: 'decision', decline: true } };
return nextFamilyAtomicsNoFieldStep(game);
}
export function advanceNoFieldAlliedSeparation(game: Game, until: (state: Game) => boolean,
actions: NoFieldAlliedSeparationStep[] = []): Game {
for (let count = 0; count < 1800; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The original allied-marker chronology ended before its requested boundary');
const next = nextNoFieldAlliedSeparation(game); assert.ok(next, 'Complete the actual native pending control');
game = actNoFieldAlliedSeparation(game, next.actor, next.action, actions);
}
throw Error('The original allied-marker chronology did not reach its finite boundary');
}
export function finishNoFieldAlliedSeparationMovement(game: Game, actions: NoFieldAlliedSeparationStep[] = []): Game {
return advanceNoFieldAlliedSeparation(game, state => state.phase !== 5 && noFieldAlliedSeparationClean(state), actions);
}
type SpiceSelection = { position: number; from: number; card: Game['spiceDeck'][number] };
function selectOriginalSpice(game: Game, position: number, worm: boolean): SpiceSelection {
const from = game.spiceDeck.findIndex((card, index) => index >= position && (worm
? 'worm' in card && !card.greatMaker && !card.suppressed
: 'territory' in card && !card.discovery && card.territory !== 'imperial_basin' && card.sector !== game.storm &&
game.players.every(player => !Object.entries(player.forces).some(([key, count]) => count > 0 && key.startsWith(`${card.territory}:`)))));
assert.ok(from >= position, 'Select an actual remaining original safe Spice face');
const [card] = game.spiceDeck.splice(from, 1); game.spiceDeck.splice(position, 0, card);
return { position, from, card: structuredClone(card) };
}
/** Real unallied Basin9 marker and Basin11 two-counter army are separated by
* actual Storm10 in turn1. A real next-turn worm supplies the Nexus where both
* owners consent. Advanced Richese is first; Basic Emperor is first and Richese
* is later, retaining both the formation grace and actual later-player rule.
*/
export function createNoFieldAlliedSeparationFixture(options: { advanced?: boolean; value?: NoFieldValue } = {}) {
const advanced = options.advanced ?? true, value = options.value ?? 0;
const actor = 'richese', ally = 'emperor', other = 'atreides';
let game = createGame('NOFIELDALLIEDSEPARATION', newPlayer(actor, 'Richese', 'richese'), advanced, ['choam']);
joinGame(game, newPlayer(ally, 'Emperor', 'emperor')); joinGame(game, newPlayer(other, 'Atreides', 'atreides'));
const initial = structuredClone(game), actions: NoFieldAlliedSeparationStep[] = [];
const act = (id: string, action: Action) => { game = actNoFieldAlliedSeparation(game, id, action, actions); };
const reach = (until: (state: Game) => boolean) => { game = advanceNoFieldAlliedSeparation(game, until, actions); };
const transition = (run: () => void) => {
const before = structuredClone(game), from = actions.length; run();
return { before, steps: structuredClone(actions.slice(from)), after: structuredClone(game) };
};
const circlesFrom = actions.length;
if (!advanced) {
for (const [index, player] of game.players.entries()) act(player.id, { type: 'seatPosition', position: index + 4 });
act(ally, { type: 'seatPosition', position: 1 }); act(actor, { type: 'seatPosition', position: 2 });
act(other, { type: 'seatPosition', position: 3 });
}
const circles = structuredClone(actions.slice(circlesFrom));
for (const player of game.players) act(player.id, { type: 'ready' });
const lobby = structuredClone(game);
act(game.host, advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
const setup = structuredClone(game);
assert.equal(game.status, 'setup'); assert.ok(game.players.every(player => !player.hand.length));
const ordinary = ['projectile', 'shield', 'snooper'].map(kind => game.deck.find(card => card.kind === kind));
assert.ok(ordinary.every(card => card));
const dealOrder = ordinary.map((card, position) => {
assert.ok(card); const from = game.deck.findIndex(candidate => candidate.id === card.id); assert.ok(from >= position);
[game.deck[position], game.deck[from]] = [game.deck[from], game.deck[position]];
return { position, from, card: card.id };
});
reach(state => state.status === 'playing');
const afterSetup = structuredClone(game), setupActions = structuredClone(actions);
assert.deepEqual(noFieldAlliedSeparationPlayer(game, actor).forces, {});
const original = noFieldAlliedSeparationInventory(game);
const firstSpiceOrder = [selectOriginalSpice(game, 0, false),
...(advanced ? [selectOriginalSpice(game, 1, false)] : [])];
const firstDials = transition(() => {
for (const [index, dialer] of game.stormDialers.entries()) act(dialer, { type: 'stormDial', amount: index === 0 ? 4 : 5 });
});
reach(state => state.turn === 1 && state.phase === 5 && noFieldAlliedSeparationClean(state));
assert.equal(game.storm, 10);
let markerShipment: ReturnType<typeof transition> | null = null;
let allyShipment: ReturnType<typeof transition> | null = null;
let token = '';
for (let count = 0; game.phase === 5 && count < 100; count++) {
if (noFieldAlliedSeparationClean(game) && game.active === actor && !noFieldAlliedSeparationPlayer(game, actor).shipped) {
const own = noFieldAlliedSeparationPlayer(game, actor), selected = own.noField!.tokens.find(token => token.value === value);
assert.ok(selected && own.noFieldEvent); token = selected.id;
markerShipment = transition(() => {
act(actor, { type: 'ship', noField: token, event: own.noFieldEvent, territory: 'imperial_basin', sector: 9 });
reach(noFieldAlliedSeparationClean);
});
assert.equal(noFieldAlliedSeparationPlayer(markerShipment.before, actor).spice - noFieldAlliedSeparationPlayer(game, actor).spice, 2);
assert.equal(noFieldAlliedSeparationPlayer(game, actor).reserves, noFieldAlliedSeparationPlayer(markerShipment.before, actor).reserves);
continue;
}
if (noFieldAlliedSeparationClean(game) && game.active === ally && !noFieldAlliedSeparationPlayer(game, ally).shipped) {
allyShipment = transition(() => {
act(ally, { type: 'ship', territory: 'imperial_basin', sector: 11, amount: 2, elite: 0, allyPayment: 0 });
reach(noFieldAlliedSeparationClean);
});
assert.equal(noFieldAlliedSeparationPlayer(allyShipment.before, ally).spice - noFieldAlliedSeparationPlayer(game, ally).spice, 4);
continue;
}
const next = nextNoFieldAlliedSeparation(game); assert.ok(next); act(next.actor, next.action);
}
assert.ok(markerShipment && allyShipment);
const deployedEvent = noFieldAlliedSeparationPlayer(markerShipment.after, actor).noFieldEvent; assert.ok(deployedEvent);
assert.notEqual(deployedEvent, noFieldAlliedSeparationPlayer(markerShipment.before, actor).noFieldEvent);
reach(state => state.turn === 2 && state.phase === 0 && noFieldAlliedSeparationClean(state));
const beforeNexusStorm = structuredClone(game);
const nexusSpiceOrder = [selectOriginalSpice(game, 0, true), selectOriginalSpice(game, 1, false),
...(advanced ? [selectOriginalSpice(game, 2, false)] : [])];
reach(state => state.turn === 2 && state.phase === 1 && !!state.nexus && !state.spiceWindow &&
!state.spiceResolution && noFieldAlliedSeparationClean(state));
const alliance = transition(() => {
act(actor, { type: 'alliance', target: ally }); act(ally, { type: 'alliance', target: actor });
});
assert.equal(noFieldAlliedSeparationPlayer(game, actor).ally, ally);
assert.equal(noFieldAlliedSeparationPlayer(game, ally).ally, actor);
assert.equal(noFieldAlliedSeparationPlayer(game, actor).allySinceTurn, 2);
reach(state => state.turn === 2 && state.phase === 5 && noFieldAlliedSeparationClean(state));
const formationMovement = structuredClone(game), formationMovementActions = structuredClone(actions);
let formationEnds: ReturnType<typeof transition> | null = null;
let thirdSpiceOrder: SpiceSelection[] = [];
if (!advanced) {
formationEnds = transition(() => { reach(state => state.phase !== 5 && noFieldAlliedSeparationClean(state)); });
assert.ok(noFieldAlliedSeparationPlayer(game, actor).noField!.deployed);
assert.equal(noFieldAlliedSeparationPlayer(game, ally).forces['imperial_basin:11'], 2);
reach(state => state.turn === 3 && state.phase === 0 && noFieldAlliedSeparationClean(state));
thirdSpiceOrder = [selectOriginalSpice(game, 0, false)];
reach(state => state.turn === 3 && state.phase === 5 && noFieldAlliedSeparationClean(state));
}
const beforeEarlierEnd = structuredClone(game), earlierFrom = actions.length;
reach(state => state.phase === 5 && state.active === actor && noFieldAlliedSeparationClean(state));
const earlierEnds = structuredClone(actions.slice(earlierFrom));
const own = noFieldAlliedSeparationPlayer(game, actor);
assert.equal(game.turn, advanced ? 2 : 3); assert.equal(game.active, actor);
assert.deepEqual(own.forces, {}); assert.equal(own.tanks, 0);
assert.equal(own.noFieldEvent, deployedEvent); assert.equal(own.noField!.deployed!.tokenId, token);
assert.deepEqual(own.noField!.deployed!.location, { territory: 'imperial_basin', sector: 9 });
assert.equal(noFieldAlliedSeparationPlayer(game, ally).forces['imperial_basin:11'], 2);
assert.ok(advanced || !(game.movementRemaining ?? []).includes(ally));
assert.deepEqual(noFieldAlliedSeparationInventory(game), original);
return { game, sourceGame: structuredClone(game), initial, lobby, setup, afterSetup, advanced, value,
actor, owner: actor, ally, other, token, event: deployedEvent, marker: structuredClone(own.noField),
action: { type: 'endMovement' } satisfies Action,
markerShipment, allyShipment, alliance, beforeNexusStorm, formationMovement, formationMovementActions,
formationEnds, beforeEarlierEnd, earlierEnds, original, circles, dealOrder, firstSpiceOrder, nexusSpiceOrder,
thirdSpiceOrder, firstDials, setupActions, actions, sourceActions: actions, sourceActionCount: actions.length,
sourceCounts: { setup: setupActions.length, circles: circles.length, firstDials: firstDials.steps.length,
markerShipment: markerShipment.steps.length, allyShipment: allyShipment.steps.length, alliance: alliance.steps.length,
formationMovement: formationMovementActions.length, formationEnds: formationEnds?.steps.length ?? 0,
earlierEnds: earlierEnds.length, source: actions.length },
sourceDisclosure: `Actual public ${advanced ? 'Advanced preview' : 'Basic'} Richese/Emperor/Atreides +CHOAM-deck setup, original Treachery/Traitors and three real tokens, no optional modules. ` +
'Only actual lobby circle actions, original unopened deal/Spice faces and scalar lotteries selected. Real first dials4+5 earn Storm10. Richese pays2 to deploy the original concealed marker in Basin9; Emperor separately pays4 for two original normal reserves in Basin11, separated by that real storm before alliance. ' +
'The next actual original worm opens a real Nexus; both owners actually consent. Marker9 stays outside the next Advanced card1 path through11. ' +
(advanced ? 'Richese is the actual first turn2 ending player, with its new ally still pending. '
: 'Actual lobby circles put Emperor first and Richese later. Both native turn2 ends retain the new-alliance grace; actual next Storm/Spice/phase controls reach turn3, then Emperor really ends first before the Richese deadline. ') +
'The destructive reveal at a due end is the explicitly provisional recommended composition, not a special publisher FAQ. Counts derive from recorded arrays; no hand, purse, force, phase, alliance, marker, event or receipt is installed.' };
}
export type NoFieldAlliedSeparationFixture = ReturnType<typeof createNoFieldAlliedSeparationFixture>;
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { AdvancedAllySeparation } from '../components/advanced-ally-separation';
type Props = Parameters<typeof AdvancedAllySeparation>[0];
const markup = (quote: Props['quote'], basic = false) =>
renderToStaticMarkup(createElement(AdvancedAllySeparation, { quote, basic }));
// Public-quote render supplements only. They establish wording and display
// behavior, not native alliance eligibility, marker custody or casualties.
void test('no due public territories produce no separation warning in either mode', () => {
for (const basic of [false, true]) {
assert.equal(markup(null, basic), '');
assert.equal(markup({ territories: [] }, basic), '');
assert.equal(markup({ territories: [], noField: true }, basic), '');
}
});
void test('ordinary warnings retain the Advanced default and explain the Basic ending deadline without a marker claim', () => {
const quote = { territories: ['Arrakeen', 'Hagga Basin'] };
const advanced = markup(quote);
assert.match(advanced, /id="advanced-ally-separation"/);
assert.match(advanced, /Finishing now sends your forces in Arrakeen, Hagga Basin to Tanks under the Advanced alliance rule/);
const basic = markup(quote, true);
assert.match(basic, /id="basic-ally-separation"/);
assert.match(basic, /Your ally’s Shipment and Movement turn is finished/);
assert.match(basic, /Ending your turn now sends your forces in Arrakeen, Hagga Basin to Tanks under the Basic alliance rule/);
for (const html of [advanced, basic]) assert.doesNotMatch(html, /Provisional|reveal your marker|button|input/);
assert.deepEqual(quote, { territories: ['Arrakeen', 'Hagga Basin'] });
});
void test('a public No-Field flag persistently explains provisional reveal and actual losses without inventing a quantity or action', () => {
const quote = { territories: ['Arrakeen'], noField: true as const };
for (const basic of [false, true]) for (let render = 0; render < 2; render++) {
const html = markup(quote, basic);
assert.match(html, /Provisional No-Field consequence/);
assert.match(html, /at this ending deadline, reveal your marker/);
assert.match(html, /using only available reserves, then send the actual revealed forces to Tanks/);
assert.match(html, /A zero marker creates no counter/);
assert.match(html, /Your ally’s forces stay unchanged/);
assert.doesNotMatch(html, /\b[035]\b|data-token|data-value|button|input|Confirm/);
}
assert.deepEqual(quote, { territories: ['Arrakeen'], noField: true });
});
@@ -0,0 +1,124 @@
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 type { Difficulty } from '../game/bot-profiles';
import { actNoFieldAlliedSeparation as act, createNoFieldAlliedSeparationFixture as fixture,
noFieldAlliedSeparationInventory as inventory, noFieldAlliedSeparationPlayer as p,
noFieldAlliedSeparationReload as reload, finishNoFieldAlliedSeparationMovement as finish,
noFieldAlliedSeparationClean as clean, nextNoFieldAlliedSeparation as next } from './fixture-no-field-allied-separation';
function reject(game: Game, actor: string, action: Action) {
const before = reload(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before);
}
function assertDueLoss(f: ReturnType<typeof fixture>, game: Game) {
const before = p(f.game, f.actor), after = p(game, f.actor), amount = Math.min(f.value, before.reserves);
assert.equal(after.noField!.deployed, null); assert.notEqual(after.noFieldEvent, f.event);
assert.deepEqual(after.noField!.tokens, f.marker!.tokens); assert.equal(after.noField!.lastShipped, f.token);
assert.equal(after.reserves, before.reserves - amount); assert.equal(after.tanks, before.tanks + amount);
assert.deepEqual(after.forces, {});
assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.equal(p(game, f.ally).tanks, 0);
assert.equal(p(game, f.ally).ally, f.actor); assert.equal(after.ally, f.ally);
assert.equal((game.movementRemaining ?? []).includes(f.actor), false);
assert.deepEqual(inventory(game), f.original);
}
void test('genuine protected Basin marker and army form a real next-worm alliance without premature revelation', () => {
const f = fixture();
assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
assert.equal(f.sourceCounts.setup, f.setupActions.length); assert.equal(f.sourceCounts.source, f.sourceActions.length);
assert.equal(f.sourceCounts.markerShipment, f.markerShipment.steps.length);
assert.equal(f.sourceCounts.allyShipment, f.allyShipment.steps.length);
assert.equal(f.sourceCounts.alliance, f.alliance.steps.length);
assert.deepEqual(f.firstDials.steps.map(step => step.action.amount), [4, 5]);
assert.equal(p(f.markerShipment.before, f.actor).spice - p(f.markerShipment.after, f.actor).spice, 2);
assert.equal(p(f.allyShipment.before, f.ally).spice - p(f.allyShipment.after, f.ally).spice, 4);
assert.deepEqual(f.alliance.steps.map(step => [step.actor, step.action.type, step.action.target]),
[[f.actor, 'alliance', f.ally], [f.ally, 'alliance', f.actor]]);
assert.equal(f.alliance.before.turn, 2); assert.equal(f.alliance.before.phase, 1); assert.equal(f.alliance.before.nexus, true);
assert.equal(viewGame(f.alliance.after, f.actor).advancedAllySeparation, null);
assert.deepEqual(p(f.game, f.actor).forces, {}); assert.equal(p(f.game, f.actor).noFieldEvent, f.event);
assert.equal(f.game.active, f.actor);
assert.deepEqual(viewGame(f.game, f.actor).advancedAllySeparation, { territories: ['Imperial Basin'], noField: true });
assert.equal(viewGame(f.game, f.ally).advancedAllySeparation, null);
assert.equal(viewGame(f.game, f.actor).basicAllySeparation, null);
assert.deepEqual(inventory(f.game), f.original);
});
void test('Advanced due ends reveal zero three and five once and lose only the ending player materialized group', () => {
for (const value of [0, 3, 5] as const) {
const f = fixture({ value }); let game = act(reload(f.game), f.actor, f.action);
assertDueLoss(f, game);
const revealedEvent = p(game, f.actor).noFieldEvent;
reject(game, f.actor, f.action);
assert.equal(viewGame(game, f.actor).advancedAllySeparation, null);
game = finish(game); assert.equal(game.phase, 7);
assert.equal(p(game, f.actor).tanks, value); assert.equal(p(game, f.actor).noFieldEvent, revealedEvent);
assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2);
assert.deepEqual(inventory(game), f.original);
}
});
void test('Basic actual new-alliance turn ends preserve the concealed marker and its ally formation grace', () => {
const f = fixture({ advanced: false, value: 5 }); assert.ok(f.formationEnds);
const before = f.formationMovement;
assert.equal(before.turn, 2); assert.equal(before.active, f.ally);
assert.equal(viewGame(before, f.ally).basicAllySeparation, null);
assert.equal(viewGame(before, f.actor).basicAllySeparation, null);
const game = finish(reload(before));
assert.equal(game.phase, 7); assert.deepEqual(p(game, f.actor).noField, p(before, f.actor).noField);
assert.equal(p(game, f.actor).noFieldEvent, f.event); assert.equal(p(game, f.actor).tanks, 0);
assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2); assert.equal(p(game, f.ally).tanks, 0);
assert.equal(f.formationEnds.after.phase, 7); assert.deepEqual(inventory(game), f.original);
});
void test('Basic later-player deadline follows an actual earlier ally end and then destroys only the due marker group', () => {
for (const value of [0, 3, 5] as const) {
const f = fixture({ advanced: false, value });
assert.equal(f.beforeEarlierEnd.turn, 3); assert.equal(f.beforeEarlierEnd.active, f.ally);
assert.equal(viewGame(f.beforeEarlierEnd, f.ally).basicAllySeparation, null);
assert.ok(f.earlierEnds.some(step => step.actor === f.ally && step.action.type === 'endMovement'));
assert.equal(p(f.game, f.actor).noFieldEvent, f.event); assert.equal(p(f.game, f.ally).forces['imperial_basin:11'], 2);
assert.equal((f.game.movementRemaining ?? []).includes(f.ally), false);
assert.deepEqual(viewGame(f.game, f.actor).basicAllySeparation, { territories: ['Imperial Basin'], noField: true });
assert.equal(viewGame(f.game, f.actor).advancedAllySeparation, null);
const game = act(reload(f.game), f.actor, f.action); assertDueLoss(f, game);
assert.equal(viewGame(game, f.actor).basicAllySeparation, null);
}
});
void test('native voluntary reveal remains an alternative before the same due physical separation without a second reveal', () => {
const f = fixture({ value: 3 });
let game = act(reload(f.game), f.actor, { type: 'revealNoField', event: f.event, token: f.token });
const event = p(game, f.actor).noFieldEvent;
assert.notEqual(event, f.event); assert.equal(p(game, f.actor).noField!.deployed, null);
assert.equal(p(game, f.actor).forces['imperial_basin:9'], 3); assert.equal(p(game, f.actor).tanks, 0);
assert.deepEqual(viewGame(game, f.actor).advancedAllySeparation, { territories: ['Imperial Basin'] });
game = act(game, f.actor, f.action);
assertDueLoss(f, game); assert.equal(p(game, f.actor).noFieldEvent, event);
assert.equal(p(game, f.actor).tanks, 3);
});
void test('all four minimal profiles legally complete the native deadline by departure, revelation or ending', () => {
const f = fixture({ value: 3 });
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] satisfies Difficulty[]) {
let game = reload(f.game);
for (let count = 0; game.phase === 5 && (game.movementRemaining ?? []).includes(f.actor) && count < 60; count++) {
if (game.active === f.actor && clean(game)) {
const view = viewGame(game, f.actor); view.players.find(player => player.id === f.actor)!.bot = difficulty;
const choice = botActions(view)[0]; assert.ok(choice, `${difficulty} has a legal current continuation`);
game = act(game, f.actor, choice);
} else {
const step = next(game); assert.ok(step); game = act(game, step.actor, step.action);
}
}
assert.equal((game.movementRemaining ?? []).includes(f.actor), false,
`${difficulty} completes instead of stalling on concealed custody`);
assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2);
assert.equal(p(game, f.ally).tanks, 0);
assert.deepEqual(inventory(game), f.original);
game = finish(game); assert.ok(game.phase === 6 || game.phase === 7);
assert.equal(p(game, f.ally).forces['imperial_basin:11'], 2);
assert.deepEqual(inventory(game), f.original);
}
});
+2
View File
@@ -192,6 +192,8 @@ void test('every reveal cause returns capped physical materialization and the ac
'gamont',
'beforeAlly',
'allyShipment',
'atomics',
'alliedSeparation',
];
for (const cause of causes)
for (const value of [0, 3, 5] as const)