electro-sb's picture
Containerization and frontend backend communication issue fixed
d086f83
# chess_engine/api/rest_api.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, Field
from typing import Dict, List, Optional, Any
import chess
from enum import Enum
from chess_engine.api.game_controller import GameController, GameOptions
from chess_engine.ai.stockfish_wrapper import DifficultyLevel
# Define API models
class ColorEnum(str, Enum):
WHITE = "white"
BLACK = "black"
class DifficultyEnum(str, Enum):
BEGINNER = "beginner"
EASY = "easy"
MEDIUM = "medium"
HARD = "hard"
EXPERT = "expert"
MASTER = "master"
class NewGameRequest(BaseModel):
player_color: ColorEnum = ColorEnum.WHITE
difficulty: DifficultyEnum = DifficultyEnum.MEDIUM
time_limit: float = Field(default=1.0, ge=0.1, le=10.0)
use_opening_book: bool = True
enable_analysis: bool = True
stockfish_path: Optional[str] = None
class MoveRequest(BaseModel):
move: str = Field(..., description="Move in UCI notation (e.g., 'e2e4', 'e7e8q') or SAN (e.g., 'e4', 'e8=Q')")
class PromotionDetails(BaseModel):
"""Details about a required promotion move"""
from_square: str = Field(..., description="Starting square of the promotion move (e.g., 'e7')")
to_square: str = Field(..., description="Destination square of the promotion move (e.g., 'e8')")
available_pieces: List[str] = Field(..., description="Available pieces for promotion: ['queen', 'rook', 'bishop', 'knight']")
class GameResponse(BaseModel):
"""Enhanced game response that can include promotion requirements"""
status: str = Field(..., description="Response status: 'success', 'error', 'promotion_required', 'game_over'")
message: Optional[str] = Field(None, description="Response message")
player_move: Optional[str] = Field(None, description="Player's move in UCI notation")
ai_move: Optional[str] = Field(None, description="AI's move in UCI notation")
board_state: Optional[Dict[str, Any]] = Field(None, description="Current board state")
analysis: Optional[Dict[str, Any]] = Field(None, description="Position analysis")
promotion_details: Optional[PromotionDetails] = Field(None, description="Promotion requirements when status is 'promotion_required'")
result: Optional[str] = Field(None, description="Game result when status is 'game_over'")
winner: Optional[str] = Field(None, description="Winner when game is over")
reason: Optional[str] = Field(None, description="Reason for game end")
class UndoRequest(BaseModel):
count: int = Field(default=2, ge=1, le=10, description="Number of half-moves to undo")
class PromotionRequest(BaseModel):
"""Request for completing a promotion move"""
from_square: str = Field(..., description="Starting square of the promotion move (e.g., 'e7')")
to_square: str = Field(..., description="Destination square of the promotion move (e.g., 'e8')")
promotion_piece: str = Field(..., description="Piece to promote to: 'queen', 'rook', 'bishop', 'knight', or 'q', 'r', 'b', 'n'")
# Create FastAPI app
app = FastAPI(
title="Chess Engine API",
description="REST API for chess engine with Stockfish integration",
version="1.0.0"
)
# Game controller instance
game_controller = GameController()
# Helper function to get game controller
def get_game_controller():
return game_controller
@app.post("/game/new", response_model=Dict[str, Any], tags=["Game Control"])
async def new_game(request: NewGameRequest, controller: GameController = Depends(get_game_controller)):
"""Start a new game with specified options"""
# Convert enum values to appropriate types
options = GameOptions(
player_color=chess.WHITE if request.player_color == ColorEnum.WHITE else chess.BLACK,
difficulty=DifficultyLevel[request.difficulty.upper()],
time_limit=request.time_limit,
use_opening_book=request.use_opening_book,
enable_analysis=request.enable_analysis,
stockfish_path=request.stockfish_path
)
result = controller.start_new_game(options)
if result["status"] == "error":
raise HTTPException(status_code=500, detail=result["message"])
return result
@app.post("/game/move", response_model=GameResponse, tags=["Game Play"])
async def make_move(request: MoveRequest, controller: GameController = Depends(get_game_controller)):
"""
Make a move on the board.
This endpoint handles regular moves and promotion moves. When a pawn reaches the back rank
without specifying a promotion piece, it returns a 'promotion_required' status with details
about the required promotion.
**Move Formats:**
- Regular moves: 'e2e4', 'Nf3', 'O-O'
- Promotion moves: 'e7e8q' (UCI) or 'e8=Q' (SAN)
**Promotion Pieces:**
- 'q' or 'queen' for Queen
- 'r' or 'rook' for Rook
- 'b' or 'bishop' for Bishop
- 'n' or 'knight' for Knight
**Response Status Values:**
- 'success': Move completed successfully
- 'promotion_required': Pawn promotion requires piece selection
- 'error': Invalid move or game state error
- 'game_over': Game ended after this move
"""
result = controller.make_player_move(request.move)
# Handle different response statuses
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result["message"])
elif result["status"] == "promotion_required":
# Return 200 with promotion_required status - this is not an error
return GameResponse(
status="promotion_required",
message=result["message"],
board_state=result["board_state"],
promotion_details=PromotionDetails(
from_square=result["promotion_details"]["from"],
to_square=result["promotion_details"]["to"],
available_pieces=result["promotion_details"]["available_pieces"]
)
)
elif result["status"] == "game_over":
return GameResponse(
status="game_over",
message=result.get("message"),
board_state=result["board_state"],
result=result["result"],
winner=result.get("winner"),
reason=result["reason"]
)
else:
# Success case
return GameResponse(
status="success",
player_move=result.get("player_move"),
ai_move=result.get("ai_move"),
board_state=result["board_state"],
analysis=result.get("analysis")
)
@app.get("/game/state", response_model=Dict[str, Any], tags=["Game Info"])
async def get_game_state(controller: GameController = Depends(get_game_controller)):
"""Get current game state"""
return controller.get_game_state()
@app.post("/game/hint", response_model=Dict[str, Any], tags=["Game Help"])
async def get_hint(controller: GameController = Depends(get_game_controller)):
"""Get a hint for the current position"""
result = controller.get_hint()
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result["message"])
return result
@app.post("/game/undo", response_model=Dict[str, Any], tags=["Game Control"])
async def undo_move(request: UndoRequest, controller: GameController = Depends(get_game_controller)):
"""Undo moves"""
result = controller.undo_move(request.count)
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result["message"])
return result
@app.post("/game/promotion", response_model=GameResponse, tags=["Game Play"])
async def complete_promotion(request: PromotionRequest, controller: GameController = Depends(get_game_controller)):
"""
Complete a pawn promotion by specifying the promotion piece.
This endpoint is used when a previous move request returned 'promotion_required' status.
It completes the promotion move with the specified piece.
**Promotion Pieces:**
- 'queen' or 'q' for Queen (most common choice)
- 'rook' or 'r' for Rook
- 'bishop' or 'b' for Bishop
- 'knight' or 'n' for Knight
**Example Usage:**
1. Make move 'e7e8' -> returns promotion_required
2. Call this endpoint with promotion_piece='queen' to complete the move
"""
from chess_engine.promotion import PromotionMoveHandler
# Create the full promotion move in UCI notation
promotion_move = PromotionMoveHandler.create_promotion_move(
request.from_square,
request.to_square,
request.promotion_piece
)
# Make the promotion move
result = controller.make_player_move(promotion_move)
# Handle different response statuses
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result["message"])
elif result["status"] == "promotion_required":
# This shouldn't happen with a properly formed promotion move
raise HTTPException(status_code=400, detail="Invalid promotion move format")
elif result["status"] == "game_over":
return GameResponse(
status="game_over",
message=result.get("message"),
board_state=result["board_state"],
result=result["result"],
winner=result.get("winner"),
reason=result["reason"]
)
else:
# Success case
return GameResponse(
status="success",
player_move=result.get("player_move"),
ai_move=result.get("ai_move"),
board_state=result["board_state"],
analysis=result.get("analysis")
)
@app.post("/game/resign", response_model=Dict[str, Any], tags=["Game Control"])
async def resign_game(controller: GameController = Depends(get_game_controller)):
"""Resign the current game"""
result = controller.resign()
if result["status"] == "error":
raise HTTPException(status_code=400, detail=result["message"])
return result
@app.get("/game/promotion/pieces", response_model=Dict[str, Any], tags=["Game Info"])
async def get_promotion_pieces():
"""
Get available promotion pieces with their details.
Returns information about all pieces that a pawn can be promoted to,
including their names, symbols, UCI notation, and relative values.
"""
from chess_engine.promotion import PromotionMoveHandler
return {
"status": "success",
"promotion_pieces": PromotionMoveHandler.get_available_promotions()
}
@app.get("/health", response_model=Dict[str, str], tags=["System"])
async def health_check():
"""Health check endpoint for container monitoring"""
return {
"status": "healthy",
"service": "chess-engine-api",
"version": "1.0.0"
}
# Mount static files AFTER defining all API routes
# Mount UI at /ui path
app.mount("/ui", StaticFiles(directory="./dist", html=True), name="static")
# Mount at root for the index.html, but with a lower priority than API endpoints
app.mount("/", StaticFiles(directory="./dist", html=True), name="root_static")