Skip to content

Commit

Permalink
refactor app to use word list masks to filter the list much more quickly
Browse files Browse the repository at this point in the history
  • Loading branch information
kiprobinson committed Sep 9, 2022
1 parent 5973638 commit 8b374f3
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 18 deletions.
16 changes: 9 additions & 7 deletions app/cli/wordle-cheater.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CliUtils from "../lib/cli-utils";
import { getWordListStats, getSortedWordList, RateWordCriteria, WordListStats, getEmptyRateWordCriteria, DEFAULT_WORD_LIST } from "../lib/word-list";
import { getWordListStats, getSortedWordList, RateWordCriteria, getEmptyRateWordCriteria, DEFAULT_WORD_LIST } from "../lib/word-list";
import WordListIndex from "../lib/word-list-index";
import { formatGuessPerResult, getResultForGuess, updateCriteriaPerResult } from "../lib/wordle-engine";

const NUM_WORDS_TO_SHOW = 40;
Expand All @@ -11,13 +12,14 @@ type GuessHistory = Array<{guess:string, result:string}>;
*/
const cheatAtWordle = async ():Promise<void> => {
let wordList = DEFAULT_WORD_LIST;
const wordListIndex = new WordListIndex(wordList);
const criteria: Required<RateWordCriteria> = getEmptyRateWordCriteria();

const correctAnswer = await askForAnswer();

let guesses:GuessHistory = [];
while(true) {
const {abort, newWordList} = showBestGuesses(wordList, criteria);
const {abort, newWordList} = showBestGuesses(wordList, wordListIndex, criteria);
if(abort)
break;

Expand Down Expand Up @@ -54,18 +56,18 @@ const askForAnswer = async ():Promise<string|null> => {
/**
* Show the best guesses at this point in the game.
*/
const showBestGuesses = (wordList:string[], criteria:RateWordCriteria): {abort:boolean, newWordList: string[]} => {
const showBestGuesses = (wordList:string[], wordListIndex:WordListIndex, criteria:RateWordCriteria): {abort:boolean, newWordList: string[]} => {
const stats = getWordListStats(wordList);
const sortedWordList = getSortedWordList(stats, criteria);
const newWordList = sortedWordList.filter(e => e.score > 0).map(e => e.word);
const sortedWordList = getSortedWordList(stats, criteria, undefined, wordListIndex);
const newWordList = sortedWordList.map(e => e.word);

if(sortedWordList[0].score <= 0) {
if(newWordList.length <= 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, newWordList};
}

const numPossibleWords = sortedWordList.reduce((count, entry) => count + (entry.score > 0 ? 1 : 0), 0);
const numPossibleWords = sortedWordList.length;

console.log();
console.log(`I found ${numPossibleWords} possible words. Here are my top ${Math.min(numPossibleWords, NUM_WORDS_TO_SHOW)} suggestions for you to try:`);
Expand Down
30 changes: 22 additions & 8 deletions app/lib/big-bit-mask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,32 +183,46 @@ class BigBitMask {
* Performs union (bitwise or) of this mask and one or more other masks.
*/
union(...masks:BigBitMask[]):BigBitMask {
return this.bitwiseOpImplementation(masks, (a, b) => a | b);
return BigBitMask.bitwiseOpImplementation([this, ...masks], (a, b) => a | b);
}

/**
* Performs union (bitwise or) of two or more masks.
*/
static union(...masks:BigBitMask[]):BigBitMask {
return BigBitMask.bitwiseOpImplementation(masks, (a, b) => a | b);
}

/**
* Performs intersect (bitwise and) of this mask and one or more other masks.
*/
intersect(...masks:BigBitMask[]):BigBitMask {
return this.bitwiseOpImplementation(masks, (a, b) => a & b);
return BigBitMask.bitwiseOpImplementation([this, ...masks], (a, b) => a & b);
}

/**
* Performs intersect (bitwise and) of this mask and one or more other masks.
*/
static intersect(...masks:BigBitMask[]):BigBitMask {
return BigBitMask.bitwiseOpImplementation(masks, (a, b) => a & b);
}

/**
* Subtracts the other mask from this mask (bitwise `this & ~other`).
*/
subtract(other:BigBitMask):BigBitMask {
return this.bitwiseOpImplementation([other], (a, b) => a & ~b);
return BigBitMask.bitwiseOpImplementation([this, other], (a, b) => a & ~b);
}

private bitwiseOpImplementation(masks:BigBitMask[], op:{(a:number, b:number):number}):BigBitMask {
if (masks.length === 0)
private static bitwiseOpImplementation(masks:BigBitMask[], op:{(a:number, b:number):number}):BigBitMask {
if (masks.length < 2)
throw new Error('Must provide at least one other mask');

if (masks.some(mask => mask.length !== this.length))
if (masks.some(mask => mask.length !== masks[0].length))
throw new Error('Masks must all be the same length');

const result = new BigBitMask(this);
for(let i = 0; i < masks.length; i++) {
const result = new BigBitMask(masks[0]);
for(let i = 1; i < masks.length; i++) {
for(let j = 0; j < result.data.length; j++) {
result.data[j] = op(result.data[j], masks[i].data[j]);
}
Expand Down
58 changes: 56 additions & 2 deletions app/lib/word-list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as fs from 'fs';
import BigBitMask from './big-bit-mask';
import { FrequencyTable } from './frequency-table';
import { arrayCount, arrayRemoveValue } from './util';
import WordListIndex from './word-list-index';

export type WordListStats = {
overallFrequencies: FrequencyTable;
Expand All @@ -24,7 +26,7 @@ export type RateWordCriteria = {
/** The letters which we know are correct. */
correctLetters?: Array<string|null>;

/**
/**
* Letters which we know have to be in the answer at least a certain number
* of times, but we don't know exactly how many or in which positions.
*/
Expand Down Expand Up @@ -170,10 +172,62 @@ export const wordMatchesCriteria = (word:string, criteria:RateWordCriteria={}):b
return true;
}

export const applyCriteriaToWordList = (criteria:RateWordCriteria, wordList:string[], wordListIndex:WordListIndex):string[] => {
// masks where a `1` means to include the word
const includeMasks:BigBitMask[] = [];

// masks where a `1` means to exclude the word
const excludeMasks:BigBitMask[] = [];

criteria.invalidLetters?.forEach(letter => includeMasks.push(wordListIndex.getMaskForWordsWithExactLetterCount(0, letter)));

criteria.invalidLettersByPosition?.forEach((letters, position) => {
letters?.forEach(letter => excludeMasks.push(wordListIndex.getMaskForWordsWithLetterInPosition(position, letter)));
});

criteria.correctLetters?.forEach((letter, position) => {
if (letter)
includeMasks.push(wordListIndex.getMaskForWordsWithLetterInPosition(position, letter));
})

if(criteria.minimumLetterCounts) {
Object.entries(criteria.minimumLetterCounts).forEach(([letter, count]) => {
includeMasks.push(wordListIndex.getMaskForWordsWithMinimumLetterCount(count, letter));
})
}

if (criteria.knownLetterCounts) {
Object.entries(criteria.knownLetterCounts).forEach(([letter, count]) => {
includeMasks.push(wordListIndex.getMaskForWordsWithExactLetterCount(count, letter));
});
}

if(excludeMasks.length && !includeMasks.length)
includeMasks.push(new BigBitMask(wordList.length, true));

if(includeMasks.length) {
const includeMask = includeMasks.length > 1 ? BigBitMask.intersect(...includeMasks) : includeMasks[0];

let finalMask = includeMask;
if(excludeMasks.length) {
const excludeMask = excludeMasks.length > 1 ? BigBitMask.union(...excludeMasks) : excludeMasks[0];
finalMask = includeMask.subtract(excludeMask);
}

wordList = finalMask.apply(wordList);
}

return wordList;
}

/**
* Gets a sorted word list, evaluating the score for each word.
*/
export const getSortedWordList = (stats:WordListStats, criteria:RateWordCriteria={}, wordList:string[]=DEFAULT_WORD_LIST):SortedWordList => {
export const getSortedWordList = (stats:WordListStats, criteria:RateWordCriteria={}, wordList:string[]=DEFAULT_WORD_LIST, wordListIndex?:WordListIndex):SortedWordList => {
if (wordListIndex) {
wordList = applyCriteriaToWordList(criteria, wordList, wordListIndex);
}

const list:SortedWordList = wordList.map(word => ({
word,
score: rateWord(word, stats, criteria),
Expand Down
82 changes: 81 additions & 1 deletion test/lib/word-list.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "chai";
import { getWordListStats, wordMatchesCriteria, rateWord } from "../../app/lib/word-list";
import { getWordListStats, wordMatchesCriteria, rateWord, applyCriteriaToWordList } from "../../app/lib/word-list";
import WordListIndex from "../../app/lib/word-list-index";


describe('test word-list.ts methods', () => {
Expand Down Expand Up @@ -169,4 +170,83 @@ describe('test word-list.ts methods', () => {
expect(wordMatchesCriteria('sassy', {knownLetterCounts: {s:2}})).to.be.false;
expect(wordMatchesCriteria('sassy', {knownLetterCounts: {s:3}})).to.be.true;
});

it('applyCriteriaToWordList', () => {
const wordList = ['myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog'];
const wordListIndex = new WordListIndex(wordList);
expect(applyCriteriaToWordList({}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);

expect(applyCriteriaToWordList({invalidLetters: new Set([])}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({invalidLetters: new Set(['m'])}, wordList, wordListIndex)).to.deep.equal([
'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({invalidLetters: new Set(['x'])}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({invalidLetters: new Set(['x', 'h', 'p'])}, wordList, wordListIndex)).to.deep.equal([
'truss', 'tessa', 'sassy', 'ickbr', 'ownfo', 'sover', 'zydog',
]);

expect(applyCriteriaToWordList({invalidLettersByPosition: [ new Set([]), new Set([]), new Set([]), new Set([]), new Set([]) ]}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({invalidLettersByPosition: [ new Set(['m']), new Set(['a']), new Set(['b']), new Set(['c']), new Set(['d']) ]}, wordList, wordListIndex)).to.deep.equal([
'truss', 'tessa', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({invalidLettersByPosition: [ new Set(['s']), new Set(['m']), new Set(['y']), new Set(['t']), new Set(['h']) ]}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'thequ', 'ickbr', 'ownfo', 'xjump', 'thela', 'zydog',
]);

expect(applyCriteriaToWordList({correctLetters: ['m', null, null, null, null]}, wordList, wordListIndex)).to.deep.equal([
'myths',
]);
expect(applyCriteriaToWordList({correctLetters: [null, null, null, null, 's']}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss',
]);
expect(applyCriteriaToWordList({correctLetters: [null, null, 'y', null, null]}, wordList, wordListIndex)).to.deep.equal([

]);
expect(applyCriteriaToWordList({correctLetters: [null, null, null, null, null]}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);

expect(applyCriteriaToWordList({minimumLetterCounts: {}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {e:1}}, wordList, wordListIndex)).to.deep.equal([
'tessa', 'thequ', 'sover', 'thela',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {t:1}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'thequ', 'thela',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {s:1}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'sover',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {s:1, t:1}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {s:2, t:1}}, wordList, wordListIndex)).to.deep.equal([
'truss', 'tessa',
]);
expect(applyCriteriaToWordList({minimumLetterCounts: {s:3}}, wordList, wordListIndex)).to.deep.equal([
'sassy',
]);

expect(applyCriteriaToWordList({knownLetterCounts: {}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'truss', 'tessa', 'sassy', 'thequ', 'ickbr', 'ownfo', 'xjump', 'sover', 'thela', 'zydog',
]);
expect(applyCriteriaToWordList({knownLetterCounts: {s:1}}, wordList, wordListIndex)).to.deep.equal([
'myths', 'sover',
]);
expect(applyCriteriaToWordList({knownLetterCounts: {s:2}}, wordList, wordListIndex)).to.deep.equal([
'truss', 'tessa',
]);
expect(applyCriteriaToWordList({knownLetterCounts: {s:3}}, wordList, wordListIndex)).to.deep.equal([
'sassy',
]);
});
});

0 comments on commit 8b374f3

Please sign in to comment.