#!/usr/bin/env python3 """ User Profile Manager for Games A PySDL2-based profile management system with gamepad-only controls Features: - Create new user profiles - Edit existing profiles - Delete profiles - Select active profile - JSON-based storage - Gamepad navigation only """ import os import json import time from typing import Dict, List, Optional, Any from dataclasses import dataclass, asdict from datetime import datetime import sdl2 import sdl2.ext from sdl2.ext.compat import byteify @dataclass class UserProfile: """User profile data structure""" name: str created_date: str last_played: str games_played: int = 0 total_score: int = 0 best_score: int = 0 settings: Dict[str, Any] = None achievements: List[str] = None def __post_init__(self): if self.settings is None: self.settings = { "difficulty": "normal", "sound_volume": 50, "music_volume": 50, "screen_shake": True, "auto_save": True } if self.achievements is None: self.achievements = [] class ProfileManager: """Main profile management system""" def __init__(self, profiles_file: str = "user_profiles.json"): self.profiles_file = profiles_file self.profiles: Dict[str, UserProfile] = {} self.active_profile: Optional[str] = None # UI State self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile self.selected_index = 0 self.input_text = "" self.input_active = False self.editing_field = None # Virtual keyboard state self.vk_cursor_x = 0 # Virtual keyboard cursor X self.vk_cursor_y = 0 # Virtual keyboard cursor Y self.virtual_keyboard = [ ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], ['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'], ['U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3'], ['4', '5', '6', '7', '8', '9', ' ', '.', '-', '_'], [' 0: self.gamepad = sdl2.SDL_JoystickOpen(0) print(f"Gamepad detected: {sdl2.SDL_JoystickName(self.gamepad).decode()}") else: print("No gamepad detected - using keyboard fallback") def load_profiles(self): """Load profiles from JSON file""" if os.path.exists(self.profiles_file): try: with open(self.profiles_file, 'r') as f: data = json.load(f) self.profiles = { name: UserProfile(**profile_data) for name, profile_data in data.get('profiles', {}).items() } self.active_profile = data.get('active_profile') except (json.JSONDecodeError, KeyError) as e: print(f"Error loading profiles: {e}") self.profiles = {} def save_profiles(self): """Save profiles to JSON file""" data = { 'profiles': { name: asdict(profile) for name, profile in self.profiles.items() }, 'active_profile': self.active_profile } try: with open(self.profiles_file, 'w') as f: json.dump(data, f, indent=2) except IOError as e: print(f"Error saving profiles: {e}") def create_profile(self, name: str) -> bool: """Create a new profile""" if name in self.profiles or not name.strip(): return False now = datetime.now().isoformat() profile = UserProfile( name=name.strip(), created_date=now, last_played=now ) self.profiles[name.strip()] = profile self.save_profiles() return True def delete_profile(self, name: str) -> bool: """Delete a profile""" if name in self.profiles: del self.profiles[name] if self.active_profile == name: self.active_profile = None self.save_profiles() return True return False def set_active_profile(self, name: str) -> bool: """Set the active profile""" if name in self.profiles: self.active_profile = name self.profiles[name].last_played = datetime.now().isoformat() self.save_profiles() return True return False def handle_gamepad_input(self): """Handle gamepad input with debouncing""" if not self.gamepad: return current_time = time.time() # D-pad navigation hat_state = sdl2.SDL_JoystickGetHat(self.gamepad, 0) # Up if hat_state & sdl2.SDL_HAT_UP: if self.can_process_input('up', current_time): self.navigate_up() # Down if hat_state & sdl2.SDL_HAT_DOWN: if self.can_process_input('down', current_time): self.navigate_down() # Left if hat_state & sdl2.SDL_HAT_LEFT: if self.can_process_input('left', current_time): self.navigate_left() # Right if hat_state & sdl2.SDL_HAT_RIGHT: if self.can_process_input('right', current_time): self.navigate_right() # Buttons button_count = sdl2.SDL_JoystickNumButtons(self.gamepad) for i in range(min(button_count, 16)): # Limit to reasonable number if sdl2.SDL_JoystickGetButton(self.gamepad, i): if self.can_process_input(f'button_{i}', current_time): self.handle_button_press(i) def can_process_input(self, input_key: str, current_time: float) -> bool: """Check if enough time has passed to process input (debouncing)""" delay = 0.15 # 150ms delay if input_key not in self.last_button_time: self.last_button_time[input_key] = current_time return True if current_time - self.last_button_time[input_key] > delay: self.last_button_time[input_key] = current_time return True return False def handle_button_press(self, button: int): """Handle gamepad button presses""" # 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 self.handle_confirm() elif button == 1: # B/Back self.handle_back() elif button == 2: # X/Delete self.handle_delete() elif button == 3: # Y/Menu self.handle_menu() def navigate_up(self): """Navigate up in current screen""" if self.current_screen == "main_menu": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "profile_list": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "edit_profile": self.selected_index = max(0, self.selected_index - 1) elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation self.vk_cursor_y = max(0, self.vk_cursor_y - 1) # Adjust x cursor if current row is shorter max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(self.vk_cursor_x, max_x) def navigate_down(self): """Navigate down in current screen""" if self.current_screen == "main_menu": max_index = 3 # Create, Select, Settings, Exit (0-3) self.selected_index = min(max_index, self.selected_index + 1) elif self.current_screen == "profile_list": max_index = len(self.profiles) # Profiles + Back self.selected_index = min(max_index, self.selected_index + 1) 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 == "create_profile" and self.input_active: # Virtual keyboard navigation max_y = len(self.virtual_keyboard) - 1 self.vk_cursor_y = min(max_y, self.vk_cursor_y + 1) # Adjust x cursor if current row is shorter max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(self.vk_cursor_x, max_x) def navigate_left(self): """Navigate left (for adjusting values)""" if self.current_screen == "edit_profile": self.adjust_setting(-1) 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.current_screen == "edit_profile": self.adjust_setting(1) elif self.current_screen == "create_profile" and self.input_active: # Virtual keyboard navigation max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1) def handle_confirm(self): """Handle confirm action (A button)""" if self.current_screen == "main_menu": if self.selected_index == 0: # Create Profile self.current_screen = "create_profile" self.input_text = "" self.input_active = True elif self.selected_index == 1: # Select Profile self.current_screen = "profile_list" self.selected_index = 0 elif self.selected_index == 2: # Settings if self.active_profile: self.current_screen = "edit_profile" self.selected_index = 0 elif self.selected_index == 3: # Exit self.running = False elif self.current_screen == "profile_list": profile_names = list(self.profiles.keys()) if self.selected_index < len(profile_names): # Select profile profile_name = profile_names[self.selected_index] self.set_active_profile(profile_name) self.current_screen = "main_menu" self.selected_index = 0 else: # Back option self.current_screen = "main_menu" self.selected_index = 1 elif self.current_screen == "create_profile": if self.input_active: # Handle virtual keyboard selection self.handle_virtual_keyboard_input() else: # Start text input mode self.input_active = True self.vk_cursor_x = 0 self.vk_cursor_y = 0 elif self.current_screen == "edit_profile": if self.selected_index == 4: # Save (index 4) self.save_profiles() self.current_screen = "main_menu" self.selected_index = 0 elif self.selected_index == 5: # Back (index 5) self.current_screen = "main_menu" self.selected_index = 0 def handle_back(self): """Handle back action (B button)""" if self.current_screen == "main_menu": self.running = False elif self.current_screen in ["profile_list", "create_profile", "edit_profile"]: self.current_screen = "main_menu" self.selected_index = 0 self.input_active = False def handle_delete(self): """Handle delete action (X button)""" if self.current_screen == "profile_list": profile_names = list(self.profiles.keys()) if self.selected_index < len(profile_names): profile_name = profile_names[self.selected_index] self.delete_profile(profile_name) self.selected_index = min(self.selected_index, len(self.profiles) - 1) elif self.current_screen == "create_profile": # Delete character from input if self.input_text: self.input_text = self.input_text[:-1] def handle_menu(self): """Handle menu action (Y button)""" pass # Reserved for future use def handle_virtual_keyboard_input(self): """Handle virtual keyboard character selection""" if self.vk_cursor_y >= len(self.virtual_keyboard): return row = self.virtual_keyboard[self.vk_cursor_y] if self.vk_cursor_x >= len(row): return selected_char = row[self.vk_cursor_x] if selected_char == ' 5: color = sdl2.ext.Color(alpha, alpha, alpha * 2) self.renderer.draw_line((0, y, 640, y), color) if self.current_screen == "main_menu": self.render_main_menu() elif self.current_screen == "profile_list": self.render_profile_list() elif self.current_screen == "create_profile": self.render_create_profile() elif self.current_screen == "edit_profile": self.render_edit_profile() self.renderer.present() def render_main_menu(self): """Render main menu screen with improved layout""" # Draw background gradient effect self.renderer.fill((0, 0, 640, 120), self.colors['dark_gray']) # Title title = "Profile Manager" self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True) # Subtitle with active profile 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) # Menu panel panel_x, panel_y = 120, 140 panel_width, panel_height = 400, 220 self.draw_panel(panel_x, panel_y, panel_width, panel_height, self.colors['dark_gray'], self.colors['gray']) # Menu items with proper spacing menu_items = [ "Create Profile", "Select Profile", "Edit Settings", "Exit" ] button_width = 280 button_height = 35 button_x = panel_x + 60 start_y = panel_y + 20 for i, item in enumerate(menu_items): button_y = start_y + i * (button_height + 10) selected = (i == self.selected_index) self.draw_button(item, button_x, button_y, button_width, button_height, selected) # Controls help panel help_panel_y = 380 self.draw_panel(10, help_panel_y, 620, 80, self.colors['black'], self.colors['dark_gray']) self.draw_text("Controls:", 20, help_panel_y + 8, self.colors['light_blue'], 'small') self.draw_text("↑↓ Navigate Enter Confirm Escape Back", 20, help_panel_y + 30, self.colors['light_gray'], 'tiny') def render_profile_list(self): """Render profile selection screen with improved layout""" # Header self.renderer.fill((0, 0, 640, 70), self.colors['dark_gray']) self.draw_text("Select Profile", 320, 15, self.colors['light_blue'], 'title', center=True) profile_names = list(self.profiles.keys()) if not profile_names: # No profiles message self.draw_panel(120, 100, 400, 150, self.colors['dark_gray'], self.colors['gray']) self.draw_text("No profiles found", 320, 150, self.colors['yellow'], 'large', center=True) self.draw_text("Create one first", 320, 180, self.colors['light_gray'], 'medium', center=True) # Back button self.draw_button("← Back", 270, 300, 100, 30, True) else: # Profile list panel panel_height = min(280, len(profile_names) * 55 + 60) self.draw_panel(30, 80, 580, panel_height, self.colors['black'], self.colors['gray']) # Profile entries for i, name in enumerate(profile_names): profile = self.profiles[name] entry_y = 95 + i * 50 entry_selected = (i == self.selected_index) # Profile entry background entry_color = self.colors['blue'] if entry_selected else self.colors['dark_gray'] border_color = self.colors['light_blue'] if entry_selected else self.colors['gray'] self.draw_panel(40, entry_y, 560, 40, entry_color, border_color) # Profile name name_color = self.colors['white'] if entry_selected else self.colors['light_gray'] self.draw_text(name, 50, entry_y + 5, name_color, 'medium') # Profile stats (compact) stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}" stats_color = self.colors['light_gray'] if entry_selected else self.colors['gray'] self.draw_text(stats_text, 50, entry_y + 22, stats_color, 'tiny') # Active indicator if name == self.active_profile: self.draw_text("★", 580, entry_y + 12, self.colors['light_green'], 'small') # Back button back_y = 95 + len(profile_names) * 50 + 10 back_selected = (self.selected_index == len(profile_names)) self.draw_button("← Back", 270, back_y, 100, 30, back_selected) # Instructions self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray']) self.draw_text("Enter: Select • Escape: Back • Delete: Remove", 320, 435, self.colors['light_gray'], 'tiny', center=True) def render_create_profile(self): """Render profile creation screen with virtual keyboard""" # Header self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray']) self.draw_text("Create Profile", 320, 15, self.colors['light_blue'], 'title', center=True) # Input field input_x, input_y = 120, 90 input_width, input_height = 400, 30 input_bg = self.colors['white'] if self.input_active else self.colors['light_gray'] input_border = self.colors['blue'] if self.input_active else self.colors['gray'] self.draw_panel(input_x, input_y, input_width, input_height, input_bg, input_border, 2) # Input text display_text = self.input_text if self.input_text else "Profile Name" text_color = self.colors['black'] if self.input_text else self.colors['gray'] self.draw_text(display_text, input_x + 10, input_y + 8, text_color, 'medium') if self.input_active: # Virtual keyboard self.render_virtual_keyboard() else: # Start input instruction self.draw_text("Press Enter to start typing", 320, 150, self.colors['yellow'], 'medium', center=True) # Back button self.draw_button("← Back", 270, 200, 100, 30, True) # Instructions if not self.input_active: self.draw_panel(50, 400, 540, 60, self.colors['black'], self.colors['dark_gray']) self.draw_text("Enter: Start Input • Escape: Back", 320, 420, self.colors['light_gray'], 'small', center=True) def render_virtual_keyboard(self): """Render the virtual keyboard""" kb_start_x = 50 kb_start_y = 150 key_width = 45 key_height = 30 for row_idx, row in enumerate(self.virtual_keyboard): row_y = kb_start_y + row_idx * (key_height + 5) # Special handling for bottom row (commands) if row_idx == len(self.virtual_keyboard) - 1: # Bottom row with command keys key_widths = [80, 80, 80, 80] # Wider keys for commands x_offset = 140 # Center the bottom row else: key_widths = [key_width] * len(row) x_offset = kb_start_x current_x = x_offset for col_idx, char in enumerate(row): selected = (row_idx == self.vk_cursor_y and col_idx == self.vk_cursor_x) # Key background if selected: self.draw_panel(current_x, row_y, key_widths[col_idx], key_height, self.colors['blue'], self.colors['light_blue']) text_color = self.colors['white'] else: self.draw_panel(current_x, row_y, key_widths[col_idx], key_height, self.colors['dark_gray'], self.colors['gray']) text_color = self.colors['light_gray'] # Key text display_char = char if char == '