Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -650,7 +650,7 @@ def analyze_image(file_path: str, query: str) -> str:
|
|
| 650 |
|
| 651 |
# Use Gemini Vision
|
| 652 |
vision_llm = ChatGoogleGenerativeAI(
|
| 653 |
-
model="gemini-2.0-flash
|
| 654 |
google_api_key=GOOGLE_API_KEY,
|
| 655 |
temperature=0
|
| 656 |
)
|
|
@@ -816,22 +816,22 @@ class ChessAnalysisInput(BaseModel):
|
|
| 816 |
image_path: str = Field(description="Path to chess board image file")
|
| 817 |
description: str = Field(description="Any additional context about the position (optional)", default="")
|
| 818 |
|
| 819 |
-
@tool(args_schema=ChessAnalysisInput)
|
| 820 |
def analyze_chess_position(image_path: str, description: str = "") -> str:
|
| 821 |
"""
|
| 822 |
-
Analyzes a chess position from an image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 823 |
|
| 824 |
Use this tool when:
|
| 825 |
- Question mentions chess, checkmate, or chess notation
|
| 826 |
- An image file shows a chess board
|
| 827 |
- Need to find the best move in a position
|
| 828 |
|
| 829 |
-
|
| 830 |
-
1. Extract the position from the image using Gemini Vision
|
| 831 |
-
2. Analyze it using Lichess cloud API
|
| 832 |
-
3. Return the best move in algebraic notation (e.g., "Qh5" or "Nf6")
|
| 833 |
-
|
| 834 |
-
Example: analyze_chess_position(image_path="/tmp/chess_board.png")
|
| 835 |
"""
|
| 836 |
if not image_path:
|
| 837 |
return "Error: image_path is required."
|
|
@@ -841,7 +841,7 @@ def analyze_chess_position(image_path: str, description: str = "") -> str:
|
|
| 841 |
# Find the file
|
| 842 |
chess_image = find_file(image_path)
|
| 843 |
|
| 844 |
-
# If not found via find_file, try direct path
|
| 845 |
if not chess_image and os.path.exists(image_path):
|
| 846 |
chess_image = Path(image_path)
|
| 847 |
|
|
@@ -851,188 +851,212 @@ def analyze_chess_position(image_path: str, description: str = "") -> str:
|
|
| 851 |
print(f"β Found chess image at: {chess_image}")
|
| 852 |
|
| 853 |
try:
|
| 854 |
-
#
|
|
|
|
|
|
|
| 855 |
GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 856 |
if not GOOGLE_API_KEY:
|
| 857 |
return "Error: GEMINI_API_KEY not set in Space secrets."
|
| 858 |
|
| 859 |
print("πΈ Extracting chess position from image using Gemini...")
|
| 860 |
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
)
|
| 881 |
-
|
| 882 |
-
fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
|
| 883 |
|
| 884 |
CRITICAL INSTRUCTIONS:
|
| 885 |
-
1. Carefully identify each piece
|
|
|
|
|
|
|
|
|
|
| 886 |
2. Read the board from rank 8 (top) to rank 1 (bottom), left to right
|
|
|
|
|
|
|
|
|
|
| 887 |
3. Determine whose turn it is:
|
| 888 |
-
-
|
| 889 |
-
- If
|
| 890 |
-
-
|
| 891 |
-
4. Return ONLY the FEN string in this exact format:
|
| 892 |
-
piece_placement active_color castling en_passant halfmove fullmove
|
| 893 |
|
| 894 |
-
|
|
|
|
| 895 |
|
| 896 |
-
|
| 897 |
-
- Use numbers for empty squares (1-8)
|
| 898 |
-
- Use '/' to separate ranks
|
| 899 |
-
- White pieces: UPPERCASE (K, Q, R, B, N, P)
|
| 900 |
-
- Black pieces: lowercase (k, q, r, b, n, p)
|
| 901 |
|
| 902 |
Return ONLY the FEN string, nothing else."""
|
| 903 |
-
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
-
|
| 911 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
)
|
| 913 |
-
|
| 914 |
-
response = vision_llm.invoke([message])
|
| 915 |
-
fen_raw = response.content.strip()
|
| 916 |
-
|
| 917 |
-
print(f"π Raw FEN response: {fen_raw}")
|
| 918 |
-
|
| 919 |
-
# Clean up FEN (remove markdown, explanations, etc.)
|
| 920 |
-
fen = None
|
| 921 |
-
fen_lines = fen_raw.split('\n')
|
| 922 |
-
|
| 923 |
-
for line in fen_lines:
|
| 924 |
-
line = line.strip()
|
| 925 |
-
# Remove markdown code fences
|
| 926 |
-
line = line.replace('```', '').replace('fen', '').strip()
|
| 927 |
-
|
| 928 |
-
# FEN should have '/' for ranks and spaces for components
|
| 929 |
-
if '/' in line and ' ' in line and not line.startswith('#'):
|
| 930 |
-
# Basic validation: should have pieces or numbers, slashes, and spaces
|
| 931 |
-
if any(c in line for c in 'kqrbnpKQRBNP12345678'):
|
| 932 |
-
fen = line
|
| 933 |
-
break
|
| 934 |
-
|
| 935 |
-
if not fen:
|
| 936 |
-
# Try to extract from any line with slash and space
|
| 937 |
-
for line in fen_lines:
|
| 938 |
-
if '/' in line and ' ' in line:
|
| 939 |
-
fen = line.strip()
|
| 940 |
-
break
|
| 941 |
-
|
| 942 |
-
if not fen:
|
| 943 |
-
return f"Error: Could not extract valid FEN notation from image. Gemini response: {fen_raw[:200]}"
|
| 944 |
-
|
| 945 |
-
print(f"β Extracted FEN: {fen}")
|
| 946 |
-
|
| 947 |
except Exception as e:
|
| 948 |
-
return f"Error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 949 |
|
| 950 |
-
|
| 951 |
-
|
|
|
|
|
|
|
|
|
|
| 952 |
|
|
|
|
|
|
|
|
|
|
| 953 |
try:
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
params = {
|
| 966 |
-
"fen": fen.strip(),
|
| 967 |
-
"multiPv": 1 # Get only the best move
|
| 968 |
-
}
|
| 969 |
-
|
| 970 |
-
response = requests.get(lichess_url, params=params, timeout=15)
|
| 971 |
-
|
| 972 |
-
if response.status_code != 200:
|
| 973 |
-
return f"Error: Lichess API returned status {response.status_code}. Position may not be in cloud database."
|
| 974 |
-
|
| 975 |
-
data = response.json()
|
| 976 |
-
print(f"π Lichess response: {data}")
|
| 977 |
-
|
| 978 |
-
# Extract best move
|
| 979 |
-
if "pvs" not in data or len(data["pvs"]) == 0:
|
| 980 |
-
return "Error: Position not found in Lichess cloud database. Try a different position or check if the FEN is valid."
|
| 981 |
-
|
| 982 |
-
best_pv = data["pvs"][0]
|
| 983 |
-
moves = best_pv.get("moves", "").split()
|
| 984 |
|
| 985 |
-
|
| 986 |
-
|
| 987 |
|
| 988 |
-
|
| 989 |
-
|
| 990 |
|
| 991 |
-
# Convert UCI to Standard Algebraic Notation (SAN)
|
| 992 |
-
if use_python_chess:
|
| 993 |
-
try:
|
| 994 |
-
board = chess.Board(fen)
|
| 995 |
-
uci_move = chess.Move.from_uci(best_move_uci)
|
| 996 |
-
san_move = board.san(uci_move)
|
| 997 |
-
|
| 998 |
-
# Get evaluation
|
| 999 |
-
cp = best_pv.get("cp") # centipawns
|
| 1000 |
-
mate = best_pv.get("mate") # mate in X moves
|
| 1001 |
-
|
| 1002 |
-
if mate is not None:
|
| 1003 |
-
eval_str = f" (Mate in {abs(mate)})"
|
| 1004 |
-
elif cp is not None:
|
| 1005 |
-
eval_str = f" (Eval: {cp/100:+.2f})"
|
| 1006 |
-
else:
|
| 1007 |
-
eval_str = ""
|
| 1008 |
-
|
| 1009 |
-
# Check if move leads to check/checkmate
|
| 1010 |
-
board.push(uci_move)
|
| 1011 |
-
if board.is_checkmate():
|
| 1012 |
-
check_str = " - Checkmate!"
|
| 1013 |
-
elif board.is_check():
|
| 1014 |
-
check_str = " - Check"
|
| 1015 |
-
else:
|
| 1016 |
-
check_str = ""
|
| 1017 |
-
|
| 1018 |
-
final_result = f"{san_move}{eval_str}{check_str}"
|
| 1019 |
-
print(f"β
Best move: {final_result}")
|
| 1020 |
-
return final_result
|
| 1021 |
-
|
| 1022 |
-
except Exception as e:
|
| 1023 |
-
print(f"β οΈ Could not convert to SAN: {e}")
|
| 1024 |
-
# Fall back to UCI notation
|
| 1025 |
-
return best_move_uci
|
| 1026 |
-
else:
|
| 1027 |
-
# Return UCI notation if python-chess not available
|
| 1028 |
-
return best_move_uci
|
| 1029 |
-
|
| 1030 |
-
except requests.Timeout:
|
| 1031 |
-
return "Error: Lichess API request timed out. Try again."
|
| 1032 |
-
except requests.RequestException as e:
|
| 1033 |
-
return f"Error querying Lichess API: {str(e)}"
|
| 1034 |
except Exception as e:
|
| 1035 |
-
|
|
|
|
|
|
|
| 1036 |
|
| 1037 |
except Exception as e:
|
| 1038 |
error_msg = f"Chess analysis failed: {str(e)}"
|
|
|
|
| 650 |
|
| 651 |
# Use Gemini Vision
|
| 652 |
vision_llm = ChatGoogleGenerativeAI(
|
| 653 |
+
model="gemini-2.0-flash",
|
| 654 |
google_api_key=GOOGLE_API_KEY,
|
| 655 |
temperature=0
|
| 656 |
)
|
|
|
|
| 816 |
image_path: str = Field(description="Path to chess board image file")
|
| 817 |
description: str = Field(description="Any additional context about the position (optional)", default="")
|
| 818 |
|
|
|
|
| 819 |
def analyze_chess_position(image_path: str, description: str = "") -> str:
|
| 820 |
"""
|
| 821 |
+
Analyzes a chess position from an image using Stockfish engine.
|
| 822 |
+
|
| 823 |
+
MUCH MORE RELIABLE than Lichess API because:
|
| 824 |
+
- Works offline
|
| 825 |
+
- Analyzes ANY position (not just cloud database)
|
| 826 |
+
- Stronger engine (Stockfish 16+)
|
| 827 |
+
- No rate limits or 404 errors
|
| 828 |
|
| 829 |
Use this tool when:
|
| 830 |
- Question mentions chess, checkmate, or chess notation
|
| 831 |
- An image file shows a chess board
|
| 832 |
- Need to find the best move in a position
|
| 833 |
|
| 834 |
+
Returns: Best move in algebraic notation (e.g., "Qh5", "Nf6+", "Rd5")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
"""
|
| 836 |
if not image_path:
|
| 837 |
return "Error: image_path is required."
|
|
|
|
| 841 |
# Find the file
|
| 842 |
chess_image = find_file(image_path)
|
| 843 |
|
| 844 |
+
# If not found via find_file, try direct path
|
| 845 |
if not chess_image and os.path.exists(image_path):
|
| 846 |
chess_image = Path(image_path)
|
| 847 |
|
|
|
|
| 851 |
print(f"β Found chess image at: {chess_image}")
|
| 852 |
|
| 853 |
try:
|
| 854 |
+
# ====================================================================
|
| 855 |
+
# STEP 1: Extract FEN notation from image using Gemini Vision
|
| 856 |
+
# ====================================================================
|
| 857 |
GOOGLE_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 858 |
if not GOOGLE_API_KEY:
|
| 859 |
return "Error: GEMINI_API_KEY not set in Space secrets."
|
| 860 |
|
| 861 |
print("πΈ Extracting chess position from image using Gemini...")
|
| 862 |
|
| 863 |
+
# Load and encode image
|
| 864 |
+
img = Image.open(chess_image)
|
| 865 |
+
print(f" Image loaded: {img.size}, mode: {img.mode}")
|
| 866 |
+
|
| 867 |
+
if img.mode not in ['RGB', 'RGBA']:
|
| 868 |
+
img = img.convert('RGB')
|
| 869 |
+
|
| 870 |
+
buffered = io.BytesIO()
|
| 871 |
+
img.save(buffered, format="JPEG")
|
| 872 |
+
img_base64 = base64.b64encode(buffered.getvalue()).decode()
|
| 873 |
+
|
| 874 |
+
# Use Gemini Vision to extract FEN
|
| 875 |
+
vision_llm = ChatGoogleGenerativeAI(
|
| 876 |
+
model="gemini-2.0-flash-exp",
|
| 877 |
+
google_api_key=GOOGLE_API_KEY,
|
| 878 |
+
temperature=0
|
| 879 |
+
)
|
| 880 |
+
|
| 881 |
+
fen_prompt = """Analyze this chess board image and provide the position in FEN notation.
|
|
|
|
|
|
|
|
|
|
| 882 |
|
| 883 |
CRITICAL INSTRUCTIONS:
|
| 884 |
+
1. Carefully identify each piece:
|
| 885 |
+
- White pieces (UPPERCASE): K=King, Q=Queen, R=Rook, B=Bishop, N=Knight, P=Pawn
|
| 886 |
+
- Black pieces (lowercase): k, q, r, b, n, p
|
| 887 |
+
|
| 888 |
2. Read the board from rank 8 (top) to rank 1 (bottom), left to right
|
| 889 |
+
- Use numbers (1-8) for consecutive empty squares
|
| 890 |
+
- Use '/' to separate ranks
|
| 891 |
+
|
| 892 |
3. Determine whose turn it is:
|
| 893 |
+
- Look for text like "Black to move" or "White to move"
|
| 894 |
+
- If unclear, analyze the position context
|
| 895 |
+
- Return 'w' for white, 'b' for black
|
|
|
|
|
|
|
| 896 |
|
| 897 |
+
4. Return ONLY the FEN string in this format:
|
| 898 |
+
piece_placement active_color castling en_passant halfmove fullmove
|
| 899 |
|
| 900 |
+
Example: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 901 |
|
| 902 |
Return ONLY the FEN string, nothing else."""
|
| 903 |
+
|
| 904 |
+
message = HumanMessage(
|
| 905 |
+
content=[
|
| 906 |
+
{"type": "text", "text": fen_prompt},
|
| 907 |
+
{
|
| 908 |
+
"type": "image_url",
|
| 909 |
+
"image_url": f"data:image/jpeg;base64,{img_base64}"
|
| 910 |
+
}
|
| 911 |
+
]
|
| 912 |
+
)
|
| 913 |
+
|
| 914 |
+
response = vision_llm.invoke([message])
|
| 915 |
+
fen_raw = response.content.strip()
|
| 916 |
+
print(f"π Raw FEN response: {fen_raw}")
|
| 917 |
+
|
| 918 |
+
# Clean up FEN (remove markdown, explanations, etc.)
|
| 919 |
+
fen = None
|
| 920 |
+
for line in fen_raw.split('\n'):
|
| 921 |
+
line = line.strip().replace('```', '').replace('fen', '')
|
| 922 |
+
# FEN should have '/' for ranks and spaces for components
|
| 923 |
+
if '/' in line and ' ' in line and not line.startswith('#'):
|
| 924 |
+
if any(c in line for c in 'kqrbnpKQRBNP12345678'):
|
| 925 |
+
fen = line
|
| 926 |
+
break
|
| 927 |
+
|
| 928 |
+
if not fen:
|
| 929 |
+
return f"Error: Could not extract valid FEN notation from image. Response: {fen_raw[:200]}"
|
| 930 |
+
|
| 931 |
+
print(f"β Extracted FEN: {fen}")
|
| 932 |
+
|
| 933 |
+
# ====================================================================
|
| 934 |
+
# STEP 2: Validate FEN with python-chess
|
| 935 |
+
# ====================================================================
|
| 936 |
+
try:
|
| 937 |
+
import chess
|
| 938 |
+
except ImportError:
|
| 939 |
+
return "Error: python-chess not installed. Add 'python-chess' to requirements.txt"
|
| 940 |
+
|
| 941 |
+
try:
|
| 942 |
+
board = chess.Board(fen)
|
| 943 |
+
print(f"β FEN validated successfully")
|
| 944 |
+
print(f" Turn: {'White' if board.turn else 'Black'}")
|
| 945 |
+
print(f" Legal moves: {board.legal_moves.count()}")
|
| 946 |
+
except ValueError as e:
|
| 947 |
+
return f"Error: Invalid FEN notation: {e}\nExtracted FEN: {fen}"
|
| 948 |
+
|
| 949 |
+
# ====================================================================
|
| 950 |
+
# STEP 3: Analyze with Stockfish
|
| 951 |
+
# ====================================================================
|
| 952 |
+
print("π Analyzing position with Stockfish...")
|
| 953 |
+
|
| 954 |
+
try:
|
| 955 |
+
from stockfish import Stockfish
|
| 956 |
+
except ImportError:
|
| 957 |
+
return "Error: stockfish not installed. Add 'stockfish' to requirements.txt and install Stockfish binary"
|
| 958 |
+
|
| 959 |
+
# Try to find Stockfish binary
|
| 960 |
+
stockfish_paths = [
|
| 961 |
+
"/usr/games/stockfish", # Linux (apt-get install)
|
| 962 |
+
"/usr/local/bin/stockfish", # Mac (brew install)
|
| 963 |
+
"/usr/bin/stockfish", # Alternative Linux
|
| 964 |
+
"stockfish", # In PATH
|
| 965 |
+
"./stockfish", # Local directory
|
| 966 |
+
"C:\\Program Files\\stockfish\\stockfish.exe" # Windows
|
| 967 |
+
]
|
| 968 |
+
|
| 969 |
+
stockfish_path = None
|
| 970 |
+
for path in stockfish_paths:
|
| 971 |
+
if os.path.exists(path) or os.path.isfile(path):
|
| 972 |
+
stockfish_path = path
|
| 973 |
+
break
|
| 974 |
+
|
| 975 |
+
if not stockfish_path:
|
| 976 |
+
# Try running 'which stockfish' on Unix systems
|
| 977 |
+
try:
|
| 978 |
+
import subprocess
|
| 979 |
+
result = subprocess.run(['which', 'stockfish'],
|
| 980 |
+
capture_output=True,
|
| 981 |
+
text=True,
|
| 982 |
+
timeout=5)
|
| 983 |
+
if result.returncode == 0:
|
| 984 |
+
stockfish_path = result.stdout.strip()
|
| 985 |
+
except:
|
| 986 |
+
pass
|
| 987 |
+
|
| 988 |
+
if not stockfish_path:
|
| 989 |
+
return """Error: Stockfish binary not found. Install it:
|
| 990 |
+
- Linux: sudo apt-get install stockfish
|
| 991 |
+
- Mac: brew install stockfish
|
| 992 |
+
- Windows: Download from stockfishchess.org
|
| 993 |
+
Or set the path manually in the code."""
|
| 994 |
+
|
| 995 |
+
print(f"β Found Stockfish at: {stockfish_path}")
|
| 996 |
+
|
| 997 |
+
# Initialize Stockfish
|
| 998 |
+
try:
|
| 999 |
+
stockfish = Stockfish(
|
| 1000 |
+
path=stockfish_path,
|
| 1001 |
+
depth=20, # Analysis depth (higher = stronger but slower)
|
| 1002 |
+
parameters={
|
| 1003 |
+
"Threads": 2,
|
| 1004 |
+
"Minimum Thinking Time": 500, # milliseconds
|
| 1005 |
+
"Hash": 512, # MB of RAM
|
| 1006 |
+
}
|
| 1007 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
except Exception as e:
|
| 1009 |
+
return f"Error initializing Stockfish: {e}"
|
| 1010 |
+
|
| 1011 |
+
# Set position
|
| 1012 |
+
stockfish.set_fen_position(fen)
|
| 1013 |
+
|
| 1014 |
+
# Get best move
|
| 1015 |
+
print(" Computing best move...")
|
| 1016 |
+
best_move_uci = stockfish.get_best_move()
|
| 1017 |
+
|
| 1018 |
+
if not best_move_uci:
|
| 1019 |
+
return "Error: Stockfish could not find a legal move. Check if position is valid."
|
| 1020 |
+
|
| 1021 |
+
print(f"π― Best move (UCI): {best_move_uci}")
|
| 1022 |
+
|
| 1023 |
+
# Get evaluation
|
| 1024 |
+
evaluation = stockfish.get_evaluation()
|
| 1025 |
+
eval_type = evaluation.get("type", "cp")
|
| 1026 |
+
eval_value = evaluation.get("value", 0)
|
| 1027 |
|
| 1028 |
+
if eval_type == "mate":
|
| 1029 |
+
eval_str = f" (Mate in {abs(eval_value)})"
|
| 1030 |
+
else:
|
| 1031 |
+
# Centipawns to pawns
|
| 1032 |
+
eval_str = f" (Eval: {eval_value/100:+.2f})"
|
| 1033 |
|
| 1034 |
+
# ====================================================================
|
| 1035 |
+
# STEP 4: Convert UCI to Standard Algebraic Notation (SAN)
|
| 1036 |
+
# ====================================================================
|
| 1037 |
try:
|
| 1038 |
+
uci_move = chess.Move.from_uci(best_move_uci)
|
| 1039 |
+
san_move = board.san(uci_move)
|
| 1040 |
+
|
| 1041 |
+
# Check if move leads to check/checkmate
|
| 1042 |
+
board.push(uci_move)
|
| 1043 |
+
if board.is_checkmate():
|
| 1044 |
+
check_str = " - Checkmate!"
|
| 1045 |
+
elif board.is_check():
|
| 1046 |
+
check_str = " - Check"
|
| 1047 |
+
else:
|
| 1048 |
+
check_str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1049 |
|
| 1050 |
+
final_result = f"{san_move}{eval_str}{check_str}"
|
| 1051 |
+
print(f"β
Best move: {final_result}")
|
| 1052 |
|
| 1053 |
+
# Return JUST the move notation for clean submission
|
| 1054 |
+
return san_move
|
| 1055 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
except Exception as e:
|
| 1057 |
+
print(f"β οΈ Could not convert to SAN: {e}")
|
| 1058 |
+
# Fall back to UCI notation
|
| 1059 |
+
return best_move_uci
|
| 1060 |
|
| 1061 |
except Exception as e:
|
| 1062 |
error_msg = f"Chess analysis failed: {str(e)}"
|