21 changed files with 2889 additions and 1266 deletions
Binary file not shown.
@ -0,0 +1,182 @@ |
|||||||
|
# Profile Manager Migration Guide |
||||||
|
|
||||||
|
## Overview of Changes |
||||||
|
|
||||||
|
The Profile Manager has been successfully migrated from a monolithic architecture to a modular screen-based system. This provides better code organization, maintainability, and extensibility. |
||||||
|
|
||||||
|
## Key Changes |
||||||
|
|
||||||
|
### Architecture |
||||||
|
|
||||||
|
**Before (Monolithic)**: |
||||||
|
- Single large `ProfileManager` class handling all screens |
||||||
|
- Mixed UI rendering and business logic |
||||||
|
- Complex navigation and state management |
||||||
|
- Large render method with screen conditionals |
||||||
|
|
||||||
|
**After (Modular)**: |
||||||
|
- Separate screen modules for each interface |
||||||
|
- Clean separation between UI and business logic |
||||||
|
- Individual screen classes with focused responsibilities |
||||||
|
- Simplified main ProfileManager class |
||||||
|
|
||||||
|
### File Structure Changes |
||||||
|
|
||||||
|
``` |
||||||
|
profile_manager/ |
||||||
|
├── profile_manager.py # Simplified main manager (UPDATED) |
||||||
|
├── profile_data.py # Business logic (unchanged) |
||||||
|
├── ui_components.py # UI components (unchanged) |
||||||
|
├── screens/ # NEW: Modular screen system |
||||||
|
│ ├── __init__.py # Screen exports |
||||||
|
│ ├── base_screen.py # Base screen class |
||||||
|
│ ├── screen_manager.py # Screen management |
||||||
|
│ ├── main_menu_screen.py # Main menu logic |
||||||
|
│ ├── profile_list_screen.py # Profile list logic |
||||||
|
│ ├── create_profile_screen.py # Profile creation logic |
||||||
|
│ ├── edit_profile_screen.py # Settings editing logic |
||||||
|
│ ├── leaderboard_screen.py # Leaderboard display logic |
||||||
|
│ ├── profile_stats_screen.py # Statistics display logic |
||||||
|
│ ├── example_integration.py # Integration example |
||||||
|
│ └── README.md # Screen system documentation |
||||||
|
``` |
||||||
|
|
||||||
|
### Code Reduction |
||||||
|
|
||||||
|
The main `profile_manager.py` has been reduced from: |
||||||
|
- **~750 lines** → **~130 lines** (83% reduction) |
||||||
|
- Complex screen handling → Simple delegation |
||||||
|
- Mixed concerns → Clean separation |
||||||
|
|
||||||
|
## Running the Updated Profile Manager |
||||||
|
|
||||||
|
### Standard Usage (No Changes) |
||||||
|
```bash |
||||||
|
cd /home/enne2/Sviluppo/mice/profile_manager |
||||||
|
python3 profile_manager.py |
||||||
|
``` |
||||||
|
|
||||||
|
The user interface and functionality remain exactly the same. All existing features work identically: |
||||||
|
- Create profiles with virtual keyboard |
||||||
|
- Edit profile settings |
||||||
|
- View leaderboards |
||||||
|
- Profile statistics |
||||||
|
- Gamepad and keyboard controls |
||||||
|
|
||||||
|
### Verifying the Migration |
||||||
|
|
||||||
|
1. **Test Basic Navigation**: |
||||||
|
- Run the profile manager |
||||||
|
- Navigate through all screens |
||||||
|
- Verify all buttons and controls work |
||||||
|
|
||||||
|
2. **Test Profile Operations**: |
||||||
|
- Create a new profile |
||||||
|
- Edit profile settings |
||||||
|
- Delete profiles |
||||||
|
- Switch active profiles |
||||||
|
|
||||||
|
3. **Test Advanced Features**: |
||||||
|
- View leaderboards (if API enabled) |
||||||
|
- Check profile statistics |
||||||
|
- Test error handling |
||||||
|
|
||||||
|
## Benefits of the New Architecture |
||||||
|
|
||||||
|
### 1. Maintainability |
||||||
|
- Each screen is independently maintainable |
||||||
|
- Bug fixes isolated to specific screen modules |
||||||
|
- Clear code organization |
||||||
|
|
||||||
|
### 2. Extensibility |
||||||
|
- Easy to add new screens |
||||||
|
- Consistent interface pattern |
||||||
|
- Minimal changes to main manager |
||||||
|
|
||||||
|
### 3. Testability |
||||||
|
- Individual screen unit tests possible |
||||||
|
- Mock dependencies easily |
||||||
|
- Isolated functionality testing |
||||||
|
|
||||||
|
### 4. Code Quality |
||||||
|
- Reduced complexity in main class |
||||||
|
- Single responsibility principle |
||||||
|
- Better error handling |
||||||
|
|
||||||
|
## Adding New Screens |
||||||
|
|
||||||
|
To add a new screen (e.g., "Settings Screen"): |
||||||
|
|
||||||
|
1. **Create screen module**: |
||||||
|
```python |
||||||
|
# screens/settings_screen.py |
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
class SettingsScreen(BaseScreen): |
||||||
|
def render(self): |
||||||
|
# Screen rendering logic |
||||||
|
pass |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
# Input handling logic |
||||||
|
pass |
||||||
|
``` |
||||||
|
|
||||||
|
2. **Register in screen manager**: |
||||||
|
```python |
||||||
|
# screens/screen_manager.py |
||||||
|
"settings": SettingsScreen(self.data_manager, self.ui_renderer, self) |
||||||
|
``` |
||||||
|
|
||||||
|
3. **Add navigation**: |
||||||
|
```python |
||||||
|
# From any screen |
||||||
|
self.screen_manager.set_screen("settings") |
||||||
|
``` |
||||||
|
|
||||||
|
## Backward Compatibility |
||||||
|
|
||||||
|
The migration maintains 100% backward compatibility: |
||||||
|
- Same user interface |
||||||
|
- Same keyboard/gamepad controls |
||||||
|
- Same file formats (user_profiles.json) |
||||||
|
- Same external API integration |
||||||
|
- Same SDL2 rendering |
||||||
|
|
||||||
|
## Performance Impact |
||||||
|
|
||||||
|
The modular system has minimal performance impact: |
||||||
|
- Slightly more memory for screen objects |
||||||
|
- Faster rendering due to delegation |
||||||
|
- Reduced complexity in main loop |
||||||
|
- Better error isolation |
||||||
|
|
||||||
|
## Troubleshooting |
||||||
|
|
||||||
|
### ImportError: screens module |
||||||
|
If you get import errors, ensure the `screens/` directory exists and has `__init__.py` |
||||||
|
|
||||||
|
### Screen not rendering |
||||||
|
Check that the screen is properly registered in `ModularScreenManager._initialize_screens()` |
||||||
|
|
||||||
|
### Navigation issues |
||||||
|
Verify screen transitions use `self.screen_manager.set_screen("screen_name")` |
||||||
|
|
||||||
|
## Development Workflow |
||||||
|
|
||||||
|
### Making Changes to Screens |
||||||
|
1. Edit the specific screen module in `screens/` |
||||||
|
2. No changes needed to main `profile_manager.py` |
||||||
|
3. Test the individual screen |
||||||
|
|
||||||
|
### Adding Features |
||||||
|
1. Determine which screen handles the feature |
||||||
|
2. Modify only that screen module |
||||||
|
3. Add any new dependencies to base class if needed |
||||||
|
|
||||||
|
### Debugging |
||||||
|
1. Enable debug mode in individual screens |
||||||
|
2. Test screens in isolation |
||||||
|
3. Use screen-specific error handling |
||||||
|
|
||||||
|
The modular architecture makes the Profile Manager much more maintainable and extensible while preserving all existing functionality. |
||||||
@ -0,0 +1,282 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
Profile Manager - Business Logic |
||||||
|
Handles profile data management without UI dependencies |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
import json |
||||||
|
from typing import Dict, List, Optional, Any |
||||||
|
from dataclasses import dataclass, asdict |
||||||
|
from datetime import datetime |
||||||
|
|
||||||
|
# Import the user profile integration system |
||||||
|
from 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 ProfileDataManager: |
||||||
|
"""Core business logic for profile management""" |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
self.load_profiles() |
||||||
|
|
||||||
|
def load_profiles(self) -> bool: |
||||||
|
"""Load profiles from JSON file""" |
||||||
|
if not os.path.exists(self.profiles_file): |
||||||
|
return True |
||||||
|
|
||||||
|
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') |
||||||
|
return True |
||||||
|
except (json.JSONDecodeError, KeyError) as e: |
||||||
|
print(f"Error loading profiles: {e}") |
||||||
|
self.profiles = {} |
||||||
|
return False |
||||||
|
|
||||||
|
def save_profiles(self) -> bool: |
||||||
|
"""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) |
||||||
|
return True |
||||||
|
except IOError as e: |
||||||
|
print(f"Error saving profiles: {e}") |
||||||
|
return False |
||||||
|
|
||||||
|
def create_profile(self, name: str) -> tuple[bool, str]: |
||||||
|
"""Create a new profile. Returns (success, message)""" |
||||||
|
name = name.strip() |
||||||
|
|
||||||
|
if not name: |
||||||
|
return False, "Profile name cannot be empty" |
||||||
|
|
||||||
|
if name in self.profiles: |
||||||
|
return False, f"Profile '{name}' already exists" |
||||||
|
|
||||||
|
now = datetime.now().isoformat() |
||||||
|
profile = UserProfile( |
||||||
|
name=name, |
||||||
|
created_date=now, |
||||||
|
last_played=now |
||||||
|
) |
||||||
|
|
||||||
|
self.profiles[name] = profile |
||||||
|
|
||||||
|
if not self.save_profiles(): |
||||||
|
del self.profiles[name] |
||||||
|
return False, "Failed to save profile" |
||||||
|
|
||||||
|
# Register with API server if available |
||||||
|
if self.api_enabled: |
||||||
|
result = self.integration.register_new_user(name) |
||||||
|
if result: |
||||||
|
print(f"Profile {name} registered with server") |
||||||
|
else: |
||||||
|
print(f"Warning: Profile {name} created locally but not registered with server") |
||||||
|
|
||||||
|
return True, f"Profile '{name}' created successfully" |
||||||
|
|
||||||
|
def delete_profile(self, name: str) -> tuple[bool, str]: |
||||||
|
"""Delete a profile. Returns (success, message)""" |
||||||
|
if name not in self.profiles: |
||||||
|
return False, f"Profile '{name}' not found" |
||||||
|
|
||||||
|
del self.profiles[name] |
||||||
|
|
||||||
|
if self.active_profile == name: |
||||||
|
self.active_profile = None |
||||||
|
|
||||||
|
if not self.save_profiles(): |
||||||
|
return False, "Failed to save changes" |
||||||
|
|
||||||
|
return True, f"Profile '{name}' deleted" |
||||||
|
|
||||||
|
def set_active_profile(self, name: str) -> tuple[bool, str]: |
||||||
|
"""Set the active profile. Returns (success, message)""" |
||||||
|
if name not in self.profiles: |
||||||
|
return False, f"Profile '{name}' not found" |
||||||
|
|
||||||
|
self.active_profile = name |
||||||
|
self.profiles[name].last_played = datetime.now().isoformat() |
||||||
|
|
||||||
|
if not self.save_profiles(): |
||||||
|
return False, "Failed to save changes" |
||||||
|
|
||||||
|
# Update integration system to load the new profile |
||||||
|
self.integration.reload_profile() |
||||||
|
|
||||||
|
return True, f"Active profile set to '{name}'" |
||||||
|
|
||||||
|
def get_profile_list(self) -> List[str]: |
||||||
|
"""Get list of profile names""" |
||||||
|
return list(self.profiles.keys()) |
||||||
|
|
||||||
|
def get_profile(self, name: str) -> Optional[UserProfile]: |
||||||
|
"""Get a specific profile""" |
||||||
|
return self.profiles.get(name) |
||||||
|
|
||||||
|
def get_active_profile(self) -> Optional[UserProfile]: |
||||||
|
"""Get the currently active profile""" |
||||||
|
if self.active_profile: |
||||||
|
return self.profiles.get(self.active_profile) |
||||||
|
return None |
||||||
|
|
||||||
|
def update_profile_settings(self, name: str, setting: str, value: Any) -> tuple[bool, str]: |
||||||
|
"""Update a profile setting. Returns (success, message)""" |
||||||
|
if name not in self.profiles: |
||||||
|
return False, f"Profile '{name}' not found" |
||||||
|
|
||||||
|
profile = self.profiles[name] |
||||||
|
|
||||||
|
if setting not in profile.settings: |
||||||
|
return False, f"Setting '{setting}' not found" |
||||||
|
|
||||||
|
# Validate setting values |
||||||
|
if not self._validate_setting(setting, value): |
||||||
|
return False, f"Invalid value for setting '{setting}'" |
||||||
|
|
||||||
|
profile.settings[setting] = value |
||||||
|
|
||||||
|
if not self.save_profiles(): |
||||||
|
return False, "Failed to save changes" |
||||||
|
|
||||||
|
return True, f"Setting '{setting}' updated" |
||||||
|
|
||||||
|
def _validate_setting(self, setting: str, value: Any) -> bool: |
||||||
|
"""Validate setting values""" |
||||||
|
validations = { |
||||||
|
'difficulty': lambda v: v in ['easy', 'normal', 'hard', 'expert'], |
||||||
|
'sound_volume': lambda v: isinstance(v, int) and 0 <= v <= 100, |
||||||
|
'music_volume': lambda v: isinstance(v, int) and 0 <= v <= 100, |
||||||
|
'screen_shake': lambda v: isinstance(v, bool), |
||||||
|
'auto_save': lambda v: isinstance(v, bool) |
||||||
|
} |
||||||
|
|
||||||
|
validator = validations.get(setting) |
||||||
|
return validator(value) if validator else True |
||||||
|
|
||||||
|
def get_leaderboard_data(self, leaderboard_type: str = "device") -> List[Dict[str, Any]]: |
||||||
|
"""Get leaderboard data""" |
||||||
|
if not self.api_enabled: |
||||||
|
return [] |
||||||
|
|
||||||
|
try: |
||||||
|
if leaderboard_type == "device": |
||||||
|
return self.integration.get_device_leaderboard(10) |
||||||
|
else: # global |
||||||
|
return self.integration.get_global_leaderboard(10) |
||||||
|
except Exception as e: |
||||||
|
print(f"Error loading leaderboard: {e}") |
||||||
|
return [] |
||||||
|
|
||||||
|
def get_profile_stats(self, name: str) -> Optional[Dict[str, Any]]: |
||||||
|
"""Get detailed profile statistics""" |
||||||
|
if name not in self.profiles: |
||||||
|
return None |
||||||
|
|
||||||
|
profile = self.profiles[name] |
||||||
|
integration_info = self.integration.get_profile_info() if self.api_enabled else {} |
||||||
|
|
||||||
|
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" |
||||||
|
|
||||||
|
return { |
||||||
|
'profile': profile, |
||||||
|
'created_formatted': created, |
||||||
|
'last_played_formatted': last_played, |
||||||
|
'integration_info': integration_info, |
||||||
|
'api_enabled': self.api_enabled |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
class SettingsManager: |
||||||
|
"""Helper class for managing profile settings""" |
||||||
|
|
||||||
|
DIFFICULTY_OPTIONS = ["easy", "normal", "hard", "expert"] |
||||||
|
VOLUME_RANGE = (0, 100, 5) # min, max, step |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def adjust_difficulty(cls, current: str, direction: int) -> str: |
||||||
|
"""Adjust difficulty setting""" |
||||||
|
try: |
||||||
|
current_index = cls.DIFFICULTY_OPTIONS.index(current) |
||||||
|
new_index = (current_index + direction) % len(cls.DIFFICULTY_OPTIONS) |
||||||
|
return cls.DIFFICULTY_OPTIONS[new_index] |
||||||
|
except ValueError: |
||||||
|
return "normal" |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def adjust_volume(cls, current: int, direction: int) -> int: |
||||||
|
"""Adjust volume setting""" |
||||||
|
min_val, max_val, step = cls.VOLUME_RANGE |
||||||
|
new_value = current + (direction * step) |
||||||
|
return max(min_val, min(max_val, new_value)) |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def toggle_boolean(cls, current: bool) -> bool: |
||||||
|
"""Toggle boolean setting""" |
||||||
|
return not current |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def get_setting_display_value(cls, setting: str, value: Any) -> str: |
||||||
|
"""Get display string for setting value""" |
||||||
|
if setting == 'difficulty': |
||||||
|
return value.title() |
||||||
|
elif setting in ['sound_volume', 'music_volume']: |
||||||
|
return f"{value}%" |
||||||
|
elif setting in ['screen_shake', 'auto_save']: |
||||||
|
return "On" if value else "Off" |
||||||
|
else: |
||||||
|
return str(value) |
||||||
@ -0,0 +1,168 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
User Profile Manager for Games |
||||||
|
A PySDL2-based profile management system with gamepad-only controls |
||||||
|
Refactored with separated UI and business logic components |
||||||
|
|
||||||
|
Features: |
||||||
|
- Create new user profiles |
||||||
|
- Edit existing profiles |
||||||
|
- Delete profiles |
||||||
|
- Select active profile |
||||||
|
- JSON-based storage |
||||||
|
- Gamepad navigation only |
||||||
|
- Modular UI components |
||||||
|
""" |
||||||
|
|
||||||
|
import time |
||||||
|
from typing import Dict, Optional |
||||||
|
|
||||||
|
import sdl2 |
||||||
|
import sdl2.ext |
||||||
|
|
||||||
|
# Import separated components |
||||||
|
from ui_components import UIRenderer, GamepadInputHandler |
||||||
|
from profile_data import ProfileDataManager, SettingsManager |
||||||
|
from screens import ModularScreenManager |
||||||
|
|
||||||
|
|
||||||
|
class ProfileManager: |
||||||
|
"""Main profile management system with separated UI and business logic""" |
||||||
|
|
||||||
|
def __init__(self, profiles_file: str = "user_profiles.json"): |
||||||
|
# Business logic components |
||||||
|
self.data_manager = ProfileDataManager(profiles_file) |
||||||
|
|
||||||
|
# Input handling |
||||||
|
self.input_handler = GamepadInputHandler() |
||||||
|
|
||||||
|
# SDL2 components |
||||||
|
self.window = None |
||||||
|
self.renderer = None |
||||||
|
self.ui_renderer = None |
||||||
|
self.running = True |
||||||
|
|
||||||
|
# NEW: Modular screen manager (will be initialized after SDL setup) |
||||||
|
self.screen_manager = None |
||||||
|
|
||||||
|
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 UI renderer |
||||||
|
self.ui_renderer = UIRenderer(self.renderer, (640, 480)) |
||||||
|
|
||||||
|
# NEW: Initialize modular screen manager |
||||||
|
self.screen_manager = ModularScreenManager(self.data_manager, self.ui_renderer) |
||||||
|
|
||||||
|
def handle_input(self): |
||||||
|
"""Handle all input (simplified with screen delegation)""" |
||||||
|
# Handle SDL events for keyboard |
||||||
|
events = sdl2.ext.get_events() |
||||||
|
for event in events: |
||||||
|
if event.type == sdl2.SDL_QUIT: |
||||||
|
self.running = False |
||||||
|
elif event.type == sdl2.SDL_KEYDOWN: |
||||||
|
action = self._keyboard_to_action(event.key.keysym.sym) |
||||||
|
if action: |
||||||
|
result = self.screen_manager.handle_input(action) |
||||||
|
if not result: |
||||||
|
self.running = False |
||||||
|
|
||||||
|
# Handle gamepad input |
||||||
|
gamepad_input = self.input_handler.get_gamepad_input() |
||||||
|
for action, pressed in gamepad_input.items(): |
||||||
|
if pressed: |
||||||
|
gamepad_action = self._gamepad_to_action(action) |
||||||
|
if gamepad_action: |
||||||
|
result = self.screen_manager.handle_input(gamepad_action) |
||||||
|
if not result: |
||||||
|
self.running = False |
||||||
|
|
||||||
|
def _keyboard_to_action(self, key) -> str: |
||||||
|
"""Convert keyboard input to action""" |
||||||
|
key_map = { |
||||||
|
sdl2.SDLK_UP: 'up', |
||||||
|
sdl2.SDLK_DOWN: 'down', |
||||||
|
sdl2.SDLK_LEFT: 'left', |
||||||
|
sdl2.SDLK_RIGHT: 'right', |
||||||
|
sdl2.SDLK_RETURN: 'confirm', |
||||||
|
sdl2.SDLK_SPACE: 'confirm', |
||||||
|
sdl2.SDLK_ESCAPE: 'back', |
||||||
|
sdl2.SDLK_DELETE: 'delete', |
||||||
|
sdl2.SDLK_BACKSPACE: 'delete' |
||||||
|
} |
||||||
|
return key_map.get(key, '') |
||||||
|
|
||||||
|
def _gamepad_to_action(self, gamepad_action: str) -> str: |
||||||
|
"""Convert gamepad input to action""" |
||||||
|
gamepad_map = { |
||||||
|
'up': 'up', |
||||||
|
'down': 'down', |
||||||
|
'left': 'left', |
||||||
|
'right': 'right', |
||||||
|
'button_3': 'confirm', # A/X button |
||||||
|
'button_4': 'back', # B/Circle button |
||||||
|
'button_9': 'delete' # X/Square button |
||||||
|
} |
||||||
|
return gamepad_map.get(gamepad_action, '') |
||||||
|
|
||||||
|
def render(self): |
||||||
|
"""Simplified rendering (delegated to screen manager)""" |
||||||
|
# Clear and draw background |
||||||
|
self.ui_renderer.clear_screen('black') |
||||||
|
self.ui_renderer.draw_background_pattern('gradient') |
||||||
|
|
||||||
|
# Delegate all screen rendering to screen manager |
||||||
|
self.screen_manager.render() |
||||||
|
|
||||||
|
self.ui_renderer.present() |
||||||
|
|
||||||
|
def run(self): |
||||||
|
"""Main application loop (unchanged)""" |
||||||
|
self.init_sdl() |
||||||
|
|
||||||
|
target_fps = 30 |
||||||
|
frame_time = 1000 // target_fps |
||||||
|
|
||||||
|
while self.running: |
||||||
|
frame_start = sdl2.SDL_GetTicks() |
||||||
|
|
||||||
|
# Handle input |
||||||
|
self.handle_input() |
||||||
|
|
||||||
|
# Render |
||||||
|
self.render() |
||||||
|
|
||||||
|
# Frame timing |
||||||
|
frame_elapsed = sdl2.SDL_GetTicks() - frame_start |
||||||
|
if frame_elapsed < frame_time: |
||||||
|
sdl2.SDL_Delay(frame_time - frame_elapsed) |
||||||
|
|
||||||
|
# Cleanup |
||||||
|
self.input_handler.cleanup() |
||||||
|
sdl2.ext.quit() |
||||||
|
|
||||||
|
|
||||||
|
# Entry point |
||||||
|
def main(): |
||||||
|
"""Main entry point""" |
||||||
|
manager = ProfileManager() |
||||||
|
manager.run() |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
main() |
||||||
@ -0,0 +1,307 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
Score API Client for Mice Game |
||||||
|
Client module to integrate with the FastAPI score server |
||||||
|
""" |
||||||
|
|
||||||
|
import requests |
||||||
|
import json |
||||||
|
from typing import Optional, List, Dict, Any |
||||||
|
import time |
||||||
|
|
||||||
|
|
||||||
|
class ScoreAPIClient: |
||||||
|
"""Client for communicating with the Mice Game Score API""" |
||||||
|
|
||||||
|
def __init__(self, api_base_url: str = "http://localhost:8000", timeout: int = 5): |
||||||
|
""" |
||||||
|
Initialize the API client |
||||||
|
|
||||||
|
Args: |
||||||
|
api_base_url: Base URL of the API server |
||||||
|
timeout: Request timeout in seconds |
||||||
|
""" |
||||||
|
self.api_base_url = api_base_url.rstrip('/') |
||||||
|
self.timeout = timeout |
||||||
|
|
||||||
|
def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Optional[Dict[str, Any]]: |
||||||
|
""" |
||||||
|
Make HTTP request to API |
||||||
|
|
||||||
|
Args: |
||||||
|
method: HTTP method (GET, POST, etc.) |
||||||
|
endpoint: API endpoint |
||||||
|
data: Request data for POST requests |
||||||
|
|
||||||
|
Returns: |
||||||
|
Response JSON or None if error |
||||||
|
""" |
||||||
|
url = f"{self.api_base_url}{endpoint}" |
||||||
|
|
||||||
|
try: |
||||||
|
if method.upper() == "GET": |
||||||
|
response = requests.get(url, timeout=self.timeout) |
||||||
|
elif method.upper() == "POST": |
||||||
|
response = requests.post(url, json=data, timeout=self.timeout) |
||||||
|
else: |
||||||
|
raise ValueError(f"Unsupported HTTP method: {method}") |
||||||
|
|
||||||
|
if response.status_code == 200: |
||||||
|
return response.json() |
||||||
|
elif response.status_code in [400, 404, 409]: |
||||||
|
# Client errors - return the error details |
||||||
|
return {"error": True, "status": response.status_code, "detail": response.json()} |
||||||
|
else: |
||||||
|
return {"error": True, "status": response.status_code, "detail": "Server error"} |
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError: |
||||||
|
return {"error": True, "detail": "Could not connect to score server"} |
||||||
|
except requests.exceptions.Timeout: |
||||||
|
return {"error": True, "detail": "Request timeout"} |
||||||
|
except Exception as e: |
||||||
|
return {"error": True, "detail": str(e)} |
||||||
|
|
||||||
|
def signup_user(self, device_id: str, user_id: str) -> Dict[str, Any]: |
||||||
|
""" |
||||||
|
Register a new user |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
user_id: User identifier |
||||||
|
|
||||||
|
Returns: |
||||||
|
Response dictionary with success/error status |
||||||
|
""" |
||||||
|
endpoint = f"/signup/{device_id}/{user_id}" |
||||||
|
response = self._make_request("POST", endpoint) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return {"success": False, "message": "Failed to connect to server"} |
||||||
|
|
||||||
|
if response.get("error"): |
||||||
|
return {"success": False, "message": response.get("detail", "Unknown error")} |
||||||
|
|
||||||
|
return response |
||||||
|
|
||||||
|
def submit_score(self, device_id: str, user_id: str, score: int, game_completed: bool = True) -> Dict[str, Any]: |
||||||
|
""" |
||||||
|
Submit a score for a user |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
user_id: User identifier |
||||||
|
score: Game score |
||||||
|
game_completed: Whether the game was completed |
||||||
|
|
||||||
|
Returns: |
||||||
|
Response dictionary with success/error status |
||||||
|
""" |
||||||
|
endpoint = f"/score/{device_id}/{user_id}" |
||||||
|
data = { |
||||||
|
"user_id": user_id, |
||||||
|
"device_id": device_id, |
||||||
|
"score": score, |
||||||
|
"game_completed": game_completed |
||||||
|
} |
||||||
|
|
||||||
|
response = self._make_request("POST", endpoint, data) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return {"success": False, "message": "Failed to connect to server"} |
||||||
|
|
||||||
|
if response.get("error"): |
||||||
|
return {"success": False, "message": response.get("detail", "Unknown error")} |
||||||
|
|
||||||
|
return response |
||||||
|
|
||||||
|
def get_device_users(self, device_id: str) -> List[Dict[str, Any]]: |
||||||
|
""" |
||||||
|
Get all users registered for a device |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
|
||||||
|
Returns: |
||||||
|
List of user dictionaries |
||||||
|
""" |
||||||
|
endpoint = f"/users/{device_id}" |
||||||
|
response = self._make_request("GET", endpoint) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return [] |
||||||
|
|
||||||
|
# Check if it's an error response (dict with error field) or success (list) |
||||||
|
if isinstance(response, dict) and response.get("error"): |
||||||
|
return [] |
||||||
|
|
||||||
|
# If it's a list (successful response), return it |
||||||
|
if isinstance(response, list): |
||||||
|
return response |
||||||
|
|
||||||
|
return [] |
||||||
|
|
||||||
|
def get_user_scores(self, device_id: str, user_id: str, limit: int = 10) -> List[Dict[str, Any]]: |
||||||
|
""" |
||||||
|
Get recent scores for a user |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
user_id: User identifier |
||||||
|
limit: Maximum number of scores to return |
||||||
|
|
||||||
|
Returns: |
||||||
|
List of score dictionaries |
||||||
|
""" |
||||||
|
endpoint = f"/scores/{device_id}/{user_id}?limit={limit}" |
||||||
|
response = self._make_request("GET", endpoint) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return [] |
||||||
|
|
||||||
|
# Check if it's an error response (dict with error field) or success (list) |
||||||
|
if isinstance(response, dict) and response.get("error"): |
||||||
|
return [] |
||||||
|
|
||||||
|
# If it's a list (successful response), return it |
||||||
|
if isinstance(response, list): |
||||||
|
return response |
||||||
|
|
||||||
|
return [] |
||||||
|
|
||||||
|
def get_leaderboard(self, device_id: str, limit: int = 10) -> List[Dict[str, Any]]: |
||||||
|
""" |
||||||
|
Get leaderboard for a device |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
limit: Maximum number of entries to return |
||||||
|
|
||||||
|
Returns: |
||||||
|
List of leaderboard entries |
||||||
|
""" |
||||||
|
endpoint = f"/leaderboard/{device_id}?limit={limit}" |
||||||
|
response = self._make_request("GET", endpoint) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return [] |
||||||
|
|
||||||
|
# Check if it's an error response (dict with error field) or success (list) |
||||||
|
if isinstance(response, dict) and response.get("error"): |
||||||
|
return [] |
||||||
|
|
||||||
|
# If it's a list (successful response), return it |
||||||
|
if isinstance(response, list): |
||||||
|
return response |
||||||
|
|
||||||
|
return [] |
||||||
|
|
||||||
|
def get_global_leaderboard(self, limit: int = 10) -> List[Dict[str, Any]]: |
||||||
|
""" |
||||||
|
Get global leaderboard across all devices |
||||||
|
|
||||||
|
Args: |
||||||
|
limit: Maximum number of entries to return |
||||||
|
|
||||||
|
Returns: |
||||||
|
List of global leaderboard entries |
||||||
|
""" |
||||||
|
endpoint = f"/leaderboard/global/top?limit={limit}" |
||||||
|
response = self._make_request("GET", endpoint) |
||||||
|
|
||||||
|
if response is None: |
||||||
|
return [] |
||||||
|
|
||||||
|
# Check if it's an error response (dict with error field) or success (list) |
||||||
|
if isinstance(response, dict) and response.get("error"): |
||||||
|
return [] |
||||||
|
|
||||||
|
# If it's a list (successful response), return it |
||||||
|
if isinstance(response, list): |
||||||
|
return response |
||||||
|
|
||||||
|
return [] |
||||||
|
|
||||||
|
def is_server_available(self) -> bool: |
||||||
|
""" |
||||||
|
Check if the API server is available |
||||||
|
|
||||||
|
Returns: |
||||||
|
True if server is reachable, False otherwise |
||||||
|
""" |
||||||
|
response = self._make_request("GET", "/") |
||||||
|
return response is not None and not response.get("error") |
||||||
|
|
||||||
|
def user_exists(self, device_id: str, user_id: str) -> bool: |
||||||
|
""" |
||||||
|
Check if a user is registered for a device |
||||||
|
|
||||||
|
Args: |
||||||
|
device_id: Device identifier |
||||||
|
user_id: User identifier |
||||||
|
|
||||||
|
Returns: |
||||||
|
True if user exists, False otherwise |
||||||
|
""" |
||||||
|
users = self.get_device_users(device_id) |
||||||
|
return any(user["user_id"] == user_id for user in users) |
||||||
|
|
||||||
|
|
||||||
|
# Convenience functions for easy integration |
||||||
|
def create_api_client(api_url: str = "http://localhost:8000") -> ScoreAPIClient: |
||||||
|
"""Create and return an API client instance""" |
||||||
|
return ScoreAPIClient(api_url) |
||||||
|
|
||||||
|
def test_connection(api_url: str = "http://localhost:8000") -> bool: |
||||||
|
"""Test if the API server is available""" |
||||||
|
client = ScoreAPIClient(api_url) |
||||||
|
return client.is_server_available() |
||||||
|
|
||||||
|
|
||||||
|
# Example usage and testing |
||||||
|
if __name__ == "__main__": |
||||||
|
# Example usage |
||||||
|
print("Testing Score API Client...") |
||||||
|
|
||||||
|
# Create client |
||||||
|
client = ScoreAPIClient() |
||||||
|
|
||||||
|
# Test server connection |
||||||
|
if not client.is_server_available(): |
||||||
|
print("ERROR: API server is not available. Start it with: python score_api.py") |
||||||
|
exit(1) |
||||||
|
|
||||||
|
print("API server is available!") |
||||||
|
|
||||||
|
# Example device and user |
||||||
|
device_id = "DEV-CLIENT01" |
||||||
|
user_id = "ClientTestUser" |
||||||
|
|
||||||
|
# Test user signup |
||||||
|
print(f"\nTesting user signup: {user_id}") |
||||||
|
result = client.signup_user(device_id, user_id) |
||||||
|
print(f"Signup result: {result}") |
||||||
|
|
||||||
|
# Test score submission |
||||||
|
print(f"\nTesting score submission...") |
||||||
|
result = client.submit_score(device_id, user_id, 1750, True) |
||||||
|
print(f"Score submission result: {result}") |
||||||
|
|
||||||
|
# Test getting users |
||||||
|
print(f"\nGetting users for device {device_id}:") |
||||||
|
users = client.get_device_users(device_id) |
||||||
|
for user in users: |
||||||
|
print(f" User: {user['user_id']}, Best Score: {user['best_score']}") |
||||||
|
|
||||||
|
# Test getting user scores |
||||||
|
print(f"\nGetting scores for {user_id}:") |
||||||
|
scores = client.get_user_scores(device_id, user_id) |
||||||
|
for score in scores: |
||||||
|
print(f" Score: {score['score']}, Time: {score['timestamp']}") |
||||||
|
|
||||||
|
# Test leaderboard |
||||||
|
print(f"\nLeaderboard for device {device_id}:") |
||||||
|
leaderboard = client.get_leaderboard(device_id) |
||||||
|
for entry in leaderboard: |
||||||
|
print(f" Rank {entry['rank']}: {entry['user_id']} - {entry['best_score']} pts") |
||||||
|
|
||||||
|
print("\nClient testing completed!") |
||||||
@ -0,0 +1,180 @@ |
|||||||
|
# Profile Manager Screen Modules |
||||||
|
|
||||||
|
This folder contains individual screen modules for the Profile Manager, providing a clean separation of concerns and modular architecture. |
||||||
|
|
||||||
|
## Structure |
||||||
|
|
||||||
|
``` |
||||||
|
screens/ |
||||||
|
├── __init__.py # Module exports |
||||||
|
├── base_screen.py # Abstract base class for all screens |
||||||
|
├── screen_manager.py # Manages screen modules and navigation |
||||||
|
├── main_menu_screen.py # Main menu interface |
||||||
|
├── profile_list_screen.py # Profile selection and management |
||||||
|
├── create_profile_screen.py # Profile creation with virtual keyboard |
||||||
|
├── edit_profile_screen.py # Profile settings editing |
||||||
|
├── leaderboard_screen.py # Leaderboard display (device/global) |
||||||
|
├── profile_stats_screen.py # Detailed profile statistics |
||||||
|
└── README.md # This file |
||||||
|
``` |
||||||
|
|
||||||
|
## Architecture |
||||||
|
|
||||||
|
### BaseScreen |
||||||
|
- Abstract base class providing common interface |
||||||
|
- Standard navigation methods (up, down, left, right) |
||||||
|
- Input handling framework |
||||||
|
- State management utilities |
||||||
|
|
||||||
|
### Screen Modules |
||||||
|
Each screen is a self-contained module with: |
||||||
|
- **Rendering**: Complete screen display logic |
||||||
|
- **Input Handling**: Screen-specific input processing |
||||||
|
- **State Management**: Internal state tracking |
||||||
|
- **Navigation**: Transitions to other screens |
||||||
|
|
||||||
|
### ModularScreenManager |
||||||
|
- Manages all screen module instances |
||||||
|
- Handles screen transitions |
||||||
|
- Provides error dialog overlay |
||||||
|
- Maintains current screen state |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
### Adding New Screens |
||||||
|
|
||||||
|
1. **Create Screen Module**: |
||||||
|
```python |
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
class NewScreen(BaseScreen): |
||||||
|
def render(self) -> None: |
||||||
|
# Implement rendering logic |
||||||
|
pass |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
# Implement input handling |
||||||
|
pass |
||||||
|
``` |
||||||
|
|
||||||
|
2. **Register in ScreenManager**: |
||||||
|
```python |
||||||
|
# In screen_manager.py _initialize_screens() |
||||||
|
"new_screen": NewScreen(self.data_manager, self.ui_renderer, self) |
||||||
|
``` |
||||||
|
|
||||||
|
3. **Add to Exports**: |
||||||
|
```python |
||||||
|
# In __init__.py |
||||||
|
from .new_screen import NewScreen |
||||||
|
``` |
||||||
|
|
||||||
|
### Screen Integration |
||||||
|
|
||||||
|
To integrate with the main Profile Manager: |
||||||
|
|
||||||
|
```python |
||||||
|
from screens import ModularScreenManager |
||||||
|
|
||||||
|
# Replace existing screen manager |
||||||
|
self.screen_manager = ModularScreenManager(self.data_manager, self.ui_renderer) |
||||||
|
|
||||||
|
# Handle input |
||||||
|
result = self.screen_manager.handle_input(action) |
||||||
|
|
||||||
|
# Render |
||||||
|
self.screen_manager.render() |
||||||
|
``` |
||||||
|
|
||||||
|
## Screen Responsibilities |
||||||
|
|
||||||
|
### MainMenuScreen |
||||||
|
- Primary navigation hub |
||||||
|
- Profile status display |
||||||
|
- API connection status |
||||||
|
- Menu option availability |
||||||
|
|
||||||
|
### ProfileListScreen |
||||||
|
- Display existing profiles |
||||||
|
- Profile selection |
||||||
|
- Profile deletion |
||||||
|
- Active profile indication |
||||||
|
|
||||||
|
### CreateProfileScreen |
||||||
|
- Virtual keyboard interface |
||||||
|
- Profile name input |
||||||
|
- Input validation |
||||||
|
- Profile creation |
||||||
|
|
||||||
|
### EditProfileScreen |
||||||
|
- Settings adjustment interface |
||||||
|
- Real-time value display |
||||||
|
- Setting validation |
||||||
|
- Changes persistence |
||||||
|
|
||||||
|
### LeaderboardScreen |
||||||
|
- Leaderboard data display |
||||||
|
- Device/Global toggle |
||||||
|
- Data refresh functionality |
||||||
|
- User highlighting |
||||||
|
|
||||||
|
### ProfileStatsScreen |
||||||
|
- Comprehensive statistics |
||||||
|
- Server sync status |
||||||
|
- Settings summary |
||||||
|
- Achievements display |
||||||
|
|
||||||
|
## Benefits |
||||||
|
|
||||||
|
### Modularity |
||||||
|
- Each screen is independent and testable |
||||||
|
- Clear separation of concerns |
||||||
|
- Easy to add/remove/modify screens |
||||||
|
|
||||||
|
### Maintainability |
||||||
|
- Isolated screen logic |
||||||
|
- Consistent interface pattern |
||||||
|
- Reduced coupling between screens |
||||||
|
|
||||||
|
### Extensibility |
||||||
|
- Simple to add new screens |
||||||
|
- Common functionality in base class |
||||||
|
- Flexible navigation system |
||||||
|
|
||||||
|
### Testability |
||||||
|
- Individual screen unit tests |
||||||
|
- Mock dependencies easily |
||||||
|
- Isolated functionality testing |
||||||
|
|
||||||
|
## Navigation Flow |
||||||
|
|
||||||
|
``` |
||||||
|
Main Menu ─┬─ Create Profile |
||||||
|
├─ Profile List |
||||||
|
├─ Edit Profile (if profile active) |
||||||
|
├─ Leaderboard (if API enabled) |
||||||
|
├─ Profile Stats (if profile active) |
||||||
|
└─ Exit |
||||||
|
|
||||||
|
All screens ─→ Back to Main Menu (ESC) |
||||||
|
``` |
||||||
|
|
||||||
|
## Input Actions |
||||||
|
|
||||||
|
Standard actions handled by screens: |
||||||
|
- `up`: Navigate selection up |
||||||
|
- `down`: Navigate selection down |
||||||
|
- `left`: Navigate left / Adjust setting |
||||||
|
- `right`: Navigate right / Adjust setting |
||||||
|
- `confirm`: Confirm selection / Enter |
||||||
|
- `back`: Return to previous screen / ESC |
||||||
|
- `delete`: Delete item / Backspace |
||||||
|
|
||||||
|
## Error Handling |
||||||
|
|
||||||
|
Screens can show error messages via: |
||||||
|
```python |
||||||
|
self.show_error("Error message") |
||||||
|
``` |
||||||
|
|
||||||
|
The ModularScreenManager handles error dialog display and auto-dismissal. |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
""" |
||||||
|
Screen modules for Profile Manager |
||||||
|
Individual screen implementations with separated rendering and logic |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
from .main_menu_screen import MainMenuScreen |
||||||
|
from .profile_list_screen import ProfileListScreen |
||||||
|
from .create_profile_screen import CreateProfileScreen |
||||||
|
from .edit_profile_screen import EditProfileScreen |
||||||
|
from .leaderboard_screen import LeaderboardScreen |
||||||
|
from .profile_stats_screen import ProfileStatsScreen |
||||||
|
from .screen_manager import ModularScreenManager |
||||||
|
|
||||||
|
__all__ = [ |
||||||
|
'BaseScreen', |
||||||
|
'MainMenuScreen', |
||||||
|
'ProfileListScreen', |
||||||
|
'CreateProfileScreen', |
||||||
|
'EditProfileScreen', |
||||||
|
'LeaderboardScreen', |
||||||
|
'ProfileStatsScreen', |
||||||
|
'ModularScreenManager' |
||||||
|
] |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
""" |
||||||
|
Base Screen Class |
||||||
|
Common interface and functionality for all screens in the Profile Manager |
||||||
|
""" |
||||||
|
|
||||||
|
from abc import ABC, abstractmethod |
||||||
|
from typing import Dict, Any, Optional, Tuple |
||||||
|
|
||||||
|
|
||||||
|
class BaseScreen(ABC): |
||||||
|
"""Base class for all profile manager screens""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer, screen_manager): |
||||||
|
""" |
||||||
|
Initialize base screen |
||||||
|
|
||||||
|
Args: |
||||||
|
data_manager: ProfileDataManager instance |
||||||
|
ui_renderer: UIRenderer instance |
||||||
|
screen_manager: ScreenManager instance |
||||||
|
""" |
||||||
|
self.data_manager = data_manager |
||||||
|
self.ui_renderer = ui_renderer |
||||||
|
self.screen_manager = screen_manager |
||||||
|
|
||||||
|
# Screen-specific state |
||||||
|
self.selected_index = 0 |
||||||
|
self.screen_state = {} |
||||||
|
|
||||||
|
@abstractmethod |
||||||
|
def render(self) -> None: |
||||||
|
"""Render the screen content""" |
||||||
|
pass |
||||||
|
|
||||||
|
@abstractmethod |
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
""" |
||||||
|
Handle input for this screen |
||||||
|
|
||||||
|
Args: |
||||||
|
action: Input action string ('up', 'down', 'left', 'right', 'confirm', 'back', 'delete') |
||||||
|
|
||||||
|
Returns: |
||||||
|
bool: True if input was handled, False otherwise |
||||||
|
""" |
||||||
|
pass |
||||||
|
|
||||||
|
def navigate_up(self) -> None: |
||||||
|
"""Navigate selection up""" |
||||||
|
if self.selected_index > 0: |
||||||
|
self.selected_index -= 1 |
||||||
|
|
||||||
|
def navigate_down(self, max_index: int) -> None: |
||||||
|
"""Navigate selection down""" |
||||||
|
if self.selected_index < max_index: |
||||||
|
self.selected_index += 1 |
||||||
|
|
||||||
|
def navigate_left(self) -> None: |
||||||
|
"""Navigate left (screen-specific implementation)""" |
||||||
|
pass |
||||||
|
|
||||||
|
def navigate_right(self) -> None: |
||||||
|
"""Navigate right (screen-specific implementation)""" |
||||||
|
pass |
||||||
|
|
||||||
|
def handle_confirm(self) -> None: |
||||||
|
"""Handle confirm action (screen-specific implementation)""" |
||||||
|
pass |
||||||
|
|
||||||
|
def handle_back(self) -> None: |
||||||
|
"""Handle back action (default: return to main menu)""" |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
|
||||||
|
def handle_delete(self) -> None: |
||||||
|
"""Handle delete action (screen-specific implementation)""" |
||||||
|
pass |
||||||
|
|
||||||
|
def show_error(self, message: str) -> None: |
||||||
|
"""Show error message through the screen manager""" |
||||||
|
# Delegate to screen manager |
||||||
|
if hasattr(self.screen_manager, 'show_error_dialog'): |
||||||
|
self.screen_manager.show_error_dialog(message) |
||||||
|
|
||||||
|
def reset_state(self) -> None: |
||||||
|
"""Reset screen state when entering""" |
||||||
|
self.selected_index = 0 |
||||||
|
self.screen_state.clear() |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for this screen""" |
||||||
|
return "↑↓ Navigate • Enter Confirm • Escape Back" |
||||||
@ -0,0 +1,141 @@ |
|||||||
|
""" |
||||||
|
Create Profile Screen |
||||||
|
Virtual keyboard interface for creating new user profiles |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
|
||||||
|
class CreateProfileScreen(BaseScreen): |
||||||
|
"""Profile creation screen with virtual keyboard""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer, screen_manager): |
||||||
|
super().__init__(data_manager, ui_renderer, screen_manager) |
||||||
|
|
||||||
|
# Virtual keyboard layout |
||||||
|
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'] |
||||||
|
] |
||||||
|
|
||||||
|
# Screen state |
||||||
|
self.input_text = "" |
||||||
|
self.input_active = False |
||||||
|
self.vk_cursor_x = 0 |
||||||
|
self.vk_cursor_y = 0 |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render profile creation screen""" |
||||||
|
self.ui_renderer.draw_header("Create Profile") |
||||||
|
|
||||||
|
# Input field |
||||||
|
placeholder = "Profile Name" if not self.input_text else "" |
||||||
|
self.ui_renderer.draw_input_field(self.input_text, 120, 90, 400, 30, |
||||||
|
self.input_active, placeholder) |
||||||
|
|
||||||
|
if self.input_active: |
||||||
|
# Virtual keyboard |
||||||
|
self.ui_renderer.draw_virtual_keyboard(self.virtual_keyboard, |
||||||
|
self.vk_cursor_x, self.vk_cursor_y) |
||||||
|
help_text = "Arrows: Navigate • Enter: Select • Escape: Cancel" |
||||||
|
else: |
||||||
|
self.ui_renderer.draw_text("Press Enter to start typing", 320, 150, |
||||||
|
'yellow', 'medium', center=True) |
||||||
|
self.ui_renderer.draw_button("← Back", 270, 200, 100, 30, True) |
||||||
|
help_text = "Enter: Start Input • Escape: Back" |
||||||
|
|
||||||
|
self.ui_renderer.draw_footer_help(help_text) |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle create profile input""" |
||||||
|
if self.input_active: |
||||||
|
# Virtual keyboard navigation |
||||||
|
if action == 'up': |
||||||
|
self.vk_cursor_y = max(0, self.vk_cursor_y - 1) |
||||||
|
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 |
||||||
|
self.vk_cursor_x = min(self.vk_cursor_x, max_x) |
||||||
|
return True |
||||||
|
elif action == 'down': |
||||||
|
max_y = len(self.virtual_keyboard) - 1 |
||||||
|
self.vk_cursor_y = min(max_y, self.vk_cursor_y + 1) |
||||||
|
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 |
||||||
|
self.vk_cursor_x = min(self.vk_cursor_x, max_x) |
||||||
|
return True |
||||||
|
elif action == 'left': |
||||||
|
self.vk_cursor_x = max(0, self.vk_cursor_x - 1) |
||||||
|
return True |
||||||
|
elif action == 'right': |
||||||
|
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1 |
||||||
|
self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1) |
||||||
|
return True |
||||||
|
elif action == 'confirm': |
||||||
|
self.handle_virtual_keyboard_input() |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
self.input_active = False |
||||||
|
return True |
||||||
|
else: |
||||||
|
# Initial screen navigation |
||||||
|
if action == 'confirm': |
||||||
|
self.input_active = True |
||||||
|
self.vk_cursor_x = 0 |
||||||
|
self.vk_cursor_y = 0 |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
self.handle_back() |
||||||
|
return True |
||||||
|
elif action == 'delete' and self.input_text: |
||||||
|
self.input_text = self.input_text[:-1] |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def handle_virtual_keyboard_input(self) -> None: |
||||||
|
"""Handle virtual keyboard character selection""" |
||||||
|
if (self.vk_cursor_y >= len(self.virtual_keyboard) or |
||||||
|
self.vk_cursor_x >= len(self.virtual_keyboard[self.vk_cursor_y])): |
||||||
|
return |
||||||
|
|
||||||
|
selected_char = self.virtual_keyboard[self.vk_cursor_y][self.vk_cursor_x] |
||||||
|
|
||||||
|
if selected_char == '<DEL': |
||||||
|
if self.input_text: |
||||||
|
self.input_text = self.input_text[:-1] |
||||||
|
elif selected_char == 'SPACE': |
||||||
|
if len(self.input_text) < 20: |
||||||
|
self.input_text += ' ' |
||||||
|
elif selected_char == 'DONE': |
||||||
|
if self.input_text.strip(): |
||||||
|
success, message = self.data_manager.create_profile(self.input_text) |
||||||
|
if success: |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
self.input_active = False |
||||||
|
self.input_text = "" |
||||||
|
else: |
||||||
|
self.show_error(message) |
||||||
|
else: |
||||||
|
self.show_error("Profile name cannot be empty!") |
||||||
|
elif selected_char == 'CANCEL': |
||||||
|
self.input_text = "" |
||||||
|
self.input_active = False |
||||||
|
else: |
||||||
|
if len(self.input_text) < 20: |
||||||
|
self.input_text += selected_char |
||||||
|
|
||||||
|
def reset_state(self) -> None: |
||||||
|
"""Reset screen state when entering""" |
||||||
|
super().reset_state() |
||||||
|
self.input_text = "" |
||||||
|
self.input_active = False |
||||||
|
self.vk_cursor_x = 0 |
||||||
|
self.vk_cursor_y = 0 |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for create profile""" |
||||||
|
if self.input_active: |
||||||
|
return "Arrows: Navigate • Enter: Select • Escape: Cancel" |
||||||
|
else: |
||||||
|
return "Enter: Start Input • Escape: Back" |
||||||
@ -0,0 +1,136 @@ |
|||||||
|
""" |
||||||
|
Edit Profile Screen |
||||||
|
Interface for editing profile settings and configurations |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
from profile_data import SettingsManager |
||||||
|
|
||||||
|
|
||||||
|
class EditProfileScreen(BaseScreen): |
||||||
|
"""Profile editing screen implementation""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer, screen_manager): |
||||||
|
super().__init__(data_manager, ui_renderer, screen_manager) |
||||||
|
|
||||||
|
# Settings configuration |
||||||
|
self.settings = [ |
||||||
|
("Difficulty", "difficulty"), |
||||||
|
("Sound Vol", "sound_volume"), |
||||||
|
("Music Vol", "music_volume"), |
||||||
|
("Screen Shake", "screen_shake"), |
||||||
|
] |
||||||
|
self.max_index = len(self.settings) + 1 # +1 for save/back buttons |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render profile editing screen""" |
||||||
|
active_profile = self.data_manager.get_active_profile() |
||||||
|
if not active_profile: |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
return |
||||||
|
|
||||||
|
# Header with profile info |
||||||
|
subtitle = f"Games: {active_profile.games_played} • Score: {active_profile.best_score}" |
||||||
|
self.ui_renderer.draw_header(f"Edit: {active_profile.name}", subtitle) |
||||||
|
|
||||||
|
# Settings panel |
||||||
|
self.ui_renderer.draw_panel(60, 90, 520, 280, 'dark_gray', 'gray') |
||||||
|
self.ui_renderer.draw_text("Settings", 320, 105, 'white', 'large', center=True) |
||||||
|
|
||||||
|
# Settings items |
||||||
|
for i, (label, setting_key) in enumerate(self.settings): |
||||||
|
item_y = 130 + i * 35 |
||||||
|
selected = (i == self.selected_index) |
||||||
|
|
||||||
|
if selected: |
||||||
|
self.ui_renderer.draw_panel(80, item_y, 480, 25, 'blue', 'light_blue') |
||||||
|
text_color = 'white' |
||||||
|
value_color = 'light_green' |
||||||
|
# Navigation arrows |
||||||
|
self.ui_renderer.draw_text("◄", 70, item_y + 5, 'white', 'small') |
||||||
|
self.ui_renderer.draw_text("►", 570, item_y + 5, 'white', 'small') |
||||||
|
else: |
||||||
|
text_color = 'light_gray' |
||||||
|
value_color = 'yellow' |
||||||
|
|
||||||
|
# Setting label and value |
||||||
|
self.ui_renderer.draw_text(label, 90, item_y + 5, text_color, 'medium') |
||||||
|
|
||||||
|
value = active_profile.settings[setting_key] |
||||||
|
display_value = SettingsManager.get_setting_display_value(setting_key, value) |
||||||
|
self.ui_renderer.draw_text(display_value, 500, item_y + 5, value_color, 'medium') |
||||||
|
|
||||||
|
# Action buttons |
||||||
|
save_selected = (self.selected_index == len(self.settings)) |
||||||
|
back_selected = (self.selected_index == len(self.settings) + 1) |
||||||
|
|
||||||
|
self.ui_renderer.draw_button("Save", 200, 310, 80, 30, save_selected) |
||||||
|
self.ui_renderer.draw_button("← Back", 320, 310, 80, 30, back_selected) |
||||||
|
|
||||||
|
self.ui_renderer.draw_footer_help(self.get_help_text()) |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle edit profile input""" |
||||||
|
max_index = len(self.settings) + 1 # settings + save + back |
||||||
|
|
||||||
|
if action == 'up': |
||||||
|
self.navigate_up() |
||||||
|
return True |
||||||
|
elif action == 'down': |
||||||
|
self.navigate_down(max_index) |
||||||
|
return True |
||||||
|
elif action == 'left': |
||||||
|
self.adjust_setting(-1) |
||||||
|
return True |
||||||
|
elif action == 'right': |
||||||
|
self.adjust_setting(1) |
||||||
|
return True |
||||||
|
elif action == 'confirm': |
||||||
|
self.handle_confirm() |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
self.handle_back() |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def adjust_setting(self, direction: int) -> None: |
||||||
|
"""Adjust setting value left/right""" |
||||||
|
if not self.data_manager.active_profile or self.selected_index >= len(self.settings): |
||||||
|
return |
||||||
|
|
||||||
|
profile = self.data_manager.get_active_profile() |
||||||
|
if not profile: |
||||||
|
return |
||||||
|
|
||||||
|
setting_label, setting_name = self.settings[self.selected_index] |
||||||
|
current_value = profile.settings[setting_name] |
||||||
|
|
||||||
|
# Adjust based on setting type |
||||||
|
if setting_name == "difficulty": |
||||||
|
new_value = SettingsManager.adjust_difficulty(current_value, direction) |
||||||
|
elif setting_name in ["sound_volume", "music_volume"]: |
||||||
|
new_value = SettingsManager.adjust_volume(current_value, direction) |
||||||
|
elif setting_name == "screen_shake": |
||||||
|
new_value = SettingsManager.toggle_boolean(current_value) |
||||||
|
else: |
||||||
|
return |
||||||
|
|
||||||
|
# Update the setting |
||||||
|
success, message = self.data_manager.update_profile_settings( |
||||||
|
self.data_manager.active_profile, setting_name, new_value |
||||||
|
) |
||||||
|
|
||||||
|
if not success: |
||||||
|
self.show_error(message) |
||||||
|
|
||||||
|
def handle_confirm(self) -> None: |
||||||
|
"""Handle profile editing confirmation""" |
||||||
|
if self.selected_index == len(self.settings): # Save |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
elif self.selected_index == len(self.settings) + 1: # Back |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for edit profile""" |
||||||
|
return "Left/Right: Adjust • Enter: Save/Back • Escape: Cancel" |
||||||
@ -0,0 +1,135 @@ |
|||||||
|
""" |
||||||
|
Leaderboard Screen |
||||||
|
Display global and device leaderboards with rankings and scores |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardScreen(BaseScreen): |
||||||
|
"""Leaderboard display screen implementation""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer, screen_manager): |
||||||
|
super().__init__(data_manager, ui_renderer, screen_manager) |
||||||
|
|
||||||
|
# Leaderboard state |
||||||
|
self.leaderboard_type = "device" # "device" or "global" |
||||||
|
self.leaderboard_data = [] |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render leaderboard screen""" |
||||||
|
title = f"{self.leaderboard_type.title()} Leaderboard" |
||||||
|
self.ui_renderer.draw_header(title) |
||||||
|
|
||||||
|
if not self.data_manager.api_enabled: |
||||||
|
self.ui_renderer.draw_text("Server offline - No leaderboard available", |
||||||
|
320, 150, 'red', 'medium', center=True) |
||||||
|
self.ui_renderer.draw_button("← Back", 270, 200, 100, 30, True) |
||||||
|
return |
||||||
|
|
||||||
|
# Controls |
||||||
|
toggle_text = f"Type: {self.leaderboard_type.title()} (Left/Right to toggle)" |
||||||
|
toggle_selected = (self.selected_index == 0) |
||||||
|
self.ui_renderer.draw_button(toggle_text, 120, 90, 400, 25, toggle_selected) |
||||||
|
|
||||||
|
refresh_selected = (self.selected_index == 1) |
||||||
|
back_selected = (self.selected_index == 2) |
||||||
|
self.ui_renderer.draw_button("Refresh", 150, 125, 80, 25, refresh_selected) |
||||||
|
self.ui_renderer.draw_button("← Back", 250, 125, 80, 25, back_selected) |
||||||
|
|
||||||
|
# Leaderboard data |
||||||
|
if not self.leaderboard_data: |
||||||
|
self.ui_renderer.draw_text("No leaderboard data available", 320, 200, |
||||||
|
'yellow', 'medium', center=True) |
||||||
|
else: |
||||||
|
self._render_leaderboard_data() |
||||||
|
|
||||||
|
self.ui_renderer.draw_footer_help(self.get_help_text()) |
||||||
|
|
||||||
|
def _render_leaderboard_data(self) -> None: |
||||||
|
"""Render the leaderboard data table""" |
||||||
|
# Draw leaderboard entries |
||||||
|
panel_height = min(250, len(self.leaderboard_data) * 25 + 40) |
||||||
|
self.ui_renderer.draw_panel(50, 160, 540, panel_height, 'black', 'gray') |
||||||
|
|
||||||
|
# Headers |
||||||
|
self.ui_renderer.draw_text("Rank", 60, 175, 'light_blue', 'small') |
||||||
|
self.ui_renderer.draw_text("Player", 120, 175, 'light_blue', 'small') |
||||||
|
self.ui_renderer.draw_text("Score", 350, 175, 'light_blue', 'small') |
||||||
|
self.ui_renderer.draw_text("Games", 450, 175, 'light_blue', 'small') |
||||||
|
|
||||||
|
if self.leaderboard_type == "global": |
||||||
|
self.ui_renderer.draw_text("Device", 520, 175, 'light_blue', 'small') |
||||||
|
|
||||||
|
# Entries |
||||||
|
for i, entry in enumerate(self.leaderboard_data): |
||||||
|
entry_y = 195 + i * 22 |
||||||
|
|
||||||
|
# Highlight current user |
||||||
|
is_current = (self.data_manager.active_profile and |
||||||
|
entry.get('user_id') == self.data_manager.active_profile) |
||||||
|
text_color = 'light_green' if is_current else 'light_gray' |
||||||
|
|
||||||
|
self.ui_renderer.draw_text(str(entry.get('rank', i+1)), 60, entry_y, text_color, 'tiny') |
||||||
|
|
||||||
|
player_name = entry.get('user_id', 'Unknown')[:20] |
||||||
|
self.ui_renderer.draw_text(player_name, 120, entry_y, text_color, 'tiny') |
||||||
|
|
||||||
|
self.ui_renderer.draw_text(str(entry.get('best_score', 0)), 350, entry_y, text_color, 'tiny') |
||||||
|
self.ui_renderer.draw_text(str(entry.get('total_games', 0)), 450, entry_y, text_color, 'tiny') |
||||||
|
|
||||||
|
if self.leaderboard_type == "global": |
||||||
|
device = entry.get('device_id', '')[:8] |
||||||
|
self.ui_renderer.draw_text(device, 520, entry_y, text_color, 'tiny') |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle leaderboard input""" |
||||||
|
max_index = 2 # Toggle, Refresh, Back |
||||||
|
|
||||||
|
if action == 'up': |
||||||
|
self.navigate_up() |
||||||
|
return True |
||||||
|
elif action == 'down': |
||||||
|
self.navigate_down(max_index) |
||||||
|
return True |
||||||
|
elif action == 'left': |
||||||
|
if self.selected_index == 0: # Toggle leaderboard type |
||||||
|
self.toggle_leaderboard_type() |
||||||
|
return True |
||||||
|
elif action == 'right': |
||||||
|
if self.selected_index == 0: # Toggle leaderboard type |
||||||
|
self.toggle_leaderboard_type() |
||||||
|
return True |
||||||
|
elif action == 'confirm': |
||||||
|
self.handle_confirm() |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
self.handle_back() |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def toggle_leaderboard_type(self) -> None: |
||||||
|
"""Toggle between device and global leaderboard""" |
||||||
|
self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device" |
||||||
|
self.load_leaderboard_data() |
||||||
|
|
||||||
|
def handle_confirm(self) -> None: |
||||||
|
"""Handle leaderboard actions""" |
||||||
|
if self.selected_index == 1: # Refresh |
||||||
|
self.load_leaderboard_data() |
||||||
|
elif self.selected_index == 2: # Back |
||||||
|
self.handle_back() |
||||||
|
|
||||||
|
def load_leaderboard_data(self) -> None: |
||||||
|
"""Load leaderboard data from data manager""" |
||||||
|
self.leaderboard_data = self.data_manager.get_leaderboard_data(self.leaderboard_type) |
||||||
|
|
||||||
|
def reset_state(self) -> None: |
||||||
|
"""Reset screen state when entering""" |
||||||
|
super().reset_state() |
||||||
|
self.load_leaderboard_data() |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for leaderboard""" |
||||||
|
return "Left/Right: Toggle Type • Enter: Select • Escape: Back" |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
""" |
||||||
|
Main Menu Screen |
||||||
|
The primary menu interface for profile management |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
|
||||||
|
class MainMenuScreen(BaseScreen): |
||||||
|
"""Main menu screen implementation""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer, screen_manager): |
||||||
|
super().__init__(data_manager, ui_renderer, screen_manager) |
||||||
|
self.menu_items = [ |
||||||
|
"Create Profile", |
||||||
|
"Select Profile", |
||||||
|
"Edit Settings", |
||||||
|
"Leaderboard", |
||||||
|
"Profile Stats", |
||||||
|
"Exit" |
||||||
|
] |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render main menu screen""" |
||||||
|
# Header |
||||||
|
active_profile = self.data_manager.active_profile |
||||||
|
subtitle = f"Active: {active_profile}" if active_profile else "No active profile" |
||||||
|
self.ui_renderer.draw_header("Profile Manager", subtitle) |
||||||
|
|
||||||
|
# API status |
||||||
|
api_status = "API: Connected" if self.data_manager.api_enabled else "API: Offline" |
||||||
|
api_color = 'light_green' if self.data_manager.api_enabled else 'red' |
||||||
|
self.ui_renderer.draw_text(api_status, 320, 80, api_color, 'tiny', center=True) |
||||||
|
|
||||||
|
# Device ID |
||||||
|
device_text = f"Device: {self.data_manager.device_id}" |
||||||
|
self.ui_renderer.draw_text(device_text, 320, 95, 'light_gray', 'tiny', center=True) |
||||||
|
|
||||||
|
# Draw menu panel |
||||||
|
self.ui_renderer.draw_panel(120, 120, 400, 240, 'dark_gray', 'gray') |
||||||
|
|
||||||
|
# Draw menu buttons |
||||||
|
for i, item in enumerate(self.menu_items): |
||||||
|
button_y = 135 + i * 38 |
||||||
|
selected = (i == self.selected_index) |
||||||
|
|
||||||
|
# Gray out unavailable options |
||||||
|
disabled = False |
||||||
|
display_text = item |
||||||
|
|
||||||
|
if (item in ["Edit Settings", "Profile Stats"]) and not active_profile: |
||||||
|
disabled = True |
||||||
|
display_text = f"{item} (No Profile)" |
||||||
|
elif item == "Leaderboard" and not self.data_manager.api_enabled: |
||||||
|
disabled = True |
||||||
|
display_text = f"{item} (Offline)" |
||||||
|
|
||||||
|
self.ui_renderer.draw_button(display_text, 180, button_y, 280, 30, |
||||||
|
selected, disabled) |
||||||
|
|
||||||
|
# Help text |
||||||
|
self.ui_renderer.draw_footer_help(self.get_help_text()) |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle main menu input""" |
||||||
|
if action == 'up': |
||||||
|
self.navigate_up() |
||||||
|
return True |
||||||
|
elif action == 'down': |
||||||
|
self.navigate_down(len(self.menu_items) - 1) |
||||||
|
return True |
||||||
|
elif action == 'confirm': |
||||||
|
self.handle_confirm() |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
# Exit application from main menu |
||||||
|
return False |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def handle_confirm(self) -> None: |
||||||
|
"""Handle main menu selections""" |
||||||
|
index = self.selected_index |
||||||
|
|
||||||
|
if index == 0: # Create Profile |
||||||
|
self.screen_manager.set_screen("create_profile") |
||||||
|
elif index == 1: # Select Profile |
||||||
|
self.screen_manager.set_screen("profile_list") |
||||||
|
elif index == 2: # Settings |
||||||
|
if self.data_manager.active_profile: |
||||||
|
self.screen_manager.set_screen("edit_profile") |
||||||
|
elif index == 3: # Leaderboard |
||||||
|
if self.data_manager.api_enabled: |
||||||
|
self.screen_manager.set_screen("leaderboard") |
||||||
|
elif index == 4: # Profile Stats |
||||||
|
if self.data_manager.active_profile: |
||||||
|
self.screen_manager.set_screen("profile_stats") |
||||||
|
elif index == 5: # Exit |
||||||
|
return False |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for main menu""" |
||||||
|
return "↑↓ Navigate • Enter Confirm • Escape Exit" |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
""" |
||||||
|
Profile List Screen |
||||||
|
Display and select from existing user profiles |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
|
||||||
|
class ProfileListScreen(BaseScreen): |
||||||
|
"""Profile list selection screen implementation""" |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render profile selection screen""" |
||||||
|
self.ui_renderer.draw_header("Select Profile") |
||||||
|
|
||||||
|
profile_names = self.data_manager.get_profile_list() |
||||||
|
|
||||||
|
if not profile_names: |
||||||
|
# No profiles message |
||||||
|
self.ui_renderer.draw_panel(120, 100, 400, 150, 'dark_gray', 'gray') |
||||||
|
self.ui_renderer.draw_text("No profiles found", 320, 150, 'yellow', 'large', center=True) |
||||||
|
self.ui_renderer.draw_text("Create one first", 320, 180, 'light_gray', 'medium', center=True) |
||||||
|
self.ui_renderer.draw_button("← Back", 270, 300, 100, 30, True) |
||||||
|
else: |
||||||
|
# Profile list |
||||||
|
panel_height = min(280, len(profile_names) * 55 + 60) |
||||||
|
self.ui_renderer.draw_panel(30, 80, 580, panel_height, 'black', 'gray') |
||||||
|
|
||||||
|
for i, name in enumerate(profile_names): |
||||||
|
profile = self.data_manager.get_profile(name) |
||||||
|
entry_y = 95 + i * 50 |
||||||
|
selected = (i == self.selected_index) |
||||||
|
|
||||||
|
# Profile stats |
||||||
|
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}" |
||||||
|
indicator = "★" if name == self.data_manager.active_profile else "" |
||||||
|
|
||||||
|
self.ui_renderer.draw_list_item(name, 40, entry_y, 560, 40, |
||||||
|
selected, stats_text, indicator) |
||||||
|
|
||||||
|
# Back button |
||||||
|
back_y = 95 + len(profile_names) * 50 + 10 |
||||||
|
back_selected = (self.selected_index == len(profile_names)) |
||||||
|
self.ui_renderer.draw_button("← Back", 270, back_y, 100, 30, back_selected) |
||||||
|
|
||||||
|
self.ui_renderer.draw_footer_help(self.get_help_text()) |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle profile list input""" |
||||||
|
profile_names = self.data_manager.get_profile_list() |
||||||
|
max_index = len(profile_names) # Includes back button |
||||||
|
|
||||||
|
if action == 'up': |
||||||
|
self.navigate_up() |
||||||
|
return True |
||||||
|
elif action == 'down': |
||||||
|
self.navigate_down(max_index) |
||||||
|
return True |
||||||
|
elif action == 'confirm': |
||||||
|
self.handle_confirm() |
||||||
|
return True |
||||||
|
elif action == 'back': |
||||||
|
self.handle_back() |
||||||
|
return True |
||||||
|
elif action == 'delete': |
||||||
|
self.handle_delete() |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def handle_confirm(self) -> None: |
||||||
|
"""Handle profile list selections""" |
||||||
|
profile_names = self.data_manager.get_profile_list() |
||||||
|
|
||||||
|
if self.selected_index < len(profile_names): |
||||||
|
# Select profile |
||||||
|
profile_name = profile_names[self.selected_index] |
||||||
|
success, message = self.data_manager.set_active_profile(profile_name) |
||||||
|
if success: |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
else: |
||||||
|
self.show_error(message) |
||||||
|
else: |
||||||
|
# Back option |
||||||
|
self.handle_back() |
||||||
|
|
||||||
|
def handle_delete(self) -> None: |
||||||
|
"""Handle profile deletion""" |
||||||
|
profile_names = self.data_manager.get_profile_list() |
||||||
|
|
||||||
|
if self.selected_index < len(profile_names): |
||||||
|
profile_name = profile_names[self.selected_index] |
||||||
|
success, message = self.data_manager.delete_profile(profile_name) |
||||||
|
if not success: |
||||||
|
self.show_error(message) |
||||||
|
else: |
||||||
|
# Adjust selection if needed |
||||||
|
max_index = max(0, len(self.data_manager.get_profile_list()) - 1) |
||||||
|
self.selected_index = min(self.selected_index, max_index) |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for profile list""" |
||||||
|
return "Enter: Select • Escape: Back • Delete: Remove" |
||||||
@ -0,0 +1,106 @@ |
|||||||
|
""" |
||||||
|
Profile Stats Screen |
||||||
|
Display detailed statistics for the active user profile |
||||||
|
""" |
||||||
|
|
||||||
|
from .base_screen import BaseScreen |
||||||
|
|
||||||
|
|
||||||
|
class ProfileStatsScreen(BaseScreen): |
||||||
|
"""Profile statistics display screen implementation""" |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render detailed profile statistics""" |
||||||
|
if not self.data_manager.active_profile: |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
return |
||||||
|
|
||||||
|
stats_data = self.data_manager.get_profile_stats(self.data_manager.active_profile) |
||||||
|
if not stats_data: |
||||||
|
self.screen_manager.set_screen("main_menu") |
||||||
|
return |
||||||
|
|
||||||
|
profile = stats_data['profile'] |
||||||
|
|
||||||
|
# Header |
||||||
|
self.ui_renderer.draw_header(f"Stats: {profile.name}") |
||||||
|
|
||||||
|
# Stats panel |
||||||
|
self.ui_renderer.draw_panel(50, 90, 540, 260, 'dark_gray', 'gray') |
||||||
|
|
||||||
|
# Render statistics content |
||||||
|
self._render_local_statistics(stats_data, profile) |
||||||
|
self._render_server_status(stats_data) |
||||||
|
self._render_settings_summary(profile) |
||||||
|
|
||||||
|
# Back button |
||||||
|
back_selected = (self.selected_index == 0) |
||||||
|
self.ui_renderer.draw_button("← Back", 270, 370, 100, 30, back_selected) |
||||||
|
|
||||||
|
self.ui_renderer.draw_footer_help(self.get_help_text()) |
||||||
|
|
||||||
|
def _render_local_statistics(self, stats_data, profile) -> None: |
||||||
|
"""Render local profile statistics""" |
||||||
|
y = 110 |
||||||
|
self.ui_renderer.draw_text("Local Statistics:", 60, y, 'light_blue', 'medium') |
||||||
|
y += 25 |
||||||
|
|
||||||
|
stats = [ |
||||||
|
f"Games Played: {profile.games_played}", |
||||||
|
f"Best Score: {profile.best_score}", |
||||||
|
f"Total Score: {profile.total_score}", |
||||||
|
f"Achievements: {len(profile.achievements)}", |
||||||
|
f"Created: {stats_data['created_formatted']}", |
||||||
|
f"Last Played: {stats_data['last_played_formatted']}" |
||||||
|
] |
||||||
|
|
||||||
|
for stat in stats: |
||||||
|
self.ui_renderer.draw_text(stat, 70, y, 'light_gray', 'small') |
||||||
|
y += 20 |
||||||
|
|
||||||
|
def _render_server_status(self, stats_data) -> None: |
||||||
|
"""Render server connection and sync status""" |
||||||
|
y = 250 |
||||||
|
|
||||||
|
if stats_data['api_enabled']: |
||||||
|
self.ui_renderer.draw_text("Server Status:", 60, y, 'light_green', 'medium') |
||||||
|
y += 20 |
||||||
|
integration_info = stats_data['integration_info'] |
||||||
|
device_id = integration_info.get('device_id', 'Unknown') |
||||||
|
self.ui_renderer.draw_text(f"Device ID: {device_id}", 70, y, 'light_gray', 'small') |
||||||
|
y += 20 |
||||||
|
self.ui_renderer.draw_text("✓ Profile synced with server", 70, y, 'light_green', 'small') |
||||||
|
else: |
||||||
|
self.ui_renderer.draw_text("Server Status:", 60, y, 'red', 'medium') |
||||||
|
y += 20 |
||||||
|
self.ui_renderer.draw_text("✗ Server offline", 70, y, 'red', 'small') |
||||||
|
|
||||||
|
def _render_settings_summary(self, profile) -> None: |
||||||
|
"""Render current profile settings summary""" |
||||||
|
settings_x = 350 |
||||||
|
y = 110 |
||||||
|
self.ui_renderer.draw_text("Current Settings:", settings_x, y, 'light_blue', 'medium') |
||||||
|
y += 25 |
||||||
|
|
||||||
|
settings_display = [ |
||||||
|
f"Difficulty: {profile.settings.get('difficulty', 'normal').title()}", |
||||||
|
f"Sound Volume: {profile.settings.get('sound_volume', 50)}%", |
||||||
|
f"Music Volume: {profile.settings.get('music_volume', 50)}%", |
||||||
|
f"Screen Shake: {'On' if profile.settings.get('screen_shake', True) else 'Off'}" |
||||||
|
] |
||||||
|
|
||||||
|
for setting in settings_display: |
||||||
|
self.ui_renderer.draw_text(setting, settings_x + 10, y, 'light_gray', 'small') |
||||||
|
y += 20 |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
"""Handle profile stats input""" |
||||||
|
if action == 'confirm' or action == 'back': |
||||||
|
self.handle_back() |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def get_help_text(self) -> str: |
||||||
|
"""Get help text for profile stats""" |
||||||
|
return "Enter: Back • Escape: Back to Main Menu" |
||||||
@ -0,0 +1,142 @@ |
|||||||
|
""" |
||||||
|
Screen Manager Module |
||||||
|
Manages individual screen modules and navigation between them |
||||||
|
""" |
||||||
|
|
||||||
|
from typing import Dict, Optional |
||||||
|
from screens import ( |
||||||
|
BaseScreen, |
||||||
|
MainMenuScreen, |
||||||
|
ProfileListScreen, |
||||||
|
CreateProfileScreen, |
||||||
|
EditProfileScreen, |
||||||
|
LeaderboardScreen, |
||||||
|
ProfileStatsScreen |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
class ModularScreenManager: |
||||||
|
"""Manages individual screen modules and navigation""" |
||||||
|
|
||||||
|
def __init__(self, data_manager, ui_renderer): |
||||||
|
""" |
||||||
|
Initialize screen manager with all screen modules |
||||||
|
|
||||||
|
Args: |
||||||
|
data_manager: ProfileDataManager instance |
||||||
|
ui_renderer: UIRenderer instance |
||||||
|
""" |
||||||
|
self.data_manager = data_manager |
||||||
|
self.ui_renderer = ui_renderer |
||||||
|
|
||||||
|
# Current screen state |
||||||
|
self.current_screen_name = "main_menu" |
||||||
|
self.screens: Dict[str, BaseScreen] = {} |
||||||
|
|
||||||
|
# Initialize all screen modules |
||||||
|
self._initialize_screens() |
||||||
|
|
||||||
|
# Error state |
||||||
|
self.show_error = False |
||||||
|
self.error_message = "" |
||||||
|
self.error_timer = 0 |
||||||
|
|
||||||
|
def _initialize_screens(self) -> None: |
||||||
|
"""Initialize all screen module instances""" |
||||||
|
self.screens = { |
||||||
|
"main_menu": MainMenuScreen(self.data_manager, self.ui_renderer, self), |
||||||
|
"profile_list": ProfileListScreen(self.data_manager, self.ui_renderer, self), |
||||||
|
"create_profile": CreateProfileScreen(self.data_manager, self.ui_renderer, self), |
||||||
|
"edit_profile": EditProfileScreen(self.data_manager, self.ui_renderer, self), |
||||||
|
"leaderboard": LeaderboardScreen(self.data_manager, self.ui_renderer, self), |
||||||
|
"profile_stats": ProfileStatsScreen(self.data_manager, self.ui_renderer, self) |
||||||
|
} |
||||||
|
|
||||||
|
def set_screen(self, screen_name: str) -> None: |
||||||
|
""" |
||||||
|
Switch to a different screen |
||||||
|
|
||||||
|
Args: |
||||||
|
screen_name: Name of the screen to switch to |
||||||
|
""" |
||||||
|
if screen_name in self.screens: |
||||||
|
self.current_screen_name = screen_name |
||||||
|
# Reset state when entering new screen |
||||||
|
self.screens[screen_name].reset_state() |
||||||
|
else: |
||||||
|
print(f"Warning: Unknown screen '{screen_name}'") |
||||||
|
|
||||||
|
def get_current_screen(self) -> Optional[BaseScreen]: |
||||||
|
"""Get the current active screen module""" |
||||||
|
return self.screens.get(self.current_screen_name) |
||||||
|
|
||||||
|
def handle_input(self, action: str) -> bool: |
||||||
|
""" |
||||||
|
Handle input by delegating to current screen |
||||||
|
|
||||||
|
Args: |
||||||
|
action: Input action string |
||||||
|
|
||||||
|
Returns: |
||||||
|
bool: True if input was handled and app should continue, False to exit |
||||||
|
""" |
||||||
|
# Dismiss error dialog on any input |
||||||
|
if self.show_error: |
||||||
|
self.show_error = False |
||||||
|
return True |
||||||
|
|
||||||
|
current_screen = self.get_current_screen() |
||||||
|
if current_screen: |
||||||
|
result = current_screen.handle_input(action) |
||||||
|
|
||||||
|
# Handle special case for main menu exit |
||||||
|
if self.current_screen_name == "main_menu" and action == 'back': |
||||||
|
return False # Exit application |
||||||
|
|
||||||
|
return result if result is not None else True |
||||||
|
|
||||||
|
return True |
||||||
|
|
||||||
|
def render(self) -> None: |
||||||
|
"""Render current screen and any overlays""" |
||||||
|
current_screen = self.get_current_screen() |
||||||
|
if current_screen: |
||||||
|
current_screen.render() |
||||||
|
|
||||||
|
# Render error dialog if active |
||||||
|
if self.show_error: |
||||||
|
import time |
||||||
|
self.ui_renderer.draw_error_dialog(self.error_message) |
||||||
|
# Auto-dismiss after timer |
||||||
|
if time.time() > self.error_timer: |
||||||
|
self.show_error = False |
||||||
|
|
||||||
|
def show_error_dialog(self, message: str) -> None: |
||||||
|
""" |
||||||
|
Show an error dialog overlay |
||||||
|
|
||||||
|
Args: |
||||||
|
message: Error message to display |
||||||
|
""" |
||||||
|
import time |
||||||
|
self.show_error = True |
||||||
|
self.error_message = message |
||||||
|
self.error_timer = time.time() + 3.0 |
||||||
|
|
||||||
|
@property |
||||||
|
def current_screen(self) -> str: |
||||||
|
"""Get current screen name (for compatibility)""" |
||||||
|
return self.current_screen_name |
||||||
|
|
||||||
|
@property |
||||||
|
def selected_index(self) -> int: |
||||||
|
"""Get selected index from current screen (for compatibility)""" |
||||||
|
current_screen = self.get_current_screen() |
||||||
|
return current_screen.selected_index if current_screen else 0 |
||||||
|
|
||||||
|
@selected_index.setter |
||||||
|
def selected_index(self, value: int) -> None: |
||||||
|
"""Set selected index on current screen (for compatibility)""" |
||||||
|
current_screen = self.get_current_screen() |
||||||
|
if current_screen: |
||||||
|
current_screen.selected_index = value |
||||||
@ -0,0 +1,444 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
Reusable UI Components for SDL2-based applications |
||||||
|
Provides common graphics operations and UI elements |
||||||
|
""" |
||||||
|
|
||||||
|
import os |
||||||
|
import time |
||||||
|
from typing import Dict, List, Optional, Tuple, Any |
||||||
|
import sdl2 |
||||||
|
import sdl2.ext |
||||||
|
|
||||||
|
|
||||||
|
class UIColors: |
||||||
|
"""Standard color palette for UI components""" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
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) |
||||||
|
} |
||||||
|
|
||||||
|
def get(self, color_name: str): |
||||||
|
"""Get color by name""" |
||||||
|
return self.colors.get(color_name, self.colors['white']) |
||||||
|
|
||||||
|
|
||||||
|
class FontManager: |
||||||
|
"""Manages multiple font sizes for the application""" |
||||||
|
|
||||||
|
def __init__(self, font_path: Optional[str] = None): |
||||||
|
self.font_path = font_path or self._find_default_font() |
||||||
|
self.fonts = {} |
||||||
|
self._initialize_fonts() |
||||||
|
|
||||||
|
def _find_default_font(self) -> Optional[str]: |
||||||
|
"""Find a suitable default font""" |
||||||
|
font_paths = [ |
||||||
|
"assets/decterm.ttf", |
||||||
|
"./assets/terminal.ttf", |
||||||
|
"./assets/AmaticSC-Regular.ttf" |
||||||
|
] |
||||||
|
|
||||||
|
for path in font_paths: |
||||||
|
if os.path.exists(path): |
||||||
|
return path |
||||||
|
return None |
||||||
|
|
||||||
|
def _initialize_fonts(self): |
||||||
|
"""Initialize font managers for different sizes""" |
||||||
|
sizes = { |
||||||
|
'title': 36, |
||||||
|
'large': 28, |
||||||
|
'medium': 22, |
||||||
|
'small': 18, |
||||||
|
'tiny': 14 |
||||||
|
} |
||||||
|
|
||||||
|
for name, size in sizes.items(): |
||||||
|
self.fonts[name] = sdl2.ext.FontManager( |
||||||
|
font_path=self.font_path, |
||||||
|
size=size |
||||||
|
) |
||||||
|
|
||||||
|
def get(self, size: str): |
||||||
|
"""Get font manager by size name""" |
||||||
|
return self.fonts.get(size, self.fonts['medium']) |
||||||
|
|
||||||
|
|
||||||
|
class UIRenderer: |
||||||
|
"""Main UI rendering class with reusable drawing operations""" |
||||||
|
|
||||||
|
def __init__(self, renderer: sdl2.ext.Renderer, window_size: Tuple[int, int] = (640, 480)): |
||||||
|
self.renderer = renderer |
||||||
|
self.window_width, self.window_height = window_size |
||||||
|
self.colors = UIColors() |
||||||
|
self.fonts = FontManager() |
||||||
|
self.sprite_factory = sdl2.ext.SpriteFactory(renderer=renderer) |
||||||
|
|
||||||
|
def clear_screen(self, color_name: str = 'black'): |
||||||
|
"""Clear screen with specified color""" |
||||||
|
self.renderer.clear(self.colors.get(color_name)) |
||||||
|
|
||||||
|
def present(self): |
||||||
|
"""Present the rendered frame""" |
||||||
|
self.renderer.present() |
||||||
|
|
||||||
|
def draw_background_pattern(self, pattern_type: str = 'gradient'): |
||||||
|
"""Draw subtle background patterns""" |
||||||
|
if pattern_type == 'gradient': |
||||||
|
for y in range(0, self.window_height, 20): |
||||||
|
alpha = int(20 * (1 - y / self.window_height)) |
||||||
|
if alpha > 5: |
||||||
|
color = sdl2.ext.Color(alpha, alpha, alpha * 2) |
||||||
|
self.renderer.draw_line((0, y, self.window_width, y), color) |
||||||
|
|
||||||
|
def draw_text(self, text: str, x: int, y: int, color_name: str = 'white', |
||||||
|
font_size: str = 'medium', center: bool = False) -> Tuple[int, int]: |
||||||
|
"""Draw text on screen with improved styling""" |
||||||
|
if not text: |
||||||
|
return (0, 0) |
||||||
|
|
||||||
|
color = self.colors.get(color_name) |
||||||
|
font = self.fonts.get(font_size) |
||||||
|
text_sprite = self.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: str = 'dark_gray', border_color: str = 'gray', |
||||||
|
border_width: int = 2): |
||||||
|
"""Draw a styled panel/box""" |
||||||
|
bg = self.colors.get(bg_color) |
||||||
|
border = self.colors.get(border_color) |
||||||
|
|
||||||
|
# Fill background |
||||||
|
self.renderer.fill((x, y, width, height), bg) |
||||||
|
|
||||||
|
# Draw border |
||||||
|
for i in range(border_width): |
||||||
|
self.renderer.draw_rect((x + i, y + i, width - 2*i, height - 2*i), border) |
||||||
|
|
||||||
|
def draw_button(self, text: str, x: int, y: int, width: int, height: int, |
||||||
|
selected: bool = False, disabled: bool = False, |
||||||
|
font_size: str = 'medium'): |
||||||
|
"""Draw a styled button""" |
||||||
|
# Button colors based on state |
||||||
|
if disabled: |
||||||
|
bg_color = 'black' |
||||||
|
border_color = 'dark_gray' |
||||||
|
text_color = 'dark_gray' |
||||||
|
elif selected: |
||||||
|
bg_color = 'blue' |
||||||
|
border_color = 'light_blue' |
||||||
|
text_color = 'white' |
||||||
|
else: |
||||||
|
bg_color = 'dark_gray' |
||||||
|
border_color = 'gray' |
||||||
|
text_color = '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, font_size, center=True) |
||||||
|
|
||||||
|
def draw_header(self, title: str, subtitle: str = '', y_pos: int = 0, |
||||||
|
height: int = 80, bg_color: str = 'dark_gray'): |
||||||
|
"""Draw a header section with title and optional subtitle""" |
||||||
|
self.renderer.fill((0, y_pos, self.window_width, height), self.colors.get(bg_color)) |
||||||
|
|
||||||
|
title_y = y_pos + 15 |
||||||
|
self.draw_text(title, self.window_width // 2, title_y, 'light_blue', 'title', center=True) |
||||||
|
|
||||||
|
if subtitle: |
||||||
|
subtitle_y = title_y + 35 |
||||||
|
self.draw_text(subtitle, self.window_width // 2, subtitle_y, 'light_green', 'medium', center=True) |
||||||
|
|
||||||
|
def draw_footer_help(self, help_text: str, y_pos: int = None): |
||||||
|
"""Draw footer with help text""" |
||||||
|
if y_pos is None: |
||||||
|
y_pos = self.window_height - 60 |
||||||
|
|
||||||
|
self.draw_panel(10, y_pos, self.window_width - 20, 40, 'black', 'dark_gray') |
||||||
|
self.draw_text(help_text, self.window_width // 2, y_pos + 15, |
||||||
|
'light_gray', 'tiny', center=True) |
||||||
|
|
||||||
|
def draw_list_item(self, text: str, x: int, y: int, width: int, height: int, |
||||||
|
selected: bool = False, secondary_text: str = '', |
||||||
|
indicator: str = ''): |
||||||
|
"""Draw a list item with optional secondary text and indicator""" |
||||||
|
# Background |
||||||
|
bg_color = 'blue' if selected else 'dark_gray' |
||||||
|
border_color = 'light_blue' if selected else 'gray' |
||||||
|
text_color = 'white' if selected else 'light_gray' |
||||||
|
|
||||||
|
self.draw_panel(x, y, width, height, bg_color, border_color) |
||||||
|
|
||||||
|
# Main text |
||||||
|
self.draw_text(text, x + 10, y + 5, text_color, 'medium') |
||||||
|
|
||||||
|
# Secondary text |
||||||
|
if secondary_text: |
||||||
|
secondary_color = 'light_gray' if selected else 'gray' |
||||||
|
self.draw_text(secondary_text, x + 10, y + 22, secondary_color, 'tiny') |
||||||
|
|
||||||
|
# Indicator (like ★ for active item) |
||||||
|
if indicator: |
||||||
|
indicator_x = x + width - 30 |
||||||
|
indicator_color = 'light_green' if selected else 'yellow' |
||||||
|
self.draw_text(indicator, indicator_x, y + height // 2 - 8, |
||||||
|
indicator_color, 'small', center=True) |
||||||
|
|
||||||
|
def draw_error_dialog(self, message: str, title: str = "ERROR"): |
||||||
|
"""Draw an error dialog overlay""" |
||||||
|
# Semi-transparent overlay |
||||||
|
overlay_color = sdl2.ext.Color(0, 0, 0, 128) |
||||||
|
self.renderer.fill((0, 0, self.window_width, self.window_height), overlay_color) |
||||||
|
|
||||||
|
# Error dialog box |
||||||
|
dialog_width = 400 |
||||||
|
dialog_height = 120 |
||||||
|
dialog_x = (self.window_width - dialog_width) // 2 |
||||||
|
dialog_y = (self.window_height - dialog_height) // 2 |
||||||
|
|
||||||
|
# Dialog background with red border |
||||||
|
self.draw_panel(dialog_x, dialog_y, dialog_width, dialog_height, |
||||||
|
'black', 'red', border_width=4) |
||||||
|
|
||||||
|
# Error title |
||||||
|
self.draw_text(title, dialog_x + dialog_width // 2, dialog_y + 20, |
||||||
|
'red', 'large', center=True) |
||||||
|
|
||||||
|
# Error message |
||||||
|
self.draw_text(message, dialog_x + dialog_width // 2, dialog_y + 50, |
||||||
|
'white', 'medium', center=True) |
||||||
|
|
||||||
|
# Dismiss instruction |
||||||
|
self.draw_text("Press any key to continue...", |
||||||
|
dialog_x + dialog_width // 2, dialog_y + 80, |
||||||
|
'light_gray', 'small', center=True) |
||||||
|
|
||||||
|
def draw_virtual_keyboard(self, keyboard_layout: List[List[str]], |
||||||
|
cursor_x: int, cursor_y: int, |
||||||
|
start_x: int = 50, start_y: int = 150): |
||||||
|
"""Draw a virtual keyboard interface""" |
||||||
|
key_width = 45 |
||||||
|
key_height = 30 |
||||||
|
|
||||||
|
for row_idx, row in enumerate(keyboard_layout): |
||||||
|
row_y = start_y + row_idx * (key_height + 5) |
||||||
|
|
||||||
|
# Special handling for bottom row (commands) |
||||||
|
if row_idx == len(keyboard_layout) - 1: |
||||||
|
key_widths = [80] * len(row) # Wider keys for commands |
||||||
|
x_offset = start_x + 90 # Center the bottom row |
||||||
|
else: |
||||||
|
key_widths = [key_width] * len(row) |
||||||
|
x_offset = start_x |
||||||
|
|
||||||
|
current_x = x_offset |
||||||
|
for col_idx, char in enumerate(row): |
||||||
|
selected = (row_idx == cursor_y and col_idx == cursor_x) |
||||||
|
|
||||||
|
# Key styling |
||||||
|
if selected: |
||||||
|
bg_color = 'blue' |
||||||
|
border_color = 'light_blue' |
||||||
|
text_color = 'white' |
||||||
|
else: |
||||||
|
bg_color = 'dark_gray' |
||||||
|
border_color = 'gray' |
||||||
|
text_color = 'light_gray' |
||||||
|
|
||||||
|
self.draw_panel(current_x, row_y, key_widths[col_idx], key_height, |
||||||
|
bg_color, border_color) |
||||||
|
|
||||||
|
# Key text |
||||||
|
display_char = self._format_keyboard_char(char) |
||||||
|
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 |
||||||
|
|
||||||
|
def _format_keyboard_char(self, char: str) -> str: |
||||||
|
"""Format keyboard character for display""" |
||||||
|
char_map = { |
||||||
|
'<DEL': 'DEL', |
||||||
|
'SPACE': 'SPC' |
||||||
|
} |
||||||
|
return char_map.get(char, char) |
||||||
|
|
||||||
|
def draw_input_field(self, text: str, x: int, y: int, width: int, height: int, |
||||||
|
active: bool = False, placeholder: str = ''): |
||||||
|
"""Draw an input text field""" |
||||||
|
# Field styling |
||||||
|
if active: |
||||||
|
bg_color = 'white' |
||||||
|
border_color = 'blue' |
||||||
|
text_color = 'black' |
||||||
|
else: |
||||||
|
bg_color = 'light_gray' |
||||||
|
border_color = 'gray' |
||||||
|
text_color = 'black' if text else 'gray' |
||||||
|
|
||||||
|
self.draw_panel(x, y, width, height, bg_color, border_color, 2) |
||||||
|
|
||||||
|
# Display text or placeholder |
||||||
|
display_text = text if text else placeholder |
||||||
|
self.draw_text(display_text, x + 10, y + 8, |
||||||
|
'black' if text else 'gray', 'medium') |
||||||
|
|
||||||
|
def draw_progress_bar(self, value: int, max_value: int, x: int, y: int, |
||||||
|
width: int, height: int = 20, |
||||||
|
bg_color: str = 'dark_gray', fill_color: str = 'green'): |
||||||
|
"""Draw a progress bar""" |
||||||
|
# Background |
||||||
|
self.draw_panel(x, y, width, height, bg_color, 'gray') |
||||||
|
|
||||||
|
# Fill |
||||||
|
if max_value > 0: |
||||||
|
fill_width = int(width * (value / max_value)) |
||||||
|
if fill_width > 0: |
||||||
|
self.renderer.fill((x + 2, y + 2, fill_width - 4, height - 4), |
||||||
|
self.colors.get(fill_color)) |
||||||
|
|
||||||
|
# Value text |
||||||
|
text = f"{value}/{max_value}" |
||||||
|
text_x = x + width // 2 |
||||||
|
text_y = y + 3 |
||||||
|
self.draw_text(text, text_x, text_y, 'white', 'tiny', center=True) |
||||||
|
|
||||||
|
def draw_menu_navigation_arrows(self, selected_index: int, total_items: int, |
||||||
|
x: int, y: int): |
||||||
|
"""Draw navigation arrows for selected menu items""" |
||||||
|
if selected_index > 0: |
||||||
|
self.draw_text("▲", x, y - 15, 'white', 'small', center=True) |
||||||
|
if selected_index < total_items - 1: |
||||||
|
self.draw_text("▼", x, y + 15, 'white', 'small', center=True) |
||||||
|
|
||||||
|
|
||||||
|
class GamepadInputHandler: |
||||||
|
"""Handle gamepad input with debouncing and mapping""" |
||||||
|
|
||||||
|
def __init__(self, debounce_delay: float = 0.15): |
||||||
|
self.gamepad = None |
||||||
|
self.button_states = {} |
||||||
|
self.last_button_time = {} |
||||||
|
self.debounce_delay = debounce_delay |
||||||
|
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) |
||||||
|
if self.gamepad: |
||||||
|
print(f"Gamepad detected: {sdl2.SDL_JoystickName(self.gamepad).decode()}") |
||||||
|
else: |
||||||
|
print("No gamepad detected - using keyboard fallback") |
||||||
|
|
||||||
|
def can_process_input(self, input_key: str, current_time: float) -> bool: |
||||||
|
"""Check if enough time has passed to process input (debouncing)""" |
||||||
|
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] > self.debounce_delay: |
||||||
|
self.last_button_time[input_key] = current_time |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def get_gamepad_input(self) -> Dict[str, bool]: |
||||||
|
"""Get current gamepad input state""" |
||||||
|
if not self.gamepad: |
||||||
|
return {} |
||||||
|
|
||||||
|
current_time = time.time() |
||||||
|
inputs = {} |
||||||
|
|
||||||
|
# D-pad navigation |
||||||
|
hat_state = sdl2.SDL_JoystickGetHat(self.gamepad, 0) |
||||||
|
|
||||||
|
if hat_state & sdl2.SDL_HAT_UP and self.can_process_input('up', current_time): |
||||||
|
inputs['up'] = True |
||||||
|
if hat_state & sdl2.SDL_HAT_DOWN and self.can_process_input('down', current_time): |
||||||
|
inputs['down'] = True |
||||||
|
if hat_state & sdl2.SDL_HAT_LEFT and self.can_process_input('left', current_time): |
||||||
|
inputs['left'] = True |
||||||
|
if hat_state & sdl2.SDL_HAT_RIGHT and self.can_process_input('right', current_time): |
||||||
|
inputs['right'] = True |
||||||
|
|
||||||
|
# Buttons |
||||||
|
button_count = sdl2.SDL_JoystickNumButtons(self.gamepad) |
||||||
|
for i in range(min(button_count, 16)): |
||||||
|
if sdl2.SDL_JoystickGetButton(self.gamepad, i): |
||||||
|
button_key = f'button_{i}' |
||||||
|
if self.can_process_input(button_key, current_time): |
||||||
|
inputs[button_key] = True |
||||||
|
|
||||||
|
return inputs |
||||||
|
|
||||||
|
def cleanup(self): |
||||||
|
"""Cleanup gamepad resources""" |
||||||
|
if self.gamepad: |
||||||
|
sdl2.SDL_JoystickClose(self.gamepad) |
||||||
|
self.gamepad = None |
||||||
|
|
||||||
|
|
||||||
|
class ScreenManager: |
||||||
|
"""Manage different screens/states in the application""" |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
self.current_screen = "main_menu" |
||||||
|
self.screen_stack = [] |
||||||
|
self.selected_index = 0 |
||||||
|
self.screen_data = {} |
||||||
|
|
||||||
|
def push_screen(self, screen_name: str, data: Dict[str, Any] = None): |
||||||
|
"""Push a new screen onto the stack""" |
||||||
|
self.screen_stack.append({ |
||||||
|
'screen': self.current_screen, |
||||||
|
'index': self.selected_index, |
||||||
|
'data': self.screen_data.copy() |
||||||
|
}) |
||||||
|
self.current_screen = screen_name |
||||||
|
self.selected_index = 0 |
||||||
|
self.screen_data = data or {} |
||||||
|
|
||||||
|
def pop_screen(self): |
||||||
|
"""Pop the previous screen from the stack""" |
||||||
|
if self.screen_stack: |
||||||
|
previous = self.screen_stack.pop() |
||||||
|
self.current_screen = previous['screen'] |
||||||
|
self.selected_index = previous['index'] |
||||||
|
self.screen_data = previous['data'] |
||||||
|
|
||||||
|
def set_screen(self, screen_name: str, data: Dict[str, Any] = None): |
||||||
|
"""Set current screen without using stack""" |
||||||
|
self.current_screen = screen_name |
||||||
|
self.selected_index = 0 |
||||||
|
self.screen_data = data or {} |
||||||
@ -0,0 +1,339 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
User Profile Integration Module |
||||||
|
Provides integration between games and the user profile system |
||||||
|
""" |
||||||
|
|
||||||
|
import json |
||||||
|
import uuid |
||||||
|
import platform |
||||||
|
import hashlib |
||||||
|
from datetime import datetime |
||||||
|
from score_api_client import ScoreAPIClient |
||||||
|
|
||||||
|
|
||||||
|
class UserProfileIntegration: |
||||||
|
"""Integration layer between the game and profile system""" |
||||||
|
|
||||||
|
def __init__(self, profiles_file="user_profiles.json", api_url="http://172.27.23.245:8000"): |
||||||
|
self.profiles_file = profiles_file |
||||||
|
self.current_profile = None |
||||||
|
self.device_id = self.generate_device_id() |
||||||
|
self.api_client = ScoreAPIClient(api_url) |
||||||
|
self.api_enabled = self.api_client.is_server_available() |
||||||
|
self.load_active_profile() |
||||||
|
|
||||||
|
if self.api_enabled: |
||||||
|
print(f"✓ Connected to score server at {api_url}") |
||||||
|
else: |
||||||
|
print(f"✗ Score server not available at {api_url} - running offline") |
||||||
|
|
||||||
|
def generate_device_id(self): |
||||||
|
"""Generate a unique device ID based on system information""" |
||||||
|
# Get system information |
||||||
|
system_info = f"{platform.system()}-{platform.machine()}-{platform.processor()}" |
||||||
|
|
||||||
|
# Try to get MAC address for more uniqueness |
||||||
|
try: |
||||||
|
mac = ':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) |
||||||
|
for ele in range(0,8*6,8)][::-1]) |
||||||
|
system_info += f"-{mac}" |
||||||
|
except: |
||||||
|
pass |
||||||
|
|
||||||
|
# Create hash and take first 8 characters |
||||||
|
device_hash = hashlib.md5(system_info.encode()).hexdigest()[:8].upper() |
||||||
|
return f"DEV-{device_hash}" |
||||||
|
|
||||||
|
def load_active_profile(self): |
||||||
|
"""Load the currently active profile""" |
||||||
|
try: |
||||||
|
with open(self.profiles_file, 'r') as f: |
||||||
|
data = json.load(f) |
||||||
|
active_name = data.get('active_profile') |
||||||
|
if active_name and active_name in data['profiles']: |
||||||
|
self.current_profile = data['profiles'][active_name] |
||||||
|
print(f"Loaded profile: {self.current_profile['name']}") |
||||||
|
|
||||||
|
# Sync with API if available |
||||||
|
if self.api_enabled: |
||||||
|
self.sync_profile_with_api() |
||||||
|
|
||||||
|
return True |
||||||
|
except (FileNotFoundError, json.JSONDecodeError) as e: |
||||||
|
print(f"Could not load profile: {e}") |
||||||
|
|
||||||
|
self.current_profile = None |
||||||
|
return False |
||||||
|
|
||||||
|
def get_profile_name(self): |
||||||
|
"""Get current profile name or default""" |
||||||
|
return self.current_profile['name'] if self.current_profile else "Guest Player" |
||||||
|
|
||||||
|
def get_device_id(self): |
||||||
|
"""Get the unique device identifier""" |
||||||
|
return self.device_id |
||||||
|
|
||||||
|
def get_setting(self, setting_name, default_value=None): |
||||||
|
"""Get a setting from the current profile, or return default""" |
||||||
|
if self.current_profile and 'settings' in self.current_profile: |
||||||
|
return self.current_profile['settings'].get(setting_name, default_value) |
||||||
|
return default_value |
||||||
|
|
||||||
|
def sync_profile_with_api(self): |
||||||
|
"""Ensure current profile is registered with the API server""" |
||||||
|
if not self.current_profile or not self.api_enabled: |
||||||
|
return False |
||||||
|
|
||||||
|
profile_name = self.current_profile['name'] |
||||||
|
|
||||||
|
# Check if user exists on server |
||||||
|
if not self.api_client.user_exists(self.device_id, profile_name): |
||||||
|
print(f"Registering {profile_name} with score server...") |
||||||
|
result = self.api_client.signup_user(self.device_id, profile_name) |
||||||
|
if result.get('success'): |
||||||
|
print(f"✓ {profile_name} registered successfully") |
||||||
|
return True |
||||||
|
else: |
||||||
|
print(f"✗ Failed to register {profile_name}: {result.get('message')}") |
||||||
|
return False |
||||||
|
else: |
||||||
|
print(f"✓ {profile_name} already registered on server") |
||||||
|
return True |
||||||
|
|
||||||
|
def register_new_user(self, user_id): |
||||||
|
"""Register a new user both locally and on the API server""" |
||||||
|
if not self.api_enabled: |
||||||
|
print("API server not available - user will only be registered locally") |
||||||
|
return True |
||||||
|
|
||||||
|
result = self.api_client.signup_user(self.device_id, user_id) |
||||||
|
if result.get('success'): |
||||||
|
print(f"✓ {user_id} registered with server successfully") |
||||||
|
return True |
||||||
|
else: |
||||||
|
print(f"✗ Failed to register {user_id} with server: {result.get('message')}") |
||||||
|
return False |
||||||
|
|
||||||
|
def update_game_stats(self, score, completed=True): |
||||||
|
"""Update the current profile's game statistics""" |
||||||
|
if not self.current_profile: |
||||||
|
print("No profile loaded - stats not saved") |
||||||
|
return False |
||||||
|
|
||||||
|
# Submit score to API first if available |
||||||
|
if self.api_enabled: |
||||||
|
profile_name = self.current_profile['name'] |
||||||
|
result = self.api_client.submit_score( |
||||||
|
self.device_id, |
||||||
|
profile_name, |
||||||
|
score, |
||||||
|
completed |
||||||
|
) |
||||||
|
if result.get('success'): |
||||||
|
print(f"✓ Score {score} submitted to server successfully") |
||||||
|
# Print server stats if available |
||||||
|
if 'user_stats' in result: |
||||||
|
stats = result['user_stats'] |
||||||
|
print(f" Server stats - Games: {stats['total_games']}, Best: {stats['best_score']}") |
||||||
|
else: |
||||||
|
print(f"✗ Failed to submit score to server: {result.get('message')}") |
||||||
|
|
||||||
|
try: |
||||||
|
# Update local profile |
||||||
|
with open(self.profiles_file, 'r') as f: |
||||||
|
data = json.load(f) |
||||||
|
|
||||||
|
profile_name = self.current_profile['name'] |
||||||
|
if profile_name in data['profiles']: |
||||||
|
profile = data['profiles'][profile_name] |
||||||
|
|
||||||
|
# Update statistics |
||||||
|
if completed: |
||||||
|
profile['games_played'] += 1 |
||||||
|
print(f"Game completed for {profile_name}! Total games: {profile['games_played']}") |
||||||
|
|
||||||
|
profile['total_score'] += score |
||||||
|
if score > profile['best_score']: |
||||||
|
profile['best_score'] = score |
||||||
|
print(f"New best score for {profile_name}: {score}!") |
||||||
|
|
||||||
|
profile['last_played'] = datetime.now().isoformat() |
||||||
|
|
||||||
|
# Update our local copy |
||||||
|
self.current_profile = profile |
||||||
|
|
||||||
|
# Save back to file |
||||||
|
with open(self.profiles_file, 'w') as f: |
||||||
|
json.dump(data, f, indent=2) |
||||||
|
|
||||||
|
print(f"Local profile stats updated: Score +{score}, Total: {profile['total_score']}") |
||||||
|
return True |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
print(f"Error updating profile stats: {e}") |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def add_achievement(self, achievement_id): |
||||||
|
"""Add an achievement to the current profile""" |
||||||
|
if not self.current_profile: |
||||||
|
return False |
||||||
|
|
||||||
|
try: |
||||||
|
with open(self.profiles_file, 'r') as f: |
||||||
|
data = json.load(f) |
||||||
|
|
||||||
|
profile_name = self.current_profile['name'] |
||||||
|
if profile_name in data['profiles']: |
||||||
|
profile = data['profiles'][profile_name] |
||||||
|
|
||||||
|
if achievement_id not in profile['achievements']: |
||||||
|
profile['achievements'].append(achievement_id) |
||||||
|
self.current_profile = profile |
||||||
|
|
||||||
|
with open(self.profiles_file, 'w') as f: |
||||||
|
json.dump(data, f, indent=2) |
||||||
|
|
||||||
|
print(f"Achievement unlocked for {profile_name}: {achievement_id}") |
||||||
|
return True |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
print(f"Error adding achievement: {e}") |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def get_profile_info(self): |
||||||
|
"""Get current profile information for display""" |
||||||
|
if self.current_profile: |
||||||
|
info = { |
||||||
|
'name': self.current_profile['name'], |
||||||
|
'games_played': self.current_profile['games_played'], |
||||||
|
'best_score': self.current_profile['best_score'], |
||||||
|
'total_score': self.current_profile['total_score'], |
||||||
|
'achievements': len(self.current_profile['achievements']), |
||||||
|
'difficulty': self.current_profile['settings'].get('difficulty', 'normal'), |
||||||
|
'device_id': self.device_id, |
||||||
|
'api_connected': self.api_enabled |
||||||
|
} |
||||||
|
return info |
||||||
|
return None |
||||||
|
|
||||||
|
def get_device_leaderboard(self, limit=10): |
||||||
|
"""Get leaderboard for the current device from API server""" |
||||||
|
if not self.api_enabled: |
||||||
|
print("API server not available - cannot get leaderboard") |
||||||
|
return [] |
||||||
|
|
||||||
|
leaderboard = self.api_client.get_leaderboard(self.device_id, limit) |
||||||
|
return leaderboard |
||||||
|
|
||||||
|
def get_global_leaderboard(self, limit=10): |
||||||
|
"""Get global leaderboard across all devices from API server""" |
||||||
|
if not self.api_enabled: |
||||||
|
print("API server not available - cannot get global leaderboard") |
||||||
|
return [] |
||||||
|
|
||||||
|
leaderboard = self.api_client.get_global_leaderboard(limit) |
||||||
|
return leaderboard |
||||||
|
|
||||||
|
def get_all_device_users(self): |
||||||
|
"""Get all users registered for this device from API server""" |
||||||
|
if not self.api_enabled: |
||||||
|
print("API server not available - cannot get user list") |
||||||
|
return [] |
||||||
|
|
||||||
|
users = self.api_client.get_device_users(self.device_id) |
||||||
|
return users |
||||||
|
|
||||||
|
def get_user_server_scores(self, user_id=None, limit=10): |
||||||
|
"""Get recent scores from server for a user (defaults to current profile)""" |
||||||
|
if not self.api_enabled: |
||||||
|
return [] |
||||||
|
|
||||||
|
if user_id is None: |
||||||
|
if not self.current_profile: |
||||||
|
return [] |
||||||
|
user_id = self.current_profile['name'] |
||||||
|
|
||||||
|
scores = self.api_client.get_user_scores(self.device_id, user_id, limit) |
||||||
|
return scores |
||||||
|
|
||||||
|
def reload_profile(self): |
||||||
|
"""Reload the current profile from disk (useful for external profile changes)""" |
||||||
|
return self.load_active_profile() |
||||||
|
|
||||||
|
|
||||||
|
# Convenience functions for quick integration |
||||||
|
def get_active_profile(): |
||||||
|
"""Quick function to get active profile info""" |
||||||
|
integration = UserProfileIntegration() |
||||||
|
return integration.get_profile_info() |
||||||
|
|
||||||
|
def update_profile_score(score, completed=True): |
||||||
|
"""Quick function to update profile score""" |
||||||
|
integration = UserProfileIntegration() |
||||||
|
return integration.update_game_stats(score, completed) |
||||||
|
|
||||||
|
def get_profile_setting(setting_name, default_value=None): |
||||||
|
"""Quick function to get a profile setting""" |
||||||
|
integration = UserProfileIntegration() |
||||||
|
return integration.get_setting(setting_name, default_value) |
||||||
|
|
||||||
|
def get_device_leaderboard(limit=10): |
||||||
|
"""Quick function to get device leaderboard""" |
||||||
|
integration = UserProfileIntegration() |
||||||
|
return integration.get_device_leaderboard(limit) |
||||||
|
|
||||||
|
def get_global_leaderboard(limit=10): |
||||||
|
"""Quick function to get global leaderboard""" |
||||||
|
integration = UserProfileIntegration() |
||||||
|
return integration.get_global_leaderboard(limit) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
# Test the integration |
||||||
|
print("Testing User Profile Integration with API...") |
||||||
|
|
||||||
|
integration = UserProfileIntegration() |
||||||
|
print(f"Device ID: {integration.get_device_id()}") |
||||||
|
print(f"Profile Name: {integration.get_profile_name()}") |
||||||
|
print(f"API Connected: {integration.api_enabled}") |
||||||
|
|
||||||
|
info = integration.get_profile_info() |
||||||
|
if info: |
||||||
|
print(f"Profile Info: {info}") |
||||||
|
else: |
||||||
|
print("No profile loaded") |
||||||
|
|
||||||
|
# Test settings |
||||||
|
difficulty = integration.get_setting('difficulty', 'normal') |
||||||
|
sound_volume = integration.get_setting('sound_volume', 50) |
||||||
|
print(f"Settings - Difficulty: {difficulty}, Sound: {sound_volume}%") |
||||||
|
|
||||||
|
# Test API features if connected |
||||||
|
if integration.api_enabled: |
||||||
|
print("\nTesting API features...") |
||||||
|
|
||||||
|
# Get leaderboard |
||||||
|
leaderboard = integration.get_device_leaderboard(5) |
||||||
|
if leaderboard: |
||||||
|
print("Device Leaderboard:") |
||||||
|
for entry in leaderboard: |
||||||
|
print(f" {entry['rank']}. {entry['user_id']}: {entry['best_score']} pts ({entry['total_games']} games)") |
||||||
|
else: |
||||||
|
print("No leaderboard data available") |
||||||
|
|
||||||
|
# Get all users |
||||||
|
users = integration.get_all_device_users() |
||||||
|
print(f"\nTotal users on device: {len(users)}") |
||||||
|
for user in users: |
||||||
|
print(f" {user['user_id']}: Best {user['best_score']}, {user['total_scores']} games") |
||||||
|
|
||||||
|
# Test score submission |
||||||
|
if integration.current_profile: |
||||||
|
print(f"\nTesting score submission for {integration.current_profile['name']}...") |
||||||
|
result = integration.update_game_stats(1234, True) |
||||||
|
print(f"Score update result: {result}") |
||||||
|
else: |
||||||
|
print("API features not available - server offline") |
||||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue