Quellcode durchsuchen

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 vor 3 Wochen
Ursprung
Commit
42f7277d14
2 geänderte Dateien mit 27 neuen und 0 gelöschten Zeilen
  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 greens = new Map<number, string>(); // position → letter
     const yellowForbidden = new Map<string, Set<number>>(); // letter → forbidden positions
     const yellowForbidden = new Map<string, Set<number>>(); // letter → forbidden positions
     const yellowMinCount = new Map<string, number>(); // letter → min occurrences in target
     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 grayCandidates = new Set<string>(); // letters seen as gray anywhere
     const allGreenYellow = new Set<string>(); // letters seen as g or y in ANY row
     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)
       // Per-row counts for green+yellow (min number of each letter in target)
       const rowCounts = new Map<string, number>();
       const rowCounts = new Map<string, number>();
+      const rowGray = new Set<string>();
 
 
       for (let i = 0; i < 5; i++) {
       for (let i = 0; i < 5; i++) {
         const letter = g[i];
         const letter = g[i];
@@ -91,6 +93,7 @@ export function createSolverRouter(wordBank: WordBank): Router {
           yellowForbidden.get(letter)!.add(i);
           yellowForbidden.get(letter)!.add(i);
         } else {
         } else {
           grayCandidates.add(letter);
           grayCandidates.add(letter);
+          rowGray.add(letter);
         }
         }
       }
       }
 
 
@@ -98,6 +101,14 @@ export function createSolverRouter(wordBank: WordBank): Router {
       for (const [letter, count] of rowCounts) {
       for (const [letter, count] of rowCounts) {
         yellowMinCount.set(letter, Math.max(yellowMinCount.get(letter) ?? 0, count));
         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
     // 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;
       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);
       candidates.push(word);
     }
     }