-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththree_cups_animated.py
More file actions
239 lines (192 loc) · 8.71 KB
/
Copy paththree_cups_animated.py
File metadata and controls
239 lines (192 loc) · 8.71 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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""
three_cups_animated.py
======================
The Three Cups problem, but the swaps play out as an ANIMATION:
* the three cups are drawn on screen, with the blue ball showing under one,
* the swap letter currently being performed is highlighted in blue,
* a live readout says which cup (Left / Middle / Right) hides the ball,
* each swap happens one at a time, with a short pause between them.
THE ONE NEW CONCEPT: root.after() -- animating without freezing
-----------------------------------------------------------------
Tempting but WRONG approach:
for swap in swaps:
do_one_swap()
time.sleep(0.7) # <-- FREEZES the window!
While your function is running, tkinter can't redraw. time.sleep() just makes
the program sit there, so the user sees nothing until the loop ends.
Right approach:
root.after(700, next_step)
after(ms, func) means "call func after `ms` milliseconds, but give control back
to the event loop NOW." So the window stays alive and repaints. We animate by
having each step schedule the NEXT step with after(). It's a loop spread out
over time instead of all at once.
HOW TO RUN
----------
python3 three_cups_animated.py
"""
import tkinter as tk
# =============================================================================
# PART 1 - PURE LOGIC (unchanged from the first version)
# =============================================================================
def hide_ball(position):
"""Starting arrangement: 'B' marks the ball, '0' an empty cup."""
if position == "L":
return "B00"
if position == "M":
return "0B0"
if position == "R":
return "00B"
return "000"
def apply_one_swap(cups, swap):
"""A = swap Left/Middle, B = swap Middle/Right, C = swap Left/Right."""
c = list(cups)
if swap == "A":
c[0], c[1] = c[1], c[0]
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)
def find_ball(cups):
"""Turn the cups string into 'Left' / 'Middle' / 'Right'."""
index = cups.find("B")
return {0: "Left", 1: "Middle", 2: "Right"}.get(index, "nowhere")
# =============================================================================
# PART 2 - THE GUI
# =============================================================================
root = tk.Tk()
root.title("Three Cups - Animated")
root.geometry("560x440")
position_var = tk.StringVar(value="L") # which cup starts with the ball
status_var = tk.StringVar(value="Pick a cup and some swaps, then press Play.")
# A small palette so colour choices live in one place and read clearly.
BALL_BLUE = "#1565c0" # cup that currently hides the ball
PLAIN = "#e0e0e0" # a normal, empty cup
DONE = "#9e9e9e" # a swap letter already performed
CURRENT = "#1565c0" # the swap letter being performed right now
PENDING = "#000000" # a swap letter not yet reached
def draw_cups(cups):
"""Redraw the three cup boxes to match the string e.g. '0B0'.
cup_labels is a list of three Label widgets (built later). The cup holding
the ball turns blue and shows the ball; the others are plain.
"""
names = ["Left", "Middle", "Right"]
for i, label in enumerate(cup_labels):
has_ball = cups[i] == "B"
label.config(
text=f"{names[i]}\n{'(O)' if has_ball else '( )'}",
bg=BALL_BLUE if has_ball else PLAIN,
fg="white" if has_ball else "black",
)
# Live readout of where the ball is.
status_var.set(f"Ball is hidden under: {find_ball(cups)}")
def highlight_swap(active_index):
"""Colour the swap letters: done = grey, current = blue, upcoming = black.
swap_labels is a list of Label widgets, one per letter in the swap string.
active_index is which one is happening now (-1 means none / finished).
"""
for i, label in enumerate(swap_labels):
if i < active_index:
label.config(fg=DONE, font=("Courier", 18))
elif i == active_index:
label.config(fg=CURRENT, font=("Courier", 18, "bold"))
else:
label.config(fg=PENDING, font=("Courier", 18))
# --- the animation engine ------------------------------------------------
# This is the recursive "loop over time". Each call handles ONE swap, then uses
# root.after() to schedule itself again for the NEXT swap. When there are no
# swaps left, it stops and re-enables the Play button.
def run_step(cups, swaps, i):
if i >= len(swaps): # base case: no more swaps
status_var.set(f"Done! Ball is under the {find_ball(cups)} cup.")
highlight_swap(-1) # clear all highlighting
play_button.config(state="normal") # let the user play again
return
highlight_swap(i) # show which letter we're doing
cups = apply_one_swap(cups, swaps[i]) # do the swap
draw_cups(cups) # show the new arrangement
# Schedule the next step 700ms from now and return immediately so the
# window can repaint. `lambda:` lets us pass arguments to the delayed call.
root.after(700, lambda: run_step(cups, swaps, i + 1))
def show_start():
"""Redraw the cups for the currently-selected radio button.
Wired to each Radiobutton's `command`, so it runs the moment the user
picks Left/Middle/Right -- keeping the picture in sync with the choice
instead of waiting until Play is pressed.
"""
draw_cups(hide_ball(position_var.get()))
def play():
"""Button handler: validate input, then kick off the animation."""
swaps = swaps_entry.get().upper()
if any(ch not in "ABC" for ch in swaps):
status_var.set("Swaps can only contain the letters A, B and C.")
return
# Disable the button so a second press can't start an overlapping animation
# while one is already running. run_step re-enables it when finished.
play_button.config(state="disabled")
cups = hide_ball(position_var.get()) # initial arrangement
draw_cups(cups) # show the starting state
highlight_swap(-1)
# Wait a beat so the user sees the start, then begin stepping.
root.after(700, lambda: run_step(cups, swaps, 0))
# --- widgets -------------------------------------------------------------
tk.Label(
root,
text="Hide the ball, enter swaps, and watch them play out.",
font=("Helvetica", 13, "bold"),
).pack(pady=10)
# Row of three "cups". We keep the three Labels in a list so draw_cups() can
# loop over them by index (0=Left, 1=Middle, 2=Right).
cups_frame = tk.Frame(root)
cups_frame.pack(pady=8)
cup_labels = []
for i in range(3):
lbl = tk.Label(
cups_frame, text="", width=8, height=3,
font=("Courier", 14), relief="raised", bd=3, bg=PLAIN,
)
lbl.pack(side="left", padx=8)
cup_labels.append(lbl)
# Choose where the ball starts.
choice_box = tk.LabelFrame(root, text="Hide the ball under:")
choice_box.pack(pady=6)
for label, value in [("Left", "L"), ("Middle", "M"), ("Right", "R")]:
# command=show_start makes each pick repaint the cups immediately.
tk.Radiobutton(
choice_box, text=label, value=value, variable=position_var,
command=show_start,
).pack(side="left", padx=8, pady=4)
# Swaps entry.
tk.Label(
root, text="Swaps (A=Left/Middle, B=Middle/Right, C=Left/Right):"
).pack(pady=(8, 2))
swaps_entry = tk.Entry(root, width=24, font=("Courier", 14), justify="center")
swaps_entry.pack()
# Row that shows the swap letters; filled in when Play is pressed. We rebuild
# these labels each time so the row always matches the current swap string.
swaps_display = tk.Frame(root)
swaps_display.pack(pady=8)
swap_labels = []
def build_swap_labels(_event=None):
"""Recreate one Label per swap letter so we can colour them individually.
Bound to the entry's key events (see below) so the row updates as you type.
"""
for old in swap_labels: # remove any labels from a previous string
old.destroy()
swap_labels.clear()
for ch in swaps_entry.get().upper():
lbl = tk.Label(swaps_display, text=ch, font=("Courier", 18))
lbl.pack(side="left", padx=3)
swap_labels.append(lbl)
# <KeyRelease> fires after each keystroke in the entry; we rebuild the letter
# row so it mirrors what you've typed. This is "binding an event" -- the same
# idea as a button's command, but for a different kind of user action.
swaps_entry.bind("<KeyRelease>", build_swap_labels)
play_button = tk.Button(root, text="Play", command=play, font=("Helvetica", 12))
play_button.pack(pady=8)
tk.Label(
root, textvariable=status_var, font=("Helvetica", 12), fg="navy"
).pack(pady=6)
# Show the initial arrangement so the cups are visible before pressing Play.
show_start()
root.mainloop()