Browse Source

feat: add favicon generator and Pyodide profile sync integration

pygame-pyodide
Matteo Benedetto 2 months ago
parent
commit
3812d36cd6
  1. 92
      engine/user_profile_integration.py
  2. BIN
      favicon.ico
  3. 966
      index.html
  4. 57
      tools/create_favicon.py

92
engine/user_profile_integration.py

@ -8,6 +8,7 @@ import json
import uuid
import platform
import hashlib
import os
from datetime import datetime
from engine.score_api_client import ScoreAPIClient
@ -28,6 +29,7 @@ class UserProfileIntegration:
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
@ -47,23 +49,45 @@ class UserProfileIntegration:
def load_active_profile(self):
"""Load the currently active profile"""
print(f"[DEBUG] Attempting to load profile from: {self.profiles_file}")
print(f"[DEBUG] File exists: {os.path.exists(self.profiles_file)}")
try:
with open(self.profiles_file, 'r') as f:
data = json.load(f)
raw_content = f.read()
print(f"[DEBUG] File content length: {len(raw_content)} bytes")
print(f"[DEBUG] File content (first 500 chars): {raw_content[:500]}")
data = json.loads(raw_content)
print(f"[DEBUG] Parsed JSON keys: {list(data.keys())}")
print(f"[DEBUG] Active profile key value: {data.get('active_profile')}")
print(f"[DEBUG] Available profiles: {list(data.get('profiles', {}).keys())}")
active_name = data.get('active_profile')
if active_name and active_name in data['profiles']:
if active_name:
print(f"[DEBUG] Looking for profile: '{active_name}'")
if active_name in data['profiles']:
self.current_profile = data['profiles'][active_name]
print(f"Loaded profile: {self.current_profile['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}")
else:
print(f"[DEBUG] Profile '{active_name}' not found in profiles dict")
else:
print(f"[DEBUG] No active_profile specified in JSON")
except FileNotFoundError as e:
print(f"✗ Profile file not found: {e}")
except json.JSONDecodeError as e:
print(f"✗ Failed to parse profile JSON: {e}")
except Exception as e:
print(f"✗ Unexpected error loading profile: {e}")
self.current_profile = None
print(f"[DEBUG] Profile loading failed, current_profile set to None")
return False
def get_profile_name(self):
@ -117,13 +141,19 @@ class UserProfileIntegration:
def update_game_stats(self, score, completed=True):
"""Update the current profile's game statistics"""
print(f"[DEBUG UPDATE_STATS] update_game_stats called with score={score}, completed={completed}")
print(f"[DEBUG UPDATE_STATS] self.current_profile is None: {self.current_profile is None}")
if not self.current_profile:
print("No profile loaded - stats not saved")
print("[DEBUG UPDATE_STATS] No profile loaded - stats not saved")
return False
print(f"[DEBUG UPDATE_STATS] Profile name: {self.current_profile.get('name', 'UNKNOWN')}")
# Submit score to API first if available
if self.api_enabled:
profile_name = self.current_profile['name']
print(f"[DEBUG UPDATE_STATS] API enabled, submitting score for {profile_name}")
result = self.api_client.submit_score(
self.device_id,
profile_name,
@ -140,23 +170,30 @@ class UserProfileIntegration:
print(f"✗ Failed to submit score to server: {result.get('message')}")
try:
# Update local profile
print(f"[DEBUG UPDATE_STATS] Attempting to read {self.profiles_file}")
with open(self.profiles_file, 'r') as f:
data = json.load(f)
print(f"[DEBUG UPDATE_STATS] Read profiles file successfully")
print(f"[DEBUG UPDATE_STATS] Profiles keys in file: {list(data.get('profiles', {}).keys())}")
profile_name = self.current_profile['name']
print(f"[DEBUG UPDATE_STATS] Looking for profile '{profile_name}' in file")
if profile_name in data['profiles']:
profile = data['profiles'][profile_name]
print(f"[DEBUG UPDATE_STATS] Found profile in file")
# Update statistics
if completed:
profile['games_played'] += 1
print(f"Game completed for {profile_name}! Total games: {profile['games_played']}")
profile['games_played'] = profile.get('games_played', 0) + 1
print(f"[DEBUG UPDATE_STATS] Game completed! Total games now: {profile['games_played']}")
profile['total_score'] += score
if score > profile['best_score']:
profile['total_score'] = profile.get('total_score', 0) + score
old_best = profile.get('best_score', 0)
if score > old_best:
profile['best_score'] = score
print(f"New best score for {profile_name}: {score}!")
print(f"[DEBUG UPDATE_STATS] New best score for {profile_name}: {score}!")
profile['last_played'] = datetime.now().isoformat()
@ -164,14 +201,41 @@ class UserProfileIntegration:
self.current_profile = profile
# Save back to file
print(f"[DEBUG UPDATE_STATS] Writing updated profile back to {self.profiles_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']}")
print(f"[DEBUG UPDATE_STATS] Successfully saved! Score +{score}, New total: {profile['total_score']}, Best: {profile.get('best_score', 0)}")
# Call JavaScript function to sync profile back to localStorage (Pyodide only)
try:
# noinspection PyUnresolvedReference
from js import window
window.syncProfileUpdateToLocalStorage(
profile_name,
profile['best_score'],
profile['games_played'],
profile['total_score']
)
print(f"[DEBUG UPDATE_STATS] JS sync call completed successfully")
except ImportError:
print(f"[DEBUG UPDATE_STATS] Note: 'js' module not available (running in non-Pyodide environment)")
except Exception as js_err:
print(f"[DEBUG UPDATE_STATS] Warning: Failed to call JS sync function: {js_err}")
return True
else:
print(f"[DEBUG UPDATE_STATS] Profile '{profile_name}' NOT FOUND in profiles file!")
print(f"[DEBUG UPDATE_STATS] Available profiles: {list(data.get('profiles', {}).keys())}")
except FileNotFoundError as e:
print(f"[DEBUG UPDATE_STATS] ERROR: Profile file not found: {e}")
except json.JSONDecodeError as e:
print(f"[DEBUG UPDATE_STATS] ERROR: Invalid JSON in profile file: {e}")
except Exception as e:
print(f"Error updating profile stats: {e}")
print(f"[DEBUG UPDATE_STATS] ERROR: Unexpected error updating profile stats: {e}")
import traceback
traceback.print_exc()
return False

BIN
favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

966
index.html

@ -0,0 +1,966 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🐭 Mice! - Browser Edition</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #1e1e1e;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #ff9f43;
margin-bottom: 10px;
}
.container {
max-width: 800px;
width: 100%;
}
#status {
margin: 20px 0;
padding: 10px;
background-color: #2d2d2d;
border-radius: 5px;
font-family: monospace;
}
.canvas-container {
margin: 20px 0;
border: 2px solid #ff9f43;
border-radius: 5px;
background-color: #000;
display: inline-block;
}
#canvas {
display: block;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.controls {
margin: 20px 0;
}
button {
background-color: #ff9f43;
color: #1e1e1e;
border: none;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
button:hover {
background-color: #ffb366;
}
button:disabled {
background-color: #666;
cursor: not-allowed;
}
.info {
background-color: #2d2d2d;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
.info h2 {
color: #ff9f43;
margin-top: 0;
}
code {
background-color: #1e1e1e;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
/* Game-specific layout */
.game-layout {
display: flex;
gap: 20px;
align-items: flex-start;
}
.game-layout .left {
flex: 1 1 640px;
display: flex;
flex-direction: column;
align-items: center;
}
.game-layout .right {
width: 320px;
flex: 0 0 320px;
}
.hud {
margin-top: 8px;
background: rgba(0,0,0,0.6);
padding: 6px 10px;
border-radius: 4px;
color: #dcdcdc;
font-family: monospace;
}
.control-row { margin-top: 10px; display:flex; gap:8px; align-items:center; }
.control-row label { min-width: 70px; font-size:0.95em; }
.controls { display:flex; flex-wrap:wrap; gap:8px; }
.progress { margin-top: 12px; }
.help { margin-top: 12px; font-size:0.95em; }
input[type="range"] { width: 100%; accent-color: #ff9f43; }
progress { width: 100%; accent-color: #ff9f43; height: 8px; }
select { background-color: #2d2d2d; color: #fff; border: 1px solid #555; padding: 6px; border-radius: 3px; }
details summary { cursor: pointer; color: #ff9f43; font-weight: bold; }
details summary:hover { color: #ffb366; }
</style>
</head>
<body>
<div class="container">
<h1>🐭 Mice! - The Game</h1>
<p><strong>Eliminate the rats before they multiply!</strong> A strategic maze game with bombs, mines, and gas.</p>
<p style="font-size:0.9em;color:#aaa;">Browser Edition powered by Python + Pygame via Pyodide</p>
<div id="status">Loading Pyodide...</div>
<!-- Profile Creation Modal -->
<div id="profileModal" title="Create New Profile">
<form id="profileForm">
<div style="margin-bottom:15px;">
<label for="profileName">Player Name:</label><br>
<input type="text" id="profileName" name="profileName" placeholder="Enter your name" style="width:100%;padding:8px;margin-top:5px;border:1px solid #555;background:#2d2d2d;color:#fff;border-radius:3px;" required>
</div>
<div style="margin-bottom:15px;">
<label for="difficulty">Difficulty:</label><br>
<select id="difficulty" name="difficulty" style="width:100%;padding:8px;margin-top:5px;border:1px solid #555;background:#2d2d2d;color:#fff;border-radius:3px;">
<option value="normal">Normal</option>
<option value="hard">Hard</option>
<option value="expert">Expert</option>
</select>
</div>
<div style="margin-bottom:15px;">
<label>
<input type="checkbox" id="soundEnabled" name="soundEnabled" checked> Enable Sound
</label>
</div>
</form>
</div>
<div class="game-layout">
<div class="left">
<div class="canvas-container">
<canvas id="canvas" width="640" height="480" aria-label="Mice game canvas"></canvas>
</div>
<div id="hud" class="hud">Status: <span id="hud-status">Idle</span></div>
</div>
<div class="right">
<div class="controls">
<button id="startBtn" disabled>Start Game</button>
<button id="stopBtn" disabled>Stop Game</button>
<button id="fullscreenBtn">Fullscreen</button>
<button id="muteBtn">Mute</button>
</div>
<div class="control-row">
<label for="volumeRange">Volume</label>
<input id="volumeRange" type="range" min="0" max="100" value="100">
</div>
<div class="control-row">
<label for="profileSelect">Profile</label>
<select id="profileSelect"><option value="">(None)</option></select>
<button id="loadProfileBtn">Load</button>
</div>
<div class="control-row">
<button id="newProfileBtn" style="flex:1;">+ New Profile</button>
</div>
<div class="progress">
<label>Asset load progress</label>
<progress id="assetProgress" value="0" max="100"></progress>
<div id="assetProgressText">0 / 0</div>
</div>
<details>
<summary>Advanced</summary>
<div style="margin-top:8px;">
<button id="runDemoBtn">Run Pygame Demo</button>
<div style="margin-top:8px;font-size:0.9em;color:#ddd;">Useful for testing the canvas rendering when debugging.</div>
</div>
</details>
<div class="help">
<h3>Controls</h3>
<ul>
<li>Arrow keys / WASD — move</li>
<li>Space — bomb</li>
<li>M — mine</li>
<li>G — gas</li>
<li>N — nuclear</li>
<li>P — pause</li>
</ul>
</div>
</div>
</div>
<div class="info">
<h2>🎮 How to Play</h2>
<p>Move your cursor around the maze and use your arsenal to eliminate rats before they overrun the level:</p>
<ul>
<li><strong>Bombs (Space)</strong>: Primary weapon. Timer detonates in 4 directions. Chain reactions kill multiple rats.</li>
<li><strong>Mines (M)</strong>: Place traps in corridors. Triggers when rats step on them, releasing poison gas.</li>
<li><strong>Gas (G)</strong>: Poison clouds linger and accumulate damage. Great for rat-dense areas.</li>
<li><strong>Nuclear Bomb (N)</strong>: One-time use. Destroys all rats on screen instantly.</li>
<li><strong>Pause (P)</strong>: Stop and plan your strategy.</li>
</ul>
<h3 style="color:#ff9f43;margin-top:15px;">💡 Tips & Strategy</h3>
<ul style="font-size:0.95em;">
<li>Early game: Target adult rats to prevent reproduction (200+ ticks).</li>
<li>Use walls to direct bomb explosions efficiently.</li>
<li>Rat population limit: 200. Exceeding this = Game Over.</li>
<li>Collect bonus points by killing multiple rats with a single bomb.</li>
<li>Save the nuclear bomb for when rats get out of control.</li>
</ul>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
<script src="https://ryanking13.github.io/pyodide-pygame-demo/dist/pyodide.js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.13.2/themes/dark/jquery-ui.css">
<script src="https://code.jquery.com/ui/1.13.2/jquery-ui.min.js"></script>
<script type="text/javascript">
// Using the original pyodide.js from the pyodide-pygame-demo distribution
let pyodide = null;
let gameTask = null;
let animationHandle = null;
let animationRunning = false;
const statusDiv = document.getElementById('status');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const canvas = document.getElementById('canvas');
function updateStatus(message) {
statusDiv.textContent = message;
console.log(message);
}
async function fetchAndWrite(path) {
const res = await fetch(path);
if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`);
const text = await res.text();
// Write file into pyodide FS later via pyodide.FS
return { path, text };
}
async function loadProjectFiles() {
// Minimal set of files/folders required to run rats.py in the browser.
// We'll fetch top-level modules and small resources. Large binary assets (audio) are skipped.
const toFetch = [
'rats.py',
'maze.json',
'key.py',
];
// fetch engine/ and units/ Python files
const engineFiles = [
'engine/controls.py','engine/graphics.py','engine/maze.py','engine/pygame_layer.py',
'engine/score_api_client.py','engine/scoring.py','engine/unit_manager.py','engine/user_profile_integration.py'
];
const unitFiles = [
'units/__init__.py','units/bomb.py','units/gas.py','units/mine.py','units/points.py','units/rat.py','units/unit.py'
];
const confFiles = [
'conf/keybindings.yaml', 'conf/keybindings_r36s.yaml', 'conf/keybindings_rg40xx.yaml'
];
const all = toFetch.concat(engineFiles, unitFiles, confFiles);
// Asset manifest (try to fetch a manifest file listing asset paths)
let assetFiles = null;
let soundFiles = null;
try {
const mres = await fetch('assets/asset-manifest.json');
if (mres.ok) {
assetFiles = await mres.json();
}
} catch (e) {
console.warn('No asset-manifest.json found, will use built-in asset list');
}
// Sound manifest: include all sounds under sound/ into FS
try {
const sres = await fetch('assets/sound-manifest.json');
if (sres.ok) {
soundFiles = await sres.json();
}
} catch (e) {
console.warn('No sound-manifest.json found');
}
if (!assetFiles) {
assetFiles = [
'assets/Rat/BMP_TUNNEL.png',
'assets/Rat/BMP_WEWIN.png',
'assets/Rat/BMP_BOMB0.png',
'assets/Rat/BMP_POISON.png',
'assets/Rat/BMP_GAS.png',
'assets/Rat/BMP_EXPLOSION.png',
'assets/Rat/BMP_MALE_DOWN.png',
'assets/Rat/BMP_FEMALE_DOWN.png',
'assets/Rat/BMP_BONUS_10.png',
'assets/Rat/BMP_START_1.png',
'assets/Rat/BMP_TITLE.png',
'assets/decterm.ttf',
'assets/AmaticSC-Regular.ttf',
'assets/terminal.ttf',
'user_profile.json'
];
}
if (soundFiles && soundFiles.length) {
for (const sf of soundFiles) {
// Sound files live under 'sound/' at project root; use path as-is
const path = sf;
if (!assetFiles.includes(path)) assetFiles.push(path);
}
}
updateStatus('Fetching project files...');
// Fetch text files; if optional files are missing, skip them instead of throwing.
const textPromises = all.map(async (p) => {
try {
return await fetchAndWrite(p);
} catch (e) {
console.warn('Skipping missing text file:', p, e);
return null;
}
});
// For binary assets use arrayBuffer and write as binary. Skip missing binaries.
const binPromises = assetFiles.map(async (path) => {
try {
const res = await fetch(path);
if (!res.ok) {
console.warn(`Skipping missing binary asset (status ${res.status}): ${path}`);
return null;
}
const buf = await res.arrayBuffer();
return { path, buffer: buf };
} catch (e) {
console.warn('Skipping binary asset fetch error:', path, e);
return null;
}
});
const texts = (await Promise.all(textPromises)).filter(x => x);
const bins = (await Promise.all(binPromises)).filter(x => x);
return { texts, bins, totalCount: texts.length + bins.length };
}
async function loadPyodideAndSetup() {
updateStatus('Loading Pyodide...');
// The included pyodide.js exposes a global loadPyodide().
// Use the same base URL as the bundled pyodide.js so versions match.
pyodide = await loadPyodide({ indexURL: 'https://ryanking13.github.io/pyodide-pygame-demo/dist/' });
updateStatus('Configuring canvas for SDL2/pygame...');
try {
// Canvas helpers provided by pygame-ce's pyodide integration
pyodide.canvas.setCanvas2D(canvas);
} catch (e) {
console.warn('Could not set canvas via pyodide.canvas:', e);
}
updateStatus('Installing required Python packages (pyyaml, requests, pygame-ce)...');
// Install runtime deps: prefer pyodide built-in packages, fall back to micropip.
try {
// Ensure requests is available
try {
await pyodide.loadPackage(['pygame-ce']);
updateStatus('Loaded built-in requests');
} catch (e) {
console.warn('Failed to load built-in requests:', e);
}
} catch (e) {
console.warn('Package installation issue:', e);
}
// Quick runtime check for yaml import
try {
await pyodide.runPythonAsync(`
try:
import yaml
print('PY: yaml OK')
except Exception as e:
print('PY: yaml ERROR', e)
`);
updateStatus('Package import check complete (see console for details)');
} catch (e) {
console.warn('Import check failed:', e);
updateStatus('Package import check failed: ' + e.message);
}
updateStatus('Loading project files into filesystem...');
const { texts, bins, totalCount } = await loadProjectFiles();
// Setup asset progress UI
const assetProgress = document.getElementById('assetProgress');
const assetProgressText = document.getElementById('assetProgressText');
let writtenCount = 0;
assetProgress.max = totalCount || 1;
function incrementProgress() {
writtenCount += 1;
assetProgress.value = writtenCount;
assetProgressText.textContent = `${writtenCount} / ${totalCount}`;
document.getElementById('hud-status').textContent = `Loading assets ${writtenCount}/${totalCount}`;
}
// Write text files into pyodide filesystem
for (const f of texts) {
const dir = f.path.substring(0, f.path.lastIndexOf('/'));
if (dir) {
try { pyodide.FS.mkdirTree(dir); } catch (e) { /* ignore */ }
}
// If this is a YAML config file, also produce a JSON counterpart so Python can read JSON without PyYAML.
if (f.path.endsWith('.yaml') || f.path.endsWith('.yml')) {
try {
const obj = jsyaml.load(f.text);
const jsonPath = f.path.replace(/\.ya?ml$/i, '.json');
pyodide.FS.writeFile(jsonPath, JSON.stringify(obj, null, 2));
console.log('Wrote JSON config:', jsonPath);
} catch (e) {
console.warn('Failed to parse YAML in browser for', f.path, e);
}
}
pyodide.FS.writeFile(f.path, f.text);
incrementProgress();
}
// Write binary assets
for (const b of bins) {
const dir = b.path.substring(0, b.path.lastIndexOf('/'));
if (dir) {
try { pyodide.FS.mkdirTree(dir); } catch (e) { /* ignore */ }
}
const arr = new Uint8Array(b.buffer);
pyodide.FS.writeFile(b.path, arr);
console.log('Wrote binary asset:', b.path);
incrementProgress();
}
updateStatus('Ready. You can Start the game.');
document.getElementById('hud-status').textContent = 'Ready';
startBtn.disabled = false;
stopBtn.disabled = true;
// Populate profile dropdown from project file if available
try {
const pres = await fetch('user_profiles.json');
if (pres.ok) {
const pjson = await pres.json();
populateProfiles(pjson);
}
} catch (e) {
console.warn('Could not fetch profiles for dropdown', e);
}
// Load current profile from localStorage into Pyodide FS
console.log('[DEBUG] About to call loadProfileToFS()');
loadProfileToFS();
console.log('[DEBUG] loadProfileToFS() completed');
// Verify file was written
try {
if (pyodide && pyodide.FS) {
const content = pyodide.FS.readFile('user_profiles.json', { encoding: 'utf8' });
console.log('[DEBUG] Verification: user_profiles.json exists in Pyodide FS, content:', content);
}
} catch (e) {
console.warn('[DEBUG] Verification failed: Could not read user_profiles.json from Pyodide FS', e);
}
}
async function startGame() {
if (!pyodide) return;
startBtn.disabled = true;
stopBtn.disabled = false;
updateStatus('Starting game (this may take a few seconds)...');
try {
// Initialize the Python solver object but do NOT run its blocking mainloop.
await pyodide.runPythonAsync(`
import sys
sys.argv = ['rats.py']
from importlib import reload
import rats
reload(rats)
solver = rats.MiceMaze('maze.json')
`);
updateStatus('Game initialized. Running...');
// Start a JS-driven animation loop that calls solver.tick() each frame.
animationRunning = true;
const frame = async () => {
if (!animationRunning) return;
try {
await pyodide.runPythonAsync(`solver.tick()`);
} catch (e) {
console.error('Error during solver.tick():', e);
// Stop on errors
animationRunning = false;
return;
}
animationHandle = requestAnimationFrame(frame);
};
animationHandle = requestAnimationFrame(frame);
updateStatus('Game started. Use Stop to terminate.');
// Start periodic syncing of profile updates from Pyodide FS
startProfileSync();
} catch (e) {
console.error('Error starting game:', e);
updateStatus('Error: ' + e.message);
startBtn.disabled = false;
stopBtn.disabled = true;
}
}
async function stopGame() {
if (!pyodide) return;
updateStatus('Stopping game...');
animationRunning = false;
if (animationHandle) {
cancelAnimationFrame(animationHandle);
animationHandle = null;
}
// Attempt to request a clean stop in Python
try {
await pyodide.runPythonAsync(`
try:
if 'solver' in globals():
try:
solver.render_engine.running = False
except Exception:
pass
except Exception as e:
print('stop error', e)
`);
} catch (e) {
console.warn('Stop call failed:', e);
}
startBtn.disabled = false;
stopBtn.disabled = true;
updateStatus('Stopped. You can Start again.');
// Stop profile syncing and do final sync
stopProfileSync();
}
startBtn.addEventListener('click', startGame);
stopBtn.addEventListener('click', stopGame);
document.getElementById('runDemoBtn').addEventListener('click', async () => {
if (!pyodide) return;
updateStatus('Writing demo script to FS and starting demo...');
const demoCode = `"""
Pygame WebAssembly Demo
"""
import asyncio, random, math, pygame
WINDOW_WIDTH = 640
WINDOW_HEIGHT = 480
class Ball:
def __init__(self):
self.x = random.uniform(20, WINDOW_WIDTH-20)
self.y = random.uniform(20, WINDOW_HEIGHT-20)
self.vx = random.uniform(-3, 3)
self.vy = random.uniform(-3, 3)
self.radius = random.randint(8, 20)
self.color = (random.randint(50,255), random.randint(50,255), random.randint(50,255))
def update(self):
self.x += self.vx
self.y += self.vy
if self.x < self.radius or self.x > WINDOW_WIDTH - self.radius:
self.vx = -self.vx
if self.y < self.radius or self.y > WINDOW_HEIGHT - self.radius:
self.vy = -self.vy
def draw(self, surf):
pygame.draw.circle(surf, self.color, (int(self.x), int(self.y)), self.radius)
balls = [Ball() for _ in range(8)]
def init():
pygame.init()
pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption('Demo')
async def main():
init()
clock = pygame.time.Clock()
frame = 0
try:
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
surf = pygame.display.get_surface()
surf.fill((10,10,30))
for b in balls:
b.update()
b.draw(surf)
pygame.display.flip()
frame += 1
await asyncio.sleep(1/60)
finally:
pygame.quit()
asyncio.create_task(main())
`;
try {
pyodide.FS.writeFile('/demo_pygame.py', demoCode);
await pyodide.runPythonAsync(`
import importlib
import demo_pygame
`);
updateStatus('Demo started (check canvas).');
} catch (e) {
console.error('Demo failed:', e);
updateStatus('Demo failed: ' + e.message);
}
});
// UI helpers: profiles, fullscreen, mute/volume
function populateProfiles(profiles) {
const sel = document.getElementById('profileSelect');
// profiles expected as {profiles: [{id,name,...}, ...]} or an array
let list = profiles;
if (profiles && profiles.profiles) list = profiles.profiles;
if (!Array.isArray(list)) return;
for (const p of list) {
const opt = document.createElement('option');
opt.value = p.id || p.name || JSON.stringify(p);
opt.textContent = p.name || p.id || opt.value;
sel.appendChild(opt);
}
}
document.getElementById('fullscreenBtn').addEventListener('click', () => {
if (!document.fullscreenElement) {
document.querySelector('.canvas-container').requestFullscreen().catch(err => console.warn('FS error', err));
} else {
document.exitFullscreen();
}
});
let muted = false;
const muteBtn = document.getElementById('muteBtn');
muteBtn.addEventListener('click', () => {
muted = !muted;
muteBtn.textContent = muted ? 'Unmute' : 'Mute';
// attempt to mute audio via Pyodide FS or global audio objects later; for now just set flag
console.log('Mute toggled:', muted);
});
document.getElementById('volumeRange').addEventListener('input', (e) => {
const v = Number(e.target.value) / 100;
console.log('Volume set to', v);
// If Python exposes an audio mixer, we could call it via pyodide.runPythonAsync
});
document.getElementById('loadProfileBtn').addEventListener('click', () => {
const sel = document.getElementById('profileSelect');
const profileName = sel.value;
if (!profileName) return alert('Select a profile first');
// Store as current profile
localStorage.setItem('currentProfile', profileName);
updateStatus('Profile selected: ' + profileName);
// Write to Pyodide FS immediately if available
if (pyodide) {
loadProfileToFS();
}
// Refresh dropdown to show (current) marker
populateProfileDropdown();
});
// Profile management: localStorage + modal
function initProfileModal() {
// Initialize jQuery UI modal dialog
$('#profileModal').dialog({
autoOpen: false,
modal: true,
buttons: {
'Create Profile': function() {
saveProfile();
$(this).dialog('close');
},
'Cancel': function() {
$(this).dialog('close');
}
},
dialogClass: 'profile-dialog'
});
// Add CSS for dark theme dialog
$('<style>')
.text(`
.ui-dialog { background-color: #2d2d2d !important; }
.ui-dialog-titlebar { background-color: #1e1e1e !important; }
.ui-dialog-title { color: #ff9f43 !important; }
.ui-button { background-color: #ff9f43 !important; color: #1e1e1e !important; }
.ui-button:hover { background-color: #ffb366 !important; }
`)
.appendTo('head');
}
function saveProfile() {
const name = $('#profileName').val();
const difficulty = $('#difficulty').val();
const soundEnabled = $('#soundEnabled').is(':checked');
if (!name || name.trim() === '') {
alert('Please enter a player name');
return;
}
// Create profile object matching UserProfileIntegration format
const profile = {
name: name,
difficulty: difficulty,
settings: {
difficulty: difficulty,
sound_volume: 100,
sound_enabled: soundEnabled
},
games_played: 0,
best_score: 0,
total_score: 0,
achievements: [],
last_played: null
};
// Get existing profiles from localStorage (as array)
let profilesArray = [];
try {
const stored = localStorage.getItem('miceProfiles');
if (stored) profilesArray = JSON.parse(stored);
} catch (e) {
console.warn('Could not read stored profiles', e);
}
// Add new profile
profilesArray.push(profile);
localStorage.setItem('miceProfiles', JSON.stringify(profilesArray));
localStorage.setItem('currentProfile', name);
// Reset form
$('#profileForm')[0].reset();
updateStatus(`Profile "${name}" created!`);
console.log('Profile saved:', profile);
// Refresh profile dropdown
populateProfileDropdown();
}
function populateProfileDropdown() {
const sel = document.getElementById('profileSelect');
sel.innerHTML = '<option value="">(None)</option>';
let profilesArray = [];
try {
const stored = localStorage.getItem('miceProfiles');
if (stored) profilesArray = JSON.parse(stored);
} catch (e) {
console.warn('Could not read stored profiles', e);
}
const currentProfileName = localStorage.getItem('currentProfile');
profilesArray.forEach(p => {
const opt = document.createElement('option');
opt.value = p.name; // Use name as the value
opt.textContent = p.name + (p.name === currentProfileName ? ' (current)' : '');
sel.appendChild(opt);
});
// Auto-select current profile
if (currentProfileName) sel.value = currentProfileName;
}
document.getElementById('newProfileBtn').addEventListener('click', () => {
$('#profileModal').dialog('open');
});
// Expose a function that Python can call to sync profile updates back to localStorage
window.syncProfileUpdateToLocalStorage = function(profileName, bestScore, gamesPlayed, totalScore) {
console.log(`[DEBUG SYNC] Python calling sync: profile=${profileName}, best=${bestScore}, games=${gamesPlayed}, total=${totalScore}`);
try {
// Get existing profiles from localStorage
let profilesArray = [];
const stored = localStorage.getItem('miceProfiles');
if (stored) {
profilesArray = JSON.parse(stored);
}
// Find and update the profile
const profileIndex = profilesArray.findIndex(p => p.name === profileName);
if (profileIndex >= 0) {
profilesArray[profileIndex].best_score = bestScore;
profilesArray[profileIndex].games_played = gamesPlayed;
profilesArray[profileIndex].total_score = totalScore;
console.log(`[DEBUG SYNC] Updated profile in array at index ${profileIndex}`);
}
// Save back to localStorage
localStorage.setItem('miceProfiles', JSON.stringify(profilesArray));
console.log('[DEBUG SYNC] Successfully saved profile updates to localStorage');
return true;
} catch (e) {
console.error('[DEBUG SYNC] Failed to sync profile update:', e);
return false;
}
};
// Sync profile changes from Pyodide FS back to localStorage (fallback method)
function syncProfileFromPyodideFS() {
if (!pyodide) return false;
try {
console.log('[DEBUG SYNC] Attempting to sync profile from Pyodide FS back to localStorage');
// Check if file exists first
try {
pyodide.FS.stat('user_profiles.json');
} catch (e) {
console.log('[DEBUG SYNC] user_profiles.json not found in Pyodide FS (this is normal during game), skipping read');
return false;
}
const content = pyodide.FS.readFile('user_profiles.json', { encoding: 'utf8' });
const profileData = JSON.parse(content);
console.log('[DEBUG SYNC] Read profile data from Pyodide FS:', profileData);
// Convert from dict format back to array format for localStorage
const profilesArray = Object.values(profileData.profiles || {});
console.log('[DEBUG SYNC] Converted to array, count:', profilesArray.length);
// Save back to localStorage
localStorage.setItem('miceProfiles', JSON.stringify(profilesArray));
if (profileData.active_profile) {
localStorage.setItem('currentProfile', profileData.active_profile);
}
console.log('[DEBUG SYNC] Successfully synced profiles back to localStorage');
return true;
} catch (e) {
console.warn('[DEBUG SYNC] Failed to sync profiles from Pyodide FS:', e);
return false;
}
}
// Periodically try to sync profiles back from Pyodide FS (every 5 seconds during game)
let syncInterval = null;
function startProfileSync() {
console.log('[DEBUG] Starting periodic profile sync');
if (syncInterval) clearInterval(syncInterval);
syncInterval = setInterval(() => {
syncProfileFromPyodideFS();
}, 5000);
}
function stopProfileSync() {
console.log('[DEBUG] Stopping periodic profile sync');
if (syncInterval) {
clearInterval(syncInterval);
syncInterval = null;
}
// Do a final sync attempt on stop
syncProfileFromPyodideFS();
}
function loadProfileToFS() {
if (!pyodide) return;
console.log('[DEBUG] loadProfileToFS called');
let profilesArray = [];
try {
const stored = localStorage.getItem('miceProfiles');
console.log('[DEBUG] Stored profiles from localStorage:', stored);
if (stored) profilesArray = JSON.parse(stored);
} catch (e) {
console.warn('Could not read stored profiles', e);
}
// Get current profile name
const currentProfileName = localStorage.getItem('currentProfile');
console.log('[DEBUG] Current profile name from localStorage:', currentProfileName);
console.log('[DEBUG] Total profiles in array:', profilesArray.length);
// Convert array of profiles to dict format expected by UserProfileIntegration
// Format: { profiles: { "name": {...}, ... }, active_profile: "name" }
const profilesDict = {};
profilesArray.forEach(p => {
profilesDict[p.name] = p;
console.log('[DEBUG] Added profile to dict:', p.name);
});
const profileData = {
profiles: profilesDict,
active_profile: currentProfileName || null
};
console.log('[DEBUG] Final profile data to write to FS:', JSON.stringify(profileData, null, 2));
try {
pyodide.FS.writeFile('user_profiles.json', JSON.stringify(profileData, null, 2));
console.log('[DEBUG] Successfully wrote user_profiles.json to Pyodide FS');
console.log('[DEBUG] Wrote profiles to Pyodide FS:', profileData);
updateStatus('Profile loaded: ' + (currentProfileName ? currentProfileName : 'None'));
} catch (e) {
console.warn('[DEBUG] Failed to write profile to FS', e);
}
}
// Initialize on page load
window.addEventListener('load', () => {
$(document).ready(() => {
initProfileModal();
populateProfileDropdown();
});
loadPyodideAndSetup().catch(err => {
console.error('Failed to initialize Pyodide:', err);
updateStatus('Failed to load Pyodide: ' + err.message);
});
});
</script>
</body>
</html>

57
tools/create_favicon.py

@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Create a favicon.ico from an existing PNG asset.
Usage: python tools/create_favicon.py
Produces: ./favicon.ico
The script will try to import Pillow. If it's not available, it prints instructions.
"""
import os
import sys
ROOT = os.path.dirname(os.path.dirname(__file__))
SRC = os.path.join(ROOT, 'assets', 'BMP_WEWIN.png')
# fallback to assets/Rat/BMP_WEWIN.png if present
if not os.path.exists(SRC):
alt = os.path.join(ROOT, 'assets', 'Rat', 'BMP_WEWIN.png')
if os.path.exists(alt):
SRC = alt
OUT = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'favicon.ico')
def ensure_pillow():
try:
from PIL import Image
return Image
except Exception:
print('Pillow (PIL) is required to run this script. Install with:')
print(' pip install Pillow')
return None
def create_favicon():
Image = ensure_pillow()
if Image is None:
return 2
if not os.path.exists(SRC):
print(f'Source PNG not found: {SRC}')
return 1
try:
img = Image.open(SRC)
# Create multiple sizes for favicon
sizes = [(16, 16), (32, 32), (48, 48), (64, 64)]
icons = [img.resize(s, Image.Resampling.LANCZOS) if hasattr(Image, 'Resampling') else img.resize(s) for s in sizes]
# Save as ICO
icons[0].save(OUT, format='ICO', sizes=sizes)
print(f'Created favicon: {OUT}')
return 0
except Exception as e:
print('Failed to create favicon:', e)
return 3
if __name__ == '__main__':
sys.exit(create_favicon())
Loading…
Cancel
Save