Allow Advanced Auditor inspection choice
Verify and publish checkpoint container / container (push) Successful in 11m25s
Verify and publish checkpoint container / container (push) Successful in 11m25s
This commit is contained in:
1 parent
0681529e8e
commit
7e6e8c2d12
9 files changed
+292
-31
No files matched your search
@@ -25,7 +25,7 @@ export function AuditorDecision({
|
||||
<p className="text-base leading-7">
|
||||
{payment
|
||||
? `Pay ${audit.count} spice to CHOAM to cancel the entire inspection, or allow it without payment.`
|
||||
: `Your Auditor ${audit.survived ? 'survived' : 'died'}. You may inspect ${audit.count} random card${audit.count === 1 ? '' : 's'} from ${opponent?.name ?? 'your opponent'}’s hand. Cards used in this battle are excluded.`}
|
||||
: `Your Auditor ${audit.survived ? 'survived' : 'died'}. You may inspect up to ${audit.count} random card${audit.count === 1 ? '' : 's'} from ${opponent?.name ?? 'your opponent'}’s hand. Cards used in this battle are excluded.`}
|
||||
</p>
|
||||
{!payment && (
|
||||
<p className="text-base leading-7">
|
||||
@@ -34,25 +34,33 @@ export function AuditorDecision({
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
act(
|
||||
payment
|
||||
? {
|
||||
type: 'decision',
|
||||
event: audit.event,
|
||||
pay: true,
|
||||
count: audit.count,
|
||||
}
|
||||
: { type: 'decision', event: audit.event, audit: true },
|
||||
)
|
||||
}
|
||||
>
|
||||
{payment
|
||||
? `Pay ${audit.count} spice · cancel audit`
|
||||
: 'Audit opposing hand'}
|
||||
</Button>
|
||||
{payment ? (
|
||||
<Button
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
act({
|
||||
type: 'decision',
|
||||
event: audit.event,
|
||||
pay: true,
|
||||
count: audit.count,
|
||||
})
|
||||
}
|
||||
>
|
||||
{`Pay ${audit.count} spice · cancel audit`}
|
||||
</Button>
|
||||
) : (
|
||||
[1, 2].filter((count) => count <= audit.count).map((count) => (
|
||||
<Button
|
||||
key={count}
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
act({ type: 'decision', event: audit.event, audit: true, count })
|
||||
}
|
||||
>
|
||||
{`Inspect ${count} card${count === 1 ? '' : 's'} · ${count} spice to cancel`}
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={busy}
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
7 September 2026 checkpoint, with an 8 October Advanced revival follow-up. Inspection, component identity and custody rules are connected. This does not certify the complete CHOAM faction or all expansion combinations; public Advanced and expansion starts remain gated.
|
||||
|
||||
## Advanced inspection choice — 29 October 2026
|
||||
|
||||
The user-authorized revised rules, physical p. 29, allow a surviving Auditor
|
||||
to inspect **up to two** randomly selected unused opposing cards. This
|
||||
implementation interprets that wording as a choice of one or two, bounded by
|
||||
the current eligible hand; the killed Auditor remains limited to one. The
|
||||
declared choice carries through Karama, cancellation and private sampling.
|
||||
The opponent can cancel the entire inspection for one spice per card actually
|
||||
viewable within that choice. Existing in-flight opportunities and old clients
|
||||
without a count retain the previous maximum. This is a reading of the supplied
|
||||
Advanced rules, not a publisher clarification or a change to Basic play.
|
||||
|
||||
## Semuta and current-hand eligibility — 11 October 2026
|
||||
|
||||
The explicit Semuta preview now preserves the original pending audit through
|
||||
|
||||
@@ -17,6 +17,15 @@ open mode gates.
|
||||
|
||||
## Current checkpoint and work
|
||||
|
||||
**Advanced CHOAM Auditor choice — 29 October. Prototyped / Partial.** A
|
||||
surviving Auditor may select one or two eligible cards; the chosen count carries
|
||||
through Karama, the actual cancellation price and the private random draw.
|
||||
Old declarations and in-flight opportunities retain the maximum. Thirty-eight
|
||||
related unit cases, a rendered control check, TypeScript, lint and the
|
||||
production build pass; complete Advanced rules and deployed acceptance remain
|
||||
open.
|
||||
[Scope](CHOAM_AUDITOR.md#advanced-inspection-choice--29-october-2026).
|
||||
|
||||
**Gitea verification diagnostics — 29 October. Partial.** The approved
|
||||
runner network configuration was already applied, and later runs reached the
|
||||
application and published images. Run #85 failed in administrator acceptance
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Rule decision index
|
||||
|
||||
## Advanced Auditor inspection choice — 29 October 2026
|
||||
|
||||
The authorized revised rules, physical p. 29, say a surviving Auditor may
|
||||
inspect up to two cards. The Advanced implementation interprets this as a
|
||||
choice of one or two eligible random cards, with cancellation charged per
|
||||
card actually viewable within the choice. This is not a publisher ruling and
|
||||
does not alter Basic play. [Scope](CHOAM_AUDITOR.md#advanced-inspection-choice--29-october-2026).
|
||||
|
||||
## Truthtrance ally-funding updates — 29 October 2026
|
||||
|
||||
The first Basic current-phase ally-funding question counts a future accepted
|
||||
|
||||
+15
-1
@@ -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
@@ -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
@@ -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.',
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction,
|
||||
createGame,
|
||||
newPlayer,
|
||||
viewGame,
|
||||
type Action,
|
||||
type Game,
|
||||
} from '../game/engine';
|
||||
import { baseDeck, createAuditorLeader, type Card } from '../game/cards';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { AuditorDecision } from '../components/choam-auditor';
|
||||
|
||||
const player = (g: Game, id: string) => g.players.find((p) => p.id === id)!;
|
||||
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
||||
function passResponses(g: Game) {
|
||||
for (let i = 0; g.response && i < 30; i++) {
|
||||
const id = g.players.find((p) => !g.response!.passed.includes(p.id))!.id;
|
||||
g = applyAction(g, id, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(g.response, null);
|
||||
return g;
|
||||
}
|
||||
function hold(g: Game, kind: Card['kind']) {
|
||||
const index = g.deck.findIndex((card) => card.kind === kind);
|
||||
assert.ok(index >= 0);
|
||||
const [card] = g.deck.splice(index, 1);
|
||||
player(g, 'e').hand.push(card);
|
||||
return card.id;
|
||||
}
|
||||
/** Earn the opportunity with a real Advanced battle, not a fabricated pending audit. */
|
||||
function offer() {
|
||||
let g = createGame('AUDITCHOICE', newPlayer('c', 'CHOAM', 'choam'), true);
|
||||
g.players.push(newPlayer('e', 'Emperor', 'emperor'), newPlayer('g', 'Guild', 'guild'));
|
||||
Object.assign(g, {
|
||||
status: 'playing', phase: 6, turn: 2, active: 'c', order: ['c', 'e', 'g'],
|
||||
storm: 18, deck: baseDeck(), discard: [],
|
||||
});
|
||||
player(g, 'c').leaders.push(createAuditorLeader());
|
||||
for (const p of g.players) {
|
||||
p.hand = [];
|
||||
p.spice = 20;
|
||||
p.traitors = [];
|
||||
p.forces = p.id === 'g' ? {} : { 'arrakeen:10': 3 };
|
||||
p.reserves = p.id === 'g' ? 20 : 17;
|
||||
}
|
||||
const eligible = [hold(g, 'shield'), hold(g, 'snooper'), hold(g, 'projectile')];
|
||||
const karamaIndex = g.deck.findIndex((card) => card.effect === 'karama');
|
||||
assert.ok(karamaIndex >= 0);
|
||||
player(g, 'g').hand.push(g.deck.splice(karamaIndex, 1)[0]);
|
||||
g = applyAction(g, 'c', { type: 'chooseBattle', territory: 'arrakeen', target: 'e' });
|
||||
for (let i = 0; i < 30; i++) {
|
||||
g = passResponses(g);
|
||||
if (g.battle?.preLeader && !g.battle.preLeader.closed) {
|
||||
const id = ['c', 'e'].find((p) => !g.battle!.preLeader!.ready.includes(p))!;
|
||||
g = applyAction(g, id, { type: 'battlePreparationReady', event: g.battle.event });
|
||||
} else if (g.battle?.preparation) {
|
||||
g = applyAction(g, g.battle.preparation.owner, { type: 'declineBattlePower' });
|
||||
} else break;
|
||||
}
|
||||
g = applyAction(g, 'c', { type: 'battlePlan', leader: 'choam-auditor', dial: 0, support: 0 });
|
||||
g = applyAction(g, 'e', { type: 'battlePlan', leader: 'emperor-0', dial: 0, support: 0 });
|
||||
g = applyAction(g, 'c', { type: 'traitorCall', call: false });
|
||||
g = applyAction(g, 'e', { type: 'traitorCall', call: false });
|
||||
g = passResponses(g);
|
||||
assert.equal(g.decision?.kind, 'choamAudit');
|
||||
assert.equal(g.pendingAuditor?.survived, true);
|
||||
assert.equal(viewGame(g, 'c').auditor?.count, 2);
|
||||
return { g, eligible };
|
||||
}
|
||||
function declare(g: Game, count?: number) {
|
||||
return applyAction(g, 'c', {
|
||||
type: 'decision', event: g.pendingAuditor!.event, audit: true,
|
||||
...(count === undefined ? {} : { count }),
|
||||
});
|
||||
}
|
||||
|
||||
void test('a surviving Auditor may choose one card and the opponent pays one spice to cancel', () => {
|
||||
let { g } = offer();
|
||||
const event = g.pendingAuditor!.event;
|
||||
const before = { c: player(g, 'c').spice, e: player(g, 'e').spice };
|
||||
g = declare(g, 1);
|
||||
assert.equal(g.response?.kind, 'choamAudit');
|
||||
g = passResponses(reload(g));
|
||||
assert.equal(g.decision?.kind, 'choamAuditPayment');
|
||||
assert.equal(viewGame(g, 'e').auditor?.count, 1);
|
||||
g = applyAction(reload(g), 'e', { type: 'decision', event, pay: true, count: 1 });
|
||||
assert.equal(player(g, 'e').spice, before.e - 1);
|
||||
assert.equal(player(g, 'c').spice, before.c + 1);
|
||||
assert.equal(g.auditorInsight ?? null, null);
|
||||
assert.equal(g.pendingAuditor, null);
|
||||
});
|
||||
|
||||
void test('Auditor controls show each valid inspection size and its cancellation price', () => {
|
||||
const { g } = offer();
|
||||
const render = (game: Game, id: string) => renderToStaticMarkup(createElement(AuditorDecision, {
|
||||
game: viewGame(game, id), act: () => {}, busy: false,
|
||||
}));
|
||||
const offerHtml = render(g, 'c');
|
||||
assert.match(offerHtml, /Inspect 1 card · 1 spice to cancel/);
|
||||
assert.match(offerHtml, /Inspect 2 cards · 2 spice to cancel/);
|
||||
const payment = passResponses(declare(g, 1));
|
||||
assert.match(render(payment, 'e'), /Pay 1 spice · cancel audit/);
|
||||
});
|
||||
|
||||
void test('a one-card choice survives the response and privately samples one eligible physical card', () => {
|
||||
const offered = offer();
|
||||
let g = offered.g;
|
||||
const { eligible } = offered;
|
||||
const event = g.pendingAuditor!.event;
|
||||
const hand = structuredClone(player(g, 'e').hand);
|
||||
g = passResponses(reload(declare(g, 1)));
|
||||
assert.equal(viewGame(g, 'e').auditor?.count, 1);
|
||||
g = applyAction(reload(g), 'e', { type: 'decision', event, pay: false });
|
||||
const insight = viewGame(g, 'c').auditorInsight;
|
||||
assert.equal(insight?.cards.length, 1);
|
||||
assert.ok(eligible.includes(insight!.cards[0].id));
|
||||
assert.deepEqual(player(g, 'e').hand, hand);
|
||||
assert.equal(viewGame(g, 'e').auditorInsight, null);
|
||||
assert.equal(viewGame(g, 'g').auditorInsight, null);
|
||||
});
|
||||
|
||||
void test('two-card and legacy omitted-count declarations retain the full two-spice price', () => {
|
||||
for (const requested of [2, undefined]) {
|
||||
let { g } = offer();
|
||||
const event = g.pendingAuditor!.event;
|
||||
const before = { c: player(g, 'c').spice, e: player(g, 'e').spice };
|
||||
g = passResponses(declare(g, requested));
|
||||
assert.equal(viewGame(g, 'e').auditor?.count, 2);
|
||||
g = applyAction(g, 'e', { type: 'decision', event, pay: true, count: 2 });
|
||||
assert.equal(player(g, 'e').spice, before.e - 2);
|
||||
assert.equal(player(g, 'c').spice, before.c + 2);
|
||||
}
|
||||
});
|
||||
|
||||
void test('invalid audit counts do not consume the opportunity', () => {
|
||||
const { g } = offer();
|
||||
for (const count of [0, -1, 1.5, 3, '1', null]) {
|
||||
const before = reload(g);
|
||||
const action = { type: 'decision', event: g.pendingAuditor!.event, audit: true, count } as unknown as Action;
|
||||
assert.throws(() => applyAction(g, 'c', action));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('all four minimal bots can still submit the legacy maximum audit action', () => {
|
||||
const { g } = offer();
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(g, 'c');
|
||||
view.players.find((p) => p.id === 'c')!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.equal(action.type, 'decision');
|
||||
assert.equal(action.audit, true);
|
||||
const next = passResponses(applyAction(reload(g), 'c', action));
|
||||
assert.equal(viewGame(next, 'e').auditor?.count, 2);
|
||||
}
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createAuditorLeader,
|
||||
auditCandidates,
|
||||
auditCount,
|
||||
selectedAuditCount,
|
||||
sampleAuditCards,
|
||||
} from '../game/choam-auditor';
|
||||
import { matchingTraitor, traitorDeck } from '../game/traitors';
|
||||
@@ -216,6 +217,43 @@ void test('partial sampling honors exclusions and valid RNG endpoints without ch
|
||||
assert.deepEqual(original, before);
|
||||
});
|
||||
|
||||
void test('Auditor can select one uniformly from two eligible cards without changing the hand', () => {
|
||||
const original = hand();
|
||||
const before = structuredClone(original);
|
||||
const used = original.slice(2).map((card) => card.id);
|
||||
assert.equal(auditCount(original, used, true), 2);
|
||||
for (const [draw, expected] of [[0, 'shield-a'], [0.5, 'shield-b']] as const) {
|
||||
let calls = 0;
|
||||
const selected = sampleAuditCards(original, used, true, () => { calls++; return draw; }, 1);
|
||||
assert.deepEqual(selected.map((card) => card.id), [expected]);
|
||||
assert.equal(calls, 1);
|
||||
selected[0].name = 'Changed snapshot';
|
||||
}
|
||||
assert.deepEqual(original, before);
|
||||
assert.deepEqual(used, ['hero', 'poison', 'portable']);
|
||||
});
|
||||
|
||||
void test('explicit Auditor inspection sizes respect actual survival and eligible bounds', () => {
|
||||
const original = hand();
|
||||
const before = structuredClone(original);
|
||||
const neverDraw = (): number => { throw new Error('Unexpected randomness'); };
|
||||
assert.equal(selectedAuditCount(0), 0);
|
||||
assert.equal(selectedAuditCount(2), 2);
|
||||
assert.equal(selectedAuditCount(2, 1), 1);
|
||||
assert.equal(selectedAuditCount(2, 2), 2);
|
||||
assert.deepEqual(sampleAuditCards(original.slice(0, 2), [], true, neverDraw, 2), original.slice(0, 2));
|
||||
assert.deepEqual(sampleAuditCards(original.slice(0, 1), [], false, neverDraw, 1), original.slice(0, 1));
|
||||
for (const bad of [0, -1, 1.5, 3, NaN, Infinity, null, '1']) {
|
||||
assert.throws(() => sampleAuditCards(original, [], true, neverDraw, bad as number), /inspection count/);
|
||||
}
|
||||
assert.throws(() => sampleAuditCards(original, [], false, neverDraw, 2), /inspection count/);
|
||||
assert.throws(() => sampleAuditCards(original, original.map((card) => card.id), true, neverDraw, 1), /inspection count/);
|
||||
assert.throws(() => selectedAuditCount(-1), /inspection maximum/);
|
||||
assert.throws(() => selectedAuditCount(3), /inspection maximum/);
|
||||
assert.throws(() => selectedAuditCount(NaN), /inspection maximum/);
|
||||
assert.deepEqual(original, before);
|
||||
});
|
||||
|
||||
void test('invalid physical pools, survival states and RNG draws reject without mutating inputs', () => {
|
||||
const original = hand();
|
||||
const before = structuredClone(original);
|
||||
|
||||
Reference in new issue
Block a user