96 lines
3.1 KiB
Python
96 lines
3.1 KiB
Python
|
|
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",
|
||
|
|
}
|