Implement private Tupile intelligence and Homeworld occupation evidence
This commit is contained in:
1 parent
1db11a0187
commit
61689514a0
43 files changed
+3420
-188
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction, createGame, initializeHomeworldGameForAudit, joinGame,
|
||||
newPlayer, viewGame, type Action, type Game,
|
||||
} from '../game/engine';
|
||||
import { baseDeck, type Card } from '../game/cards';
|
||||
import { botActions } from '../game/bots';
|
||||
import { homeworldContext, homeworldGameIntegrity } from '../game/homeworld-game';
|
||||
import { quoteHomeworldCustody } from '../game/homeworld-custody';
|
||||
|
||||
export const tupilePlayer = (g: Game, id: string) => g.players.find((p) => p.id === id)!;
|
||||
export const tupileReload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
||||
|
||||
/** Real faction setup creates both signed ledgers using the final authenticated
|
||||
* seat IDs. Only conserved armies and a settled movement opportunity are staged;
|
||||
* the default contact then comes from an actual paid Homeworld shipment. */
|
||||
export function tupileIntelligenceFixture(options: {
|
||||
seatIds?: [string, string, string]; contact?: boolean; advanced?: boolean;
|
||||
} = {}): Game {
|
||||
const [c, a, h] = options.seatIds ?? ['c', 'a', 'h'];
|
||||
let g = createGame('TUPILEINTELLIGENCE', newPlayer(c, 'CHOAM', 'choam'), options.advanced ?? false, []);
|
||||
joinGame(g, newPlayer(a, 'Atreides', 'atreides'));
|
||||
joinGame(g, newPlayer(h, 'Harkonnen', 'harkonnen'));
|
||||
g = applyAction(g, c, { type: 'homeworlds', enabled: true });
|
||||
for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' });
|
||||
g = initializeHomeworldGameForAudit(g);
|
||||
for (let step = 0; g.status === 'setup' && step < 60; step++) {
|
||||
let next: Game | undefined;
|
||||
for (const p of g.players) {
|
||||
const view = viewGame(g, p.id);
|
||||
view.players.find((seat) => seat.id === p.id)!.bot = 'Easy';
|
||||
const action = botActions(view)[0];
|
||||
if (action) { next = applyAction(g, p.id, action); break; }
|
||||
}
|
||||
assert.ok(next, `Setup stalled at ${g.setupStage}/${g.decision?.kind}`);
|
||||
g = next;
|
||||
}
|
||||
assert.equal(g.status, 'playing');
|
||||
assert.equal(g.homeworlds?.historyVersion, 1);
|
||||
assert.equal(g.tupileIntelligence?.owner, c);
|
||||
assert.deepEqual(g.homeworldOccupationHistory?.qualifications, []);
|
||||
for (const p of g.players) {
|
||||
g.deck.push(...p.hand.splice(0));
|
||||
Object.assign(p, { spice: 20, forces: {}, reserves: 20, tanks: 0, shipped: false, moved: 0 });
|
||||
}
|
||||
Object.assign(tupilePlayer(g, c), { reserves: 10, forces: { 'polar_sink:0': 10 } });
|
||||
Object.assign(g, {
|
||||
phase: 5, turn: 2, active: c, order: [c, a, h], movementRemaining: [c, a, h],
|
||||
ready: [], phaseOpening: null, response: null, decision: null, storm: 18, spice: {},
|
||||
});
|
||||
if (options.contact !== false) g = enterTupileContact(g);
|
||||
tupileInventory(g);
|
||||
return g;
|
||||
}
|
||||
|
||||
export function enterTupileContact(g: Game): Game {
|
||||
const owner = g.players.find((p) => p.faction === 'choam')!;
|
||||
return applyAction(g, owner.id, {
|
||||
type: 'homeworldShip', event: viewGame(g, owner.id).homeworldShipment!.event,
|
||||
destination: 'homeworld:atreides', sources: { 'homeworld:choam': { normal: 1, elite: 0 } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Conserved leave/reentry scenarios exercise current contact without forging
|
||||
* occupation or intelligence receipts. Both worlds retain native garrisons. */
|
||||
export function positionTupileContact(g: Game, present: boolean): void {
|
||||
const owner = g.players.find((p) => p.faction === 'choam')!;
|
||||
const one = { normal: 1, elite: 0 }, zero = { normal: 0, elite: 0 };
|
||||
const quote = quoteHomeworldCustody(homeworldContext(g), g.homeworlds!.custody!, [
|
||||
{ homeworld: 'homeworld:choam', player: owner.id, withdraw: present ? one : zero, deposit: present ? zero : one },
|
||||
{ homeworld: 'homeworld:atreides', player: owner.id, withdraw: present ? zero : one, deposit: present ? one : zero },
|
||||
]);
|
||||
g.homeworlds!.custody = quote.state;
|
||||
for (const p of quote.players) tupilePlayer(g, p.id).reserves = p.reserves;
|
||||
tupileInventory(g);
|
||||
}
|
||||
|
||||
export function holdTupileCard(g: Game, id: string, kind: string): Card {
|
||||
const index = g.deck.findIndex((card) => card.id === kind || card.kind === kind || card.effect === kind);
|
||||
assert.ok(index >= 0, `Missing canonical card ${kind}`);
|
||||
const card = g.deck.splice(index, 1)[0];
|
||||
tupilePlayer(g, id).hand.push(card);
|
||||
return card;
|
||||
}
|
||||
export function tupileReject(g: Game, id: string, action: Action, match: RegExp = /./): void {
|
||||
const before = tupileReload(g);
|
||||
assert.throws(() => applyAction(g, id, action), match);
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
export function tupileInventory(g: Game): void {
|
||||
homeworldGameIntegrity(g);
|
||||
for (const p of g.players) {
|
||||
const visitors = Object.values(g.homeworlds!.custody!.visitors)
|
||||
.reduce((sum, world) => sum + (world[p.id]?.normal ?? 0) + (world[p.id]?.elite ?? 0), 0);
|
||||
assert.equal(p.reserves + p.tanks + Object.values(p.forces).reduce((sum, n) => sum + n, 0) + visitors, 20);
|
||||
}
|
||||
assert.deepEqual([...g.deck, ...g.discard, ...g.players.flatMap((p) => p.hand)].map((c) => c.id).sort(), baseDeck().map((c) => c.id).sort());
|
||||
}
|
||||
Reference in new issue
Block a user