143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import {
|
|
nexusRicheseFixture,
|
|
nexusRicheseInventory,
|
|
holdNexusRicheseCard,
|
|
type NexusRicheseFixtureOptions,
|
|
} from './fixture-nexus-richese';
|
|
import {
|
|
nexusReload,
|
|
nexusAllow,
|
|
orderNexusSpice,
|
|
} from './fixture-nexus-cards';
|
|
import { botActions } from '../game/bots';
|
|
import { nexusTraitorInventory } from './fixture-nexus-traitors';
|
|
import { homeworldGameIntegrity } from '../game/homeworld-game';
|
|
|
|
export type NexusGuildSecretAllyFixtureOptions = Omit<
|
|
NexusRicheseFixtureOptions,
|
|
'guild' | 'ownerFaction' | 'opponentFaction'
|
|
> & {
|
|
ownerFaction?: Exclude<NexusRicheseFixtureOptions['ownerFaction'], 'guild'>;
|
|
opponentFaction?: Exclude<
|
|
NexusRicheseFixtureOptions['opponentFaction'],
|
|
'guild'
|
|
>;
|
|
};
|
|
/** CONTROLLED historical Movement boundary after genuine setup: the inherited
|
|
* Richese fixture stages its wallets, reserve pools and phase5 rather than
|
|
* playing the intervening phases. Retire its old phase2 Charity frame with that
|
|
* declared jump. Physical Nexus identities are exchanged before declaration;
|
|
* no transport receipt or completed native Charity opportunity is invented. */
|
|
export function nexusGuildSecretAllyFixture(
|
|
options: NexusGuildSecretAllyFixtureOptions = {},
|
|
) {
|
|
const f = nexusRicheseFixture({
|
|
...options,
|
|
opponentFaction:
|
|
options.opponentFaction ??
|
|
(options.ownerFaction === 'harkonnen' ? 'beneGesserit' : 'harkonnen'),
|
|
});
|
|
assert.equal(f.g.phase, 5);
|
|
delete f.g.charityOrder;
|
|
const cards = f.g.nexusCards!.cards!,
|
|
index = cards.deck.indexOf('guild');
|
|
assert.ok(index >= 0);
|
|
assert.equal(cards.hands[f.owner], 'richese');
|
|
cards.deck[index] = 'richese';
|
|
cards.hands[f.owner] = 'guild';
|
|
assert.equal(
|
|
f.g.players.some((p) => p.faction === 'guild'),
|
|
false,
|
|
);
|
|
nexusRicheseInventory(f.g);
|
|
return f;
|
|
}
|
|
export function nexusGuildSecretAllyRequest(
|
|
g: Game,
|
|
owner: string,
|
|
action: Action = {
|
|
type: 'ship',
|
|
territory: 'arrakeen',
|
|
sector: 10,
|
|
amount: 5,
|
|
},
|
|
): Action {
|
|
return { ...action, nexus: viewGame(g, owner).nexusGuildSecretAlly!.event };
|
|
}
|
|
export function nexusGuildSecretAllyInventory(g: Game) {
|
|
const census = structuredClone(g);
|
|
for (const p of census.players)
|
|
p.reserves += Object.values(g.homeworlds?.custody?.visitors ?? {}).reduce(
|
|
(sum, visitors) =>
|
|
sum + (visitors[p.id]?.normal ?? 0) + (visitors[p.id]?.elite ?? 0),
|
|
0,
|
|
);
|
|
nexusTraitorInventory(census);
|
|
if (g.homeworlds) homeworldGameIntegrity(g);
|
|
}
|
|
export const nexusGuildSecretAllyReload = nexusReload;
|
|
export const holdGuildSecretAllyCard = holdNexusRicheseCard;
|
|
|
|
export function nexusGuildSecretAllianceFixture(
|
|
seatIds?: [string, string, string],
|
|
) {
|
|
const f = nexusGuildSecretAllyFixture({
|
|
seatIds,
|
|
opponentFaction: 'moritani',
|
|
});
|
|
let g = f.g;
|
|
while (g.phase === 5) g = applyAction(g, g.active!, { type: 'endMovement' });
|
|
assert.equal(g.phase, 7);
|
|
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
|
assert.equal(g.decision?.kind, 'moritaniPlacement');
|
|
const token = g.moritaniTerror!.tokens.find((t) => t.kind === 'robbery')!;
|
|
g = nexusAllow(
|
|
applyAction(g, f.target, {
|
|
type: 'decision',
|
|
token: token.id,
|
|
territory: 'arrakeen',
|
|
}),
|
|
);
|
|
let ordered = false;
|
|
for (
|
|
let step = 0;
|
|
!(
|
|
g.turn === 2 &&
|
|
g.phase === 5 &&
|
|
!g.response &&
|
|
!g.decision &&
|
|
!g.phaseOpening
|
|
) && step < 180;
|
|
step++
|
|
) {
|
|
if (g.turn === 2 && g.phase === 1 && !ordered) {
|
|
orderNexusSpice(g, ['land', 'land']);
|
|
ordered = true;
|
|
}
|
|
let next: Game | undefined;
|
|
for (const p of g.players) {
|
|
const v = viewGame(g, p.id);
|
|
v.players.find((s) => s.id === p.id)!.bot = 'Easy';
|
|
const a = botActions(v)[0];
|
|
if (a) {
|
|
next = applyAction(g, p.id, a);
|
|
break;
|
|
}
|
|
}
|
|
assert.ok(
|
|
next,
|
|
`Secret Ally alliance progression stalled at ${g.turn}/${g.phase}/${g.decision?.kind}`,
|
|
);
|
|
g = next;
|
|
}
|
|
assert.equal(g.turn, 2);
|
|
assert.equal(g.phase, 5);
|
|
while (g.active !== f.owner)
|
|
g = applyAction(g, g.active!, { type: 'endMovement' });
|
|
assert.equal(g.nexusCards!.cards!.hands[f.owner], 'guild');
|
|
nexusGuildSecretAllyInventory(g);
|
|
return { ...f, g };
|
|
}
|