Players can ask total-hand and primary-role card-count comparisons using the existing count controls and compound facts. Component presentation shares the same default-role classification, including canonical Richese cards, Ix alternate uses, and Ecaz slot-only specials. Only the respondent receives the verified aggregate answer; current facts do not impose future custody promises. Eight new tests cover all 60 catalog cards, real engine actions, malformed input, private worlds, all four bots, JSON continuation, and a legal custody transfer. Expanded production-room SQLite cases cover restored total/category/compound facts, suspended auction state, and one winning concurrent answer/discard. Independent review's two copy issues were fixed before broad verification. Validation: types, lint and 4,200 offline tests passed; production build passed; 40 HTTP tests passed. One stable broad run (check 123036 ms, build 6505 ms, integration 23467 ms). Browser room LC3E55GY verified combined role counts, total hand size, invalid-input feedback and refresh at 390px phone width. All 33 pre-integration rooms preserved unchanged; HTTP checks added 30 rooms. No complete-mode, full-game or publication gate opened. Private report: 20260913-hand-inventory-final/report.json Source fingerprint: c6a69a9e69941cdd1e5734d92afedbf10d31fda36c91d26ca7e15f4fb7c59b18 Efficiency pilot milestone 3: aggregate counter 58,695,053 to 59,144,653 at completed verification = 449,600 tokens, including context/coordination overhead. Two bounded workhorse-model helpers, reused UI and SQLite fixtures, one browser room. Counter is not monetary cost and this differently sized batch does not establish a savings percentage. Final Git/push overhead follows this sample.
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import type { Card } from './cards';
|
|
import {
|
|
CARD_CATEGORIES,
|
|
printedCardCategory,
|
|
type CardCategory,
|
|
} from './card-category';
|
|
|
|
/** A current custody question, not a promise to retain cards or use them. */
|
|
export type HandInventoryFact = {
|
|
kind: 'handInventory';
|
|
category: 'all' | CardCategory;
|
|
compare: 'eq' | 'gte' | 'lte';
|
|
value: number;
|
|
};
|
|
|
|
export const HAND_INVENTORY_LABELS: Record<
|
|
HandInventoryFact['category'],
|
|
string
|
|
> = {
|
|
all: 'All cards in hand',
|
|
weapon: 'Weapons · primary role',
|
|
defense: 'Defenses · primary role',
|
|
worthless: 'Worthless cards',
|
|
hero: 'Cheap Heroes and Heroines',
|
|
special: 'Special cards without a primary battle role',
|
|
};
|
|
|
|
export function parseHandInventoryFact(
|
|
v: Record<string, unknown>,
|
|
): HandInventoryFact {
|
|
if (
|
|
v.kind !== 'handInventory' ||
|
|
(v.category !== 'all' &&
|
|
!CARD_CATEGORIES.includes(v.category as CardCategory))
|
|
)
|
|
throw new Error('Choose all held cards or a known primary card role.');
|
|
if (v.compare !== 'eq' && v.compare !== 'gte' && v.compare !== 'lte')
|
|
throw new Error('Compare held cards using exactly, at least, or at most.');
|
|
if (
|
|
typeof v.value !== 'number' ||
|
|
!Number.isSafeInteger(v.value) ||
|
|
v.value < 0
|
|
)
|
|
throw new Error(
|
|
'Choose a nonnegative whole card count within the safe integer range.',
|
|
);
|
|
return {
|
|
kind: 'handInventory',
|
|
category: v.category as HandInventoryFact['category'],
|
|
compare: v.compare,
|
|
value: v.value,
|
|
};
|
|
}
|
|
|
|
export function handInventoryFactMatches(
|
|
hand: readonly Card[],
|
|
fact: HandInventoryFact,
|
|
): boolean {
|
|
const count =
|
|
fact.category === 'all'
|
|
? hand.length
|
|
: hand.filter((card) => printedCardCategory(card) === fact.category)
|
|
.length;
|
|
return fact.compare === 'eq'
|
|
? count === fact.value
|
|
: fact.compare === 'gte'
|
|
? count >= fact.value
|
|
: count <= fact.value;
|
|
}
|
|
|
|
export function handInventoryFactText(fact: HandInventoryFact): string {
|
|
const comparison =
|
|
fact.compare === 'eq'
|
|
? 'exactly'
|
|
: fact.compare === 'gte'
|
|
? 'at least'
|
|
: 'at most';
|
|
const cards = fact.value === 1 ? 'card' : 'cards';
|
|
const description: Record<HandInventoryFact['category'], string> = {
|
|
all: `${cards} in your hand`,
|
|
weapon: `${cards} whose primary role is a weapon`,
|
|
defense: `${cards} whose primary role is a defense`,
|
|
worthless: `Worthless ${cards}`,
|
|
hero: `Cheap Hero or Cheap Heroine ${cards}`,
|
|
special: `Special ${cards} without a primary battle role`,
|
|
};
|
|
return `you currently hold ${comparison} ${fact.value} ${description[fact.category]}`;
|
|
}
|