greedy.ts 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * Greedy position-masking Wordle solver.
  3. * Strategy: strictly respect known greens (lock positions).
  4. * For yellows, test words that have the letter in different positions.
  5. * For grays, eliminate words containing that letter.
  6. *
  7. * Simpler than entropy — models how many humans actually play.
  8. */
  9. function getFeedback(target: string, guess: string): string {
  10. const tu = target.toLowerCase();
  11. const gu = guess.toLowerCase();
  12. const counts: Record<string, number> = {};
  13. for (const ch of tu) counts[ch] = (counts[ch] ?? 0) + 1;
  14. const result: string[] = Array(5).fill('x');
  15. for (let i = 0; i < 5; i++) {
  16. if (gu[i] === tu[i]) { result[i] = 'g'; counts[gu[i]]--; }
  17. }
  18. for (let i = 0; i < 5; i++) {
  19. if (result[i] === 'g') continue;
  20. if ((counts[gu[i]] ?? 0) > 0) { result[i] = 'y'; counts[gu[i]]--; }
  21. }
  22. return result.join('');
  23. }
  24. function matchesFeedback(candidate: string, guess: string, feedback: string): boolean {
  25. return getFeedback(candidate, guess) === feedback;
  26. }
  27. /**
  28. * Solve using greedy position-masking.
  29. * Pick any candidate — the filtering does the work.
  30. */
  31. export function solveGreedy(target: string, allWords: string[], maxAttempts = 6): number {
  32. let candidates = [...allWords];
  33. // Good human-style starting word
  34. const starters = ['crane', 'slate', 'arise', 'stare'];
  35. const starter = starters.find((w) => allWords.includes(w)) ?? allWords[0];
  36. for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  37. const guess = attempt === 1 ? starter : candidates[0];
  38. const fb = getFeedback(target, guess);
  39. if (fb === 'ggggg') return attempt;
  40. candidates = candidates.filter((c) => matchesFeedback(c, guess, fb));
  41. if (candidates.length === 0) return maxAttempts + 1;
  42. // Greedy: prefer candidates with letters in positions matching known greens
  43. if (attempt < maxAttempts) {
  44. const greenPositions: number[] = [];
  45. for (let i = 0; i < 5; i++) {
  46. if (fb[i] === 'g') greenPositions.push(i);
  47. }
  48. // Sort candidates by how many greens they match from previous feedback
  49. if (greenPositions.length > 0) {
  50. candidates.sort((a, b) => {
  51. let aMatch = 0, bMatch = 0;
  52. for (const pos of greenPositions) {
  53. if (a[pos] === guess[pos]) aMatch++;
  54. if (b[pos] === guess[pos]) bMatch++;
  55. }
  56. return bMatch - aMatch; // more matches first
  57. });
  58. }
  59. }
  60. }
  61. return maxAttempts + 1;
  62. }