From 2600e6fd1470469c2d7962ca078626301dffacd2 Mon Sep 17 00:00:00 2001 From: Kip Robinson Date: Sat, 8 Jan 2022 22:26:12 -0500 Subject: [PATCH] implement wordle solver --- app/cli/cli.ts | 7 +- app/cli/wordle-cheater.ts | 131 ++++++++++++++++++++++++++++++++++++++ app/lib/cli-utils.ts | 8 +-- app/lib/word-list.ts | 2 +- tsconfig.json | 2 + 5 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 app/cli/wordle-cheater.ts diff --git a/app/cli/cli.ts b/app/cli/cli.ts index 495918d..e52f61c 100644 --- a/app/cli/cli.ts +++ b/app/cli/cli.ts @@ -1,10 +1,15 @@ import CliUtils from '../lib/cli-utils'; import dumpStats from "./dump-stats"; import dumpWordRatings from './dump-word-ratings'; +import { cheatAtWordle } from './wordle-cheater'; const menuOptions:Array<{name:string, handler:Function}> = [ { - name: 'Show the best and worst words to start with.', + name: 'Cheat at Wordle ;)', + handler: cheatAtWordle, + }, + { + name: 'Show the best words to start with.', handler: dumpWordRatings, }, { diff --git a/app/cli/wordle-cheater.ts b/app/cli/wordle-cheater.ts new file mode 100644 index 0000000..544bcbc --- /dev/null +++ b/app/cli/wordle-cheater.ts @@ -0,0 +1,131 @@ +import CliUtils from "../lib/cli-utils"; +import { arrayRemoveValue } from "../lib/util"; +import { getWordListStats, getSortedWordList, RateWordCriteria, WordListStats } from "../lib/word-list"; + + + +export const cheatAtWordle = async ():Promise => { + const stats = getWordListStats(); + const criteria: Required = { + correctLetters: [null, null, null, null, null], + requiredLetters: [], + invalidLetters: new Set(), + invalidLettersByPosition: [ + new Set(), + new Set(), + new Set(), + new Set(), + new Set(), + ], + } + + let guessCount = 0; + while(true) { + const {abort} = await showBestGuesses(stats, criteria); + if(abort) + break; + + const {guess, result} = await playTurn(); + guessCount++; + + const {success} = processResponse(guess, result, guessCount, criteria); + if(success) + break; + } +} + +/** + * Show the best guesses at this point in the game. + */ +const showBestGuesses = (stats:WordListStats, criteria:RateWordCriteria): {abort:boolean} => { + const sortedWordList = getSortedWordList(stats, criteria); + if(sortedWordList[0].score <= 0) { + console.log(); + console.log("I'm sorry, but I couldn't find any words matching this criteria. Wordle must have a larger vocabulary than me!"); + return {abort: true}; + } + + console.log(); + console.log('Here are my top ten suggestions for you to try:'); + for(let i = 0; i < 10 && sortedWordList[i].score > 0; i++) + console.log(`${i+1}: ${sortedWordList[i].word}`); + + return {abort: false}; +} + +/** + * Let's the user play the current turn - they tell us what they guessed, and what Wordle told them. + */ +const playTurn = async ():Promise<{guess:string, result:string}> => { + console.log(); + const guess = await CliUtils.ask({ + question: 'What word did you try?', + formatter: (s:string):string => s.toLowerCase(), + regex: /^[a-z]{5}$/i, + retryMessage: "Sorry that doesn't look like a five letter word. Try again!", + }); + + console.log(); + console.log('Great, now what color did Wordle give each letter in that guess?'); + console.log('Enter the response as five letters, using G=green, Y=yellow, B=black/gray.'); + const result = await CliUtils.ask({ + question: 'What was the result? ', + formatter: (s:string):string => s.toLowerCase(), + regex: /^[gyb]{5}$/i, + retryMessage: "Sorry, I didn't understand that. Please enter the response as five letters, using G=green, Y=yellow, B=black/gray.", + }); + + return {guess, result}; +} + + +/** + * Process the response that Wordle gave us, and update our criteria for use in the next round. + */ +const processResponse = (guess: string, result: string, guessCount: number, criteria: Required): {success: boolean} => { + if(result === 'ggggg') { + console.log(); + console.log(`Congratulations! We did it in ${guessCount} guess${guessCount === 1 ? '' : 'es'}! That's some nice teamwork!`); + return {success: true}; + } + + // required letters are kind of tricky. say the correct answer is "truss", and we guessed "shore". + // => requiredLetters has an S. + // Now, say we guess "shops". We need to know now that required letters has two Ss. + // Now, say we guess "sassy". We need to make sure that we don't end up with three Ss in required letters + const requiredLettersCopy = [...criteria.requiredLetters]; + + for(let i = 0; i < 5; i++) { + const guessLetter = guess[i]; + const resultColor = result[i]; + if(resultColor === 'g') { + // green - this is a correct letter! + criteria.correctLetters[i] = guessLetter; + } + else if(resultColor === 'y') { + // yellow - this letter is in the word, but not at this position + criteria.invalidLettersByPosition[i].add(guessLetter); + } + else if(resultColor === 'b') { + // black/gray - this letter is not in the word at all + criteria.invalidLetters.add(guessLetter); + } + else { + throw new Error(`Invalid color: ${resultColor}`) + } + + if(resultColor === 'g' || resultColor === 'y') { + if(requiredLettersCopy.includes(guessLetter)) { + // this letter was already in criteria.required letters. Remove it from the copy, so that if + // there is another yellow/green of this letter, it doesn't get skipped. + arrayRemoveValue(requiredLettersCopy, guessLetter); + } + else { + // this yellow/green letter wasn't already in required letters, so add it. + criteria.requiredLetters.push(guessLetter); + } + } + } + + return {success: false}; +} \ No newline at end of file diff --git a/app/lib/cli-utils.ts b/app/lib/cli-utils.ts index 06c9e02..87d408d 100644 --- a/app/lib/cli-utils.ts +++ b/app/lib/cli-utils.ts @@ -22,21 +22,21 @@ type AskOptions = { * A few utilities for command line interfaces. */ class CliUtils { - private static rl:readline.Interface = null; + private static rl:readline.Interface|null = null; - private static lazyInit() { + private static getRl():readline.Interface { if(!this.rl) this.rl = readline.createInterface(process.stdin, process.stdout); + return this.rl; } /** * Async version of `readline.question()`. */ private static askImpl(question: string):Promise { - this.lazyInit(); return new Promise((resolve, reject) => { try { - this.rl.question(question.trimEnd() + ' ', answer => resolve(answer.trim())); + this.getRl().question(question.trimEnd() + ' ', answer => resolve(answer.trim())); } catch (err) { reject(err); diff --git a/app/lib/word-list.ts b/app/lib/word-list.ts index 4387908..fb1ac54 100644 --- a/app/lib/word-list.ts +++ b/app/lib/word-list.ts @@ -22,7 +22,7 @@ export type RateWordCriteria = { invalidLettersByPosition?: Array>; /** The letters which we know are correct. */ - correctLetters?: string[]; + correctLetters?: Array; /** * Letters which we know have to be in the answer, but we don't know where. diff --git a/tsconfig.json b/tsconfig.json index 4033670..de39de7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,12 @@ { "compilerOptions": { + "strict": true, "module": "commonjs", "esModuleInterop": true, "target": "es6", "moduleResolution": "node", "sourceMap": true, + "noFallthroughCasesInSwitch": true, "outDir": "dist" }, "lib": ["es2015"]