157 lines
4.5 KiB
Python
Raw Normal View History

2025-10-04 20:40:44 +02:00
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"]
})