Skip to content

Instantly share code, notes, and snippets.

@tshddx
Created January 23, 2022 20:05
Show Gist options
  • Save tshddx/72b6a4a16d188eedef14d669aed756ef to your computer and use it in GitHub Desktop.
Save tshddx/72b6a4a16d188eedef14d669aed756ef to your computer and use it in GitHub Desktop.
Script to test Wordle bots against each other
type Word = string;
// A game is a series of guesses and a status (note that a Game might still be
// in progress, and the current game status is the same as the status of the
// last guess).
type Game = {
readonly guesses: Guess[];
readonly status: Status;
};
// A guess is an array of LetterInfos, one for each letter in the guessed word.
type Guess = {
readonly letterInfos: LetterInfo[];
readonly status: Status;
};
type Status = 'in progress' | 'succeeded' | 'failed';
const MAXIMUM_GUESSES = 6;
// These are the green/yellow/gray colors for each letter of a guessed word.
type LetterInfo = {
readonly letter: string;
readonly index: number;
readonly info: 'correct position' | 'incorrect position' | 'not in word';
};
// A player has a function which takes a game and guesses the next word.
type Player = {
readonly name: string;
readonly description: string;
readonly guessAWord: (game: Game) => Word;
};
function runGame(correctWord: Word, player: Player): Game {
const correctLetters = [...correctWord.toUpperCase()];
// Start a new game.
let game: Game = { guesses: [], status: 'in progress' };
// Take guesses from the player until the game is completed.
while (game.status === 'in progress') {
const guessedWord = player.guessAWord(game);
const guessedLetters = [...guessedWord.toUpperCase()];
const letterInfos = guessedLetters.map((letter, index): LetterInfo => {
const info =
letter === correctLetters[index]
? 'correct position'
: correctLetters.includes(letter)
? 'incorrect position'
: 'not in word';
return { letter, info, index };
});
// Determine the status of this guess and concatenate it onto the game's
// guesses.
const status = letterInfos.every(({ info }) => info === 'correct position')
? 'succeeded'
: game.guesses.length + 1 >= MAXIMUM_GUESSES
? 'failed'
: 'in progress';
const guess: Guess = { letterInfos, status };
const guesses = [...game.guesses, guess];
game = { guesses, status };
}
return game;
}
const WORDS = ['way', 'day', 'man', 'eye', 'job', 'lot', 'lol'].map((word) =>
word.toUpperCase()
);
const ConstantGuessPlayer: Player = {
name: 'ConstantGuessPlayer',
description: 'always guesses the word "lol"',
guessAWord: (_game) => 'lol',
};
const FirstPossibleWordPlayer: Player = {
name: 'FirstPossibleWordPlayer',
description:
'determines all *possible* words from past clues and guesses the first one',
guessAWord: (game) => {
// Get all letterInfos from all guesses.
const allLetterInfos: LetterInfo[] = [];
game.guesses.forEach((guess) => {
guess.letterInfos.forEach((letterInfo) =>
allLetterInfos.push(letterInfo)
);
});
// Check for words that match all green letters.
const greenLetters: LetterInfo[] = [];
allLetterInfos
.filter(({ info }) => info === 'correct position')
.forEach((letterInfo) => greenLetters.push(letterInfo));
function matchesAllGreenLetters(word: Word) {
return greenLetters.every(
({ letter, index }) => word.charAt(index) === letter
);
}
// Check for words that match all yellow letters.
const yellowLetters: LetterInfo[] = [];
allLetterInfos
.filter(({ info }) => info === 'incorrect position')
.forEach((letterInfo) => yellowLetters.push(letterInfo));
function matchesAllYellowLetters(word: Word) {
return yellowLetters.every(
({ letter, index }) =>
word.charAt(index) !== letter && word.includes(letter)
);
}
// Check for words containing no gray letters.
const grayLetters: string[] = [];
allLetterInfos
.filter(({ info }) => info === 'not in word')
.forEach(({ letter }) => grayLetters.push(letter));
function containsNoGrayLetters(word: Word) {
return grayLetters.every((letter) => !word.includes(letter));
}
return WORDS.find(
(word) =>
matchesAllGreenLetters(word) &&
matchesAllYellowLetters(word) &&
containsNoGrayLetters(word)
)!;
},
};
const players = [
// ConstantGuessPlayer,
FirstPossibleWordPlayer,
];
WORDS.forEach((correctWord) => {
console.log('\n-----------------------------------------------------------');
console.log(bold(`The correct word is ${[...correctWord].join(' ')}`));
console.log();
players.forEach((player) => {
console.log(`${player.name} (${player.description})`);
const completedGame = runGame(correctWord, player);
completedGame.guesses.forEach((guess, index) => {
const numeral = `${index + 1}.`;
const lettersWithColors = guess.letterInfos
.map(({ letter, info }) => {
const color =
info === 'correct position'
? green
: info === 'incorrect position'
? yellow
: gray;
return color(' ' + letter + ' ');
})
.join(' ');
const gameResult =
guess.status === 'succeeded'
? 'Game won!'
: guess.status === 'failed'
? 'Game lost'
: '';
console.log(numeral, lettersWithColors, gameResult);
});
console.log();
});
});
//
// Bash string formatting utilities
//
function green(s: string) {
let output = `\x1b[42m${s}\x1b[0m`;
return output;
}
function yellow(s: string) {
let output = `\x1b[43m${s}\x1b[0m`;
return output;
}
function gray(s: string) {
let output = `\x1b[100m${s}\x1b[0m`;
return output;
}
function bold(s: string) {
let output = `\x1b[22m${s}\x1b[0m`;
return output;
}
//
// General utilities
//
function last<T>(array: T[]): T {
return array[array.length - 1];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment