Files
Dune/game/truthtrance-freeform-facts-v6.ts
T
Quintine cc9cc0945d
Verify and publish checkpoint container / container (push) Canceled after 0s
Expand freeform Truthtrance current facts
2026-10-29 18:46:17 +11:00

52 lines
2.9 KiB
TypeScript

import { TERRITORIES } from './board';
import { FACTIONS } from './catalog';
import { knowledgeFactInputError, SPICE_PEEK_VALUES, type KnowledgeFact } from './truthtrance-knowledge';
import type { ForceCountFact } from './truthtrance-force-count';
/** Anchored present facts only. This version is independent of earlier saved
* interpretations and consults printed public registries, never a game state. */
export function parseFreeformFactV6(text: string): KnowledgeFact | ForceCountFact | null {
if (typeof text !== 'string' || text.length > 500 || /[\r\n]/.test(text)) return null;
const input = text.trim().toLowerCase().replace(/[ \t]+/g, ' ');
const comparison = (value: string): 'eq' | 'gte' | 'lte' =>
value === 'at least' ? 'gte' : value === 'at most' ? 'lte' : 'eq';
const knowledge = (fact: KnowledgeFact): KnowledgeFact | null =>
knowledgeFactInputError(fact) ? null : fact;
const faction = /^(?:did you predict (.+?)(?: to win)?|is your predicted faction (.+?))\??$/.exec(input);
if (faction) {
const name = faction[1] ?? faction[2];
const matches = FACTIONS.filter(candidate => candidate.name.toLowerCase() === name || candidate.id.toLowerCase() === name);
return matches.length === 1 ? knowledge({kind:'prediction', field:'faction', faction:matches[0].id}) : null;
}
const numeric = /^is your (predicted turn|storm forecast|(?:submitted )?storm dial) (at least|exactly|at most) ([0-9]+)\??$/.exec(input);
if (numeric) {
const compare = comparison(numeric[2]), value = Number(numeric[3]);
const fact: KnowledgeFact = numeric[1] === 'predicted turn'
? {kind:'prediction', field:'turn', compare, value}
: {kind:numeric[1] === 'storm forecast' ? 'stormForecast' : 'stormDial', compare, value};
return knowledge(fact);
}
const spice = /^is the next spice blow (?:a |an )?(sandworm|worm|sandtrout)\??$/.exec(input);
if (spice) return knowledge({kind:'spicePeek', value:spice[1] === 'sandworm' ? 'worm' : spice[1]});
const blow = /^is the next spice blow (?:in|at) (.+?)\??$/.exec(input);
if (blow) {
const name = blow[1];
const matches = TERRITORIES.filter(candidate => SPICE_PEEK_VALUES.includes(candidate.id) &&
(candidate.name.toLowerCase() === name || candidate.name.toLowerCase().replace(/^the /, '') === name));
return matches.length === 1 ? knowledge({kind:'spicePeek', value:matches[0].id}) : null;
}
const physical = /^do you have (at least|exactly|at most) ([0-9]+) (?:physical )?forces? (in (?:your )?reserves|in (?:the |your )?tanks|on the board)\??$/.exec(input);
if (physical) {
const value = Number(physical[2]);
if (!Number.isSafeInteger(value) || value < 0) return null;
const zone: ForceCountFact['zone'] = physical[3] === 'on the board' ? {kind:'board'}
: physical[3].endsWith('reserves') ? {kind:'reserves'} : {kind:'tanks'};
return {kind:'forceCount', zone, counter:'total', compare:comparison(physical[1]), value};
}
return null;
}