0
\$\begingroup\$

Recently I was given homework to make a hangman game, where I wrote the below.
How can I refactor this to use classes?

import secrets


def guess_the_word():
    tries_left = 9
    output = ["_"] * len(secret_word)

    print(str(' '.join([letter for letter in output])).strip())

    while tries_left >= 0 and not secret_word == str(''.join([letter for letter in output])):
        # print tries left
        print("\nYou have {} tries left".format(tries_left))
        # display the _ which is length of the secret_word and fill in _ as and when the user 
        # types correct word

        guessed_letter = input("Guess a letter from secret word:")

        if guessed_letter in secret_word:
            print("You guessed correct")
            for i, x in enumerate(secret_word):
                if x is guessed_letter:
                    output[i] = guessed_letter
        else:
            tries_left -= 1
            print("You guessed incorrect")

        print(str(' '.join([letter for letter in output])).strip())


print("\nWelcome to Hangman Game!\n")
# open words.txt file to read words
with open("words.txt") as words_file:
    words_list = words_file.read().split()
# choose a random word from words_list
secret_word = secrets.choice(words_list)
guess_the_word()
\$\endgroup\$
2
\$\begingroup\$

How can I refactor this to use classes?

You don't. There is absolutely no need for that in this program as you don't share a common state between multiple functions. On the same subject, see also this PyCon talk.

Instead, we should focus on clearing the obvious mistakes made in this function:

  1. Be sure to pass information to functions using parameters: global variable can be fine for constants, not so much for information to work on; besides if I import your code in an interactive session, I am greeted with a game but any other attempt to call guess_the_word() will reuse the same word to guess.
  2. This also lead to how you call your code: running the program on the command line will play a game, but importing it will play it too; which may be undesirable. To avoid this side effect, get yourself familiar with the if __name__ == '__main__': construct.
  3. secrets is a module designed for cryptographically secure sources of random numbers. You don't need such strong randomness here so you can avoid the associated overheads using the simpler random module.
  4. You can join directly on lists: no need to write a list-comprehension which will just copy the underlying list before feeding it to join. In the same vein, striping whitespace and converting to str is superfluous here as the result of join already fulfill the two conditions.
  5. Checking if a letter is in a string is an \$\mathcal{O}(n)\$ operation, so you can skip that and directly perform the iteration over each letter it won't cost more. You can use a flag to know if you have found a matching letter.
  6. A message at the end of the while loop to conclude would be a great addition to the user experience.

All in all, this could yield the following code:

import random


def guess_the_word(secret_word):
    tries_left = 9
    output = ["_"] * len(secret_word)

    while tries_left >= 0 and secret_word != ''.join(output):
        print(' '.join(output))
        print("\nYou have", tries_left, "tries left")

        guessed_letter = input("Guess a letter from secret word: ")

        found = False
        for i, letter in enumerate(secret_word):
            if guessed_letter == letter:
                output[i] = guessed_letter
                found = True

        if found:
            print("You guessed correct")
        else:
            tries_left -= 1
            print("You guessed incorrect")

    if tries_left < 0:
        print("\nNo more tries, you lose")
    else:
        print("\nCongratz! You found the secret word:", secret_word)


def main(filename="words.txt"):
    print("\nWelcome to Hangman Game!\n")

    with open(filename) as words_file:
        words = words_file.read().split()

    guess_the_word(random.choice(words))


if __name__ == '__main__':
    main()

An other nice addition to have is the ability to repeat the games automatically. You could change your main part to:

def main(filename="words.txt", repeat=sys.maxsize):
    print("\nWelcome to Hangman Game!\n")

    with open(filename) as words_file:
        words = words_file.read().split()

    for _ in range(repeat):
        guess_the_word(random.choice(words))


if __name__ == '__main__':
    main(repeat=1)

to keep the current behaviour but allows importing the file to run main() the desired amount of time without having to reload the file each time.

\$\endgroup\$
0
\$\begingroup\$

Comments about the code in no particular order:

  • Dont use global variables
  • [letter for letter in output] same as output
  • strip() why do you use this function alot? it just removes white space and in this code it it dosent do anything.
  • join() already returns a string, no need to cast it.

Bugs?

  • you can ask for the same letter without getting a penalty.
  • just "enter" counts as a "correct" guess.
  • you can't guess the whole word.

class Hangman:
    def __init__(self, word, num_guesses = 9):
        # first take care of init arguments
        self._num_guesses = num_guesses
        self._word = word.lower()
        # after that we define other variables we will be needing
        self._guessed_word = ["_"]*len(word)

    def _guess(self, letter):
        if letter.lower() in self._word:
            for i, c in enumerate(self._word):
                if c is letter:
                    self._guessed_word[i] = letter
            return True
        return False

    def play(self):
        print("\nWelcome to Hangman Game!\n")
        while self._num_guesses > 0:
            letter = input("Guess a letter from the secret word:")
            if self._guess(letter):
                print("You guessed correct")
                if self._word == "".join(self._guessed_word):
                    break
            else:
                self._num_guesses -= 1
                print("You guessed incorrect")
            print(" ".join(self._guessed_word))


if __name__ == "__main__":
    h = Hangman("codereview")
    h.play()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.