-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththree_cups_interactive.py
More file actions
380 lines (311 loc) · 13 KB
/
Copy paththree_cups_interactive.py
File metadata and controls
380 lines (311 loc) · 13 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""
three_cups_interactive.py
=========================
The Canvas version of Three Cups, upgraded into a real "guess where the ball
went" game with two new bits of interaction:
1. ROSWELL DOTS. Just before each swap, a GREEN dot lights up over the first
cup and a BLUE dot over the second -- the pair about to trade places. They
blink for about a third of a second (the little UFO "beam" cue), then wink
out the instant the swap happens.
2. CLICK TO LIFT. The ball is kept HIDDEN during the swaps. When the run
finishes, you click any cup to lift it and see what's underneath: if the
blue ball is there it flashes in and out 7 times; if not, the cup is empty.
TWO NEW CONCEPTS
----------------
* Mouse clicks: canvas.bind("<Button-1>", handler). tkinter calls handler with
an event object that carries event.x / event.y -- where you clicked. We use x
to work out which of the three cups was hit.
* Flashing / lifting with root.after(): exactly the same "loop spread out over
time" trick from the animated version. To flash the ball we just redraw the
scene over and over, toggling whether the ball is shown, each redraw scheduled
a moment after the last. To "lift" a cup we redraw it shifted upward.
We never move individual canvas items -- every frame we wipe the canvas and
redraw the whole scene from the current state. Same idea you already know.
HOW TO RUN
----------
python3 three_cups_interactive.py
"""
import tkinter as tk
# =============================================================================
# PART 1 - PURE LOGIC (unchanged from the earlier versions)
# =============================================================================
def hide_ball(position):
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):
index = cups.find("B")
return {0: "Left", 1: "Middle", 2: "Right"}.get(index, "nowhere")
# The two cup indices each swap letter exchanges (left=0, middle=1, right=2).
SWAP_PAIRS = {"A": (0, 1), "B": (1, 2), "C": (0, 2)}
# =============================================================================
# PART 2 - THE GUI
# =============================================================================
root = tk.Tk()
root.title("Three Cups - Interactive")
root.geometry("560x560")
# Start with NO radio selected (value="") so clicking any one fires its command.
position_var = tk.StringVar(value="")
status_var = tk.StringVar(value="Pick a cup to hide the ball, then enter swaps.")
BALL_BLUE = "#1565c0"
CUP_FILL = "#d9c39a"
DARK_RIM = "#6f5d34"
DOT_GREEN = "#2e7d32"
DOT_BLUE = "#1565c0"
DONE = "#9e9e9e"
CURRENT = "#1565c0"
PENDING = "#000000"
CUP_CENTERS = [120, 270, 420] # horizontal centre of each cup
CUP_NAMES = ["Left", "Middle", "Right"]
# Vertical layout (y grows downward). A resting cup's closed base sits at TOP;
# its mouth (and the ball on the table) is MOUTH_DY below that.
TOP = 90
MOUTH_DY = 100
BALL_Y = TOP + MOUTH_DY # the ball rests here on the "table"
NAME_Y = BALL_Y + 40
DOT_Y = 46 # Roswell dots hover above the cups
LIFT = 70 # how far a lifted cup rises
# --- live game state (module-level so the handlers can share it) ----------
current_cups = "000" # "B" marks the ball, "0" an empty cup
busy = False # an animation is playing -> ignore input
finished = False # the swap run is done -> clicking a cup is allowed
def draw_scene(cups, lifted=None, reveal_ball=False, dot_pair=None, dot_on=False):
"""Wipe and redraw the whole scene from the given state.
lifted -- index of a cup to draw raised (or None)
reveal_ball -- when a cup is lifted, whether to show the ball in the gap
dot_pair -- (i, j) cups to mark with the green/blue dots (or None)
dot_on -- whether those dots are lit this frame (for blinking)
"""
canvas.delete("all")
for i, cx in enumerate(CUP_CENTERS):
has_ball = cups[i] == "B"
is_lifted = (i == lifted)
dy = -LIFT if is_lifted else 0
top = TOP + dy
# A lifted cup exposes the table beneath it: draw the ball there (when
# revealing). An empty lifted cup shows nothing -- just the bare table.
if is_lifted and has_ball and reveal_ball:
canvas.create_oval(
cx - 13, BALL_Y - 13, cx + 13, BALL_Y + 13,
fill=BALL_BLUE, outline="",
)
# Cup body: an upside-down trapezoid, narrow (closed base) at the top,
# wide (mouth) at the bottom.
canvas.create_polygon(
cx - 24, top, cx + 24, top,
cx + 44, top + MOUTH_DY, cx - 44, top + MOUTH_DY,
fill=CUP_FILL, outline="#555555", width=3,
)
# Closed base (small ellipse) on top.
canvas.create_oval(
cx - 24, top - 6, cx + 24, top + 6,
fill=CUP_FILL, outline="#555555", width=3,
)
# The open mouth (larger ellipse) at the bottom, dark interior.
canvas.create_oval(
cx - 44, top + MOUTH_DY - 18, cx + 44, top + MOUTH_DY + 18,
fill=DARK_RIM, outline="#555555", width=3,
)
# The cup's name stays put on the table (it doesn't ride up when lifted).
canvas.create_text(cx, NAME_Y, text=CUP_NAMES[i], font=("Helvetica", 12, "bold"))
# The Roswell dots: green over the first cup of the pair, blue over the
# second. Drawn last so they sit on top of everything.
if dot_pair is not None and dot_on:
gi, bj = dot_pair
canvas.create_oval(
CUP_CENTERS[gi] - 9, DOT_Y - 9, CUP_CENTERS[gi] + 9, DOT_Y + 9,
fill=DOT_GREEN, outline="",
)
canvas.create_oval(
CUP_CENTERS[bj] - 9, DOT_Y - 9, CUP_CENTERS[bj] + 9, DOT_Y + 9,
fill=DOT_BLUE, outline="",
)
def highlight_swap(active_index):
"""Colour the swap letters: done = grey, current = blue/bold, upcoming = black."""
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))
def set_controls(enabled):
"""Enable/disable the radios + Play button together during an animation."""
state = "normal" if enabled else "disabled"
play_button.config(state=state)
for rb in radio_buttons:
rb.config(state=state)
# --- lifting + flashing a single cup -------------------------------------
# Used both to introduce the ball (when you pick a cup) and to peek under a cup
# after the game. It lifts the cup, flashes the ball 7 times if it's there (or
# shows an empty cup briefly if not), then lowers it and calls `after_done`.
def flash_reveal(cup_index, found_msg, empty_msg, done_msg, after_done):
global busy
busy = True
set_controls(False)
has_ball = current_cups[cup_index] == "B"
status_var.set(found_msg if has_ball else empty_msg)
def flash(n):
# n counts DOWN: even -> ball shown, odd -> hidden. 14 toggles = 7 blinks.
if n <= 0:
lower()
return
draw_scene(current_cups, lifted=cup_index, reveal_ball=(n % 2 == 0))
root.after(230, lambda: flash(n - 1))
def show_empty():
draw_scene(current_cups, lifted=cup_index, reveal_ball=False)
root.after(750, lower)
def lower():
global busy
draw_scene(current_cups) # cup back down, ball hidden again
busy = False
set_controls(True)
status_var.set(done_msg)
after_done()
# Lift first, then start whichever sequence applies.
draw_scene(current_cups, lifted=cup_index, reveal_ball=has_ball)
root.after(350, (lambda: flash(14)) if has_ball else show_empty)
def show_start():
"""A radio pick hides the ball under that cup and flashes it once so the
player can memorise where it starts."""
global current_cups, finished
if busy:
return
finished = False
current_cups = hide_ball(position_var.get())
where = find_ball(current_cups).lower()
flash_reveal(
cup_index=current_cups.find("B"),
found_msg="Watch closely -- remember where the ball is!",
empty_msg="",
done_msg=f"Ball hidden under the {where} cup. Enter swaps and press Play.",
after_done=lambda: None,
)
# --- the swap run: dots, then swap, one letter at a time -----------------
def play():
global busy, finished
if busy:
return
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
if current_cups.find("B") == -1:
status_var.set("Pick a cup to hide the ball first.")
return
busy = True
finished = False
set_controls(False)
run_step(swaps, 0)
def run_step(swaps, i):
global busy, finished
if i >= len(swaps): # no swaps left -> finished
highlight_swap(-1)
draw_scene(current_cups)
busy = False
finished = True
set_controls(True)
status_var.set("Done! Click any cup to lift it and see what's underneath.")
return
highlight_swap(i)
a, b = SWAP_PAIRS[swaps[i]]
# Blink the Roswell dots over the pair (~1/3 s), then perform the swap.
def blink(n):
if n <= 0:
do_swap()
return
draw_scene(current_cups, dot_pair=(a, b), dot_on=(n % 2 == 0))
root.after(80, lambda: blink(n - 1))
def do_swap():
global current_cups
current_cups = apply_one_swap(current_cups, swaps[i])
draw_scene(current_cups) # dots vanish as the swap lands
root.after(450, lambda: run_step(swaps, i + 1))
blink(4) # 4 toggles x 80ms = ~320ms (green/blue on, off, on, off)
def on_canvas_click(event):
"""After the run, a click lifts whichever cup you hit to reveal its secret."""
if busy or not finished:
return
for i, cx in enumerate(CUP_CENTERS):
if abs(event.x - cx) <= 44: # within the cup's mouth half-width
flash_reveal(
cup_index=i,
found_msg="You found the ball!",
empty_msg="Nothing under that cup -- keep looking!",
done_msg="Click any cup to lift it.",
after_done=lambda: None,
)
return
def reset():
"""Clear everything back to the opening state."""
global current_cups, finished
if busy:
return
current_cups = "000"
finished = False
position_var.set("")
swaps_entry.delete(0, "end")
build_swap_labels()
draw_scene(current_cups)
status_var.set("Pick a cup to hide the ball, then enter swaps.")
# --- widgets -------------------------------------------------------------
tk.Label(
root,
text="Hide the ball, run the swaps, then click a cup to find it.",
font=("Helvetica", 13, "bold"),
).pack(pady=10)
canvas = tk.Canvas(root, width=540, height=250, bg="white", highlightthickness=0)
canvas.pack(pady=4)
canvas.bind("<Button-1>", on_canvas_click) # listen for mouse clicks
choice_box = tk.LabelFrame(root, text="Hide the ball under:")
choice_box.pack(pady=6)
radio_buttons = []
for label, value in [("Left", "L"), ("Middle", "M"), ("Right", "R")]:
rb = tk.Radiobutton(
choice_box, text=label, value=value,
variable=position_var, command=show_start,
)
rb.pack(side="left", padx=8, pady=4)
radio_buttons.append(rb)
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()
swaps_display = tk.Frame(root)
swaps_display.pack(pady=8)
swap_labels = []
def build_swap_labels(_event=None):
"""Rebuild the row of swap letters so it mirrors the entry as you type."""
for old in swap_labels:
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)
swaps_entry.bind("<KeyRelease>", build_swap_labels)
button_row = tk.Frame(root)
button_row.pack(pady=8)
play_button = tk.Button(button_row, text="Play", command=play, font=("Helvetica", 12))
play_button.pack(side="left", padx=6)
tk.Button(button_row, text="Reset", command=reset, font=("Helvetica", 12)).pack(
side="left", padx=6
)
tk.Label(root, textvariable=status_var, font=("Helvetica", 12), fg="navy").pack(pady=6)
draw_scene(current_cups) # opening scene: three plain cups, ball hidden
root.mainloop()