How I Made My Own Wordle
I built my own version of Wordle over a weekend. Here's how I did it.
If you haven't heard of Wordle by now, you might've recently emerged from a cryogenic sleep chamber. If you're like the rest of us computer-toucher types, you've been enjoying Josh Wardle's daily language game, based on Lingo.
I've been in the process of re-developing my site, and having finished a couple other games[flags][bpm], I thought I would take a crack at my own version of the internet's favourite daily challenge. Here's how I did it.
Rules Of The Game
To get started, we need to know the rules and parameters.
- • You have six chances to guess the word
- • Your entries must be dictionary words
- • If your letter is not in the word, it will be grey
- • If you get a letter correct, but it's in the wrong place it will be yellow
- • If you get a letter in the right spot it will be green
I'm going to add a few more wrinkles.
- • Players can choose to play 4-letter, 5-letter, or 6-letter versions
- • Players can play multiple rounds in a row
- • If a word has multiple occurrences of a letter, and the player makes a guess with doubles of that letter, the colour coding will behave as you would expect based on the above (in the real wordle, this doesn't happen)
The Words
For any good language game, we'll need a list of words. I downloaded mine from this github repository (the words_alpha.txt
file). Next, we'll need to create a list of reasonable guesses, because no one's going to be happy if their word is "tariqa" or "aahed". We also need a separate list of all the possible words so we can validate our players guesses (we don't want them to be able to enter "aeiou", for example).
To do this I created a script in python to narrow down the list of words using zipf_frequency
from wordfreq, plus words
from the nltk.corpus, and I used better_profanity to clean things up.
The script looks something like this:
with open('words.txt') as words:
for word in words:
if word in word_list:
freq = get_frequency(word)
if freq > 0:
all[word] = True
if freq > 3.1: # this is an arbitrary cut-off
if profanity.contains_profanity(word):
continue
else:
common_words.append(word)
# output files of common words of length 4,5,6 & save them
(the real script is a bit more complex, obviously)
The world lists I was able to generate were pretty good, but I still needed to go through each list to remove some words (ex. names and other proper nouns). My final lists for 4, 5, & 6 letter words have about 1500, 2200, and 2800 words respectively.
Now that we have our word lists, we can get to work on the game.
Game Time
Wordle is a pretty simple game to get working, but there is a lot to keep track of. I won't give away too much code, but hopefully it'll be enough to get you started on your own version.
Props & State
First, let's set up our game component with it's props and state.
Props
- •
words
: an array of strings, the list words we're using for the game.
and
- •
onGameEnd
, a callback function that fires when... you guessed it.
State
- •
round
: number, which word we are on (important if we're playing the 3 or 5 round version) - •
word
: string, the word we are trying to guess - •
guessesMade
: number, how many previous attempts we've taken at the current word - •
wordPosition
: number, the cursor position within the grid - •
letters
: array of arrays of characters, an array that holds the position of letters for the grid - •
guesses
: array of arrays of strings ('correct'|'has_letter'|'none' ), an array to track the state of previous guesses - •
guessedLetters
: object { letter: 'correct'|'has_letter'|'none' }, an object to help us track the previously guessed letters, and update the colours in the UI.
The initial state will be set with a couple useEffect
hooks, that run when the round
changes:
useEffect(() => {
setWord(words[round]);
}, [round]);
And when the word
changes:
useEffect(() => {
const letterMap: string[][] = [[]];
const guessMap: string[][] = [[]];
[0, 1, 2, 3, 4, 5].map((n) => {
letterMap[n] = new Array(word.length).fill('');
guessMap[n] = new Array(word.length).fill('');
});
setLetters(letterMap);
setGuesses(guessMap);
setGuessedLetters({});
setGuessesMade(0);
setWordPosition(0);
}, [word]);
Next we'll add some event functions, onKeyEntered
, onBack
, and onEnter
const onKeyEntered = (letter: string) => {
// update `letters`
// incremenent `wordPosition`
};
const onBack = () => {
// update `letters`
// decremenent `wordPosition`
};
const onEnter = () => {
// check that the word is long enough (no missing chars)
// make the guess (I use a separate function here)
};
Next, we need the function to actually make the guess. We can call it makeGuess
(if we're feeling creative). I'll leave the code up to you, but it should be just a few lines.
const makeGuess = (guess: string) => {
// check if the word is in the longer list of acceptable words
/* map the guess to the two arrays (`letters`, `guesses`)
* (it's a bit cleaner if you make another function for this)
* basically, loop through the characters in the guess
* and based on their position, update the guesses array with
* the appropriate state ('correct', 'has_letter', 'none').
*/
if (word === guess) {
// increment the round and end it with a success message
} else if (guessesMade === 5) {
// increment the round and end it with a fail message
} else {
// increment the round and set the word position to `0`
}
};
And we'll add an event listener that uses our three functions.
const keyDown = (event: KeyboardEvent) => {
const key = event.code;
if (key === 'Backspace') {
onBack();
}
if (key === 'Enter') {
onEnter();
}
if (key.includes('Key')) {
const [, letter] = key.split('Key');
onKeyEntered(letter);
}
};
useEffect(() => {
document.addEventListener('keydown', keyDown);
return () => document.removeEventListener('keydown', keyDown);
});
The UI
The game component's UI is made up of two main elements, the keyboard, and the grid. We can start with the grid. Using React's JS in HTML capabilities, we can take our letters
array and .map()
it:
letters.map((row, row_index) => {
return (
<div>
{row.map((col, col_index) => {
return <div>{letters[row_index][col_index]}</div>;
})}
</div>
);
});
I'll leave the classes and styles up to you ;)
Remember you can get the grid block's guess state like so:
const guess_state = guesses[row_index][col_index]; // 'correct'|'has_letter'|'none'
We can do something similar with the Keyboard element
{
[
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
['back', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'enter'],
].map((row) => {
return (
<div>
{row.map((key) => {
return <span onClick={onClick}>{key}</span>;
})}
</div>
);
});
}
Remember to colour code the keys here as well!
That's pretty much it! Now all you need is a bit of code that selects game settings, feeds a list of words into the component, and starts the game, plus a function to run when the game finishes.
You can try the game here. If you have any questions about the (pseudo)code above, you can reach out to me via email or Linkedin!
Thanks for reading!