Browse Source

nice

master
Matteo Benedetto 4 months ago
parent
commit
79369a1dd8
  1. 1
      .gitignore
  2. 34
      api.log
  3. 0
      engine/score_api_client.py
  4. 2
      engine/user_profile_integration.py
  5. 431
      profile_manager.py
  6. 2
      rats.py
  7. 0
      server/api_requirements.txt
  8. BIN
      server/mice_game.db
  9. 0
      server/score_api.py
  10. 112
      simple_profile_demo.py
  11. 140
      test_score_api.py
  12. 38
      user_profiles.json

1
.gitignore vendored

@ -14,3 +14,4 @@ rats.spec
release/
unit/__pycache__
conf/keybindings.yaml
conf/keybindings.json

34
api.log

@ -2089,3 +2089,37 @@ INFO: 127.0.0.1:52734 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:52740 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:41304 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:41308 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:33472 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:33474 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:52616 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:35690 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:35706 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:35722 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:35724 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:35736 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:57592 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:57608 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:44530 - "POST /signup/DEV-3A2D87B7/AA HTTP/1.1" 200 OK
INFO: 127.0.0.1:37822 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:37834 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:46744 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:46752 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:43292 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:43296 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:43300 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:43310 - "GET /leaderboard/global/top?limit=10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59822 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:59832 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:36158 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:36170 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:40986 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:40990 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:39030 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:39046 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:38630 - "GET /users/DEV-3A2D87B7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:38640 - "POST /signup/DEV-3A2D87B7/Alice HTTP/1.1" 409 Conflict
INFO: 127.0.0.1:38490 - "GET /leaderboard/DEV-3A2D87B7?limit=10 HTTP/1.1" 200 OK
INFO: Shutting down
INFO: Waiting for application shutdown.
INFO: Application shutdown complete.
INFO: Finished server process [41455]

0
score_api_client.py → engine/score_api_client.py

2
user_profile_integration.py → engine/user_profile_integration.py

@ -9,7 +9,7 @@ import uuid
import platform
import hashlib
from datetime import datetime
from score_api_client import ScoreAPIClient
from engine.score_api_client import ScoreAPIClient
class UserProfileIntegration:

431
profile_manager.py

@ -23,6 +23,9 @@ import sdl2
import sdl2.ext
from sdl2.ext.compat import byteify
# Import the user profile integration system
from engine.user_profile_integration import UserProfileIntegration
@dataclass
class UserProfile:
@ -57,13 +60,27 @@ class ProfileManager:
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
# UI State
self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile
self.current_screen = "main_menu" # main_menu, profile_list, create_profile, edit_profile, leaderboard, profile_stats
self.selected_index = 0
self.input_text = ""
self.input_active = False
self.editing_field = None
# Error dialog state
self.show_error = False
self.error_message = ""
self.error_timer = 0
# Leaderboard data
self.leaderboard_data = []
self.leaderboard_type = "device" # "device" or "global"
# Virtual keyboard state
self.vk_cursor_x = 0 # Virtual keyboard cursor X
self.vk_cursor_y = 0 # Virtual keyboard cursor Y
@ -191,6 +208,15 @@ class ProfileManager:
self.profiles[name.strip()] = profile
self.save_profiles()
# Register with API server if available
if self.api_enabled:
result = self.integration.register_new_user(name.strip())
if result:
print(f"Profile {name.strip()} registered with server")
else:
print(f"Warning: Profile {name.strip()} created locally but not registered with server")
return True
def delete_profile(self, name: str) -> bool:
@ -209,6 +235,10 @@ class ProfileManager:
self.active_profile = name
self.profiles[name].last_played = datetime.now().isoformat()
self.save_profiles()
# Update integration system to load the new profile
self.integration.reload_profile()
return True
return False
@ -264,23 +294,31 @@ class ProfileManager:
def handle_button_press(self, button: int):
"""Handle gamepad button presses"""
# If error dialog is shown, dismiss it on any button press
if self.show_error:
self.show_error = False
return
# 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
if button == 3: # A/Confirm
self.handle_confirm()
elif button == 1: # B/Back
elif button == 4: # B/Back
self.handle_back()
elif button == 2: # X/Delete
elif button == 9: # X/Delete
self.handle_delete()
elif button == 3: # Y/Menu
elif button == 10: # Y/Menu
self.handle_menu()
def navigate_up(self):
"""Navigate up in current screen"""
if self.show_error:
return
if self.current_screen == "main_menu":
self.selected_index = max(0, self.selected_index - 1)
elif self.current_screen == "profile_list":
@ -296,8 +334,11 @@ class ProfileManager:
def navigate_down(self):
"""Navigate down in current screen"""
if self.show_error:
return
if self.current_screen == "main_menu":
max_index = 3 # Create, Select, Settings, Exit (0-3)
max_index = 5 # Create, Select, Settings, Leaderboard, Stats, Exit (0-5)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "profile_list":
max_index = len(self.profiles) # Profiles + Back
@ -305,6 +346,12 @@ class ProfileManager:
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 == "leaderboard":
max_index = 2 # Toggle type, refresh, back (0-2)
self.selected_index = min(max_index, self.selected_index + 1)
elif self.current_screen == "profile_stats":
max_index = 0 # Just back button
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
@ -315,16 +362,32 @@ class ProfileManager:
def navigate_left(self):
"""Navigate left (for adjusting values)"""
if self.show_error:
return
if self.current_screen == "edit_profile":
self.adjust_setting(-1)
elif self.current_screen == "leaderboard":
# Toggle between device and global leaderboard
if self.selected_index == 0:
self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device"
self.load_leaderboard_data()
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.show_error:
return
if self.current_screen == "edit_profile":
self.adjust_setting(1)
elif self.current_screen == "leaderboard":
# Toggle between device and global leaderboard
if self.selected_index == 0:
self.leaderboard_type = "global" if self.leaderboard_type == "device" else "device"
self.load_leaderboard_data()
elif self.current_screen == "create_profile" and self.input_active:
# Virtual keyboard navigation
max_x = len(self.virtual_keyboard[self.vk_cursor_y]) - 1
@ -332,6 +395,9 @@ class ProfileManager:
def handle_confirm(self):
"""Handle confirm action (A button)"""
if self.show_error:
return
if self.current_screen == "main_menu":
if self.selected_index == 0: # Create Profile
self.current_screen = "create_profile"
@ -344,7 +410,16 @@ class ProfileManager:
if self.active_profile:
self.current_screen = "edit_profile"
self.selected_index = 0
elif self.selected_index == 3: # Exit
elif self.selected_index == 3: # Leaderboard
if self.api_enabled:
self.current_screen = "leaderboard"
self.selected_index = 0
self.load_leaderboard_data()
elif self.selected_index == 4: # Profile Stats
if self.active_profile:
self.current_screen = "profile_stats"
self.selected_index = 0
elif self.selected_index == 5: # Exit
self.running = False
elif self.current_screen == "profile_list":
@ -378,17 +453,35 @@ class ProfileManager:
self.current_screen = "main_menu"
self.selected_index = 0
elif self.current_screen == "leaderboard":
if self.selected_index == 1: # Refresh
self.load_leaderboard_data()
elif self.selected_index == 2: # Back
self.current_screen = "main_menu"
self.selected_index = 3
elif self.current_screen == "profile_stats":
if self.selected_index == 0: # Back
self.current_screen = "main_menu"
self.selected_index = 4
def handle_back(self):
"""Handle back action (B button)"""
if self.show_error:
return
if self.current_screen == "main_menu":
self.running = False
elif self.current_screen in ["profile_list", "create_profile", "edit_profile"]:
elif self.current_screen in ["profile_list", "create_profile", "edit_profile", "leaderboard", "profile_stats"]:
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
def handle_delete(self):
"""Handle delete action (X button)"""
if self.show_error:
return
if self.current_screen == "profile_list":
profile_names = list(self.profiles.keys())
if self.selected_index < len(profile_names):
@ -426,10 +519,18 @@ class ProfileManager:
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
# Check if profile already exists
if self.input_text.strip() in self.profiles:
self.show_error_dialog(f"Profile '{self.input_text.strip()}' already exists!")
else:
if self.create_profile(self.input_text):
self.current_screen = "main_menu"
self.selected_index = 0
self.input_active = False
else:
self.show_error_dialog("Failed to create profile!")
else:
self.show_error_dialog("Profile name cannot be empty!")
elif selected_char == 'CANCEL':
# Cancel input
self.input_text = ""
@ -461,6 +562,27 @@ class ProfileManager:
elif self.selected_index == 3: # Screen Shake
profile.settings["screen_shake"] = not profile.settings["screen_shake"]
def load_leaderboard_data(self):
"""Load leaderboard data based on current type"""
if not self.api_enabled:
self.leaderboard_data = []
return
try:
if self.leaderboard_type == "device":
self.leaderboard_data = self.integration.get_device_leaderboard(10)
else: # global
self.leaderboard_data = self.integration.get_global_leaderboard(10)
except Exception as e:
print(f"Error loading leaderboard: {e}")
self.leaderboard_data = []
def show_error_dialog(self, message: str):
"""Show an error dialog with the given message"""
self.show_error = True
self.error_message = message
self.error_timer = time.time() + 3.0 # Show error for 3 seconds
def render(self):
"""Main rendering method"""
# Clear with dark background
@ -481,6 +603,17 @@ class ProfileManager:
self.render_create_profile()
elif self.current_screen == "edit_profile":
self.render_edit_profile()
elif self.current_screen == "leaderboard":
self.render_leaderboard()
elif self.current_screen == "profile_stats":
self.render_profile_stats()
# Render error dialog on top if active
if self.show_error:
self.render_error_dialog()
# Auto-dismiss after timer expires
if time.time() > self.error_timer:
self.show_error = False
self.renderer.present()
@ -493,16 +626,25 @@ class ProfileManager:
title = "Profile Manager"
self.draw_text(title, 320, 20, self.colors['light_blue'], 'title', center=True)
# Subtitle with active profile
# Subtitle with active profile and API status
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)
# API connection status
api_status = "API: Connected" if self.api_enabled else "API: Offline"
api_color = self.colors['light_green'] if self.api_enabled else self.colors['red']
self.draw_text(api_status, 320, 80, api_color, 'tiny', center=True)
# Device ID display
device_text = f"Device: {self.device_id}"
self.draw_text(device_text, 320, 95, self.colors['light_gray'], 'tiny', center=True)
# Menu panel
panel_x, panel_y = 120, 140
panel_width, panel_height = 400, 220
panel_x, panel_y = 120, 120
panel_width, panel_height = 400, 240
self.draw_panel(panel_x, panel_y, panel_width, panel_height,
self.colors['dark_gray'], self.colors['gray'])
@ -511,18 +653,27 @@ class ProfileManager:
"Create Profile",
"Select Profile",
"Edit Settings",
"Leaderboard",
"Profile Stats",
"Exit"
]
button_width = 280
button_height = 35
button_height = 30
button_x = panel_x + 60
start_y = panel_y + 20
start_y = panel_y + 15
for i, item in enumerate(menu_items):
button_y = start_y + i * (button_height + 10)
button_y = start_y + i * (button_height + 8)
selected = (i == self.selected_index)
self.draw_button(item, button_x, button_y, button_width, button_height, selected)
# Gray out unavailable options
if (item == "Edit Settings" or item == "Profile Stats") and not self.active_profile:
self.draw_button(f"{item} (No Profile)", button_x, button_y, button_width, button_height, False, disabled=True)
elif item == "Leaderboard" and not self.api_enabled:
self.draw_button(f"{item} (Offline)", button_x, button_y, button_width, button_height, False, disabled=True)
else:
self.draw_button(item, button_x, button_y, button_width, button_height, selected)
# Controls help panel
help_panel_y = 380
@ -748,6 +899,219 @@ class ProfileManager:
self.draw_text("Left/Right: Adjust • Enter: Save/Back • Escape: Cancel",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_leaderboard(self):
"""Render leaderboard screen"""
# Header
self.renderer.fill((0, 0, 640, 80), self.colors['dark_gray'])
title = f"{self.leaderboard_type.title()} Leaderboard"
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True)
if not self.api_enabled:
self.draw_text("Server offline - No leaderboard available", 320, 50,
self.colors['red'], 'medium', center=True)
self.draw_button("← Back", 270, 200, 100, 30, True)
return
# Toggle button
toggle_text = f"Type: {self.leaderboard_type.title()} (Left/Right to toggle)"
toggle_selected = (self.selected_index == 0)
self.draw_button(toggle_text, 120, 90, 400, 25, toggle_selected)
# Refresh button
refresh_selected = (self.selected_index == 1)
self.draw_button("Refresh", 150, 125, 80, 25, refresh_selected)
# Back button
back_selected = (self.selected_index == 2)
self.draw_button("← Back", 250, 125, 80, 25, back_selected)
# Leaderboard data
if not self.leaderboard_data:
self.draw_text("No leaderboard data available", 320, 200,
self.colors['yellow'], 'medium', center=True)
else:
# Leaderboard panel
panel_height = min(250, len(self.leaderboard_data) * 25 + 40)
self.draw_panel(50, 160, 540, panel_height,
self.colors['black'], self.colors['gray'])
# Headers
self.draw_text("Rank", 60, 175, self.colors['light_blue'], 'small')
self.draw_text("Player", 120, 175, self.colors['light_blue'], 'small')
self.draw_text("Score", 350, 175, self.colors['light_blue'], 'small')
self.draw_text("Games", 450, 175, self.colors['light_blue'], 'small')
if self.leaderboard_type == "global":
self.draw_text("Device", 520, 175, self.colors['light_blue'], 'small')
# Leaderboard entries
for i, entry in enumerate(self.leaderboard_data):
entry_y = 195 + i * 22
# Highlight current user
is_current_user = (self.active_profile and
entry.get('user_id') == self.active_profile)
text_color = self.colors['light_green'] if is_current_user else self.colors['light_gray']
# Rank
self.draw_text(str(entry.get('rank', i+1)), 60, entry_y, text_color, 'tiny')
# Player name
player_name = entry.get('user_id', 'Unknown')
if len(player_name) > 20:
player_name = player_name[:17] + "..."
self.draw_text(player_name, 120, entry_y, text_color, 'tiny')
# Score
score = entry.get('best_score', 0)
self.draw_text(str(score), 350, entry_y, text_color, 'tiny')
# Games
games = entry.get('total_games', 0)
self.draw_text(str(games), 450, entry_y, text_color, 'tiny')
# Device (global leaderboard only)
if self.leaderboard_type == "global":
device = entry.get('device_id', '')[:8]
self.draw_text(device, 520, entry_y, text_color, 'tiny')
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Left/Right: Toggle Type • Enter: Select • Escape: Back",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_profile_stats(self):
"""Render detailed profile statistics"""
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"Stats: {profile.name}"
self.draw_text(title, 320, 15, self.colors['light_blue'], 'title', center=True)
# Get additional info from integration system
integration_info = self.integration.get_profile_info()
# Main stats panel
self.draw_panel(50, 90, 540, 260, self.colors['dark_gray'], self.colors['gray'])
# Basic stats
stats_y = 105
line_height = 20
self.draw_text("Profile Statistics", 320, stats_y, self.colors['white'], 'large', center=True)
stats_y += 30
# Local stats
self.draw_text("Local Statistics:", 60, stats_y, self.colors['light_blue'], 'medium')
stats_y += line_height
self.draw_text(f"Games Played: {profile.games_played}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
self.draw_text(f"Best Score: {profile.best_score}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
self.draw_text(f"Total Score: {profile.total_score}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
self.draw_text(f"Achievements: {len(profile.achievements)}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
# Creation and last played dates
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"
self.draw_text(f"Created: {created}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
self.draw_text(f"Last Played: {last_played}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
# Server stats (if available)
if self.api_enabled and integration_info:
stats_y += 10
self.draw_text("Server Status:", 60, stats_y, self.colors['light_green'], 'medium')
stats_y += line_height
self.draw_text(f"Device ID: {integration_info.get('device_id', 'Unknown')}", 70, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
self.draw_text("✓ Profile synced with server", 70, stats_y, self.colors['light_green'], 'small')
else:
stats_y += 10
self.draw_text("Server Status:", 60, stats_y, self.colors['red'], 'medium')
stats_y += line_height
self.draw_text("✗ Server offline", 70, stats_y, self.colors['red'], 'small')
# Settings summary
stats_y = 105
settings_x = 350
self.draw_text("Current Settings:", settings_x, stats_y + 30, self.colors['light_blue'], 'medium')
stats_y += 50
difficulty = profile.settings.get('difficulty', 'normal').title()
self.draw_text(f"Difficulty: {difficulty}", settings_x + 10, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
sound_vol = profile.settings.get('sound_volume', 50)
self.draw_text(f"Sound Volume: {sound_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
music_vol = profile.settings.get('music_volume', 50)
self.draw_text(f"Music Volume: {music_vol}%", settings_x + 10, stats_y, self.colors['light_gray'], 'small')
stats_y += line_height
screen_shake = "On" if profile.settings.get('screen_shake', True) else "Off"
self.draw_text(f"Screen Shake: {screen_shake}", settings_x + 10, stats_y, self.colors['light_gray'], 'small')
# Back button
back_selected = (self.selected_index == 0)
self.draw_button("← Back", 270, 370, 100, 30, back_selected)
# Instructions
self.draw_panel(10, 420, 620, 40, self.colors['black'], self.colors['dark_gray'])
self.draw_text("Enter: Back • Escape: Back to Main Menu",
320, 435, self.colors['light_gray'], 'tiny', center=True)
def render_error_dialog(self):
"""Render error dialog overlay"""
# Semi-transparent overlay
overlay_color = sdl2.ext.Color(0, 0, 0, 128) # Black with alpha
self.renderer.fill((0, 0, 640, 480), overlay_color)
# Error dialog box
dialog_width = 400
dialog_height = 120
dialog_x = (640 - dialog_width) // 2
dialog_y = (480 - dialog_height) // 2
# Dialog background with red border
self.draw_panel(dialog_x, dialog_y, dialog_width, dialog_height,
self.colors['black'], self.colors['red'], border_width=4)
# Error title
self.draw_text("ERROR", dialog_x + dialog_width // 2, dialog_y + 20,
self.colors['red'], 'large', center=True)
# Error message
self.draw_text(self.error_message, dialog_x + dialog_width // 2, dialog_y + 50,
self.colors['white'], 'medium', center=True)
# Dismiss instruction
self.draw_text("Press any key to continue...", dialog_x + dialog_width // 2, dialog_y + 80,
self.colors['light_gray'], 'small', 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:
@ -774,10 +1138,14 @@ class ProfileManager:
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):
def draw_button(self, text: str, x: int, y: int, width: int, height: int, selected: bool = False, disabled: bool = False):
"""Draw a styled button"""
# Button colors based on state
if selected:
if disabled:
bg_color = self.colors['black']
border_color = self.colors['dark_gray']
text_color = self.colors['dark_gray']
elif selected:
bg_color = self.colors['blue']
border_color = self.colors['light_blue']
text_color = self.colors['white']
@ -798,6 +1166,11 @@ class ProfileManager:
"""Handle keyboard input for navigation only (no text input)"""
key = event.key.keysym.sym
# If error dialog is shown, dismiss it on any key press
if self.show_error:
self.show_error = False
return
# Navigation mode only
if key == sdl2.SDLK_UP:
self.navigate_up()
@ -852,22 +1225,34 @@ class ProfileManager:
def main():
"""Entry point"""
print("Starting Profile Manager...")
print("Starting Profile Manager with User Profile Integration...")
print("Features:")
print(" • Profile creation and management")
print(" • API server integration for leaderboards")
print(" • Device-specific and global leaderboards")
print(" • Detailed profile statistics")
print(" • Settings management")
print("")
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(" Left/Right: Adjust settings and toggle options")
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")
print("")
manager = ProfileManager()
print(f"Device ID: {manager.device_id}")
print(f"API Server: {'Connected' if manager.api_enabled else 'Offline'}")
print("")
manager.run()

2
rats.py

@ -6,7 +6,7 @@ import json
from engine import maze, sdl2 as engine, controls, graphics, unit_manager, scoring
from units import points
from user_profile_integration import UserProfileIntegration, get_global_leaderboard
from engine.user_profile_integration import UserProfileIntegration, get_global_leaderboard
class MiceMaze(

0
api_requirements.txt → server/api_requirements.txt

BIN
mice_game.db → server/mice_game.db

Binary file not shown.

0
score_api.py → server/score_api.py

112
simple_profile_demo.py

@ -1,112 +0,0 @@
#!/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

@ -1,140 +0,0 @@
#!/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()

38
user_profiles.json

@ -3,7 +3,7 @@
"Player1": {
"name": "Player1",
"created_date": "2024-01-15T10:30:00",
"last_played": "2024-01-20T14:45:00",
"last_played": "2025-08-21T17:57:06.304649",
"games_played": 25,
"total_score": 15420,
"best_score": 980,
@ -23,7 +23,7 @@
"Alice": {
"name": "Alice",
"created_date": "2024-01-10T09:15:00",
"last_played": "2025-08-21T13:00:29.825909",
"last_played": "2025-08-21T17:39:28.013833",
"games_played": 43,
"total_score": 33000,
"best_score": 1250,
@ -58,7 +58,39 @@
"auto_save": true
},
"achievements": []
},
"AA": {
"name": "AA",
"created_date": "2025-08-21T17:12:50.321070",
"last_played": "2025-08-21T17:12:50.321070",
"games_played": 0,
"total_score": 0,
"best_score": 0,
"settings": {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": true,
"auto_save": true
},
"achievements": []
},
"B0B": {
"name": "B0B",
"created_date": "2025-08-21T18:03:12.189612",
"last_played": "2025-08-21T18:04:57.426796",
"games_played": 0,
"total_score": 0,
"best_score": 0,
"settings": {
"difficulty": "normal",
"sound_volume": 50,
"music_volume": 50,
"screen_shake": true,
"auto_save": true
},
"achievements": []
}
},
"active_profile": "MAT"
"active_profile": "B0B"
}
Loading…
Cancel
Save