3
\$\begingroup\$

I built upon a previous beginners' class' hangman game to do graphics with Turtle. It should work with any .txt list of words in whatever location you specify at the top of the code (in my case on Windows, E:/My Documents/Programming/Projects/Hangman/words.txt). Alternatively you can uncomment the commented line containing #COMMENT AFTER TESTING and comment the line containing #UNCOMMENT AFTER TESTING at the start of Main to use test as the word to be guessed.

How did I do? What can/should I improve upon?

EDIT to add: I neglected to mention which code was borrowed from the lesson. I've added a comment where the code becomes mine.

import random

WORDLIST_FILENAME = "E:/My Documents/Programming/Projects/Hangman/words.txt"

def loadWords():
    """
    Returns a list of valid words. Words are strings of lowercase letters.
    """
    print("Loading word list from file...")
    # inFile: file
    inFile = open(WORDLIST_FILENAME, 'r')
    # line: string
    line = inFile.readline()
    # wordlist: list of strings
    wordlist = line.split()
    print("  ", len(wordlist), "words loaded.")
    return wordlist

def chooseWord(wordlist):
    """
    wordlist (list): list of words (strings)

    Returns a word from wordlist at random
    """
    return random.choice(wordlist)

end of lecturer's code

def isWordGuessed(secretWord, lettersGuessed):
    '''
    secretWord: string, the word the user is guessing
    lettersGuessed: list, what letters have been guessed so far
    returns: boolean, True if all the letters of secretWord are in lettersGuessed;
      False otherwise
    '''
    secretWordList = list(secretWord)
    return all(elem in lettersGuessed for elem in secretWordList)


def getGuessedWord(secretWord, lettersGuessed):
    '''
    secretWord: string, the word the user is guessing
    lettersGuessed: list, what letters have been guessed so far
    returns: string, comprised of letters and underscores that represents
    what letters in secretWord have been guessed so far.
    '''
    secretWordList = list(secretWord)
    lettersGuessedList = list(set(lettersGuessed))
    for letter in secretWordList:
        if letter not in lettersGuessedList:
            secretWordList[secretWordList.index(letter)] = '_ '
    return ''.join(map(str, secretWordList))



def getAvailableLetters(lettersGuessed):
    '''
    lettersGuessed: list, what letters have been guessed so far
    returns: string, comprised of letters that represents what letters have not
    yet been guessed.
    '''
    import string
    alphaList = list(string.ascii_lowercase)
    for char in lettersGuessed:
        if char in alphaList:
            alphaList.remove(char)
    return ''.join(map(str, alphaList)) 
    
#-----Setup-----
import turtle
wordlist = loadWords()
version = "v0.1"
gameTitle = "Basic Hangman "+version
window = turtle.Screen()
window.title(gameTitle)
window.bgcolor("black")
window.setup()
window.tracer(0)
tMessage = "Welcome to "+gameTitle+"!"
bMessage = "Input 'new' for a new game or 'quit' to quit."
font = ("Share Tech Mono", 24, "normal")

#TopText
tText = turtle.Turtle()
tText.speed(0)
tText.color("darkgreen")
tText.penup()
tText.hideturtle()
tText.goto(0,-260)
tText.write(tMessage, align="center", font=(font))

#BottomText
bText = turtle.Turtle()
bText.speed(0)
bText.color("darkgreen")
bText.penup()
bText.hideturtle()
bText.goto(0,-290)
bText.write(bMessage, align="center", font=(font))

#Creating the noose
noose = turtle.Turtle()
noose.width(10)
noose.hideturtle()
noose.color("darkgreen")
noose.penup()

#Drawing the noose
def drawNoose():
    noose.penup()
    noose.goto(-200,-200)
    noose.pendown()
    noose.goto(200,-200)
    noose.goto(150,-200)
    noose.goto(100,-150)
    noose.goto(50,-200)
    noose.penup()
    noose.goto(100,-200)
    noose.pendown()
    noose.goto(100,200)
    noose.goto(0,200)
    noose.penup()
    window.update()

#Creating the man
man = turtle.Turtle()
man.width(10)
man.hideturtle()
man.color("darkgreen")
man.penup()

#Drawing the man
def newLimb(limbs):
    if limbs == 6: #Draw rope
        man.goto(0,200)
        man.pendown()
        man.goto(0,150)
        man.penup()

    elif limbs == 5: #Draw head
        man.goto(0,100)
        man.pendown()
        man.circle(25)
        man.penup()        

    elif limbs == 4: #Draw body
        man.goto(0,100)
        man.pendown()
        man.goto(0,25)
        man.penup()
    
    elif limbs == 3: #Draw left leg
        man.goto(0,25)
        man.pendown()
        man.goto(-30,-40)
        man.penup()
    
    elif limbs == 2: #Draw right leg
        man.goto(0,25)
        man.pendown()
        man.goto(30,-40)
        man.penup()

    elif limbs == 1: #Draw left arm
        man.goto(0,75)
        man.pendown()
        man.goto(-30,15)
        man.penup()

    elif limbs == 0: 
        #Draw right arm
        man.goto(0,75)
        man.pendown()
        man.goto(30,15)
        man.penup()

guessedLetterRaw = turtle.textinput("Input", ' ')
guessedLetter = guessedLetterRaw.lower()

# --- Main ---
while guessedLetter == 'new':
    drawNoose()
    man.clear()
    lettersGuessed = []
    #secretWord = "test"                                                                #COMMENT AFTER TESTING
    secretWord = chooseWord(wordlist).lower()                                          #UNCOMMENT AFTER TESTING
    guessesLeft = 7
    tText.clear()
    bText.clear()
    tMessage = "Welcome to "+gameTitle+"!"

    while guessesLeft > 0:
        # Window Updating
        bMessage = 'Remaining letters: ' + getAvailableLetters(lettersGuessed)
        window.update()
        bText.clear()
        bText.write(bMessage, align="center", font=(font))
        tText.clear()
        tText.write(tMessage, align="center", font=(font))
        
        # Guessing
        guessedLetter = turtle.textinput("Input", 'Please guess a letter:')

        if guessedLetter in lettersGuessed:
            tMessage = "Oops! You've already guessed that letter: " + getGuessedWord(secretWord, lettersGuessed) 
        else:
            lettersGuessed.append(guessedLetter)

            if guessedLetter in secretWord:
                if isWordGuessed(secretWord, lettersGuessed):
                    noose.clear()
                    tText.clear()
                    man.clear()
                    for n in range(0, 6):
                        newLimb(n)
                    tMessage = 'Congratulations, you won!'
                    tText.write(tMessage, align="center", font=(font))
                    guessedLetter = turtle.textinput("Input", "Input 'new' for a new game or 'quit' to quit.")
                    break
                else:
                    tMessage = 'Good guess: ' + getGuessedWord(secretWord, lettersGuessed)
            else:
                guessesLeft -= 1
                newLimb(guessesLeft)
                if guessesLeft < 1:
                    tText.clear()      
                    tMessage = 'Sorry, you ran out of guesses. The word was ' + secretWord + '.'
                    tText.write(tMessage, align="center", font=(font))
                    guessedLetter = turtle.textinput("Input", "Input 'new' for a new game or 'quit' to quit.")
                else:
                    tMessage = 'Oops! That letter is not in my word: ' + getGuessedWord(secretWord, lettersGuessed)
\$\endgroup\$
3
  • \$\begingroup\$ Whereas your lecturer has issues in their code that I've spoken about in my answer, there's a policy about code ownership on Code Review - it has to be yours. Overall I'd say that you have enough of your own code here that this is not off-topic; and hopefully you can derive learning value from the issues in the lecturer's code as well as your own. \$\endgroup\$
    – Reinderien
    Commented Dec 21, 2020 at 20:05
  • \$\begingroup\$ All of that is to say - your question is good as-is, but for sure next time it would be good to delineate authorship from the get-go. \$\endgroup\$
    – Reinderien
    Commented Dec 21, 2020 at 20:11
  • \$\begingroup\$ Yeah I only included it in order for someone to be able to easily run the entire code on their own without having to put effort into writing loadWords() and chooseWord(). I didn't realize until your corrections that it was not only not helpful but accidentally dishonest; not that anyone's going to be extending a job offer because I did such a great job importing a list from a text document and choosing a random item in a list. My main hurdles now are figuring out how best - and why - to encompass the global code in functions. I will reply to your answer in an itemized fashion momentarily. \$\endgroup\$ Commented Dec 21, 2020 at 21:22

1 Answer 1

3
\$\begingroup\$

Relative file paths

WORDLIST_FILENAME = "E:/My Documents/Programming/Projects/Hangman/words.txt"

should be modified so that it's relative. The easy thing to do is make it relative to the current directory and assume that the current directory is that of the project, but that's a little fragile. The more reliable thing to do is make this relative to the location of the current Python file, for which you can use __file__. Either way, using pathlib will make this relative-operation easy.

Context management for file handles

inFile = open(WORDLIST_FILENAME, 'r')

first of all, this does not have a corresponding close(), which is important. Beyond that, though, a close as implied by a with open() is safer than an explicit close, so do the latter.

File format

It seems that your file assumes a word list all on one line, separated by spaces. The more usual, marginally easier, and potentially (?) more performant thing to do is separate by line breaks. Python has more built-in machinery to do this - you can simply treat the file handle itself as an iterable over the file's lines, and would not need a call to split.

Formatting

print("  ", len(wordlist), "words loaded.")

is probably better-represented as

print(f'{len(words)} words loaded.')

Also note that words is a slightly simpler name for that variable, and the list part can be conveyed using a type hint. The type hint needn't even be list if you yield from the file handle, i.e.

def load_words() -> Iterable[str]:
    with open(WORD_LIST_FILENAME) as f:
        yield from f

Unnecessary casts

secretWordList = list(secretWord)
return all(elem in lettersGuessed for elem in secretWordList)

does not need the first line, since it's assumed that secretWord (which should be secret_word) is iterable. Also, this is better-represented by a set:

def is_word_guessed(secret_word: str, letters_guessed: Set[str]) -> bool:
    return set(secret_word) <= letters_guessed

getGuessedWord

Consider representing this as a join on a generator rather than an intermediate list:

def get_guessed_word(secret_word: str, letters_guessed: Set[str]) -> str:
    return ''.join(
        c if c in letters_guessed else '_'
        for c in secret_word
    ) 

Avoid local imports

Move this import:

def getAvailableLetters(lettersGuessed):
    import string

to the top of the file.

Set subtraction

Consider rewriting getAvailableLetters to use set subtraction:

def get_available_letters(letters_guessed: Set[str]) -> str:
    return ''.join(set(ascii_lowercase) - letters_guessed)

Global code

You have a bunch of it, starting at

#-----Setup-----

Move this into functions.

\$\endgroup\$
4
  • \$\begingroup\$ The file path was a compatibility issue when writing the original console-based game that Visual Studio Code was unable to fetch the words.txt file in the same folder. Can't remember exactly how it was pulled before but I do remember they had a troubleshooting step-by-step and manually entering the file path like that was the final (and luckily functional) step. I'll have to look in to your suggestions, including how to ensure it'll run on the target machine. I've heard of the perils of not close()-ing a file - I foolishly trusted the lecturer wrote perfect code and overlooked it. \$\endgroup\$ Commented Dec 21, 2020 at 21:29
  • \$\begingroup\$ Something I've encountered a lot in my recent outside-of-coursework learning is the use of <= and ->, which you seem to use quite frequently. It was never mentioned in my intro to Python class and I'll have to figure out how and when to use that. You mention making getGuessedWord a generator - I thought those were functions that use 'yield'? Would including that global setup code in a setup() function to be run after defining it be preferable? \$\endgroup\$ Commented Dec 21, 2020 at 21:37
  • \$\begingroup\$ making getGuessedWord a generator - not quite. The function uses a generator internally but is not itself a generator. The generator is everything within the join(). \$\endgroup\$
    – Reinderien
    Commented Dec 22, 2020 at 21:49
  • \$\begingroup\$ Would including that global setup code in a setup() function to be run after defining it be preferable? - a new setup and main at the very least, if not more functions for granularity. \$\endgroup\$
    – Reinderien
    Commented Dec 22, 2020 at 21:50

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.