Skip to main content
2 of 2
unnecessary to specify previous language knowledge in title

Console-based hangman in Python

I am a Java + Angular developer. My new job is as a Senior Python developer ... but I've never written a Python program in my life. In order to teach myself the language, I've started writing some simple projects.

This is a console-based Hangman game in Python 3.7. The game picks a 'secret word' from a list, and then enters a game loop prompting the user to guess letters. After 6 incorrect guesses, the game is over and the user loses. Otherwise if the user guesses all the letters, the user wins. After the game finishes, the user is prompted to start a new game, which reruns the method with a new secret word.

The word list and the hangman drawing I have omitted because I cribbed them from this gist on GitHub as a direct copy-paste (renaming the hangman array to HANGMAN_STAGES and the word list to WORDS). I can include it in the post if need be, but it seems like an extra 60 lines which really aren't needed.

import random
import sys
from typing import Tuple

# Omitted declarations: https://gist.github.com/chrishorton/8510732aa9a80a03c829b09f12e20d9c
# HANGMAN_STAGES = [...]
# WORDS = ...

def run_game() -> None:
    """The main game loop. Will prompt the user if they would like to start a new game at the end."""
    print("WELCOME TO HANGMAN.")

    secret_word = pick_secret_word()
    guessed_letters = []
    incorrect_guesses = 0
    won = False
    round = 1

    while won == False and incorrect_guesses < 6:
        print('\n\nROUND ' + str(round))
        incorrect_guesses, won = process_turn(incorrect_guesses, secret_word, guessed_letters)
        round += 1

    print("\n\n")

    if won == False:
        print("GAME OVER! You lost.")
        draw_hangman(6)
    else:
        print("Congratulations! You won!", end=" ")

    print("The secret word was: " + secret_word)

    if play_again():
        run_game()


def pick_secret_word() -> str:
    """
    Chooses a new secret word from the list of available secret words. The word is chosen psuedo-randomly.
    :return: the new secret word
    """
    index = random.randint(0, len(WORDS))
    return WORDS[index].upper()


def process_turn(incorrect_guess_count: int, secret_word: str, guessed_letters: list) -> Tuple[int, bool]:
    """
    Processes a user's turn. First draws the current state of the game: current hangman, partially-guessed word, and
    list of previously guessed letters. Then prompts the user for their next guess, evaluates that guess to see if it
    was correct, and then updates the game state.

    :param incorrect_guess_count: the number of previous incorrect guesses
    :param secret_word: the secret word
    :param guessed_letters: the list of previously guessed letters
    :return: (updated number of inccorect guesses, True/False indication of whether the user has won)
    """
    draw_hangman(incorrect_guess_count)
    draw_secret_word(secret_word, guessed_letters)
    print_guessed_letters(guessed_letters)
    next_letter = prompt_for_guess(guessed_letters)
    return apply_guess(next_letter, secret_word, incorrect_guess_count, guessed_letters)


def print_guessed_letters(guessed_letters: list) -> None:
    """
    Sorts the list of previously-guessed letters and prints it to screen.

    :param guessed_letters: the list of previously guessed letters
    :return: Nothing
    """
    guessed_letters.sort()
    print("Guesses: " + str(guessed_letters))


def apply_guess(next_letter: str, secret_word: str, incorrect_guess_count: int, guessed_letters: list) -> Tuple[int, bool]:
    """
    Checks the validity of the user's guess. If the guess was incorrect, increments the number of incorrect guesses by
    1. If the user has guessed all of the letters in the secret word, return an indication that the user has won the
    game.

    :param next_letter: the user's guess
    :param secret_word: the secret word
    :param incorrect_guess_count: the number of previously incorrect guesses
    :param guessed_letters: the list of previously guessed letters
    :return: (the updated number of incorrected guesses, True/False indicating if the user has won the game)
    """
    guessed_letters.append(next_letter)
    correct, letters_remaining = check_guess_against_secret(next_letter, secret_word, guessed_letters)

    if correct == False:
        incorrect_guess_count += 1

    if letters_remaining == 0:
        return incorrect_guess_count, True

    return incorrect_guess_count, False


def check_guess_against_secret(next_letter: str, secret_word: str, guessed_letters: list) -> Tuple[bool, int]:
    """
    Determines if the user has guessed correctly. Also evaluates the secret word to determine if there are more letters
    left for the user to guess.

    :param next_letter: the user's guessed letter
    :param secret_word: the secret word
    :param guessed_letters: the list of previously guessed letters
    :return: (True/False indicating if the guess was correct, 0 if no letters left and positive integer otherwise)
    """
    correct = next_letter in secret_word

    letters_remaining = 0
    for letter in secret_word:
        # Known issue: if a letter is present in the secret multiple times, and is not guessed,
        # letters_remaining incremented by more than one.
        if letter not in guessed_letters:
            letters_remaining += 1

    return correct, letters_remaining


def prompt_for_guess(guessed_letters: list) -> str:
    """
    Prompts the user for their next guess. Rejects guesses that are more than a single letter, and guesses which were
    already made previously. Returns the (validated) guess.

    :param guessed_letters: the list of previously guessed letters
    :return: the user's next guess
    """
    guess = input("Your guess? ").strip().upper()
    if len(guess) > 1:
        print("Sorry, you can only guess one letter at a time.")
        return prompt_for_guess(guessed_letters)
    elif guess in guessed_letters:
        print("Sorry, you already guessed that letter.")
        return prompt_for_guess(guessed_letters)
    return guess


def draw_hangman(number_of_incorrect_guesses: int) -> None:
    """
    Draws the appropriate hangman stage, given the number of incorrect guesses. 0 or fewer will draw the empty scaffold.
    6 or more will draw the fully hanged man.

    :param number_of_incorrect_guesses: the number of incorrect guesses the player has made in the current game
    :return: Nothing
    """
    if (number_of_guesses < 0):
        number_of_guesses = 0
    if (number_of_guesses > 6):
        number_of_guesses = 6
    print(HANGMAN_STAGES[number_of_guesses])


def draw_secret_word(secret_word: str, guessed_letters: list) -> None:
    """
    Prints the secret word, with underscores representing unknown letters and with any correctly-guessed leters printed
    in the appropriate location within the word.

    :param secret_word: The secret word
    :param guessed_letters: All previous guesses
    :return: Nothing
    """
    for letter in secret_word:
        to_print = letter if letter in guessed_letters else '_'
        print(to_print, end=' ')
    print("\n")


def play_again() -> bool:
    """
    Prompts the user if they would like to play again. If the user enters something other than Y/y/N/n, it will continue
    prompting until the use enters a valid value. If the user indicates Y or y, this method returns True; N or n will
    return False

    :return: True if the user would like to start a new game; False otherwise
    """
    choice = ''
    while choice != "Y" and choice != "N":
        choice = input("Play again? (Y/N)").strip().upper()
    return choice == "Y"

run_game()

Things I know are an issue:

  • No unit tests. I'm still trying to figure out how to test input() and print() statements
  • I attempted pydoc docstring comments on all of the methods, but couldn't figure out how to properly document a returned tuple.
  • I'm a bit inconsistent in my string quotation mark usage because of my dual background with Java (double quotes) and Angular/Typescript (single quotes)
  • The check_guess_against_secret method doesn't accurately count how many unique letters remain; duplicate unguessed letters in the secret are counted twice. But since we only evaluate whether that value is 0, it could be swapped over to a boolean flag on refactor

In addition to a general review, I would very much appreciate it if folks could point out places where I'm doing things in a more Java (or Typescript!) way rather than a Python way.