diff --git a/.gitignore b/.gitignore index 573ed1c..0024cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ rats.spec .env release/ unit/__pycache__ -conf/keybindings.yaml \ No newline at end of file +conf/keybindings.yaml +conf/keybindings.json \ No newline at end of file diff --git a/api.log b/api.log index 9249f7e..4acbf35 100644 --- a/api.log +++ b/api.log @@ -2089,3 +2089,37 @@ INFO: 127.0.0.1:52734 - "GET / HTTP/1.1" 200 OK INFO: 127.0.0.1:52740 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK INFO: 127.0.0.1:41304 - "GET / HTTP/1.1" 200 OK INFO: 127.0.0.1:41308 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:33472 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:33474 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:52616 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:35690 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:35706 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:35722 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:35724 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:35736 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:57592 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:57608 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:44530 - "POST /signup/DEV-3A2D87B7/AA HTTP/1.1" 200 OK +INFO: 127.0.0.1:37822 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:37834 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:46744 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:46752 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:43292 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:43296 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:43300 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:43310 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK +INFO: 127.0.0.1:59822 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:59832 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:36158 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:36170 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:40986 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:40990 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:39030 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:39046 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:38630 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK +INFO: 127.0.0.1:38640 - "POST /signup/DEV-3A2D87B7/Alice HTTP/1.1" 409 Conflict +INFO: 127.0.0.1:38490 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK +INFO: Shutting down +INFO: Waiting for application shutdown. +INFO: Application shutdown complete. +INFO: Finished server process [41455] diff --git a/score_api_client.py b/engine/score_api_client.py similarity index 100% rename from score_api_client.py rename to engine/score_api_client.py diff --git a/user_profile_integration.py b/engine/user_profile_integration.py similarity index 99% rename from user_profile_integration.py rename to engine/user_profile_integration.py index 2af9c90..63c540c 100644 --- a/user_profile_integration.py +++ b/engine/user_profile_integration.py @@ -9,7 +9,7 @@ import uuid import platform import hashlib from datetime import datetime -from score_api_client import ScoreAPIClient +from engine.score_api_client import ScoreAPIClient class UserProfileIntegration: diff --git a/profile_manager.py b/profile_manager.py index ca1bb59..029f6ee 100644 --- a/profile_manager.py +++ b/profile_manager.py @@ -23,6 +23,9 @@ import sdl2 import sdl2.ext from sdl2.ext.compat import byteify +# Import the user profile integration system +from engine.user_profile_integration import UserProfileIntegration + @dataclass class UserProfile: @@ -57,13 +60,27 @@ class ProfileManager: self.profiles: Dict[str, UserProfile] = {} self.active_profile: Optional[str] = None + # Initialize user profile integration system + self.integration = UserProfileIntegration(profiles_file) + self.device_id = self.integration.get_device_id() + self.api_enabled = self.integration.api_enabled + # UI State - self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile + self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile, leaderboard, profile_stats self.selected_index = 0 self.input_text = "" self.input_active = False self.editing_field = None + # Error dialog state + self.show_error = False + self.error_message = "" + self.error_timer = 0 + + # Leaderboard data + self.leaderboard_data = [] + self.leaderboard_type = "device" # "device" or "global" + # Virtual keyboard state self.vk_cursor_x = 0 # Virtual keyboard cursor X self.vk_cursor_y = 0 # Virtual keyboard cursor Y @@ -191,6 +208,15 @@ class ProfileManager: self.profiles[name.strip()] = profile self.save_profiles() + + # Register with API server if available + if self.api_enabled: + result = self.integration.register_new_user(name.strip()) + if result: + print(f"Profile {name.strip()} registered with server") + else: + print(f"Warning: Profile {name.strip()} created locally but not registered with server") + return True def delete_profile(self, name: str) -> bool: @@ -209,6 +235,10 @@ class ProfileManager: self.active_profile = name self.profiles[name].last_played = datetime.now().isoformat() self.save_profiles() + + # Update integration system to load the new profile + self.integration.reload_profile() + return True return False @@ -264,23 +294,31 @@ class ProfileManager: def handle_button_press(self, button: int): """Handle gamepad button presses""" + # If error dialog is shown, dismiss it on any button press + if self.show_error: + self.show_error = False + return + # Button mappings (common gamepad layout) # 0: A/Cross - Confirm # 1: B/Circle - Back # 2: X/Square - Delete/Special # 3: Y/Triangle - Menu - if button == 0: # A/Confirm + if button == 3: # A/Confirm self.handle_confirm() - elif button == 1: # B/Back + elif button == 4: # B/Back self.handle_back() - elif button == 2: # X/Delete + elif button == 9: # X/Delete self.handle_delete() - elif button == 3: # Y/Menu + elif button == 10: # Y/Menu self.handle_menu() def navigate_up(self): """Navigate up in current screen""" + if self.show_error: + return + if self.current_screen == "main_menu": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "profile_list": @@ -296,8 +334,11 @@ class ProfileManager: def navigate_down(self): """Navigate down in current screen""" + if self.show_error: + return + if self.current_screen == "main_menu": - max_index = 3 # Create, Select, Settings, Exit (0-3) + max_index = 5 # Create, Select, Settings, Leaderboard, Stats, Exit (0-5) self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "profile_list": max_index = len(self.profiles) # Profiles + Back @@ -305,6 +346,12 @@ class ProfileManager: elif self.current_screen == "edit_profile": max_index = 5 # 4 settings + save + back (0-5) self.selected_index = min(max_index, self.selected_index + 1) + elif self.current_screen == "leaderboard": + max_index = 2 # Toggle type, refresh, back (0-2) + self.selected_index = min(max_index, self.selected_index + 1) + elif self.current_screen == "profile_stats": + max_index = 0 # Just back button + self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation max_y = len(self.virtual_keyboard) - 1 @@ -315,16 +362,32 @@ class ProfileManager: def navigate_left(self): """Navigate left (for adjusting values)""" + if self.show_error: + return + if self.current_screen == "edit_profile": self.adjust_setting(-1) + elif self.current_screen == "leaderboard": + # Toggle between device and global leaderboard + if self.selected_index == 0: + self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device" + self.load_leaderboard_data() elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation self.vk_cursor_x = max(0, self.vk_cursor_x - 1) def navigate_right(self): """Navigate right (for adjusting values)""" + if self.show_error: + return + if self.current_screen == "edit_profile": self.adjust_setting(1) + elif self.current_screen == "leaderboard": + # Toggle between device and global leaderboard + if self.selected_index == 0: + self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device" + self.load_leaderboard_data() elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 @@ -332,6 +395,9 @@ class ProfileManager: def handle_confirm(self): """Handle confirm action (A button)""" + if self.show_error: + return + if self.current_screen == "main_menu": if self.selected_index == 0: # Create Profile self.current_screen = "create_profile" @@ -344,7 +410,16 @@ class ProfileManager: if self.active_profile: self.current_screen = "edit_profile" self.selected_index = 0 - elif self.selected_index == 3: # Exit + elif self.selected_index == 3: # Leaderboard + if self.api_enabled: + self.current_screen = "leaderboard" + self.selected_index = 0 + self.load_leaderboard_data() + elif self.selected_index == 4: # Profile Stats + if self.active_profile: + self.current_screen = "profile_stats" + self.selected_index = 0 + elif self.selected_index == 5: # Exit self.running = False elif self.current_screen == "profile_list": @@ -377,18 +452,36 @@ class ProfileManager: elif self.selected_index == 5: # Back (index 5) self.current_screen = "main_menu" self.selected_index = 0 + + elif self.current_screen == "leaderboard": + if self.selected_index == 1: # Refresh + self.load_leaderboard_data() + elif self.selected_index == 2: # Back + self.current_screen = "main_menu" + self.selected_index = 3 + + elif self.current_screen == "profile_stats": + if self.selected_index == 0: # Back + self.current_screen = "main_menu" + self.selected_index = 4 def handle_back(self): """Handle back action (B button)""" + if self.show_error: + return + if self.current_screen == "main_menu": self.running = False - elif self.current_screen in ["profile_list", "create_profile", "edit_profile"]: + elif self.current_screen in ["profile_list", "create_profile", "edit_profile", "leaderboard", "profile_stats"]: self.current_screen = "main_menu" self.selected_index = 0 self.input_active = False def handle_delete(self): """Handle delete action (X button)""" + if self.show_error: + return + if self.current_screen == "profile_list": profile_names = list(self.profiles.keys()) if self.selected_index < len(profile_names): @@ -426,10 +519,18 @@ class ProfileManager: elif selected_char == 'DONE': # Finish input if self.input_text.strip(): - if self.create_profile(self.input_text): - self.current_screen = "main_menu" - self.selected_index = 0 - self.input_active = False + # Check if profile already exists + if self.input_text.strip() in self.profiles: + self.show_error_dialog(f"Profile '{self.input_text.strip()}' already exists!") + else: + if self.create_profile(self.input_text): + self.current_screen = "main_menu" + self.selected_index = 0 + self.input_active = False + else: + self.show_error_dialog("Failed to create profile!") + else: + self.show_error_dialog("Profile name cannot be empty!") elif selected_char == 'CANCEL': # Cancel input self.input_text = "" @@ -461,6 +562,27 @@ class ProfileManager: elif self.selected_index == 3: # Screen Shake profile.settings["screen_shake"] = not profile.settings["screen_shake"] + def load_leaderboard_data(self): + """Load leaderboard data based on current type""" + if not self.api_enabled: + self.leaderboard_data = [] + return + + try: + if self.leaderboard_type == "device": + self.leaderboard_data = self.integration.get_device_leaderboard(10) + else: # global + self.leaderboard_data = self.integration.get_global_leaderboard(10) + except Exception as e: + print(f"Error loading leaderboard: {e}") + self.leaderboard_data = [] + + def show_error_dialog(self, message: str): + """Show an error dialog with the given message""" + self.show_error = True + self.error_message = message + self.error_timer = time.time() + 3.0 # Show error for 3 seconds + def render(self): """Main rendering method""" # Clear with dark background @@ -481,6 +603,17 @@ class ProfileManager: self.render_create_profile() elif self.current_screen == "edit_profile": self.render_edit_profile() + elif self.current_screen == "leaderboard": + self.render_leaderboard() + elif self.current_screen == "profile_stats": + self.render_profile_stats() + + # Render error dialog on top if active + if self.show_error: + self.render_error_dialog() + # Auto-dismiss after timer expires + if time.time() > self.error_timer: + self.show_error = False self.renderer.present() @@ -493,16 +626,25 @@ class ProfileManager: title = "Profile Manager" self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True) - # Subtitle with active profile + # Subtitle with active profile and API status if self.active_profile: active_text = f"Active: {self.active_profile}" self.draw_text(active_text, 320, 60, self.colors['light_green'], 'medium', center=True) else: self.draw_text("No active profile", 320, 60, self.colors['yellow'], 'medium', center=True) + # API connection status + api_status = "API: Connected" if self.api_enabled else "API: Offline" + api_color = self.colors['light_green'] if self.api_enabled else self.colors['red'] + self.draw_text(api_status, 320, 80, api_color, 'tiny', center=True) + + # Device ID display + device_text = f"Device: {self.device_id}" + self.draw_text(device_text, 320, 95, self.colors['light_gray'], 'tiny', center=True) + # Menu panel - panel_x, panel_y = 120, 140 - panel_width, panel_height = 400, 220 + panel_x, panel_y = 120, 120 + panel_width, panel_height = 400, 240 self.draw_panel(panel_x, panel_y, panel_width, panel_height, self.colors['dark_gray'], self.colors['gray']) @@ -511,18 +653,27 @@ class ProfileManager: "Create Profile", "Select Profile", "Edit Settings", + "Leaderboard", + "Profile Stats", "Exit" ] button_width = 280 - button_height = 35 + button_height = 30 button_x = panel_x + 60 - start_y = panel_y + 20 + start_y = panel_y + 15 for i, item in enumerate(menu_items): - button_y = start_y + i * (button_height + 10) + button_y = start_y + i * (button_height + 8) selected = (i == self.selected_index) - self.draw_button(item, button_x, button_y, button_width, button_height, selected) + + # Gray out unavailable options + if (item == "Edit Settings" or item == "Profile Stats") and not self.active_profile: + self.draw_button(f"{item} (No Profile)", button_x, button_y, button_width, button_height, False, disabled=True) + elif item == "Leaderboard" and not self.api_enabled: + self.draw_button(f"{item} (Offline)", button_x, button_y, button_width, button_height, False, disabled=True) + else: + self.draw_button(item, button_x, button_y, button_width, button_height, selected) # Controls help panel help_panel_y = 380 @@ -748,6 +899,219 @@ class ProfileManager: self.draw_text("Left/Right: Adjust • Enter: Save/Back • Escape: Cancel", 320, 435, self.colors['light_gray'], 'tiny', center=True) + def render_leaderboard(self): + """Render leaderboard screen""" + # Header + self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) + + title = f"{self.leaderboard_type.title()} Leaderboard" + self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True) + + if not self.api_enabled: + self.draw_text("Server offline - No leaderboard available", 320, 50, + self.colors['red'], 'medium', center=True) + self.draw_button("← Back", 270, 200, 100, 30, True) + return + + # Toggle button + toggle_text = f"Type: {self.leaderboard_type.title()} (Left/Right to toggle)" + toggle_selected = (self.selected_index == 0) + self.draw_button(toggle_text, 120, 90, 400, 25, toggle_selected) + + # Refresh button + refresh_selected = (self.selected_index == 1) + self.draw_button("Refresh", 150, 125, 80, 25, refresh_selected) + + # Back button + back_selected = (self.selected_index == 2) + self.draw_button("← Back", 250, 125, 80, 25, back_selected) + + # Leaderboard data + if not self.leaderboard_data: + self.draw_text("No leaderboard data available", 320, 200, + self.colors['yellow'], 'medium', center=True) + else: + # Leaderboard panel + panel_height = min(250, len(self.leaderboard_data) * 25 + 40) + self.draw_panel(50, 160, 540, panel_height, + self.colors['black'], self.colors['gray']) + + # Headers + self.draw_text("Rank", 60, 175, self.colors['light_blue'], 'small') + self.draw_text("Player", 120, 175, self.colors['light_blue'], 'small') + self.draw_text("Score", 350, 175, self.colors['light_blue'], 'small') + self.draw_text("Games", 450, 175, self.colors['light_blue'], 'small') + + if self.leaderboard_type == "global": + self.draw_text("Device", 520, 175, self.colors['light_blue'], 'small') + + # Leaderboard entries + for i, entry in enumerate(self.leaderboard_data): + entry_y = 195 + i * 22 + + # Highlight current user + is_current_user = (self.active_profile and + entry.get('user_id') == self.active_profile) + text_color = self.colors['light_green'] if is_current_user else self.colors['light_gray'] + + # Rank + self.draw_text(str(entry.get('rank', i+1)), 60, entry_y, text_color, 'tiny') + + # Player name + player_name = entry.get('user_id', 'Unknown') + if len(player_name) > 20: + player_name = player_name[:17] + "..." + self.draw_text(player_name, 120, entry_y, text_color, 'tiny') + + # Score + score = entry.get('best_score', 0) + self.draw_text(str(score), 350, entry_y, text_color, 'tiny') + + # Games + games = entry.get('total_games', 0) + self.draw_text(str(games), 450, entry_y, text_color, 'tiny') + + # Device (global leaderboard only) + if self.leaderboard_type == "global": + device = entry.get('device_id', '')[:8] + self.draw_text(device, 520, entry_y, text_color, 'tiny') + + # Instructions + self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) + self.draw_text("Left/Right: Toggle Type • Enter: Select • Escape: Back", + 320, 435, self.colors['light_gray'], 'tiny', center=True) + + def render_profile_stats(self): + """Render detailed profile statistics""" + if not self.active_profile: + self.current_screen = "main_menu" + return + + profile = self.profiles[self.active_profile] + + # Header + self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) + title = f"Stats: {profile.name}" + self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True) + + # Get additional info from integration system + integration_info = self.integration.get_profile_info() + + # Main stats panel + self.draw_panel(50, 90, 540, 260, self.colors['dark_gray'], self.colors['gray']) + + # Basic stats + stats_y = 105 + line_height = 20 + + self.draw_text("Profile Statistics", 320, stats_y, self.colors['white'], 'large', center=True) + stats_y += 30 + + # Local stats + self.draw_text("Local Statistics:", 60, stats_y, self.colors['light_blue'], 'medium') + stats_y += line_height + + self.draw_text(f"Games Played: {profile.games_played}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + self.draw_text(f"Best Score: {profile.best_score}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + self.draw_text(f"Total Score: {profile.total_score}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + self.draw_text(f"Achievements: {len(profile.achievements)}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + # Creation and last played dates + try: + created = datetime.fromisoformat(profile.created_date).strftime("%Y-%m-%d") + last_played = datetime.fromisoformat(profile.last_played).strftime("%Y-%m-%d") + except: + created = "Unknown" + last_played = "Unknown" + + self.draw_text(f"Created: {created}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + self.draw_text(f"Last Played: {last_played}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + # Server stats (if available) + if self.api_enabled and integration_info: + stats_y += 10 + self.draw_text("Server Status:", 60, stats_y, self.colors['light_green'], 'medium') + stats_y += line_height + + self.draw_text(f"Device ID: {integration_info.get('device_id', 'Unknown')}", 70, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + self.draw_text("✓ Profile synced with server", 70, stats_y, self.colors['light_green'], 'small') + else: + stats_y += 10 + self.draw_text("Server Status:", 60, stats_y, self.colors['red'], 'medium') + stats_y += line_height + self.draw_text("✗ Server offline", 70, stats_y, self.colors['red'], 'small') + + # Settings summary + stats_y = 105 + settings_x = 350 + + self.draw_text("Current Settings:", settings_x, stats_y + 30, self.colors['light_blue'], 'medium') + stats_y += 50 + + difficulty = profile.settings.get('difficulty', 'normal').title() + self.draw_text(f"Difficulty: {difficulty}", settings_x + 10, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + sound_vol = profile.settings.get('sound_volume', 50) + self.draw_text(f"Sound Volume: {sound_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + music_vol = profile.settings.get('music_volume', 50) + self.draw_text(f"Music Volume: {music_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small') + stats_y += line_height + + screen_shake = "On" if profile.settings.get('screen_shake', True) else "Off" + self.draw_text(f"Screen Shake: {screen_shake}", settings_x + 10, stats_y, self.colors['light_gray'], 'small') + + # Back button + back_selected = (self.selected_index == 0) + self.draw_button("← Back", 270, 370, 100, 30, back_selected) + + # Instructions + self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) + self.draw_text("Enter: Back • Escape: Back to Main Menu", + 320, 435, self.colors['light_gray'], 'tiny', center=True) + + def render_error_dialog(self): + """Render error dialog overlay""" + # Semi-transparent overlay + overlay_color = sdl2.ext.Color(0, 0, 0, 128) # Black with alpha + self.renderer.fill((0, 0, 640, 480), overlay_color) + + # Error dialog box + dialog_width = 400 + dialog_height = 120 + dialog_x = (640 - dialog_width) // 2 + dialog_y = (480 - dialog_height) // 2 + + # Dialog background with red border + self.draw_panel(dialog_x, dialog_y, dialog_width, dialog_height, + self.colors['black'], self.colors['red'], border_width=4) + + # Error title + self.draw_text("ERROR", dialog_x + dialog_width // 2, dialog_y + 20, + self.colors['red'], 'large', center=True) + + # Error message + self.draw_text(self.error_message, dialog_x + dialog_width // 2, dialog_y + 50, + self.colors['white'], 'medium', center=True) + + # Dismiss instruction + self.draw_text("Press any key to continue...", dialog_x + dialog_width // 2, dialog_y + 80, + self.colors['light_gray'], 'small', center=True) + def draw_text(self, text: str, x: int, y: int, color, font_type='medium', center: bool = False): """Draw text on screen with improved styling""" if not text: @@ -774,10 +1138,14 @@ class ProfileManager: for i in range(border_width): self.renderer.draw_rect((x + i, y + i, width - 2*i, height - 2*i), border_color) - def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False): + def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False, disabled: bool = False): """Draw a styled button""" # Button colors based on state - if selected: + if disabled: + bg_color = self.colors['black'] + border_color = self.colors['dark_gray'] + text_color = self.colors['dark_gray'] + elif selected: bg_color = self.colors['blue'] border_color = self.colors['light_blue'] text_color = self.colors['white'] @@ -798,6 +1166,11 @@ class ProfileManager: """Handle keyboard input for navigation only (no text input)""" key = event.key.keysym.sym + # If error dialog is shown, dismiss it on any key press + if self.show_error: + self.show_error = False + return + # Navigation mode only if key == sdl2.SDLK_UP: self.navigate_up() @@ -852,22 +1225,34 @@ class ProfileManager: def main(): """Entry point""" - print("Starting Profile Manager...") + print("Starting Profile Manager with User Profile Integration...") + print("Features:") + print(" • Profile creation and management") + print(" • API server integration for leaderboards") + print(" • Device-specific and global leaderboards") + print(" • Detailed profile statistics") + print(" • Settings management") + print("") print("Controls:") print(" Arrow Keys: Navigate menus") print(" Enter/Space: Confirm/Select") print(" Escape: Back/Cancel") print(" Delete/Backspace: Delete") print(" Tab: Menu (reserved)") - print(" Keyboard typing: Text input when creating profiles") + print(" Left/Right: Adjust settings and toggle options") print("") print("Gamepad Controls (if connected):") print(" D-Pad: Navigate menus") print(" A Button: Confirm/Select") print(" B Button: Back/Cancel") print(" X Button: Delete") + print("") manager = ProfileManager() + print(f"Device ID: {manager.device_id}") + print(f"API Server: {'Connected' if manager.api_enabled else 'Offline'}") + print("") + manager.run() diff --git a/rats.py b/rats.py index 4fa8fc0..d56e961 100644 --- a/rats.py +++ b/rats.py @@ -6,7 +6,7 @@ import json from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring from units import points -from user_profile_integration import UserProfileIntegration, get_global_leaderboard +from engine.user_profile_integration import UserProfileIntegration, get_global_leaderboard class MiceMaze( diff --git a/api_requirements.txt b/server/api_requirements.txt similarity index 100% rename from api_requirements.txt rename to server/api_requirements.txt diff --git a/mice_game.db b/server/mice_game.db similarity index 97% rename from mice_game.db rename to server/mice_game.db index 0789933..e586eae 100644 Binary files a/mice_game.db and b/server/mice_game.db differ diff --git a/score_api.py b/server/score_api.py similarity index 100% rename from score_api.py rename to server/score_api.py diff --git a/simple_profile_demo.py b/simple_profile_demo.py deleted file mode 100644 index 7773cec..0000000 --- a/simple_profile_demo.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple Profile Manager with API Integration Demo -""" - -import json -from user_profile_integration import UserProfileIntegration - -def main(): - print("=== Profile Manager with API Integration ===") - - # Initialize the integration - integration = UserProfileIntegration() - - print(f"\\nDevice ID: {integration.get_device_id()}") - print(f"Active Profile: {integration.get_profile_name()}") - print(f"API Server: {'Connected' if integration.api_enabled else 'Offline'}") - - while True: - print("\\n=== MAIN MENU ===") - print("1. Show Profile Info") - print("2. View Online Leaderboard") - print("3. View All Device Users") - print("4. Submit Test Score") - print("5. Create New Profile") - print("6. Exit") - - try: - choice = input("\\nSelect option (1-6): ").strip() - - if choice == "1": - # Show profile info - info = integration.get_profile_info() - if info: - print(f"\\n=== PROFILE INFO ===") - for key, value in info.items(): - print(f"{key.replace('_', ' ').title()}: {value}") - else: - print("No active profile loaded") - - elif choice == "2": - # Show leaderboard - if not integration.api_enabled: - print("API server not available") - continue - - print(f"\\n=== LEADERBOARD ===") - leaderboard = integration.get_device_leaderboard(10) - if leaderboard: - for entry in leaderboard: - print(f"{entry['rank']}. {entry['user_id']}: {entry['best_score']} pts ({entry['total_games']} games)") - else: - print("No scores recorded yet") - - elif choice == "3": - # Show all users - if not integration.api_enabled: - print("API server not available") - continue - - print(f"\\n=== DEVICE USERS ===") - users = integration.get_all_device_users() - if users: - for user in users: - print(f"{user['user_id']}: Best {user['best_score']}, {user['total_scores']} games") - else: - print("No users registered yet") - - elif choice == "4": - # Submit test score - if not integration.current_profile: - print("No active profile to submit score for") - continue - - try: - score = int(input("Enter test score: ")) - result = integration.update_game_stats(score, True) - if result: - print(f"Score {score} submitted successfully!") - else: - print("Failed to submit score") - except ValueError: - print("Invalid score entered") - - elif choice == "5": - # Create new profile - simplified for demo - name = input("Enter new profile name: ").strip() - if not name: - print("Name cannot be empty") - continue - - success = integration.register_new_user(name) - if success or not integration.api_enabled: - print(f"Profile '{name}' created successfully!") - else: - print("Failed to create profile") - - elif choice == "6": - print("Goodbye!") - break - - else: - print("Invalid choice") - - except KeyboardInterrupt: - print("\\nGoodbye!") - break - except Exception as e: - print(f"Error: {e}") - -if __name__ == "__main__": - main() diff --git a/test_score_api.py b/test_score_api.py deleted file mode 100644 index 2399131..0000000 --- a/test_score_api.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script for Mice Game Score API -""" - -import requests -import json -import time - -API_BASE_URL = "http://localhost:8000" - -def test_api(): - """Test all API endpoints""" - print("Testing Mice Game Score API...") - print("=" * 50) - - # Test device and user IDs - device_id = "DEV-TEST001" - user1 = "TestUser1" - user2 = "TestUser2" - - try: - # 1. Test root endpoint - print("\n1. Testing root endpoint:") - response = requests.get(f"{API_BASE_URL}/") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 2. Test user signup - print(f"\n2. Testing user signup for {user1}:") - response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user1}") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - print(f"\n3. Testing user signup for {user2}:") - response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user2}") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 4. Test duplicate signup (should fail) - print(f"\n4. Testing duplicate signup for {user1} (should fail):") - response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user1}") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 5. Test getting users for device - print(f"\n5. Testing get users for device {device_id}:") - response = requests.get(f"{API_BASE_URL}/users/{device_id}") - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 6. Test score submission - print(f"\n6. Testing score submission for {user1}:") - score_data = { - "user_id": user1, - "device_id": device_id, - "score": 1500, - "game_completed": True - } - response = requests.post( - f"{API_BASE_URL}/score/{device_id}/{user1}", - json=score_data - ) - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 7. Test more score submissions - scores_to_test = [ - (user1, 2000, True), - (user1, 1200, False), - (user2, 1800, True), - (user2, 2500, True) - ] - - print(f"\n7. Testing multiple score submissions:") - for user, score, completed in scores_to_test: - score_data = { - "user_id": user, - "device_id": device_id, - "score": score, - "game_completed": completed - } - response = requests.post( - f"{API_BASE_URL}/score/{device_id}/{user}", - json=score_data - ) - print(f" {user} - Score: {score}, Completed: {completed} -> Status: {response.status_code}") - time.sleep(0.1) # Small delay between requests - - # 8. Test score submission for non-registered user (should fail) - print(f"\n8. Testing score submission for non-registered user (should fail):") - score_data = { - "user_id": "NonExistentUser", - "device_id": device_id, - "score": 1000, - "game_completed": True - } - response = requests.post( - f"{API_BASE_URL}/score/{device_id}/NonExistentUser", - json=score_data - ) - print(f"Status: {response.status_code}") - print(f"Response: {response.json()}") - - # 9. Test getting user scores - print(f"\n9. Testing get scores for {user1}:") - response = requests.get(f"{API_BASE_URL}/scores/{device_id}/{user1}") - print(f"Status: {response.status_code}") - scores = response.json() - print(f"Number of scores: {len(scores)}") - for score in scores: - print(f" Score: {score['score']}, Completed: {score['game_completed']}, Time: {score['timestamp']}") - - # 10. Test leaderboard - print(f"\n10. Testing leaderboard for device {device_id}:") - response = requests.get(f"{API_BASE_URL}/leaderboard/{device_id}") - print(f"Status: {response.status_code}") - leaderboard = response.json() - print("Leaderboard:") - for entry in leaderboard: - print(f" Rank {entry['rank']}: {entry['user_id']} - Best Score: {entry['best_score']} ({entry['total_games']} games)") - - # 11. Test final user list - print(f"\n11. Final user list for device {device_id}:") - response = requests.get(f"{API_BASE_URL}/users/{device_id}") - users = response.json() - for user in users: - print(f" {user['user_id']}: Best Score: {user['best_score']}, Total Games: {user['total_scores']}") - - print("\n" + "=" * 50) - print("API Testing completed successfully!") - - except requests.exceptions.ConnectionError: - print("ERROR: Could not connect to API server.") - print("Make sure the API server is running with: python score_api.py") - except Exception as e: - print(f"ERROR: {e}") - -if __name__ == "__main__": - test_api() diff --git a/user_profiles.json b/user_profiles.json index 9c75de1..fe5d1d4 100644 --- a/user_profiles.json +++ b/user_profiles.json @@ -3,7 +3,7 @@ "Player1": { "name": "Player1", "created_date": "2024-01-15T10:30:00", - "last_played": "2024-01-20T14:45:00", + "last_played": "2025-08-21T17:57:06.304649", "games_played": 25, "total_score": 15420, "best_score": 980, @@ -23,7 +23,7 @@ "Alice": { "name": "Alice", "created_date": "2024-01-10T09:15:00", - "last_played": "2025-08-21T13:00:29.825909", + "last_played": "2025-08-21T17:39:28.013833", "games_played": 43, "total_score": 33000, "best_score": 1250, @@ -58,7 +58,39 @@ "auto_save": true }, "achievements": [] + }, + "AA": { + "name": "AA", + "created_date": "2025-08-21T17:12:50.321070", + "last_played": "2025-08-21T17:12:50.321070", + "games_played": 0, + "total_score": 0, + "best_score": 0, + "settings": { + "difficulty": "normal", + "sound_volume": 50, + "music_volume": 50, + "screen_shake": true, + "auto_save": true + }, + "achievements": [] + }, + "B0B": { + "name": "B0B", + "created_date": "2025-08-21T18:03:12.189612", + "last_played": "2025-08-21T18:04:57.426796", + "games_played": 0, + "total_score": 0, + "best_score": 0, + "settings": { + "difficulty": "normal", + "sound_volume": 50, + "music_volume": 50, + "screen_shake": true, + "auto_save": true + }, + "achievements": [] } }, - "active_profile": "MAT" + "active_profile": "B0B" } \ No newline at end of file