Skip to content

Commit d7defbe

Browse files
authored
Merge pull request #464 from realpython/hangman-pysimplegui
Sample code for hangman with PySimpleGUI
2 parents 25a5652 + ee93683 commit d7defbe

File tree

13 files changed

+1588
-0
lines changed

13 files changed

+1588
-0
lines changed

hangman-pysimplegui/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Build a Hangman Game With Python and PySimpleGUI
2+
3+
This folder provides the code examples for the Real Python tutorial [Build a Hangman Game With Python and PySimpleGUI](https://realpython.com/python-hangman-pysimplegui/).
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
from random import choice
2+
from string import ascii_uppercase
3+
4+
import PySimpleGUI as sg
5+
6+
MAX_WRONG_GUESSES = 6
7+
8+
9+
class Hangman:
10+
def __init__(self):
11+
layout = [
12+
[
13+
self._build_canvas_frame(),
14+
self._build_letters_frame(),
15+
],
16+
[
17+
self._build_guessed_word_frame(),
18+
],
19+
[
20+
self._build_action_buttons_frame(),
21+
],
22+
]
23+
self._window = sg.Window(title="Hangman", layout=layout, finalize=True)
24+
self._canvas = self._window["-CANVAS-"]
25+
self._new_game()
26+
self.quit = False
27+
self._played_games = 0
28+
self._won_games = 0
29+
30+
def _build_canvas_frame(self):
31+
return sg.Frame(
32+
"Hangman",
33+
[
34+
[
35+
sg.Graph(
36+
key="-CANVAS-",
37+
canvas_size=(200, 400),
38+
graph_bottom_left=(0, 0),
39+
graph_top_right=(200, 400),
40+
)
41+
]
42+
],
43+
font="Any 20",
44+
)
45+
46+
def _build_letters_frame(self):
47+
letter_groups = [
48+
ascii_uppercase[i : i + 4]
49+
for i in range(0, len(ascii_uppercase), 4)
50+
]
51+
letter_buttons = [
52+
[
53+
sg.Button(
54+
button_text=f" {letter} ",
55+
font="Courier 20",
56+
border_width=0,
57+
button_color=(None, sg.theme_background_color()),
58+
key=f"-letter-{letter}-",
59+
enable_events=True,
60+
)
61+
for letter in letter_group
62+
]
63+
for letter_group in letter_groups
64+
]
65+
return sg.Column(
66+
[
67+
[
68+
sg.Frame(
69+
"Letters",
70+
letter_buttons,
71+
font="Any 20",
72+
),
73+
sg.Sizer(),
74+
]
75+
]
76+
)
77+
78+
def _build_guessed_word_frame(self):
79+
return sg.Frame(
80+
"",
81+
[
82+
[
83+
sg.Text(
84+
key="-DISPLAY-WORD-",
85+
font="Courier 20",
86+
)
87+
]
88+
],
89+
element_justification="center",
90+
)
91+
92+
def _build_action_buttons_frame(self):
93+
return sg.Frame(
94+
"",
95+
[
96+
[
97+
sg.Sizer(h_pixels=90),
98+
sg.Button(
99+
button_text="New",
100+
key="-NEW-",
101+
font="Any 20",
102+
),
103+
sg.Sizer(h_pixels=60),
104+
sg.Button(
105+
button_text="Restart",
106+
key="-RESTART-",
107+
font="Any 20",
108+
),
109+
sg.Sizer(h_pixels=60),
110+
sg.Button(
111+
button_text="Quit",
112+
key="-QUIT-",
113+
font="Any 20",
114+
),
115+
sg.Sizer(h_pixels=90),
116+
]
117+
],
118+
font="Any 20",
119+
)
120+
121+
def _draw_scaffold(self):
122+
lines = [
123+
((40, 55), (180, 55), 10),
124+
((165, 60), (165, 365), 10),
125+
((160, 360), (100, 360), 10),
126+
((100, 365), (100, 330), 10),
127+
((100, 330), (100, 310), 1),
128+
]
129+
for *points, width in lines:
130+
self._canvas.DrawLine(*points, color="black", width=width)
131+
132+
def _draw_hanged_man(self):
133+
head = (100, 290)
134+
torso = [((100, 270), (100, 170))]
135+
left_arm = [
136+
((100, 250), (80, 250)),
137+
((80, 250), (60, 210)),
138+
((60, 210), (60, 190)),
139+
]
140+
right_arm = [
141+
((100, 250), (120, 250)),
142+
((120, 250), (140, 210)),
143+
((140, 210), (140, 190)),
144+
]
145+
left_leg = [
146+
((100, 170), (80, 170)),
147+
((80, 170), (70, 140)),
148+
((70, 140), (70, 80)),
149+
((70, 80), (60, 80)),
150+
]
151+
right_leg = [
152+
((100, 170), (120, 170)),
153+
((120, 170), (130, 140)),
154+
((130, 140), (130, 80)),
155+
((130, 80), (140, 80)),
156+
]
157+
body = [
158+
torso,
159+
left_arm,
160+
right_arm,
161+
left_leg,
162+
right_leg,
163+
]
164+
if self._wrong_guesses == 1:
165+
self._canvas.DrawCircle(head, 20, line_color="red", line_width=2)
166+
elif self._wrong_guesses > 1:
167+
for part in body[self._wrong_guesses - 2]:
168+
self._canvas.DrawLine(*part, color="red", width=2)
169+
170+
def _select_word(self):
171+
with open("words.txt", mode="r", encoding="utf-8") as words:
172+
word_list = words.readlines()
173+
return choice(word_list).strip().upper()
174+
175+
def _build_guessed_word(self):
176+
current_letters = []
177+
for letter in self._target_word:
178+
if letter in self._guessed_letters:
179+
current_letters.append(letter)
180+
else:
181+
current_letters.append("_")
182+
return " ".join(current_letters)
183+
184+
def _new_game(self):
185+
self._target_word = self._select_word()
186+
self._restart_game()
187+
188+
def _restart_game(self):
189+
self._guessed_letters = set()
190+
self._wrong_guesses = 0
191+
self._guessed_word = self._build_guessed_word()
192+
# Restart GUI
193+
self._canvas.erase()
194+
self._draw_scaffold()
195+
for letter in ascii_uppercase:
196+
self._window[f"-letter-{letter}-"].update(disabled=False)
197+
self._window["-DISPLAY-WORD-"].update(self._guessed_word)
198+
199+
def _play(self, letter):
200+
if letter not in self._target_word:
201+
self._wrong_guesses += 1
202+
self._guessed_letters.add(letter)
203+
self._guessed_word = self._build_guessed_word()
204+
# Update GUI
205+
self._window[f"-letter-{letter}-"].update(disabled=True)
206+
self._window["-DISPLAY-WORD-"].update(self._guessed_word)
207+
self._draw_hanged_man()
208+
209+
def read_event(self):
210+
event = self._window.read()
211+
event_id = event[0] if event is not None else None
212+
return event_id
213+
214+
def process_event(self, event):
215+
if event[:8] == "-letter-":
216+
self._play(letter=event[8])
217+
elif event == "-RESTART-":
218+
self._restart_game()
219+
elif event == "-NEW-":
220+
self._new_game()
221+
222+
def is_over(self):
223+
return any(
224+
[
225+
self._wrong_guesses == MAX_WRONG_GUESSES,
226+
set(self._target_word) <= self._guessed_letters,
227+
]
228+
)
229+
230+
def check_winner(self):
231+
self._played_games += 1
232+
if self._wrong_guesses < MAX_WRONG_GUESSES:
233+
self._won_games += 1
234+
answer = sg.PopupYesNo(
235+
"You've won! Congratulations!\n"
236+
f"That's {self._won_games} out of {self._played_games}!\n"
237+
"Another round?",
238+
title="Winner!",
239+
)
240+
else:
241+
answer = sg.PopupYesNo(
242+
f"You've lost! The word was '{self._target_word}'.\n"
243+
f"That's {self._won_games} out of {self._played_games}!\n"
244+
"Another round?",
245+
title="Sorry!",
246+
)
247+
self.quit = answer == "No"
248+
if not self.quit:
249+
self._new_game()
250+
251+
def close(self):
252+
self._window.close()
253+
254+
255+
if __name__ == "__main__":
256+
game = Hangman()
257+
# Rounds
258+
while not game.quit:
259+
# Event loop
260+
while not game.is_over():
261+
event_id = game.read_event()
262+
if event_id in {sg.WIN_CLOSED, "-QUIT-"}:
263+
game.quit = True
264+
break
265+
game.process_event(event_id)
266+
267+
if not game.quit:
268+
game.check_winner()
269+
270+
game.close()

0 commit comments

Comments
 (0)