Browse Source

Implement Score API Client and User Profile Integration

- Added ScoreAPIClient for communication with the Mice Game Score API, including methods for user signup, score submission, and leaderboard retrieval.
- Developed a simple profile manager demo to showcase user profile management and API integration.
- Created a test script for the Score API to validate all endpoints and functionality.
- Introduced UserProfileIntegration to manage user profiles, including local storage and API synchronization.
- Added a JSON file for user profiles with sample data for testing and demonstration purposes.
master
Matteo Benedetto 4 months ago
parent
commit
cbb60a19d9
  1. BIN
      15_melodic_rpg_chiptunes_mid/rpgchip03_town.mid
  2. 308
      README_PROFILE_MANAGER.md
  3. 2091
      api.log
  4. 6
      api_requirements.txt
  5. 24
      engine/scoring.py
  6. 50
      engine/sdl2.py
  7. 9
      keycode.txt
  8. BIN
      mice_game.db
  9. 875
      profile_manager.py
  10. 79
      rats.py
  11. 547
      score_api.py
  12. 307
      score_api_client.py
  13. 112
      simple_profile_demo.py
  14. 140
      test_score_api.py
  15. BIN
      units/__pycache__/rat.cpython-313.pyc
  16. 339
      user_profile_integration.py
  17. 64
      user_profiles.json

BIN
15_melodic_rpg_chiptunes_mid/rpgchip03_town.mid

Binary file not shown.

308
README_PROFILE_MANAGER.md

@ -0,0 +1,308 @@
# Game Profile Manager
A PySDL2-based user profile management system designed for gamepad-only control with virtual keyboard input. This system allows players to create, edit, delete, and select user profiles for games using only gamepad inputs or directional keys, with no need for physical keyboard text input.
## Features
- **640x480 Resolution**: Optimized for retro gaming systems and handheld devices
- **Create New Profiles**: Add new user profiles with custom names using virtual keyboard
- **Profile Selection**: Browse and select active profiles
- **Edit Settings**: Modify profile settings including difficulty, volume levels, and preferences
- **Delete Profiles**: Remove unwanted profiles
- **Gamepad/Directional Navigation**: Full control using only gamepad/joystick inputs or arrow keys
- **Virtual Keyboard**: Text input using directional controls - no physical keyboard typing required
- **JSON Storage**: Profiles stored in human-readable JSON format
- **Persistent Settings**: All changes automatically saved
## Installation
### Requirements
- Python 3.6+
- PySDL2
- SDL2 library
### Setup
```bash
# Install required Python packages
pip install pysdl2
# For Ubuntu/Debian users, you may also need:
sudo apt-get install libsdl2-dev libsdl2-ttf-dev
# Make launcher executable
chmod +x launch_profile_manager.sh
```
## Usage
### Running the Profile Manager
```bash
# Method 1: Use the launcher script
./launch_profile_manager.sh
# Method 2: Run directly with Python
python3 profile_manager.py
```
### Gamepad Controls
#### Standard Gamepad Layout (Xbox/PlayStation compatible)
- **D-Pad/Hat**: Navigate menus up/down/left/right, control virtual keyboard cursor
- **Button 0 (A/X)**: Confirm selection, enter menus, select virtual keyboard characters
- **Button 1 (B/Circle)**: Go back, cancel action
- **Button 2 (X/Square)**: Delete profile, backspace in virtual keyboard
- **Button 3 (Y/Triangle)**: Reserved for future features
#### Keyboard Controls (Alternative)
- **Arrow Keys**: Navigate menus and virtual keyboard cursor
- **Enter/Space**: Confirm selection, select virtual keyboard characters
- **Escape**: Go back, cancel action
- **Delete/Backspace**: Delete profile, backspace in virtual keyboard
- **Tab**: Reserved for future features
#### Virtual Keyboard Text Input
When creating or editing profile names:
1. **Navigate**: Use D-Pad/Arrow Keys to move cursor over virtual keyboard
2. **Select Character**: Press A/Enter to add character to profile name
3. **Backspace**: Press X/Delete to remove last character
4. **Complete**: Navigate to "DONE" and press A/Enter to finish input
5. **Cancel**: Navigate to "CANCEL" and press A/Enter to abort
#### Navigation Flow
1. **Main Menu**: Create Profile → Select Profile → Edit Settings → Exit
2. **Profile List**: Choose from existing profiles, or go back
3. **Create Profile**: Use virtual keyboard to enter name, confirm with directional controls
4. **Edit Profile**: Adjust settings using left/right navigation
### Display Specifications
- **Resolution**: 640x480 pixels (4:3 aspect ratio)
- **Optimized for**: Retro gaming systems, handheld devices, embedded systems
- **Font Scaling**: Adaptive font sizes for optimal readability at low resolution
### Profile Structure
Profiles are stored in `user_profiles.json` with the following structure:
```json
{
"profiles": {
"PlayerName": {
"name": "PlayerName",
"created_date": "2024-01-15T10:30:00",
"last_played": "2024-01-20T14:45:00",
"games_played": 25,
"total_score": 15420,
"best_score": 980,
"settings": {
"difficulty": "normal",
"sound_volume": 75,
"music_volume": 60,
"screen_shake": true,
"auto_save": true
},
"achievements": [
"first_win",
"score_500"
]
}
},
"active_profile": "PlayerName"
}
```
## Integration with Games
### Loading Active Profile
```python
import json
def load_active_profile():
try:
with open('user_profiles.json', 'r') as f:
data = json.load(f)
active_name = data.get('active_profile')
if active_name and active_name in data['profiles']:
return data['profiles'][active_name]
except (FileNotFoundError, json.JSONDecodeError):
pass
return None
# Usage in your game
profile = load_active_profile()
if profile:
difficulty = profile['settings']['difficulty']
sound_volume = profile['settings']['sound_volume']
```
### Updating Profile Stats
```python
def update_profile_stats(score, game_completed=True):
try:
with open('user_profiles.json', 'r') as f:
data = json.load(f)
active_name = data.get('active_profile')
if active_name and active_name in data['profiles']:
profile = data['profiles'][active_name]
if game_completed:
profile['games_played'] += 1
profile['total_score'] += score
profile['best_score'] = max(profile['best_score'], score)
profile['last_played'] = datetime.now().isoformat()
with open('user_profiles.json', 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error updating profile: {e}")
```
## Customization
### Adding New Settings
Edit the `UserProfile` dataclass and the settings adjustment methods:
```python
# In profile_manager.py, modify the UserProfile.__post_init__ method
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,
"your_new_setting": "default_value" # Add here
}
```
### Custom Font
Place your font file in the `assets/` directory and update the font path:
```python
font_path = "assets/your_font.ttf"
```
### Screen Resolution
The application is optimized for 640x480 resolution. To change resolution, modify the window size in the init_sdl method:
```python
self.window = sdl2.ext.Window(
title="Profile Manager",
size=(your_width, your_height) # Change from (640, 480)
)
```
### Virtual Keyboard Layout
Customize the virtual keyboard characters by modifying the keyboard_chars list:
```python
self.keyboard_chars = [
['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', '1', '2', '3', '4'],
['5', '6', '7', '8', '9', '0', '_', '-', ' ', '<'],
['DONE', 'CANCEL', '', '', '', '', '', '', '', '']
]
```
## Troubleshooting
### No Gamepad Detected
- Ensure your gamepad is connected before starting the application
- Try different USB ports
- Check if your gamepad is recognized by your system
- The application will show "No gamepad detected - using keyboard fallback"
- Virtual keyboard works with both gamepad and keyboard controls
### Font Issues
- Ensure the font file exists in the assets directory
- The system will fall back to default font if custom font is not found
- Supported font formats: TTF, OTF
- Font sizes are automatically scaled for 640x480 resolution
### Virtual Keyboard Not Responding
- Ensure you're in text input mode (creating/editing profile names)
- Use arrow keys or D-Pad to navigate the virtual keyboard cursor
- Press Enter/A button to select characters
- The virtual keyboard cursor should be visible as a highlighted character
### Profile Not Saving
- Check file permissions in the application directory
- Ensure sufficient disk space
- Verify JSON format is not corrupted
### Resolution Issues
- The application is designed for 640x480 resolution
- On higher resolution displays, the window may appear small
- This is intentional for compatibility with retro gaming systems
- Content is optimized and readable at this resolution
## File Structure
```
project_directory/
├── profile_manager.py # Main application (640x480, virtual keyboard)
├── launch_profile_manager.sh # Launcher script
├── user_profiles.json # Profile data storage
├── test_profile_manager.py # Test suite for core functions
├── game_profile_integration.py # Example game integration
├── assets/
│ └── decterm.ttf # Font file (optional)
└── README_PROFILE_MANAGER.md # This documentation
```
## Development Notes
### Virtual Keyboard Implementation
The virtual keyboard is implemented as a 2D grid of characters:
- Cursor position tracked with (keyboard_cursor_x, keyboard_cursor_y)
- Character selection adds to input_text string
- Special functions: DONE (confirm), CANCEL (abort), < (backspace)
- Fully navigable with directional controls only
### Screen Layout for 640x480
- Header area: 0-80px (titles, status)
- Content area: 80-400px (main UI elements)
- Controls area: 400-480px (help text, instructions)
- All elements scaled and positioned for optimal readability
### Adding New Screens
1. Add screen name to `current_screen` handling
2. Create render method (e.g., `render_new_screen()`)
3. Add navigation logic in input handlers
4. Update screen transitions in confirm/back handlers
### Gamepad Button Mapping
The application uses SDL2's joystick interface. Button numbers may vary by controller:
- Most modern controllers follow the Xbox layout
- PlayStation controllers map similarly but may have different button numbers
- Test with your specific controller and adjust mappings if needed
### Performance Considerations
- Rendering is capped at 60 FPS for smooth operation
- Input debouncing prevents accidental rapid inputs
- JSON operations are minimized and occur only when necessary
- Virtual keyboard rendering optimized for 640x480 resolution
- Font scaling automatically adjusted for readability
### Adding Support for Different Resolutions
To support different screen resolutions, modify these key areas:
1. Window initialization in `init_sdl()`
2. Panel and button positioning in render methods
3. Font size scaling factors
4. Virtual keyboard grid positioning
### Gamepad Integration Notes
- Uses SDL2's joystick interface for maximum compatibility
- Button mapping follows standard Xbox controller layout
- Hat/D-Pad input prioritized over analog sticks for precision
- Input timing designed for responsive but not accidental activation
## Target Platforms
This profile manager is specifically designed for:
- **Handheld Gaming Devices**: Steam Deck, ROG Ally, etc.
- **Retro Gaming Systems**: RetroPie, Batocera, etc.
- **Embedded Gaming Systems**: Custom arcade cabinets, portable devices
- **Low-Resolution Displays**: 640x480, 800x600, and similar resolutions
- **Gamepad-Only Environments**: Systems without keyboard access
## License
This profile manager is provided as-is for educational and personal use. Designed for integration with retro and handheld gaming systems.

2091
api.log

File diff suppressed because it is too large Load Diff

6
api_requirements.txt

@ -0,0 +1,6 @@
# API Requirements for Mice Game Score API
fastapi>=0.104.1
uvicorn[standard]>=0.24.0
pydantic>=2.5.0
sqlite3 # Built into Python
python-multipart>=0.0.6

24
engine/scoring.py

@ -6,17 +6,35 @@ class Scoring:
# ==================== SCORING ==================== # ==================== SCORING ====================
def save_score(self): def save_score(self):
# Save to traditional scores.txt file
with open("scores.txt", "a") as f: with open("scores.txt", "a") as f:
f.write(f"{datetime.datetime.now()} - {self.points}\n") player_name = getattr(self, 'profile_integration', None)
if player_name and hasattr(player_name, 'get_profile_name'):
name = player_name.get_profile_name()
device_id = player_name.get_device_id()
f.write(f"{datetime.datetime.now()} - {self.points} - {name} - {device_id}\n")
else:
f.write(f"{datetime.datetime.now()} - {self.points} - Guest\n")
def read_score(self): def read_score(self):
table = [] table = []
try:
with open("scores.txt") as f: with open("scores.txt") as f:
rows = f.read().splitlines() rows = f.read().splitlines()
for row in rows: for row in rows:
table.append(row.split(" - ")) parts = row.split(" - ")
if len(parts) >= 2:
# Handle both old format (date - score) and new format (date - score - name - device)
if len(parts) >= 4:
table.append([parts[0], parts[1], parts[2], parts[3]]) # date, score, name, device
elif len(parts) >= 3:
table.append([parts[0], parts[1], parts[2], "Unknown"]) # date, score, name, unknown device
else:
table.append([parts[0], parts[1], "Guest", "Unknown"]) # date, score, guest, unknown device
table.sort(key=lambda x: int(x[1]), reverse=True) table.sort(key=lambda x: int(x[1]), reverse=True)
return table[:3] except FileNotFoundError:
pass
return table[:5] # Return top 5 scores instead of 3
def add_point(self, value): def add_point(self, value):
self.points += value self.points += value

50
engine/sdl2.py

@ -200,32 +200,52 @@ class GameWindow:
self.target_size[0] - 100, self.target_size[1] - 100, self.target_size[0] - 100, self.target_size[1] - 100,
"win", filling=(255, 255, 255)) "win", filling=(255, 255, 255))
# Draw main text # Calculate layout positions to avoid overlaps
self.draw_text(text, self.fonts[self.target_size[1]//20], "center", sdl2.ext.Color(0, 0, 0)) title_y = self.target_size[1] // 4 # Title at 1/4 of screen height
# Draw subtitle if provided # Draw main text (title)
if subtitle := kwargs.get("subtitle"): self.draw_text(text, self.fonts[self.target_size[1]//20],
self.draw_text(subtitle, self.fonts[self.target_size[1]//30], ("center", title_y), sdl2.ext.Color(0, 0, 0))
("center", self.target_size[1] // 2 + 50), sdl2.ext.Color(0, 0, 0))
# Draw image if provided # Draw image if provided - position it below title
image_bottom_y = title_y + 60 # Default position if no image
if image := kwargs.get("image"): if image := kwargs.get("image"):
image_size = self.get_image_size(image) image_size = self.get_image_size(image)
image_y = title_y + 50
self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset, self.draw_image(self.target_size[0] // 2 - image_size[0] // 2 - self.w_offset,
self.target_size[1] // 2 - image_size[1] * 2 - self.h_offset, image_y - self.h_offset,
image, "win") image, "win")
image_bottom_y = image_y + image_size[1] + 20
# Draw subtitle if provided - handle multi-line text, position below image
if subtitle := kwargs.get("subtitle"):
subtitle_lines = subtitle.split('\n')
base_y = image_bottom_y + 20
line_height = 25 # Fixed line height for consistent spacing
for i, line in enumerate(subtitle_lines):
if line.strip(): # Only draw non-empty lines
self.draw_text(line.strip(), self.fonts[self.target_size[1]//35],
("center", base_y + i * line_height), sdl2.ext.Color(0, 0, 0))
# Draw scores if provided # Draw scores if provided - position at bottom
if scores := kwargs.get("scores"): if scores := kwargs.get("scores"):
sprite = self.factory.from_text("Scores:", color=sdl2.ext.Color(0, 0, 0), scores_start_y = self.target_size[1] * 3 // 4 # Bottom quarter of screen
fontmanager=self.fonts[self.target_size[1]//20]) sprite = self.factory.from_text("High Scores:", color=sdl2.ext.Color(0, 0, 0),
sprite.position = (self.target_size[0] // 2 - 50, self.target_size[1] // 2 + 30) fontmanager=self.fonts[self.target_size[1]//25])
sprite.position = (self.target_size[0] // 2 - sprite.size[0] // 2, scores_start_y)
self.renderer.copy(sprite, dstrect=sprite.position) self.renderer.copy(sprite, dstrect=sprite.position)
for i, score in enumerate(scores[:5]): for i, score in enumerate(scores[:5]):
score_text = " - ".join(score) if len(score) >= 4: # New format: date, score, name, device
self.draw_text(score_text, self.fonts[self.target_size[1]//40], score_text = f"{score[2]}: {score[1]} pts ({score[3]})"
("center", self.target_size[1] // 2 + 50 + 30 * (i + 1)), elif len(score) >= 3: # Medium format: date, score, name
score_text = f"{score[2]}: {score[1]} pts"
else: # Old format: date, score
score_text = f"Guest: {score[1]} pts"
self.draw_text(score_text, self.fonts[self.target_size[1]//45],
("center", scores_start_y + 30 + 25 * (i + 1)),
sdl2.ext.Color(0, 0, 0)) sdl2.ext.Color(0, 0, 0))
def start_dialog(self, **kwargs): def start_dialog(self, **kwargs):

9
keycode.txt

@ -1,9 +0,0 @@
Pressed: 13
Pressed: 13
Pressed: 13
Pressed: 13
Released: 13
Pressed: 1073741912
Released: 1073741912
Pressed: 1073742048
Released: 1073742048

BIN
mice_game.db

Binary file not shown.

875
profile_manager.py

@ -0,0 +1,875 @@
#!/usr/bin/env python3
"""
User Profile Manager for Games
A PySDL2-based profile management system with gamepad-only controls
Features:
- Create new user profiles
- Edit existing profiles
- Delete profiles
- Select active profile
- JSON-based storage
- Gamepad navigation only
"""
import os
import json
import time
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from datetime import datetime
import sdl2
import sdl2.ext
from sdl2.ext.compat import byteify
@dataclass
class UserProfile:
"""User profile data structure"""
name: str
created_date: str
last_played: str
games_played: int = 0
total_score: int = 0
best_score: int = 0
settings: Dict[str, Any] = None
achievements: List[str] = None
def __post_init__(self):
if self.settings is None:
self.settings = {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": True,
"auto_save": True
}
if self.achievements is None:
self.achievements = []
class ProfileManager:
"""Main profile management system"""
def __init__(self, profiles_file: str = "user_profiles.json"):
self.profiles_file = profiles_file
self.profiles: Dict[str, UserProfile] = {}
self.active_profile: Optional[str] = None
# UI State
self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile
self.selected_index = 0
self.input_text = ""
self.input_active = False
self.editing_field = None
# Virtual keyboard state
self.vk_cursor_x = 0 # Virtual keyboard cursor X
self.vk_cursor_y = 0 # Virtual keyboard cursor Y
self.virtual_keyboard = [
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T'],
['U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3'],
['4', '5', '6', '7', '8', '9', ' ', '.', '-', '_'],
['<DEL', 'SPACE', 'DONE', 'CANCEL']
]
# SDL2 Setup
self.window = None
self.renderer = None
self.font_manager = None
self.running = True
# Gamepad state
self.gamepad = None
self.button_states = {}
self.last_button_time = {}
# Colors
self.colors = {
'white': sdl2.ext.Color(255, 255, 255),
'black': sdl2.ext.Color(0, 0, 0),
'gray': sdl2.ext.Color(128, 128, 128),
'light_gray': sdl2.ext.Color(200, 200, 200),
'dark_gray': sdl2.ext.Color(64, 64, 64),
'blue': sdl2.ext.Color(70, 130, 200),
'light_blue': sdl2.ext.Color(120, 180, 255),
'green': sdl2.ext.Color(50, 200, 50),
'light_green': sdl2.ext.Color(100, 255, 100),
'red': sdl2.ext.Color(200, 50, 50),
'yellow': sdl2.ext.Color(255, 220, 0),
'orange': sdl2.ext.Color(255, 165, 0),
'purple': sdl2.ext.Color(150, 50, 200)
}
self.load_profiles()
def init_sdl(self):
"""Initialize SDL2 components"""
sdl2.ext.init(joystick=True)
# Create window
self.window = sdl2.ext.Window(
title="Profile Manager",
size=(640, 480)
)
self.window.show()
# Create renderer
self.renderer = sdl2.ext.Renderer(
self.window,
flags=sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC
)
# Initialize font system - create multiple sizes
font_path = "assets/decterm.ttf" if os.path.exists("assets/decterm.ttf") else None
self.fonts = {
'title': sdl2.ext.FontManager(font_path=font_path, size=36),
'large': sdl2.ext.FontManager(font_path=font_path, size=28),
'medium': sdl2.ext.FontManager(font_path=font_path, size=22),
'small': sdl2.ext.FontManager(font_path=font_path, size=18),
'tiny': sdl2.ext.FontManager(font_path=font_path, size=14)
}
# Initialize gamepad
self.init_gamepad()
def init_gamepad(self):
"""Initialize gamepad support"""
sdl2.SDL_Init(sdl2.SDL_INIT_JOYSTICK | sdl2.SDL_INIT_GAMECONTROLLER)
num_joysticks = sdl2.SDL_NumJoysticks()
if num_joysticks > 0:
self.gamepad = sdl2.SDL_JoystickOpen(0)
print(f"Gamepad detected: {sdl2.SDL_JoystickName(self.gamepad).decode()}")
else:
print("No gamepad detected - using keyboard fallback")
def load_profiles(self):
"""Load profiles from JSON file"""
if os.path.exists(self.profiles_file):
try:
with open(self.profiles_file, 'r') as f:
data = json.load(f)
self.profiles = {
name: UserProfile(**profile_data)
for name, profile_data in data.get('profiles', {}).items()
}
self.active_profile = data.get('active_profile')
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading profiles: {e}")
self.profiles = {}
def save_profiles(self):
"""Save profiles to JSON file"""
data = {
'profiles': {
name: asdict(profile)
for name, profile in self.profiles.items()
},
'active_profile': self.active_profile
}
try:
with open(self.profiles_file, 'w') as f:
json.dump(data, f, indent=2)
except IOError as e:
print(f"Error saving profiles: {e}")
def create_profile(self, name: str) -> bool:
"""Create a new profile"""
if name in self.profiles or not name.strip():
return False
now = datetime.now().isoformat()
profile = UserProfile(
name=name.strip(),
created_date=now,
last_played=now
)
self.profiles[name.strip()] = profile
self.save_profiles()
return True
def delete_profile(self, name: str) -> bool:
"""Delete a profile"""
if name in self.profiles:
del self.profiles[name]
if self.active_profile == name:
self.active_profile = None
self.save_profiles()
return True
return False
def set_active_profile(self, name: str) -> bool:
"""Set the active profile"""
if name in self.profiles:
self.active_profile = name
self.profiles[name].last_played = datetime.now().isoformat()
self.save_profiles()
return True
return False
def handle_gamepad_input(self):
"""Handle gamepad input with debouncing"""
if not self.gamepad:
return
current_time = time.time()
# D-pad navigation
hat_state = sdl2.SDL_JoystickGetHat(self.gamepad, 0)
# Up
if hat_state & sdl2.SDL_HAT_UP:
if self.can_process_input('up', current_time):
self.navigate_up()
# Down
if hat_state & sdl2.SDL_HAT_DOWN:
if self.can_process_input('down', current_time):
self.navigate_down()
# Left
if hat_state & sdl2.SDL_HAT_LEFT:
if self.can_process_input('left', current_time):
self.navigate_left()
# Right
if hat_state & sdl2.SDL_HAT_RIGHT:
if self.can_process_input('right', current_time):
self.navigate_right()
# Buttons
button_count = sdl2.SDL_JoystickNumButtons(self.gamepad)
for i in range(min(button_count, 16)): # Limit to reasonable number
if sdl2.SDL_JoystickGetButton(self.gamepad, i):
if self.can_process_input(f'button_{i}', current_time):
self.handle_button_press(i)
def can_process_input(self, input_key: str, current_time: float) -> bool:
"""Check if enough time has passed to process input (debouncing)"""
delay = 0.15 # 150ms delay
if input_key not in self.last_button_time:
self.last_button_time[input_key] = current_time
return True
if current_time - self.last_button_time[input_key] > delay:
self.last_button_time[input_key] = current_time
return True
return False
def handle_button_press(self, button: int):
"""Handle gamepad button presses"""
# Button mappings (common gamepad layout)
# 0: A/Cross - Confirm
# 1: B/Circle - Back
# 2: X/Square - Delete/Special
# 3: Y/Triangle - Menu
if button == 0: # A/Confirm
self.handle_confirm()
elif button == 1: # B/Back
self.handle_back()
elif button == 2: # X/Delete
self.handle_delete()
elif button == 3: # Y/Menu
self.handle_menu()
def navigate_up(self):
"""Navigate up in current screen"""
if self.current_screen == "main_menu":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "profile_list":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "edit_profile":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
self.vk_cursor_y = max(0, self.vk_cursor_y - 1)
# Adjust x cursor if current row is shorter
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(self.vk_cursor_x, max_x)
def navigate_down(self):
"""Navigate down in current screen"""
if self.current_screen == "main_menu":
max_index = 3 # Create, Select, Settings, Exit (0-3)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "profile_list":
max_index = len(self.profiles) # Profiles + Back
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "edit_profile":
max_index = 5 # 4 settings + save + back (0-5)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
max_y = len(self.virtual_keyboard) - 1
self.vk_cursor_y = min(max_y, self.vk_cursor_y + 1)
# Adjust x cursor if current row is shorter
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(self.vk_cursor_x, max_x)
def navigate_left(self):
"""Navigate left (for adjusting values)"""
if self.current_screen == "edit_profile":
self.adjust_setting(-1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
self.vk_cursor_x = max(0, self.vk_cursor_x - 1)
def navigate_right(self):
"""Navigate right (for adjusting values)"""
if self.current_screen == "edit_profile":
self.adjust_setting(1)
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
self.vk_cursor_x = min(max_x, self.vk_cursor_x + 1)
def handle_confirm(self):
"""Handle confirm action (A button)"""
if self.current_screen == "main_menu":
if self.selected_index == 0: # Create Profile
self.current_screen = "create_profile"
self.input_text = ""
self.input_active = True
elif self.selected_index == 1: # Select Profile
self.current_screen = "profile_list"
self.selected_index = 0
elif self.selected_index == 2: # Settings
if self.active_profile:
self.current_screen = "edit_profile"
self.selected_index = 0
elif self.selected_index == 3: # Exit
self.running = False
elif self.current_screen == "profile_list":
profile_names = list(self.profiles.keys())
if self.selected_index < len(profile_names):
# Select profile
profile_name = profile_names[self.selected_index]
self.set_active_profile(profile_name)
self.current_screen = "main_menu"
self.selected_index = 0
else: # Back option
self.current_screen = "main_menu"
self.selected_index = 1
elif self.current_screen == "create_profile":
if self.input_active:
# Handle virtual keyboard selection
self.handle_virtual_keyboard_input()
else:
# Start text input mode
self.input_active = True
self.vk_cursor_x = 0
self.vk_cursor_y = 0
elif self.current_screen == "edit_profile":
if self.selected_index == 4: # Save (index 4)
self.save_profiles()
self.current_screen = "main_menu"
self.selected_index = 0
elif self.selected_index == 5: # Back (index 5)
self.current_screen = "main_menu"
self.selected_index = 0
def handle_back(self):
"""Handle back action (B button)"""
if self.current_screen == "main_menu":
self.running = False
elif self.current_screen in ["profile_list", "create_profile", "edit_profile"]:
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
def handle_delete(self):
"""Handle delete action (X button)"""
if self.current_screen == "profile_list":
profile_names = list(self.profiles.keys())
if self.selected_index < len(profile_names):
profile_name = profile_names[self.selected_index]
self.delete_profile(profile_name)
self.selected_index = min(self.selected_index, len(self.profiles) - 1)
elif self.current_screen == "create_profile":
# Delete character from input
if self.input_text:
self.input_text = self.input_text[:-1]
def handle_menu(self):
"""Handle menu action (Y button)"""
pass # Reserved for future use
def handle_virtual_keyboard_input(self):
"""Handle virtual keyboard character selection"""
if self.vk_cursor_y >= len(self.virtual_keyboard):
return
row = self.virtual_keyboard[self.vk_cursor_y]
if self.vk_cursor_x >= len(row):
return
selected_char = row[self.vk_cursor_x]
if selected_char == '<DEL':
# Delete last character
if self.input_text:
self.input_text = self.input_text[:-1]
elif selected_char == 'SPACE':
# Add space
if len(self.input_text) < 20:
self.input_text += ' '
elif selected_char == 'DONE':
# Finish input
if self.input_text.strip():
if self.create_profile(self.input_text):
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
elif selected_char == 'CANCEL':
# Cancel input
self.input_text = ""
self.input_active = False
else:
# Add character
if len(self.input_text) < 20:
self.input_text += selected_char
def adjust_setting(self, direction: int):
"""Adjust setting value left/right"""
if self.current_screen == "edit_profile" and self.active_profile:
profile = self.profiles[self.active_profile]
if self.selected_index == 0: # Difficulty
difficulties = ["easy", "normal", "hard", "expert"]
current = difficulties.index(profile.settings["difficulty"])
new_index = (current + direction) % len(difficulties)
profile.settings["difficulty"] = difficulties[new_index]
elif self.selected_index == 1: # Sound Volume
profile.settings["sound_volume"] = max(0, min(100,
profile.settings["sound_volume"] + direction * 5))
elif self.selected_index == 2: # Music Volume
profile.settings["music_volume"] = max(0, min(100,
profile.settings["music_volume"] + direction * 5))
elif self.selected_index == 3: # Screen Shake
profile.settings["screen_shake"] = not profile.settings["screen_shake"]
def render(self):
"""Main rendering method"""
# Clear with dark background
self.renderer.clear(self.colors['black'])
# Draw subtle background pattern/gradient
for y in range(0, 480, 20):
alpha = int(20 * (1 - y / 480)) # Fade effect
if alpha > 5:
color = sdl2.ext.Color(alpha, alpha, alpha * 2)
self.renderer.draw_line((0, y, 640, y), color)
if self.current_screen == "main_menu":
self.render_main_menu()
elif self.current_screen == "profile_list":
self.render_profile_list()
elif self.current_screen == "create_profile":
self.render_create_profile()
elif self.current_screen == "edit_profile":
self.render_edit_profile()
self.renderer.present()
def render_main_menu(self):
"""Render main menu screen with improved layout"""
# Draw background gradient effect
self.renderer.fill((0, 0, 640, 120), self.colors['dark_gray'])
# Title
title = "Profile Manager"
self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True)
# Subtitle with active profile
if self.active_profile:
active_text = f"Active: {self.active_profile}"
self.draw_text(active_text, 320, 60, self.colors['light_green'], 'medium', center=True)
else:
self.draw_text("No active profile", 320, 60, self.colors['yellow'], 'medium', center=True)
# Menu panel
panel_x, panel_y = 120, 140
panel_width, panel_height = 400, 220
self.draw_panel(panel_x, panel_y, panel_width, panel_height,
self.colors['dark_gray'], self.colors['gray'])
# Menu items with proper spacing
menu_items = [
"Create Profile",
"Select Profile",
"Edit Settings",
"Exit"
]
button_width = 280
button_height = 35
button_x = panel_x + 60
start_y = panel_y + 20
for i, item in enumerate(menu_items):
button_y = start_y + i * (button_height + 10)
selected = (i == self.selected_index)
self.draw_button(item, button_x, button_y, button_width, button_height, selected)
# Controls help panel
help_panel_y = 380
self.draw_panel(10, help_panel_y, 620, 80,
self.colors['black'], self.colors['dark_gray'])
self.draw_text("Controls:", 20, help_panel_y + 8,
self.colors['light_blue'], 'small')
self.draw_text("↑↓ Navigate Enter Confirm Escape Back", 20, help_panel_y + 30,
self.colors['light_gray'], 'tiny')
def render_profile_list(self):
"""Render profile selection screen with improved layout"""
# Header
self.renderer.fill((0, 0, 640, 70), self.colors['dark_gray'])
self.draw_text("Select Profile", 320, 15, self.colors['light_blue'], 'title', center=True)
profile_names = list(self.profiles.keys())
if not profile_names:
# No profiles message
self.draw_panel(120, 100, 400, 150, self.colors['dark_gray'], self.colors['gray'])
self.draw_text("No profiles found", 320, 150, self.colors['yellow'], 'large', center=True)
self.draw_text("Create one first", 320, 180,
self.colors['light_gray'], 'medium', center=True)
# Back button
self.draw_button("← Back", 270, 300, 100, 30, True)
else:
# Profile list panel
panel_height = min(280, len(profile_names) * 55 + 60)
self.draw_panel(30, 80, 580, panel_height,
self.colors['black'], self.colors['gray'])
# Profile entries
for i, name in enumerate(profile_names):
profile = self.profiles[name]
entry_y = 95 + i * 50
entry_selected = (i == self.selected_index)
# Profile entry background
entry_color = self.colors['blue'] if entry_selected else self.colors['dark_gray']
border_color = self.colors['light_blue'] if entry_selected else self.colors['gray']
self.draw_panel(40, entry_y, 560, 40, entry_color, border_color)
# Profile name
name_color = self.colors['white'] if entry_selected else self.colors['light_gray']
self.draw_text(name, 50, entry_y + 5, name_color, 'medium')
# Profile stats (compact)
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}"
stats_color = self.colors['light_gray'] if entry_selected else self.colors['gray']
self.draw_text(stats_text, 50, entry_y + 22, stats_color, 'tiny')
# Active indicator
if name == self.active_profile:
self.draw_text("", 580, entry_y + 12,
self.colors['light_green'], 'small')
# Back button
back_y = 95 + len(profile_names) * 50 + 10
back_selected = (self.selected_index == len(profile_names))
self.draw_button("← Back", 270, back_y, 100, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Enter: Select • Escape: Back • Delete: Remove",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_create_profile(self):
"""Render profile creation screen with virtual keyboard"""
# Header
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray'])
self.draw_text("Create Profile", 320, 15, self.colors['light_blue'], 'title', center=True)
# Input field
input_x, input_y = 120, 90
input_width, input_height = 400, 30
input_bg = self.colors['white'] if self.input_active else self.colors['light_gray']
input_border = self.colors['blue'] if self.input_active else self.colors['gray']
self.draw_panel(input_x, input_y, input_width, input_height, input_bg, input_border, 2)
# Input text
display_text = self.input_text if self.input_text else "Profile Name"
text_color = self.colors['black'] if self.input_text else self.colors['gray']
self.draw_text(display_text, input_x + 10, input_y + 8, text_color, 'medium')
if self.input_active:
# Virtual keyboard
self.render_virtual_keyboard()
else:
# Start input instruction
self.draw_text("Press Enter to start typing", 320, 150,
self.colors['yellow'], 'medium', center=True)
# Back button
self.draw_button("← Back", 270, 200, 100, 30, True)
# Instructions
if not self.input_active:
self.draw_panel(50, 400, 540, 60, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Enter: Start Input • Escape: Back", 320, 420,
self.colors['light_gray'], 'small', center=True)
def render_virtual_keyboard(self):
"""Render the virtual keyboard"""
kb_start_x = 50
kb_start_y = 150
key_width = 45
key_height = 30
for row_idx, row in enumerate(self.virtual_keyboard):
row_y = kb_start_y + row_idx * (key_height + 5)
# Special handling for bottom row (commands)
if row_idx == len(self.virtual_keyboard) - 1:
# Bottom row with command keys
key_widths = [80, 80, 80, 80] # Wider keys for commands
x_offset = 140 # Center the bottom row
else:
key_widths = [key_width] * len(row)
x_offset = kb_start_x
current_x = x_offset
for col_idx, char in enumerate(row):
selected = (row_idx == self.vk_cursor_y and col_idx == self.vk_cursor_x)
# Key background
if selected:
self.draw_panel(current_x, row_y, key_widths[col_idx], key_height,
self.colors['blue'], self.colors['light_blue'])
text_color = self.colors['white']
else:
self.draw_panel(current_x, row_y, key_widths[col_idx], key_height,
self.colors['dark_gray'], self.colors['gray'])
text_color = self.colors['light_gray']
# Key text
display_char = char
if char == '<DEL':
display_char = 'DEL'
elif char == 'SPACE':
display_char = 'SPC'
text_x = current_x + key_widths[col_idx] // 2
text_y = row_y + 8
self.draw_text(display_char, text_x, text_y, text_color, 'tiny', center=True)
current_x += key_widths[col_idx] + 5
# Instructions for virtual keyboard
self.draw_panel(50, 420, 540, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Arrows: Navigate • Enter: Select • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_edit_profile(self):
"""Render profile editing screen with improved layout"""
if not self.active_profile:
self.current_screen = "main_menu"
return
profile = self.profiles[self.active_profile]
# Header
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray'])
title = f"Edit: {profile.name}"
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True)
# Profile stats
stats_text = f"Games: {profile.games_played} • Score: {profile.best_score}"
self.draw_text(stats_text, 320, 50, self.colors['light_green'], 'small', center=True)
# Settings panel
panel_height = 280
self.draw_panel(60, 90, 520, panel_height, self.colors['dark_gray'], self.colors['gray'])
# Settings title
self.draw_text("Settings", 320, 105, self.colors['white'], 'large', center=True)
settings_items = [
("Difficulty", f"{profile.settings['difficulty'].title()}"),
("Sound Vol", f"{profile.settings['sound_volume']}%"),
("Music Vol", f"{profile.settings['music_volume']}%"),
("Screen Shake", "On" if profile.settings['screen_shake'] else "Off"),
]
start_y = 130
item_height = 35
for i, (label, value) in enumerate(settings_items):
item_y = start_y + i * item_height
selected = (i == self.selected_index)
# Setting item background
if selected:
self.draw_panel(80, item_y, 480, 25, self.colors['blue'], self.colors['light_blue'])
text_color = self.colors['white']
value_color = self.colors['light_green']
# Navigation arrows for selected item
self.draw_text("", 70, item_y + 5, self.colors['white'], 'small')
self.draw_text("", 570, item_y + 5, self.colors['white'], 'small')
else:
text_color = self.colors['light_gray']
value_color = self.colors['yellow']
# Setting label and value
self.draw_text(label, 90, item_y + 5, text_color, 'medium')
self.draw_text(value, 500, item_y + 5, value_color, 'medium')
# Action buttons
button_y = 310
save_selected = (self.selected_index == len(settings_items))
back_selected = (self.selected_index == len(settings_items) + 1)
self.draw_button("Save", 200, button_y, 80, 30, save_selected)
self.draw_button("← Back", 320, button_y, 80, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Left/Right: Adjust • Enter: Save/Back • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def draw_text(self, text: str, x: int, y: int, color, font_type='medium', center: bool = False):
"""Draw text on screen with improved styling"""
if not text:
return
factory = sdl2.ext.SpriteFactory(renderer=self.renderer)
font = self.fonts.get(font_type, self.fonts['medium'])
text_sprite = factory.from_text(text, color=color, fontmanager=font)
if center:
x = x - text_sprite.size[0] // 2
text_sprite.position = (x, y)
self.renderer.copy(text_sprite, dstrect=text_sprite.position)
return text_sprite.size
def draw_panel(self, x: int, y: int, width: int, height: int, bg_color=None, border_color=None, border_width=2):
"""Draw a styled panel/box"""
if bg_color:
self.renderer.fill((x, y, width, height), bg_color)
if border_color:
# Draw border
for i in range(border_width):
self.renderer.draw_rect((x + i, y + i, width - 2*i, height - 2*i), border_color)
def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False):
"""Draw a styled button"""
# Button colors based on state
if selected:
bg_color = self.colors['blue']
border_color = self.colors['light_blue']
text_color = self.colors['white']
else:
bg_color = self.colors['dark_gray']
border_color = self.colors['gray']
text_color = self.colors['light_gray']
# Draw button background and border
self.draw_panel(x, y, width, height, bg_color, border_color)
# Draw button text centered
text_x = x + width // 2
text_y = y + height // 2 - 12 # Approximate text height offset
self.draw_text(text, text_x, text_y, text_color, 'medium', center=True)
def handle_keyboard_input(self, event):
"""Handle keyboard input for navigation only (no text input)"""
key = event.key.keysym.sym
# Navigation mode only
if key == sdl2.SDLK_UP:
self.navigate_up()
elif key == sdl2.SDLK_DOWN:
self.navigate_down()
elif key == sdl2.SDLK_LEFT:
self.navigate_left()
elif key == sdl2.SDLK_RIGHT:
self.navigate_right()
elif key == sdl2.SDLK_RETURN or key == sdl2.SDLK_SPACE:
self.handle_confirm()
elif key == sdl2.SDLK_ESCAPE:
self.handle_back()
elif key == sdl2.SDLK_DELETE or key == sdl2.SDLK_BACKSPACE:
self.handle_delete()
elif key == sdl2.SDLK_TAB:
self.handle_menu()
def run(self):
"""Main application loop"""
self.init_sdl()
# Use SDL2's built-in timing instead of Clock
target_fps = 30 # Lower FPS for better performance on smaller screens
frame_time = 1000 // target_fps # milliseconds per frame
while self.running:
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
self.running = False
elif event.type == sdl2.SDL_KEYDOWN:
self.handle_keyboard_input(event)
# Handle gamepad input (if available)
self.handle_gamepad_input()
# Render frame
frame_start = sdl2.SDL_GetTicks()
self.render()
# Cap framerate
frame_duration = sdl2.SDL_GetTicks() - frame_start
if frame_duration < frame_time:
sdl2.SDL_Delay(frame_time - frame_duration)
# Cleanup
if self.gamepad:
sdl2.SDL_JoystickClose(self.gamepad)
sdl2.ext.quit()
def main():
"""Entry point"""
print("Starting Profile Manager...")
print("Controls:")
print(" Arrow Keys: Navigate menus")
print(" Enter/Space: Confirm/Select")
print(" Escape: Back/Cancel")
print(" Delete/Backspace: Delete")
print(" Tab: Menu (reserved)")
print(" Keyboard typing: Text input when creating profiles")
print("")
print("Gamepad Controls (if connected):")
print(" D-Pad: Navigate menus")
print(" A Button: Confirm/Select")
print(" B Button: Back/Cancel")
print(" X Button: Delete")
manager = ProfileManager()
manager.run()
if __name__ == "__main__":
main()

79
rats.py

@ -6,6 +6,8 @@ import json
from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring
from units import points from units import points
from user_profile_integration import UserProfileIntegration, get_global_leaderboard
class MiceMaze( class MiceMaze(
controls.KeyBindings, controls.KeyBindings,
@ -17,13 +19,30 @@ class MiceMaze(
# ==================== INITIALIZATION ==================== # ==================== INITIALIZATION ====================
def __init__(self, maze_file): def __init__(self, maze_file):
# Initialize user profile integration
self.profile_integration = UserProfileIntegration()
self.map = maze.Map(maze_file) self.map = maze.Map(maze_file)
self.audio = True
# Load profile-specific settings
self.audio = self.profile_integration.get_setting('sound_enabled', True)
sound_volume = self.profile_integration.get_setting('sound_volume', 50)
self.cell_size = 40 self.cell_size = 40
self.full_screen = False self.full_screen = False
# Initialize render engine with profile-aware title
player_name = self.profile_integration.get_profile_name()
window_title = f"Mice! - {player_name}"
self.render_engine = engine.GameWindow(self.map.width, self.map.height, self.render_engine = engine.GameWindow(self.map.width, self.map.height,
self.cell_size, "Mice!", self.cell_size, window_title,
key_callback=self.trigger) key_callback=self.trigger)
# Apply profile settings
if hasattr(self.render_engine, 'set_volume'):
self.render_engine.set_volume(sound_volume)
self.load_assets() self.load_assets()
self.render_engine.window.show() self.render_engine.window.show()
self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2)) self.pointer = (random.randint(1, self.map.width-2), random.randint(1, self.map.height-2))
@ -40,6 +59,7 @@ class MiceMaze(
self.start_game() self.start_game()
self.background_texture = None self.background_texture = None
self.configs = self.get_config() self.configs = self.get_config()
self.combined_scores = None
def get_config(self): def get_config(self):
@ -51,6 +71,7 @@ class MiceMaze(
return configs return configs
def start_game(self): def start_game(self):
self.combined_scores = False
self.ammo = { self.ammo = {
"bomb": { "bomb": {
"count": 2, "count": 2,
@ -104,7 +125,27 @@ class MiceMaze(
self.render_engine.dialog("Pause") self.render_engine.dialog("Pause")
return return
if self.game_status == "start_menu": if self.game_status == "start_menu":
self.render_engine.dialog("Welcome to the Mice!", subtitle="A game by Matteo because he was bored",image=self.assets["BMP_WEWIN"]) # Create personalized greeting
player_name = self.profile_integration.get_profile_name()
device_id = self.profile_integration.get_device_id()
greeting_title = f"Welcome to Mice, {player_name}!"
# Build subtitle with proper formatting
subtitle = "A game by Matteo, because he was bored."
device_line = f"Device: {device_id}"
# Show profile stats if available
if self.profile_integration.current_profile:
profile = self.profile_integration.current_profile
stats_line = f"Best Score: {profile['best_score']} | Games: {profile['games_played']}"
full_subtitle = f"{subtitle}\n{device_line}\n{stats_line}"
else:
full_subtitle = f"{device_line}\nNo profile loaded - playing as guest"
self.render_engine.dialog(greeting_title,
subtitle=full_subtitle,
image=self.assets["BMP_WEWIN"])
return return
self.render_engine.delete_tag("unit") self.render_engine.delete_tag("unit")
self.render_engine.delete_tag("effect") self.render_engine.delete_tag("effect")
@ -131,10 +172,30 @@ class MiceMaze(
def game_over(self): def game_over(self):
if self.game_end[0]: if self.game_end[0]:
if not self.combined_scores:
self.combined_scores = get_global_leaderboard(4)
global_scores = []
for entry in self.combined_scores:
# Convert to format expected by dialog: [date, score, name, device]
global_scores.append([
entry.get('last_play', ''),
entry.get('best_score', 0),
entry.get('user_id', 'Unknown'),
])
if not self.game_end[1]: if not self.game_end[1]:
self.render_engine.dialog("Game Over: Mice are too many!", image=self.assets["BMP_WEWIN"]) self.render_engine.dialog(
"Game Over: Mice are too many!",
image=self.assets["BMP_WEWIN"],
scores=global_scores
)
else: else:
self.render_engine.dialog(f"You Win! Points: {self.points}", image=self.assets["BMP_WEWIN"], scores=self.read_score()) self.render_engine.dialog(
f"You Win! Points: {self.points}",
image=self.assets["BMP_WEWIN"],
scores=global_scores
)
return True return True
@ -144,6 +205,10 @@ class MiceMaze(
self.render_engine.play_sound("WEWIN.WAV") self.render_engine.play_sound("WEWIN.WAV")
self.game_end = (True, False) self.game_end = (True, False)
self.game_status = "paused" self.game_status = "paused"
# Track incomplete game in profile
self.profile_integration.update_game_stats(self.points, completed=False)
return True return True
if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()): if not count_rats and not any(isinstance(unit, points.Point) for unit in self.units.values()):
self.render_engine.stop_sound() self.render_engine.stop_sound()
@ -151,7 +216,11 @@ class MiceMaze(
self.render_engine.play_sound("WELLDONE.WAV", tag="effects") self.render_engine.play_sound("WELLDONE.WAV", tag="effects")
self.game_end = (True, True) self.game_end = (True, True)
self.game_status = "paused" self.game_status = "paused"
# Save score to both traditional file and user profile
self.save_score() self.save_score()
self.profile_integration.update_game_stats(self.points, completed=True)
return True return True

547
score_api.py

@ -0,0 +1,547 @@
#!/usr/bin/env python3
"""
Mice Game Score API
FastAPI application with SQLite database for user and score management
"""
from fastapi import FastAPI, HTTPException, Depends
from fastapi.responses import JSONResponse
import sqlite3
import os
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel
import hashlib
import uuid
# Pydantic models for request/response
class User(BaseModel):
user_id: str
device_id: str
created_at: Optional[str] = None
last_active: Optional[str] = None
class Score(BaseModel):
user_id: str
device_id: str
score: int
game_completed: bool = True
timestamp: Optional[str] = None
class ScoreResponse(BaseModel):
id: int
user_id: str
device_id: str
score: int
game_completed: bool
timestamp: str
class UserResponse(BaseModel):
user_id: str
device_id: str
created_at: str
last_active: str
total_scores: int
best_score: int
# Database setup
DATABASE_FILE = "mice_game.db"
def init_database():
"""Initialize the SQLite database with required tables"""
conn = sqlite3.connect(DATABASE_FILE)
cursor = conn.cursor()
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
device_id TEXT NOT NULL,
created_at TEXT NOT NULL,
last_active TEXT NOT NULL,
UNIQUE(user_id, device_id)
)
''')
# Create scores table
cursor.execute('''
CREATE TABLE IF NOT EXISTS scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
device_id TEXT NOT NULL,
score INTEGER NOT NULL,
game_completed BOOLEAN NOT NULL DEFAULT 1,
timestamp TEXT NOT NULL,
FOREIGN KEY (user_id, device_id) REFERENCES users (user_id, device_id)
)
''')
# Create indexes for better performance
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_device ON users(device_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scores_user_device ON scores(user_id, device_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scores_timestamp ON scores(timestamp)')
conn.commit()
conn.close()
def get_db_connection():
"""Get database connection"""
if not os.path.exists(DATABASE_FILE):
init_database()
conn = sqlite3.connect(DATABASE_FILE)
conn.row_factory = sqlite3.Row # Enable column access by name
return conn
def close_db_connection(conn):
"""Close database connection"""
conn.close()
# FastAPI app
app = FastAPI(
title="Mice Game Score API",
description="API for managing users and scores in the Mice game",
version="1.0.0"
)
# Initialize database on startup
@app.on_event("startup")
def startup_event():
init_database()
# Utility functions
def validate_device_id(device_id: str) -> bool:
"""Validate device ID format"""
return device_id.startswith("DEV-") and len(device_id) == 12
def validate_user_id(user_id: str) -> bool:
"""Validate user ID format"""
return len(user_id.strip()) > 0 and len(user_id) <= 50
def user_exists(conn, user_id: str, device_id: str) -> bool:
"""Check if user exists in database"""
cursor = conn.cursor()
cursor.execute(
"SELECT 1 FROM users WHERE user_id = ? AND device_id = ?",
(user_id, device_id)
)
return cursor.fetchone() is not None
def user_id_unique_globally(conn, user_id: str) -> bool:
"""Check if user_id is globally unique (across all devices)"""
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM users WHERE user_id = ?", (user_id,))
return cursor.fetchone() is None
# API Endpoints
@app.get("/")
def read_root():
"""API root endpoint"""
return {
"message": "Mice Game Score API",
"version": "1.0.0",
"endpoints": [
"GET /users/{device_id} - Get all users for device",
"POST /signup/{device_id}/{user_id} - Register new user",
"POST /score/{device_id}/{user_id} - Submit score"
]
}
@app.post("/signup/{device_id}/{user_id}")
def signup_user(device_id: str, user_id: str):
"""
Register a new user for a device
- device_id: Device identifier (format: DEV-XXXXXXXX)
- user_id: Unique user identifier
"""
# Validate inputs
if not validate_device_id(device_id):
raise HTTPException(
status_code=400,
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'"
)
if not validate_user_id(user_id):
raise HTTPException(
status_code=400,
detail="Invalid user_id. Must be 1-50 characters long"
)
conn = get_db_connection()
try:
# Check if user_id is globally unique
if not user_id_unique_globally(conn, user_id):
raise HTTPException(
status_code=409,
detail=f"User ID '{user_id}' already exists. Choose a different user ID."
)
# Check if user already exists for this device
if user_exists(conn, user_id, device_id):
raise HTTPException(
status_code=409,
detail=f"User '{user_id}' already registered for device '{device_id}'"
)
# Register the user
cursor = conn.cursor()
now = datetime.now().isoformat()
cursor.execute(
"INSERT INTO users (user_id, device_id, created_at, last_active) VALUES (?, ?, ?, ?)",
(user_id, device_id, now, now)
)
conn.commit()
return {
"success": True,
"message": f"User '{user_id}' successfully registered for device '{device_id}'",
"user": {
"user_id": user_id,
"device_id": device_id,
"created_at": now
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
@app.post("/score/{device_id}/{user_id}")
def submit_score(device_id: str, user_id: str, score_data: Score):
"""
Submit a score for a registered user
- device_id: Device identifier
- user_id: User identifier
- score_data: Score information (score, game_completed)
"""
# Validate inputs
if not validate_device_id(device_id):
raise HTTPException(
status_code=400,
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'"
)
if not validate_user_id(user_id):
raise HTTPException(
status_code=400,
detail="Invalid user_id"
)
if score_data.score < 0:
raise HTTPException(
status_code=400,
detail="Score must be non-negative"
)
conn = get_db_connection()
try:
# Check if user exists and is registered for this device
if not user_exists(conn, user_id, device_id):
raise HTTPException(
status_code=404,
detail=f"User '{user_id}' not registered for device '{device_id}'. Please signup first."
)
# Add the score
cursor = conn.cursor()
timestamp = datetime.now().isoformat()
cursor.execute(
"INSERT INTO scores (user_id, device_id, score, game_completed, timestamp) VALUES (?, ?, ?, ?, ?)",
(user_id, device_id, score_data.score, score_data.game_completed, timestamp)
)
# Update user's last_active timestamp
cursor.execute(
"UPDATE users SET last_active = ? WHERE user_id = ? AND device_id = ?",
(timestamp, user_id, device_id)
)
conn.commit()
# Get user's statistics
cursor.execute(
"""
SELECT
COUNT(*) as total_games,
MAX(score) as best_score,
AVG(score) as avg_score,
SUM(score) as total_score
FROM scores
WHERE user_id = ? AND device_id = ?
""",
(user_id, device_id)
)
stats = cursor.fetchone()
return {
"success": True,
"message": f"Score {score_data.score} submitted successfully",
"score_id": cursor.lastrowid,
"user_stats": {
"user_id": user_id,
"device_id": device_id,
"total_games": stats["total_games"],
"best_score": stats["best_score"],
"average_score": round(stats["avg_score"], 2) if stats["avg_score"] else 0,
"total_score": stats["total_score"]
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
@app.get("/users/{device_id}")
def get_device_users(device_id: str) -> List[UserResponse]:
"""
Get all registered users for a specific device
- device_id: Device identifier
"""
# Validate device_id
if not validate_device_id(device_id):
raise HTTPException(
status_code=400,
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'"
)
conn = get_db_connection()
try:
cursor = conn.cursor()
# Get users with their statistics
cursor.execute(
"""
SELECT
u.user_id,
u.device_id,
u.created_at,
u.last_active,
COUNT(s.id) as total_scores,
COALESCE(MAX(s.score), 0) as best_score
FROM users u
LEFT JOIN scores s ON u.user_id = s.user_id AND u.device_id = s.device_id
WHERE u.device_id = ?
GROUP BY u.user_id, u.device_id, u.created_at, u.last_active
ORDER BY u.last_active DESC
""",
(device_id,)
)
users = cursor.fetchall()
if not users:
return []
return [
UserResponse(
user_id=user["user_id"],
device_id=user["device_id"],
created_at=user["created_at"],
last_active=user["last_active"],
total_scores=user["total_scores"],
best_score=user["best_score"]
)
for user in users
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
# Additional useful endpoints
@app.get("/scores/{device_id}/{user_id}")
def get_user_scores(device_id: str, user_id: str, limit: int = 10) -> List[ScoreResponse]:
"""
Get recent scores for a user
- device_id: Device identifier
- user_id: User identifier
- limit: Maximum number of scores to return (default: 10)
"""
if not validate_device_id(device_id) or not validate_user_id(user_id):
raise HTTPException(status_code=400, detail="Invalid device_id or user_id")
if limit < 1 or limit > 100:
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100")
conn = get_db_connection()
try:
# Check if user exists
if not user_exists(conn, user_id, device_id):
raise HTTPException(
status_code=404,
detail=f"User '{user_id}' not found for device '{device_id}'"
)
cursor = conn.cursor()
cursor.execute(
"""
SELECT id, user_id, device_id, score, game_completed, timestamp
FROM scores
WHERE user_id = ? AND device_id = ?
ORDER BY timestamp DESC
LIMIT ?
""",
(user_id, device_id, limit)
)
scores = cursor.fetchall()
return [
ScoreResponse(
id=score["id"],
user_id=score["user_id"],
device_id=score["device_id"],
score=score["score"],
game_completed=bool(score["game_completed"]),
timestamp=score["timestamp"]
)
for score in scores
]
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
@app.get("/leaderboard/{device_id}")
def get_device_leaderboard(device_id: str, limit: int = 10):
"""
Get leaderboard for a device (top scores)
- device_id: Device identifier
- limit: Maximum number of entries to return (default: 10)
"""
if not validate_device_id(device_id):
raise HTTPException(status_code=400, detail="Invalid device_id")
if limit < 1 or limit > 100:
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100")
conn = get_db_connection()
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
user_id,
MAX(score) as best_score,
COUNT(*) as total_games,
MAX(timestamp) as last_play
FROM scores
WHERE device_id = ?
GROUP BY user_id
ORDER BY best_score DESC, last_play DESC
LIMIT ?
""",
(device_id, limit)
)
leaderboard = cursor.fetchall()
return [
{
"rank": idx + 1,
"user_id": entry["user_id"],
"best_score": entry["best_score"],
"total_games": entry["total_games"],
"last_play": entry["last_play"]
}
for idx, entry in enumerate(leaderboard)
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
@app.get("/leaderboard/global/top")
def get_global_leaderboard(limit: int = 10):
"""
Get global leaderboard across all devices (absolute top scores)
- limit: Maximum number of entries to return (default: 10)
"""
if limit < 1 or limit > 100:
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100")
conn = get_db_connection()
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
user_id,
device_id,
MAX(score) as best_score,
COUNT(*) as total_games,
MAX(timestamp) as last_play
FROM scores
GROUP BY user_id, device_id
ORDER BY best_score DESC, last_play DESC
LIMIT ?
""",
(limit,)
)
leaderboard = cursor.fetchall()
return [
{
"rank": idx + 1,
"user_id": entry["user_id"],
"device_id": entry["device_id"],
"best_score": entry["best_score"],
"total_games": entry["total_games"],
"last_play": entry["last_play"]
}
for idx, entry in enumerate(leaderboard)
]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
finally:
close_db_connection(conn)
if __name__ == "__main__":
import uvicorn
print("Starting Mice Game Score API...")
print("Available endpoints:")
print(" POST /signup/{device_id}/{user_id} - Register new user")
print(" POST /score/{device_id}/{user_id} - Submit score")
print(" GET /users/{device_id} - Get all users for device")
print(" GET /scores/{device_id}/{user_id} - Get user scores")
print(" GET /leaderboard/{device_id} - Get device leaderboard")
print()
# Initialize database
init_database()
uvicorn.run(app, host="0.0.0.0", port=8000)

307
score_api_client.py

@ -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!")

112
simple_profile_demo.py

@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Simple Profile Manager with API Integration Demo
"""
import json
from user_profile_integration import UserProfileIntegration
def main():
print("=== Profile Manager with API Integration ===")
# Initialize the integration
integration = UserProfileIntegration()
print(f"\\nDevice ID: {integration.get_device_id()}")
print(f"Active Profile: {integration.get_profile_name()}")
print(f"API Server: {'Connected' if integration.api_enabled else 'Offline'}")
while True:
print("\\n=== MAIN MENU ===")
print("1. Show Profile Info")
print("2. View Online Leaderboard")
print("3. View All Device Users")
print("4. Submit Test Score")
print("5. Create New Profile")
print("6. Exit")
try:
choice = input("\\nSelect option (1-6): ").strip()
if choice == "1":
# Show profile info
info = integration.get_profile_info()
if info:
print(f"\\n=== PROFILE INFO ===")
for key, value in info.items():
print(f"{key.replace('_', ' ').title()}: {value}")
else:
print("No active profile loaded")
elif choice == "2":
# Show leaderboard
if not integration.api_enabled:
print("API server not available")
continue
print(f"\\n=== LEADERBOARD ===")
leaderboard = integration.get_device_leaderboard(10)
if leaderboard:
for entry in leaderboard:
print(f"{entry['rank']}. {entry['user_id']}: {entry['best_score']} pts ({entry['total_games']} games)")
else:
print("No scores recorded yet")
elif choice == "3":
# Show all users
if not integration.api_enabled:
print("API server not available")
continue
print(f"\\n=== DEVICE USERS ===")
users = integration.get_all_device_users()
if users:
for user in users:
print(f"{user['user_id']}: Best {user['best_score']}, {user['total_scores']} games")
else:
print("No users registered yet")
elif choice == "4":
# Submit test score
if not integration.current_profile:
print("No active profile to submit score for")
continue
try:
score = int(input("Enter test score: "))
result = integration.update_game_stats(score, True)
if result:
print(f"Score {score} submitted successfully!")
else:
print("Failed to submit score")
except ValueError:
print("Invalid score entered")
elif choice == "5":
# Create new profile - simplified for demo
name = input("Enter new profile name: ").strip()
if not name:
print("Name cannot be empty")
continue
success = integration.register_new_user(name)
if success or not integration.api_enabled:
print(f"Profile '{name}' created successfully!")
else:
print("Failed to create profile")
elif choice == "6":
print("Goodbye!")
break
else:
print("Invalid choice")
except KeyboardInterrupt:
print("\\nGoodbye!")
break
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()

140
test_score_api.py

@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
Test script for Mice Game Score API
"""
import requests
import json
import time
API_BASE_URL = "http://localhost:8000"
def test_api():
"""Test all API endpoints"""
print("Testing Mice Game Score API...")
print("=" * 50)
# Test device and user IDs
device_id = "DEV-TEST001"
user1 = "TestUser1"
user2 = "TestUser2"
try:
# 1. Test root endpoint
print("\n1. Testing root endpoint:")
response = requests.get(f"{API_BASE_URL}/")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 2. Test user signup
print(f"\n2. Testing user signup for {user1}:")
response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user1}")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
print(f"\n3. Testing user signup for {user2}:")
response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user2}")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 4. Test duplicate signup (should fail)
print(f"\n4. Testing duplicate signup for {user1} (should fail):")
response = requests.post(f"{API_BASE_URL}/signup/{device_id}/{user1}")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 5. Test getting users for device
print(f"\n5. Testing get users for device {device_id}:")
response = requests.get(f"{API_BASE_URL}/users/{device_id}")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 6. Test score submission
print(f"\n6. Testing score submission for {user1}:")
score_data = {
"user_id": user1,
"device_id": device_id,
"score": 1500,
"game_completed": True
}
response = requests.post(
f"{API_BASE_URL}/score/{device_id}/{user1}",
json=score_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 7. Test more score submissions
scores_to_test = [
(user1, 2000, True),
(user1, 1200, False),
(user2, 1800, True),
(user2, 2500, True)
]
print(f"\n7. Testing multiple score submissions:")
for user, score, completed in scores_to_test:
score_data = {
"user_id": user,
"device_id": device_id,
"score": score,
"game_completed": completed
}
response = requests.post(
f"{API_BASE_URL}/score/{device_id}/{user}",
json=score_data
)
print(f" {user} - Score: {score}, Completed: {completed} -> Status: {response.status_code}")
time.sleep(0.1) # Small delay between requests
# 8. Test score submission for non-registered user (should fail)
print(f"\n8. Testing score submission for non-registered user (should fail):")
score_data = {
"user_id": "NonExistentUser",
"device_id": device_id,
"score": 1000,
"game_completed": True
}
response = requests.post(
f"{API_BASE_URL}/score/{device_id}/NonExistentUser",
json=score_data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
# 9. Test getting user scores
print(f"\n9. Testing get scores for {user1}:")
response = requests.get(f"{API_BASE_URL}/scores/{device_id}/{user1}")
print(f"Status: {response.status_code}")
scores = response.json()
print(f"Number of scores: {len(scores)}")
for score in scores:
print(f" Score: {score['score']}, Completed: {score['game_completed']}, Time: {score['timestamp']}")
# 10. Test leaderboard
print(f"\n10. Testing leaderboard for device {device_id}:")
response = requests.get(f"{API_BASE_URL}/leaderboard/{device_id}")
print(f"Status: {response.status_code}")
leaderboard = response.json()
print("Leaderboard:")
for entry in leaderboard:
print(f" Rank {entry['rank']}: {entry['user_id']} - Best Score: {entry['best_score']} ({entry['total_games']} games)")
# 11. Test final user list
print(f"\n11. Final user list for device {device_id}:")
response = requests.get(f"{API_BASE_URL}/users/{device_id}")
users = response.json()
for user in users:
print(f" {user['user_id']}: Best Score: {user['best_score']}, Total Games: {user['total_scores']}")
print("\n" + "=" * 50)
print("API Testing completed successfully!")
except requests.exceptions.ConnectionError:
print("ERROR: Could not connect to API server.")
print("Make sure the API server is running with: python score_api.py")
except Exception as e:
print(f"ERROR: {e}")
if __name__ == "__main__":
test_api()

BIN
units/__pycache__/rat.cpython-313.pyc

Binary file not shown.

339
user_profile_integration.py

@ -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://localhost: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")

64
user_profiles.json

@ -0,0 +1,64 @@
{
"profiles": {
"Player1": {
"name": "Player1",
"created_date": "2024-01-15T10:30:00",
"last_played": "2024-01-20T14:45:00",
"games_played": 25,
"total_score": 15420,
"best_score": 980,
"settings": {
"difficulty": "normal",
"sound_volume": 75,
"music_volume": 60,
"screen_shake": true,
"auto_save": true
},
"achievements": [
"first_win",
"score_500",
"games_played_10"
]
},
"Alice": {
"name": "Alice",
"created_date": "2024-01-10T09:15:00",
"last_played": "2025-08-21T13:00:29.825909",
"games_played": 43,
"total_score": 33000,
"best_score": 1250,
"settings": {
"difficulty": "hard",
"sound_volume": 50,
"music_volume": 80,
"screen_shake": false,
"auto_save": true
},
"achievements": [
"first_win",
"score_500",
"score_1000",
"games_played_10",
"games_played_25",
"hard_mode_master"
]
},
"MAT": {
"name": "MAT",
"created_date": "2025-08-21T13:24:53.489189",
"last_played": "2025-08-21T16:43:50.615171",
"games_played": 2,
"total_score": 1234,
"best_score": 1234,
"settings": {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": true,
"auto_save": true
},
"achievements": []
}
},
"active_profile": "MAT"
}
Loading…
Cancel
Save