-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththree_cups_canvas.py
More file actions
243 lines (195 loc) · 8.13 KB
/
Copy paththree_cups_canvas.py
File metadata and controls
243 lines (195 loc) · 8.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
"""
three_cups_canvas.py
====================
The animated Three Cups game, upgraded to draw real CUP SHAPES on a tkinter
Canvas (instead of text boxes), plus two bits of polish:
* the radio buttons AND the Play button are disabled while an animation runs,
* the cups redraw the instant you pick a different starting position.
NEW CONCEPT: the Canvas -- tkinter's drawing surface
----------------------------------------------------
A Canvas is a rectangle you draw shapes onto with coordinates.
* The origin (0, 0) is the TOP-LEFT corner.
* x grows to the right; y grows DOWNWARD (not upward like a maths graph).
* You draw with methods that each return an "item id":
canvas.create_rectangle(x1, y1, x2, y2, ...)
canvas.create_oval(x1, y1, x2, y2, ...) # a circle/ellipse in a box
canvas.create_polygon(x1, y1, x2, y2, ...) # connect the points
canvas.create_text(x, y, text="...")
* Shapes drawn later sit ON TOP of earlier ones (painter's order). That's why
we draw each cup first, then the ball on top so it's visible.
To animate, we don't move individual shapes here -- we just wipe everything
with canvas.delete("all") and redraw the whole scene from the cups string each
step. Simple and matches the "redraw from state" idea you already know.
HOW TO RUN
----------
python3 three_cups_canvas.py
"""
import tkinter as tk
# =============================================================================
# PART 1 - PURE LOGIC (unchanged)
# =============================================================================
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):
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")
# =============================================================================
# PART 2 - THE GUI
# =============================================================================
root = tk.Tk()
root.title("Three Cups - Canvas")
root.geometry("560x520")
position_var = tk.StringVar(value="L")
status_var = tk.StringVar(value="Pick a cup and some swaps, then press Play.")
BALL_BLUE = "#1565c0"
CUP_FILL = "#d9c39a"
DONE = "#9e9e9e"
CURRENT = "#1565c0"
PENDING = "#000000"
# Where the centre of each cup sits horizontally on the canvas.
CUP_CENTERS = [120, 270, 420]
CUP_NAMES = ["Left", "Middle", "Right"]
def draw_cups(cups):
"""Wipe the canvas and redraw all three cups for a string like '0B0'."""
canvas.delete("all") # clear everything from the previous frame
for i, cx in enumerate(CUP_CENTERS):
has_ball = cups[i] == "B"
outline = BALL_BLUE if has_ball else "#555555"
# UPSIDE-DOWN cup body: a trapezoid that is NARROW at the top (the
# closed base, now pointing up) and WIDE at the bottom (the mouth).
canvas.create_polygon(
cx - 24,
50, # top-left (closed base -- narrow)
cx + 24,
50, # top-right
cx + 44,
150, # bottom-right (mouth -- wide)
cx - 44,
150, # bottom-left
fill=CUP_FILL,
outline=outline,
width=3,
)
# The closed base at the top: a SMALL ellipse drawn side-to-side
# (left edge cx-24, right edge cx+24), filled solid like the cup.
canvas.create_oval(
cx - 24, 44, cx + 24, 56, fill=CUP_FILL, outline=outline, width=3
)
# The MOUTH: a LARGER ellipse shaped and lined the SAME way -- spanning
# from one side (cx-44) to the opposite (cx+44) -- but filled with the
# dark interior colour so it reads as the open rim facing us. Drawn
# after the body so the rim sits on top of the cup's bottom edge.
canvas.create_oval(
cx - 44, 132, cx + 44, 168, fill="#6f5d34", outline=outline, width=3
)
# The ball: centred inside the mouth ellipse with a gap all around, so
# it never touches the rim. (Mouth and ball share centre y=150.) Only
# the cup currently hiding the ball shows one.
if has_ball:
canvas.create_oval(cx - 12, 138, cx + 12, 162, fill=BALL_BLUE, outline="")
# The cup's name underneath.
canvas.create_text(cx, 192, text=CUP_NAMES[i], font=("Helvetica", 12, "bold"))
status_var.set(f"Ball is hidden under: {find_ball(cups)}")
def highlight_swap(active_index):
"""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 as one group.
Disabling them during the animation stops the user from starting a second
run (or changing the start) while one is already playing.
"""
state = "normal" if enabled else "disabled"
play_button.config(state=state)
for rb in radio_buttons:
rb.config(state=state)
def show_start():
"""Redraw cups for the current radio choice (runs on every pick)."""
draw_cups(hide_ball(position_var.get()))
# --- the animation engine (recursive loop over time via root.after) ------
def run_step(cups, swaps, i):
if i >= len(swaps): # no swaps left -> finished
status_var.set(f"Done! Ball is under the {find_ball(cups)} cup.")
highlight_swap(-1)
set_controls(True) # hand the controls back
return
highlight_swap(i)
cups = apply_one_swap(cups, swaps[i])
draw_cups(cups)
root.after(700, lambda: run_step(cups, swaps, i + 1))
def play():
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
set_controls(False) # lock controls during play
cups = hide_ball(position_var.get())
draw_cups(cups)
highlight_swap(-1)
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)
# The Canvas: our drawing surface. width/height are in pixels.
canvas = tk.Canvas(root, width=540, height=220, bg="white", highlightthickness=0)
canvas.pack(pady=4)
# Radio buttons -- kept in a list so set_controls() can disable them together.
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)
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_start() # draw the opening scene
root.mainloop()