Skip to content

krystianczajkowski/hangman

Repository files navigation

UNIVERSAL HANGMAN

This is my final project for CS50P. - Also this is my first README


Video Demo: Done


Description:

What's Universal Hangman?

It's normal hangman but you can choose any language you want (that is aslo coincidentally supported by Google Translate - more on that later) and maybe a few other features that end user won't notice.

How does it work?

It's made in Python using blessed and deep_translator It doesn't divagate too much from standard hangman everyone had a chance to play or at least spectate at some point in their lives.
You will find detailed description bellow

Structure:

  • Every function (except tests) in the project is located here.

    Details

    Description of the contents

    Miscellaneous

    First iteration of the hangman just printed text on the screen

    os.system('cls') # clearing the screen
    for i in range(5): # that's how space was created
        print()
    print(HANGMAN[tries])

    This was ugly, so the next step was to find a library that would solve this problem

    Curses library was the obvious first choice but after a bit of research Blessed was the final answer

    from Blessed import Terminal
    
    #Initializes the terminal
    term = Terminal()
    print(term.clear+term.home) # clear the screen and place the cursor at the top-left corner of the screen
    For more information check the PyPi

    No internet

    # in case of no internet connection, this list is used.
    SUPPORTED_LANGUAGES = ['english', 'polish']

    Storing the gallows...

    # HANGMAN is a dictionary for storing the gallows
    # It's made out of int keys and string literals as values
    HANGMAN = {
    7: """
     ____
    |/   |
    |   
    |    
    |    
    |    
    |
    |_____
    """, ...}

    Main

    Main - Besides starting the program not much going on in there.

    def main():
        print(f'{term.clear+term.home}') # make space on the screen
        print(pyfiglet.figlet_format('HANGMAN', font='cybermedium')) # print the title
        # check internet
        if check_internet():
        # begin online play
        play_game(*translate_lang())
        else:
        # start offline if check failed
        print(f'{term.clear}{term.red2}No internet connection!{term.normal}')
        play_game(
            get_word(
                select_language(
                    input(
                        f"Choose your language: supported languages - {SUPPORTED_LANGUAGES} "
                    )
                )# definitions for words are not available in offline mode because of their format and size
           ), 'Definition unavailable due to lack of internet!')

    Checking for internet connection

    📄 Docstring says it all!

    def check_internet() -> bool:
        """Checks for network conectivity"""
        try:
            # try to get a response from google
            requests.get('https://translate.google.com')
            # on success
               return True
            except ConnectionError:
            # on failure
                return False

    Selecting a language

    Does just that

    *Only used if check for internet failed.
    def select_language(lang: str) -> str:
        """Returns language if it's supported otherwise returns english"""
        if lang.lower() in SUPPORTED_LANGUAGES: # manually created list of languages
            return lang.lower()
        else:
            print(
                f"{term.red2}Language: {term.cornflowerblue}{lang}{term.red2} unsupported! Defaulting to English.{term.normal}")
            sleep(2) # let the user read the error message
            return "english"

    Translating words

    Using deep_translator to translate english to every language supported by the translator

    For the purpose of this game I've chosen Google Translate as it seemed unnecessary to complicate things with switching between multiple translators.


    Translate function first asks for input

    language = input('Enter a language: ').lower()

    Then selects a random line from the file and separates word from it's definition

    with open('words_with_definitions.txt', encoding='utf-8') as f:
        data = choice(f.readlines()).lower().split()
        word = data[0]
        definition = ' '.join(data[1:])

    Avoiding translating english to english

    if language == 'en' or language == 'english':
        return word, f'{word.upper()} - {definition}'

    Check if user provided any language

    If language is not supported return english version, otherwise return translated word with definition

    else:
        try:
            if language:
                translated = GoogleTranslator(
                    source='en', target=language).translate(word)
            else:
                print('No language provided, defaulting to english!')
                sleep(1)
                return word, f'{word.upper()} - {definition}'
    
        except LanguageNotSupportedException:
            print(f'Language {language} not supported, swiched to english!')
            sleep(1)
            return word, f'{word.upper()} - {definition}'
    
        return translated, f'{word.upper()} - {definition}'

    Selecting the word if there's no internet

    Gets the words in the same fashion as translate function.

    *Only used if check for internet failed.
    # doesn't have to check for language as it was already done by select_language
    def get_word(language: str) -> str:
    """Grabs a random word from a file"""
    with open(f"{language}.txt", encoding="utf-8") as f:
        return choice(f.readlines()).strip().lower()

    Playing the game

    Now we can play the game

    def play_game(word: str, word_with_definition: str) -> None:
    """Plays the game of hangman""" # documenting what this function does turned out to be harder that expected

    Before any loops, initialize a few variables

    # imagine if all variables were named i, j, k, l, m, n, 🤯
    TITLE = pyfiglet.figlet_format("HANGMAN", font="banner3-D")
    wrong_lttrs, g_word = "", ["_" if letter != " " else letter for letter in word]
    spaces = 0 + sum([1 for i in word if i == " "])
    len_word = len(word) - spaces # number of letters
    num_of_words = len(word.split())
    tries = len(HANGMAN)

    Now time for loops

    As far as I know it's not possible to get SIGWINCH(terminal resize signal) on Windows or the equivalent solution is not practical. [citation needed]
    shown = False
    # loop forever
    while True:
        # this one is checks if the game will fit on the screen
        while term.height < 22 or term.width < 80:
            if not shown:
            # if the size is too small it will print the warninig
                print(
                f"{term.clear+term.home+term.cyan+term.move_xy(term.width//2-10, term.height//2)}Increase your terminal size!{term.red2}"
            , 'NOW!')
            sleep(0.15)
            shown = term.width > 22 and term.width > 80
        shown = False 

    After that is done it's time to draw the game

    # clear the screen and draw the big hangman banner
    print(f"{term.normal+term.clear}{TITLE}")
    # if spaces are present display how many are there
    if spaces:
        print(f"{term.move_xy(20, term.height//2-2)}Lenght of the word: {len_word}\n {term.move_xy(20, term.height//2-3)}Number of spaces: {spaces}",end=" ",)
        
        print(f"{term.move_xy(20, term.height//2+3)}Number of words: {num_of_words}")
    else: # if not print just the lenght
        print(f"{term.move_xy(20, term.height//2+2)}Lenght of the word: {len_word}")
    # if incorrect guesses were made display entered letters and state of the gallows
    if wrong_lttrs:
        print(f'{term.move_xy(20, term.height//2)}Entered letters: {" ".join(wrong_lttrs)}')
        print(f"{term.move_xy(0, term.height//2)}{HANGMAN[tries]}")

    Getting user input, displaying correctly guessed letters and handling succes

    print(f"{term.move_xy(20, term.height//2+7)}{' '.join(g_word).upper()}")
    guess = input(f"{term.move_xy(20, term.height//2+8)}Enter a letter: {term.green}")
    
    # validating user input
    if guess and guess.lower()[0] not in word:
            tries -= 1
            wrong_lttrs += guess.upper()[0]
        
    else:
        # replace underscores in g_word with correctly guessed letters
        g_word = find_lttr(word, g_word, guess)
        # if all letters match the game is won
        if "".join(g_word) == word:
            print(f"{term.move_xy(20, term.height//2+6)}{' '.join(g_word).upper()}")
            print(f"{term.green+term.move_right(20)}Congratulations The word was {word.upper()}{term.normal}")
            # prompt to play again/see the definition of the word
            play_again_or_get_definition(word_with_definition)

    Game over state

    if tries == 0:
        # clear the screen, draw the title in red
        print(f'{term.clear}{term.normal}{term.red2_on_black}{TITLE}')
        # draw the hangman on the gallows
        print(f"{term.red2_on_black}{term.move_xy(0, term.height//2)}{(HANGMAN[tries])}","RIP")
        # let the player know what was the word
        print(f"{term.move_xy(20, term.height//2+4)}Word: {word}")
        sleep(3) # give user time to read the word
        # prompt to play again/see the definition of the word
        play_again_or_get_definition(word_with_definition) 

    Checking if user input matches the selected word

    This is easily done. Since words will never get long enough for us to notice any performance drops, just loop through them each time.

    # if user entered the whole word return the word
    if lttr.lower() == word:
        g_word = word
        return g_word
    # if letter is not a single character, consider only the first one
    elif len(lttr) > 1:
        lttr = lttr[0]
    # replace words where they match
    for i, l in enumerate(word):
        if l == lttr:
            g_word[i] = word[i]
    return g_word

    Playing again

    First display some info

    print(f"{term.skyblue}{term.move_right(20)}Press SPACE to play again or ESC to quit.")
    print(f'{term.move_right(20)}If you want to get the definition press "i".{term.normal}')

    Then prompt the user for input without the need to confirm it(pressing enter)

    val = ""
    shown = False
    with term.cbreak():
        # wait for keypress
        while val.lower() != " ":
            val = term.inkey(timeout=20)
            if not val:
                # after 20 seconds display reminder for the user
                with term.location(0, 8):
                    print(
                        f"{term.red}Press 'ESC' to quit, 'i' to get the definition(EN) or 'SPACE' to play again.{term.normal}"
                    )
            # exit the program on Esc
            elif val.name == "KEY_ESCAPE":
                exit(
                    f"{term.clear}{term.move_xy(0 ,term.height//2)}{term.red_on_white(term.center('BYE!'))}{term.normal}{term.move_xy(0,term.height)}"
                )
            # diplay the definition once per game
            elif val.lower() == "i" and not shown:
                print(
                    f"{term.move_xy(20, term.height//2)+term.lightblue}{definition}{term.move_xy(0,term.height)+term.normal}"
                )
                shown = True

    When user decides to press SPACE

    # clear the screen and call main so that we have come full circle back to the beginning
    if val == " ":
        print(term.clear + term.normal)
        main()
    Thanks for coming to my tedx talk

english/polish.txt

  • these are files that contain words for offline play
  • english.txt contains only 12 words (guess which)
  • polish.txt has only slighty more at around 3 million words, taken from sjp.pl

words_with_definitions.txt

  • around 300 thousand english words with definitions

About

simple hangman game

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages