import { parseFreeformFactV2, type FreeformFactV2 } from './truthtrance-freeform-facts-v2'; import type { HandInventoryFact } from './truthtrance-hand-inventory'; export type FreeformFactV3 = FreeformFactV2 | HandInventoryFact; /** Exact category phrases only. These mean the existing printed primary role, * not every card a faction might use in that slot through a special power. */ const categories: Record = { weapon: 'weapon', weapons: 'weapon', defense: 'defense', defenses: 'defense', 'worthless card': 'worthless', 'worthless cards': 'worthless', 'projectile weapon': 'projectileWeapon', 'projectile weapons': 'projectileWeapon', 'poison weapon': 'poisonWeapon', 'poison weapons': 'poisonWeapon', 'projectile defense': 'projectileDefense', 'projectile defenses': 'projectileDefense', 'poison defense': 'poisonDefense', 'poison defenses': 'poisonDefense', }; /** * Version 3 extends the stable v2 grammar with anchored, current hand-category * questions. Saved v1 and v2 receipts must continue using their own parsers. * The caller restricts this to fact-scoped questions and displays the primary * role interpretation before an answer is checked. */ export function parseFreeformFactV3(text: string): FreeformFactV3 | null { if (typeof text !== 'string' || text.length > 500) return null; const existing = parseFreeformFactV2(text); if (existing) return existing; const input = text.trim(); const match = /^do[ \t]+you[ \t]+have[ \t]+(?:(a|an|any)|(at[ \t]+least|exactly|at[ \t]+most)[ \t]+([0-9]+))[ \t]+([a-z]+(?:[ \t]+[a-z]+)*?)(?:[ \t]+in[ \t]+your[ \t]+hand)?[ \t]*\??$/i.exec(input); if (!match) return null; const label = match[4].toLowerCase().replace(/[ \t]+/g, ' '); if (!Object.hasOwn(categories, label)) return null; const category = categories[label]; if (match[1] && /^(?:a|an)$/i.test(match[1]) && label.endsWith('s')) return null; const count = match[3] === undefined ? 1 : Number(match[3]); if (!Number.isSafeInteger(count) || count < 0) return null; const comparison = match[2]?.toLowerCase().replace(/[ \t]+/g, ' '); return { kind: 'handInventory', category, compare: comparison === 'exactly' ? 'eq' : comparison === 'at most' ? 'lte' : 'gte', value: count }; }