Skip to content

Commit

Permalink
refactor criteria.requiredLetters into criteria.minimumLetterCounts
Browse files Browse the repository at this point in the history
  • Loading branch information
kiprobinson committed Sep 9, 2022
1 parent 96c43c8 commit 5973638
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 59 deletions.
27 changes: 16 additions & 11 deletions app/lib/word-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ export type RateWordCriteria = {
correctLetters?: Array<string|null>;

/**
* Letters which we know have to be in the answer, but we don't know where.
* Defined as an array (rather than set) because e.g. if the word is "tasty"
* and we guessed "stint", we would know that there are *two* Ts that have
* to be in the answer.
* 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.
*/
requiredLetters?: string[];
minimumLetterCounts?: Record<string, number>;

/**
* Stores any letters for which we know exactly how many of that letter is
Expand All @@ -49,7 +47,7 @@ export const getEmptyRateWordCriteria = ():Required<RateWordCriteria> => ({
invalidLetters: new Set(),
invalidLettersByPosition: [new Set(), new Set(), new Set(), new Set(), new Set()],
correctLetters: [null, null, null, null, null],
requiredLetters: [],
minimumLetterCounts: {},
knownLetterCounts: {},
});

Expand Down Expand Up @@ -101,13 +99,17 @@ export const rateWord = (word:string, stats:WordListStats, criteria:RateWordCrit

let score = 0;

const requiredLetters = criteria.requiredLetters ? [...criteria.requiredLetters] : [];
const minimumLetterCounts = criteria.minimumLetterCounts ? { ...criteria.minimumLetterCounts } : {};

const lettersProcessed = new Set<string>();
for(let i = 0; i < 5; i++) {
const letter = word[i];

const wasRequiredLetter = arrayRemoveValue(requiredLetters, letter);
const wasRequiredLetter = !!minimumLetterCounts[letter];

if(wasRequiredLetter) {
minimumLetterCounts[letter]--;
}

// award points based on how likely this letter is to be the right answer at this position.
// But if this wasn't a required letter, multiply the position-specific points by 1/1000,
Expand All @@ -132,7 +134,7 @@ export const rateWord = (word:string, stats:WordListStats, criteria:RateWordCrit
* Returns whether the given word matches the provided criteria.
*/
export const wordMatchesCriteria = (word:string, criteria:RateWordCriteria={}):boolean => {
const requiredLetters = criteria.requiredLetters ? [...criteria.requiredLetters] : [];
const minimumLetterCounts = criteria.minimumLetterCounts ? { ...criteria.minimumLetterCounts } : {};

for(let i = 0; i < 5; i++) {
const letter = word[i];
Expand All @@ -146,11 +148,14 @@ export const wordMatchesCriteria = (word:string, criteria:RateWordCriteria={}):b
if(criteria?.correctLetters?.[i] && criteria.correctLetters[i] !== letter)
return false;

arrayRemoveValue(requiredLetters, letter);
if(minimumLetterCounts[letter] > 1)
minimumLetterCounts[letter]--;
else if (minimumLetterCounts[letter])
delete minimumLetterCounts[letter];
}

// if we didn't have all required letters, this can't be a match.
if(requiredLetters.length)
if(Object.keys(minimumLetterCounts).length)
return false;

// check for character counts if any are known
Expand Down
43 changes: 19 additions & 24 deletions app/lib/wordle-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,9 @@ export const formatGuessPerResult = (guess: string, result: string):string => {
* @param criteria Current criteria. This object will be updated based on the response.
*/
export const updateCriteriaPerResult = (guess: string, result: string, criteria:Required<RateWordCriteria>):void => {
// 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];

const blackLetters:string[] = [];
const nonBlackLetters:string[] = [];
const nonBlackLetterCounts:Record<string, number> = {};

for(let i = 0; i < 5; i++) {
const guessLetter = guess[i];
Expand All @@ -94,31 +89,22 @@ export const updateCriteriaPerResult = (guess: string, result: string, criteria:

if(resultColor === 'g' || resultColor === 'y') {
nonBlackLetters.push(guessLetter);
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);
}

if(nonBlackLetterCounts[guessLetter])
nonBlackLetterCounts[guessLetter]++;
else
nonBlackLetterCounts[guessLetter] = 1;
}
else {
blackLetters.push(guessLetter);
}
}

// in the case where we guessed a word that has two instances of a letter, but only one is correct,
// we will have the letter in invalidLetters but also in correctLetters or requiredLetters, which
// results in an impossible solution. So we have to remove that letter from invalidLetters
for(const letter of criteria.correctLetters) {
if(letter)
criteria.invalidLetters.delete(letter);
}
for(const letter of criteria.requiredLetters) {
if(letter)
criteria.invalidLetters.delete(letter);
// we will have the letter in invalidLetters but also in nonBlackLetters, which results in an
// impossible solution. So we have to remove that letter from invalidLetters
for(const letter of nonBlackLetters) {
criteria.invalidLetters.delete(letter);
}

// If word is "myths" and we guess "truss" (ybbbg) or "tessa" (ybybb),
Expand All @@ -130,5 +116,14 @@ export const updateCriteriaPerResult = (guess: string, result: string, criteria:
const multicolorLetters:string[] = arrayIntersection(blackLetters, nonBlackLetters);
for(const letter of multicolorLetters) {
criteria.knownLetterCounts[letter] = arrayCount(nonBlackLetters, letter);

//if we have known letter counts, there's no reason to have minimum letter counts
delete criteria.minimumLetterCounts[letter];
delete nonBlackLetterCounts[letter];
}

Object.entries(nonBlackLetterCounts).forEach(([letter, count]) => {
if(!criteria.minimumLetterCounts[letter] || criteria.minimumLetterCounts[letter] < count)
criteria.minimumLetterCounts[letter] = count;
});
}
36 changes: 18 additions & 18 deletions test/lib/word-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@ describe('test word-list.ts methods', () => {
expect(rateWord('myths', stats, {correctLetters: [null, null, 'y', null, null]})).to.equal(0);
expect(rateWord('myths', stats, {correctLetters: [null, null, null, null, null]})).to.be.above(0);

expect(rateWord('myths', stats, {requiredLetters: []})).to.be.above(0);
expect(rateWord('myths', stats, {requiredLetters: ['e']})).to.equal(0);
expect(rateWord('myths', stats, {requiredLetters: ['t']})).to.be.above(0);
expect(rateWord('myths', stats, {requiredLetters: ['s']})).to.be.above(0);
expect(rateWord('myths', stats, {requiredLetters: ['s', 't']})).to.be.above(0);
expect(rateWord('myths', stats, {requiredLetters: ['s', 't', 's']})).to.equal(0);
expect(rateWord('truss', stats, {requiredLetters: ['s', 't', 's']})).to.be.above(0);
expect(rateWord('truss', stats, {requiredLetters: ['s', 's', 's']})).to.equal(0);
expect(rateWord('sassy', stats, {requiredLetters: ['s', 's', 's']})).to.be.above(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {}})).to.be.above(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {e:1}})).to.equal(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {t:1}})).to.be.above(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {s:1}})).to.be.above(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {s:1, t:1}})).to.be.above(0);
expect(rateWord('myths', stats, {minimumLetterCounts: {s:2, t:1}})).to.equal(0);
expect(rateWord('truss', stats, {minimumLetterCounts: {s:2, t:1}})).to.be.above(0);
expect(rateWord('truss', stats, {minimumLetterCounts: {s:3}})).to.equal(0);
expect(rateWord('sassy', stats, {minimumLetterCounts: {s:3}})).to.be.above(0);

expect(rateWord('myths', stats, {knownLetterCounts: {}})).to.be.above(0);
expect(rateWord('myths', stats, {knownLetterCounts: {s:1}})).to.be.above(0);
Expand Down Expand Up @@ -149,15 +149,15 @@ describe('test word-list.ts methods', () => {
expect(wordMatchesCriteria('myths', {correctLetters: [null, null, 'y', null, null]})).to.be.false;
expect(wordMatchesCriteria('myths', {correctLetters: [null, null, null, null, null]})).to.be.true;

expect(wordMatchesCriteria('myths', {requiredLetters: []})).to.be.true;
expect(wordMatchesCriteria('myths', {requiredLetters: ['e']})).to.be.false;
expect(wordMatchesCriteria('myths', {requiredLetters: ['t']})).to.be.true;
expect(wordMatchesCriteria('myths', {requiredLetters: ['s']})).to.be.true;
expect(wordMatchesCriteria('myths', {requiredLetters: ['s', 't']})).to.be.true;
expect(wordMatchesCriteria('myths', {requiredLetters: ['s', 't', 's']})).to.be.false;
expect(wordMatchesCriteria('truss', {requiredLetters: ['s', 't', 's']})).to.be.true;
expect(wordMatchesCriteria('truss', {requiredLetters: ['s', 's', 's']})).to.be.false;
expect(wordMatchesCriteria('sassy', {requiredLetters: ['s', 's', 's']})).to.be.true;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {}})).to.be.true;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {e:1}})).to.be.false;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {t:1}})).to.be.true;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {s:1}})).to.be.true;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {s:1, t:1}})).to.be.true;
expect(wordMatchesCriteria('myths', {minimumLetterCounts: {s:2, t:1}})).to.be.false;
expect(wordMatchesCriteria('truss', {minimumLetterCounts: {s:2, t:1}})).to.be.true;
expect(wordMatchesCriteria('truss', {minimumLetterCounts: {s:3}})).to.be.false;
expect(wordMatchesCriteria('sassy', {minimumLetterCounts: {s:3}})).to.be.true;

expect(wordMatchesCriteria('myths', {knownLetterCounts: {}})).to.be.true;
expect(wordMatchesCriteria('myths', {knownLetterCounts: {s:1}})).to.be.true;
Expand Down
12 changes: 6 additions & 6 deletions test/lib/wordle-engine.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal(['e']);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal([]);
expect(criteria.knownLetterCounts).to.deep.equal({});
expect(criteria.requiredLetters).to.deep.equal(['t', 'r', 's']);
expect(criteria.minimumLetterCounts).to.deep.equal({t:1, r:1, s:1});

updateCriteriaPerResult('tours', 'gbgyg', criteria);
expect(criteria.correctLetters).to.deep.equal(['t', null, 'u', null, 's']);
Expand All @@ -66,7 +66,7 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal(['e', 'r']);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal([]);
expect(criteria.knownLetterCounts).to.deep.equal({});
expect(criteria.requiredLetters).to.deep.equal(['t', 'r', 's', 'u']);
expect(criteria.minimumLetterCounts).to.deep.equal({t:1, r:1, s:1, u:1});
});

//If word is "myths" and we guess "truss" (ybbbg) or "tessa" (ybybb),
Expand All @@ -81,7 +81,7 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal(['s']);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal([]);
expect(criteria.knownLetterCounts).to.deep.equal({s:1});
expect(criteria.requiredLetters).to.deep.equal(['t', 's']);
expect(criteria.minimumLetterCounts).to.deep.equal({t:1});
});

it('updateCriteriaPerResult - word is "myths" and we guess "tessa"', () => {
Expand All @@ -95,7 +95,7 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal(['s']);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal(['a']);
expect(criteria.knownLetterCounts).to.deep.equal({s:1});
expect(criteria.requiredLetters).to.deep.equal(['t', 's']);
expect(criteria.minimumLetterCounts).to.deep.equal({t:1});
});

it('updateCriteriaPerResult - word is "truss" and we guess "tessa"', () => {
Expand All @@ -109,7 +109,7 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal([]);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal(['a']);
expect(criteria.knownLetterCounts).to.deep.equal({});
expect(criteria.requiredLetters).to.deep.equal(['t', 's', 's']);
expect(criteria.minimumLetterCounts).to.deep.equal({t:1, s:2});
});

it('updateCriteriaPerResult - word is "truss" and we guess "sassy"', () => {
Expand All @@ -123,6 +123,6 @@ describe('test wordle-engine.ts methods', () => {
expect([...criteria.invalidLettersByPosition[3]]).to.deep.equal([]);
expect([...criteria.invalidLettersByPosition[4]]).to.deep.equal(['y']);
expect(criteria.knownLetterCounts).to.deep.equal({s: 2});
expect(criteria.requiredLetters).to.deep.equal(['s', 's']);
expect(criteria.minimumLetterCounts).to.deep.equal({});
});
});

0 comments on commit 5973638

Please sign in to comment.