|
| 1 | +import sys |
| 2 | +import os |
| 3 | +import random |
| 4 | +import json |
| 5 | +import tkinter as tk |
| 6 | +from tkinter import ttk, messagebox |
| 7 | +import sv_ttk |
| 8 | + |
| 9 | +# ========================= |
| 10 | +# Resource Path Helper |
| 11 | +# ========================= |
| 12 | +def resource_path(file_name): |
| 13 | + base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) |
| 14 | + return os.path.join(base_path, file_name) |
| 15 | + |
| 16 | +# ========================= |
| 17 | +# Globals |
| 18 | +# ========================= |
| 19 | +root = tk.Tk() |
| 20 | +root.title("Number Guessing Game with Leaderboard & Stats") |
| 21 | +root.geometry("750x600") |
| 22 | +# root.iconbitmap(resource_path("logo.ico")) |
| 23 | + |
| 24 | +sv_ttk.set_theme("light") |
| 25 | + |
| 26 | +target_number = None |
| 27 | +attempts = 0 |
| 28 | +max_attempts = 0 |
| 29 | +score = 0 |
| 30 | +number_range = (1, 100) |
| 31 | +dark_mode_var = tk.BooleanVar(value=False) |
| 32 | +difficulty_var = tk.StringVar(value="Medium") |
| 33 | + |
| 34 | +leaderboard_file = resource_path("leaderboard.json") |
| 35 | +stats_file = resource_path("player_stats.json") |
| 36 | +leaderboard_data = [] |
| 37 | +player_stats = { |
| 38 | + "total_games": 0, |
| 39 | + "attempts_list": [], |
| 40 | + "high_score_streak": 0, |
| 41 | + "current_streak": 0 |
| 42 | +} |
| 43 | + |
| 44 | +# ========================= |
| 45 | +# Helper Functions |
| 46 | +# ========================= |
| 47 | +def set_status(msg): |
| 48 | + status_var.set(msg) |
| 49 | + root.update_idletasks() |
| 50 | + |
| 51 | +def toggle_theme(): |
| 52 | + style.theme_use("clam") |
| 53 | + if dark_mode_var.get(): |
| 54 | + root.configure(bg="#2E2E2E") |
| 55 | + style.configure("TLabel", background="#2E2E2E", foreground="white") |
| 56 | + style.configure("TFrame", background="#2E2E2E") |
| 57 | + style.configure("TButton", background="#4CAF50", foreground="white") |
| 58 | + style.configure("Treeview", background="#3C3C3C", foreground="white", fieldbackground="#3C3C3C") |
| 59 | + else: |
| 60 | + root.configure(bg="#FFFFFF") |
| 61 | + style.configure("TLabel", background="#FFFFFF", foreground="black") |
| 62 | + style.configure("TFrame", background="#FFFFFF") |
| 63 | + style.configure("TButton", background="#4CAF50", foreground="white") |
| 64 | + style.configure("Treeview", background="white", foreground="black", fieldbackground="white") |
| 65 | + update_leaderboard_tree() |
| 66 | + update_stats_labels() |
| 67 | + set_status(f"Theme switched to {'Dark' if dark_mode_var.get() else 'Light'} mode") |
| 68 | + |
| 69 | +# ========================= |
| 70 | +# Leaderboard Functions |
| 71 | +# ========================= |
| 72 | +def load_leaderboard(): |
| 73 | + global leaderboard_data |
| 74 | + if os.path.exists(leaderboard_file): |
| 75 | + try: |
| 76 | + with open(leaderboard_file, "r", encoding="utf-8") as f: |
| 77 | + leaderboard_data = json.load(f) |
| 78 | + except: |
| 79 | + leaderboard_data = [] |
| 80 | + else: |
| 81 | + leaderboard_data = [] |
| 82 | + |
| 83 | +def save_leaderboard(): |
| 84 | + with open(leaderboard_file, "w", encoding="utf-8") as f: |
| 85 | + json.dump(leaderboard_data, f, indent=4) |
| 86 | + |
| 87 | +def update_leaderboard_tree(): |
| 88 | + for row in leaderboard_tree.get_children(): |
| 89 | + leaderboard_tree.delete(row) |
| 90 | + for entry in sorted(leaderboard_data, key=lambda x: x["score"], reverse=True)[:10]: |
| 91 | + leaderboard_tree.insert("", "end", values=(entry["name"], entry["score"], entry["difficulty"])) |
| 92 | + |
| 93 | +# ========================= |
| 94 | +# Player Stats Functions |
| 95 | +# ========================= |
| 96 | +def load_stats(): |
| 97 | + global player_stats |
| 98 | + if os.path.exists(stats_file): |
| 99 | + try: |
| 100 | + with open(stats_file, "r", encoding="utf-8") as f: |
| 101 | + player_stats = json.load(f) |
| 102 | + except: |
| 103 | + player_stats = { |
| 104 | + "total_games": 0, |
| 105 | + "attempts_list": [], |
| 106 | + "high_score_streak": 0, |
| 107 | + "current_streak": 0 |
| 108 | + } |
| 109 | + else: |
| 110 | + player_stats = { |
| 111 | + "total_games": 0, |
| 112 | + "attempts_list": [], |
| 113 | + "high_score_streak": 0, |
| 114 | + "current_streak": 0 |
| 115 | + } |
| 116 | + |
| 117 | +def save_stats(): |
| 118 | + with open(stats_file, "w", encoding="utf-8") as f: |
| 119 | + json.dump(player_stats, f, indent=4) |
| 120 | + |
| 121 | +def update_stats_labels(): |
| 122 | + total_games_label.config(text=f"Total Games Played: {player_stats['total_games']}") |
| 123 | + avg_attempts = (sum(player_stats['attempts_list']) / len(player_stats['attempts_list'])) if player_stats['attempts_list'] else 0 |
| 124 | + avg_attempts_label.config(text=f"Average Attempts per Game: {avg_attempts:.2f}") |
| 125 | + high_streak_label.config(text=f"High Score Streak: {player_stats['high_score_streak']}") |
| 126 | + |
| 127 | +# ========================= |
| 128 | +# Difficulty & Game Logic |
| 129 | +# ========================= |
| 130 | +def set_difficulty(): |
| 131 | + global max_attempts, number_range |
| 132 | + diff = difficulty_var.get() |
| 133 | + if diff == "Easy": |
| 134 | + max_attempts = 15 |
| 135 | + number_range = (1, 50) |
| 136 | + elif diff == "Medium": |
| 137 | + max_attempts = 10 |
| 138 | + number_range = (1, 100) |
| 139 | + elif diff == "Hard": |
| 140 | + max_attempts = 5 |
| 141 | + number_range = (1, 200) |
| 142 | + start_game() |
| 143 | + |
| 144 | +def start_game(): |
| 145 | + global target_number, attempts |
| 146 | + target_number = random.randint(*number_range) |
| 147 | + attempts = 0 |
| 148 | + feedback_label.config(text="") |
| 149 | + attempts_label.config(text=f"Attempts left: {max_attempts}") |
| 150 | + guess_entry.delete(0, tk.END) |
| 151 | + set_status(f"New game! Guess a number between {number_range[0]} and {number_range[1]}.") |
| 152 | + |
| 153 | +def check_guess(): |
| 154 | + global attempts, score |
| 155 | + guess = guess_entry.get() |
| 156 | + if not guess.isdigit(): |
| 157 | + messagebox.showwarning("Invalid Input", "Please enter a valid number!") |
| 158 | + return |
| 159 | + guess = int(guess) |
| 160 | + attempts += 1 |
| 161 | + remaining = max_attempts - attempts |
| 162 | + diff = abs(target_number - guess) |
| 163 | + |
| 164 | + if guess < target_number: |
| 165 | + hint = "📉 Too low!" |
| 166 | + elif guess > target_number: |
| 167 | + hint = "📈 Too high!" |
| 168 | + else: |
| 169 | + hint = f"🎉 Correct! The number was {target_number}" |
| 170 | + points = max(0, 100 - attempts * 5) |
| 171 | + update_score(points) |
| 172 | + feedback_label.config(text=hint) |
| 173 | + save_score_dialog() |
| 174 | + return |
| 175 | + |
| 176 | + if diff <= 2: |
| 177 | + hint += " 🔥 Very close!" |
| 178 | + elif diff <= 5: |
| 179 | + hint += " 😊 Close!" |
| 180 | + |
| 181 | + feedback_label.config(text=hint) |
| 182 | + |
| 183 | + if remaining <= 0: |
| 184 | + feedback_label.config(text=f"💥 Game Over! The number was {target_number}") |
| 185 | + set_status("Game Over! Start a new game.") |
| 186 | + |
| 187 | + else: |
| 188 | + attempts_label.config(text=f"Attempts left: {remaining}") |
| 189 | + set_status(f"Attempts used: {attempts}, remaining: {remaining}") |
| 190 | + |
| 191 | +def update_score(points): |
| 192 | + global score |
| 193 | + score += points |
| 194 | + score_label.config(text=f"Score: {score}") |
| 195 | + |
| 196 | +# ========================= |
| 197 | +# Score Saving |
| 198 | +# ========================= |
| 199 | +def save_score_dialog(): |
| 200 | + def save_name(): |
| 201 | + name = name_entry.get().strip() |
| 202 | + if name: |
| 203 | + # Update leaderboard |
| 204 | + leaderboard_data.append({ |
| 205 | + "name": name, |
| 206 | + "score": score, |
| 207 | + "difficulty": difficulty_var.get() |
| 208 | + }) |
| 209 | + save_leaderboard() |
| 210 | + update_leaderboard_tree() |
| 211 | + # Update player stats |
| 212 | + player_stats['total_games'] += 1 |
| 213 | + player_stats['attempts_list'].append(attempts) |
| 214 | + # High score streak logic (score >= 80) |
| 215 | + if score >= 80: |
| 216 | + player_stats['current_streak'] += 1 |
| 217 | + player_stats['high_score_streak'] = max(player_stats['high_score_streak'], player_stats['current_streak']) |
| 218 | + else: |
| 219 | + player_stats['current_streak'] = 0 |
| 220 | + save_stats() |
| 221 | + update_stats_labels() |
| 222 | + dialog.destroy() |
| 223 | + else: |
| 224 | + messagebox.showwarning("Invalid Input", "Please enter your name.") |
| 225 | + |
| 226 | + dialog = tk.Toplevel(root) |
| 227 | + dialog.title("Save Score") |
| 228 | + dialog.geometry("300x150") |
| 229 | + ttk.Label(dialog, text=f"Congratulations! Score: {score}", font=("Segoe UI", 12)).pack(pady=10) |
| 230 | + ttk.Label(dialog, text="Enter your name:", font=("Segoe UI", 11)).pack(pady=5) |
| 231 | + name_entry = ttk.Entry(dialog, font=("Segoe UI", 12)) |
| 232 | + name_entry.pack(pady=5) |
| 233 | + ttk.Button(dialog, text="Save", command=save_name, style="Action.TButton").pack(pady=5) |
| 234 | + dialog.grab_set() |
| 235 | + |
| 236 | +# ========================= |
| 237 | +# Styles |
| 238 | +# ========================= |
| 239 | +style = ttk.Style() |
| 240 | +style.theme_use("clam") |
| 241 | +style.configure("Action.TButton", font=("Segoe UI", 11, "bold"), |
| 242 | + foreground="white", background="#4CAF50", padding=8) |
| 243 | +style.map("Action.TButton", background=[("active", "#45a049")]) |
| 244 | + |
| 245 | +# ========================= |
| 246 | +# Status Bar |
| 247 | +# ========================= |
| 248 | +status_var = tk.StringVar(value="Welcome to Number Guessing Game!") |
| 249 | +ttk.Label(root, textvariable=status_var, anchor="w", font=("Segoe UI", 10)).pack(side=tk.BOTTOM, fill="x") |
| 250 | + |
| 251 | +# ========================= |
| 252 | +# Notebook |
| 253 | +# ========================= |
| 254 | +tabs = ttk.Notebook(root) |
| 255 | +tabs.pack(expand=True, fill="both", padx=20, pady=20) |
| 256 | + |
| 257 | +# ========================= |
| 258 | +# Game Tab |
| 259 | +# ========================= |
| 260 | +game_tab = ttk.Frame(tabs, padding=20) |
| 261 | +tabs.add(game_tab, text="🎮 Play Game") |
| 262 | + |
| 263 | +# Difficulty |
| 264 | +ttk.Label(game_tab, text="Select Difficulty:", font=("Segoe UI", 12)).pack(pady=(5,2)) |
| 265 | +ttk.Combobox(game_tab, textvariable=difficulty_var, values=["Easy","Medium","Hard"], |
| 266 | + state="readonly", font=("Segoe UI", 12)).pack(pady=2) |
| 267 | +ttk.Button(game_tab, text="Apply Difficulty", command=set_difficulty, style="Action.TButton").pack(pady=5) |
| 268 | + |
| 269 | +# Guess input |
| 270 | +ttk.Label(game_tab, text="Enter your guess:", font=("Segoe UI", 12)).pack(pady=(10,5)) |
| 271 | +guess_entry = ttk.Entry(game_tab, font=("Segoe UI", 14)) |
| 272 | +guess_entry.pack(pady=5) |
| 273 | + |
| 274 | +# Feedback, attempts, score |
| 275 | +feedback_label = ttk.Label(game_tab, text="", font=("Segoe UI", 12, "bold")) |
| 276 | +feedback_label.pack(pady=10) |
| 277 | +attempts_label = ttk.Label(game_tab, text=f"Attempts left: {max_attempts}", font=("Segoe UI", 12)) |
| 278 | +attempts_label.pack(pady=5) |
| 279 | +score_label = ttk.Label(game_tab, text=f"Score: {score}", font=("Segoe UI", 12, "bold")) |
| 280 | +score_label.pack(pady=5) |
| 281 | + |
| 282 | +# Buttons |
| 283 | +ttk.Button(game_tab, text="Start New Game", command=start_game, style="Action.TButton").pack(pady=5) |
| 284 | +ttk.Button(game_tab, text="Submit Guess", command=check_guess, style="Action.TButton").pack(pady=5) |
| 285 | + |
| 286 | +# Dark Mode |
| 287 | +ttk.Checkbutton(game_tab, text="Dark Mode", variable=dark_mode_var, command=toggle_theme).pack(pady=10) |
| 288 | + |
| 289 | +# ========================= |
| 290 | +# Leaderboard Tab |
| 291 | +# ========================= |
| 292 | +leader_tab = ttk.Frame(tabs, padding=20) |
| 293 | +tabs.add(leader_tab, text="🏆 Leaderboard") |
| 294 | + |
| 295 | +leaderboard_tree = ttk.Treeview(leader_tab, columns=("Name", "Score", "Difficulty"), show="headings", height=15) |
| 296 | +leaderboard_tree.heading("Name", text="Name") |
| 297 | +leaderboard_tree.heading("Score", text="Score") |
| 298 | +leaderboard_tree.heading("Difficulty", text="Difficulty") |
| 299 | +leaderboard_tree.pack(fill="both", expand=True, pady=10) |
| 300 | + |
| 301 | +# ========================= |
| 302 | +# Stats Tab |
| 303 | +# ========================= |
| 304 | +stats_tab = ttk.Frame(tabs, padding=20) |
| 305 | +tabs.add(stats_tab, text="📊 Player Stats") |
| 306 | + |
| 307 | +total_games_label = ttk.Label(stats_tab, text="Total Games Played: 0", font=("Segoe UI", 12)) |
| 308 | +total_games_label.pack(pady=5) |
| 309 | +avg_attempts_label = ttk.Label(stats_tab, text="Average Attempts per Game: 0", font=("Segoe UI", 12)) |
| 310 | +avg_attempts_label.pack(pady=5) |
| 311 | +high_streak_label = ttk.Label(stats_tab, text="High Score Streak: 0", font=("Segoe UI", 12)) |
| 312 | +high_streak_label.pack(pady=5) |
| 313 | + |
| 314 | +# ========================= |
| 315 | +# Run App |
| 316 | +# ========================= |
| 317 | +load_leaderboard() |
| 318 | +load_stats() |
| 319 | +update_leaderboard_tree() |
| 320 | +update_stats_labels() |
| 321 | +set_difficulty() |
| 322 | +root.mainloop() |
0 commit comments