AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
1/**
2 * plurals.ts — detect inflected -S forms (plurals, 3rd-person-singular verbs) so
3 * they can be kept out of the daily-answer pool. They remain valid as *guesses*.
4 *
5 * The singular of a 5-letter -S form is shorter than five letters (ROOMS -> ROOM,
6 * BOXES -> BOX), so `isWord` MUST recognise words of every length. The historical
7 * bug here passed a 5-letter-only set, so no 4-letter singular ever matched and
8 * the filter silently did nothing.
9 */
10
11// Stems that take "-es" rather than a bare "-s": those ending in a sibilant
12// (S, X, Z) or the digraphs CH / SH (BOXES -> BOX, ASHES -> ASH, MATCHES -> MATCH).
13const SIBILANT_STEM = /([SXZ]|[CS]H)$/;
14
15/**
16 * True when `word` looks like a regular -S inflection of a shorter real word.
17 *
18 * @param word Uppercase A–Z candidate.
19 * @param isWord Predicate that recognises real words of ANY length.
20 */
21export function isInflectedSForm(word: string, isWord: (w: string) => boolean): boolean {
22 if (!word.endsWith('S')) return false;
23 // Regular plural / 3rd-person singular: drop the trailing S (ROOMS -> ROOM).
24 if (isWord(word.slice(0, -1))) return true;
25 // -ES inflection after a sibilant stem (BOXES -> BOX, BUSES -> BUS, ASHES -> ASH).
26 if (word.endsWith('ES')) {
27 const stem = word.slice(0, -2);
28 if (SIBILANT_STEM.test(stem) && isWord(stem)) return true;
29 // Y -> IES inflection (DRIES -> DRY, FLIES -> FLY, SKIES -> SKY).
30 if (word.endsWith('IES') && isWord(word.slice(0, -3) + 'Y')) return true;
31 }
32 return false;
33}