42 lines
2.2 KiB
TypeScript
42 lines
2.2 KiB
TypeScript
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<string, HandInventoryFact['category']> = {
|
|
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 };
|
|
}
|