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 |
@willhardy Yeah exactly, I understood "aahed" as soon as @pjt33 mentioned it was that past participle of aah. I don't think you need ML for this though. The Wordle list has fewer and simpler words. There are a few weird ones in that list as well but it's not as bad.
I liked the idea of using the English as a Second Language dictionary. It would reduce frustration a bit.
This isn't scrabble 😄
Wordle in less than 30 lines of PowerShell
$url = 'https://gist.githubusercontent.com/zneak/53f885f1fc5856741cb4/raw/a17a81d15acb8109cda8524c743186901dd269b6/words.txt'
$words = ((Invoke-WebRequest -Uri $url).Content.toLower() -split '\n' | Select-String '^\w\w\w\w\w$').Line
$actual = $words[(Get-Random -Maximum $words.Count)];
$max_guess = if ($args[0] -eq 'unlimit') {999999} else {6}
for ($guess_count = 1; $guess_count -le $max_guess; $guess_count++) {
$guess = (Read-Host -Prompt "Enter your guess ($guess_count / $max_guess)").ToLower()
if ($words -notcontains $guess) {
Write-Host 'Please enter a valid word with 5 letters!'
$guess_count--
continue
}
if ($actual -eq $guess) {
Write-Host 'You guessed right!'
Write-Host $guess -ForegroundColor Black -BackgroundColor Green
return
}
$remaining = ''
for ($i = 0; $i -lt $actual.Length; $i++) {if ($actual[$i] -ne $guess[$i]) {$remaining += $actual[$i]}}
for ($i = 0; $i -lt $actual.Length; $i++) {
if ($actual[$i] -ne $guess[$i]) {
if ($remaining.Contains($guess[$i])) {
Write-Host $guess[$i] -ForegroundColor Black -BackgroundColor DarkYellow -NoNewLine
$remaining = $remaining.Remove($remaining.IndexOf($guess[$i]), 1)
} else {Write-Host $guess[$i] -ForegroundColor Black -BackgroundColor Gray -NoNewLine}
} else {Write-Host $guess[$i] -ForegroundColor Black -BackgroundColor Green -NoNewLine}
}
Write-Host
}
Write-Host "You lose! The word is: $actual"
Nicely done.
The New General Service List is a list of commonly used words for English language learners. It has a list of words ranked by frequency. I don't remember exactly how they determine frequency of use, but it has proven to work well for me in building some word games.
http://www.newgeneralservicelist.org
It's licensed under the Creative Commons Attribution-ShareAlike 4.0 International License.
Ok, so we throw out the ~50 line thing and put a bit more into it...
Demo:
Code:
#!/bin/bash
# A bash implementation of wordle
# Provenance: https://gist.github.com/huytd/6a1a6a7b34a0d0abcac00b47e3d01513
# Figure out our dictionary and vars
wordle_dir="${XDG_DATA_HOME:-$HOME/.local/share}/wordle"
dict="${wordle_dir}/wordles"
mkdir -p "${wordle_dir}" || exit 1
dict_url="https://gist.githubusercontent.com/prichey/95db6bdef37482ba3f6eb7b4dec99101/raw/"
if [[ ! -f "${wordle_dir}/wordles" ]]; then
if command -v curl >/dev/null 2>&1; then
curl -s "${dict_url}" | tr '[:lower:]' '[:upper:]' > "${dict}"
else
LC_COLLATE=C grep -Eh '^[A-Za-z].{4}$' /usr/{,share/}dict/words 2>/dev/null |
grep -v "'" |
tr '[:lower:]' '[:upper:]' > "${dict}"
fi
fi
# If 'shuf' is present, we use it. It's simply the best thing for this.
if command -v shuf >/dev/null 2>&1; then
actual=$(shuf -n 1 "${dict}")
# Otherwise, if we're going to modulo, let's at least try to debias it
else
wordcount=$(wc -l < "${dict}")
randmax=$(( 32768 / wordcount * wordcount ))
while (( (rand=RANDOM) >= randmax )); do :; done
rand=$(( rand % wordcount ))
actual=$(sed -ne "${rand}{p;q}" "${dict}")
fi
# Usage: printcharbox int color char color char color char color char color char
# Example: printcharbox 1 green g green h white o white s yellow t
# int tells the function which iteration we're on.
# If it's 1, the function prints the top bar, otherwise it doesn't
# Either way, '\033[1F\033[K' is the magic: Move up one line, empty the line.
printcharbox() {
while (( "${#}" > 0 )); do
case "${1}" in
(1) printf -- '\033[1F\033[K%s\n' "+---+---+---+---+---+"; shift 1 ;;
([2-9]) printf -- '\033[1F\033[K%s' ""; shift 1 ;;
(green) printf -- '|\033[30;102m %s \033[0m' "${2}"; shift 2 ;;
(yellow) printf -- '|\033[30;103m %s \033[0m' "${2}"; shift 2 ;;
(white) printf -- '|\033[30;107m %s \033[0m' "${2}"; shift 2 ;;
(red) printf -- '|\033[30;101m %s \033[0m' "${2}"; shift 2 ;;
(*) printf -- '%s\n' "Unrecognised input" >&2; exit 1 ;;
esac
# For versions of sleep that don't support floats, we quietly failover
sleep 0.5 >/dev/null 2>&1 || sleep 1
done
printf -- '%s\n' "|" "+---+---+---+---+---+"
}
[[ "${1}" == "unlimit" ]] && max_guess=999999
for (( guess_count=1; guess_count<=${max_guess:-6}; guess_count++ )); do
fail_count=0
until grep -xiq "^${guess}$" "${dict}"; do
read -rp "Please enter a valid 5 letter word (${guess_count} / ${max_guess:-6}): " guess
(( fail_count++ ))
done
# If the above interaction has iterated fail_count, we use it to determine
# how many lines to walk up and blank out
for (( i=1; i<fail_count; i++ )); do
printf -- '\033[1F\033[K%s' ""
done
guess="${guess^^}"; output=( "${guess_count}" ); remaining=""
if [[ "${actual}" == "${guess}" ]]; then
for ((i=0; i<5; i++)); do
output+=( green "${guess:$i:1}" )
done
printcharbox "${output[@]}"; exit 0
fi
for ((i=0; i<5; i++)); do
[[ "${actual:$i:1}" != "${guess:$i:1}" ]] && remaining+=${actual:$i:1}
done
for ((i=0; i<5; i++)); do
if [[ "${actual:$i:1}" != "${guess:$i:1}" ]]; then
if [[ "${remaining}" == *"${guess:$i:1}"* ]]; then
output+=( yellow "${guess:$i:1}" )
remaining=${remaining/"${guess:$i:1}"/}
else
output+=( white "${guess:$i:1}" )
fi
else
output+=( green "${guess:$i:1}" )
fi
done
printcharbox "${output[@]}"; guess=""
done
# If we reach this point, then we're wrong. Let's output the correct answer...
printf -- '%s\n' "" "Correct Answer:" ""
output=( 1 )
for ((i=0; i<5; i++)); do
output+=( red "${actual:$i:1}" )
done
printcharbox "${output[@]}"
exit 1
I've made a list of the 3000 most frequently used 5-letter words in the English language. It is based on the New General Service List.
https://raw.githubusercontent.com/MikeLippold/word-lists/main/ngsl-5-letter-words.txt
It looks like other people have made similar changes, but here's my version of this with Wordle's actual guess/answer lists and with an emoji output at the end like the real thing: https://gist.github.com/mariecrane/1c9ad6fe5220d452176c97f32fcf7155
@elliottcarlson What is the difference between the wordle word list and the 'allowed list? I thought there were the same.
@lenihan What is powerShell? What is the shebang for that?
@tkaravou Assuming there is something magincal about 50 line limit, is there any restriction that this has to conform to some defined pretty print standard?
On trivial clauses like this:
if [[ $1 == "unlimit" ]]; then
max_guess=999999
fi
to:
if [[ $1 == "unlimit" ]]; then max_guess=999999; fi
that bad? :)
@CqN No the original post that linked to this repo was the author saying that he wrote it in under 50 lines.
@CqN: If you check my first post, you can see that that clause can be code golfed at least a step further:
[[ $1 == "unlimit" ]] && max_guess=999999
At one point I had the whole thing code golfed down to around 26 lines... but honestly, extreme code golfing is for people who like perl
lol
40 lines, and doesn't use any memory at all compared to implementations above. Also much easier to follow along IMO. Assumes suitable dictionary exists. So who's going to be the first to write it in 1 line of obfuscated Perl? This game is just a word version of Master Mind(tm)
#!/bin/bash -e
shopt -u nocaseglob nocasematch
: ${LETTERS:=5}; : ${ROUNDS:=6}; : ${DICTIONARY:=wordle.dict}
pool=abcdefghijklmnopqrstuvwxyz
hit='\e[30;102m'; match='\e[30;103m'; miss='\e[30;107m'; reset='\e[0m'
words=`wc -l < "$DICTIONARY"`
until (( ${line:-0} != 0 )); do line=$((RANDOM % $words)); done
declare -l guess= secret=$(sed -n "${line}p" "$DICTIONARY")
${DEBUG:+echo "$secret"}
for (( j=1; j <= $ROUNDS; j++ )); do
read -ep "Enter your guess ($LETTERS letters | $j/$ROUNDS): " guess || exit
(( ${#guess} == $LETTERS )) || { echo -e " ERROR\timproper guess"; continue; }
[[ "$guess" == "$secret" ]] && { win=1; break; }
declare -a pad=() matched=()
# mark hits while loading pad
for (( i=0; i < $LETTERS; i++ )); do
c=${guess:$i:1}; k=${secret:$i:1}
[[ "$k" == "$c" ]] && pad[$i]='_' || pad[$i]=$k
done
for (( i=0; i < $LETTERS; i++ )); do
c=${guess:$i:1}
if [[ ${pad[$i]} == '_' ]]; then
color=$hit; matched+=( $c )
elif [[ `printf '%s' "${pad[@]}"` =~ "$c" ]]; then
color=$match; pad=( ${pad[@]/$c/.} ); matched+=( $c )
else
color=$miss
[[ `printf '%s' "${matched[@]}"` =~ "$c" ]] || pool=${pool/$c/ }
fi
echo -en "$color ${c^^} "
done
echo -e "${reset}\t\t${pool^^}\n"
done
[ ${win:-0} -eq 1 ] && echo "Congratulations!" ||
echo "The word was '$secret'. Better luck next time!"
Great stuff! I made a version with variable # of letters and multiple languages at https://github.com/pforret/shwordle
@rawiriblundell
Thank you very much for pointing this out. I had seen your earlier post, and had made a mental note to study that later. I like newer practices and cleaner code. But I had not realized you had shrunk it down to 26 at that time.
BTW, if we do not care much about code readability, I think the code can be in one line by stringing all together, no? :) I do not think there is any construct that requires a line end. I am not shell expert. So the popular notion of lines of code is rather fluid.
@tb3088
Thanks for posting. I do not think I had seen this particular formatting and syntax of bash before. I like this. Has this particular style got an accepted name such as Posix? You have used the no-op : extensively. I am intrigued and like to study further.
I would call it a 35 line version, really. There are 6 blank lines :)
PS. By experimenting I found out ${a:=3} is really defining a as a constant and setting it! Cannot be reassigned a new value before unsetting it. Great. I am eager to find out the name and the full set of syntax.
@CqN I'm not doing anything exotic. It's how I write all shell code. take a gander at https://github.com/tb3088/shell-environment/blob/master/.functions
Also read up on Bash Parameter Expansion. The original post was written rather poorly (disorganized) and tried to keep using strings when arrays make it so much more straightforward.
I found that my dict/words contains accented words such as "éCLAT, éPéES, éTUDE". \w
matches accented e, but [a-z]
would not.
@tb3088
Mathew, thanks for the feedback.
Now how would you perform your bash magic on this line
read -ep "Enter your guess ($LETTERS letters | $j/$ROUNDS): " guess || exit
so, I will see the guess I type in shown in capital?
Here's a variant done in (Edit:) under 20 lines of bash: https://gist.github.com/aaronNGi/04a209b1cd17f1fa03af7a87eb14cc42
Cleaned up a few things. Biggest changes:
read(1)
and to save ourselves an if
indentation on the rest of the logic with a while
.#!/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
--- 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....
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
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?
I wrote a SAS version and placed it here sascommunities/wordle-sas. Uses the word lists from cfreshman (thanks) and arrays to check guesses.
I learned about this from an Oreilly blog post. Good job!
Code golfed a bit with a few older techniques cleaned up, take from it whatever you will :)
It's obviously not ideal just blindly downloading a dict off the internet like that, but that's the trade-off you make for staying under 50 lines.