Published
Edited
Mar 1, 2022
2 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class State {
constructor({ inclusions, exclusions }) {
this.inclusions = inclusions;
this.exclusions = exclusions;
}

static empty() {
return new State({ inclusions: [], exclusions: [] });
}

isEmpty() {
return !this.inclusions.length && !this.exclusions.length;
}

static fromGuessesAndResults({ guesses, results }) {
let s = State.empty();

_.zip(guesses, results).forEach(([guess, result]) => {
// min uneven length lists
if (!guess || !result) {
return;
}

s = s.addGuessResult({ guess, result });
});

return s;
}

addGuessResult({ guess, result }) {
// result is a string of emojis so its length is not necessarily 5,
// as emojis may occupy more than 1 code point in a string.
// We have to break the string into a list so that each emoji
// gets its own index.
result = [...result];

let positionalInclusions = "";
let positionalExclusions = [];
const globalInclusions = [];
let globalExclusions = [];

for (let i = 0; i < guess.length; ++i) {
const letter = guess[i];
const color = result[i];

switch (color) {
case "🟩":
case "g":
positionalInclusions += letter;
break;
case "🟨":
case "y":
positionalInclusions += ".";
positionalExclusions.push([i, letter]);
globalInclusions.push(letter);
break;
case "⬛":
case "b":
positionalInclusions += ".";
positionalExclusions.push([i, letter]);
globalExclusions.push(letter);
break;
}
}

// global exclusions should never override inclusions
// strip out global exclusions that are marked as needed to be included
globalExclusions = globalExclusions.filter(
(exclusion) =>
!globalInclusions.includes(exclusion) &&
!positionalInclusions.includes(exclusion)
);

const newInclusions = [...this.inclusions];
const newExclusions = [...this.exclusions];

// positional inclusions
newInclusions.unshift(new RegExp(`^${positionalInclusions}$`));
// PERFORMANCE: We're using unshift here because new Regexes will be used first
// by the _.every and the _.some that we use later on.

// positional exclusions
for (const [i, letter] of positionalExclusions) {
// split on every character
const base = ".....".split("");
base[i] = letter;
newExclusions.unshift(new RegExp(`^${_.sum(base)}$`));
}

// global inclusions
if (globalInclusions.length !== 0) {
// https://stackoverflow.com/a/15341118
// match at least the characters anywhere in the string
newInclusions.unshift(
new RegExp(
`(.*[${_.sum(globalInclusions)}]){${globalInclusions.length}}`
)
);
}

// global exclusions
if (globalExclusions.length !== 0) {
newExclusions.unshift(new RegExp(`[${_.sum(globalExclusions)}]`));
}

return new State({ inclusions: newInclusions, exclusions: newExclusions });
}

getPossibleQuarries(oldQuarryPool) {
// PERFORMANCE: don't traverse the dictionary every time when we can use the old candidate pool
const pool = oldQuarryPool ? oldQuarryPool : dictionary;

return pool.filter(
(word) =>
// PERFORMANCE: _.some will fail fast, _.every will not
// Do not reorder
!_.some(this.exclusions, (regex) => regex.test(word)) &&
_.every(this.inclusions, (regex) => regex.test(word))
);
}
}
Insert cell
Insert cell
State.empty()
Insert cell
State.empty().addGuessResult({ guess: "wrong", result: "bbygb" })
Insert cell
Insert cell
State.empty().getPossibleQuarries()
Insert cell
State.empty()
.addGuessResult({ guess: "wrong", result: "bbygb" })
.getPossibleQuarries()
Insert cell
testCandidates = State.empty()
.addGuessResult({ guess: "wrong", result: "bbygb" })
Insert cell
Insert cell
function scoreGuess({ guess, quarry }) {
if (guess === undefined) {
throw new Error("Unable to score undefined guess");
}
const zipped = guess.split("").map((guessChar, i) => ({
guessChar,
color: guessChar === quarry[i] ? "🟩" : "⬛",
quarryChar: quarry[i],
// burned === true if the quarry cannot be used to trigger yellow detection
burned: guessChar === quarry[i] ? true : false
}));

for (const row of zipped) {
if (row.color === "⬛" && quarry.includes(row.guessChar)) {
// try and find a non-burned pairing
const maybePair = zipped.find(
(anotherRow) =>
!anotherRow.burned && row.guessChar === anotherRow.quarryChar
);

// if we find it, mark the quarry burned and the guess yellow
if (maybePair) {
row.color = "🟨";
maybePair.burned = true;
}
}
}

// extract only color results
return _.sum(zipped.map((row) => row.color));
}
Insert cell
Insert cell
Insert cell
scoreGuess({ guess: "slate", quarry: "rebus" })
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3.range(0, 10)
Insert cell
R = require("ramda")
Insert cell
R.aperture(2, d3.range(0, 10, 3))
Insert cell
sortByHighestInformationGainWord = async ({ state }) => {
if (state.isEmpty()) {
// PERFORMANCE: hardcode the top results to avoid expensive computation on first load
return ["raise", "slate", "crate", "arise", "trace"];
}

// PERFORMANCE: Run computation in worker so we don't hang the UI
let informationGains = await spawnCalculateInformationGainsWorker({
state
});

// strip annotation assigned by worker
informationGains = _.omit(informationGains, "_time");

// sort highest to lowest
informationGains = Object.fromEntries(
Object.entries(informationGains)
// highest to lowest
.sort(([, a], [, b]) => b - a)
);

console.table(informationGains);

// yield words
return Object.keys(informationGains);
}
Insert cell
function calculateInformationGains({ state }) {
// Class prototypes do not cross the Web Worker boundary.
// We need to clone the State to get a State with a full prototype that
// has been imported into the Web Worker.
state = new State(state);
// Given the current pool of possible quarries...
const oldQuarryPool = state.getPossibleQuarries();

// HACK: When we've found the quarry, _no_ choice gives us more information.
// We hardcode the information gains to maximise our only valid choice.
if (oldQuarryPool.length === 1) {
return { [oldQuarryPool[0]]: 1 };
}

const informationGains = {};

// ...choose a guess
for (const guess of oldQuarryPool) {
let informationGainSum = 0;

// ...and compare it against every quarry
for (const quarry of oldQuarryPool) {
if (guess === quarry) {
continue;
}

const result = scoreGuess({ guess, quarry });
// ...see what happens to the candidate pool size if we guess it
const newQuarryPool = state
.addGuessResult({
guess,
result
})
// PERFORMANCE: Don't retraverse the entire dictionary when we
// know previous non-exclusions
.getPossibleQuarries(oldQuarryPool);

// ...and record the information gain
const I = Math.log2(oldQuarryPool.length / newQuarryPool.length);
informationGainSum += I;
}

const averageInformationGain = (informationGains[guess] =
informationGainSum / oldQuarryPool.length);
}
return informationGains;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more