Implement main menu, profile list, and profile stats screens; add screen manager and user profile integration
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user