Pārlūkot izejas kodu

fix(solver): cap duplicated letters that are gray and non-gray in one row

When a guess repeats a letter and the result marks one occurrence gray and
another green/yellow, the solver now treats the green+yellow count as the
exact number of that letter in the target. Previously only the lower bound
was enforced, so words like 'award' (two 'a's) slipped through after
'karma' -> 'xxxxy'.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Oleg Panashchenko 3 nedēļas atpakaļ
vecāks
revīzija
42f7277d14
2 mainītis faili ar 27 papildinājumiem un 0 dzēšanām
  1. 3 0
      raw/15-bug-gray-yellow.md
  2. 24 0
      server/src/routes/solver.ts

+ 3 - 0
raw/15-bug-gray-yellow.md

@@ -0,0 +1,3 @@
+# Bug in server/src/routes/solver.ts
+
+When a guess has one letter twice, and result has one of the letters gray and another non-gray, the solver should exclude words, where that letter occures more than once. For example, if 'karma' guess results in 'xxxxy', the word 'award' should not be suggested, as guess results show only one occurence of the letter 'a',

+ 24 - 0
server/src/routes/solver.ts

@@ -64,6 +64,7 @@ export function createSolverRouter(wordBank: WordBank): Router {
     const greens = new Map<number, string>(); // position → letter
     const yellowForbidden = new Map<string, Set<number>>(); // letter → forbidden positions
     const yellowMinCount = new Map<string, number>(); // letter → min occurrences in target
+    const maxCounts = new Map<string, number>(); // letter → exact occurrences in target (gray + non-gray in same row)
     const grayCandidates = new Set<string>(); // letters seen as gray anywhere
     const allGreenYellow = new Set<string>(); // letters seen as g or y in ANY row
 
@@ -73,6 +74,7 @@ export function createSolverRouter(wordBank: WordBank): Router {
 
       // Per-row counts for green+yellow (min number of each letter in target)
       const rowCounts = new Map<string, number>();
+      const rowGray = new Set<string>();
 
       for (let i = 0; i < 5; i++) {
         const letter = g[i];
@@ -91,6 +93,7 @@ export function createSolverRouter(wordBank: WordBank): Router {
           yellowForbidden.get(letter)!.add(i);
         } else {
           grayCandidates.add(letter);
+          rowGray.add(letter);
         }
       }
 
@@ -98,6 +101,14 @@ export function createSolverRouter(wordBank: WordBank): Router {
       for (const [letter, count] of rowCounts) {
         yellowMinCount.set(letter, Math.max(yellowMinCount.get(letter) ?? 0, count));
       }
+
+      // A letter that is both gray and green/yellow in this row is capped
+      // at its green+yellow count (the gray marks the "extra" occurrence).
+      for (const [letter, count] of rowCounts) {
+        if (rowGray.has(letter)) {
+          maxCounts.set(letter, Math.min(maxCounts.get(letter) ?? count, count));
+        }
+      }
     }
 
     // Remove from gray set: any letter that also appears as green or yellow
@@ -156,6 +167,19 @@ export function createSolverRouter(wordBank: WordBank): Router {
       }
       if (!ok) continue;
 
+      // Exact (upper-bound) count check
+      for (const [letter, maxCount] of maxCounts) {
+        let count = 0;
+        for (let i = 0; i < 5; i++) {
+          if (word[i] === letter) count++;
+        }
+        if (count > maxCount) {
+          ok = false;
+          break;
+        }
+      }
+      if (!ok) continue;
+
       candidates.push(word);
     }