1
+ import tkinter as tk
2
+ from tkinter import ttk
3
+ import random
4
+ import time
5
+ import threading
6
+ import matplotlib .pyplot as plt
7
+ from matplotlib .backends .backend_tkagg import FigureCanvasTkAgg
8
+ from collections import Counter
9
+ import numpy as np
10
+
11
+ class ButtonGame :
12
+ def __init__ (self , root ):
13
+ self .root = root
14
+ self .root .title ("Button Game" )
15
+ self .root .geometry ("900x600" ) # Wider window
16
+
17
+ # Game variables
18
+ self .current_count = 0
19
+ self .high_score = 0
20
+ self .scores = []
21
+ self .score_counter = Counter () # For efficient counting
22
+ self .avg_score = 0
23
+ self .total_presses = 0
24
+ self .auto_press_active = False
25
+ self .auto_press_thread = None
26
+ self .last_update_time = 0
27
+ self .update_interval = 500 # Minimum ms between graph updates
28
+
29
+ # Create main layout with two columns
30
+ self .left_frame = tk .Frame (root )
31
+ self .left_frame .pack (side = tk .LEFT , padx = 20 , pady = 10 , fill = tk .BOTH )
32
+
33
+ self .right_frame = tk .Frame (root )
34
+ self .right_frame .pack (side = tk .RIGHT , padx = 20 , pady = 10 , fill = tk .BOTH , expand = True )
35
+
36
+ # Canvas for circular button (left frame)
37
+ self .canvas = tk .Canvas (self .left_frame , width = 150 , height = 150 , highlightthickness = 0 )
38
+ self .canvas .pack (pady = 20 )
39
+
40
+ # Create circular button
41
+ self .button_bg = self .canvas .create_oval (10 , 10 , 140 , 140 , fill = "#4CAF50" , outline = "#2E7D32" , width = 2 )
42
+ self .button_text = self .canvas .create_text (75 , 75 , text = "0" , font = ("Arial" , 24 , "bold" ), fill = "white" )
43
+
44
+ # Bind click event to canvas
45
+ self .canvas .tag_bind (self .button_bg , "<Button-1>" , lambda e : self .press_button ())
46
+ self .canvas .tag_bind (self .button_text , "<Button-1>" , lambda e : self .press_button ())
47
+
48
+ # Score display frame
49
+ score_frame = tk .Frame (self .left_frame )
50
+ score_frame .pack (pady = 15 , fill = tk .X )
51
+
52
+ tk .Label (score_frame , text = "High Score:" , font = ("Arial" , 12 )).grid (row = 0 , column = 0 , padx = 5 , sticky = tk .W )
53
+ self .high_score_label = tk .Label (score_frame , text = "0" , font = ("Arial" , 12 , "bold" ))
54
+ self .high_score_label .grid (row = 0 , column = 1 , padx = 5 , sticky = tk .W )
55
+
56
+ tk .Label (score_frame , text = "Average Score:" , font = ("Arial" , 12 )).grid (row = 1 , column = 0 , padx = 5 , pady = 5 , sticky = tk .W )
57
+ self .avg_score_label = tk .Label (score_frame , text = "0.0" , font = ("Arial" , 12 , "bold" ))
58
+ self .avg_score_label .grid (row = 1 , column = 1 , padx = 5 , pady = 5 , sticky = tk .W )
59
+
60
+ # Total presses display
61
+ total_frame = tk .Frame (self .left_frame )
62
+ total_frame .pack (pady = 10 , fill = tk .X )
63
+
64
+ tk .Label (total_frame , text = "Total Button Presses:" , font = ("Arial" , 12 )).grid (row = 0 , column = 0 , padx = 5 , sticky = tk .W )
65
+ self .total_presses_label = tk .Label (total_frame , text = "0" , font = ("Arial" , 12 , "bold" ))
66
+ self .total_presses_label .grid (row = 0 , column = 1 , padx = 5 , sticky = tk .W )
67
+
68
+ # Auto-press buttons frame
69
+ auto_frame = tk .LabelFrame (self .left_frame , text = "Auto Press Options" , font = ("Arial" , 10 , "bold" ))
70
+ auto_frame .pack (pady = 15 , fill = tk .X )
71
+
72
+ for i , speed in enumerate ([10 , 100 , 1000 ]):
73
+ tk .Button (
74
+ auto_frame ,
75
+ text = f"{ speed } /s" ,
76
+ bg = "#3498db" ,
77
+ fg = "white" ,
78
+ command = lambda s = speed : self .toggle_auto_press (s )
79
+ ).grid (row = 0 , column = i , padx = 10 , pady = 10 , sticky = tk .W )
80
+
81
+ self .auto_status_label = tk .Label (self .left_frame , text = "Auto Press: Off" , font = ("Arial" , 10 , "italic" ))
82
+ self .auto_status_label .pack (pady = 5 )
83
+
84
+ # Stop button (initially hidden)
85
+ self .stop_button = tk .Button (
86
+ self .left_frame ,
87
+ text = "STOP Auto Press" ,
88
+ bg = "#e74c3c" ,
89
+ fg = "white" ,
90
+ command = self .stop_auto_press
91
+ )
92
+
93
+ # Title for the graph
94
+ tk .Label (self .right_frame , text = "Score Distribution" , font = ("Arial" , 14 , "bold" )).pack (pady = 5 , anchor = tk .W )
95
+
96
+ # Create the matplotlib figure (right frame)
97
+ self .fig , self .ax = plt .subplots (figsize = (7 , 4.5 ))
98
+ self .fig .patch .set_facecolor ('#F0F0F0' ) # Match Tkinter background
99
+ self .ax .set_title ('Score Frequency' , fontsize = 12 )
100
+ self .ax .set_xlabel ('Score Value' , fontsize = 10 )
101
+ self .ax .set_ylabel ('Frequency (Count)' , fontsize = 10 )
102
+ self .ax .grid (True , linestyle = '--' , alpha = 0.7 , axis = 'y' )
103
+
104
+ # Setup the canvas - using double-buffered rendering can help with performance
105
+ self .canvas_graph = FigureCanvasTkAgg (self .fig , master = self .right_frame )
106
+ self .canvas_widget = self .canvas_graph .get_tk_widget ()
107
+ self .canvas_widget .pack (fill = tk .BOTH , expand = True )
108
+
109
+ # Add stats display below graph
110
+ self .stats_frame = tk .Frame (self .right_frame )
111
+ self .stats_frame .pack (pady = 10 , fill = tk .X )
112
+
113
+ # Create labels for stats in a grid (5 columns)
114
+ self .stat_labels = {}
115
+ stats = [
116
+ ('Games' , 'Games: 0' ),
117
+ ('High' , 'High: 0' ),
118
+ ('Avg' , 'Avg: 0.0' ),
119
+ ('Median' , 'Median: 0' ),
120
+ ('Mode' , 'Mode: N/A' )
121
+ ]
122
+
123
+ for i , (key , text ) in enumerate (stats ):
124
+ lbl = tk .Label (self .stats_frame , text = text , font = ("Arial" , 10 ),
125
+ bg = "#f0f0f0" , relief = tk .GROOVE , padx = 5 , pady = 3 )
126
+ lbl .grid (row = 0 , column = i , padx = 5 , sticky = tk .W + tk .E )
127
+ self .stat_labels [key ] = lbl
128
+ self .stats_frame .grid_columnconfigure (i , weight = 1 )
129
+
130
+ # Pre-render empty graph
131
+ self .update_graph_initial ()
132
+
133
+ # Create bars collection for efficient updates
134
+ self .bars = None
135
+ self .x_data = []
136
+ self .y_data = []
137
+
138
+ def update_graph_initial (self ):
139
+ """Initial graph setup with placeholder text."""
140
+ self .ax .text (0.5 , 0.5 , 'Play games to see your stats!' ,
141
+ horizontalalignment = 'center' , verticalalignment = 'center' ,
142
+ transform = self .ax .transAxes , fontsize = 12 )
143
+ self .ax .set_xlim (0 , 10 )
144
+ self .ax .set_ylim (0 , 10 )
145
+ self .fig .tight_layout ()
146
+ self .canvas_graph .draw ()
147
+
148
+ def press_button (self ):
149
+ # Increment counters
150
+ self .current_count += 1
151
+ self .total_presses += 1
152
+
153
+ # Update UI
154
+ self .canvas .itemconfig (self .button_text , text = str (self .current_count ))
155
+ self .total_presses_label .config (text = str (self .total_presses ))
156
+
157
+ # Check if reset occurs based on probability
158
+ if random .randint (1 , 100 ) <= self .current_count :
159
+ # Game over - update stats
160
+ if self .current_count > self .high_score :
161
+ self .high_score = self .current_count
162
+ self .high_score_label .config (text = str (self .high_score ))
163
+
164
+ # Update score collection
165
+ self .scores .append (self .current_count )
166
+ self .score_counter [self .current_count ] += 1
167
+
168
+ # Calculate average
169
+ self .avg_score = sum (self .scores ) / len (self .scores )
170
+ self .avg_score_label .config (text = f"{ self .avg_score :.1f} " )
171
+
172
+ # Reset counter
173
+ self .current_count = 0
174
+ self .canvas .itemconfig (self .button_text , text = "0" )
175
+
176
+ # Flash the button to indicate reset
177
+ self .flash_button ()
178
+
179
+ # Update stats panel immediately
180
+ self .update_stats_panel ()
181
+
182
+ # Check if enough time has passed for graph update
183
+ current_time = time .time () * 1000 # Convert to ms
184
+ if current_time - self .last_update_time > self .update_interval :
185
+ self .update_graph (force_redraw = True )
186
+ self .last_update_time = current_time
187
+ else :
188
+ # Schedule a deferred update
189
+ self .root .after (self .update_interval , lambda : self .update_graph (force_redraw = False ))
190
+
191
+ def update_stats_panel (self ):
192
+ """Update just the statistics labels without redrawing the graph."""
193
+ # Calculate stats
194
+ median_score = sorted (self .scores )[len (self .scores )// 2 ] if self .scores else 0
195
+ most_common = self .score_counter .most_common (1 )[0 ] if self .scores else (0 , 0 )
196
+
197
+ # Update labels
198
+ self .stat_labels ['Games' ].config (text = f"Games: { len (self .scores )} " )
199
+ self .stat_labels ['High' ].config (text = f"High: { self .high_score } " )
200
+ self .stat_labels ['Avg' ].config (text = f"Avg: { self .avg_score :.1f} " )
201
+ self .stat_labels ['Median' ].config (text = f"Median: { median_score } " )
202
+ self .stat_labels ['Mode' ].config (text = f"Mode: { most_common [0 ]} ({ most_common [1 ]} x)" )
203
+
204
+ def update_graph (self , force_redraw = False ):
205
+ """Update the graph with current score distribution."""
206
+ try :
207
+ if not self .scores :
208
+ return
209
+
210
+ # Get sorted score counts
211
+ sorted_items = sorted (self .score_counter .items ())
212
+ new_x = [item [0 ] for item in sorted_items ]
213
+ new_y = [item [1 ] for item in sorted_items ]
214
+
215
+ # Check if data structure changed (requiring full redraw)
216
+ structure_changed = new_x != self .x_data
217
+
218
+ # Store current data
219
+ self .x_data = new_x
220
+ self .y_data = new_y
221
+
222
+ # If first time or structure changed, do a full redraw
223
+ if self .bars is None or structure_changed or force_redraw :
224
+ self .ax .clear ()
225
+ self .ax .set_title ('Score Frequency' , fontsize = 12 )
226
+ self .ax .set_xlabel ('Score Value' , fontsize = 10 )
227
+ self .ax .set_ylabel ('Frequency (Count)' , fontsize = 10 )
228
+ self .ax .grid (True , linestyle = '--' , alpha = 0.7 , axis = 'y' )
229
+
230
+ # For color gradient
231
+ norm = plt .Normalize (min (new_y ), max (new_y ))
232
+ colors = plt .cm .viridis (norm (new_y ))
233
+
234
+ # Create new bars
235
+ self .bars = self .ax .bar (new_x , new_y , color = colors , alpha = 0.8 , width = 0.8 )
236
+
237
+ # Add value labels
238
+ for bar in self .bars :
239
+ height = bar .get_height ()
240
+ self .ax .text (bar .get_x () + bar .get_width ()/ 2. , height + 0.1 ,
241
+ f'{ int (height )} ' , ha = 'center' , va = 'bottom' , fontsize = 9 )
242
+
243
+ # Add average line
244
+ self .avg_line = self .ax .axvline (x = self .avg_score , color = '#e74c3c' ,
245
+ linestyle = '--' , label = f'Avg: { self .avg_score :.1f} ' )
246
+
247
+ # Set limits
248
+ max_y = max (new_y )
249
+ self .ax .set_ylim (0 , max (5 , max_y * 1.2 ))
250
+ self .ax .set_xlim (min (new_x ) - 0.5 , max (new_x ) + 0.5 )
251
+
252
+ # Set x-axis ticks
253
+ self .ax .set_xticks (new_x )
254
+
255
+ # Add legend
256
+ self .ax .legend (loc = 'upper right' )
257
+
258
+ # Adjust layout
259
+ self .fig .tight_layout ()
260
+
261
+ # Full redraw
262
+ self .canvas_graph .draw ()
263
+ else :
264
+ # Efficient update - just update the heights and average line
265
+ for bar , new_height in zip (self .bars , new_y ):
266
+ bar .set_height (new_height )
267
+
268
+ # Update average line
269
+ self .avg_line .set_xdata ([self .avg_score , self .avg_score ])
270
+ self .avg_line .set_label (f'Avg: { self .avg_score :.1f} ' )
271
+
272
+ # Update legend
273
+ self .ax .legend (loc = 'upper right' )
274
+
275
+ # Use blit for faster rendering (only update changed parts)
276
+ self .canvas_graph .draw_idle ()
277
+
278
+ except Exception as e :
279
+ print (f"Error updating graph: { e } " )
280
+
281
+ def flash_button (self ):
282
+ original_fill = self .canvas .itemcget (self .button_bg , "fill" )
283
+ self .canvas .itemconfig (self .button_bg , fill = "#e74c3c" ) # Red color
284
+ self .root .after (200 , lambda : self .canvas .itemconfig (self .button_bg , fill = original_fill ))
285
+
286
+ def toggle_auto_press (self , clicks_per_second ):
287
+ if self .auto_press_active :
288
+ self .stop_auto_press ()
289
+ else :
290
+ self .auto_press_active = True
291
+ self .auto_status_label .config (text = f"Auto Press: { clicks_per_second } /s" )
292
+ self .stop_button .pack (pady = 5 )
293
+
294
+ # Limit actual click rate to prevent overwhelming the UI
295
+ effective_rate = min (clicks_per_second , 50 )
296
+ delay = 1.0 / effective_rate
297
+
298
+ # Adjust graph update interval based on click speed
299
+ if clicks_per_second > 50 :
300
+ self .update_interval = 1000 # ms between updates for fast clicking
301
+
302
+ self .auto_press_thread = threading .Thread (
303
+ target = self .auto_press_loop ,
304
+ args = (delay , clicks_per_second , effective_rate ),
305
+ daemon = True
306
+ )
307
+ self .auto_press_thread .start ()
308
+
309
+ def auto_press_loop (self , delay , requested_rate , actual_rate ):
310
+ click_batch = max (1 , round (requested_rate / actual_rate ))
311
+
312
+ while self .auto_press_active :
313
+ for _ in range (click_batch ):
314
+ if not self .auto_press_active :
315
+ break
316
+ self .root .after_idle (self .press_button )
317
+ time .sleep (delay )
318
+
319
+ def stop_auto_press (self ):
320
+ self .auto_press_active = False
321
+ self .auto_status_label .config (text = "Auto Press: Off" )
322
+ self .stop_button .pack_forget ()
323
+ self .update_interval = 500 # Reset to default update interval
324
+
325
+ if __name__ == "__main__" :
326
+ root = tk .Tk ()
327
+ game = ButtonGame (root )
328
+ root .mainloop ()
0 commit comments