4
\$\begingroup\$

I'm still a beginner in python and made this simple program to practice OOP, I've ended up using the self keyword a lot and I'm not sure if this is normal or if I just misunderstood something.

Any criticism on the program itself or good practices in python are also greatly appreciated.

(Text File 'Words' is just 3000+ English words listed)

Edit: Instance variable cont was only used in 1 function so it was switched to a local variable inside function play

Hangman.py

import random
from phases import hang_phases


class HangMan:
    def __init__(self, random_word, letters):
        self.word = random_word
        self.letters = letters
        self.excluded_letters = []
        self.included_letters = []
        self.original_word = random_word
        self.chances = 9

    def remove_letter(self, guessed_letter):
        self.word = self.word.replace(guessed_letter, "")
        self.included_letters.append(guessed_letter)

    def guess_letter(self):
        print(hang_phases[self.chances])
        guessed_letter = input("Guess A Letter: ").upper()

        if guessed_letter in self.excluded_letters:
            print("You Already Guessed This Letter")
        elif guessed_letter in self.word:
            number_of_letter = self.word.count(guessed_letter)
            if number_of_letter == 1:
                print(f"There is {number_of_letter} {guessed_letter}")
            else:
                print(f"There are {number_of_letter} {guessed_letter}'s")
            self.remove_letter(guessed_letter)
        else:
            print(f"There are no {guessed_letter}'s")
            self.excluded_letters.append(guessed_letter)
            self.chances -= 1

    def play(self):
        cont = True
        while cont:
            if self.word == "":
                print(f"Congrats! The Word Was {self.original_word}")
                cont = False
            else:
                try:
                    if self.chances == 0:
                        print("The Man Has Been Hung! Better Luck Next Time :/")
                        print(f"The Word Was {self.original_word}")
                        print(hang_phases[0])
                        cont = False
                    else:
                        print()
                        print("What Would You Like To Do?:\n*Please Enter The Integer Value Corresponding To The Action")
                        print("1. Guess A Letter\n"
                              "2. Check Included Guessed Letters\n"
                              "3. Check Excluded Guessed Letters\n"
                              "4. Get Length Of The Word\n"
                              "5. Give Up\n")

                        action = int(input("> "))
                        int(action)
                        if action == 1:
                            self.guess_letter()
                        elif action == 2:
                            if len(self.included_letters) == 0:
                                print("You Have Not Guessed Any Letters That Are In The Word")
                            else:
                                print(self.included_letters)
                        elif action == 3:
                            if len(self.excluded_letters) == 0:
                                print("You Have Not Guessed Any Letters That Are Not In The Word")
                            else:
                                print(self.excluded_letters)
                        elif action == 4:
                            print(len(self.original_word))
                        elif action == 5:
                            print(f"The Word Is {self.original_word}")
                            cont = False
                        else:
                            print("Pleas Enter A Value Between 1 & 5")
                except ValueError:
                    print("Please Enter A Valid Integer")


def main():
    play = True
    while play:
        word_list = open('Words').read().split("\n")
        word = random.choice(word_list).upper()

        hm = HangMan(word, len(word))
        hm.play()
        print("Play Again?")
        play_again = input("Y/n > ").upper()
        if play_again == "Y":
            print("Setting Up...")
        elif play_again == "N":
            print("Goodbye")
            play = False


if __name__ == "__main__":
    main()

phases.py

hang_phases = [
    '''
      ______    rip lol.
     |     |
     |     O
     |    \|/
     |     |
     |   _/ \_
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|/
     |     |
     |   _/ \\
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|/
     |     |
     |   _/ 
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|/
     |     |
     |    / 
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|/
     |     |
     |     
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|/
     |     
     |     
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |    \|
     |     
     |     
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |     |
     |     
     |     
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     O
     |     
     |     
     |     
    _|_
    '''
    ,
    '''
      ______
     |     |
     |     
     |     
     |     
     |     
    _|_
    '''
]
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Simpler UI. A lot of your code is devoted to managing the user-interface, which is organized around five numbered choices (guess letter, see successful guesses, see failed guesses, see length of word, or give up). As a user, I find that process very tedious: type 1, press enter, type a letter, press enter, press 1, type letter, press enter, type 4, see length of word, etc etc etc. All of that hassle is unnecessary because you could easily display all of the relevant information all of the time. Furthermore, that's what most Hangman implementations do. The other thing that most Hangman games do is tell you where the successful guesses reside within the entire word. Here's a mock-up showing everything the user needs to know, followed by a prompt waiting for the next guess. And if the user just presses enter, you can interpret that as "give up", which would eliminate the need for a numbered UI. That relatively simple adjustment to your plan would substantially reduce the amount of code needed while also making life better for users:

  ______
 |     |
 |     O
 |    \|/
 |     |
 |     
_|_

Word         T _ D I O U _

Missses      A C F R Z

>

Strings are sequences with stealth members. Python strings are great because they are built on top of sequences, which means you can easily iterate over them character by character. However, strings are not merely character sequences, and naive membership queries can sometimes go awry. In your case, if a user guessing a letter just presses enter rather than typing a letter, your program incorrectly reports that the "letter" (in this case the empty string) appears in the word N + 1 times, where N is the length of the word. And for strings, that is technically correct: the empty string can be found before every letter and at the end of the string. That makes sense from a computer-sciency point of view of strings, but it's not intuitive if we view strings purely as a sequence of characters.

Don't force the caller to do routine work. A user of your HangMan class is required to supply both a word and the number of letters in that word. A friendlier hangman would do that computation for the victim, free of charge.

Boundaries: consider the purpose of the HangMan class. It's good to find projects to practice OOP. One part of that learning process it developing solid instincts about boundaries: what belongs in the class and what does not. These can be tricky questions and software engineers have a variety of opinions about how to draw such lines. If it were me, I would probably treat HangMan as nothing more that a dataclass to keep basic facts about game state (the word, prior guesses) and a variety of derived or computable attributes (prior hits, prior misses, number of remaining guesses). In other words, the class would not collect user input or orchestrate the playing of the game. Such matters would be handled elsewhere, maybe in a different class, maybe in a static method of the class, or in an ordinary function (I would probably opt for the latter). Why that division? Because most of the algorithmic aspects of the code are questions about data: letters in the word, prior guesses, remaining guesses before death. I want algorithmic logic in data-oriented code that can be easily tested and debugged. I don't want such code to be burdened by the annoyances of user-interface. Meanwhile, I want the UI portions of the code to be very simple from an algorithmic/logical point of view so that I don't need to spend too much effort with testing and debugging. I want the inevitably annoying UI-oriented code to be as free of logical complexity as possible: just procedural step-by-step stuff whenever possible. Gary Bernhardt's talk on the subject of class boundaries is a classic in this line of thought.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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