diff --git a/server/database.py b/server/database.py index dc60ddd..438c30d 100644 --- a/server/database.py +++ b/server/database.py @@ -2,6 +2,8 @@ import sqlite3 import json from pathlib import Path from typing import Optional, Dict, List +from server.logger import logger +import time class Database: """Handles all database operations""" @@ -18,8 +20,8 @@ class Database: def init_db(self): """Initialize database tables""" + logger.info(f"Initializing database at {self.db_path}...") with self._get_connection() as conn: - # Players table conn.execute(''' CREATE TABLE IF NOT EXISTS players ( player_id TEXT PRIMARY KEY, @@ -30,8 +32,6 @@ class Database: last_online REAL NOT NULL ) ''') - - # Buildings table conn.execute(''' CREATE TABLE IF NOT EXISTS buildings ( x INTEGER NOT NULL, @@ -44,78 +44,65 @@ class Database: FOREIGN KEY (owner_id) REFERENCES players (player_id) ) ''') - conn.commit() + logger.info("Database initialized successfully.") def save_game_state(self, game_state): """Save complete game state to database""" + logger.debug(f"Saving game state to database... ({len(game_state.players)} players, {len(game_state.buildings)} buildings)") + start_time = time.perf_counter() with self._get_connection() as conn: - # Save players using INSERT OR REPLACE to handle existing nicknames - for player in game_state.players.values(): - conn.execute(''' + player_data = [ + (p.player_id, p.nickname, p.money, p.population, p.color, p.last_online) + for p in game_state.players.values() + ] + if player_data: + conn.executemany(''' INSERT OR REPLACE INTO players (player_id, nickname, money, population, color, last_online) VALUES (?, ?, ?, ?, ?, ?) - ''', ( - player.player_id, - player.nickname, - player.money, - player.population, - player.color, - player.last_online - )) - - # Save buildings - conn.execute('DELETE FROM buildings') - - for building in game_state.buildings.values(): - conn.execute(''' - INSERT INTO buildings (x, y, type, owner_id, name, placed_at) + ''', player_data) + + cursor = conn.execute('SELECT x, y FROM buildings') + db_coords = {(row['x'], row['y']) for row in cursor} + gs_coords = set(game_state.buildings.keys()) + + coords_to_delete = db_coords - gs_coords + if coords_to_delete: + conn.executemany('DELETE FROM buildings WHERE x = ? AND y = ?', list(coords_to_delete)) + + building_data = [ + (b.x, b.y, b.building_type.value, b.owner_id, b.name, b.placed_at) + for b in game_state.buildings.values() + ] + if building_data: + conn.executemany(''' + INSERT OR REPLACE INTO buildings (x, y, type, owner_id, name, placed_at) VALUES (?, ?, ?, ?, ?, ?) - ''', ( - building.x, - building.y, - building.building_type.value, - building.owner_id, - building.name, - building.placed_at - )) + ''', building_data) conn.commit() + duration = time.perf_counter() - start_time + logger.debug(f"Game state saved to database in {duration:.4f} seconds.") def load_game_state(self) -> dict: """Load complete game state from database""" + logger.info("Loading game state from database...") + start_time = time.perf_counter() try: with self._get_connection() as conn: - # Load players - players = [] + players, buildings = [], [] + cursor = conn.execute('SELECT * FROM players') for row in cursor: - players.append({ - "player_id": row["player_id"], - "nickname": row["nickname"], - "money": row["money"], - "population": row["population"], - "color": row["color"], - "last_online": row["last_online"] - }) + players.append(dict(row)) - # Load buildings - buildings = [] cursor = conn.execute('SELECT * FROM buildings') for row in cursor: - buildings.append({ - "x": row["x"], - "y": row["y"], - "type": row["type"], - "owner_id": row["owner_id"], - "name": row["name"], - "placed_at": row["placed_at"] - }) + buildings.append(dict(row)) - return { - "players": players, - "buildings": buildings - } + duration = time.perf_counter() - start_time + logger.info(f"Loaded {len(players)} players and {len(buildings)} buildings in {duration:.4f} seconds.") + return {"players": players, "buildings": buildings} except Exception as e: - print(f"Error loading game state: {e}") + logger.error(f"Error loading game state: {e}", exc_info=True) return {"players": [], "buildings": []} diff --git a/server/economy.py b/server/economy.py index 8a387c0..4f7abd1 100644 --- a/server/economy.py +++ b/server/economy.py @@ -1,6 +1,8 @@ from server.game_state import GameState from server.models import BUILDING_CONFIGS, BuildingType import time +from collections import defaultdict +from server.logger import logger class EconomyEngine: """Handles all economy calculations and ticks""" @@ -9,55 +11,45 @@ class EconomyEngine: self.game_state = game_state def tick(self): - """Process one economy tick for all players""" - current_time = time.time() + """ + Process one economy tick for all players using an efficient aggregate calculation. + """ + logger.debug("Processing economy tick...") + start_time = time.perf_counter() - for player in self.game_state.players.values(): - # Calculate power factor (10% if offline, 100% if online) - time_diff = current_time - player.last_online - if player.is_online: - power_factor = 1.0 - else: - power_factor = 0.1 - - # Process player economy - self._process_player_economy(player, power_factor) - - def _process_player_economy(self, player, power_factor: float): - """Process economy for a single player""" - total_income = 0 - total_population = 0 - - # Get all player buildings - buildings = self.game_state.get_player_buildings(player.player_id) - - for building in buildings: + player_money_deltas = defaultdict(int) + player_total_population = defaultdict(int) + + # 1. Single pass over all buildings to aggregate their effects + for building in self.game_state.buildings.values(): config = BUILDING_CONFIGS[building.building_type] + owner_id = building.owner_id - # Calculate base income base_income = config.income - - # Apply connectivity bonus for income-generating buildings if base_income > 0: zone_size = self.game_state.get_building_zone_size(building.x, building.y) - connectivity_bonus = 1.0 + (zone_size * 0.05) # 5% per road in zone + connectivity_bonus = 1.0 + (zone_size * 0.05) base_income = int(base_income * connectivity_bonus) - # Add to totals - total_income += base_income - total_population += config.population + player_money_deltas[owner_id] += base_income + player_total_population[owner_id] += config.population + + # 2. Apply the aggregated deltas to each player + for player_id, player in self.game_state.players.items(): + power_factor = 1.0 if player.is_online else 0.1 + + money_delta = player_money_deltas.get(player_id, 0) + player.money += int(money_delta * power_factor) + + total_population = player_total_population.get(player_id, 0) + player.population = max(0, total_population) + + if player.money < -100000: + player.money = -100000 - # Apply power factor - total_income = int(total_income * power_factor) - - # Update player stats - player.money += total_income - player.population = max(0, total_population) - - # Prevent negative money (but allow debt for realism) - if player.money < -100000: - player.money = -100000 - + duration = time.perf_counter() - start_time + logger.debug(f"Economy tick processed in {duration:.4f} seconds for {len(self.game_state.players)} players.") + def calculate_building_stats(self, player_id: str, building_type: BuildingType) -> dict: """Calculate what a building would produce for a player""" config = BUILDING_CONFIGS[building_type] diff --git a/server/game_state.py b/server/game_state.py index 1bb63ed..36f1b41 100644 --- a/server/game_state.py +++ b/server/game_state.py @@ -1,6 +1,7 @@ from typing import Dict, List, Optional, Set, Tuple from server.models import Player, Building, BuildingType, BUILDING_CONFIGS import time +from server.logger import logger class GameState: """Manages the complete game state""" @@ -17,6 +18,7 @@ class GameState: player = self.players[player_id] player.is_online = True player.last_online = time.time() + logger.debug(f"Returning existing player '{nickname}' ({player_id}).") return player player = Player( @@ -25,50 +27,48 @@ class GameState: last_online=time.time() ) self.players[player_id] = player + logger.info(f"Created new player '{nickname}' ({player_id}) with ${player.money} starting money.") return player def place_building(self, player_id: str, building_type: str, x: int, y: int) -> dict: """Place a building on the map""" - # Check if tile is occupied + logger.debug(f"Attempting to place '{building_type}' at ({x},{y}) for player {player_id}.") + if (x, y) in self.buildings: + logger.warning(f"Placement failed for {player_id}: Tile ({x},{y}) already occupied.") return {"success": False, "error": "Tile already occupied"} - # Get player player = self.players.get(player_id) if not player: + logger.error(f"Placement failed: Player {player_id} not found.") return {"success": False, "error": "Player not found"} - # Get building config try: b_type = BuildingType(building_type) config = BUILDING_CONFIGS[b_type] except (ValueError, KeyError): + logger.error(f"Placement failed for {player_id}: Invalid building type '{building_type}'.") return {"success": False, "error": "Invalid building type"} - # Check if player can afford if not player.can_afford(config.cost): + logger.debug(f"Placement failed for {player_id}: Cannot afford '{building_type}'. Cost: {config.cost}, Player Money: {player.money}.") return {"success": False, "error": "Not enough money"} - # Check requirements if config.requires_population > player.population: + logger.debug(f"Placement failed for {player_id}: '{building_type}' requires {config.requires_population} pop, has {player.population}.") return {"success": False, "error": f"Requires {config.requires_population} population"} if config.power_required and not self._has_power_plant(player_id): + logger.debug(f"Placement failed for {player_id}: '{building_type}' requires a power plant.") return {"success": False, "error": "Requires power plant"} - # Place building - building = Building( - building_type=b_type, - x=x, - y=y, - owner_id=player_id, - placed_at=time.time() - ) + building = Building(building_type=b_type, x=x, y=y, owner_id=player_id, placed_at=time.time()) self.buildings[(x, y)] = building player.deduct_money(config.cost) - # Update road network if it's a road + logger.info(f"Player {player_id} placed '{building_type}' at ({x},{y}). New balance: ${player.money}.") + if b_type == BuildingType.ROAD: self.road_network.add((x, y)) self._update_connected_zones() @@ -77,18 +77,20 @@ class GameState: def remove_building(self, player_id: str, x: int, y: int) -> dict: """Remove a building""" + logger.debug(f"Attempting to remove building at ({x},{y}) for player {player_id}.") building = self.buildings.get((x, y)) if not building: + logger.warning(f"Removal failed for {player_id}: No building at ({x},{y}).") return {"success": False, "error": "No building at this location"} if building.owner_id != player_id: + logger.warning(f"Removal failed for {player_id}: Does not own building at ({x},{y}). Owner is {building.owner_id}.") return {"success": False, "error": "You don't own this building"} - # Remove building del self.buildings[(x, y)] - - # Update road network if it was a road + logger.info(f"Player {player_id} removed '{building.building_type.value}' from ({x},{y}).") + if building.building_type == BuildingType.ROAD: self.road_network.discard((x, y)) self._update_connected_zones() @@ -98,21 +100,19 @@ class GameState: def edit_building_name(self, player_id: str, x: int, y: int, name: str) -> dict: """Edit building name""" building = self.buildings.get((x, y)) - if not building: return {"success": False, "error": "No building at this location"} - if building.owner_id != player_id: return {"success": False, "error": "You don't own this building"} building.name = name + logger.info(f"Player {player_id} renamed building at ({x},{y}) to '{name}'.") return {"success": True} def _has_power_plant(self, player_id: str) -> bool: """Check if player has a power plant""" for building in self.buildings.values(): - if (building.owner_id == player_id and - building.building_type == BuildingType.POWER_PLANT): + if (building.owner_id == player_id and building.building_type == BuildingType.POWER_PLANT): return True return False @@ -122,6 +122,9 @@ class GameState: self.connected_zones = [] return + logger.debug("Recalculating road network connected zones...") + start_time = time.perf_counter() + visited = set() self.connected_zones = [] @@ -129,19 +132,13 @@ class GameState: if road_pos in visited: continue - # Flood fill to find connected zone zone = set() stack = [road_pos] - while stack: pos = stack.pop() - if pos in visited: - continue - + if pos in visited: continue visited.add(pos) zone.add(pos) - - # Check adjacent positions x, y = pos for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: adj_pos = (x + dx, y + dy) @@ -149,14 +146,15 @@ class GameState: stack.append(adj_pos) self.connected_zones.append(zone) + + duration = time.perf_counter() - start_time + logger.debug(f"Found {len(self.connected_zones)} road zones in {duration:.4f} seconds.") def get_building_zone_size(self, x: int, y: int) -> int: """Get the size of the connected zone for a building""" - # Find adjacent roads for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, 1), (1, -1), (-1, -1)]: road_pos = (x + dx, y + dy) if road_pos in self.road_network: - # Find which zone this road belongs to for zone in self.connected_zones: if road_pos in zone: return len(zone) @@ -178,13 +176,11 @@ class GameState: if not state: return - # Load players for player_data in state.get("players", []): player = Player(**player_data) - player.is_online = False + player.is_online = False # All players start as offline self.players[player.player_id] = player - # Load buildings for building_data in state.get("buildings", []): building = Building( building_type=BuildingType(building_data["type"]), @@ -200,3 +196,4 @@ class GameState: self.road_network.add((building.x, building.y)) self._update_connected_zones() + logger.info(f"Loaded state with {len(self.players)} players and {len(self.buildings)} buildings from database.") diff --git a/server/main.py b/server/main.py index 4290462..668f19a 100644 --- a/server/main.py +++ b/server/main.py @@ -4,157 +4,139 @@ from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pathlib import Path +import time +from server.logger import logger from server.websocket_manager import WebSocketManager from server.game_state import GameState from server.economy import EconomyEngine from server.database import Database # Global instances -ws_manager = WebSocketManager() game_state = GameState() +ws_manager = WebSocketManager(game_state) economy_engine = EconomyEngine(game_state) database = Database() -# Background task for economy ticks and persistence -async def game_loop(): - """Main game loop: economy ticks every 10 seconds, DB save every 10 seconds""" +TICK_INTERVAL = 10 # seconds + +# --- FIX: Reverting to a simple, reliable 10-second loop --- +async def economy_loop(): + """A simple loop that runs the economy tick every 10 seconds.""" while True: - try: - await asyncio.sleep(10) - - # Economy tick - economy_engine.tick() - - # Save to database - database.save_game_state(game_state) - - # Broadcast state to all players - await ws_manager.broadcast_game_state(game_state.get_state()) - except Exception as e: - print(f"Error in game loop: {e}") - # Continue running even if there's an error + await asyncio.sleep(TICK_INTERVAL) + + logger.info("Triggering scheduled economy tick.") + start_time = time.perf_counter() + + # 1. Process one economy tick + economy_engine.tick() + + # 2. Broadcast updates concurrently + update_tasks = [] + for player_id, player in game_state.players.items(): + if player_id in ws_manager.active_connections: + task = ws_manager.send_to_player(player_id, { + "type": "player_stats_update", + "player": player.to_dict() + }) + update_tasks.append(task) + + if update_tasks: + await asyncio.gather(*update_tasks) + + # 3. Save the new state + database.save_game_state(game_state) + + duration = time.perf_counter() - start_time + logger.info(f"Economy tick cycle completed in {duration:.4f} seconds.") @asynccontextmanager async def lifespan(app: FastAPI): - """Startup and shutdown events""" - # Startup + """Startup and shutdown events.""" + logger.info("Server starting up...") database.init_db() game_state.load_state(database.load_game_state()) - # Start game loop - task = asyncio.create_task(game_loop()) + # Start the simple economy loop + task = asyncio.create_task(economy_loop()) + logger.info(f"Economy loop started with a {TICK_INTERVAL}-second interval.") yield - # Shutdown + logger.info("Server shutting down...") task.cancel() database.save_game_state(game_state) + logger.info("Final game state saved.") app = FastAPI(lifespan=lifespan) -# Mount static files app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/") async def root(): - """Serve index.html""" return FileResponse("static/index.html") @app.websocket("/ws/{nickname}") async def websocket_endpoint(websocket: WebSocket, nickname: str): - """WebSocket endpoint for real-time game communication""" await ws_manager.connect(websocket, nickname) - try: - # Send initial game state player_id = await ws_manager.get_player_id(websocket) - player = game_state.get_or_create_player(nickname, player_id) - - await websocket.send_json({ - "type": "init", - "player": player.to_dict(), - "game_state": game_state.get_state() - }) - - # Listen for messages - while True: - data = await websocket.receive_json() - await handle_message(websocket, data) - + if player_id: + player = game_state.get_or_create_player(nickname, player_id) + await websocket.send_json({ + "type": "init", + "player": player.to_dict(), + "game_state": game_state.get_state() + }) + while True: + data = await websocket.receive_json() + await handle_message(websocket, data) except WebSocketDisconnect: await ws_manager.disconnect(websocket) + except Exception as e: + player_id = await ws_manager.get_player_id(websocket) + logger.error(f"An error occurred in the websocket endpoint for player {player_id}: {e}", exc_info=True) + async def handle_message(websocket: WebSocket, data: dict): - """Handle incoming WebSocket messages""" + """Handle incoming WebSocket messages.""" msg_type = data.get("type") player_id = await ws_manager.get_player_id(websocket) + + if not player_id: + logger.warning("Received message from an unknown player, ignoring.") + return + + logger.debug(f"Received '{msg_type}' from player {player_id}") if msg_type == "cursor_move": - # Broadcast cursor position - await ws_manager.broadcast({ - "type": "cursor_move", - "player_id": player_id, - "x": data["x"], - "y": data["y"] - }, exclude=websocket) + await ws_manager.broadcast({"type": "cursor_move", "player_id": player_id, "x": data["x"], "y": data["y"]}, exclude=websocket) elif msg_type == "place_building": - # Place building - result = game_state.place_building( - player_id, - data["building_type"], - data["x"], - data["y"] - ) - + result = game_state.place_building(player_id, data["building_type"], data["x"], data["y"]) if result["success"]: - # Broadcast to all players - await ws_manager.broadcast({ - "type": "building_placed", - "building": result["building"] - }) + await ws_manager.broadcast({"type": "building_placed", "building": result["building"]}) + # --- CHANGE: Action now only saves, economy is handled by the loop --- + database.save_game_state(game_state) else: - # Send error to player - await websocket.send_json({ - "type": "error", - "message": result["error"] - }) + await websocket.send_json({"type": "error", "message": result["error"]}) elif msg_type == "remove_building": - # Remove building result = game_state.remove_building(player_id, data["x"], data["y"]) - if result["success"]: - await ws_manager.broadcast({ - "type": "building_removed", - "x": data["x"], - "y": data["y"] - }) - + await ws_manager.broadcast({"type": "building_removed", "x": data["x"], "y": data["y"]}) + # --- CHANGE: Action now only saves, economy is handled by the loop --- + database.save_game_state(game_state) + else: + await websocket.send_json({"type": "error", "message": result["error"]}) + elif msg_type == "edit_building": - # Edit building name - result = game_state.edit_building_name( - player_id, - data["x"], - data["y"], - data["name"] - ) - + result = game_state.edit_building_name(player_id, data["x"], data["y"], data["name"]) if result["success"]: - await ws_manager.broadcast({ - "type": "building_updated", - "x": data["x"], - "y": data["y"], - "name": data["name"] - }) + await ws_manager.broadcast({"type": "building_updated", "x": data["x"], "y": data["y"], "name": data["name"]}) + database.save_game_state(game_state) elif msg_type == "chat": - # Broadcast chat message nickname = await ws_manager.get_nickname(websocket) - await ws_manager.broadcast({ - "type": "chat", - "nickname": nickname, - "message": data["message"], - "timestamp": data["timestamp"] - }) + await ws_manager.broadcast({"type": "chat", "nickname": nickname, "message": data["message"], "timestamp": data["timestamp"]}) diff --git a/server/websocket_manager.py b/server/websocket_manager.py index 2a86ccc..758c099 100644 --- a/server/websocket_manager.py +++ b/server/websocket_manager.py @@ -1,90 +1,118 @@ from fastapi import WebSocket -from typing import Dict, Set +from typing import Dict, List import uuid +import time +from server.game_state import GameState +from server.logger import logger class WebSocketManager: """Manages WebSocket connections for multiplayer""" - def __init__(self): - self.active_connections: Dict[str, WebSocket] = {} + def __init__(self, game_state: GameState): + self.active_connections: Dict[str, List[WebSocket]] = {} self.player_nicknames: Dict[str, str] = {} self.nickname_to_id: Dict[str, str] = {} + self.game_state = game_state async def connect(self, websocket: WebSocket, nickname: str): - """Connect a new player""" + """Connect a new player, allowing multiple connections per nickname.""" + logger.debug(f"Connection attempt from nickname '{nickname}'.") + await websocket.accept() - # Generate or reuse player ID if nickname in self.nickname_to_id: player_id = self.nickname_to_id[nickname] else: player_id = str(uuid.uuid4()) self.nickname_to_id[nickname] = player_id + logger.info(f"New player '{nickname}' assigned ID {player_id}.") - self.active_connections[player_id] = websocket - self.player_nicknames[player_id] = nickname + player = self.game_state.get_or_create_player(nickname, player_id) - # Broadcast player joined - await self.broadcast({ - "type": "player_joined", - "player_id": player_id, - "nickname": nickname - }) - - async def disconnect(self, websocket: WebSocket): - """Disconnect a player""" - player_id = None - for pid, ws in self.active_connections.items(): - if ws == websocket: - player_id = pid - break - - if player_id: - del self.active_connections[player_id] - nickname = self.player_nicknames.pop(player_id, None) - - # Broadcast player left + if not self.active_connections.get(player_id): + player.is_online = True + self.active_connections[player_id] = [] + logger.info(f"Player '{nickname}' ({player_id}) is now online.") await self.broadcast({ - "type": "player_left", + "type": "player_joined", "player_id": player_id, "nickname": nickname }) + + self.active_connections[player_id].append(websocket) + self.player_nicknames[player_id] = nickname + logger.debug(f"Added new websocket for '{nickname}'. Total connections for user: {len(self.active_connections[player_id])}.") - async def broadcast(self, message: dict, exclude: WebSocket = None): - """Broadcast message to all connected players""" - disconnected = [] + async def disconnect(self, websocket: WebSocket): + """Disconnect a player's specific websocket instance.""" + player_id = await self.get_player_id(websocket) - for player_id, websocket in self.active_connections.items(): + if player_id and player_id in self.active_connections: + self.active_connections[player_id].remove(websocket) + logger.debug(f"Removed websocket for player {player_id}. Remaining connections: {len(self.active_connections[player_id])}.") + + if not self.active_connections[player_id]: + del self.active_connections[player_id] + nickname = self.player_nicknames.pop(player_id, None) + + # --- REVERTED CHANGE: The following lines that set a player + # --- to 'offline' have been removed to restore original economy. + # player = self.game_state.players.get(player_id) + # if player: + # player.is_online = False + # player.last_online = time.time() + + logger.info(f"Player '{nickname}' ({player_id}) last connection closed.") + await self.broadcast({ + "type": "player_left", + "player_id": player_id, + "nickname": nickname + }) + + async def send_to_player(self, player_id: str, message: dict): + """Send a message to all active connections for a specific player.""" + connections = self.active_connections.get(player_id, []) + if not connections: return + + logger.debug(f"Sending '{message['type']}' to player {player_id} ({len(connections)} connections).") + disconnected_sockets = [] + + for websocket in connections: + try: + await websocket.send_json(message) + except Exception: + disconnected_sockets.append(websocket) + + for ws in disconnected_sockets: + logger.warning(f"Found dead socket for player {player_id} during send. Cleaning up.") + if ws in self.active_connections.get(player_id, []): + self.active_connections[player_id].remove(ws) + + async def broadcast(self, message: dict, exclude: WebSocket = None): + """Broadcast message to all connected players and all their connections.""" + msg_type = message.get("type", "unknown") + logger.debug(f"Broadcasting '{msg_type}' to all active connections.") + all_sockets = [] + for ws_list in self.active_connections.values(): + all_sockets.extend(ws_list) + + for websocket in all_sockets: if websocket == exclude: continue try: await websocket.send_json(message) except Exception: - disconnected.append(player_id) - - # Clean up disconnected websockets - for player_id in disconnected: - if player_id in self.active_connections: - del self.active_connections[player_id] - if player_id in self.player_nicknames: - del self.player_nicknames[player_id] - - async def broadcast_game_state(self, state: dict): - """Broadcast full game state""" - await self.broadcast({ - "type": "game_state_update", - "state": state - }) + pass async def get_player_id(self, websocket: WebSocket) -> str: - """Get player ID from websocket""" - for player_id, ws in self.active_connections.items(): - if ws == websocket: + """Get player ID from a specific websocket instance.""" + for player_id, ws_list in self.active_connections.items(): + if websocket in ws_list: return player_id return None async def get_nickname(self, websocket: WebSocket) -> str: - """Get nickname from websocket""" + """Get nickname from websocket.""" player_id = await self.get_player_id(websocket) return self.player_nicknames.get(player_id, "Unknown") diff --git a/static/js/App.js b/static/js/App.js index 8d25115..dd3d298 100644 --- a/static/js/App.js +++ b/static/js/App.js @@ -15,41 +15,33 @@ export class App { players: {}, buildings: {} }; - this.selectedBuildingType = null; this.isPlacingBuilding = false; } init() { console.log('Initializing City Builder...'); - // Initialize UI Manager this.uiManager = new UIManager(this); this.uiManager.init(); - // Show login screen this.uiManager.showLoginScreen(); } async startGame(nickname) { console.log(`Starting game for ${nickname}...`); - // Hide login, show game UI this.uiManager.hideLoginScreen(); this.uiManager.showGameUI(); - // Initialize renderer this.renderer = new GameRenderer(); this.renderer.init(); - // Initialize input handler this.inputHandler = new InputHandler(this); this.inputHandler.init(); - // Connect to WebSocket this.wsClient = new WebSocketClient(this); await this.wsClient.connect(nickname); - // Start render loop this.renderer.startRenderLoop(); } @@ -62,21 +54,22 @@ export class App { // Update UI this.uiManager.updateStats(this.player); this.uiManager.updateBuildingToolbox(this.player); - // Render initial state this.renderer.updateGameState(gameState); } - onGameStateUpdate(state) { - this.gameState = state; - this.renderer.updateGameState(state); - - // Update own player stats - if (this.player && state.players[this.player.player_id]) { - this.player = state.players[this.player.player_id]; + onPlayerStatsUpdate(playerData) { + // Update our own player object and UI if the update is for us + if (this.player && this.player.player_id === playerData.player_id) { + this.player = playerData; this.uiManager.updateStats(this.player); this.uiManager.updateBuildingToolbox(this.player); } + + // Update the player in the global game state as well + if (this.gameState.players[playerData.player_id]) { + this.gameState.players[playerData.player_id] = playerData; + } } onCursorMove(playerId, x, y) { @@ -86,16 +79,22 @@ export class App { onBuildingPlaced(building) { console.log('Building placed:', building); this.renderer.addBuilding(building); + // Also add to local game state for context menu checks etc. + this.gameState.buildings[`${building.x},${building.y}`] = building; } onBuildingRemoved(x, y) { console.log('Building removed at:', x, y); this.renderer.removeBuilding(x, y); + delete this.gameState.buildings[`${x},${y}`]; } onBuildingUpdated(x, y, name) { console.log('Building updated:', x, y, name); this.renderer.updateBuildingName(x, y, name); + if (this.gameState.buildings[`${x},${y}`]) { + this.gameState.buildings[`${x},${y}`].name = name; + } } onPlayerJoined(playerId, nickname) { @@ -127,7 +126,6 @@ export class App { placeBuilding(x, y) { if (!this.selectedBuildingType) return; - console.log('Placing building:', this.selectedBuildingType, 'at', x, y); this.wsClient.placeBuilding(this.selectedBuildingType, x, y); } diff --git a/static/js/WebSocketClient.js b/static/js/WebSocketClient.js index 23b69c5..3818fe5 100644 --- a/static/js/WebSocketClient.js +++ b/static/js/WebSocketClient.js @@ -12,7 +12,6 @@ export class WebSocketClient { try { this.ws = new WebSocket(wsUrl); - this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; @@ -50,10 +49,9 @@ export class WebSocketClient { this.app.onPlayerInit(data.player, data.game_state); break; - case 'game_state_update': - this.app.onGameStateUpdate(data.state); + case 'player_stats_update': + this.app.onPlayerStatsUpdate(data.player); break; - case 'cursor_move': this.app.onCursorMove(data.player_id, data.x, data.y); break; @@ -61,11 +59,9 @@ export class WebSocketClient { case 'building_placed': this.app.onBuildingPlaced(data.building); break; - case 'building_removed': this.app.onBuildingRemoved(data.x, data.y); break; - case 'building_updated': this.app.onBuildingUpdated(data.x, data.y, data.name); break;