Implement main menu, profile list, and profile stats screens; add screen manager and user profile integration
This commit is contained in:
-1260
File diff suppressed because it is too large
Load Diff
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.
+6
-6
@@ -3,7 +3,7 @@
|
||||
"Player1": {
|
||||
"name": "Player1",
|
||||
"created_date": "2024-01-15T10:30:00",
|
||||
"last_played": "2025-08-21T17:57:06.304649",
|
||||
"last_played": "2025-08-24T21:49:04.187787",
|
||||
"games_played": 25,
|
||||
"total_score": 15420,
|
||||
"best_score": 980,
|
||||
@@ -23,7 +23,7 @@
|
||||
"Alice": {
|
||||
"name": "Alice",
|
||||
"created_date": "2024-01-10T09:15:00",
|
||||
"last_played": "2025-08-21T17:39:28.013833",
|
||||
"last_played": "2025-08-24T21:48:42.749128",
|
||||
"games_played": 43,
|
||||
"total_score": 33000,
|
||||
"best_score": 1250,
|
||||
@@ -78,10 +78,10 @@
|
||||
"B0B": {
|
||||
"name": "B0B",
|
||||
"created_date": "2025-08-21T18:03:12.189612",
|
||||
"last_played": "2025-08-21T18:59:35.393040",
|
||||
"last_played": "2025-08-22T14:42:49.357304",
|
||||
"games_played": 0,
|
||||
"total_score": 590,
|
||||
"best_score": 590,
|
||||
"total_score": 1410,
|
||||
"best_score": 820,
|
||||
"settings": {
|
||||
"difficulty": "normal",
|
||||
"sound_volume": 50,
|
||||
@@ -92,5 +92,5 @@
|
||||
"achievements": []
|
||||
}
|
||||
},
|
||||
"active_profile": "B0B"
|
||||
"active_profile": "Player1"
|
||||
}
|
||||
Reference in New Issue
Block a user