🎮 Features implemented: - Pokemon-style pet collection and battles - Multi-location exploration system - Dynamic weather with background updates - Achievement system with location unlocks - Web dashboard for player stats - Modular command system - Async database with SQLite - PM flood prevention - Persistent player data 🌤️ Weather System: - 6 weather types with spawn modifiers - 30min-3hour dynamic durations - Background task for automatic updates - Location-specific weather patterns 🐛 Recent Bug Fixes: - Database persistence on restart - Player page SQLite row conversion - Achievement count calculations - Travel requirement messages - Battle move color coding - Locations page display 🔧 Generated with Claude Code 🤖 Co-Authored-By: Claude <noreply@anthropic.com>
1425 lines
No EOL
47 KiB
Python
1425 lines
No EOL
47 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PetBot Web Server
|
|
Provides web interface for bot data including help, player stats, and pet collections
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import asyncio
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
from threading import Thread
|
|
import time
|
|
|
|
# Add the project directory to the path
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
from src.database import Database
|
|
|
|
class PetBotRequestHandler(BaseHTTPRequestHandler):
|
|
"""HTTP request handler for PetBot web server"""
|
|
|
|
def do_GET(self):
|
|
"""Handle GET requests"""
|
|
parsed_path = urlparse(self.path)
|
|
path = parsed_path.path
|
|
|
|
# Route handling
|
|
if path == '/':
|
|
self.serve_index()
|
|
elif path == '/help':
|
|
self.serve_help()
|
|
elif path == '/players':
|
|
self.serve_players()
|
|
elif path.startswith('/player/'):
|
|
nickname = path[8:] # Remove '/player/' prefix
|
|
self.serve_player_profile(nickname)
|
|
elif path == '/leaderboard':
|
|
self.serve_leaderboard()
|
|
elif path == '/locations':
|
|
self.serve_locations()
|
|
else:
|
|
self.send_error(404, "Page not found")
|
|
|
|
def serve_index(self):
|
|
"""Serve the main index page"""
|
|
html = """<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot Game Hub</title>
|
|
<style>
|
|
:root {
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 3em;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.nav-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.nav-card {
|
|
background: var(--bg-secondary);
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
color: var(--text-primary);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.nav-card:hover {
|
|
transform: translateY(-5px);
|
|
color: var(--text-accent);
|
|
}
|
|
|
|
.nav-card h3 {
|
|
margin: 0 0 15px 0;
|
|
color: var(--text-accent);
|
|
font-size: 1.5em;
|
|
}
|
|
|
|
.status {
|
|
background: var(--bg-secondary);
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
text-align: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>🐾 PetBot Game Hub</h1>
|
|
<p>Welcome to the PetBot web interface!</p>
|
|
<p><em>Connect to irc.libera.chat #petz to play</em></p>
|
|
</div>
|
|
|
|
<div class="nav-grid">
|
|
<a href="/help" class="nav-card">
|
|
<h3>📚 Command Help</h3>
|
|
<p>Complete reference for all bot commands, battle mechanics, and game features</p>
|
|
</a>
|
|
|
|
<a href="/players" class="nav-card">
|
|
<h3>👥 Player List</h3>
|
|
<p>View all registered players and their basic stats</p>
|
|
</a>
|
|
|
|
<a href="/leaderboard" class="nav-card">
|
|
<h3>🏆 Leaderboard</h3>
|
|
<p>Top players by level, pets caught, and achievements earned</p>
|
|
</a>
|
|
|
|
<a href="/locations" class="nav-card">
|
|
<h3>🗺️ Locations</h3>
|
|
<p>Explore all game locations and see what pets can be found where</p>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="status">
|
|
<p><strong>🤖 Bot Status:</strong> Online and ready for commands!</p>
|
|
<p>Use <code>!help</code> in #petz for quick command reference</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_help(self):
|
|
"""Serve the help page"""
|
|
try:
|
|
with open('help.html', 'r') as f:
|
|
content = f.read()
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(content.encode())
|
|
except FileNotFoundError:
|
|
self.send_error(404, "Help file not found")
|
|
|
|
def serve_players(self):
|
|
"""Serve the players page with real data"""
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_error_page("Players", "Database not available")
|
|
return
|
|
|
|
# Fetch players data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
players_data = loop.run_until_complete(self.fetch_players_data(database))
|
|
loop.close()
|
|
|
|
self.serve_players_data(players_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching players data: {e}")
|
|
self.serve_error_page("Players", f"Error loading players: {str(e)}")
|
|
|
|
async def fetch_players_data(self, database):
|
|
"""Fetch all players data from database"""
|
|
try:
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get all players with basic stats
|
|
cursor = await db.execute("""
|
|
SELECT p.nickname, p.level, p.experience, p.money, p.created_at,
|
|
l.name as location_name,
|
|
(SELECT COUNT(*) FROM pets WHERE player_id = p.id) as pet_count,
|
|
(SELECT COUNT(*) FROM pets WHERE player_id = p.id AND is_active = 1) as active_pets,
|
|
(SELECT COUNT(*) FROM player_achievements WHERE player_id = p.id) as achievement_count
|
|
FROM players p
|
|
LEFT JOIN locations l ON p.current_location_id = l.id
|
|
ORDER BY p.level DESC, p.experience DESC
|
|
""")
|
|
|
|
rows = await cursor.fetchall()
|
|
# Convert SQLite rows to dictionaries properly
|
|
players = []
|
|
for row in rows:
|
|
player_dict = {
|
|
'nickname': row[0],
|
|
'level': row[1],
|
|
'experience': row[2],
|
|
'money': row[3],
|
|
'created_at': row[4],
|
|
'location_name': row[5],
|
|
'pet_count': row[6],
|
|
'active_pets': row[7],
|
|
'achievement_count': row[8]
|
|
}
|
|
players.append(player_dict)
|
|
return players
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching players: {e}")
|
|
return []
|
|
|
|
def serve_players_data(self, players_data):
|
|
"""Serve players page with real data"""
|
|
|
|
# Build players table HTML
|
|
if players_data:
|
|
players_html = ""
|
|
for i, player in enumerate(players_data, 1):
|
|
rank_emoji = {"1": "🥇", "2": "🥈", "3": "🥉"}.get(str(i), f"{i}.")
|
|
|
|
players_html += f"""
|
|
<tr onclick="window.location.href='/player/{player['nickname']}'" style="cursor: pointer;">
|
|
<td><strong>{rank_emoji}</strong></td>
|
|
<td><a href="/player/{player['nickname']}" style="color: var(--text-accent); text-decoration: none;"><strong>{player['nickname']}</strong></a></td>
|
|
<td>{player['level']}</td>
|
|
<td>{player['experience']}</td>
|
|
<td>${player['money']}</td>
|
|
<td>{player['pet_count']}</td>
|
|
<td>{player['active_pets']}</td>
|
|
<td>{player['achievement_count']}</td>
|
|
<td>{player.get('location_name', 'Unknown')}</td>
|
|
</tr>"""
|
|
else:
|
|
players_html = """
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; padding: 40px;">
|
|
No players found. Be the first to use !start in #petz!
|
|
</td>
|
|
</tr>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Players</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.players-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.players-table th, .players-table td {{
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.players-table th {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
font-weight: 600;
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.players-table tr:hover {{
|
|
background: var(--hover-color);
|
|
}}
|
|
|
|
.stats-summary {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-tertiary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-value {{
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.stat-label {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>👥 Registered Players</h1>
|
|
<p>All trainers on their pet collection journey</p>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">📊 Server Statistics</div>
|
|
<div class="section-content">
|
|
<div class="stats-summary">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len(players_data)}</div>
|
|
<div class="stat-label">Total Players</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['pet_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Pets Caught</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{sum(p['achievement_count'] for p in players_data)}</div>
|
|
<div class="stat-label">Total Achievements</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{max((p['level'] for p in players_data), default=0)}</div>
|
|
<div class="stat-label">Highest Level</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🏆 Player Rankings</div>
|
|
<div class="section-content">
|
|
<table class="players-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Rank</th>
|
|
<th>Player</th>
|
|
<th>Level</th>
|
|
<th>Experience</th>
|
|
<th>Money</th>
|
|
<th>Pets</th>
|
|
<th>Active</th>
|
|
<th>Achievements</th>
|
|
<th>Location</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{players_html}
|
|
</tbody>
|
|
</table>
|
|
|
|
<div style="text-align: center; margin-top: 20px; color: var(--text-secondary);">
|
|
<p>💡 Click on any player name to view their detailed profile</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_error_page(self, page_name, error_msg):
|
|
"""Serve a generic error page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>⚠️ Error Loading {page_name}</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Unable to load page</h2>
|
|
<p>{error_msg}</p>
|
|
<p>Please try again later or contact an administrator.</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_leaderboard(self):
|
|
"""Serve the leaderboard page - redirect to players for now"""
|
|
# For now, leaderboard is the same as players page since they're ranked
|
|
# In the future, this could have different categories
|
|
self.send_response(302) # Temporary redirect
|
|
self.send_header('Location', '/players')
|
|
self.end_headers()
|
|
|
|
def serve_locations(self):
|
|
"""Serve the locations page with real data"""
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_error_page("Locations", "Database not available")
|
|
return
|
|
|
|
# Fetch locations data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
locations_data = loop.run_until_complete(self.fetch_locations_data(database))
|
|
loop.close()
|
|
|
|
self.serve_locations_data(locations_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching locations data: {e}")
|
|
self.serve_error_page("Locations", f"Error loading locations: {str(e)}")
|
|
|
|
async def fetch_locations_data(self, database):
|
|
"""Fetch all locations and their spawn data from database"""
|
|
try:
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get all locations
|
|
cursor = await db.execute("""
|
|
SELECT l.*,
|
|
GROUP_CONCAT(ps.name || ' (' || ps.type1 ||
|
|
CASE WHEN ps.type2 IS NOT NULL THEN '/' || ps.type2 ELSE '' END || ')') as spawns
|
|
FROM locations l
|
|
LEFT JOIN location_spawns ls ON l.id = ls.location_id
|
|
LEFT JOIN pet_species ps ON ls.species_id = ps.id
|
|
GROUP BY l.id
|
|
ORDER BY l.id
|
|
""")
|
|
|
|
rows = await cursor.fetchall()
|
|
# Convert SQLite rows to dictionaries properly
|
|
locations = []
|
|
for row in rows:
|
|
location_dict = {
|
|
'id': row[0],
|
|
'name': row[1],
|
|
'description': row[2],
|
|
'level_min': row[3],
|
|
'level_max': row[4],
|
|
'spawns': row[5] if len(row) > 5 else None
|
|
}
|
|
locations.append(location_dict)
|
|
return locations
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching locations: {e}")
|
|
return []
|
|
|
|
def serve_locations_data(self, locations_data):
|
|
"""Serve locations page with real data"""
|
|
|
|
# Build locations HTML
|
|
locations_html = ""
|
|
if locations_data:
|
|
for location in locations_data:
|
|
spawns = location.get('spawns', 'No pets found')
|
|
if not spawns or spawns == 'None':
|
|
spawns = "No pets spawn here yet"
|
|
|
|
# Split spawns into a readable list
|
|
spawn_list = spawns.split(',') if spawns != "No pets spawn here yet" else []
|
|
spawn_badges = ""
|
|
for spawn in spawn_list[:6]: # Limit to first 6 for display
|
|
spawn_badges += f'<span class="spawn-badge">{spawn.strip()}</span>'
|
|
if len(spawn_list) > 6:
|
|
spawn_badges += f'<span class="spawn-badge">+{len(spawn_list) - 6} more</span>'
|
|
|
|
if not spawn_badges:
|
|
spawn_badges = '<em style="color: var(--text-secondary);">No pets spawn here yet</em>'
|
|
|
|
locations_html += f"""
|
|
<div class="location-card">
|
|
<div class="location-header">
|
|
<h3>🗺️ {location['name']}</h3>
|
|
<div class="location-id">ID: {location['id']}</div>
|
|
</div>
|
|
<div class="location-description">
|
|
{location['description']}
|
|
</div>
|
|
<div class="location-levels">
|
|
<strong>Level Range:</strong> {location['level_min']}-{location['level_max']}
|
|
</div>
|
|
<div class="location-spawns">
|
|
<strong>Wild Pets:</strong><br>
|
|
{spawn_badges}
|
|
</div>
|
|
</div>"""
|
|
else:
|
|
locations_html = """
|
|
<div class="location-card">
|
|
<div class="location-header">
|
|
<h3>No Locations Found</h3>
|
|
</div>
|
|
<div class="location-description">
|
|
No game locations are configured yet.
|
|
</div>
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Locations</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.locations-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
|
gap: 25px;
|
|
margin-top: 30px;
|
|
}}
|
|
|
|
.location-card {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
transition: transform 0.3s ease;
|
|
}}
|
|
|
|
.location-card:hover {{
|
|
transform: translateY(-5px);
|
|
}}
|
|
|
|
.location-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}}
|
|
|
|
.location-header h3 {{
|
|
margin: 0;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.location-id {{
|
|
background: rgba(255,255,255,0.2);
|
|
padding: 5px 10px;
|
|
border-radius: 20px;
|
|
font-size: 0.8em;
|
|
}}
|
|
|
|
.location-description {{
|
|
padding: 20px;
|
|
color: var(--text-primary);
|
|
font-style: italic;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.location-spawns {{
|
|
padding: 20px;
|
|
}}
|
|
|
|
.spawn-badge {{
|
|
background: var(--bg-tertiary);
|
|
color: var(--text-accent);
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 0.85em;
|
|
margin: 3px;
|
|
display: inline-block;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.info-section {{
|
|
background: var(--bg-secondary);
|
|
border-radius: 15px;
|
|
padding: 25px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.info-section h2 {{
|
|
color: var(--text-accent);
|
|
margin-top: 0;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🗺️ Game Locations</h1>
|
|
<p>Explore all areas and discover what pets await you!</p>
|
|
</div>
|
|
|
|
<div class="info-section">
|
|
<h2>🎯 How Locations Work</h2>
|
|
<p><strong>Travel:</strong> Use <code>!travel <location></code> to move between areas</p>
|
|
<p><strong>Explore:</strong> Use <code>!explore</code> to find wild pets in your current location</p>
|
|
<p><strong>Unlock:</strong> Some locations require achievements - catch specific pet types to unlock new areas!</p>
|
|
<p><strong>Weather:</strong> Check <code>!weather</code> for conditions that boost certain pet spawn rates</p>
|
|
</div>
|
|
|
|
<div class="locations-grid">
|
|
{locations_html}
|
|
</div>
|
|
|
|
<div class="info-section">
|
|
<p style="text-align: center; color: var(--text-secondary); margin: 0;">
|
|
💡 Use <code>!wild <location></code> in #petz to see what pets spawn in a specific area
|
|
</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_profile(self, nickname):
|
|
"""Serve individual player profile page"""
|
|
# URL decode the nickname in case it has special characters
|
|
from urllib.parse import unquote
|
|
nickname = unquote(nickname)
|
|
|
|
# Get database instance from the server class
|
|
database = self.server.database if hasattr(self.server, 'database') else None
|
|
|
|
if not database:
|
|
self.serve_player_error(nickname, "Database not available")
|
|
return
|
|
|
|
# Fetch player data
|
|
try:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
|
|
player_data = loop.run_until_complete(self.fetch_player_data(database, nickname))
|
|
loop.close()
|
|
|
|
if player_data is None:
|
|
self.serve_player_not_found(nickname)
|
|
return
|
|
|
|
self.serve_player_data(nickname, player_data)
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching player data for {nickname}: {e}")
|
|
self.serve_player_error(nickname, f"Error loading player data: {str(e)}")
|
|
return
|
|
|
|
async def fetch_player_data(self, database, nickname):
|
|
"""Fetch all player data from database"""
|
|
try:
|
|
# Get player info
|
|
import aiosqlite
|
|
async with aiosqlite.connect(database.db_path) as db:
|
|
# Get player basic info
|
|
cursor = await db.execute("""
|
|
SELECT p.*, l.name as location_name, l.description as location_desc
|
|
FROM players p
|
|
LEFT JOIN locations l ON p.current_location_id = l.id
|
|
WHERE p.nickname = ?
|
|
""", (nickname,))
|
|
player = await cursor.fetchone()
|
|
if not player:
|
|
return None
|
|
|
|
# Convert to dict manually
|
|
player_dict = {
|
|
'id': player[0],
|
|
'nickname': player[1],
|
|
'created_at': player[2],
|
|
'last_active': player[3],
|
|
'level': player[4],
|
|
'experience': player[5],
|
|
'money': player[6],
|
|
'current_location_id': player[7],
|
|
'location_name': player[8],
|
|
'location_desc': player[9]
|
|
}
|
|
|
|
# Get player pets
|
|
cursor = await db.execute("""
|
|
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
|
|
FROM pets p
|
|
JOIN pet_species ps ON p.species_id = ps.id
|
|
WHERE p.player_id = ?
|
|
ORDER BY p.is_active DESC, p.level DESC, p.id ASC
|
|
""", (player_dict['id'],))
|
|
pets_rows = await cursor.fetchall()
|
|
pets = []
|
|
for row in pets_rows:
|
|
pet_dict = {
|
|
'id': row[0], 'player_id': row[1], 'species_id': row[2],
|
|
'nickname': row[3], 'level': row[4], 'experience': row[5],
|
|
'hp': row[6], 'max_hp': row[7], 'attack': row[8],
|
|
'defense': row[9], 'speed': row[10], 'happiness': row[11],
|
|
'caught_at': row[12], 'is_active': row[13],
|
|
'species_name': row[14], 'type1': row[15], 'type2': row[16]
|
|
}
|
|
pets.append(pet_dict)
|
|
|
|
# Get player achievements
|
|
cursor = await db.execute("""
|
|
SELECT pa.*, a.name as achievement_name, a.description as achievement_desc
|
|
FROM player_achievements pa
|
|
JOIN achievements a ON pa.achievement_id = a.id
|
|
WHERE pa.player_id = ?
|
|
ORDER BY pa.completed_at DESC
|
|
""", (player_dict['id'],))
|
|
achievements_rows = await cursor.fetchall()
|
|
achievements = []
|
|
for row in achievements_rows:
|
|
achievement_dict = {
|
|
'id': row[0], 'player_id': row[1], 'achievement_id': row[2],
|
|
'completed_at': row[3], 'achievement_name': row[4], 'achievement_desc': row[5]
|
|
}
|
|
achievements.append(achievement_dict)
|
|
|
|
return {
|
|
'player': player_dict,
|
|
'pets': pets,
|
|
'achievements': achievements
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"Database error fetching player {nickname}: {e}")
|
|
return None
|
|
|
|
def serve_player_not_found(self, nickname):
|
|
"""Serve player not found page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Player Not Found</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🚫 Player Not Found</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Player "{nickname}" not found</h2>
|
|
<p>This player hasn't started their journey yet or doesn't exist.</p>
|
|
<p>Players can use <code>!start</code> in #petz to begin their adventure!</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(404)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_error(self, nickname, error_msg):
|
|
"""Serve player error page"""
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - Error</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--text-primary: #cccccc;
|
|
--text-accent: #66ff66;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
text-align: center;
|
|
}}
|
|
|
|
.header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
}}
|
|
|
|
.error-message {{
|
|
background: var(--bg-secondary);
|
|
padding: 40px;
|
|
border-radius: 15px;
|
|
border: 2px solid #ff4444;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>⚠️ Error</h1>
|
|
</div>
|
|
|
|
<div class="error-message">
|
|
<h2>Unable to load player data</h2>
|
|
<p>{error_msg}</p>
|
|
<p>Please try again later or contact an administrator.</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(500)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def serve_player_data(self, nickname, player_data):
|
|
"""Serve player profile page with real data"""
|
|
player = player_data['player']
|
|
pets = player_data['pets']
|
|
achievements = player_data['achievements']
|
|
|
|
# Calculate stats
|
|
active_pets = [pet for pet in pets if pet['is_active']]
|
|
total_pets = len(pets)
|
|
active_count = len(active_pets)
|
|
|
|
# Build pets table HTML
|
|
pets_html = ""
|
|
if pets:
|
|
for pet in pets:
|
|
status = "⭐ Active" if pet['is_active'] else "📦 Storage"
|
|
status_class = "pet-active" if pet['is_active'] else "pet-stored"
|
|
name = pet['nickname'] or pet['species_name']
|
|
|
|
type_str = pet['type1']
|
|
if pet['type2']:
|
|
type_str += f"/{pet['type2']}"
|
|
|
|
pets_html += f"""
|
|
<tr>
|
|
<td class="{status_class}">{status}</td>
|
|
<td><strong>{name}</strong></td>
|
|
<td>{pet['species_name']}</td>
|
|
<td><span class="type-badge">{type_str}</span></td>
|
|
<td>{pet['level']}</td>
|
|
<td>{pet['hp']}/{pet['max_hp']}</td>
|
|
<td>ATK: {pet['attack']} | DEF: {pet['defense']} | SPD: {pet['speed']}</td>
|
|
</tr>"""
|
|
else:
|
|
pets_html = """
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; padding: 40px;">
|
|
No pets found. Use !explore and !catch to start your collection!
|
|
</td>
|
|
</tr>"""
|
|
|
|
# Build achievements HTML
|
|
achievements_html = ""
|
|
if achievements:
|
|
for achievement in achievements:
|
|
achievements_html += f"""
|
|
<div style="background: var(--bg-tertiary); padding: 15px; border-radius: 8px; margin: 10px 0; border-left: 4px solid var(--text-accent);">
|
|
<strong>🏆 {achievement['achievement_name']}</strong><br>
|
|
<small>{achievement['achievement_desc']}</small><br>
|
|
<em style="color: var(--text-secondary);">Earned: {achievement['completed_at']}</em>
|
|
</div>"""
|
|
else:
|
|
achievements_html = """
|
|
<div style="text-align: center; padding: 40px; color: var(--text-secondary);">
|
|
No achievements yet. Keep exploring and catching pets to earn achievements!
|
|
</div>"""
|
|
|
|
html = f"""<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PetBot - {nickname}'s Profile</title>
|
|
<style>
|
|
:root {{
|
|
--bg-primary: #0f0f23;
|
|
--bg-secondary: #1e1e3f;
|
|
--bg-tertiary: #2a2a4a;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #999999;
|
|
--text-accent: #66ff66;
|
|
--border-color: #333366;
|
|
--hover-color: #3a3a5a;
|
|
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
|
|
}}
|
|
|
|
body {{
|
|
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
min-height: 100vh;
|
|
}}
|
|
|
|
.header {{
|
|
text-align: center;
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 30px;
|
|
border-radius: 15px;
|
|
margin-bottom: 30px;
|
|
box-shadow: var(--shadow-dark);
|
|
}}
|
|
|
|
.header h1 {{
|
|
margin: 0;
|
|
font-size: 2.5em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.back-link {{
|
|
color: var(--text-accent);
|
|
text-decoration: none;
|
|
margin-bottom: 20px;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}}
|
|
|
|
.back-link:hover {{
|
|
text-decoration: underline;
|
|
}}
|
|
|
|
.section {{
|
|
background: var(--bg-secondary);
|
|
margin-bottom: 30px;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow-dark);
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.section-header {{
|
|
background: var(--gradient-primary);
|
|
color: white;
|
|
padding: 20px 25px;
|
|
font-size: 1.3em;
|
|
font-weight: 700;
|
|
}}
|
|
|
|
.section-content {{
|
|
padding: 25px;
|
|
}}
|
|
|
|
.stats-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}}
|
|
|
|
.stat-card {{
|
|
background: var(--bg-tertiary);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
border: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.stat-value {{
|
|
font-size: 2em;
|
|
font-weight: bold;
|
|
color: var(--text-accent);
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.stat-label {{
|
|
color: var(--text-secondary);
|
|
font-size: 0.9em;
|
|
}}
|
|
|
|
.pets-table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 20px 0;
|
|
background: var(--bg-tertiary);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.pets-table th, .pets-table td {{
|
|
padding: 12px 15px;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}}
|
|
|
|
.pets-table th {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
font-weight: 600;
|
|
}}
|
|
|
|
.pets-table tr:hover {{
|
|
background: var(--hover-color);
|
|
}}
|
|
|
|
.pet-active {{
|
|
color: var(--text-accent);
|
|
font-weight: bold;
|
|
}}
|
|
|
|
.pet-stored {{
|
|
color: var(--text-secondary);
|
|
}}
|
|
|
|
.type-badge {{
|
|
background: var(--bg-primary);
|
|
color: var(--text-accent);
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.8em;
|
|
margin-right: 5px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="/" class="back-link">← Back to Game Hub</a>
|
|
|
|
<div class="header">
|
|
<h1>🐾 {nickname}'s Profile</h1>
|
|
<p>Level {player['level']} Trainer</p>
|
|
<p><em>Currently in {player.get('location_name', 'Unknown Location')}</em></p>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">📊 Player Statistics</div>
|
|
<div class="section-content">
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value">{player['level']}</div>
|
|
<div class="stat-label">Level</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{player['experience']}</div>
|
|
<div class="stat-label">Experience</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">${player['money']}</div>
|
|
<div class="stat-label">Money</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{total_pets}</div>
|
|
<div class="stat-label">Pets Caught</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{active_count}</div>
|
|
<div class="stat-label">Active Pets</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{len(achievements)}</div>
|
|
<div class="stat-label">Achievements</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🐾 Pet Collection</div>
|
|
<div class="section-content">
|
|
<table class="pets-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Status</th>
|
|
<th>Name</th>
|
|
<th>Species</th>
|
|
<th>Type</th>
|
|
<th>Level</th>
|
|
<th>HP</th>
|
|
<th>Stats</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{pets_html}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">🏆 Achievements</div>
|
|
<div class="section-content">
|
|
{achievements_html}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
|
|
def log_message(self, format, *args):
|
|
"""Override to reduce logging noise"""
|
|
pass
|
|
|
|
class PetBotWebServer:
|
|
def __init__(self, database, port=8080):
|
|
self.database = database
|
|
self.port = port
|
|
self.server = None
|
|
|
|
def run(self):
|
|
"""Start the HTTP web server"""
|
|
print(f"🌐 Starting PetBot web server on http://0.0.0.0:{self.port}")
|
|
print(f"📡 Accessible from WSL at: http://172.27.217.61:{self.port}")
|
|
print(f"📡 Accessible from Windows at: http://localhost:{self.port}")
|
|
self.server = HTTPServer(('0.0.0.0', self.port), PetBotRequestHandler)
|
|
# Pass database to the server so handlers can access it
|
|
self.server.database = self.database
|
|
self.server.serve_forever()
|
|
|
|
def start_in_thread(self):
|
|
"""Start the web server in a background thread"""
|
|
thread = Thread(target=self.run, daemon=True)
|
|
thread.start()
|
|
print(f"✅ Web server started at http://localhost:{self.port}")
|
|
return thread
|
|
|
|
def run_standalone():
|
|
"""Run the web server standalone for testing"""
|
|
print("🐾 PetBot Web Server (Standalone Mode)")
|
|
print("=" * 40)
|
|
|
|
# Initialize database
|
|
database = Database()
|
|
# Note: In standalone mode, we can't easily run async init
|
|
# This is mainly for testing the web routes
|
|
|
|
# Start web server
|
|
server = PetBotWebServer(database)
|
|
print("🚀 Starting web server...")
|
|
print("📝 Available routes:")
|
|
print(" http://localhost:8080/ - Game Hub")
|
|
print(" http://localhost:8080/help - Command Help")
|
|
print(" http://localhost:8080/players - Player List")
|
|
print(" http://localhost:8080/leaderboard - Leaderboard")
|
|
print(" http://localhost:8080/locations - Locations")
|
|
print("")
|
|
print("Press Ctrl+C to stop")
|
|
|
|
try:
|
|
server.run()
|
|
except KeyboardInterrupt:
|
|
print("\n✅ Web server stopped")
|
|
|
|
if __name__ == "__main__":
|
|
run_standalone() |