How to Create a Hangman Game using PyGame in Python

Learn how to use PyGame to create a Hangman word guessing game in Python.
  · 15 min read · Updated mar 2023 · Game Development

Before we get started, have you tried our new Python Code Assistant? It's like having an expert coder at your fingertips. Check it out!

Hangman is a pretty popular game project for beginner programmers. It is a word guessing game by guessing each secret word's letter one by one and for every error guess you got, a man's body part is drawn hanging in the gallow. If the figure of a man is finished (head, body, arms, and legs are drawn), that means you are out of guesses and the game is over.

In a previous tutorial, we've made a text-based hangman game. However, in this tutorial, we'll learn about how to create a graphical hangman game using Pygame in Python.

Table of Contents

Installation & Setup

Let's start by installing the Pygame module with pip or pip3 in our terminal:

$ pip install pygame

Then create hangman.py in your app directory. I name mine hangman-gui. Another file we need is the words.txt which contains words where we pick a secret word to guess.

Now head to hangman.py and import pygame module, pygame.locals for font and other stuff, os for accessing the words.txt file, random module for random word picking, and ascii_letters to make sure the player's guess is ASCII.

# hangman-gui/hangman.py
import pygame
from pygame.locals import *

import os
import random
from string import ascii_letters

Let's check first if everything will works:

pygame.init()
pygame.font.init()

font = pygame.font.SysFont("Courier New", 40)

running = True

screen = pygame.display.set_mode((400, 500))
screen.fill("white")

message = font.render("hello world", True, ("red"))
screen.blit(message,(0,0))

pygame.display.update()

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
pygame.quit()

Let's run our app by typing python hangman.py in our terminal in the hangman-gui directory:

Hello world

Now that we see Pygame is working we can delete all the codes (left the imports) and type:

pygame.init()
pygame.font.init()

screen = pygame.display.set_mode((400, 500))
pygame.display.set_caption("Hangman")

The pygame.init() initiates the game while the pygame.font.init() initiates the fonts so we can use the Pygame's built-in fonts. The screen creates the game window with the caption "Hangman" at the top.

The Hangman class

Let's start coding by adding the Hangman class and inside it, make a function named __init__() which needs components such as self.secret_word, self.guessed_word, self.wrong_guesses (a list of false guess), self.wrong_guess_count, self.taking_guess, and self.running:

class Hangman():
    def __init__(self):
        with open("./words.txt", "r") as file:
            # picks secret word and passing it's length
            words = file.read().split("\n")
            self.secret_word = random.choice(words)
            # passing secret word's length for making letter blanks
            self.guessed_word = "*" * len(self.secret_word)
        self.wrong_guesses = []
        self.wrong_guess_count = 0
        self.taking_guess = True
        self.running = True

As you noticed, I put two boolean variables, the self.taking_guess and the self.running, with the same value.

The self.running manage to run the game. While true, it will render the game window continuously so it won't automatically collapse. When the user hits the exit button of the window, the value of the self.running will turn to False causing to collapse the game window to.

On the other hand, the self.taking_guess controls the ability to take guesses. While its value is True, it allows the user to enter a guess and when the game is over (the user had won/lost), its value will turn to False, not allowing the player to make guesses anymore.

Let's also add other game components colors, font, etc. we were using later.

class Hangman():
    def __init__(self):
        ...
        self.background_color = (155, 120, 70)
        self.gallow_color = (0,0,0)
        self.body_color = (255,253,175)
        self.font = pygame.font.SysFont("Courier New", 20)
        self.FPS = pygame.time.Clock()

Now let's proceed to the other parts of the game.

Creating the Gallow

The gallow is where our man figure will be hanging. To draw the gallow, we are using Pygame Rects. The Rect is short for rectangle, with the syntax of pygame.Rect(X, Y, Width, Height)). To draw it on the game surface, you can do pygame.draw.rect(screen, color, pygame.Rect(X, Y, Width, Height)) then pygame.display.flip() to update the screen so we can see the Rect.

Inside the Hangman class, make a function named _gallow(). This function will draw the gallow image using the rects:

    def _gallow(self):
        stand = pygame.draw.rect(screen, self.gallow_color, pygame.Rect(75, 280, 120, 10))
        body = pygame.draw.rect(screen, self.gallow_color, pygame.Rect(128, 40, 10, 240))
        hanger = pygame.draw.rect(screen, self.gallow_color, pygame.Rect(128, 40, 80, 10))
        rope = pygame.draw.rect(screen, self.gallow_color, pygame.Rect(205, 40,10, 30))

Each of these game configs will draw an image of a gallow and pygame.display.flip() pass it to the game window, which we'll put inside the while loop later. The reason is if we put multiple flip() or update() per function and call them all together, it will cause our whole game to over and over while running.

Adding Man Pieces

Now that we have a gallow, let's draw the man figure part by part which represents how many wrong guesses the player already has. Create a _man_pieces() function inside the class which we call every time we want to draw the man's body parts for every wrong guess. The man is supposed to be placed close to the gallow's rope so it will look like he is hanging in it:

    def _man_pieces(self):
        head = pygame.draw.circle(screen, self.body_color, [210, 85], 20, 0)
        body = pygame.draw.rect(screen, self.body_color, pygame.Rect(206, 105, 8, 45))
        r_arm = pygame.draw.line(screen, self.body_color, [183, 149], [200, 107], 6)
        l_arm = pygame.draw.line(screen, self.body_color, [231, 149], [218, 107], 6),
        r_leg = pygame.draw.line(screen, self.body_color, [189, 198], [208, 148], 6),
        l_leg = pygame.draw.line(screen, self.body_color, [224, 198], [210, 148], 6)

Just like the gallow, each this piece will draw a figure of a human inside the game window after calling the flip().

Calling The Game

Now we'll try to run the game to see what the gallow and the man looks like. Let's create another function in the class and name it main(). This function controls running and/or stopping the game:

    def main(self):
        # game's main components (no need to update)
        screen.fill(self.background_color)
        self._gallow()
        self._man_pieces()

        while self.running:

            for self.event in pygame.event.get():
                if self.event.type == pygame.QUIT:
                    self.running = False

            pygame.display.flip()
            self.FPS.tick(60)

        pygame.quit()

Inside the while loop of the main() function, we put a for loop that catches all types of events so we can catch for every second all the events happening inside the game window (mouse movements, keys pressed, mouse clicks, etc.). We also put the flip() and the clock tick so it will refresh all the animation inside the while loop for every second at a normal speed.

We put the screen.fill(), the self._gallow(), and the self._man_pieces() ahead and outside of the while loop because these animations do not need to refresh per frame so our code will run faster.

It is also important to put pygame.quit() after the while loop or else we'll catch some error.

Below the class and outside of it, let's call the game and then run the hangman.py in our terminal to see what it looks like:

if __name__ == "__main__":
    h = Hangman()
    h.main()

the game with the man and the gallowThe man was properly placed in the gallow as we expected. Let's create the other parts of the game for handling guesses from keys pressed.

Taking and Checking Guesses

Create another function in the class and name it _guess_taker(). This function will be responsible for taking guesses the player submitted:

    def _guess_taker(self, guess_letter):
        if guess_letter in ascii_letters:
            if guess_letter in self.secret_word and guess_letter not in self.guessed_word:
                self._right_guess(guess_letter)
            elif guess_letter not in self.secret_word and guess_letter not in self.wrong_guesses:
                self._wrong_guess(guess_letter)

The _guess_taker() checks the value of the key pressed by the player as a letter and not a symbol or number by looking at the ascii_letters and if it isn't, it will just ignore it. And if it is, it will proceed.

If the guess_letter is in self.secret_word and not yet guessed (guess_letter not in self.guessed_word), it will send the letter in the self._right_guess() function. And if not in self.secret_word, and vice versa, it will send the letter to self._wrong_guess().

We include the guess_letter, not in self.guessed_word and guess_letter not in self.wrong_guesses to not waste a move. This doesn't allow the guess to proceed in self._right_guess() or self._wrong_guess() in case the letter is already guessed, or the letter is already in the wrong guesses to prevent adding another body part just because of the mistake the player already committed in the earlier situation.

Let's update the main() function. Let's put game instructions below and inside the for loop, we'll call the self._guess_taker() function.

    def main(self):
        # game's main components (no need to update)
        screen.fill(self.background_color)
        self._gallow()
        instructions = self.font.render('Press any key to take Guess', True, (9,255,78))
        screen.blit(instructions,(35,460))
        while self.running:
            # shows the guessed word in the game window
            guessed_word = self.font.render(f"guessed word: {self.guessed_word}", True, (0,0,138))
            screen.blit(guessed_word,(10,370))
            # shows the wrong guesses in the game window
            wrong_guesses = self.font.render(f"wrong guesses: {' '.join(map(str, self.wrong_guesses))}", True, (125,0,0))
            screen.blit(wrong_guesses,(10,420))
            for self.event in pygame.event.get():
                ...
                # manages keys pressed
                elif self.event.type == pygame.KEYDOWN:
                    if self.taking_guess:
                        self._guess_taker(self.event.unicode)
            pygame.display.flip()
            self.FPS.tick(60)
        pygame.quit()

Below the screen fill and the _gallow() call, we removed the self._man_pieces and add some text instructions. We also added to the screen the guessed word and the wrong guesses which we put inside the while loop since they are updating for every guess the player.

The unicode gets the value of the pressed key so if the type of the self.event is a KEYDOWN (a key was pressed) and the game is still taking guesses if self.taking_guess, we'll call the self._guess_taker() function and give self.event.unicode as its argument.

If we check the game by running, you might see:

the game with the man, the gallow and the instructionsNow it has the game instructions saying "Press any key to take a guess" and if you take a guess you'll catch an error.

A picture of errorThis is because of the two functions (the self._right_guess() and the self._wrong_guess()) we called but currently, we don't have. And those are the next things we're adding to the game.

Saving the Guesses

Above the _guess_taker() function, create two other functions and name them _right_guess() and _wrong_guess(). These functions will save the guess whether it belongs to the wrong guesses lista or to guessed word (the right guess).

    def _right_guess(self, guess_letter):
        index_positions = [index for index, item in enumerate(self.secret_word) if item == guess_letter]
        for i in index_positions:
            self.guessed_word = self.guessed_word[0:i] + guess_letter + self.guessed_word[i+1:]
        # stacks a layer of color on guessed word to hide multiple guessed_word stack
        screen.fill(pygame.Color(self.background_color), (10, 370, 390, 20))

The index_positions collects all the indices where the guessed_letter is present. We put it in a list because some words had one letter two or more than times. Words like 'dull' with two letters 'l', 'bottom' with two 'o's and 't's, and 'attention' with three 't's.

Once we have them, we'll replace the asterisk "*" character with the letter guessed by the player using the indices we collect. So let's say our secret word is "attention" and the player's guess is the letter 't', the index_positions list contains 1, 2, and 5 since we start our count from 0. The self.guessed_word will be "*tt**t***".

We also put another layer of color with the same color as the background, this will cover the rest of the stacked guesses behind it that were configured for every right guess the player did. If we didn't put a fill it would be messy and the guessed word is kind of hard to read.

A picture of multiple stacks of the guessed word

Once an object is already uploaded to the game screen using flip() or update(), it is impossible to remove and one of the fast fixes for this is adding a layer of color fill above the area we want to cover.

Next, let's modify the _wrong_guess() function:

    def _wrong_guess(self, guess_letter):
        self.wrong_guesses.append(guess_letter)
        self.wrong_guess_count += 1
        self._man_pieces()

The _wrong_guess() appends the guessed letter to self.wrong_guesses, adds 1 to the self.wrong_guess_count, and calls the _man_pieces() to add a man's body part based on the self.wrong_guess_count.

Let's next modify our self._man_pieces() since we made it earlier to draw the whole body in just a call. We need it to draw each of the parts for every mistake from the head, to the legs:

    # draw man's body parts for every wrong guess
    def _man_pieces(self):
        if self.wrong_guess_count == 1:
            head = pygame.draw.circle(screen, self.body_color, [210, 85], 20, 0)
        elif self.wrong_guess_count == 2:
            body = pygame.draw.rect(screen, self.body_color, pygame.Rect(206, 105, 8, 45))
        elif self.wrong_guess_count == 3:
            r_arm = pygame.draw.line(screen, self.body_color, [183, 149], [200, 107], 6)
        elif self.wrong_guess_count == 4:
            l_arm = pygame.draw.line(screen, self.body_color, [231, 149], [218, 107], 6),
        elif self.wrong_guess_count == 5:
            r_leg = pygame.draw.line(screen, self.body_color, [189, 198], [208, 148], 6),
        elif self.wrong_guess_count == 6:
            l_leg = pygame.draw.line(screen, self.body_color, [224, 198], [210, 148], 6)

If we try our game, it will work now better, we can take guesses now and check them but it won't tell you whether you win or you lose. It doesn't even stop if the man is fully drawn or the world is already guessed.

A picture of the game having errorIn the next one, we're adding the game situation analyzer which also sends us the message for the game state.

Analyzing Game Situations

Below the _guess_taker() create a function we call _message(). This will serve as the analyzer of the game situation and also the responsible for sending messages about whether the player wins or loses:

    def _message(self):
        # win situation
        if self.guessed_word == self.secret_word:
            self.taking_guess = False
            screen.fill(pygame.Color(0,0,79), (40, 218, 320, 30))
            message = self.font.render("YOU WIN!!", True, (255,235,0))
            screen.blit(message,(152,224))

        # lose situation
        elif self.wrong_guess_count == 6:
            self.taking_guess = False
            screen.fill(pygame.Color("grey"), (40, 218, 320, 30))
            message = self.font.render("GAME OVER YOU LOSE!!", True, (150,0,10))
            screen.blit(message,(78,224))
            # shows the secret word if the player lose
            word = self.font.render(f"secret word: {self.secret_word}", True, (255,255,255))
            screen.blit(word,(10,300))
        
        # removes the instruction message if not taking guesses anymore
        if not self.taking_guess:
            screen.fill(pygame.Color(self.background_color), (35, 460, 390, 20))

We also cover a color layer in the game instructions another indicator that the game is over.

Now let's call it inside the main(). We're placing it inside the while loop so it will review the game state for every second. Let's call it above the for loop:

    def main(self):
        ...

        while self.running:
            ...

            # checking game state
            self._message()
        
            for self.event in pygame.event.get():
                ...

            pygame.display.flip()
            self.FPS.tick(60)

        pygame.quit()

And now we're done! Here are some of the game snapshots.

The player won with minimal errors:

A picture of the game winning situation with minimum errors

The player won with no errors:

A picture of the game winning situation with no error/sThe game while playing.

A picture of the game while playingThe player loses:

A picture of the game losing situationNow we know how to create a pretty cool and easy game with Pygame. Discover new words and have fun by playing Hangman.

In this tutorial, we learned about the basics of the Pygame module and make a Hangman game from scratch. I hope you learned something and you liked this tutorial. See you at the next one and have a nice day!

You can check the complete code here.

Here are some PyGame tutorials:

Happy coding ♥

Ready for more? Dive deeper into coding with our AI-powered Code Explainer. Don't miss it!

View Full Code Explain The Code for Me
Sharing is caring!



Read Also



Comment panel

    Got a coding query or need some guidance before you comment? Check out our Python Code Assistant for expert advice and handy tips. It's like having a coding tutor right in your fingertips!