Hangman
2023-06-26 python hangman random unicodedata rich game funIn order to try out python a bit more, I am trying to write some simple programs. One thing I worked on recently is an implementation of classic Hangman game for guessing words. I did some preparation that I mentioned in previous post on creating dictionary file.
Now there are number a problems I needed to address:
- Load the list of words
- Randomly pick one for the game
- The Czech words contain accents that I don’t want to take into account when comparing
- Visualize current state (show remaining attempts, the already guessed letters, already tried letters and the hangman picture)
- Determine success/fail of the guess
- Check winning/losing conditions and communicate that to the user
Going step by step, here is my approach to each one.
Loading of the list is quite easy, simple function can load the whole file and return it in array. Only complication was to load the file as unicode (utf-8) and strip terminating newlines.
def get_words(input_file):
with open(input_file, encoding='utf-8') as file:
return [line.rstrip() for line in file]
For random picking of one of words, there is handy module random and its random.choice
. In terms of code this is something like
import random
words = get_words('hangman-words.txt')
word_to_guess = random.choice(words)
For comparison, the module unicodedata provides means to encode any unicode string into ascii. Let’s define a function to turn a string into uppercase form without any accents
import unicodedata
def upper_ascii(s):
return unicodedata.normalize('NFKD', s.upper()).encode('ASCII', 'ignore')
print(upper_ascii('žluťoučký')) # b'ZLUTOUCKY'
For simple console-based interface I used rich library and its method console.clear
, console.print
, Panel
, and console.input
. The interface would look like this
╭────── HANGMAN ───────╮
│ Remaining 2 attempts │
│ │
│ ┌┬┬┬┬┬┬┬┐ │
│ ┌┼┼┴┴┴┴┴┴┴┐ │
│ ├┼┘ │ │
│ ├┤ ╭┴╮ │
│ ├┤ ╰┬╯ │
│ ├┤ ┌─┼─┐ │
│ ├┤ │ │ │ │
│ ├┤ │ │
│ ├┤ │
│ ├┤ │
│ ├┤ │
│ ├┤ │
│ ═══╧╧═════════════ │
│ │
│ Word to guess: │
│ - - D - - Č E K │
╰──────────────────────╯
Already tried: A B F G H
Guess letter (? = help):
For game state I defined few variables in main loop:
to_guess
is array of tuples, each having a letter and whether it was guessedremaining_attempts
goes from starting number down to zerotries
is simple array of already tried letters
Manipulation of to_guess
is easy with list comprehensions that I grown to like. For instance
- print already guessed letters and keep others replaced with dash
' '.join([c if visible else '-' for c,visible in to_guess]
- count number of visible/guessed letters
visible = sum(visible for _,visible in to_guess)
- build new
to_guess
based on input inguess
variableto_guess = [(c, visible or upper_ascii(guess) == upper_ascii(c)) for c,visible in to_guess]
Success of the guess can be determined by checking of whether number of visible letters changed during to_guess
array update. Based on this information, we can alter tries
array and remaining_attempts
counter. We win when all items in to_guess
are visible and we lose when remaining_attempts
goes down to zero.
When we put all this together, the result is something like this
import unicodedata
import random
from rich.console import Console
from rich.panel import Panel
def get_words(input_file):
with open(input_file, encoding='utf-8') as file:
return [line.rstrip() for line in file]
def upper_ascii(s):
return unicodedata.normalize('NFKD', s.upper()).encode('ASCII', 'ignore')
def show_entry(console, guess, remaining_attempts):
console.clear()
console.print(Panel(
f'Remaining {str(remaining_attempts)} attempts\n'
+ get_hangman_picture(remaining_attempts) + '\n'
+ 'Word to guess:\n'
+ ' '.join([c if visible else '-' for c,visible in guess]
), expand=False, title='HANGMAN'))
console.print()
def play(console, word):
to_guess = [(c,False) for c in word.upper()]
tries = []
remaining_attempts = 7
while remaining_attempts > 0:
show_entry(console, to_guess, remaining_attempts)
if len(tries) > 0:
console.print('Already tried: ' + ' '.join(tries))
guess = console.input(f'Guess letter (? = help): ')
if guess == '?':
guess = random.choice([c for c, visible in to_guess if visible == False])
visible_before = sum(visible for _,visible in to_guess)
to_guess = [(c, visible or upper_ascii(guess) == upper_ascii(c)) for c,visible in to_guess]
visible_after = sum(visible for _,visible in to_guess)
if visible_after == len(to_guess):
show_entry(console, to_guess, remaining_attempts)
print('You won')
break
if visible_after == visible_before:
tries.append(guess)
remaining_attempts -= 1
else:
show_entry(console, to_guess, remaining_attempts)
word = ' '.join([c for c,_ in to_guess])
print(f'You lost, the word was {word}')
def main():
words = get_words('hangman-words.txt')
console = Console()
while True:
play(console, random.choice(words))
if input("Play Again? (Y/N) ").upper() == "N":
break
def get_hangman_picture(remaining_attempts):
return [
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┴┐
├┼┘ │
├┤ ╭┴╮
├┤ ╰┬╯
├┤ ┌─┼─┐
├┤ │ │ │
├┤ │
├┤ ┌┴┐
├┤ │ │
├┤ ─┘ └─
├┤
═══╧╧═════════════
""",
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┴┐
├┼┘ │
├┤ ╭┴╮
├┤ ╰┬╯
├┤ ┌─┼─┐
├┤ │ │ │
├┤ │
├┤ ┌┘
├┤ │
├┤ ─┘
├┤
═══╧╧═════════════
""",
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┴┐
├┼┘ │
├┤ ╭┴╮
├┤ ╰┬╯
├┤ ┌─┼─┐
├┤ │ │ │
├┤ │
├┤
├┤
├┤
├┤
═══╧╧═════════════
""",
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┴┐
├┼┘ │
├┤ ╭┴╮
├┤ ╰┬╯
├┤ ┌─┘
├┤ │
├┤
├┤
├┤
├┤
├┤
═══╧╧═════════════
""",
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┴┐
├┼┘ │
├┤ ╭┴╮
├┤ ╰─╯
├┤
├┤
├┤
├┤
├┤
├┤
├┤
═══╧╧═════════════
""",
"""
┌┬┬┬┬┬┬┬┐
┌┼┼┴┴┴┴┴┴┘
├┼┘
├┤
├┤
├┤
├┤
├┤
├┤
├┤
├┤
├┤
═══╧╧═════════════
""",
"""
┌┐
├┤
├┤
├┤
├┤
├┤
├┤
├┤
├┤
├┤
├┤
═══╧╧═════════════
""",
"""
══════════════════
""",
][remaining_attempts]
if __name__ == "__main__":
main()
I liked the exercise, it was quite some learning of new concepts. Obviously it would be better to use a little better data structures, but I will need to learn more on how to build classes/objects in python and use it in my code. This will be for next time.