Solve NYT Letter Boxed with this simple API. Scrabble dictionary is recommended.
const fs = require('fs');
/**
* Find all valid one and two word NYT Letter Boxed solutions with
* words selected from the Scrabble dictionary
* @param {string} left
* @param {string} top
* @param {string} right
* @param {string} bottom
* @returns {any} result
*/
module.exports = async (left, top, right, bottom, context) => {
// First, we add error checking for inputs
let sides = [left, top, right, bottom];
let duplicateLetterCheck = {};
sides = sides.map((side) => {
side = side.toLowerCase().trim();
if (side.length !== 3) {
throw new Error(`Sides must be 3 characters each`);
} else if (!side.match(/^[a-z]+$/i)) {
throw new Error(`Sides must be alphanumeric`);
}
side.split('').forEach((letter) => {
if (duplicateLetterCheck[letter]) {
throw new Error(`Sides must not have duplicate letters: "${letter}"`);
} else {
duplicateLetterCheck[letter] = true;
}
});
return side.toLowerCase();
});
// Next, get a list of all scrabble words (lowercase)
// You will need to import into your prject root yourself:
// https://github.com/raun/Scrabble/blob/master/words.txt
let words = fs.readFileSync('./words_alpha.txt').toString();
let wordsArray = words.split('\n').map((word) => word.toLowerCase());
// keep track of execution time
let t0 = new Date().valueOf();
console.log(`Total wordcount: ${wordsArray.length}`);
// First pass, filter word list by words containing the 12 letters
let sidesMatchString = `^[${sides.join('')}]+$`;
let acceptableRE = new RegExp(sidesMatchString, 'i');
let filteredWords = wordsArray.filter(
(word) => word.match(acceptableRE) && word.length >= 3
);
console.log(`Filtered words: ${filteredWords.length}`);
// Next, get rid of all words with disallowed letter combinations
// e.g. doubles, letters on same side
// This will generate a RegExp that looks like /aa|ab|ac|ba|ca ... /i
let disallowedLetterCombos = {};
sides.forEach((side) => {
let sideArray = side.split('');
sideArray.forEach((letter1) => {
sideArray.forEach((letter2) => {
disallowedLetterCombos[letter1 + letter2] = true;
disallowedLetterCombos[letter2 + letter1] = true;
});
});
});
let disallowedLetterCombosList = Object.keys(disallowedLetterCombos);
let disallowedLetterCombosRE = new RegExp(
disallowedLetterCombosList.join('|'),
'i'
);
let validWords = filteredWords.filter(
(word) => !word.match(disallowedLetterCombosRE)
);
console.log(`Valid words: ${validWords.length}`);
// Now we want to generate a lookup of all words that start with a specific
// letter. We will use this to connect words together.
let startsWithLookup = {};
validWords.forEach((word) => {
startsWithLookup[word[0]] = startsWithLookup[word[0]] || [];
startsWithLookup[word[0]].push(word);
});
// We need to keep track of solutions
let iterations = 0;
let oneWordSolutions = [];
let twoWordSolutions = [];
validWords.forEach((word) => {
// We're going to keep track of what letters we need to use in an object
// and delete the letters from the object every time we find them
// When the object has no keys left, all letters have been found
let letterLookup = {};
sides
.join('')
.split('')
.forEach((letter) => (letterLookup[letter] = true));
// Now iterate over the current word, deleting the found letters
word.split('').forEach((letter) => delete letterLookup[letter]);
if (Object.keys(letterLookup).length === 0) {
// If no letters in our lookup, it's a one word solution!
oneWordSolutions.push([word]);
} else {
// Otherwise, we need to find the words that can link to this word
// We do this by checking our startsWith table
let nextWords = startsWithLookup[word[word.length - 1]];
nextWords.forEach((nextWord) => {
// We do the same thing for the next word, checking to see if we have
// found a valid solution
iterations++;
let curLetterLookup = {...letterLookup};
nextWord.split('').forEach((letter) => delete curLetterLookup[letter]);
if (Object.keys(curLetterLookup).length === 0) {
// we have solved letter-boxed!
twoWordSolutions.push([word, nextWord]);
}
});
}
});
// Track the total execution time!
let time = new Date().valueOf() - t0;
// Return our results
return {
time,
iterations,
solutions: [].concat(oneWordSolutions, twoWordSolutions),
};
};