Spaces:
Sleeping
Sleeping
| """ | |
| Chess game manager using python-chess library. | |
| Handles: | |
| - Board state and move validation | |
| - Game status (check, checkmate, stalemate, etc.) | |
| - Move history and notation | |
| - Position analysis and FEN | |
| - ASCII board representation | |
| """ | |
| import logging | |
| import chess as python_chess | |
| from typing import Optional, List, Dict, Any, Tuple | |
| from datetime import datetime | |
| logger = logging.getLogger(__name__) | |
| class ChessGame: | |
| """ | |
| Chess game state manager. | |
| Wraps python-chess board with additional tracking for player names, | |
| move history, and metadata. | |
| """ | |
| def __init__( | |
| self, | |
| white_player: str = "White", | |
| black_player: str = "Black", | |
| fen: Optional[str] = None, | |
| ): | |
| """ | |
| Initialize a chess game. | |
| Args: | |
| white_player: Name of white player (e.g., "Player") | |
| black_player: Name of black player (e.g., "Chess Master") | |
| fen: Optional starting position (default: standard starting position) | |
| """ | |
| self.white_player = white_player | |
| self.black_player = black_player | |
| self.board = python_chess.Board(fen if fen else python_chess.STARTING_FEN) | |
| self.move_history: List[Dict[str, Any]] = [] | |
| self.created_at = datetime.now() | |
| self.started_at = None | |
| self.ended_at = None | |
| self.result = None # "1-0", "0-1", "1/2-1/2", or None | |
| def get_move_count(self) -> int: | |
| """Get total number of moves made.""" | |
| return len(self.move_history) | |
| def get_current_player(self) -> str: | |
| """Get name of player whose turn it is.""" | |
| return ( | |
| self.white_player | |
| if self.board.turn == python_chess.WHITE | |
| else self.black_player | |
| ) | |
| def get_opponent_player(self) -> str: | |
| """Get name of player waiting to move.""" | |
| return ( | |
| self.black_player | |
| if self.board.turn == python_chess.WHITE | |
| else self.white_player | |
| ) | |
| def make_move(self, move_uci: str) -> Tuple[bool, Optional[str]]: | |
| """ | |
| Make a move on the board. | |
| Args: | |
| move_uci: Move in UCI format (e.g., "e2e4") or algebraic (e.g., "e4") | |
| Returns: | |
| Tuple of (success, error_message) | |
| """ | |
| try: | |
| # Try SAN first (handles e4, Nf3, O-O, and promotion e7e8=Q) | |
| try: | |
| move = self.board.parse_san(move_uci) | |
| except Exception: | |
| # Fallback to UCI (e2e4, e7e8q, etc.) | |
| try: | |
| move = python_chess.Move.from_uci(move_uci) | |
| except Exception: | |
| return False, f"Invalid move format: {move_uci}" | |
| if move not in self.board.legal_moves: | |
| return False, f"Illegal move: {move_uci}" | |
| player_who_moved = self.get_current_player() | |
| fen_before = self.board.fen() | |
| san = self.board.san(move) # compute before pushing | |
| self.board.push(move) | |
| self.move_history.append({ | |
| "move_number": len(self.move_history) + 1, | |
| "player": player_who_moved, | |
| "move_uci": move.uci(), | |
| "move_san": san, | |
| "timestamp": datetime.now().isoformat(), | |
| "fen_before": fen_before, | |
| "fen_after": self.board.fen(), | |
| }) | |
| logger.info(f"{player_who_moved} played {san}") | |
| return True, None | |
| except ValueError: | |
| return False, f"Invalid move format: {move_uci}" | |
| except Exception as e: | |
| return False, f"Error making move: {str(e)}" | |
| def get_legal_moves(self) -> List[str]: | |
| """Get all legal moves in algebraic notation.""" | |
| return [self.board.san(move) for move in self.board.legal_moves] | |
| def get_legal_moves_uci(self) -> List[str]: | |
| """Get all legal moves in UCI notation.""" | |
| return [move.uci() for move in self.board.legal_moves] | |
| def is_check(self) -> bool: | |
| """Is the current player in check?""" | |
| return self.board.is_check() | |
| def is_checkmate(self) -> bool: | |
| """Is the game in checkmate?""" | |
| return self.board.is_checkmate() | |
| def is_stalemate(self) -> bool: | |
| """Is the game in stalemate?""" | |
| return self.board.is_stalemate() | |
| def is_game_over(self) -> bool: | |
| """Is the game over?""" | |
| return self.board.is_game_over() or self.result is not None | |
| def get_game_result(self) -> Optional[str]: | |
| """ | |
| Get game result. | |
| Returns: | |
| "1-0" (white wins), "0-1" (black wins), "1/2-1/2" (draw), or None (ongoing) | |
| """ | |
| if not self.is_game_over(): | |
| return None | |
| # Resignation and agreed draws set self.result directly | |
| if self.result is not None: | |
| return self.result | |
| if self.board.is_checkmate(): | |
| return "1-0" if self.board.turn == python_chess.BLACK else "0-1" | |
| return "1/2-1/2" | |
| def get_game_status(self) -> Dict[str, Any]: | |
| """Get detailed game status.""" | |
| return { | |
| "is_check": self.is_check(), | |
| "is_checkmate": self.is_checkmate(), | |
| "is_stalemate": self.is_stalemate(), | |
| "is_game_over": self.is_game_over(), | |
| "result": self.get_game_result(), | |
| "current_player": self.get_current_player(), | |
| "moves_count": self.get_move_count(), | |
| "legal_moves_count": self.board.legal_moves.count(), | |
| } | |
| def get_ascii_board(self) -> str: | |
| """Get ASCII representation of the board.""" | |
| return str(self.board) | |
| def get_piece_positions(self, white_label: str = "White", black_label: str = "Black") -> str: | |
| """ | |
| Get structured piece positions for LLM context. | |
| Much clearer than ASCII art — explicit piece, color, and square. | |
| """ | |
| _names = { | |
| python_chess.KING: "King", | |
| python_chess.QUEEN: "Queen", | |
| python_chess.ROOK: "Rook", | |
| python_chess.BISHOP: "Bishop", | |
| python_chess.KNIGHT: "Knight", | |
| python_chess.PAWN: "Pawn", | |
| } | |
| _order = ["King", "Queen", "Rook", "Bishop", "Knight", "Pawn"] | |
| white, black = [], [] | |
| for square, piece in self.board.piece_map().items(): | |
| entry = f"{_names[piece.piece_type]}-{python_chess.square_name(square)}" | |
| (white if piece.color == python_chess.WHITE else black).append(entry) | |
| white.sort(key=lambda p: _order.index(p.split("-")[0])) | |
| black.sort(key=lambda p: _order.index(p.split("-")[0])) | |
| return ( | |
| f"{white_label} (White): {', '.join(white)}\n" | |
| f"{black_label} (Black): {', '.join(black)}" | |
| ) | |
| def get_fen(self) -> str: | |
| """Get FEN string of current position.""" | |
| return self.board.fen() | |
| def set_fen(self, fen: str) -> Tuple[bool, Optional[str]]: | |
| """ | |
| Set board to a specific FEN position. | |
| Args: | |
| fen: FEN string | |
| Returns: | |
| Tuple of (success, error_message) | |
| """ | |
| try: | |
| self.board.set_fen(fen) | |
| logger.info(f"Position set to: {fen}") | |
| return True, None | |
| except ValueError as e: | |
| return False, f"Invalid FEN: {str(e)}" | |
| def get_move_history(self) -> List[Dict[str, Any]]: | |
| """Get full move history.""" | |
| return self.move_history.copy() | |
| def get_last_move(self) -> Optional[Dict[str, Any]]: | |
| """Get the last move made.""" | |
| return self.move_history[-1] if self.move_history else None | |
| def get_opening_name(self) -> Optional[str]: | |
| """ | |
| Guess the opening name from move history using SAN prefix matching. | |
| Patterns are ordered longest-first so the most specific name wins. | |
| """ | |
| if len(self.move_history) < 2: | |
| return None | |
| moves = [m["move_san"] for m in self.move_history[:6]] | |
| s = " ".join(moves) | |
| # Ordered longest-to-shortest so specific lines beat generic ones | |
| patterns = [ | |
| # Sicilian variations | |
| ("e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 a6", "Sicilian Najdorf"), | |
| ("e4 c5 Nf3 Nc6 d4 cxd4 Nxd4 g6", "Sicilian Dragon"), | |
| ("e4 c5 Nf3 e6 d4 cxd4 Nxd4 Nc6", "Sicilian Scheveningen"), | |
| ("e4 c5 Nf3 d6 d4 cxd4 Nxd4 Nf6 Nc3 Nc6", "Sicilian Classical"), | |
| ("e4 c5 Nc3 Nc6 g3", "Sicilian Closed"), | |
| ("e4 c5", "Sicilian Defense"), | |
| # Open game (e4 e5) lines | |
| ("e4 e5 Nf3 Nc6 Bb5 a6", "Ruy Lopez — Morphy Defence"), | |
| ("e4 e5 Nf3 Nc6 Bb5", "Ruy Lopez"), | |
| ("e4 e5 Nf3 Nc6 Bc4 Bc5", "Giuoco Piano"), | |
| ("e4 e5 Nf3 Nc6 Bc4 Nf6", "Two Knights Defense"), | |
| ("e4 e5 Nf3 Nc6 Bc4", "Italian Game"), | |
| ("e4 e5 Nf3 Nc6 d4 exd4 Bc4", "Scotch Gambit"), | |
| ("e4 e5 Nf3 Nc6 d4", "Scotch Game"), | |
| ("e4 e5 Nf3 f5", "Latvian Gambit"), | |
| ("e4 e5 f4", "King's Gambit"), | |
| ("e4 e5 Nc3 Nc6 f4", "Vienna Gambit"), | |
| ("e4 e5 Nc3", "Vienna Game"), | |
| ("e4 e5", "Open Game"), | |
| # French and Caro | |
| ("e4 e6 d4 d5 Nc3 Bb4", "French Winawer"), | |
| ("e4 e6 d4 d5 Nc3 Nf6", "French Classical"), | |
| ("e4 e6 d4 d5 e5", "French Advance"), | |
| ("e4 e6 d4 d5 Nd2", "French Tarrasch"), | |
| ("e4 e6", "French Defense"), | |
| ("e4 c6 d4 d5 Nc3 dxe4 Nxe4 Bf5", "Caro-Kann Classical"), | |
| ("e4 c6 d4 d5 e5 Bf5", "Caro-Kann Advance"), | |
| ("e4 c6", "Caro-Kann Defense"), | |
| # Pirc / Modern | |
| ("e4 d6 d4 Nf6 Nc3 g6", "Pirc Defense"), | |
| ("e4 g6 d4 Bg7", "Modern Defense"), | |
| # e4 other | |
| ("e4 d5 exd5", "Scandinavian Defense"), | |
| ("e4 Nf6", "Alekhine's Defense"), | |
| # Queen's pawn | |
| ("d4 d5 c4 e6 Nc3 Nf6 Bg5", "Queen's Gambit Declined — Orthodox"), | |
| ("d4 d5 c4 e6 Nc3 Nf6", "Queen's Gambit Declined"), | |
| ("d4 d5 c4 dxc4", "Queen's Gambit Accepted"), | |
| ("d4 d5 c4 c6 Nf3 Nf6", "Slav Defense"), | |
| ("d4 d5 c4 c6", "Slav Defense"), | |
| ("d4 d5 c4", "Queen's Gambit"), | |
| ("d4 Nf6 c4 e6 Nc3 Bb4", "Nimzo-Indian Defense"), | |
| ("d4 Nf6 c4 e6 Nf3 b6", "Queen's Indian Defense"), | |
| ("d4 Nf6 c4 g6 Nc3 d5", "Grünfeld Defense"), | |
| ("d4 Nf6 c4 g6 Nc3 Bg7 e4", "King's Indian Defense"), | |
| ("d4 Nf6 c4 g6", "King's Indian"), | |
| ("d4 Nf6 c4 c5", "Benoni Defense"), | |
| ("d4 f5", "Dutch Defense"), | |
| ("d4 d5", "Closed Game"), | |
| ("d4", "Queen's Pawn Game"), | |
| # Flank | |
| ("c4 e5", "English Opening — Reversed Sicilian"), | |
| ("c4 Nf6 Nc3 d5", "English — Anglo-Indian"), | |
| ("c4", "English Opening"), | |
| ("Nf3 d5 c4", "Reti Opening"), | |
| ("Nf3 Nf6 c4", "Reti Opening"), | |
| ("Nf3", "King's Indian Attack"), | |
| ("g3", "King's Fianchetto Opening"), | |
| ("b3", "Nimzowitsch-Larsen Attack"), | |
| ("f4", "Bird's Opening"), | |
| ] | |
| for pattern, name in patterns: | |
| if s.startswith(pattern): | |
| return name | |
| return None | |
| def get_game_phase(self) -> str: | |
| """Estimate the game phase (opening, middlegame, endgame).""" | |
| move_count = self.get_move_count() | |
| # Count pieces on board (excluding kings) | |
| piece_count = len(self.board.piece_map()) | |
| # Endgame: very few pieces left (5 or fewer) | |
| if piece_count <= 5: | |
| return "endgame" | |
| # Opening: early in the game | |
| elif move_count < 10: | |
| return "opening" | |
| # Middlegame: beyond opening with reasonable piece count | |
| else: | |
| return "middlegame" | |
| def undo_move(self) -> Tuple[bool, Optional[str]]: | |
| """ | |
| Undo the last move. | |
| Returns: | |
| Tuple of (success, error_message) | |
| """ | |
| try: | |
| if not self.move_history: | |
| return False, "No moves to undo" | |
| self.board.pop() | |
| self.move_history.pop() | |
| logger.info("Last move undone") | |
| return True, None | |
| except Exception as e: | |
| return False, f"Error undoing move: {str(e)}" | |
| def resign(self, player: str) -> Tuple[bool, str]: | |
| """ | |
| Resign the game for a player. | |
| Args: | |
| player: Player name who is resigning | |
| Returns: | |
| Tuple of (success, result) | |
| """ | |
| if self.is_game_over(): | |
| return False, "Game is already over" | |
| if player == self.white_player: | |
| result = "0-1" # Black wins | |
| elif player == self.black_player: | |
| result = "1-0" # White wins | |
| else: | |
| return False, f"Unknown player: {player}" | |
| self.result = result | |
| self.ended_at = datetime.now() | |
| logger.info(f"{player} resigned. Result: {result}") | |
| return True, result | |
| def draw(self) -> Tuple[bool, str]: | |
| """ | |
| Accept a draw. | |
| Returns: | |
| Tuple of (success, result) | |
| """ | |
| if self.is_game_over(): | |
| return False, "Game is already over" | |
| self.result = "1/2-1/2" | |
| self.ended_at = datetime.now() | |
| logger.info("Game drawn by agreement") | |
| return True, self.result | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Convert game to dictionary for serialization.""" | |
| return { | |
| "white_player": self.white_player, | |
| "black_player": self.black_player, | |
| "fen": self.get_fen(), | |
| "move_history": self.get_move_history(), | |
| "move_count": self.get_move_count(), | |
| "current_player": self.get_current_player(), | |
| "game_status": self.get_game_status(), | |
| "opening": self.get_opening_name(), | |
| "phase": self.get_game_phase(), | |
| "created_at": self.created_at.isoformat(), | |
| "started_at": self.started_at.isoformat() if self.started_at else None, | |
| "ended_at": self.ended_at.isoformat() if self.ended_at else None, | |
| "result": self.result, | |
| } | |
| __all__ = ["ChessGame"] | |