Gameplay (legal moves, move application, pressure maps, PGN parsing) now runs entirely in the browser via a chess.js port of engine.py (static/engine.js + vendored chess.min.js). The app no longer calls the server during play, so it stays instant and works even when the backend is scaled to zero or offline. - engine.js: line-for-line port of engine.py; parity-verified identical output (pressure, FEN, status, SAN/UCI) across all 10 built-in games / 829 frames. - app.js: replace all /api fetches with local engine calls; graceful PGN parse errors; asset paths moved to root for static serving. - Save games to localStorage + load from the game dropdown; export any game as a .pgn file. Minimal additive UI in the existing export row. - games.json generated from games.py (scripts/export_games.py). - Production Dockerfile -> static-web-server (~9MB image, was ~150MB python). - make dev -> bun static server; add make games / make parity targets. - app.py kept as optional fallback (serves static app + /api routes). - parity/: automated JS-vs-python engine parity test. git diff stat: 16 files changed, 722 insertions(+), 128 deletions(-)
119 lines
2.6 KiB
Python
119 lines
2.6 KiB
Python
"""FastAPI application for chess pressure visualization."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
import uvicorn
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
from .engine import make_move, parse_pgn
|
|
from .games import get_game_list, get_game_pgn
|
|
|
|
STATIC = Path(__file__).resolve().parent / "static"
|
|
|
|
app = FastAPI(title="Chess Pressure", docs_url=None, redoc_url=None)
|
|
|
|
|
|
# --- Models ---
|
|
|
|
|
|
class PGNUpload(BaseModel):
|
|
pgn: str
|
|
|
|
|
|
class MoveRequest(BaseModel):
|
|
fen: str
|
|
uci: str
|
|
|
|
|
|
# --- Health ---
|
|
|
|
|
|
@app.get("/health", include_in_schema=False)
|
|
@app.get("/up", include_in_schema=False)
|
|
@app.get("/status", include_in_schema=False)
|
|
@app.get("/health-check", include_in_schema=False)
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
# --- API ---
|
|
|
|
|
|
@app.get("/api/games")
|
|
def list_games():
|
|
return get_game_list()
|
|
|
|
|
|
@app.get("/api/games/{game_id}")
|
|
@lru_cache(maxsize=16)
|
|
def load_game(game_id: str):
|
|
pgn = get_game_pgn(game_id)
|
|
if pgn is None:
|
|
raise HTTPException(404, "Game not found")
|
|
return parse_pgn(pgn)
|
|
|
|
|
|
@app.post("/api/parse")
|
|
def parse_uploaded_pgn(body: PGNUpload):
|
|
try:
|
|
return parse_pgn(body.pgn)
|
|
except ValueError as e:
|
|
raise HTTPException(400, str(e))
|
|
|
|
|
|
@app.get("/api/legal")
|
|
def legal_moves(fen: str):
|
|
import chess
|
|
|
|
board = chess.Board(fen)
|
|
return [m.uci() for m in board.legal_moves]
|
|
|
|
|
|
@app.post("/api/move")
|
|
def do_move(body: MoveRequest):
|
|
try:
|
|
return make_move(body.fen, body.uci)
|
|
except ValueError as e:
|
|
raise HTTPException(400, str(e))
|
|
|
|
|
|
# --- Static files ---
|
|
# The app is fully client-side; production is served statically (see Dockerfile).
|
|
# This Python server is kept as an optional fallback / parity reference and the
|
|
# /api routes above remain available, but the UI no longer depends on them.
|
|
# Mounted last so /api and /health take precedence.
|
|
|
|
app.mount("/", StaticFiles(directory=str(STATIC), html=True), name="static")
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Chess Pressure server")
|
|
parser.add_argument(
|
|
"--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)"
|
|
)
|
|
parser.add_argument(
|
|
"--port", "-p", type=int, default=8888, help="Port (default: 8888)"
|
|
)
|
|
parser.add_argument(
|
|
"--workers", "-w", type=int, default=2, help="Worker processes (default: 2)"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
uvicorn.run(
|
|
"chess_pressure.app:app",
|
|
host=args.host,
|
|
port=args.port,
|
|
workers=args.workers,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|