Skip to content

Instantly share code, notes, and snippets.

@MikePandel
Created February 23, 2020 17:20
Show Gist options
  • Save MikePandel/fe0e4e7f894098327aa1d094c1e49346 to your computer and use it in GitHub Desktop.
Save MikePandel/fe0e4e7f894098327aa1d094c1e49346 to your computer and use it in GitHub Desktop.
Python - Hangman [GUI]
#!/usr/bin/env python3
import string
import os # Used for clearing the term window
import time # Used for 5 second buffer at the end of the game
import random # To pick random word from list
from tkinter import * # GUI
import tkinter as tk # GUI
from tkinter import messagebox # GUI Message Box
'''
Start of Console Code
'''
class hangman:
played_word = "" # Word in play
gameboard = [] # Playing game board
gameboard_finished = [] # End-State game board
guess = '' # Guess that's made
guess_archieve = ['Guesses:'] # Creates list of all guesses
lives = ['Lives(5):'] # Players life count
end_state = False # Is the game over
# List create from random word generator
word_list = ['stun','amuse','comment','systematic','adviser','argument','chemistry','ward','goal','knot','confession','desk','opinion','dilute','horoscope','number','overall','dark','girl','association','reserve','shrink','autonomy','worker','confrontation','mountain','conception','corpse','prestige','family','belief','mobile','trouble','temptation']
def set_Word(self):
word = random.choice(self.word_list) # Using random to grab random word from word_list
self.played_word = word
def set_finished_board(self,word):
word_list_finished = list(word)
self.gameboard_finished = word_list_finished
def set_create_board(self,word):
word_list_playing = ['_'] * len(word)
self.gameboard = word_list_playing
def set_move(self,guess,location):
self.gameboard[location] = guess
def set_guess(self,player_guess):
if(player_guess in self.guess_archieve): # Check if guess has already been made
print("You have already tried to play " + player_guess)
elif(player_guess in self.gameboard_finished): # Checking if guess is in found in gameboard_finished
for position,char in enumerate(self.gameboard_finished):
if char== player_guess: # Checks for all chances of the guess within gameboard_finished
self.set_move(self,player_guess,position)
self.guess_archieve.append(player_guess)
else:
self.lives.append('x') # Add x to lives
self.guess_archieve.append(player_guess)
def get_eg_status(self):
if(len(self.lives) == 6):
os.system('cls' if os.name == 'nt' else 'clear') # Clear term
self.end_state = True
messagebox.showinfo("GAME OVER!", "GAME OVER: Thanks for playing! \n Answer:\t" + str(''.join([str(elem) for elem in self.gameboard_finished])))
main_form.quit()
elif(self.gameboard == self.gameboard_finished):
os.system('cls' if os.name == 'nt' else 'clear') # Clear term
self.end_state = True
messagebox.showinfo("Congrats!", "You won! Thanks for playing!")
main_form.quit()
def get_user_guess(self,letter):
char = str(letter)
if(len(char) == 1 and char.isalpha()):
self.set_guess(self,char.lower())
else:
print("Guess must be a single letter!")
game = hangman # Create Game Object
game.set_Word(game) # Word in play
game.set_create_board(game,game.played_word) # game board
game.set_finished_board(game,game.played_word) # end-state
'''
End of Console Code
'''
'''
Build GUI interface using Tkinter
'''
main_form = Tk() # Create Form Object
main_form.title("Hangman")
main_form.geometry("600x310") # Set form size
main_form.resizable(0,0) # Disables Resizing
# GUI Vars
alphaList = list(string.ascii_lowercase) # Creates Alpha list
game.gameboard
gui_gameboard = tk.Label(main_form, text=game.gameboard ,font = "Verdana 30 bold")
gui_gameboard.pack(side="top")
gui_guess_archieve = tk.Label(main_form, text=game.guess_archieve ,font = "Verdana 10 bold")
gui_guess_archieve.pack()
gui_guess_archieve.place(bordermode=OUTSIDE, x=200, y=260)
gui_lives = tk.Label(main_form, text=game.lives ,font = "Verdana 10 bold")
gui_lives.pack()
gui_lives.place(bordermode=OUTSIDE, x=200, y=280)
def btn_Click(self,letter):
self.config(state="disabled")
game.get_user_guess(game,letter.lower())
gui_gameboard['text'] = game.gameboard
gui_guess_archieve['text'] = game.guess_archieve
gui_lives['text'] = game.lives
game.get_eg_status(game) # Check End-Game Status
print(letter)
def create_button(letter,xpos,ypos,index):
letter = tk.Button(main_form, text=(alphaList[index].upper()),command = lambda: btn_Click(letter,alphaList[index].upper()))
letter.pack()
letter.place(bordermode=OUTSIDE, height=50, width=100,x=xpos,y=ypos)
def populate_board(): # Generates Form with Alpha. buttons
c = 0
startpos = 60
xpos = 0
ypos = startpos
while(c < 26):
# Formating Buttons
if(c == 6):
ypos = startpos + 50
xpos = 0
elif(c == 12):
ypos = startpos + 100
xpos = 0
elif(c == 18):
ypos = startpos + 150
xpos = 0
elif(c == 24):
ypos = startpos + 200
xpos = 0
create_button(alphaList[c],xpos,ypos,c)
xpos = xpos + 100
c = c + 1
populate_board()
main_form.mainloop()
'''
End of GUI Build
'''
@uday535
Copy link

uday535 commented Oct 26, 2022

The minimum requirement of GUI as follows:
A GUI should be made using Images. Images would show on after clicking on New Word
Button. The user is provided with a Textbox in which the correct word corresponding to the
image is typed. An OK button would take the input from the user. Apart from it a virtual
Keyboard is there to type the words. A counter for number of correct guesses is also given,
utmost the user can give 3 wrong answers, and hence he need to start a new game after it. The
correct names corresponding to each image and the hints are to be stored in an external database.
Project DescriptionK21PK pdf - Google Chrome 26-10-2022 20_06_34

@calimangto119
Copy link

calimangto119 commented Feb 27, 2025

`#!/usr/bin/env python3
import os
import random
import string
import tkinter as tk
from tkinter import messagebox
import requests
from bs4 import BeautifulSoup
from PIL import Image, ImageTk
import io

def get_random_word(difficulty):
"""
Scrape a random word from randomword.com that meets the difficulty criteria.
Easy: 3-5 letters, Medium: 6-8 letters, Hard: 9+ letters.
"""
criteria = {
'easy': (3, 5),
'medium': (6, 8),
'hard': (9, 100)
}
min_len, max_len = criteria.get(difficulty, (6, 8))

# Loop until a word meeting the criteria is found.
while True:
    try:
        response = requests.get("https://randomword.com")
        if response.status_code != 200:
            continue
        soup = BeautifulSoup(response.text, 'html.parser')
        word_elem = soup.find(id="random_word")
        if word_elem:
            word = word_elem.text.strip().lower()
            if min_len <= len(word) <= max_len:
                return word
    except Exception as e:
        print("Error fetching random word:", e)

def get_word_hint(word):
"""
Attempt to scrape Wiktionary for an image hint for the given word.
Returns an image URL if found, otherwise None.
"""
url = f"https://en.wiktionary.org/wiki/{word}"
try:
response = requests.get(url)
if response.status_code != 200:
return None
soup = BeautifulSoup(response.text, 'html.parser')
# Look for the first image tag in the entry
img = soup.find("img")
if img and img.get('src'):
src = img['src']
if src.startswith("//"):
src = "https:" + src
elif src.startswith("/"):
src = "https://en.wiktionary.org" + src
return src
except Exception as e:
print("Error fetching hint image:", e)
return None

def get_word_definition(word):
"""
Attempt to scrape Wiktionary for a text definition of the word.
Returns the first definition found, or None if not available.
"""
url = f"https://en.wiktionary.org/wiki/{word}"
try:
response = requests.get(url)
if response.status_code != 200:
return None
soup = BeautifulSoup(response.text, 'html.parser')
# Attempt to find the English definitions.
english_span = soup.find("span", id="English")
if english_span:
# The header for English is typically an h2. Look for the first

    after that header.
    for sibling in english_span.parent.next_siblings:
    if sibling.name == "ol":
    li = sibling.find("li")
    if li:
    definition = li.text.strip()
    return definition
    # Fallback: try to return the first ordered list definition.
    ol = soup.find("ol")
    if ol:
    li = ol.find("li")
    if li:
    return li.text.strip()
    except Exception as e:
    print("Error fetching definition:", e)
    return None

    def load_hint_image(url):
    """
    Downloads the image from the provided URL and returns a resized PhotoImage.
    """
    try:
    response = requests.get(url)
    if response.status_code == 200:
    image_data = response.content
    image = Image.open(io.BytesIO(image_data))
    image = image.resize((150, 150), Image.ANTIALIAS)
    return ImageTk.PhotoImage(image)
    except Exception as e:
    print("Error loading hint image:", e)
    return None

    class Hangman:
    def init(self, difficulty):
    # Use scraping to choose a random word based on difficulty.
    self.played_word = get_random_word(difficulty)
    # Attempt to get an image hint URL from Wiktionary.
    self.hint_url = get_word_hint(self.played_word)
    lives_by_difficulty = {'easy': 7, 'medium': 5, 'hard': 3}
    self.lives = lives_by_difficulty.get(difficulty, 5)
    self.finished_board = list(self.played_word)
    self.gameboard = ['_'] * len(self.played_word)
    self.guess_archive = []

    def process_guess(self, guess):
        guess = guess.lower()
        if guess in self.guess_archive:
            print(f"You already guessed '{guess}'.")
            return
        self.guess_archive.append(guess)
        if guess in self.finished_board:
            for index, letter in enumerate(self.finished_board):
                if letter == guess:
                    self.gameboard[index] = guess
        else:
            self.lives -= 1
    
    def check_game_status(self):
        if self.lives <= 0:
            return "lost"
        elif self.gameboard == self.finished_board:
            return "won"
        return "ongoing"
    

    Global variables for game state and GUI components

    game = None
    main_form = None
    letter_buttons = {}
    gameboard_label = None
    archive_label = None
    lives_label = None
    hint_label = None
    current_difficulty = None

    Keep a reference to the hint image to prevent garbage collection

    hint_photo_ref = None

    def start_game(difficulty):
    global game, letter_buttons, gameboard_label, archive_label, lives_label, hint_label, hint_photo_ref, current_difficulty
    current_difficulty = difficulty
    # Clear any existing widgets from the main window.
    for widget in main_form.winfo_children():
    widget.destroy()

    # Create a new game instance.
    game = Hangman(difficulty)
    
    # Set up labels for game board, guess archive, and lives.
    gameboard_label = tk.Label(main_form, text=" ".join(game.gameboard), font="Verdana 30 bold")
    gameboard_label.pack(pady=10)
    
    archive_label = tk.Label(main_form, text="Guesses: " + ", ".join(game.guess_archive), font="Verdana 10 bold")
    archive_label.pack(pady=5)
    
    lives_label = tk.Label(main_form, text=f"Lives: {game.lives}", font="Verdana 10 bold")
    lives_label.pack(pady=5)
    
    # Load and display the hint image if available;
    # Otherwise, attempt to show a text definition as an alternative hint.
    if game.hint_url:
        hint_img = load_hint_image(game.hint_url)
        if hint_img:
            hint_photo_ref = hint_img
            hint_label = tk.Label(main_form, image=hint_photo_ref)
            hint_label.pack(pady=5)
        else:
            definition = get_word_definition(game.played_word)
            if definition:
                hint_label = tk.Label(main_form, text=f"Hint: {definition}", font="Verdana 10 italic", wraplength=500)
            else:
                hint_label = tk.Label(main_form, text="No hint available", font="Verdana 10 italic")
            hint_label.pack(pady=5)
    else:
        definition = get_word_definition(game.played_word)
        if definition:
            hint_label = tk.Label(main_form, text=f"Hint: {definition}", font="Verdana 10 italic", wraplength=500)
        else:
            hint_label = tk.Label(main_form, text="No hint available", font="Verdana 10 italic")
        hint_label.pack(pady=5)
    
    # Create a frame for the alphabet letter buttons.
    buttons_frame = tk.Frame(main_form)
    buttons_frame.pack(pady=10)
    letter_buttons.clear()
    alpha = list(string.ascii_lowercase)
    rows, cols = 4, 7  # Arrange letters in a grid.
    r, c = 0, 0
    for letter in alpha:
        btn = tk.Button(buttons_frame, text=letter.upper(), width=4, height=2)
        btn.config(command=lambda l=letter, b=btn: on_letter_click(l, b))
        btn.grid(row=r, column=c, padx=5, pady=5)
        letter_buttons[letter] = btn
        c += 1
        if c >= cols:
            c = 0
            r += 1
    
    # Bind keyboard input to the main window.
    main_form.bind("<Key>", on_key_press)
    

    def on_letter_click(letter, button):
    global game
    # Disable the letter button once pressed.
    button.config(state="disabled")
    game.process_guess(letter)
    update_labels()
    status = game.check_game_status()
    if status in ("won", "lost"):
    if status == "lost":
    msg = f"GAME OVER: The word was '{game.played_word}'."
    else:
    msg = "Congratulations! You won!"
    if messagebox.askyesno("Game Over", msg + "\nPlay again?"):
    start_game(current_difficulty)
    else:
    main_form.destroy()

    def on_key_press(event):
    letter = event.char.lower()
    if letter in string.ascii_lowercase and letter in letter_buttons:
    button = letter_buttons[letter]
    if button['state'] == tk.NORMAL:
    on_letter_click(letter, button)

    def update_labels():
    gameboard_label.config(text=" ".join(game.gameboard))
    archive_label.config(text="Guesses: " + ", ".join(game.guess_archive))
    lives_label.config(text=f"Lives: {game.lives}")

    def show_start_screen():
    # Clear the window for the start screen.
    for widget in main_form.winfo_children():
    widget.destroy()
    title_label = tk.Label(main_form, text="Welcome to Hangman!", font="Verdana 24 bold")
    title_label.pack(pady=20)
    difficulty_label = tk.Label(main_form, text="Select Difficulty:", font="Verdana 14")
    difficulty_label.pack(pady=10)

    difficulty_var = tk.StringVar(value="medium")
    difficulties = [("Easy", "easy"), ("Medium", "medium"), ("Hard", "hard")]
    for text, mode in difficulties:
        rb = tk.Radiobutton(main_form, text=text, variable=difficulty_var, value=mode, font="Verdana 12")
        rb.pack(anchor="w", padx=100)
    
    start_button = tk.Button(main_form, text="Start Game", font="Verdana 14",
                             command=lambda: start_game(difficulty_var.get()))
    start_button.pack(pady=20)
    

    def main():
    global main_form
    main_form = tk.Tk()
    main_form.title("Hangman")
    main_form.geometry("600x500")
    main_form.resizable(False, False)
    show_start_screen()
    main_form.mainloop()

    if name == 'main':
    main()
    `

@calimangto119
Copy link

Below is a summary of all the major changes made from the original code:

Object-Oriented Refactoring & Separation of Concerns:
The game logic was encapsulated in an instantiated Hangman class instead of using static methods and attributes. This improved clarity and maintainability by separating game state from the GUI.

Improved GUI Layout and Input Handling:

The GUI now arranges letter buttons in a grid and uses clear labels to display the game board, guessed letters, and remaining lives.
Both mouse clicks and keyboard inputs are supported, so players can type their guesses as well as click on buttons.
Difficulty Selection and Dynamic Lives:
A start screen was added that lets the player choose a difficulty level (easy, medium, or hard). The difficulty setting dynamically adjusts the number of lives available to the player.

Dynamic Word Selection via Web Scraping:
Instead of a hardcoded word list, the game now scrapes a public dictionary website (using randomword.com) to fetch a random word that meets the chosen difficulty criteria (based on word length).

Enhanced Hint System with Scraping:

The code attempts to scrape Wiktionary for an image hint corresponding to the chosen word.
If no image hint is available or the image fails to load, it falls back to scraping a text definition as a clue.
The hint (either image or text) is then displayed in the game’s GUI.
Game Restart Without Closing the Application:
When the game ends (win or lose), a message box prompts the user to either play again or exit. If the player chooses to continue, a new game is initialized without closing the window.

External Dependencies Added:
The updated code uses additional libraries such as requests and beautifulsoup4 for web scraping and Pillow for image handling, ensuring that dynamic content (words and hints) can be loaded from the web.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment