Skip to content

Instantly share code, notes, and snippets.

@huytd
Last active March 26, 2026 21:25
Show Gist options
  • Select an option

  • Save huytd/6a1a6a7b34a0d0abcac00b47e3d01513 to your computer and use it in GitHub Desktop.

Select an option

Save huytd/6a1a6a7b34a0d0abcac00b47e3d01513 to your computer and use it in GitHub Desktop.
Wordle in less than 50 lines of Bash

image

How to use:

./wordle.sh

Or try the unlimit mode:

./wordle.sh unlimit
words=($(grep '^\w\w\w\w\w$' /usr/share/dict/words | tr '[a-z]' '[A-Z]'))
actual=${words[$[$RANDOM % ${#words[@]}]]} end=false guess_count=0 max_guess=6
if [[ $1 == "unlimit" ]]; then
max_guess=999999
fi
while [[ $end != true ]]; do
guess_count=$(( $guess_count + 1 ))
if [[ $guess_count -le $max_guess ]]; then
echo "Enter your guess ($guess_count / $max_guess):"
read guess
guess=$(echo $guess | tr '[a-z]' '[A-Z]')
if [[ " ${words[*]} " =~ " $guess " ]]; then
output="" remaining=""
if [[ $actual == $guess ]]; then
echo "You guessed right!"
for ((i = 0; i < ${#actual}; i++)); do
output+="\033[30;102m ${guess:$i:1} \033[0m"
done
printf "$output\n"
end=true
else
for ((i = 0; i < ${#actual}; i++)); do
if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
remaining+=${actual:$i:1}
fi
done
for ((i = 0; i < ${#actual}; i++)); do
if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
if [[ "$remaining" == *"${guess:$i:1}"* ]]; then
output+="\033[30;103m ${guess:$i:1} \033[0m"
remaining=${remaining/"${guess:$i:1}"/}
else
output+="\033[30;107m ${guess:$i:1} \033[0m"
fi
else
output+="\033[30;102m ${guess:$i:1} \033[0m"
fi
done
printf "$output\n"
fi
else
echo "Please enter a valid word with 5 letters!";
guess_count=$(( $guess_count - 1 ))
fi
else
echo "You lose! The word is:"
echo $actual
end=true
fi
done
@aaronNGi

aaronNGi commented Feb 6, 2022 •

Copy link
Copy Markdown

Here's a variant done in (Edit:) under 20 lines of bash: https://gist.github.com/aaronNGi/04a209b1cd17f1fa03af7a87eb14cc42

@BMDan

BMDan commented Feb 9, 2022 •

Copy link
Copy Markdown

Cleaned up a few things. Biggest changes:

  1. argv[1] can now be "unlimit" or "unlimited" (or anything else starting with "unlimit").
  2. Use proper termcap codes to set colors (should work better on a wider variety of terminals).
  3. Clean up input to use read(1) and to save ourselves an if indentation on the rest of the logic with a while.
  4. Provide more feedback on rejected words.
Full script
#!/bin/bash

function wordsgrep() {
  (IFS=$'\n'; echo "${words[*]}") | grep -qixF "${1:-inv.alid}"
}

words=($(grep -xE '\w{5}' /usr/share/dict/words | tr '[:lower:]' '[:upper:]'))
actual=${words[$[$RANDOM % ${#words[@]}]]} guess_count=0 max_guess=6
[[ "${1//unlimit}" != "${1:-}" ]] && max_guess=999999
while true; do
    guess_count=$(( $guess_count + 1 ))
    if [[ $guess_count -le $max_guess ]]; then
        while read -r -p "Enter your guess ($guess_count / $max_guess): " guess; do
            wordsgrep "$guess" && break
            [[ ${#guess} != 5 ]] && echo "Too short/long." && continue
            echo "Not a real word."
        done
        guess="$(tr '[:lower:]' '[:upper:]' <<<"$guess")"
        output="" remaining=""
        if [[ $actual == $guess ]]; then
            echo "You guessed right!"
            for ((i = 0; i < ${#actual}; i++)); do
                output+="$(tput setaf 0)$(tput setab 10) ${guess:$i:1} $(tput sgr0)"
            done
            echo "$output"
            break
        else
            for ((i = 0; i < ${#actual}; i++)); do
                if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
                    remaining+=${actual:$i:1}
                fi
            done
            for ((i = 0; i < ${#actual}; i++)); do
                if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
                    if [[ "$remaining" == *"${guess:$i:1}"* ]]; then
                        output+="$(tput setaf 0)$(tput setab 11) ${guess:$i:1} $(tput sgr0)"
                        remaining=${remaining/"${guess:$i:1}"/}
                    else
                        output+="$(tput setaf 0)$(tput setab 15) ${guess:$i:1} $(tput sgr0)"
                    fi
                else
                    output+="$(tput setaf 0)$(tput setab 10) ${guess:$i:1} $(tput sgr0)"
                fi
            done
            echo "$output"
        fi
    else
        echo "You lose! The word is:"
        echo $actual
        break
    fi
done
Diff (ignoring whitespace changes)
--- a/wordle.sh	2022-02-09 15:45:06.000000000 -0800
+++ b/wordle.sh	2022-02-09 15:45:06.000000000 -0800
@@ -1,23 +1,29 @@
-words=($(grep '^\w\w\w\w\w$' /usr/share/dict/words | tr '[a-z]' '[A-Z]'))
-actual=${words[$[$RANDOM % ${#words[@]}]]} end=false guess_count=0 max_guess=6
-if [[ $1 == "unlimit" ]]; then
-    max_guess=999999
-fi
-while [[ $end != true ]]; do
+#!/bin/bash
+
+function wordsgrep() {
+  (IFS=$'\n'; echo "${words[*]}") | grep -qixF "${1:-inv.alid}"
+}
+
+words=($(grep -xE '\w{5}' /usr/share/dict/words | tr '[:lower:]' '[:upper:]'))
+actual=${words[$[$RANDOM % ${#words[@]}]]} guess_count=0 max_guess=6
+[[ "${1//unlimit}" != "${1:-}" ]] && max_guess=999999
+while true; do
     guess_count=$(( $guess_count + 1 ))
     if [[ $guess_count -le $max_guess ]]; then
-        echo "Enter your guess ($guess_count / $max_guess):"
-        read guess
-        guess=$(echo $guess | tr '[a-z]' '[A-Z]')
-        if [[ " ${words[*]} " =~ " $guess " ]]; then
+        while read -r -p "Enter your guess ($guess_count / $max_guess): " guess; do
+            wordsgrep "$guess" && break
+            [[ ${#guess} != 5 ]] && echo "Too short/long." && continue
+            echo "Not a real word."
+        done
+        guess="$(tr '[:lower:]' '[:upper:]' <<<"$guess")"
             output="" remaining=""
             if [[ $actual == $guess ]]; then
                 echo "You guessed right!"
                 for ((i = 0; i < ${#actual}; i++)); do
-                    output+="\033[30;102m ${guess:$i:1} \033[0m"
+                output+="$(tput setaf 0)$(tput setab 10) ${guess:$i:1} $(tput sgr0)"
                 done
-                printf "$output\n"
-                end=true
+            echo "$output"
+            break
             else
                 for ((i = 0; i < ${#actual}; i++)); do
                     if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
@@ -27,24 +33,20 @@
                 for ((i = 0; i < ${#actual}; i++)); do
                     if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
                         if [[ "$remaining" == *"${guess:$i:1}"* ]]; then
-                            output+="\033[30;103m ${guess:$i:1} \033[0m"
+                        output+="$(tput setaf 0)$(tput setab 11) ${guess:$i:1} $(tput sgr0)"
                             remaining=${remaining/"${guess:$i:1}"/}
                         else
-                            output+="\033[30;107m ${guess:$i:1} \033[0m"
+                        output+="$(tput setaf 0)$(tput setab 15) ${guess:$i:1} $(tput sgr0)"
                         fi
                     else
-                        output+="\033[30;102m ${guess:$i:1} \033[0m"
+                    output+="$(tput setaf 0)$(tput setab 10) ${guess:$i:1} $(tput sgr0)"
                     fi
                 done
-                printf "$output\n"
-            fi
-        else
-            echo "Please enter a valid word with 5 letters!";
-            guess_count=$(( $guess_count - 1 ))
+            echo "$output"
         fi
     else
         echo "You lose! The word is:"
         echo $actual
-        end=true
+        break
     fi
 done

IMHO, storing the words as an array, while a neat trick, also gets pretty slow pretty quickly. A version without arrays to follow....

@BMDan

BMDan commented Feb 9, 2022 •

Copy link
Copy Markdown

As promised, the version without arrays. Also makes some minor stylistic changes, deletes an unnecessary loop, and adds an "abandon" feature (press CTRL-D at the prompt). 36 lines, total.

actual="$(sort -R /usr/share/dict/words | grep -xEm 1 '\w{5}' | tr '[:lower:]' '[:upper:]')"
guess_count=0 max_guess=6
[[ "${1//unlimit}" != "${1:-}" ]] && max_guess=999999
while true; do
    guess_count=$(( guess_count + 1 ))
    if [[ $guess_count -le $max_guess ]]; then
        while read -r -p "Enter your guess ($guess_count / $max_guess): " guess; do
            grep -ixF "${guess:-inv.alid}" /usr/share/dict/words | grep -xqE '\w{5}' && break
            [[ ${#guess} != 5 ]] && echo "Too short/long." && continue
            echo "Not a real word."
        done
        [ ${#guess} -eq 0 ] && echo && echo "Giving up so soon?  The answer was $actual." && break
        guess="$(tr '[:lower:]' '[:upper:]' <<<"$guess")"
        output="" remaining=""
        for ((i = 0; i < ${#actual}; i++)); do
            [[ "${actual:$i:1}" != "${guess:$i:1}" ]] && remaining+=${actual:$i:1}
        done
        for ((i = 0; i < ${#actual}; i++)); do
            if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
                if [[ "$remaining" == *"${guess:$i:1}"* ]]; then
                    output+="$(tput setaf 0)$(tput setab 11) ${guess:$i:1} $(tput sgr0)"
                    remaining=${remaining/"${guess:$i:1}"/}
                else
                    output+="$(tput setaf 0)$(tput setab 15) ${guess:$i:1} $(tput sgr0)"
                fi
            else
                output+="$(tput setaf 0)$(tput setab 10) ${guess:$i:1} $(tput sgr0)"
            fi
        done
        echo "$output"
        [ "$actual" = "$guess" ] && echo "You guessed right!" && break
    else
        echo "You lose!  The word was $(tput setaf 1)$(tput bold)$actual$(tput sgr0)."
        break
    fi
done

@kristinbell

Copy link
Copy Markdown

Here's a challenge for someone: Can we make a game that uses more than one language at a time? Would that just involve using two dictionaries? Or would the game need different rules?

@cjdinger

Copy link
Copy Markdown

I wrote a SAS version and placed it here sascommunities/wordle-sas. Uses the word lists from cfreshman (thanks) and arrays to check guesses.

example-game-dsobj

@aramvr

aramvr commented Mar 18, 2022

Copy link
Copy Markdown

I learned about this from an Oreilly blog post. Good job!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment