czakop's picture
update get_top_moves tool
0775bfb
from collections import Counter
import chess
import gradio as gr
import pandas as pd
from gradio_chessboard import Chessboard
def get_position(fen: str) -> dict:
"""
Describe the current chess position from a FEN string, plus a material summary.
Attempts to classify the opening, and if successful, adds the opening information to the position.
Otherwise, it adds a piece map with the current pieces and the list of legal moves.
Args:
fen (str): The FEN string representing the chess position.
"""
board = chess.Board(fen)
position = {
"turn": _get_color_name(board.turn),
"castling": {
"white": {
"kingside": board.has_kingside_castling_rights(chess.WHITE),
"queenside": board.has_queenside_castling_rights(chess.WHITE),
},
"black": {
"kingside": board.has_kingside_castling_rights(chess.BLACK),
"queenside": board.has_queenside_castling_rights(chess.BLACK),
},
},
"en_passant": chess.square_name(board.ep_square) if board.ep_square else None,
"mate": board.is_checkmate(),
"stalemate": board.is_stalemate(),
}
opening = classify_opening(board.fen())
if "error" not in opening:
# If the opening classification was successful, add it to the position
position["opening"] = opening
elif board.fen() == board.starting_fen:
# If the position is the starting position, add a default opening
position["opening"] = {"name": "Starting Position"}
else:
# If there was an error, just add a piece map (potentionally with fewer pieces)
position["pieces"] = (
[
f"{chess.square_name(s)}: {_get_color_name(p.color)} {chess.piece_name(p.piece_type)}"
for s, p in board.piece_map().items()
],
)
position["legal_moves"] = ([move.uci() for move in board.legal_moves],)
white_counts = Counter(
piece.piece_type
for square, piece in board.piece_map().items()
if piece.color == chess.WHITE
)
black_counts = Counter(
piece.piece_type
for square, piece in board.piece_map().items()
if piece.color == chess.BLACK
)
def format_counts(counter):
order = [chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT, chess.PAWN]
symbol_map = {
chess.QUEEN: "Q",
chess.ROOK: "R",
chess.BISHOP: "B",
chess.KNIGHT: "N",
chess.PAWN: "P",
}
parts = []
for p_type in order:
cnt = counter.get(p_type, 0)
parts.append(f"{symbol_map[p_type]}={cnt}")
return ", ".join(parts)
material_count = {
"white": format_counts(white_counts),
"black": format_counts(black_counts),
}
diff = {
p_type: white_counts.get(p_type, 0) - black_counts.get(p_type, 0)
for p_type in (chess.QUEEN, chess.ROOK, chess.BISHOP, chess.KNIGHT, chess.PAWN)
}
white_adv = [(ptype, diff[ptype]) for ptype in diff if diff[ptype] > 0]
black_adv = [(ptype, -diff[ptype]) for ptype in diff if diff[ptype] < 0]
def summarize_advantages(side_name, adv_list):
"""
adv_list: list of tuples (piece_type, count), count > 0
Returns phrases like "1 rook and 2 pawns"
"""
if not adv_list:
return ""
piece_names = {
chess.QUEEN: "queen",
chess.ROOK: "rook",
chess.BISHOP: "bishop",
chess.KNIGHT: "knight",
chess.PAWN: "pawn",
}
parts = []
for ptype, cnt in adv_list:
name = piece_names[ptype]
# pluralize
if cnt > 1:
name += "s"
parts.append(f"{cnt} {name}")
# join with " and "
joined = " and ".join(parts)
return f"{side_name} is up {joined}"
white_summary = summarize_advantages("White", white_adv)
black_summary = summarize_advantages("Black", black_adv)
if white_summary and black_summary:
# If both sides have something (e.g. piece‐for‐pawn imbalances), combine
imbalance = f"Mixed: {white_summary}; {black_summary}"
elif white_summary:
imbalance = white_summary
elif black_summary:
imbalance = black_summary
else:
imbalance = "Material is equal"
position["material_count"] = material_count
position["imbalance"] = imbalance
return position
def get_square_info(fen: str, square_name: str) -> dict:
"""Get information about a specific square in the chess position.
This function retrieves the piece on the specified square, as well as the attackers and defenders of that square.
Args:
fen (str): The FEN string representing the chess position.
square_name (str): The name of the square (e.g., 'e4').
"""
board = chess.Board(fen)
square = chess.parse_square(square_name)
return {
"square": square_name,
"piece": _get_piece_info_on_square(board, square),
"attackers/defenders": [
_get_attackers(board, square, color) for color in (chess.WHITE, chess.BLACK)
],
}
def get_top_moves(fen: str, top_n: int = 5) -> dict:
"""Get the top N moves for a given chess position using StockFish.
Returns a list of the top moves with their absolute scores (in centipawns) and whether they are leading to a mate.
DISCLAIMER: This function uses the Stockfish chess engine, ONLY use it if explicitly allowed.
Args:
fen (str): The FEN string representing the chess position.
top_n (int): The number of top moves to return.
"""
import chess.engine
board = chess.Board(fen)
with chess.engine.SimpleEngine.popen_uci("/usr/games/stockfish") as engine:
info = engine.analyse(board, chess.engine.Limit(time=2.0), multipv=top_n)
top_moves = [
{
"move": move["pv"][0].uci(),
"score": move["score"].white().score(),
"mate": move["score"].is_mate(),
}
for move in info
]
return {"top_moves": top_moves}
def analyze_pawn_structure(fen):
"""
Analyze pawn‐structure features for both White and Black from a given FEN string.
Args:
fen (str): The FEN string representing the chess position.
"""
board = chess.Board(fen)
white_pawns = list(board.pieces(chess.PAWN, chess.WHITE))
black_pawns = list(board.pieces(chess.PAWN, chess.BLACK))
def pawn_islands_and_doubles(pawn_squares):
"""
Given a list of pawn squares (for one color), compute:
- num_islands: how many contiguous runs of files have at least one pawn
- doubled_files: [file_letters ...] where there are 2+ pawns on that file
- files_with_pawns: set of file indices that have ≥1 pawn
- file_to_count: dict mapping file→count_of_pawns
"""
file_counts = {}
for sq in pawn_squares:
f = chess.square_file(sq)
file_counts[f] = file_counts.get(f, 0) + 1
files_with_pawns = set(file_counts.keys())
# Count how many contiguous runs of True in an 8‐long boolean array
num_islands = 0
in_run = False
for f in range(8):
if f in files_with_pawns:
if not in_run:
num_islands += 1
in_run = True
else:
in_run = False
doubled_files = [
chess.FILE_NAMES[f] for f, cnt in file_counts.items() if cnt > 1
]
return num_islands, doubled_files, files_with_pawns, file_counts
# White: islands, doubled, and helper sets
w_islands, w_doubled, w_files, w_file_count = pawn_islands_and_doubles(white_pawns)
# Black: same
b_islands, b_doubled, b_files, b_file_count = pawn_islands_and_doubles(black_pawns)
# 2) Isolated pawns: a pawn whose file f has no friendly pawn on f-1 or f+1
def find_isolated(pawn_sqs, files_with, color):
"""
Returns [square_name ...] where each pawn is isolated:
- its file f has no friendly pawn on f-1 or f+1.
"""
isolated = []
for sq in pawn_sqs:
f = chess.square_file(sq)
# check adjacent files
if (f - 1) not in files_with and (f + 1) not in files_with:
isolated.append(chess.square_name(sq))
return isolated
w_isolated = find_isolated(white_pawns, w_files, chess.WHITE)
b_isolated = find_isolated(black_pawns, b_files, chess.BLACK)
# 3) Passed pawns: a pawn with no enemy pawn ahead of it on same or adjacent file
def find_passed(pawn_sqs, enemy_sqs, is_white):
"""
For each pawn of 'is_white' color:
- Let (f,r) be its file and rank index (0..7), where r=0 means rank 1, r=7 means rank 8.
- If is_white: check enemy pawns on files f-1,f,f+1 with rank_index > r. If none, it's passed.
- If black: check enemy pawns on files f-1,f,f+1 with rank_index < r. If none, it's passed.
"""
passed = []
# Pre‐compute enemy file/rank for quick checks
enemy_positions = [
(chess.square_file(e), chess.square_rank(e)) for e in enemy_sqs
]
for sq in pawn_sqs:
f = chess.square_file(sq)
r = chess.square_rank(sq)
is_passed = True
for ef, er in enemy_positions:
if abs(ef - f) <= 1:
if is_white:
if er > r:
# an enemy pawn is “in front” on same/adjacent file
is_passed = False
break
else:
if er < r:
is_passed = False
break
if is_passed:
passed.append(chess.square_name(sq))
return passed
w_passed = find_passed(white_pawns, black_pawns, True)
b_passed = find_passed(black_pawns, white_pawns, False)
# 4) Backward pawns: heuristic:
# - No friendly pawn on adjacent file with rank ≤ r
# - The square in front is either occupied or attacked by an enemy pawn
def find_backward(pawn_sqs, friend_sqs, enemy_sqs, is_white):
"""
For each pawn sq of this color:
- Let f,r be its file/rank
- Condition A: No friendly pawn on file f-1 or f+1 with rank ≤ r (for white) or ≥ r (for black)
- Condition B: The square in front (r+1 for white; r-1 for black) is either occupied or attacked by an enemy pawn
- If both hold → mark as backward.
"""
backward = []
friend_pos = [
(chess.square_file(fsq), chess.square_rank(fsq)) for fsq in friend_sqs
]
enemy_pawn_positions = set(enemy_sqs) # for quick “occupied‐by‐pawn” checks
for sq in pawn_sqs:
f = chess.square_file(sq)
r = chess.square_rank(sq)
# 4A) no friendly adjacent “supporter”
has_support = False
for ff, rr in friend_pos:
if abs(ff - f) == 1:
if is_white:
if rr <= r:
has_support = True
break
else:
if rr >= r:
has_support = True
break
if has_support:
continue # NOT backward if there is a supporting pawn
# 4B) check the square in front
if is_white:
if r == 7:
continue # already on rank 8 → can’t be “backward” in the usual sense
front_sq = chess.square(f, r + 1)
else:
if r == 0:
continue
front_sq = chess.square(f, r - 1)
# If front‐square is occupied by any piece OR attacked by an enemy pawn → block
if board.piece_at(front_sq) is not None:
blocked = True
else:
# attacked by an enemy pawn?
attackers = board.attackers(
chess.BLACK if is_white else chess.WHITE, front_sq
)
# see if any of those attackers is an enemy pawn:
attacked_by_pawn = False
for attacker_sq in attackers:
p = board.piece_at(attacker_sq)
if (
p is not None
and p.piece_type == chess.PAWN
and p.color != board.piece_at(sq).color
):
attacked_by_pawn = True
break
blocked = attacked_by_pawn
if blocked:
backward.append(chess.square_name(sq))
return backward
w_backward = find_backward(white_pawns, white_pawns, black_pawns, True)
b_backward = find_backward(black_pawns, black_pawns, white_pawns, False)
# 5) Potential break squares:
# For each pawn of a side, if front‐square is empty and there is an enemy pawn diagonally ahead,
# then that front‐square is a “break point” where advancing would challenge the enemy pawn.
def find_break_sqs(pawn_sqs, is_white):
"""
For each pawn sq:
- Compute front = (f, r+1) if white; (f, r-1) if black
- If front is on board, empty, and has an enemy pawn on one of its diagonals, add front.
"""
breaks = set()
for sq in pawn_sqs:
f = chess.square_file(sq)
r = chess.square_rank(sq)
if is_white and r == 7:
continue
if not is_white and r == 0:
continue
if is_white:
front = chess.square(f, r + 1)
# diagonals at (f-1, r+1) and (f+1, r+1)
diag1 = chess.square(f - 1, r + 1) if f > 0 else None
diag2 = chess.square(f + 1, r + 1) if f < 7 else None
enemy_color = chess.BLACK
else:
front = chess.square(f, r - 1)
diag1 = chess.square(f - 1, r - 1) if f > 0 else None
diag2 = chess.square(f + 1, r - 1) if f < 7 else None
enemy_color = chess.WHITE
# Must be empty to “break” into
if board.piece_at(front) is not None:
continue
# If any diagonal contains an enemy pawn, mark front as break square
for diag in (diag1, diag2):
if diag is not None:
piece = board.piece_at(diag)
if (
piece is not None
and piece.piece_type == chess.PAWN
and piece.color == enemy_color
):
breaks.add(front)
break
return [chess.square_name(sq) for sq in sorted(breaks)]
w_breaks = find_break_sqs(white_pawns, True)
b_breaks = find_break_sqs(black_pawns, False)
# Assemble final result
return {
"pawn_islands": {"white": w_islands, "black": b_islands},
"doubled_pawns": {"white": w_doubled, "black": b_doubled},
"isolated_pawns": {"white": w_isolated, "black": b_isolated},
"passed_pawns": {"white": w_passed, "black": b_passed},
"backward_pawns": {"white": w_backward, "black": b_backward},
"break_squares": {"white": w_breaks, "black": b_breaks},
}
def analyze_tactical_patterns(fen):
"""
Analyze immediate tactical patterns from a given FEN string.
This function detects:
- Potential knight forks and double attacks (refering to next move)
- Pins, skewers, discovered attacks and x‐ray attacks in the current position.
Args:
fen (str): The FEN string representing the chess position.
"""
board = chess.Board(fen)
piece_name = {
chess.PAWN: "pawn",
chess.KNIGHT: "knight",
chess.BISHOP: "bishop",
chess.ROOK: "rook",
chess.QUEEN: "queen",
chess.KING: "king",
}
def find_forks_and_double_attacks(color):
"""
For each legal move by 'color', detect:
- Knight forks: moved knight attacks ≥2 enemy pieces
- Double attacks: moved non-knight piece attacks ≥2 enemy pieces
Returns two lists of descriptive strings.
"""
forks = []
double_attacks = []
b = board.copy()
b.turn = color
for move in b.legal_moves:
moving_piece = b.piece_at(move.from_square)
if moving_piece is None:
continue
b.push(move)
to_sq = move.to_square
attacked_squares = b.attacks(to_sq)
attacked_pieces = []
for sq in attacked_squares:
piece = b.piece_at(sq)
if piece is not None and piece.color != color:
attacked_pieces.append((sq, piece))
if len(attacked_pieces) >= 2:
mover_symbol = moving_piece.symbol().upper()
dest = chess.square_name(to_sq)
targets = [
f"{piece_name[p.piece_type]} on {chess.square_name(sq)}"
for sq, p in attacked_pieces
]
target_str = " and ".join(targets)
if moving_piece.piece_type == chess.KNIGHT:
forks.append(f"{mover_symbol}{dest} forks {target_str}")
else:
double_attacks.append(
f"{mover_symbol}{dest} double‐attacks {target_str}"
)
b.pop()
return forks, double_attacks
def find_pins(color):
"""
Find pinned pieces of 'color'. For each pinned piece, identify the pinning piece.
Returns list of descriptive strings.
"""
pins = []
king_sq = board.king(color)
if king_sq is None:
return pins
for sq in (
board.pieces(chess.PAWN, color)
| board.pieces(chess.KNIGHT, color)
| board.pieces(chess.BISHOP, color)
| board.pieces(chess.ROOK, color)
| board.pieces(chess.QUEEN, color)
):
if sq == king_sq:
continue
if board.is_pinned(color, sq):
# Compute direction from king to this pinned piece
f_k, r_k = chess.square_file(king_sq), chess.square_rank(king_sq)
f_p, r_p = chess.square_file(sq), chess.square_rank(sq)
df = f_p - f_k
dr = r_p - r_k
# Normalize direction to unit step
df_norm = (df // abs(df)) if df != 0 else 0
dr_norm = (dr // abs(dr)) if dr != 0 else 0
# Move from pinned piece outward to find pinning slider
cur_f, cur_r = f_p + df_norm, r_p + dr_norm
while 0 <= cur_f < 8 and 0 <= cur_r < 8:
cur_sq = chess.square(cur_f, cur_r)
piece = board.piece_at(cur_sq)
if piece is not None and piece.color != color:
# Check if this piece type can pin along this direction
if dr_norm == 0 and piece.piece_type in (
chess.ROOK,
chess.QUEEN,
):
pinning = piece
elif df_norm == 0 and piece.piece_type in (
chess.ROOK,
chess.QUEEN,
):
pinning = piece
elif abs(df_norm) == abs(dr_norm) and piece.piece_type in (
chess.BISHOP,
chess.QUEEN,
):
pinning = piece
else:
pinning = None
if pinning is not None:
pin_sym = pinning.symbol().upper()
pin_sq = chess.square_name(cur_sq)
pinned_sym = board.piece_at(sq).piece_type
pinned_name = piece_name[board.piece_at(sq).piece_type]
pinned_sq_name = chess.square_name(sq)
king_sq_name = chess.square_name(king_sq)
pins.append(
f"{pin_sym}{pin_sq} pins {pinned_name} on {pinned_sq_name} to king on {king_sq_name}"
)
break
if piece is not None:
# Non-sliding or same-color piece blocks further search
break
cur_f += df_norm
cur_r += dr_norm
return pins
def find_skewers(color):
"""
Find static skewers: slider attacks a high-value enemy piece, behind it on same ray is a lower-value enemy piece.
Returns list of descriptive strings.
"""
skewers = []
enemy_color = not color
for s_sq in (
board.pieces(chess.BISHOP, color)
| board.pieces(chess.ROOK, color)
| board.pieces(chess.QUEEN, color)
):
s_f, s_r = chess.square_file(s_sq), chess.square_rank(s_sq)
# Directions for this slider
directions = []
if board.piece_at(s_sq).piece_type == chess.BISHOP:
directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
elif board.piece_at(s_sq).piece_type == chess.ROOK:
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
else: # Queen
directions = [
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
for df, dr in directions:
cur_f, cur_r = s_f + df, s_r + dr
# Look for first enemy piece
first_found = False
first_sq = None
first_piece = None
while 0 <= cur_f < 8 and 0 <= cur_r < 8:
sq = chess.square(cur_f, cur_r)
piece = board.piece_at(sq)
if piece is not None:
if not first_found and piece.color == enemy_color:
first_found = True
first_sq = sq
first_piece = piece
else:
if first_found and piece.color == enemy_color:
# We have A (first_sq, first_piece) and B (sq, piece)
# Check that first_piece has higher value than piece
values = {
chess.KING: 1000,
chess.QUEEN: 9,
chess.ROOK: 5,
chess.BISHOP: 3,
chess.KNIGHT: 3,
chess.PAWN: 1,
}
if (
values[first_piece.piece_type]
> values[piece.piece_type]
):
s_sym = board.piece_at(s_sq).symbol().upper()
s_sq_name = chess.square_name(s_sq)
high_name = piece_name[first_piece.piece_type]
high_sq = chess.square_name(first_sq)
low_name = piece_name[piece.piece_type]
low_sq = chess.square_name(sq)
skewers.append(
f"{s_sym}{s_sq_name} skewers {high_name} on {high_sq} to {low_name} on {low_sq}"
)
break
else:
# Something that breaks the ray (friendly piece or no second enemy)
break
cur_f += df
cur_r += dr
return skewers
def find_discovered_attacks(color):
"""
Static discovered‐attack patterns: a friendly slider is currently blocked by one friendly piece from attacking an enemy target.
Returns list of descriptive strings.
"""
discovered = []
enemy_color = not color
for s_sq in (
board.pieces(chess.BISHOP, color)
| board.pieces(chess.ROOK, color)
| board.pieces(chess.QUEEN, color)
):
s_f, s_r = chess.square_file(s_sq), chess.square_rank(s_sq)
# Determine directions like in skewers
if board.piece_at(s_sq).piece_type == chess.BISHOP:
directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
elif board.piece_at(s_sq).piece_type == chess.ROOK:
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
else: # Queen
directions = [
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
for df, dr in directions:
cur_f, cur_r = s_f + df, s_r + dr
blocker_sq = None
blocker_piece = None
while 0 <= cur_f < 8 and 0 <= cur_r < 8:
sq = chess.square(cur_f, cur_r)
piece = board.piece_at(sq)
if piece is not None:
if piece.color == color and blocker_sq is None:
# first friendly piece blocks the ray
blocker_sq = sq
blocker_piece = piece
else:
# either second piece or enemy piece
if blocker_sq is not None and piece.color == enemy_color:
# Discovered attack: blocker_sq moving would allow slider at s_sq to attack this piece at sq
s_sym = board.piece_at(s_sq).symbol().upper()
blocker_name = piece_name[blocker_piece.piece_type]
blocker_loc = chess.square_name(blocker_sq)
target_name = piece_name[piece.piece_type]
target_loc = chess.square_name(sq)
discovered.append(
f"Moving {blocker_name} from {blocker_loc} uncovers {s_sym}{chess.square_name(s_sq)} attacking {target_name} on {target_loc}"
)
break
cur_f += df
cur_r += dr
return discovered
def find_xray_attacks(color):
"""
Static x‐ray attacks: slider attacks through one piece (friendly or enemy) to an enemy target behind it.
Returns list of descriptive strings.
"""
xray = []
enemy_color = not color
for s_sq in (
board.pieces(chess.BISHOP, color)
| board.pieces(chess.ROOK, color)
| board.pieces(chess.QUEEN, color)
):
s_f, s_r = chess.square_file(s_sq), chess.square_rank(s_sq)
if board.piece_at(s_sq).piece_type == chess.BISHOP:
directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
elif board.piece_at(s_sq).piece_type == chess.ROOK:
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
else:
directions = [
(-1, -1),
(-1, 1),
(1, -1),
(1, 1),
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
for df, dr in directions:
cur_f, cur_r = s_f + df, s_r + dr
first_blocker = None
first_blocker_sq = None
while 0 <= cur_f < 8 and 0 <= cur_r < 8:
sq = chess.square(cur_f, cur_r)
piece = board.piece_at(sq)
if piece is not None:
if first_blocker is None:
first_blocker = piece
first_blocker_sq = sq
else:
if piece.color == enemy_color:
# X-ray: slider at s_sq x‐rays this piece at sq through first_blocker at first_blocker_sq
s_sym = board.piece_at(s_sq).symbol().upper()
target_name = piece_name[piece.piece_type]
target_loc = chess.square_name(sq)
blocker_name = piece_name[first_blocker.piece_type]
blocker_loc = chess.square_name(first_blocker_sq)
xray.append(
f"{s_sym}{chess.square_name(s_sq)} x‐rays {target_name} on {target_loc} through {blocker_name} on {blocker_loc}"
)
break
cur_f += df
cur_r += dr
return xray
# Build result structure
result = {
"forks": {"white": [], "black": []},
"double_attacks": {"white": [], "black": []},
"pins": {"white": [], "black": []},
"skewers": {"white": [], "black": []},
"discovered_attacks": {"white": [], "black": []},
"xray_attacks": {"white": [], "black": []},
}
# White patterns
w_forks, w_double = find_forks_and_double_attacks(chess.WHITE)
result["forks"]["white"] = w_forks
result["double_attacks"]["white"] = w_double
result["pins"]["white"] = find_pins(chess.WHITE)
result["skewers"]["white"] = find_skewers(chess.WHITE)
result["discovered_attacks"]["white"] = find_discovered_attacks(chess.WHITE)
result["xray_attacks"]["white"] = find_xray_attacks(chess.WHITE)
# Black patterns
b_forks, b_double = find_forks_and_double_attacks(chess.BLACK)
result["forks"]["black"] = b_forks
result["double_attacks"]["black"] = b_double
result["pins"]["black"] = find_pins(chess.BLACK)
result["skewers"]["black"] = find_skewers(chess.BLACK)
result["discovered_attacks"]["black"] = find_discovered_attacks(chess.BLACK)
result["xray_attacks"]["black"] = find_xray_attacks(chess.BLACK)
return result
def evaluate_king_safety(fen):
"""
Evaluate king safety for both White and Black from a given FEN string.
Args:
fen (str): The FEN string representing the chess position.
"""
board = chess.Board(fen)
def get_shield_and_files(color):
"""
For 'color', find:
- pawn_shield: count of own pawns directly in front of king on files f-1,f,f+1.
- max_shield: maximum possible shield pawns (1-3 depending on king file at edge).
- open_or_semi_open_files: list of file names (adjacent to king) that are open or semi-open.
"""
king_sq = board.king(color)
if king_sq is None:
return 0, 0, []
kf = chess.square_file(king_sq)
kr = chess.square_rank(king_sq)
# Direction “forward” for shield pawns
ranks_dir = 1 if color == chess.WHITE else -1
shield_rank = kr + ranks_dir
files_to_check = [f for f in (kf - 1, kf, kf + 1) if 0 <= f < 8]
max_shield = len(files_to_check)
# Count intact shield pawns: own pawn at (file, shield_rank)
shield_count = 0
for f in files_to_check:
sq = chess.square(f, shield_rank) if 0 <= shield_rank < 8 else None
if sq is not None:
piece = board.piece_at(sq)
if (
piece is not None
and piece.piece_type == chess.PAWN
and piece.color == color
):
shield_count += 1
# Determine open or semi‐open files among files_to_check
open_or_semi_open = []
for f in files_to_check:
# Gather all pawns on that file
pawns_on_file = [
board.piece_at(chess.square(f, r))
for r in range(8)
if (p := board.piece_at(chess.square(f, r))) is not None
and p.piece_type == chess.PAWN
]
has_friendly = any(p.color == color for p in pawns_on_file)
has_enemy = any(p.color != color for p in pawns_on_file)
file_name = chess.FILE_NAMES[f]
if not pawns_on_file:
# fully open file
open_or_semi_open.append(file_name)
elif has_enemy and not has_friendly:
# semi‐open (enemy pawn only)
open_or_semi_open.append(file_name)
return shield_count, max_shield, open_or_semi_open
def get_attacker_count(color):
"""
Count unique enemy pieces attacking any of the up to 8 squares adjacent to the king.
"""
king_sq = board.king(color)
if king_sq is None:
return 0
enemy_color = not color
kf = chess.square_file(king_sq)
kr = chess.square_rank(king_sq)
attackers = set()
# Loop over the eight neighbors
for df in (-1, 0, 1):
for dr in (-1, 0, 1):
if df == 0 and dr == 0:
continue
f = kf + df
r = kr + dr
if 0 <= f < 8 and 0 <= r < 8:
sq = chess.square(f, r)
for attacker_sq in board.attackers(enemy_color, sq):
attackers.add(attacker_sq)
return len(attackers)
def compute_shelter_score(shield_count, max_shield, open_count, attacker_count):
"""
Compute a composite shelter score in [0, 1], combining:
- shield_factor: shield_count / max_shield
- file_factor: 1 - (open_count / max_shield)
- attacker_factor: 1 - min(attacker_count, 8) / 8
Return average of the three, rounded to 2 decimals.
"""
if max_shield == 0:
shield_factor = 0
file_factor = 0
else:
shield_factor = shield_count / max_shield
file_factor = 1 - (open_count / max_shield)
attacker_factor = 1 - min(attacker_count, 8) / 8
return round((shield_factor + file_factor + attacker_factor) / 3, 2)
result = {
"pawn_shield": {"white": "", "black": ""},
"open_files": {"white": [], "black": []},
"attacker_count": {"white": 0, "black": 0},
"shelter_score": {"white": 0.0, "black": 0.0},
}
# White evaluation
w_shield, w_max_shield, w_open = get_shield_and_files(chess.WHITE)
w_attackers = get_attacker_count(chess.WHITE)
w_shelter = compute_shelter_score(w_shield, w_max_shield, len(w_open), w_attackers)
result["pawn_shield"]["white"] = f"{w_shield} of {w_max_shield} shield pawns"
result["open_files"]["white"] = w_open
result["attacker_count"]["white"] = w_attackers
result["shelter_score"]["white"] = w_shelter
# Black evaluation
b_shield, b_max_shield, b_open = get_shield_and_files(chess.BLACK)
b_attackers = get_attacker_count(chess.BLACK)
b_shelter = compute_shelter_score(b_shield, b_max_shield, len(b_open), b_attackers)
result["pawn_shield"]["black"] = f"{b_shield} of {b_max_shield} shield pawns"
result["open_files"]["black"] = b_open
result["attacker_count"]["black"] = b_attackers
result["shelter_score"]["black"] = b_shelter
return result
def classify_opening(fen: str) -> dict:
"""
Attempt to classify a chess opening using the Lichess openings database.
Return the ECO code, name, moves and main sub-variations of the opening.
Args:
fen (str): The FEN string representing the chess position.
"""
board = chess.Board(fen)
epd_key = board.epd()
df = _load_lichess_openings()
match = df[df["epd"] == epd_key]
if match.empty:
return {"error": f"No ECO code found for position: {fen}"}
eco_code = match.iloc[0]["eco"]
opening_name = match.iloc[0]["name"]
base_pgn = match.iloc[0]["pgn"]
base_uci = match.iloc[0]["uci"]
base_len = len(base_uci.split())
def next_move(uci_str: str) -> str | None:
parts = uci_str.split()
if not parts[:base_len] == base_uci.split():
return None
return parts[base_len] if len(parts) > base_len else None
df["next_move"] = df["uci"].apply(next_move)
subs = (
df[df["next_move"].notna()]
.sort_values("uci")
.drop_duplicates("next_move", keep="first")
)
subvariants = [
{"name": row["name"], "pgn": row["pgn"], "fen": row["epd"]}
for _, row in subs.iterrows()
]
return {
"eco": eco_code,
"name": opening_name,
"pgn": base_pgn,
"subvariants": subvariants,
}
def find_opening_by_name(name: str) -> dict:
"""
Search for a chess opening by its name in the Lichess openings database.
Return the ECO code, name, PGN, FEN and sub-variations of a chess opening by its name.
The name is matched case-insensitively.
Args:
name (str): The name of the chess opening to search for (e.g. Caro-Kann Defense: Advance Variation).
"""
df = _load_lichess_openings()
mask = df["name"].str.contains(name, case=False, regex=False)
matches = df[mask]
if matches.empty:
return {"error": f"No opening found matching name: '{name}'"}
row = matches.iloc[0]
eco_code = row["eco"]
full_name = row["name"]
base_pgn = row["pgn"]
base_uci = row["uci"]
epd = row["epd"]
fen = f"{epd} 0 1"
base_moves = base_uci.split()
base_len = len(base_moves)
def next_move(uci_str: str) -> str | None:
parts = uci_str.split()
if not parts[:base_len] == base_uci.split():
return None
return parts[base_len] if len(parts) > base_len else None
df["next_move"] = df["uci"].apply(next_move)
subs = (
df[df["next_move"].notna()]
.sort_values("uci")
.drop_duplicates("next_move", keep="first")
)
subvariants = [
{"name": sub_row["name"], "pgn": sub_row["pgn"], "fen": sub_row["epd"]}
for _, sub_row in subs.iterrows()
]
return {
"eco": eco_code,
"name": full_name,
"pgn": base_pgn,
"fen": fen,
"subvariants": subvariants,
}
def _get_color_name(color: chess.Color) -> str:
return "white" if color == chess.WHITE else "black"
def _get_piece_info_on_square(board: chess.Board, square: chess.Square) -> str:
piece = board.piece_at(square)
if piece is None:
return f"No piece on {chess.square_name(square)}"
color = _get_color_name(piece.color)
result = f"There is a {color} {chess.piece_name(piece.piece_type)} on {chess.square_name(square)}."
legal_moves = [
chess.square_name(m.to_square)
for m in board.legal_moves
if m.from_square == square
]
if not legal_moves:
result += f" It can't move because"
if board.turn != piece.color:
result += f" it is not {_get_color_name(piece.color)}'s turn."
elif board.is_pinned(piece.color, square):
result += " it is pinned."
elif board.is_check():
result += f" it is a check and the {chess.piece_name(piece.piece_type)} can't block"
else:
result += " it is blocked."
result += f" However, it attacks the following squares: {', '.join([chess.square_name(s) for s in board.attacks(square)])}."
else:
result += f" It can move to the following squares: {', '.join(legal_moves)}."
return result
def _get_attackers(board: chess.Board, square: chess.Square, color: chess.Color) -> str:
piece = board.piece_at(square)
title = "attackers" if piece is None or piece.color != color else "defenders"
attackers = board.attackers(color, square)
color_name = _get_color_name(color)
if not attackers:
return f"No {color_name} {title} for {chess.square_name(square)}"
return (
f"{len(attackers)} {color_name.title()} {title} for {chess.square_name(square)}: "
+ ", ".join(
[
f"{chess.piece_name(board.piece_at(s).piece_type)} on {chess.square_name(s)}"
for s in attackers
]
)
)
def _load_lichess_openings(
path_prefix: str = "/app/data/lichess_openings/dist/",
) -> pd.DataFrame:
"""Load Lichess openings data from TSV files.
Assumes files 'a.tsv', 'b.tsv', 'c.tsv', 'd.tsv', 'e.tsv' are in path_prefix.
Each has columns: eco, name, pgn, uci, epd.
"""
files = [f"{path_prefix}{vol}.tsv" for vol in ("a", "b", "c", "d", "e")]
dfs = []
for fn in files:
df = pd.read_csv(fn, sep="\t", usecols=["eco", "name", "pgn", "uci", "epd"])
dfs.append(df)
return pd.concat(dfs, ignore_index=True)
get_position_tool = gr.Interface(
fn=get_position,
inputs=Chessboard(label="FEN String"),
outputs=gr.JSON(label="Chess Position"),
title="Chess Position Viewer",
description="Enter a FEN string to view the current chess position.",
)
get_square_info_tool = gr.Interface(
fn=get_square_info,
inputs=[Chessboard(label="FEN String"), gr.Textbox(label="Square Name")],
outputs=gr.JSON(label="Square Info"),
title="Chess Square Info",
description="Enter a FEN string and a square name (e.g., 'e4') to get information about the piece on that square.",
)
get_top_moves_tool = gr.Interface(
fn=get_top_moves,
inputs=[Chessboard(label="FEN String"), gr.Number(value=5, label="Top N Moves")],
outputs=gr.JSON(label="Top Moves"),
title="Top Moves Analyzer",
description="Enter a FEN string to get the top moves for the current position using StockFish.",
)
analyze_pawn_structure_tool = gr.Interface(
fn=analyze_pawn_structure,
inputs=Chessboard(label="FEN String"),
outputs=gr.JSON(label="Pawn Structure Analysis"),
title="Pawn Structure Analyzer",
description="Enter a FEN string to analyze the pawn structure features for both White and Black.",
)
analyze_tactical_patterns_tool = gr.Interface(
fn=analyze_tactical_patterns,
inputs=Chessboard(label="FEN String"),
outputs=gr.JSON(label="Tactical Patterns Analysis"),
title="Tactical Patterns Analyzer",
description="Enter a FEN string to analyze immediate tactical patterns for both White and Black.",
)
evaluate_king_safety_tool = gr.Interface(
fn=evaluate_king_safety,
inputs=Chessboard(label="FEN String"),
outputs=gr.JSON(label="King Safety Evaluation"),
title="King Safety Evaluator",
description="Enter a FEN string to evaluate the safety of both kings in the current position.",
)
classify_opening_tool = gr.Interface(
fn=classify_opening,
inputs=Chessboard(label="FEN String"),
outputs=gr.JSON(label="Opening Classification"),
title="Opening Classifier",
description="Enter a FEN string to classify the opening and get its ECO code, name, and sub-variations.",
)
find_opening_by_name_tool = gr.Interface(
fn=find_opening_by_name,
inputs=gr.Textbox(label="Opening Name"),
outputs=gr.JSON(label="Opening Details"),
title="Find Opening by Name",
description="Enter the name of a chess opening to find its ECO code, PGN, FEN, and sub-variations.",
)
app = gr.TabbedInterface(
[
get_position_tool,
get_square_info_tool,
get_top_moves_tool,
analyze_pawn_structure_tool,
analyze_tactical_patterns_tool,
evaluate_king_safety_tool,
classify_opening_tool,
find_opening_by_name_tool,
],
tab_names=[
"Get Position",
"Get Square Info",
"Get Top Moves",
"Analyze Pawn Structure",
"Analyze Tactical Patterns",
"Evaluate King Safety",
"Classify Opening",
"Find Opening by Name",
],
title="Chess Tools",
)
if __name__ == "__main__":
app.launch(mcp_server=True)