I’ve been looking at the Wordle app and it seems to me that it’s pretty darn easy to duplicate. I’m teaching myself shell script programming so am wondering if there’s a way to create a wordle for the command line in Linux?
There’s a lot about Wordle that was really smart from the first release, not the least of which is that it’s simple to grasp and hard to master. Since it only has one word puzzle per day, it’s also a perfect casual game for a mobile device on the way to school, work, even while laying in bed, slowly waking up. Six guesses. Five letters. It shows if you guess a letter in the correct slot, a letter that’s in the word but not the first slot, and any letters in your guess that are missing from the word.
The other facet that worked really well is that it’s really easy to share your results with friends on social media. Of course, friends who don’t play Wordle won’t be interested, but hey, those that do can easily compare their number of guesses with yours, and sweet are the days when you get the word in just a few guesses! Since there are no clues, it’s definitely tough because there are thousands upon thousands of 5-letter words in the English dictionary.
Still, if you can code it, you can also code it as a shell script, though it might not be the most efficient or elegant (particularly compared to the simple and effective design of the mobile app). Let’s dig in and see how to recreate Wordle as a Linux and MacOS shell script.
STEP 1: PICK A WORD FROM THE DICTIONARY
Various distros of Linux include dictionaries, but they often include hundreds of quite obscure words that don’t even show up in standard English language dictionaries. The game’s no fun if it’s prompting for words you’ve never heard of, right? Fortunately, the Web comes through and there’s a nice text file of about 5,000 5-letter English words you can grab right off the net at
https://www-cs-faculty.stanford.edu/~knuth/sgb-words.txt
Grab it and save it as “5-letter-words.txt”. Then define a variable so we can refer to it as “dict” in the future:
dict="5-letter-words.txt".
Now let’s get into the first interesting bit of coding: Picking a random word from a text file. This can be done by calculating the number of lines in the file, then using the built-in $RANDOM variable in the shell to generate a random 2**32 integer, modded down to be between 0 and the number of lines – 1:
# 1. Pick a word from the list lines="$(wc -l < $dict)" randomchoice="$(( $RANDOM % $lines ))" word="$(head -n $randomchoice $dict | tail -1)"
Hopefully, that’s all clear to you, using $randomchoice to remember what line we want, then head|tail to pull out just that line from the dictionary. The result is that $word is the word to guess.
Now, while we have it, let’s break it into five one-letter variables. There are lots of ways to do it, but I’ll use a simple cut in a subshell approach:
w1="$(echo $word | cut -c1)" ; w2="$(echo $word | cut -c2)" w3="$(echo $word | cut -c3)" ; w4="$(echo $word | cut -c4)" w5="$(echo $word | cut -c5)"
Step one’s done. $word contains the word to guess and $w1 thru $w5 contain the five letters that comprise that word.
STEP 2: PROMPT USER FOR THEIR GUESS
The second step is to create a loop and within it prompt the user for their guess. Easily done:
until [ 0 -eq 1 ] ; do echo "\nGuess: \c" read answer more code will go here done
The rest of our code (with the exception of a function that will show up in a bit) will be tucked after ‘read answer’ but before ‘done’. While we’re processing the guess, let’s break it down into five sequential variables too:
a1="$(echo $answer | cut -c1)" ; a2="$(echo $answer | cut -c2)" a3="$(echo $answer | cut -c3)" ; a4="$(echo $answer | cut -c4)" a5="$(echo $answer | cut -c5)"
You can always slice a variable with a ${varname:x:y} notation, but I prefer separate variables for clarity.
STEP 3: CHECK GUESS AGAINST DICTIONARY
Since one of the Wordle rules is that your guess has to be a proper English word, we’ll need to check for that too. Turns out it’s pretty darn easy…
if [ "$(grep -E "^$answer\$" $dict)" = "" ] ; then # Is answer in dictionary? If not, don't evaluate it? echo "Your guess is not in my dictionary." else
I’ve formatted this as a conditional because it’s one of a number of tests we’ll apply…
STEP 4: COMPARE EACH LETTER AGAINST TARGET
The next step, and by far the most complicated, is to test each letter of the user’s guess against not just that same letter slot in the target word, but against all the letters in the word. To accomplish this I’m going to create a shell function:
lettercheck() { # answer-letter lettercount # all answer letters are already in w1 w2 etc # returns value in "$returnchar" if [ "$1" = "$(eval echo \$w$2)" ] ; then # letter guessed in correct spot! returnchar="$(echo $1 | tr "[:lower:]" "[:upper:]" )" elif [ "$(echo $word | grep "$1")" != "" ] ; then # letter present, wrong spot (needs refinement) returnchar="$1" else # letter not present in word returnchar="-" fi }
Since you can pass variables to functions in the shell, it will be invoked with the first parameter the letter guessed and the second parameter the ordinal slot (in other words, if I guessed “carom” then letter 3 would be “r” and it would be the #3 letter in my guess.
Since I can’t change the color of letters guessed, I’m instead using the notation that echoing back the guess with a letter transliterated to uppercase means it’s the right letter in the right spot, if it’s lowercase then it’s the right letter in the wrong spot, and if it’s just a “-” it’s a letter that isn’t present. Can you see how that all shows up in the code above?
STEP 5: EVALUATE GUESS WITH LETTERCHECK
With the function written, it’s time to utilize it, and here’s how I do just that:
echo "Result of your guess: \c" lettercheck "$a1" 1 ; echo " $returnchar \c" lettercheck "$a2" 2 ; echo " $returnchar \c" lettercheck "$a3" 3 ; echo " $returnchar \c" lettercheck "$a4" 4 ; echo " $returnchar \c" lettercheck "$a5" 5 ; echo " $returnchar"
I’m taking advantage of the \c notation in echo to have it not include a carriage return, allowing the script to build up an output line letter by letter. Handy!
PUTTING IT ALL TOGETHER
Before I show you all the code with loops, let’s have a look at the output, a quick game of SHWORDLE:
$ sh shwordle.sh Guess: birds Result of your guess: - - - - S Guess: carps Result of your guess: - a - - S Guess: reels Result of your guess: - E e L S Guess: zeals You guessed it: ZEALS! Well done.
I cheated because I knew the word in advance, but you can see that it’s hard, but when you remember that uppercase means correct letter, correct slot, and lowercase means correct letter, wrong slot, it works perfectly. Well, not quite, but I’ll get back to that.
For now, here’s the entire script, line-for-line, including comments:
#!/bin/sh # SHWORDLE - duplicates functionality of the Wordle word guessing game # written by Dave Taylor. Still has a few bugs to iron out. dict="/Users/taylor/bin/5-letter-words.txt" lettercheck() { # answer-letter lettercount # all answer letters are already in w1 w2 etc # returns value in "$returnchar" if [ "$1" = "$(eval echo \$w$2)" ] ; then # letter guessed in correct spot! returnchar="$(echo $1 | tr "[:lower:]" "[:upper:]" )" elif [ "$(echo $word | grep "$1")" != "" ] ; then # letter present, wrong spot (needs refinement) returnchar="$1" else # letter not present in word returnchar="-" fi } # 1. Pick a word from the list lines="$(wc -l < $dict)" randomchoice="$(( $RANDOM % $lines ))" word="$(head -n $randomchoice $dict | tail -1)" w1="$(echo $word | cut -c1)" ; w2="$(echo $word | cut -c2)" w3="$(echo $word | cut -c3)" ; w4="$(echo $word | cut -c4)" w5="$(echo $word | cut -c5)" # here's a cheat: echo " -- $word -- $w1 $w2 $w3 $w4 $w5 " # 2. Loop to prompt user for a guess. until [ 0 -eq 1 ] ; do echo "\nGuess: \c" read answer # 3. break down guessed word into letters a1="$(echo $answer | cut -c1)" ; a2="$(echo $answer | cut -c2)" a3="$(echo $answer | cut -c3)" ; a4="$(echo $answer | cut -c4)" a5="$(echo $answer | cut -c5)" if [ "$answer" = "$word" ] ; then # got it! well done echo "You guessed it: $(echo $word | tr '[:lower:]' '[:upper:]')! Well done." exit 0 elif [ "$answer" = "quit" -o "$answer" = "" ] ; then # escape the infinite loop echo "The word was $(echo $word | tr '[:lower:]' '[:upper:]'). See ya next time." exit 0 else # evaluate the guess if [ "$(grep -E "^$answer\$" $dict)" = "" ] ; then # 4. Is answer in dictionary? If not, don't evaluate it? echo "Your guess is not in my dictionary." else # 5. evaluate and output result: echo "Result of your guess: \c" lettercheck "$a1" 1 ; echo " $returnchar \c" lettercheck "$a2" 2 ; echo " $returnchar \c" lettercheck "$a3" 3 ; echo " $returnchar \c" lettercheck "$a4" 4 ; echo " $returnchar \c" lettercheck "$a5" 5 ; echo " $returnchar" fi fi done exit 0
So what’s not quite right? The main functionality issue appears when you have duplicate letters in a word. If you look at the lettercheck code closely, you’ll realize that if the word is, say, zeals and I guess bossy, both ‘s’ letters will be shown as matching a letter in the final word, even though only the first one should be a match. That bug, however, I will leave for you to solve, and remember that it can happen with a duplicate letter in the guess or in the target word. Otherwise, this should give you a big headstart in creating Wordle for the Linux command line. Have fun!
Pro Tip: I’ve been writing about Linux since the dawn of the operating system, and Unix before that. Please check out my extensive Linux help area and Linux shell script programming area for lots of additional tutorial content while you’re visiting. Thanks!