152 lines
4.3 KiB
TypeScript
152 lines
4.3 KiB
TypeScript
import type { FactionId } from './catalog';
|
|
|
|
export const CHOAM_NEXUS_EFFECTS = Object.freeze({
|
|
kulon: 'Kulon',
|
|
laLaLa: 'La La La',
|
|
gamont: 'Trip to Gamont',
|
|
baliset: 'Baliset',
|
|
jubba: 'Jubba Cloak',
|
|
kull: 'Kull Wahad',
|
|
} as const);
|
|
export type NexusChoamEffect = keyof typeof CHOAM_NEXUS_EFFECTS;
|
|
export type NexusChoamContext = {
|
|
turn: number;
|
|
players: readonly { id: string; faction: FactionId }[];
|
|
};
|
|
export type NexusChoamReceipt = {
|
|
version: 1;
|
|
event: string;
|
|
turn: number;
|
|
phase: number;
|
|
owner: string;
|
|
card: string;
|
|
effect: NexusChoamEffect;
|
|
roster: { id: string; faction: FactionId }[];
|
|
signature: string;
|
|
};
|
|
function requireChoam(value: unknown, message: string): asserts value {
|
|
if (!value) throw new Error(message);
|
|
}
|
|
function identifier(value: unknown): value is string {
|
|
return (
|
|
typeof value === 'string' &&
|
|
value.trim().length > 0 &&
|
|
!['__proto__', 'constructor', 'prototype'].includes(value)
|
|
);
|
|
}
|
|
function roster(context: NexusChoamContext) {
|
|
requireChoam(
|
|
Number.isSafeInteger(context.turn) &&
|
|
context.turn > 0 &&
|
|
Array.isArray(context.players) &&
|
|
context.players.length > 0 &&
|
|
context.players.every(
|
|
(p) => p && identifier(p.id) && identifier(p.faction),
|
|
) &&
|
|
new Set(context.players.map((p) => p.id)).size ===
|
|
context.players.length &&
|
|
new Set(context.players.map((p) => p.faction)).size ===
|
|
context.players.length,
|
|
'CHOAM Nexus needs its original faction roster and turn.',
|
|
);
|
|
return context.players
|
|
.map(({ id, faction }) => ({ id, faction }))
|
|
.sort((a, b) => a.id.localeCompare(b.id));
|
|
}
|
|
function phaseMatches(effect: NexusChoamEffect, phase: number): boolean {
|
|
if (!Number.isSafeInteger(phase) || phase < 0 || phase > 8) return false;
|
|
switch (effect) {
|
|
case 'kulon':
|
|
case 'baliset':
|
|
return phase === 5;
|
|
case 'laLaLa':
|
|
return phase === 4;
|
|
case 'gamont':
|
|
return phase === 8;
|
|
case 'jubba':
|
|
return phase === 0;
|
|
// Any phase can contain a Karama attempt; the caller authorizes that timing.
|
|
case 'kull':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
function signature(receipt: NexusChoamReceipt): string {
|
|
return JSON.stringify([
|
|
receipt.version,
|
|
receipt.event,
|
|
receipt.turn,
|
|
receipt.phase,
|
|
receipt.owner,
|
|
receipt.card,
|
|
receipt.effect,
|
|
receipt.roster.map((p) => [p.id, p.faction]),
|
|
]);
|
|
}
|
|
/** Historical evidence is independent of later card custody or alliances.
|
|
* Current eligibility, canonical card inventory and progress belong to the engine. */
|
|
export function validateNexusChoam(
|
|
context: NexusChoamContext,
|
|
receipt: NexusChoamReceipt,
|
|
): void {
|
|
const seats = roster(context);
|
|
requireChoam(
|
|
receipt &&
|
|
typeof receipt === 'object' &&
|
|
!Array.isArray(receipt) &&
|
|
[Object.prototype, null].includes(Object.getPrototypeOf(receipt)) &&
|
|
Object.keys(receipt).sort().join(',') ===
|
|
'card,effect,event,owner,phase,roster,signature,turn,version' &&
|
|
receipt.version === 1 &&
|
|
Number.isSafeInteger(receipt.turn) &&
|
|
receipt.turn > 0 &&
|
|
receipt.turn <= context.turn &&
|
|
identifier(receipt.card) &&
|
|
Object.hasOwn(CHOAM_NEXUS_EFFECTS, receipt.effect) &&
|
|
phaseMatches(receipt.effect, receipt.phase) &&
|
|
seats.some((p) => p.id === receipt.owner && p.faction === 'choam') &&
|
|
receipt.event ===
|
|
JSON.stringify([
|
|
'nexusChoam',
|
|
receipt.turn,
|
|
receipt.phase,
|
|
receipt.owner,
|
|
receipt.card,
|
|
receipt.effect,
|
|
]) &&
|
|
JSON.stringify(receipt.roster) === JSON.stringify(seats) &&
|
|
receipt.signature === signature(receipt),
|
|
'CHOAM Nexus has lost its original card, chosen effect, timing or owner.',
|
|
);
|
|
}
|
|
export function createNexusChoam(
|
|
context: NexusChoamContext,
|
|
owner: string,
|
|
phase: number,
|
|
card: string,
|
|
effect: NexusChoamEffect,
|
|
): NexusChoamReceipt {
|
|
const receipt: NexusChoamReceipt = {
|
|
version: 1,
|
|
event: JSON.stringify([
|
|
'nexusChoam',
|
|
context.turn,
|
|
phase,
|
|
owner,
|
|
card,
|
|
effect,
|
|
]),
|
|
turn: context.turn,
|
|
phase,
|
|
owner,
|
|
card,
|
|
effect,
|
|
roster: roster(context),
|
|
signature: '',
|
|
};
|
|
receipt.signature = signature(receipt);
|
|
validateNexusChoam(context, receipt);
|
|
return receipt;
|
|
}
|