|
@@ -1,89 +1,49 @@
|
|
|
-/**
|
|
|
|
|
- * Vowel-first Wordle solver.
|
|
|
|
|
- * Strategy: maximize vowel coverage in early guesses,
|
|
|
|
|
- * then switch to consonant hunting with positional constraints.
|
|
|
|
|
- */
|
|
|
|
|
|
|
+import type { SolverStep } from '@wordle/shared';
|
|
|
|
|
|
|
|
-const VOWELS = new Set(['a', 'e', 'i', 'o', 'u']);
|
|
|
|
|
-const VOWEL_HEAVY_STARTS = ['adieu', 'audio', 'raise', 'arose', 'irate', 'alone', 'ouija'];
|
|
|
|
|
|
|
+const VOWELS = new Set(['a','e','i','o','u']);
|
|
|
|
|
+const VOWEL_STARTS = ['adieu','audio','raise','arose','irate'];
|
|
|
|
|
|
|
|
function getFeedback(target: string, guess: string): string {
|
|
function getFeedback(target: string, guess: string): string {
|
|
|
- const tu = target.toLowerCase();
|
|
|
|
|
- const gu = guess.toLowerCase();
|
|
|
|
|
|
|
+ const tu = target.toLowerCase(), gu = guess.toLowerCase();
|
|
|
const counts: Record<string, number> = {};
|
|
const counts: Record<string, number> = {};
|
|
|
for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
|
|
for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
|
|
|
- const result: string[] = Array(5).fill('x');
|
|
|
|
|
- for (let i = 0; i < 5; i++) {
|
|
|
|
|
- if (gu[i] === tu[i]) { result[i] = 'g'; counts[gu[i]]--; }
|
|
|
|
|
- }
|
|
|
|
|
- for (let i = 0; i < 5; i++) {
|
|
|
|
|
- if (result[i] === 'g') continue;
|
|
|
|
|
- if ((counts[gu[i]] ?? 0) > 0) { result[i] = 'y'; counts[gu[i]]--; }
|
|
|
|
|
- }
|
|
|
|
|
- return result.join('');
|
|
|
|
|
|
|
+ const r: string[] = Array(5).fill('x');
|
|
|
|
|
+ for (let i = 0; i < 5; i++) { if (gu[i] === tu[i]) { r[i] = 'g'; counts[gu[i]]--; } }
|
|
|
|
|
+ for (let i = 0; i < 5; i++) { if (r[i] === 'g') continue; if ((counts[gu[i]] ?? 0) > 0) { r[i] = 'y'; counts[gu[i]]--; } }
|
|
|
|
|
+ return r.join('');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function matchesFeedback(candidate: string, guess: string, feedback: string): boolean {
|
|
|
|
|
- return getFeedback(candidate, guess) === feedback;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+function matchesFeedback(c: string, g: string, fb: string): boolean { return getFeedback(c, g) === fb; }
|
|
|
|
|
|
|
|
-function countNewVowels(word: string, knownVowels: Set<string>): number {
|
|
|
|
|
- let count = 0;
|
|
|
|
|
- for (const ch of word) {
|
|
|
|
|
- if (VOWELS.has(ch) && !knownVowels.has(ch)) count++;
|
|
|
|
|
- }
|
|
|
|
|
- return count;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-function scoreVowel(word: string, knownVowels: Set<string>): number {
|
|
|
|
|
- const newVowels = countNewVowels(word, knownVowels);
|
|
|
|
|
- const uniqueLetters = new Set(word.split('')).size;
|
|
|
|
|
- return newVowels * 3 + uniqueLetters;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/**
|
|
|
|
|
- * Solve using vowel-first strategy.
|
|
|
|
|
- */
|
|
|
|
|
-export function solveVowelFirst(target: string, allWords: string[], maxAttempts = 6): number {
|
|
|
|
|
|
|
+export function solveVowelFirst(target: string, allWords: string[], maxAttempts = 6): { attempts: number; steps: SolverStep[] } {
|
|
|
let candidates = [...allWords];
|
|
let candidates = [...allWords];
|
|
|
const knownVowels = new Set<string>();
|
|
const knownVowels = new Set<string>();
|
|
|
- let guesses: { word: string; fb: string }[] = [];
|
|
|
|
|
|
|
+ const steps: SolverStep[] = [];
|
|
|
|
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
let bestGuess: string;
|
|
let bestGuess: string;
|
|
|
-
|
|
|
|
|
if (attempt === 1) {
|
|
if (attempt === 1) {
|
|
|
- // Pick best vowel-heavy start word from shortlist
|
|
|
|
|
- bestGuess = VOWEL_HEAVY_STARTS.find((w) => allWords.includes(w)) ?? 'arise';
|
|
|
|
|
|
|
+ bestGuess = VOWEL_STARTS.find((w) => allWords.includes(w)) ?? 'arise';
|
|
|
} else if (attempt <= 3 && knownVowels.size < 5) {
|
|
} else if (attempt <= 3 && knownVowels.size < 5) {
|
|
|
- // Still hunting vowels — pick word with most new vowels
|
|
|
|
|
const pool = candidates.length > 0 ? candidates : allWords;
|
|
const pool = candidates.length > 0 ? candidates : allWords;
|
|
|
- bestGuess = pool[0];
|
|
|
|
|
- let bestScore = -1;
|
|
|
|
|
|
|
+ bestGuess = pool[0]; let best = -1;
|
|
|
for (const w of pool) {
|
|
for (const w of pool) {
|
|
|
- const s = scoreVowel(w, knownVowels);
|
|
|
|
|
- if (s > bestScore) { bestScore = s; bestGuess = w; }
|
|
|
|
|
|
|
+ const newV = [...w].filter((c) => VOWELS.has(c) && !knownVowels.has(c)).length;
|
|
|
|
|
+ const s = newV * 3 + new Set(w.split('')).size;
|
|
|
|
|
+ if (s > best) { best = s; bestGuess = w; }
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // Consonant phase — pick from remaining candidates
|
|
|
|
|
bestGuess = candidates[0];
|
|
bestGuess = candidates[0];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const fb = getFeedback(target, bestGuess);
|
|
const fb = getFeedback(target, bestGuess);
|
|
|
- guesses.push({ word: bestGuess, fb });
|
|
|
|
|
-
|
|
|
|
|
- if (fb === 'ggggg') return attempt;
|
|
|
|
|
-
|
|
|
|
|
- // Track known vowels from greens and yellows
|
|
|
|
|
- for (let i = 0; i < 5; i++) {
|
|
|
|
|
- if ((fb[i] === 'g' || fb[i] === 'y') && VOWELS.has(bestGuess[i])) {
|
|
|
|
|
- knownVowels.add(bestGuess[i]);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ steps.push({ guess: bestGuess, colors: fb.split('') });
|
|
|
|
|
+ if (fb === 'ggggg') return { attempts: attempt, steps };
|
|
|
|
|
|
|
|
|
|
+ for (let i = 0; i < 5; i++) { if ((fb[i] === 'g' || fb[i] === 'y') && VOWELS.has(bestGuess[i])) knownVowels.add(bestGuess[i]); }
|
|
|
candidates = candidates.filter((c) => matchesFeedback(c, bestGuess, fb));
|
|
candidates = candidates.filter((c) => matchesFeedback(c, bestGuess, fb));
|
|
|
- if (candidates.length === 0) return maxAttempts + 1;
|
|
|
|
|
|
|
+ if (candidates.length === 0) return { attempts: maxAttempts + 1, steps };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return maxAttempts + 1;
|
|
|
|
|
|
|
+ return { attempts: maxAttempts + 1, steps };
|
|
|
}
|
|
}
|