"""
"""
import gradio as gr
import chess
import chess.svg
import chess.pgn
import re
import torch
import os
import io
import math
from typing import Optional, Tuple, List
import traceback
from datetime import datetime
from utils import Engine, ChessformerConfig, StockfishConfig, UCI_MOVE_TO_IDX
from model import ChessFormerModel
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file, load_model
import subprocess
# Stockfish should be available after packages.txt installation
def get_stockfish_path():
"""Get Stockfish path after system installation"""
try:
result = subprocess.run(['which', 'stockfish'], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip()
else:
# Fallback paths where Stockfish might be installed
possible_paths = [
'/usr/bin/stockfish',
'/usr/local/bin/stockfish',
'/opt/stockfish/stockfish',
'/usr/games/stockfish'
]
for path in possible_paths:
if os.path.exists(path):
return path
return None
except Exception as e:
print(f"Error finding Stockfish: {e}")
return None
STOCKFISH_PATH = get_stockfish_path()
class ChessApp:
def __init__(self):
self.board = chess.Board()
self.move_history = []
self.current_engine = None
self.analysis_engine = None
self.game_over = False
self.user_color = chess.WHITE
self.models = {}
self.device = torch.device("cpu")
self.current_engine_eval = 0.0
self.stockfish_eval = 0.0
self.load_models()
# Only create analysis engine if Stockfish is available
if STOCKFISH_PATH:
self.create_analysis_engine()
print(f"Stockfish found at: {STOCKFISH_PATH}")
else:
print("Warning: Stockfish not found. Analysis features will be limited.")
def load_models(self):
"""Load models on CPU only"""
model_paths = {
"ChessFormer-SL": "kaupane/ChessFormer-SL",
"ChessFormer-RL": "kaupane/ChessFormer-RL"
}
for name, repo_id in model_paths.items():
try:
print(f"Loading {name} from {repo_id}...")
try:
config_path = hf_hub_download(repo_id=repo_id,filename="config.json")
import json
with open(config_path,'r') as f:
config = json.load(f)
except:
config = {
"num_blocks": 20,
"hidden_size": 640,
"intermediate_size": 1728,
"num_heads": 8,
"dropout": 0.00,
"possible_moves": 1969,
"dtype": "float32"
}
model = ChessFormerModel(**config)
model_path = hf_hub_download(repo_id=repo_id,filename="model.safetensors")
load_model(model,model_path)
model.to(self.device)
model.eval()
self.models[name] = model
print(f"Successfully loaded {name}.")
except Exception as e:
print(f"Failed to load {name} from HuggingFace Hub: {e}.")
import traceback
traceback.print_exc()
def get_depth_limits(self, engine_type: str) -> Tuple[int,int]:
if engine_type == "Stockfish":
return 1,18,6
else:
return 0,6,0
def create_evaluation_bar(self, eval_score: float, title: str) -> str:
"""Create HTML evaluation bar from user's perspective with page-matching colors"""
# Convert eval_score from white's perspective to user's perspective
user_eval = eval_score if self.user_color == chess.WHITE else -eval_score
# Clamp evaluation between -1 and 1 for display
clamped_eval = max(-1.0, min(1.0, user_eval))
# Convert to percentage (0 = user losing, 100 = user winning)
percentage = (clamped_eval + 1.0) / 2.0 * 100
# Format evaluation text from user's perspective
eval_text = f"{clamped_eval:+.2f}"
# Determine advantage text and colors (matching page theme)
if user_eval > 0.5:
advantage_text = "WINNING"
text_color = "#1e40af" # Blue-700
indicator_color = "#3b82f6" # Blue-500
elif user_eval > 0.1:
advantage_text = "SLIGHT ADVANTAGE"
text_color = "#1e40af"
indicator_color = "#60a5fa" # Blue-400
elif user_eval < -0.5:
advantage_text = "LOSING"
text_color = "#7c2d12" # Orange-800 (more muted than red)
indicator_color = "#ea580c" # Orange-600
elif user_eval < -0.1:
advantage_text = "SLIGHT DISADVANTAGE"
text_color = "#9a3412" # Orange-700
indicator_color = "#f97316" # Orange-500
else:
advantage_text = "EQUAL POSITION"
text_color = "#4b5563" # Gray-600
indicator_color = "#6b7280" # Gray-500
return f"""
{title}
{eval_text}
{advantage_text}
"""
def create_analysis_engine(self):
"""Create optimized Stockfish depth 20 engine for analysis"""
try:
config = StockfishConfig(
engine_path=STOCKFISH_PATH,
depth=20
)
self.analysis_engine = Engine(type="stockfish", stockfish_config=config)
print("Analysis engine (Stockfish depth 20) created successfully")
except Exception as e:
print(f"Failed to create analysis engine: {e}")
self.analysis_engine = None
def update_evaluations(self):
"""Update evaluations from both engines with optimized Stockfish analysis"""
# Get current engine evaluation
if self.current_engine:
try:
self.current_engine_eval = self.current_engine.analyze_position(self.board.copy())
if self.current_engine_eval is None:
self.current_engine_eval = 0.0
except:
self.current_engine_eval = 0.0
# Get optimized Stockfish analysis
if self.analysis_engine:
try:
self.stockfish_eval = self.analysis_engine.analyze_position(self.board.copy())
if self.stockfish_eval is None:
self.stockfish_eval = 0.0
except:
self.stockfish_eval = 0.0
def create_engine(self, engine_type: str, depth: int, temperature: float=0.5) -> Optional[Engine]:
if engine_type == "Stockfish":
if not STOCKFISH_PATH:
print("Stockfish not available")
return None
config = StockfishConfig(
engine_path=STOCKFISH_PATH,
depth=depth
)
return Engine(type="stockfish", stockfish_config=config)
elif engine_type in self.models:
config = ChessformerConfig(
chessformer=self.models[engine_type],
device=self.device,
temperature=temperature,
depth=depth if depth > 0 else 0,
top_k=8,
decay_rate=0.6,
max_batch_size=800
)
return Engine(type="chessformer", chessformer_config=config)
return None
def parse_move(self, move_str: str) -> Optional[chess.Move]:
"""Parse move input in either UCI format ("e2e4") or algebraic notation ("Ne5")"""
if not move_str:
return None
move_str = move_str.strip()
# Try UCI format first
uci_pattern = r'^[a-h][1-8][a-h][1-8][qrbn]?$'
if re.match(uci_pattern,move_str.lower()):
try:
return chess.Move.from_uci(move_str.lower())
except ValueError:
pass
# Try algrebraic notation
try:
return self.board.parse_san(move_str)
except ValueError:
pass
return None
def get_board_svg(self) -> str:
"""Generate SVG representation of the chess board"""
flip = (self.user_color == chess.BLACK)
lastmove = None
if self.move_history:
lastmove = self.move_history[-1]
svg = chess.svg.board(
board=self.board,
flipped=flip,
lastmove=lastmove,
size=600
)
return svg
def get_move_history_text(self) -> str:
"""Generate move history in PGN format"""
try:
game = chess.pgn.Game()
game.headers["Event"] = "ChessFormer Demo"
game.headers["Date"] = datetime.now().strftime("%Y.%m.%d")
game.headers["White"] = "You" if self.user_color == chess.WHITE else "Engine"
game.headers["Black"] = "Engine" if self.user_color == chess.WHITE else "You"
node = game
temp_board = chess.Board()
for move in self.move_history:
node = node.add_variation(move)
temp_board.push(move)
if self.game_over:
outcome = self.board.outcome()
if outcome:
if outcome.winner == chess.WHITE:
game.headers["Result"] = "1-0"
elif outcome.winner == chess.BLACK:
game.headers["Result"] = "0-1"
else:
game.headers["Result"] = "1/2-1/2"
else:
game.headers["Result"] = "*"
else:
game.headers["Result"] = "*"
return str(game)
except Exception as e:
print(f"Error generating move history: {e}")
return "Move history unavailable"
def export_pgn(self) -> str:
return self.get_move_history_text()
def import_fen(self, fen: str) -> Tuple[str,str,str,str,str]:
try:
test_board = chess.Board(fen.strip())
self.board = test_board
self.move_history = []
self.game_over = False
self.update_evaluations()
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Position loaded from FEN: {fen}",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
except Exception as e:
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Invalid FEN: {str(e)}",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
def import_pgn(self, pgn_text: str) -> Tuple[str,str,str,str,str]:
try:
pgn_io = io.StringIO(pgn_text.strip())
game = chess.pgn.read_game(pgn_io)
if game is None:
raise ValueError("Could not parse PGN")
self.board = game.board()
self.move_history = []
for move in game.mainline_moves():
self.board.push(move)
self.move_history.append(move)
self.game_over = self.board.is_game_over()
self.update_evaluations()
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Game loaded from PGN ({len(self.move_history)} moves)",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
except Exception as e:
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Invalid PGN: {str(e)}",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
def make_user_move(self, move_str: str) -> Tuple[str,str,str,str,str,str]:
if self.game_over:
return (
self.get_board_svg(),
self.get_move_history_text(),
"Game is over. Click 'New Game' to start a new game.",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
if self.board.turn != self.user_color:
return (
self.get_board_svg(),
self.get_move_history_text(),
"It's not your turn now!",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
move = self.parse_move(move_str)
if move is None:
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Invalid move: '{move_str}'. Try formats like 'e2e4' or 'Ne5'",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
if move not in self.board.legal_moves:
return (
self.get_board_svg(),
self.get_move_history_text(),
f"Illegal move: '{move_str}'",
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
self.board.push(move)
self.move_history.append(move)
self.update_evaluations()
if self.board.is_game_over():
self.game_over = True
outcome = self.board.outcome()
if outcome:
if outcome.winner == self.user_color:
status = "šš CONGRATULATIONS! YOU WON! šš"
status += f"\nšÆ Victory by {outcome.termination.name}! šÆ"
elif outcome.winner is None:
status = "š¤ GAME DRAWN š¤"
status += f"\nāļø Draw by {outcome.termination.name} āļø"
else:
status = "š YOU LOST š"
status += f"\nš Defeated by {outcome.termination.name} š"
else:
status = "š GAME OVER š"
return (
self.get_board_svg(),
self.get_move_history_text(),
status,
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
# Get engine move
try:
engine_move_uci, engine_value = self.current_engine.move(self.board)
if engine_move_uci == "":
self.game_over = True
status = "Engine claimed a draw."
else:
engine_move = chess.Move.from_uci(engine_move_uci)
self.board.push(engine_move)
self.move_history.append(engine_move)
if self.board.is_game_over():
self.game_over = True
outcome = self.board.outcome()
if outcome:
if outcome.winner == self.user_color:
status = "šš CONGRATULATIONS! YOU WON! šš"
status += f"\nšÆ Victory by {outcome.termination.name}! šÆ"
elif outcome.winner is None:
status = "š¤ GAME DRAWN š¤"
status += f"\nāļø Draw by {outcome.termination.name} āļø"
else:
status = "š YOU LOST š"
status += f"\nš Defeated by {outcome.termination.name} š"
else:
status = "š GAME OVER š"
else:
status = f"Engine played: {engine_move.uci()}."
except Exception as e:
status = f"Engine error: {str(e)}"
print(f"Engine error: {e}")
traceback.print_exc()
return (
self.get_board_svg(),
self.get_move_history_text(),
status,
"", # clear input
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
def new_game(self, engine_type: str, depth: int, color: str, temperature: float) -> Tuple[str,str,str,str,str,str]:
"Start a new game"
self.board = chess.Board()
self.move_history = []
self.game_over = False
self.user_color = chess.WHITE if color == "White" else chess.BLACK
# Create new engine
self.current_engine = self.create_engine(engine_type, depth, temperature)
self.update_evaluations()
if self.current_engine is None:
status = f"Failed to create {engine_type} engine."
else:
status = f"New game started! You are playing {color} against {engine_type} (depth {depth})."
# If user is black, make engine move first
if self.user_color == chess.BLACK:
try:
engine_move_uci, engine_value = self.current_engine.move(self.board)
if engine_move_uci != "":
engine_move = chess.Move.from_uci(engine_move_uci)
self.board.push(engine_move)
self.move_history.append(engine_move)
status += f" Engine opened with: {engine_move.uci()}"
except Exception as e:
status += f" Engine error on first move: {str(e)}"
return (
self.get_board_svg(),
self.get_move_history_text(),
status,
"",
self.create_evaluation_bar(self.stockfish_eval, "Stockfish Analysis (from your perspective)"),
self.create_evaluation_bar(self.current_engine_eval, "Engine Analysis (from your perspective)")
)
app = ChessApp()
def create_interface():
"""Create the Gradio interface with improved layout"""
with gr.Blocks(title="ChessFormer Demo", theme=gr.themes.Soft()) as interface:
gr.Markdown("# š ChessFormer Demo")
gr.Markdown("Play chess against ChessFormer models or Stockfish!")
with gr.Row():
# Left column - Analysis + History
with gr.Column(scale=1):
gr.Markdown("### š Position Analysis")
# Stockfish Analysis
stockfish_eval_display = gr.HTML(
value=app.create_evaluation_bar(0.0, "Stockfish Analysis"),
label="Stockfish"
)
# Current Engine Analysis
current_engine_eval_display = gr.HTML(
value=app.create_evaluation_bar(0.0, "Engine Analysis"),
label="Engine"
)
# Move history
gr.Markdown("### š Game History")
history_display = gr.Textbox(
value=app.get_move_history_text(),
label="PGN",
lines=12,
max_lines=15,
interactive=False
)
# Middle column - Game Board + Controls
with gr.Column(scale=4):
# Chess board display
board_display = gr.HTML(
value=app.get_board_svg(),
label="Chess Board"
)
# Move input
with gr.Row():
move_input = gr.Textbox(
placeholder="Enter move (e.g., 'e2e4' or 'Ne5')",
label="Your Move",
scale=4
)
move_button = gr.Button("Make Move", variant="primary", scale=1)
# Game status
status_display = gr.Textbox(
value="Click 'New Game' to start playing!",
label="Game Status",
interactive=False,
lines=2
)
# Right column - Settings + Import/Export
with gr.Column(scale=2):
# Engine settings
gr.Markdown("### āļø Game Settings")
engine_choices = ["Stockfish"] + list(app.models.keys())
engine_select = gr.Dropdown(
choices=engine_choices,
value="ChessFormer-RL" if engine_choices else None,
label="Opponent Engine"
)
depth_slider = gr.Slider(
minimum=0,
maximum=6,
value=2,
step=1,
label="Engine Depth"
)
color_select = gr.Radio(
choices=["White", "Black"],
value="White",
label="Your Color"
)
temperature_slider = gr.Slider(
minimum=0.1,
maximum=2.0,
value=0.5,
step=0.1,
label="Temperature (ChessFormer only)"
)
new_game_button = gr.Button("š New Game", variant="secondary", size="lg")
# Import/Export section
gr.Markdown("### š Import/Export")
with gr.Tabs():
with gr.Tab("Import FEN"):
fen_input = gr.Textbox(
placeholder="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
label="FEN String",
lines=2
)
import_fen_button = gr.Button("Import FEN")
with gr.Tab("Import PGN"):
pgn_input = gr.Textbox(
placeholder="1. e4 e5 2. Nf3 Nc6...",
label="PGN Text",
lines=3
)
import_pgn_button = gr.Button("Import PGN")
with gr.Tab("Export"):
export_button = gr.Button("š Download PGN")
export_output = gr.File(label="Download")
# Available models info
gr.Markdown("### š¤ Available Models")
if app.models:
model_info = "**Loaded ChessFormer models:**\n" + "\n".join([f"⢠{name}" for name in app.models.keys()])
else:
model_info = "ā ļø No ChessFormer models found. Make sure model checkpoints exist in Huggingface Hub."
gr.Markdown(model_info)
# Function to update depth limits based on engine selection
def update_depth_limits(engine_type):
min_depth, max_depth, value = app.get_depth_limits(engine_type)
return gr.Slider(minimum=min_depth, maximum=max_depth, value=value, step=1)
# Function to export PGN
def export_pgn_file():
pgn_content = app.export_pgn()
filename = f"chess_game_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pgn"
with open(filename, 'w') as f:
f.write(pgn_content)
return filename
# Event handlers (same as before...)
engine_select.change(
fn=update_depth_limits,
inputs=[engine_select],
outputs=[depth_slider]
)
move_button.click(
fn=app.make_user_move,
inputs=[move_input],
outputs=[board_display, history_display, status_display, move_input,
stockfish_eval_display, current_engine_eval_display]
)
move_input.submit(
fn=app.make_user_move,
inputs=[move_input],
outputs=[board_display, history_display, status_display, move_input,
stockfish_eval_display, current_engine_eval_display]
)
new_game_button.click(
fn=app.new_game,
inputs=[engine_select, depth_slider, color_select, temperature_slider],
outputs=[board_display, history_display, status_display, move_input,
stockfish_eval_display, current_engine_eval_display]
)
import_fen_button.click(
fn=app.import_fen,
inputs=[fen_input],
outputs=[board_display, history_display, status_display, fen_input,
stockfish_eval_display, current_engine_eval_display]
)
import_pgn_button.click(
fn=app.import_pgn,
inputs=[pgn_input],
outputs=[board_display, history_display, status_display, pgn_input,
stockfish_eval_display, current_engine_eval_display]
)
export_button.click(
fn=export_pgn_file,
outputs=[export_output]
)
# Auto-start a new game when interface loads
interface.load(
fn=app.new_game,
inputs=[gr.State("ChessFormer-RL"), gr.State(2), gr.State("White"), gr.State(0.5)],
outputs=[board_display, history_display, status_display, move_input,
stockfish_eval_display, current_engine_eval_display]
)
return interface
if __name__ == "__main__":
# Create and launch interface
interface = create_interface()
interface.launch()