File size: 11,151 Bytes
6674651
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d086f83
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# 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")