Hangman Game

There's a certain thing about a classic hangman game. Some kind of charm, perhaps. I find it pretty versatile, especially for remote meetings. Sometimes there's got to be some little tension-breaker among co-workers.

So I wrote this little python script to run a hangman game, getting random words from a local word_list.txt file.

There's really not much to it. It's a single python script, one text file with the words, and nothing else. All it needs is a python interpreter. Regularly, tkinter is included in python3. Some linux distributions, however, have it in a separate package. Lubuntu, for example, made me apt-get install python3-tk in order for it to work, but other than that, nothing else. I prefer tkinter over pyQt6 (although pyQt6 is prettier in my not-so-humble opinion) because I'd rather not have to deal with virtual environments.

  • Random word taken from a local `word_list.txt` (one word per line).
  • Classic ASCII-art hang-man that updates after each wrong guess.
  • Input field for single-letter guesses (case-insensitive).
  • New-Game button, win/lose messages, and a "reset" button.
  • No external image files - everything is drawn with text.








Code:
hangman.py
"""
Simple Hang-Man game with a Tkinter GUI.

by Jonathan Torres

This program is free software distributed under the GNU
General Public License, version 3, or any later version
published by the Free Software Foundation. 

"""

import random
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from typing import List


# ASCII stages

HANGMAN_STAGES: List[str] = [
    """
     +---+
         |
         |
         |
        ===""",
    """
     +---+
     O   |
         |
         |
        ===""",
    """
     +---+
     O   |
     |   |
         |
        ===""",
    """
     +---+
     O   |
    /|   |
         |
        ===""",
    """
     +---+
     O   |
    /|\\  |
         |
        ===""",
    """
     +---+
     O   |
    /|\\  |
    /    |
        ===""",
    """
     +---+
     O   |
    /|\\  |
    / \\  |
        ===""",
]

MAX_WRONG = len(HANGMAN_STAGES) - 1


# wordfile loader helper

def load_word_list(path: Path) -> List[str]:

    if not path.is_file():
        raise FileNotFoundError(f"Word list not found: {path}")
        """hang"""
    words = []
    with path.open(encoding="utf-8") as fh:
        for line in fh:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            words.append(line.lower())
    if not words:
        raise ValueError("The word list is empty.")
    return words


# main

class HangmanWindow:
    def __init__(self, word_list: List[str]):
        self.word_list = word_list
        self.root = tk.Tk()
        self.root.title("Hang-Man")
        self.root.geometry("300x400")
        self._setup_ui()
        self._new_game()

    def _setup_ui(self):
        self.drawing_lbl = tk.Label(
            self.root,
            font=("Courier", 12),
            justify="center"
        )

        self.word_lbl = tk.Label(
            self.root,
            font=("Sans Serif", 20, "bold")
        )

        #input field.  width 2 because otherwise it looks too small.
        self.guess_input = tk.Entry(self.root, font=("Sans Serif", 14), width=2)
        self.guess_input.bind("<Return>", lambda e: self._process_guess())

        self.wrong_lbl = tk.Label(
            self.root,
            font=("Sans Serif", 12),
            fg="red"
        )

        self.new_btn = tk.Button(self.root, text="New Game", command=self._new_game)
        self.quit_btn = tk.Button(self.root, text="Quit", command=self.root.quit)


        self.drawing_lbl.pack(pady=10)
        self.word_lbl.pack(pady=10)
        self.guess_input.pack(pady=5)
        self.wrong_lbl.pack(pady=5)
        """the"""
        btn_frame = tk.Frame(self.root)
        btn_frame.pack(pady=20, side="top", fill="none", expand=False)
        self.new_btn.pack(side="left", padx=10)
        self.quit_btn.pack(side="left", padx=10)


    # game flow helpers

    def _new_game(self):
        #new
        self.secret_word = random.choice(self.word_list)
        self.correct_set = set()
        self.wrong_set = set()
        self.wrong_cnt = 0
        self._update_ui()
        self.guess_input.config(state="normal")
        self.guess_input.delete(0, tk.END)
        self.guess_input.focus()

    def _process_guess(self):
        # check input & win/lose status
        guess = self.guess_input.get().lower()
        self.guess_input.delete(0, tk.END)

        if not guess.isalpha():
            messagebox.showinfo("Invalid", "Please type a letter (A-Z).")
            return

        if guess in self.correct_set or guess in self.wrong_set:
            messagebox.showinfo("Repeated", f"You already guessed \"{guess}\".")
            return

        if guess in self.secret_word:
            self.correct_set.add(guess)
        else:
            self.wrong_set.add(guess)
            self.wrong_cnt += 1

        self._update_ui()
        self._check_game_over()


    # refresh

    def _update_ui(self):

        self.drawing_lbl.config(text=HANGMAN_STAGES[self.wrong_cnt])
        """stickman"""
        displayed = " ".join(
            ch if ch in self.correct_set else "_" for ch in self.secret_word
        )
        self.word_lbl.config(text=displayed)

        # change color when almost lose/lose
        if self.wrong_cnt == MAX_WRONG:
            self.drawing_lbl.config(fg="red")
        elif self.wrong_cnt >= MAX_WRONG - 1:
            self.drawing_lbl.config(fg="darkred")
        else:
            self.drawing_lbl.config(fg="black")

        # display wrong letters
        if self.wrong_set:
            wrong_letters = " ".join(sorted(self.wrong_set))
            self.wrong_lbl.config(text=f"Wrong: {wrong_letters}")
        else:
            self.wrong_lbl.config(text="")


    # checks

    def _check_game_over(self):
        if self.wrong_cnt >= MAX_WRONG:
            self._end_game(False)
            return


        if all(ch in self.correct_set for ch in set(self.secret_word)):
            self._end_game(True)

    def _end_game(self, won: bool):
        self.guess_input.config(state="disabled")
        if won:
            msg = f"You guessed it! The word was \"{self.secret_word}\"."
            messagebox.showinfo("You win!", msg)
        else:

            self.drawing_lbl.config(text=HANGMAN_STAGES[-1])
            msg = f"You missed it. The word was \"{self.secret_word}\"."
            messagebox.showinfo("Game Over", msg)



# start

def main() -> None:

    script_dir = Path(__file__).parent
    words = load_word_list(script_dir / "word_list.txt")
    """for sparta!"""
    window = HangmanWindow(words)
    window.root.mainloop()


if __name__ == "__main__":
    main()