import json import logging from pathlib import Path from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from app.api.routes import router as api_router from app.api.websocket import manager from app.config import settings from app.game import game_engine logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) app = FastAPI( title="botWebWars API", description="Backend service for botWebWars 64x64 grid battle arena.", version="1.0.0", ) # Enable CORS for frontend development app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Mount REST API routes app.include_router(api_router, prefix="/api") # WebSocket endpoint for real-time board updates @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: # Immediately transmit current board state on connection state = await game_engine.get_board_state() await websocket.send_text( json.dumps({"event": "init", "state": state.model_dump()}, default=str) ) while True: # Keep connection open, receive ping or client commands data = await websocket.receive_text() try: msg = json.loads(data) if msg.get("action") == "ping": await websocket.send_text(json.dumps({"event": "pong"})) except json.JSONDecodeError: pass except WebSocketDisconnect: manager.disconnect(websocket) except Exception as e: logger.error(f"WebSocket error: {e}") manager.disconnect(websocket) # Mount static frontend files if built frontend_dist = settings.FRONTEND_DIR if frontend_dist.exists() and (frontend_dist / "index.html").exists(): logger.info(f"Serving frontend static files from: {frontend_dist}") # Mount assets folder assets_dir = frontend_dist / "assets" if assets_dir.exists(): app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets") # SPA catch-all for root and unmatched GET paths (excluding /api and /ws) @app.get("/{full_path:path}") async def serve_spa(full_path: str): # If the requested file directly exists inside frontend_dist, serve it target_file = frontend_dist / full_path if full_path and target_file.is_file(): return FileResponse(target_file) # Otherwise fallback to index.html for SPA routing return FileResponse(frontend_dist / "index.html") else: logger.info("Frontend dist directory not found. Running in API-only mode.") @app.get("/") async def root(): return { "message": "Welcome to botWebWars API! Frontend build not detected yet.", "docs": "/docs", "health": "/api/health", "board": "/api/board", }