|
@@ -0,0 +1,89 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Vowel-first Wordle solver.
|
|
|
|
|
+ * Strategy: maximize vowel coverage in early guesses,
|
|
|
|
|
+ * then switch to consonant hunting with positional constraints.
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+const VOWELS = new Set(['a', 'e', 'i', 'o', 'u']);
|
|
|
|
|
+const VOWEL_HEAVY_STARTS = ['adieu', 'audio', 'raise', 'arose', 'irate', 'alone', 'ouija'];
|
|
|
|
|
+
|
|
|
|
|
+function getFeedback(target: string, guess: string): string {
|
|
|
|
|
+ const tu = target.toLowerCase();
|
|
|
|
|
+ const gu = guess.toLowerCase();
|
|
|
|
|
+ const counts: Record<string, number> = {};
|
|
|
|
|
+ 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('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function matchesFeedback(candidate: string, guess: string, feedback: string): boolean {
|
|
|
|
|
+ return getFeedback(candidate, guess) === feedback;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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 {
|
|
|
|
|
+ let candidates = [...allWords];
|
|
|
|
|
+ const knownVowels = new Set<string>();
|
|
|
|
|
+ let guesses: { word: string; fb: string }[] = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
|
|
|
+ let bestGuess: string;
|
|
|
|
|
+
|
|
|
|
|
+ if (attempt === 1) {
|
|
|
|
|
+ // Pick best vowel-heavy start word from shortlist
|
|
|
|
|
+ bestGuess = VOWEL_HEAVY_STARTS.find((w) => allWords.includes(w)) ?? 'arise';
|
|
|
|
|
+ } else if (attempt <= 3 && knownVowels.size < 5) {
|
|
|
|
|
+ // Still hunting vowels — pick word with most new vowels
|
|
|
|
|
+ const pool = candidates.length > 0 ? candidates : allWords;
|
|
|
|
|
+ bestGuess = pool[0];
|
|
|
|
|
+ let bestScore = -1;
|
|
|
|
|
+ for (const w of pool) {
|
|
|
|
|
+ const s = scoreVowel(w, knownVowels);
|
|
|
|
|
+ if (s > bestScore) { bestScore = s; bestGuess = w; }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Consonant phase — pick from remaining candidates
|
|
|
|
|
+ bestGuess = candidates[0];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ candidates = candidates.filter((c) => matchesFeedback(c, bestGuess, fb));
|
|
|
|
|
+ if (candidates.length === 0) return maxAttempts + 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return maxAttempts + 1;
|
|
|
|
|
+}
|