AT Mot — a bilingual (EN/FR) daily word game native to the AT Protocol.
1import { WORD_LENGTH } from '../config.js';
2import type { TileState } from './types.js';
3
4/**
5 * Score a guess against the answer using the standard two-pass rules:
6 * 1. Mark exact matches (correct / green).
7 * 2. For the rest, mark present (yellow) only while unused answer letters of
8 * that value remain — so duplicate letters are handled correctly
9 * (a second guessed 'E' goes grey if the answer has only one 'E' and it
10 * was already matched).
11 *
12 * Both inputs MUST already be normalized (uppercase A–Z, length WORD_LENGTH).
13 */
14export function scoreGuess(guess: string, answer: string): TileState[] {
15 if (guess.length !== WORD_LENGTH || answer.length !== WORD_LENGTH) {
16 throw new Error(`scoreGuess expects ${WORD_LENGTH}-letter words`);
17 }
18
19 const states: TileState[] = new Array(WORD_LENGTH).fill('absent');
20 // Count answer letters not yet consumed by a green match.
21 const remaining: Record<string, number> = {};
22
23 for (let i = 0; i < WORD_LENGTH; i++) {
24 const a = answer[i]!;
25 if (guess[i] === a) {
26 states[i] = 'correct';
27 } else {
28 remaining[a] = (remaining[a] ?? 0) + 1;
29 }
30 }
31
32 for (let i = 0; i < WORD_LENGTH; i++) {
33 if (states[i] === 'correct') continue;
34 const g = guess[i]!;
35 if ((remaining[g] ?? 0) > 0) {
36 states[i] = 'present';
37 remaining[g]!--;
38 }
39 }
40
41 return states;
42}
43
44/** True when every tile is correct. */
45export function isWin(states: TileState[]): boolean {
46 return states.every((s) => s === 'correct');
47}