You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1260 lines
51 KiB

#!/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
# Import the user profile integration system
from engine.user_profile_integration import UserProfileIntegration
@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
# 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, 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
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', ' ', '.', '-', '_'],
['<DEL', 'SPACE', 'DONE', 'CANCEL']
]
# SDL2 Setup
self.window = None
self.renderer = None
self.font_manager = None
self.running = True
# Gamepad state
self.gamepad = None
self.button_states = {}
self.last_button_time = {}
# Colors
self.colors = {
'white': sdl2.ext.Color(255, 255, 255),
'black': sdl2.ext.Color(0, 0, 0),
'gray': sdl2.ext.Color(128, 128, 128),
'light_gray': sdl2.ext.Color(200, 200, 200),
'dark_gray': sdl2.ext.Color(64, 64, 64),
'blue': sdl2.ext.Color(70, 130, 200),
'light_blue': sdl2.ext.Color(120, 180, 255),
'green': sdl2.ext.Color(50, 200, 50),
'light_green': sdl2.ext.Color(100, 255, 100),
'red': sdl2.ext.Color(200, 50, 50),
'yellow': sdl2.ext.Color(255, 220, 0),
'orange': sdl2.ext.Color(255, 165, 0),
'purple': sdl2.ext.Color(150, 50, 200)
}
self.load_profiles()
def init_sdl(self):
"""Initialize SDL2 components"""
sdl2.ext.init(joystick=True)
# Create window
self.window = sdl2.ext.Window(
title="Profile Manager",
size=(640, 480)
)
self.window.show()
# Create renderer
self.renderer = sdl2.ext.Renderer(
self.window,
flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC
)
# Initialize font system - create multiple sizes
font_path = "assets/decterm.ttf" if os.path.exists("assets/decterm.ttf") else None
self.fonts = {
'title': sdl2.ext.FontManager(font_path=font_path, size=36),
'large': sdl2.ext.FontManager(font_path=font_path, size=28),
'medium': sdl2.ext.FontManager(font_path=font_path, size=22),
'small': sdl2.ext.FontManager(font_path=font_path, size=18),
'tiny': sdl2.ext.FontManager(font_path=font_path, size=14)
}
# Initialize gamepad
self.init_gamepad()
def init_gamepad(self):
"""Initialize gamepad support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER)
num_joysticks = sdl2.SDL_NumJoysticks()
if num_joysticks > 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()
# 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:
"""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()
# Update integration system to load the new profile
self.integration.reload_profile()
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"""
# 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 == 3: # A/Confirm
self.handle_confirm()
elif button == 4: # B/Back
self.handle_back()
elif button == 9: # X/Delete
self.handle_delete()
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":
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.show_error:
return
if self.current_screen == "main_menu":
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
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 == "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
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.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
self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1)
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"
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: # 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":
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
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", "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):
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 == '<DEL':
# Delete last character
if self.input_text:
self.input_text = self.input_text[:-1]
elif selected_char == 'SPACE':
# Add space
if len(self.input_text) < 20:
self.input_text += ' '
elif selected_char == 'DONE':
# Finish input
if self.input_text.strip():
# 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 = ""
self.input_active = False
else:
# Add character
if len(self.input_text) < 20:
self.input_text += selected_char
def adjust_setting(self, direction: int):
"""Adjust setting value left/right"""
if self.current_screen == "edit_profile" and self.active_profile:
profile = self.profiles[self.active_profile]
if self.selected_index == 0: # Difficulty
difficulties = ["easy", "normal", "hard", "expert"]
current = difficulties.index(profile.settings["difficulty"])
new_index = (current + direction) % len(difficulties)
profile.settings["difficulty"] = difficulties[new_index]
elif self.selected_index == 1: # Sound Volume
profile.settings["sound_volume"] = max(0, min(100,
profile.settings["sound_volume"] + direction * 5))
elif self.selected_index == 2: # Music Volume
profile.settings["music_volume"] = max(0, min(100,
profile.settings["music_volume"] + direction * 5))
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
self.renderer.clear(self.colors['black'])
# Draw subtle background pattern/gradient
for y in range(0, 480, 20):
alpha = int(20 * (1 - y / 480)) # Fade effect
if alpha > 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()
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()
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 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, 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'])
# Menu items with proper spacing
menu_items = [
"Create Profile",
"Select Profile",
"Edit Settings",
"Leaderboard",
"Profile Stats",
"Exit"
]
button_width = 280
button_height = 30
button_x = panel_x + 60
start_y = panel_y + 15
for i, item in enumerate(menu_items):
button_y = start_y + i * (button_height + 8)
selected = (i == self.selected_index)
# 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
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 == '<DEL':
display_char = 'DEL'
elif char == 'SPACE':
display_char = 'SPC'
text_x = current_x + key_widths[col_idx] // 2
text_y = row_y + 8
self.draw_text(display_char, text_x, text_y, text_color, 'tiny', center=True)
current_x += key_widths[col_idx] + 5
# Instructions for virtual keyboard
self.draw_panel(50, 420, 540, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Arrows: Navigate • Enter: Select • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_edit_profile(self):
"""Render profile editing screen with improved layout"""
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"Edit: {profile.name}"
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True)
# Profile stats
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}"
self.draw_text(stats_text, 320, 50, self.colors['light_green'], 'small', center=True)
# Settings panel
panel_height = 280
self.draw_panel(60, 90, 520, panel_height, self.colors['dark_gray'], self.colors['gray'])
# Settings title
self.draw_text("Settings", 320, 105, self.colors['white'], 'large', center=True)
settings_items = [
("Difficulty", f"{profile.settings['difficulty'].title()}"),
("Sound Vol", f"{profile.settings['sound_volume']}%"),
("Music Vol", f"{profile.settings['music_volume']}%"),
("Screen Shake", "On" if profile.settings['screen_shake'] else "Off"),
]
start_y = 130
item_height = 35
for i, (label, value) in enumerate(settings_items):
item_y = start_y + i * item_height
selected = (i == self.selected_index)
# Setting item background
if selected:
self.draw_panel(80, item_y, 480, 25, self.colors['blue'], self.colors['light_blue'])
text_color = self.colors['white']
value_color = self.colors['light_green']
# Navigation arrows for selected item
self.draw_text("", 70, item_y + 5, self.colors['white'], 'small')
self.draw_text("", 570, item_y + 5, self.colors['white'], 'small')
else:
text_color = self.colors['light_gray']
value_color = self.colors['yellow']
# Setting label and value
self.draw_text(label, 90, item_y + 5, text_color, 'medium')
self.draw_text(value, 500, item_y + 5, value_color, 'medium')
# Action buttons
button_y = 310
save_selected = (self.selected_index == len(settings_items))
back_selected = (self.selected_index == len(settings_items) + 1)
self.draw_button("Save", 200, button_y, 80, 30, save_selected)
self.draw_button("← Back", 320, button_y, 80, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
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:
return
factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
font = self.fonts.get(font_type, self.fonts['medium'])
text_sprite = factory.from_text(text, color=color, fontmanager=font)
if center:
x = x - text_sprite.size[0] // 2
text_sprite.position = (x, y)
self.renderer.copy(text_sprite, dstrect=text_sprite.position)
return text_sprite.size
def draw_panel(self, x: int, y: int, width: int, height: int, bg_color=None, border_color=None, border_width=2):
"""Draw a styled panel/box"""
if bg_color:
self.renderer.fill((x, y, width, height), bg_color)
if border_color:
# Draw border
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, disabled: bool = False):
"""Draw a styled button"""
# Button colors based on state
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']
else:
bg_color = self.colors['dark_gray']
border_color = self.colors['gray']
text_color = self.colors['light_gray']
# Draw button background and border
self.draw_panel(x, y, width, height, bg_color, border_color)
# Draw button text centered
text_x = x + width // 2
text_y = y + height // 2 - 12 # Approximate text height offset
self.draw_text(text, text_x, text_y, text_color, 'medium', center=True)
def handle_keyboard_input(self, event):
"""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()
elif key == sdl2.SDLK_DOWN:
self.navigate_down()
elif key == sdl2.SDLK_LEFT:
self.navigate_left()
elif key == sdl2.SDLK_RIGHT:
self.navigate_right()
elif key == sdl2.SDLK_RETURN or key == sdl2.SDLK_SPACE:
self.handle_confirm()
elif key == sdl2.SDLK_ESCAPE:
self.handle_back()
elif key == sdl2.SDLK_DELETE or key == sdl2.SDLK_BACKSPACE:
self.handle_delete()
elif key == sdl2.SDLK_TAB:
self.handle_menu()
def run(self):
"""Main application loop"""
self.init_sdl()
# Use SDL2's built-in timing instead of Clock
target_fps = 30 # Lower FPS for better performance on smaller screens
frame_time = 1000 // target_fps # milliseconds per frame
while self.running:
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN:
self.handle_keyboard_input(event)
# Handle gamepad input (if available)
self.handle_gamepad_input()
# Render frame
frame_start = sdl2.SDL_GetTicks()
self.render()
# Cap framerate
frame_duration = sdl2.SDL_GetTicks() - frame_start
if frame_duration < frame_time:
sdl2.SDL_Delay(frame_time - frame_duration)
# Cleanup
if self.gamepad:
sdl2.SDL_JoystickClose(self.gamepad)
sdl2.ext.quit()
def main():
"""Entry point"""
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(" 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()
if __name__ == "__main__":
main()