176 lines
8.1 KiB
TypeScript
176 lines
8.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction, viewGame, type Action, type Game,
|
|
} from '../game/engine';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { createSemutaHomeworldGuildIncomeFixture } from './fixture-semuta-homeworld-guild-income';
|
|
import {
|
|
moveSemutaEmperorCard, semutaEmperorCardId, semutaEmperorPlayer,
|
|
} from './fixture-semuta-emperor-share';
|
|
|
|
export type SemutaHomeworldOrdinaryFixture = {
|
|
game: Game; source: Game; holder: string; owner: string; semuta: string;
|
|
hajr: string; action: Action;
|
|
};
|
|
|
|
/** Genuine original Homeworld occupation state already at an active Movement
|
|
* owner. Only conserved later physical cards isolate this ordinary play; the
|
|
* occupation epoch, custody, wallets and clocks stay native. */
|
|
export function createSemutaHomeworldOrdinaryFixture(): SemutaHomeworldOrdinaryFixture {
|
|
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
|
const game = income.game;
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.active, 'a');
|
|
const holder = income.holder, owner = income.canceller;
|
|
assert.equal(semutaEmperorPlayer(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID), true);
|
|
const hajr = moveSemutaEmperorCard(game,
|
|
semutaEmperorCardId(game, card => card.effect === 'hajr'), 'a');
|
|
assert.ok(semutaEmperorPlayer(game, 'a').hand.some(card => card.id === hajr));
|
|
return { game, source: income.game, holder, owner, semuta: SEMUTA_DRUG_ID, hajr,
|
|
action: { type: 'card', card: hajr } };
|
|
}
|
|
|
|
export type SemutaHomeworldTruthtranceFixture = {
|
|
game: Game; holder: string; asker: string; questioned: string;
|
|
truthtrance: string; answer: Action;
|
|
};
|
|
|
|
/** Genuine original Homeworld occupation state; a real Truthtrance declaration,
|
|
* storm priority pass and definite fact answer produce the consumed card. The
|
|
* Truthtrance and the answering Shield are conserved physical cards. */
|
|
export function createSemutaHomeworldTruthtranceFixture(): SemutaHomeworldTruthtranceFixture {
|
|
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
|
let game = income.game;
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.active, 'a');
|
|
const holder = income.holder, asker = 'a', questioned = income.owner;
|
|
const truthtrance = moveSemutaEmperorCard(game,
|
|
semutaEmperorCardId(game, card => card.effect === 'truthtrance'), asker);
|
|
const shieldIndex = game.deck.findIndex(card => card.name === 'Shield');
|
|
assert.ok(shieldIndex >= 0, 'A real Shield must remain in the original deck');
|
|
semutaEmperorPlayer(game, questioned).hand.push(game.deck.splice(shieldIndex, 1)[0]);
|
|
game = applyAction(game, asker, { type: 'card', card: truthtrance, cards: [] });
|
|
while (game.truthtrance?.stage === 'priority') {
|
|
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id))!;
|
|
game = applyAction(game, actor.id, { type: 'truthPass' });
|
|
}
|
|
assert.equal(game.truthtrance?.stage, 'ask');
|
|
game = applyAction(game, game.truthtrance!.queue[0].player, {
|
|
type: 'truthAsk',
|
|
question: { kind: 'fact', target: questioned, fact: { kind: 'hand', name: 'Shield' } },
|
|
});
|
|
assert.equal(viewGame(game, questioned).truthAnswer, 'yes');
|
|
return { game, holder, asker, questioned, truthtrance,
|
|
answer: { type: 'truthAnswer', answer: 'yes' } };
|
|
}
|
|
|
|
export function reactSemutaHomeworldTruthtrance(
|
|
state: Game, fixture: SemutaHomeworldTruthtranceFixture, claim: boolean,
|
|
): Game {
|
|
const event = state.pendingTreacheryDiscard!.batch.event;
|
|
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
|
|
let game = state;
|
|
for (const player of state.players)
|
|
game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
return game;
|
|
}
|
|
|
|
export type SemutaHomeworldDistransFixture = {
|
|
game: Game; holder: string; owner: string; distrans: string; given: string;
|
|
recipient: string; action: Action;
|
|
};
|
|
|
|
/** Genuine original Homeworld occupation state; the Distrans transfer and its
|
|
* given card are conserved physical cards, not minted, and the recipient is a
|
|
* real seat. No clock, wallet or occupation value is assigned. */
|
|
export function createSemutaHomeworldDistransFixture(): SemutaHomeworldDistransFixture {
|
|
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
|
const game = income.game;
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.active, 'a');
|
|
const holder = income.holder, owner = 'a', recipient = 'e';
|
|
const distrans = moveSemutaEmperorCard(game,
|
|
semutaEmperorCardId(game, card => card.effect === 'distrans'), owner);
|
|
const given = moveSemutaEmperorCard(game, semutaEmperorCardId(game, card =>
|
|
card.effect !== 'distrans' && card.kind !== 'special' && card.effect !== 'karama' &&
|
|
card.effect !== 'nullentropyBox'), owner);
|
|
return { game, holder, owner, distrans, given, recipient,
|
|
action: { type: 'card', card: distrans, target: recipient, give: given } };
|
|
}
|
|
|
|
export function reactSemutaHomeworldDistrans(
|
|
state: Game, fixture: SemutaHomeworldDistransFixture, claim: boolean,
|
|
): Game {
|
|
const event = state.pendingTreacheryDiscard!.batch.event;
|
|
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
|
|
let game = state;
|
|
for (const player of state.players)
|
|
game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
return game;
|
|
}
|
|
|
|
export type SemutaHomeworldBoxFixture = {
|
|
game: Game; source: Game; holder: string; box: string; owner: string;
|
|
action: Action; cost: number;
|
|
};
|
|
|
|
/** Genuine original Homeworld occupation state; a real paid Nullentropy Box
|
|
* search runs through the engine's own singleton completion. The searched card
|
|
* and its candidate discard are conserved physical cards, not minted. */
|
|
export function createSemutaHomeworldBoxFixture(): SemutaHomeworldBoxFixture {
|
|
const income = createSemutaHomeworldGuildIncomeFixture({ advanced: false });
|
|
const game = income.game;
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.active, 'a');
|
|
const holder = income.holder, owner = 'a';
|
|
assert.equal(semutaEmperorPlayer(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID), true);
|
|
if (!game.discard.some(card => card.effect !== 'nullentropyBox')) {
|
|
const id = semutaEmperorCardId(game, card => card.effect !== 'nullentropyBox' &&
|
|
card.kind !== 'special' && card.effect !== 'karama');
|
|
moveSemutaEmperorCard(game, id, owner);
|
|
game.discard.push(semutaEmperorPlayer(game, owner).hand.pop()!);
|
|
}
|
|
const box = moveSemutaEmperorCard(game,
|
|
semutaEmperorCardId(game, card => card.effect === 'nullentropyBox'), owner);
|
|
const atreides = semutaEmperorPlayer(game, owner);
|
|
assert.ok(atreides.hand.some(card => card.id === box));
|
|
const action: Action = { type: 'card', card: box };
|
|
return { game, source: income.game, holder, box, owner, action, cost: 2 };
|
|
}
|
|
|
|
export function reactSemutaHomeworldBox(
|
|
state: Game, fixture: SemutaHomeworldBoxFixture, claim: boolean,
|
|
): Game {
|
|
const event = state.pendingTreacheryDiscard!.batch.event;
|
|
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
|
|
let game = state;
|
|
for (const player of state.players)
|
|
game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
return game;
|
|
}
|
|
|
|
export function reactSemutaHomeworldOrdinary(
|
|
state: Game, fixture: SemutaHomeworldOrdinaryFixture, claim: boolean,
|
|
): Game {
|
|
const event = state.pendingTreacheryDiscard!.batch.event;
|
|
if (claim) return applyAction(state, fixture.holder, { type: 'semutaCommit', event });
|
|
let game = state;
|
|
for (const player of state.players)
|
|
game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
return game;
|
|
}
|
|
|
|
/** Homeworld authority that an ordinary card play must not disturb. */
|
|
export function semutaHomeworldOrdinaryState(game: Game) {
|
|
return {
|
|
homeworlds: game.homeworlds,
|
|
history: game.homeworldOccupationHistory,
|
|
percentages: game.homeworldOccupiedPercentageLedger,
|
|
players: game.players.map(player => ({
|
|
id: player.id, faction: player.faction, ally: player.ally, spice: player.spice,
|
|
reserves: player.reserves, tanks: player.tanks, forces: player.forces,
|
|
elites: player.elites, shipped: player.shipped, moved: player.moved,
|
|
})),
|
|
};
|
|
}
|