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 |
@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!
@elliottcarlson What is the difference between the wordle word list and the 'allowed list? I thought there were the same.