Add Maia chess gameplay and deployment
Some checks failed
Deploy Blog Backend / test (push) Successful in 2m6s
Deploy Blog Backend / deploy (push) Failing after 1m8s

This commit is contained in:
박원엽
2026-06-19 14:49:20 +09:00
parent b47258ea53
commit 3042dc898e
26 changed files with 1682 additions and 32 deletions

20
maia-engine/Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV HF_HOME=/models/huggingface
RUN apt-get update \
&& apt-get install -y --no-install-recommends git build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

151
maia-engine/app/main.py Normal file
View File

@@ -0,0 +1,151 @@
import os
import subprocess
import threading
from datetime import datetime, timezone
import chess
import chess.engine
import chess.pgn
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
DEFAULT_MODEL = os.getenv("MAIA_MODEL", "5m").removeprefix("maia3-")
DEFAULT_DEVICE = os.getenv("MAIA_DEVICE", "cpu")
DEFAULT_USE_AMP = os.getenv("MAIA_USE_AMP", "false").lower() == "true"
app = FastAPI(title="Maia Engine Bridge")
engine_lock = threading.Lock()
engines: dict[str, chess.engine.SimpleEngine] = {}
class StateRequest(BaseModel):
moves: list[str] = Field(default_factory=list)
white: str | None = None
black: str | None = None
event: str = "Maia3"
class PlayRequest(StateRequest):
rating: int = Field(default=1500, ge=600, le=2600)
model: str = Field(default=DEFAULT_MODEL, pattern="^(3m|5m|23m|79m)$")
temperature: float = Field(default=0.8, ge=0.0, le=2.0)
topP: float = Field(default=0.95, ge=0.0, le=1.0)
def model_alias(model: str) -> str:
return f"maia3-{model}"
def engine_command(model: str) -> list[str]:
command = [
"maia3-uci",
"--model",
model_alias(model),
"--use-uci-history",
"--device",
DEFAULT_DEVICE,
]
if not DEFAULT_USE_AMP:
command.append("--no-use-amp")
return command
def get_engine(model: str) -> chess.engine.SimpleEngine:
if model not in engines:
try:
engines[model] = chess.engine.SimpleEngine.popen_uci(engine_command(model))
except FileNotFoundError as exc:
raise HTTPException(status_code=503, detail="maia3-uci executable was not found") from exc
except (chess.engine.EngineError, subprocess.SubprocessError, OSError) as exc:
raise HTTPException(status_code=503, detail="failed to start Maia engine") from exc
return engines[model]
def build_board(moves: list[str]) -> chess.Board:
board = chess.Board()
for move in moves:
try:
board.push_uci(move)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"invalid move: {move}") from exc
return board
def pgn_payload(board: chess.Board, request: StateRequest, result: str | None) -> str:
game = chess.pgn.Game.from_board(board)
game.headers["Event"] = request.event
game.headers["Site"] = "blog-backend"
game.headers["Date"] = datetime.now(timezone.utc).strftime("%Y.%m.%d")
if request.white:
game.headers["White"] = request.white
if request.black:
game.headers["Black"] = request.black
game.headers["Result"] = result or "*"
exporter = chess.pgn.StringExporter(headers=True, variations=False, comments=False)
return game.accept(exporter)
def board_payload(board: chess.Board, request: StateRequest) -> dict:
outcome = board.outcome(claim_draw=True)
if outcome is None:
status = "IN_PROGRESS"
result = None
else:
status = outcome.termination.name
result = outcome.result()
return {
"fen": board.fen(),
"turn": "white" if board.turn == chess.WHITE else "black",
"status": status,
"result": result,
"pgn": pgn_payload(board, request, result),
}
@app.get("/health")
def health() -> dict:
return {"status": "ok"}
@app.post("/maia/state")
def state(request: StateRequest) -> dict:
board = build_board(request.moves)
return board_payload(board, request)
@app.post("/maia/move")
def move(request: PlayRequest) -> dict:
board = build_board(request.moves)
current = board_payload(board, request)
if current["status"] != "IN_PROGRESS":
return {"move": None, **current}
with engine_lock:
maia = get_engine(request.model)
try:
maia.configure(
{
"Elo": request.rating,
"Temperature": request.temperature,
"TopP": request.topP,
}
)
result = maia.play(board, chess.engine.Limit(nodes=1))
except chess.engine.EngineError as exc:
raise HTTPException(status_code=503, detail="Maia engine failed to select a move") from exc
if result.move is None:
raise HTTPException(status_code=503, detail="Maia engine returned no move")
board.push(result.move)
return {"move": result.move.uci(), **board_payload(board, request)}
@app.on_event("shutdown")
def shutdown() -> None:
for maia in engines.values():
maia.quit()
engines.clear()

View File

@@ -0,0 +1,4 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
python-chess==1.999
git+https://github.com/CSSLab/maia3.git