Skip to content

Commit

Permalink
implement wordle solver
Browse files Browse the repository at this point in the history
  • Loading branch information
kiprobinson committed Jan 9, 2022
1 parent eac3121 commit 2600e6f
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 6 deletions.
7 changes: 6 additions & 1 deletion app/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -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,
},
{
Expand Down
131 changes: 131 additions & 0 deletions app/cli/wordle-cheater.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
const stats = getWordListStats();
const criteria: Required<RateWordCriteria> = {
correctLetters: [null, null, null, null, null],
requiredLetters: [],
invalidLetters: new Set<string>(),
invalidLettersByPosition: [
new Set<string>(),
new Set<string>(),
new Set<string>(),
new Set<string>(),
new Set<string>(),
],
}

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<RateWordCriteria>): {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};
}
8 changes: 4 additions & 4 deletions app/lib/cli-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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);
Expand Down
2 changes: 1 addition & 1 deletion app/lib/word-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type RateWordCriteria = {
invalidLettersByPosition?: Array<Set<string>>;

/** The letters which we know are correct. */
correctLetters?: string[];
correctLetters?: Array<string|null>;

/**
* Letters which we know have to be in the answer, but we don't know where.
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"noFallthroughCasesInSwitch": true,
"outDir": "dist"
},
"lib": ["es2015"]
Expand Down

0 comments on commit 2600e6f

Please sign in to comment.