import asyncio from contextlib import asynccontextmanager from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from pathlib import Path 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() 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""" while True: 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()) @asynccontextmanager async def lifespan(app: FastAPI): """Startup and shutdown events""" # Startup database.init_db() game_state.load_state(database.load_game_state()) # Start game loop task = asyncio.create_task(game_loop()) yield # Shutdown task.cancel() database.save_game_state(game_state) 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) except WebSocketDisconnect: await ws_manager.disconnect(websocket) async def handle_message(websocket: WebSocket, data: dict): """Handle incoming WebSocket messages""" msg_type = data.get("type") player_id = await ws_manager.get_player_id(websocket) 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) elif msg_type == "place_building": # Place building 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"] }) else: # Send error to player 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"] }) elif msg_type == "edit_building": # Edit building 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"] }) 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"] })