Files
Dune/game/choam-auditor.ts
T
2026-09-08 07:37:52 +10:00

90 lines
2.8 KiB
TypeScript

import type { Card } from './cards';
export {
CHOAM_AUDITOR_ID,
isAuditorLeader,
createAuditorLeader,
} from './cards';
function physicalId(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
/**
* GF9 CHOAM & Richese p. 8: exclude cards actually used in this battle,
* including any subsequently retained cards. The engine supplies those IDs
* and the opponent's current hand; this helper does not discover hidden state.
*/
export function auditCandidates(
hand: readonly Card[],
usedIds: readonly string[],
): Card[] {
if (
!Array.isArray(hand) ||
!Array.isArray(usedIds) ||
usedIds.some((id) => !physicalId(id))
)
throw new Error('Auditor requires a hand and physical used-card IDs.');
const used = new Set(usedIds);
const seen = new Set<string>();
const eligible: Card[] = [];
for (const card of hand) {
if (!card || !physicalId(card.id) || seen.has(card.id))
throw new Error('Auditor requires unique physical cards in the hand.');
seen.add(card.id);
if (!used.has(card.id)) eligible.push(structuredClone(card));
}
return eligible;
}
function inspectionLimit(survived: boolean): number {
if (typeof survived !== 'boolean')
throw new Error('Auditor requires the actual leader survival outcome.');
return survived ? 2 : 1;
}
/** Also the price to cancel the entire audit (printed p. 10 FAQ). */
export function auditCount(
hand: readonly Card[],
usedIds: readonly string[],
survived: boolean,
): number {
return Math.min(
inspectionLimit(survived),
auditCandidates(hand, usedIds).length,
);
}
/**
* Call once after the engine resolves cancellation/payment, and persist the
* resulting private snapshot. A read or projection must never call this helper.
* Partial Fisher-Yates samples physical cards uniformly without replacement.
* Selecting the entire eligible pool consumes no randomness.
*/
export function sampleAuditCards(
hand: readonly Card[],
usedIds: readonly string[],
survived: boolean,
random: () => number,
): Card[] {
const limit = inspectionLimit(survived);
const eligible = auditCandidates(hand, usedIds);
if (typeof random !== 'function')
throw new Error('Auditor requires a server-supplied random source.');
const count = Math.min(limit, eligible.length);
if (count === eligible.length) return eligible;
for (let i = 0; i < count; i++) {
const draw = random();
if (
typeof draw !== 'number' ||
!Number.isFinite(draw) ||
draw < 0 ||
draw >= 1
)
throw new Error('Auditor random draws must be finite numbers in [0, 1).');
const j = i + Math.floor(draw * (eligible.length - i));
[eligible[i], eligible[j]] = [eligible[j], eligible[i]];
}
return eligible.slice(0, count);
}