02202e4d3d
- Added `image_clean.png` to the output directory for the clean image representation. - Added `image_clean_preview.png` for the preview of the clean image. - Introduced `image_svg_clean.png` for the SVG clean image representation.
288 lines
9.8 KiB
Python
288 lines
9.8 KiB
Python
#!/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
|
|
from pathlib import Path
|
|
|
|
# Import the user profile integration system
|
|
from user_profile_integration import UserProfileIntegration
|
|
from runtime_paths import DEFAULT_PROFILE_DATA, persistent_data_path
|
|
|
|
|
|
@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 = persistent_data_path(
|
|
profiles_file,
|
|
default_text=DEFAULT_PROFILE_DATA,
|
|
)
|
|
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 Path(self.profiles_file).open('r', encoding='utf-8') 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 Path(self.profiles_file).open('w', encoding='utf-8') 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)
|