-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththree_cups_gui.py
More file actions
182 lines (143 loc) · 7.34 KB
/
Copy paththree_cups_gui.py
File metadata and controls
182 lines (143 loc) · 7.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""
three_cups_gui.py
=================
A tkinter version of the Three Cups problem (Ch.3 of "Learning to Code by
Solving Problems").
WHY A GUI FEELS DIFFERENT FROM A CONSOLE PROGRAM
------------------------------------------------
Your console version runs top to bottom: print, input(), print, input()...
Each input() *pauses* the whole program until the user types something.
A GUI program is inside-out. You don't pause for input. Instead you:
1. Build the window and place widgets (labels, boxes, buttons) in it.
2. Tell a button "when you are clicked, call THIS function."
3. Start the "event loop" -- tkinter then sits and waits.
When the user clicks, tkinter calls your function. That function reads the
widgets, does the work, and writes the answer back into a widget.
So the logic that used to sit in a straight line now lives in a function that
fires on a click. That single shift is the whole mental model.
HOW TO RUN
----------
python3 three_cups_gui.py
(tkinter ships with Python, so there is nothing to install.)
"""
# tkinter is Python's built-in GUI toolkit. By convention it's imported as `tk`.
import tkinter as tk
# =============================================================================
# PART 1 - THE PURE LOGIC (no GUI here at all)
# =============================================================================
# Good habit: keep the "what the program does" separate from the "how it looks."
# These three functions know nothing about windows or buttons. That means you
# could unit-test them exactly like you tested telemarketer.py, and you could
# reuse them in a console version too. The GUI will just *call* them.
def hide_ball(position):
"""Return the starting cups string for where the ball is hidden.
position is "L", "M", or "R". The cups are a 3-character string where
"B" marks the ball and "0" marks an empty cup, e.g. "B00" = ball on left.
"""
if position == "L":
return "B00"
if position == "M":
return "0B0"
if position == "R":
return "00B"
return "000" # no/invalid position -> ball not placed
def apply_one_swap(cups, swap):
"""Apply a single swap letter to the cups and return the new arrangement.
A = swap Left & Middle (indices 0 and 1)
B = swap Middle & Right (indices 1 and 2)
C = swap Left & Right (indices 0 and 2)
Strings can't be changed in place in Python, so we convert to a list,
swap two elements, then join back into a string.
"""
c = list(cups) # e.g. "B00" -> ["B", "0", "0"]
if swap == "A":
c[0], c[1] = c[1], c[0] # this is Python's tuple-swap; no temp var needed
elif swap == "B":
c[1], c[2] = c[2], c[1]
elif swap == "C":
c[0], c[2] = c[2], c[0]
return "".join(c) # ["0", "B", "0"] -> "0B0"
def apply_all_swaps(cups, swaps):
"""Run every swap letter in the `swaps` string, one after another."""
for swap in swaps: # loop over each character, left to right
cups = apply_one_swap(cups, swap)
return cups
def find_ball(cups):
"""Translate the final cups string back into a human-readable position."""
index = cups.find("B") # find() returns the position of "B", or -1
return {0: "Left", 1: "Middle", 2: "Right"}.get(index, "nowhere (lost!)")
# =============================================================================
# PART 2 - THE GUI
# =============================================================================
# `root` is the main window -- the top-level container everything else lives in.
root = tk.Tk()
root.title("Three Cups") # text in the window's title bar
root.geometry("520x360") # starting width x height in pixels
# --- tkinter "variables" -------------------------------------------------
# Widgets don't hand you a plain Python string. Instead you bind a special
# tkinter variable to a widget; reading/writing that variable stays in sync
# with what's on screen. StringVar holds a string.
position_var = tk.StringVar(value="L") # which cup the user picked; default "L"
result_var = tk.StringVar(value="") # the answer text we'll display
# --- This is the function the button will call ("the event handler") -----
# Notice it takes no arguments and returns nothing. It reads from the widgets,
# does the work by CALLING THE PART 1 FUNCTIONS, then writes the answer back
# into result_var (which is wired to the result label, so the screen updates).
def play():
position = position_var.get() # read the selected radio button
swaps = swaps_entry.get().upper() # read the text box; upper() so "a"=="A"
# Validate the swap string: only A, B, C are legal moves.
if any(ch not in "ABC" for ch in swaps):
result_var.set("Swaps can only contain the letters A, B and C.")
return
start = hide_ball(position) # e.g. "B00"
final = apply_all_swaps(start, swaps) # e.g. "00B"
where = find_ball(final) # e.g. "Right"
# Build the multi-line message and push it into the result label.
result_var.set(
f"Start: {start}\n"
f"Swaps: {swaps or '(none)'}\n"
f"Final: {final}\n\n"
f"The ball ends up under the {where} cup."
)
# --- Lay out the widgets -------------------------------------------------
# A "widget" is any on-screen element. We create each one, then .pack() it,
# which stacks it into the window top-to-bottom. (pack is the simplest of
# tkinter's layout managers; there's also grid for rows/columns.)
tk.Label(
root,
text="There is one blue ball and three opaque cups (Left, Middle, Right).\n"
"Pick the cup to hide the ball under, then enter a string of swaps.",
justify="left",
).pack(padx=12, pady=10, anchor="w")
# A LabelFrame is just a titled box to group the three radio buttons.
choice_box = tk.LabelFrame(root, text="Hide the ball under:")
choice_box.pack(padx=12, pady=4, fill="x")
# Radio buttons: all three share the SAME variable (position_var). Picking one
# sets that variable to its `value`, and automatically un-picks the others.
for label, value in [("Left", "L"), ("Middle", "M"), ("Right", "R")]:
tk.Radiobutton(choice_box, text=label, value=value, variable=position_var).pack(
side="left", padx=8, pady=4
)
tk.Label(
root,
text="Swaps (A=Left/Middle, B=Middle/Right, C=Left/Right):",
).pack(padx=12, pady=(10, 2), anchor="w")
# An Entry is a single-line text box. We keep a reference (`swaps_entry`) so the
# play() function can read it later with swaps_entry.get().
swaps_entry = tk.Entry(root, width=30)
swaps_entry.pack(padx=12, anchor="w")
# The button. `command=play` is the key line: it wires the click to our handler.
# Note: we write `play` WITHOUT parentheses -- we're handing tkinter the function
# itself to call later, not calling it now.
tk.Button(root, text="Run the swaps", command=play).pack(padx=12, pady=12, anchor="w")
# The result label is bound to result_var via textvariable. Whenever play()
# does result_var.set(...), this label redraws itself with the new text.
tk.Label(
root, textvariable=result_var, justify="left", font=("Courier", 12), fg="navy"
).pack(padx=12, pady=6, anchor="w")
# --- Hand control to tkinter --------------------------------------------
# This call STARTS the event loop and blocks here, keeping the window open and
# responsive until the user closes it. Any code after this line won't run until
# the window is closed. This replaces the old "fall off the end of main()" flow.
root.mainloop()