Allow Advanced Auditor inspection choice
Verify and publish checkpoint container / container (push) Successful in 11m25s

This commit is contained in:
Quintine committed 2026-10-29 05:52:58 +11:00
1 parent 0681529e8e
commit 7e6e8c2d12
9 files changed
+292 -31

No files matched your search

+15 -1
View File
@@ -55,23 +55,37 @@ export function auditCount(
);
}
/** Validate the holder's chosen size against the actual eligible maximum.
* An omitted choice preserves the legacy maximum, including an empty audit. */
export function selectedAuditCount(maximum: number, selectedCount?: number): number {
if (!Number.isInteger(maximum) || maximum < 0 || maximum > 2)
throw new Error('Auditor requires a valid inspection maximum.');
if (selectedCount === undefined) return maximum;
if (!Number.isInteger(selectedCount) || selectedCount < 1 || selectedCount > maximum)
throw new Error('Auditor inspection count must be between one and the eligible maximum.');
return selectedCount;
}
/**
* 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.
* Omit selectedCount for the legacy maximum; a supplied count must be at
* least one and no greater than the current eligible inspection maximum.
*/
export function sampleAuditCards(
hand: readonly Card[],
usedIds: readonly string[],
survived: boolean,
random: () => number,
selectedCount?: 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);
const count = selectedAuditCount(Math.min(limit, eligible.length), selectedCount);
if (count === eligible.length) return eligible;
for (let i = 0; i < count; i++) {
const draw = random();
+18 -7
View File
@@ -2346,6 +2346,8 @@ export type Game = {
survived: boolean;
usedCards: string[];
stage: 'offer' | 'response' | 'payment';
/** Chosen maximum; older in-flight audits omit this and inspect the maximum. */
count?: number;
} | null;
auditorInsight?: {
event: string;
@@ -35521,10 +35523,9 @@ function finishBattle(g: Game) {
function currentAuditCount(g: Game) {
const pending = g.pendingAuditor;
return pending
? auditCount(
getPlayer(g, pending.opponent).hand,
pending.usedCards,
pending.survived,
? Math.min(
pending.count ?? (pending.survived ? 2 : 1),
auditCount(getPlayer(g, pending.opponent).hand, pending.usedCards, pending.survived),
)
: 0;
}
@@ -35554,6 +35555,7 @@ function finishAuditor(
pending.usedCards,
pending.survived,
random,
count,
);
g.auditorInsight = {
event: pending.event,
@@ -35617,6 +35619,9 @@ function auditorIntegrity(g: Game) {
g.players.some((p) => p.id === pending.opponent) &&
typeof pending.survived === 'boolean' &&
['offer', 'response', 'payment'].includes(pending.stage) &&
(pending.count === undefined ||
(pending.stage !== 'offer' && Number.isInteger(pending.count) &&
pending.count >= 1 && pending.count <= (pending.survived ? 2 : 1))) &&
Array.isArray(pending.usedCards) &&
pending.usedCards.every((id) => typeof id === 'string') &&
new Set(pending.usedCards).size === pending.usedCards.length,
@@ -43808,11 +43813,17 @@ function applyActionInner(
requireRule(
typeof action.audit === 'boolean' &&
action.pay === undefined &&
action.count === undefined,
'Choose whether to use the Auditor.',
(action.audit
? currentAuditCount(g) > 0 &&
(action.count === undefined ||
(typeof action.count === 'number' && Number.isInteger(action.count) &&
action.count >= 1 && action.count <= currentAuditCount(g)))
: action.count === undefined),
'Choose a valid number of cards to audit or decline.',
);
if (!action.audit) finishAuditor(g, 'decline');
else {
pending.count = typeof action.count === 'number' ? action.count : currentAuditCount(g);
pending.stage = 'response';
g.response = {
kind: 'choamAudit',
@@ -43822,7 +43833,7 @@ function applyActionInner(
};
log(
g,
`${p.name} declared an Auditor inspection. Opposing Karama cancellation resolves before any inspection payment.`,
`${p.name} declared an Auditor inspection of up to ${pending.count} card${pending.count === 1 ? '' : 's'}. Opposing Karama cancellation resolves before any inspection payment.`,
);
}
} else {
+3 -3
View File
@@ -5010,9 +5010,9 @@ export const RULE_TOPICS: RuleTopic[] = [
'An additional strength-2 leader inspects unused opposing hand cards after battle.',
steps: [
'Advanced CHOAM adds the Auditor and its matching traitor identity before setup draws. The five ordinary CHOAM leaders are unchanged. The Auditor can also appear in the remaining Face Dancer pool.',
'Using the Auditor in a battle grants an optional inspection of two random opposing hand cards if it survives, or one if it dies. Victory is not required. Cards that opposing player used in the battle are excluded, including their retained weapon, defense, hero or late Portable Snooper. A later-acquired card used only by the other combatant can remain eligible; the current hand determines the pool.',
'Choose Audit or Decline after battle cleanup. The table completes battle income, tech claims and Harkonnen capture before this choice, then resolves Face Dancing afterward. Auditor survival is recorded from the battle itself.',
'Karama cancellation resolves first. If the power remains active, the opponent can pay CHOAM one spice per actually viewable card to cancel the entire inspection. A surviving Auditor with only one eligible card costs one spice to stop. There is no partial payment.',
'Using the Auditor in a battle grants an optional inspection of up to two random opposing hand cards if it survives, or one if it dies. Victory is not required. Cards that opposing player used in the battle are excluded, including their retained weapon, defense, hero or late Portable Snooper. A later-acquired card used only by the other combatant can remain eligible; the current hand determines the pool.',
'Choose how many cards to inspect, or decline, after battle cleanup. The table completes battle income, tech claims and Harkonnen capture before this choice, then resolves Face Dancing afterward. Auditor survival is recorded from the battle itself.',
'Karama cancellation resolves first. If the power remains active, the opponent can pay CHOAM one spice per actually viewable card within the chosen count to cancel the entire inspection. Choosing one card costs at most one spice to stop. There is no partial payment.',
'If the opponent cannot afford the full payment, inspection proceeds automatically. An empty eligible hand needs no decision. Sampling happens once after cancellation choices; no card is transferred or discarded.',
'Only CHOAM receives the inspected faces. Its private snapshot survives refresh and the transition out of Battle, and expires at the next battle or turn. It does not track later changes in the opposing hand and needs no confirmation to continue.',
'The Auditor cannot be captured, acquired as a foreign ghola by Tleilaxu, or receive a leader skill. CHOAM’s own Ghola card can revive it. Supplied Advanced p29 permits ordinary revival each turn even after repeated deaths, for two spice before discounts, using the usual one-leader allowance.',