52 lines
2.9 KiB
TypeScript
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;
|
|
}
|