92 lines
4.1 KiB
TypeScript
92 lines
4.1 KiB
TypeScript
import type { Card } from './cards';
|
|
import type { Game } from './engine';
|
|
import { nexusCardMode } from './nexus-cards';
|
|
import { nexusCleanPlayBlocked } from './nexus-play-boundary';
|
|
|
|
export type NexusChoamBetrayalOffer = Readonly<{
|
|
event: string;
|
|
blocked: string | null;
|
|
target: Readonly<{ id: string; name: string; handSize: number | null }>;
|
|
}>;
|
|
|
|
/** The sampled card is a private, committed Treachery identity, not offer data. */
|
|
export type NexusChoamBetrayal = {
|
|
event: string;
|
|
turn: number;
|
|
phase: number;
|
|
owner: string;
|
|
target: string;
|
|
card: string;
|
|
handBefore: number;
|
|
stage: 'discard' | 'complete';
|
|
signature: string;
|
|
};
|
|
|
|
export function nexusChoamBetrayalEvent(
|
|
turn: number, phase: number, owner: string, target: string,
|
|
): string {
|
|
return JSON.stringify(['nexusChoamBetrayal', turn, phase, owner, target]);
|
|
}
|
|
|
|
export function nexusChoamBetrayalSignature(record: NexusChoamBetrayal): string {
|
|
return JSON.stringify([record.event, record.turn, record.phase, record.owner,
|
|
record.target, record.card, record.handBefore, record.stage]);
|
|
}
|
|
|
|
/** The hand count is public during Bidding only; identities are never offer data. */
|
|
export function quoteNexusChoamBetrayal(
|
|
g: Game, owner: string, automaticPending = false,
|
|
): NexusChoamBetrayalOffer | null {
|
|
const holder = g.players.find(player => player.id === owner);
|
|
const choam = g.players.find(player => player.faction === 'choam');
|
|
if (!holder || !choam || choam.id === owner ||
|
|
g.nexusCards?.cards?.hands[owner] !== 'choam' ||
|
|
nexusCardMode('choam', holder.faction, g.players.map(player => player.faction)) !== 'betrayal')
|
|
return null;
|
|
const handSize = g.phase === 3 ? choam.hand.length : null;
|
|
let blocked: string | null = null;
|
|
if (holder.ally) blocked = 'Use a Nexus card while unallied.';
|
|
else if (g.status !== 'playing') blocked = 'Use CHOAM Betrayal during play.';
|
|
else if (nexusCleanPlayBlocked(g, automaticPending))
|
|
blocked = 'Finish the current interaction before using CHOAM Betrayal.';
|
|
else if (handSize === 0) blocked = 'CHOAM has no Treachery cards to discard.';
|
|
return {
|
|
event: nexusChoamBetrayalEvent(g.turn, g.phase, owner, choam.id),
|
|
blocked,
|
|
target: { id: choam.id, name: choam.name, handSize },
|
|
};
|
|
}
|
|
|
|
/** Verify intrinsic history against the original seated Betrayal mode and one
|
|
* conserved physical card, without requiring its former hand to remain held. */
|
|
export function validateNexusChoamBetrayal(
|
|
g: Game, record: NexusChoamBetrayal, physicalCards: readonly Card[],
|
|
): void {
|
|
const keys = 'card,event,handBefore,owner,phase,signature,stage,target,turn';
|
|
const owner = record && g.players.find(player => player.id === record.owner);
|
|
const target = record && g.players.find(player => player.id === record.target);
|
|
if (!record || typeof record !== 'object' || Array.isArray(record) ||
|
|
Object.keys(record).sort().join(',') !== keys ||
|
|
!owner || !target || owner.id === target.id || target.faction !== 'choam' ||
|
|
owner.faction === 'choam' ||
|
|
nexusCardMode('choam', owner.faction, g.players.map(player => player.faction)) !== 'betrayal' ||
|
|
!Number.isSafeInteger(g.turn) || g.turn < 1 ||
|
|
!Number.isSafeInteger(g.phase) || g.phase < 0 || g.phase > 8 ||
|
|
!Number.isSafeInteger(record.turn) || record.turn < 1 || record.turn > g.turn ||
|
|
!Number.isSafeInteger(record.phase) || record.phase < 0 || record.phase > 8 ||
|
|
(record.turn === g.turn && record.phase > g.phase) ||
|
|
!Number.isSafeInteger(record.handBefore) || record.handBefore < 1 ||
|
|
typeof record.card !== 'string' || !record.card.trim() ||
|
|
(record.stage !== 'discard' && record.stage !== 'complete') ||
|
|
record.event !== nexusChoamBetrayalEvent(record.turn, record.phase,
|
|
record.owner, record.target) ||
|
|
record.signature !== nexusChoamBetrayalSignature(record) ||
|
|
!Array.isArray(physicalCards))
|
|
throw new Error('CHOAM Betrayal has lost its original roster, event or physical Treachery card.');
|
|
let matches = 0;
|
|
for (const card of physicalCards)
|
|
if (card?.id === record.card) matches++;
|
|
if (matches !== 1)
|
|
throw new Error('CHOAM Betrayal has lost its original physical Treachery card.');
|
|
}
|