| | import customtkinter as ctk
|
| | import chess
|
| |
|
| | class BoardUI(ctk.CTkFrame):
|
| | def __init__(self, master, game_state, **kwargs):
|
| | super().__init__(master, **kwargs)
|
| | self.game_state = game_state
|
| | self.canvas = ctk.CTkCanvas(self, bg="#302E2B", highlightthickness=0)
|
| | self.canvas.pack(fill="both", expand=True)
|
| |
|
| | self.square_size = 50
|
| | self.selected_square = None
|
| | self.pieces = {}
|
| | self.flipped = False
|
| | self.edit_mode = False
|
| | self.selected_edit_piece = None
|
| | self.last_analysis_moves = []
|
| |
|
| | self.canvas.bind("<Button-1>", self.on_click)
|
| | self.canvas.bind("<Configure>", self.on_resize)
|
| |
|
| | def on_resize(self, event):
|
| |
|
| | size = min(event.width, event.height)
|
| | self.square_size = size // 8
|
| | self.draw_board()
|
| |
|
| | def get_visual_coords(self, file, rank):
|
| | """Convert chess file/rank to visual coordinates accounting for flip."""
|
| | if self.flipped:
|
| | visual_file = 7 - file
|
| | visual_rank = rank
|
| | else:
|
| | visual_file = file
|
| | visual_rank = 7 - rank
|
| | return visual_file, visual_rank
|
| |
|
| | def get_chess_square_from_visual(self, visual_file, visual_rank):
|
| | """Convert visual coordinates to chess square."""
|
| | if self.flipped:
|
| | file = 7 - visual_file
|
| | rank = visual_rank
|
| | else:
|
| | file = visual_file
|
| | rank = 7 - visual_rank
|
| | return chess.square(file, rank)
|
| |
|
| | def draw_board(self):
|
| | self.canvas.delete("all")
|
| | colors = ["#EBECD0", "#779556"]
|
| |
|
| |
|
| | canvas_w = self.canvas.winfo_width()
|
| | canvas_h = self.canvas.winfo_height()
|
| | board_size = self.square_size * 8
|
| | offset_x = (canvas_w - board_size) // 2
|
| | offset_y = (canvas_h - board_size) // 2
|
| |
|
| | self.offset_x = max(0, offset_x)
|
| | self.offset_y = max(0, offset_y)
|
| |
|
| | for visual_rank in range(8):
|
| | for visual_file in range(8):
|
| | color = colors[(visual_rank + visual_file) % 2]
|
| | x1 = self.offset_x + visual_file * self.square_size
|
| | y1 = self.offset_y + visual_rank * self.square_size
|
| | x2 = x1 + self.square_size
|
| | y2 = y1 + self.square_size
|
| |
|
| | self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="")
|
| |
|
| |
|
| | chess_square = self.get_chess_square_from_visual(visual_file, visual_rank)
|
| | piece = self.game_state.board.piece_at(chess_square)
|
| |
|
| | if piece:
|
| | self.draw_piece(x1, y1, piece)
|
| |
|
| |
|
| | if self.game_state.board.is_check():
|
| | self.highlight_king_check()
|
| |
|
| | if self.selected_square is not None:
|
| | self.highlight_square(self.selected_square)
|
| |
|
| |
|
| | if self.last_analysis_moves:
|
| | self.display_analysis(self.last_analysis_moves, cache=False)
|
| |
|
| | def draw_piece(self, x, y, piece):
|
| | symbol = piece.unicode_symbol()
|
| | font_size = int(self.square_size * 0.7)
|
| | fill_color = "#1a1a1a" if piece.color == chess.BLACK else "#ffffff"
|
| |
|
| |
|
| | shadow_offset = max(1, int(self.square_size * 0.02))
|
| | self.canvas.create_text(
|
| | x + self.square_size/2 + shadow_offset,
|
| | y + self.square_size/2 + shadow_offset,
|
| | text=symbol,
|
| | font=("Segoe UI Symbol", font_size, "bold"),
|
| | fill="gray30"
|
| | )
|
| | self.canvas.create_text(
|
| | x + self.square_size/2,
|
| | y + self.square_size/2,
|
| | text=symbol,
|
| | font=("Segoe UI Symbol", font_size, "bold"),
|
| | fill=fill_color
|
| | )
|
| |
|
| | def highlight_king_check(self):
|
| | """Highlight the King's square in red if in check."""
|
| | king_square = self.game_state.board.king(self.game_state.board.turn)
|
| | if king_square is not None:
|
| | file = chess.square_file(king_square)
|
| | rank = chess.square_rank(king_square)
|
| | visual_file, visual_rank = self.get_visual_coords(file, rank)
|
| |
|
| | x1 = self.offset_x + visual_file * self.square_size
|
| | y1 = self.offset_y + visual_rank * self.square_size
|
| | x2 = x1 + self.square_size
|
| | y2 = y1 + self.square_size
|
| |
|
| |
|
| | self.canvas.create_oval(x1+2, y1+2, x2-2, y2-2, outline="#FF0000", width=4, tag="check")
|
| | self.canvas.create_rectangle(x1, y1, x2, y2, fill="red", stipple="gray25", outline="")
|
| |
|
| | def highlight_square(self, square):
|
| | file = chess.square_file(square)
|
| | rank = chess.square_rank(square)
|
| | visual_file, visual_rank = self.get_visual_coords(file, rank)
|
| |
|
| | x1 = self.offset_x + visual_file * self.square_size
|
| | y1 = self.offset_y + visual_rank * self.square_size
|
| | x2 = x1 + self.square_size
|
| | y2 = y1 + self.square_size
|
| |
|
| | self.canvas.create_rectangle(x1, y1, x2, y2, fill="yellow", stipple="gray50", outline="gold", width=2)
|
| |
|
| | def on_click(self, event):
|
| |
|
| | adj_x = event.x - getattr(self, 'offset_x', 0)
|
| | adj_y = event.y - getattr(self, 'offset_y', 0)
|
| |
|
| | if adj_x < 0 or adj_y < 0:
|
| | return
|
| |
|
| | visual_file = int(adj_x // self.square_size)
|
| | visual_rank = int(adj_y // self.square_size)
|
| |
|
| | if visual_file < 0 or visual_file > 7 or visual_rank < 0 or visual_rank > 7:
|
| | return
|
| |
|
| | chess_square = self.get_chess_square_from_visual(visual_file, visual_rank)
|
| |
|
| | if self.edit_mode:
|
| |
|
| | self.game_state.set_piece(chess_square, self.selected_edit_piece)
|
| | self.draw_board()
|
| | return
|
| |
|
| |
|
| | if self.selected_square is None:
|
| | piece = self.game_state.board.piece_at(chess_square)
|
| | if piece and piece.color == self.game_state.board.turn:
|
| | self.selected_square = chess_square
|
| | self.draw_board()
|
| | else:
|
| |
|
| | move = chess.Move(self.selected_square, chess_square)
|
| |
|
| | piece_at_start = self.game_state.board.piece_at(self.selected_square)
|
| | if piece_at_start and piece_at_start.piece_type == chess.PAWN:
|
| | if (self.game_state.board.turn == chess.WHITE and chess.square_rank(chess_square) == 7) or \
|
| | (self.game_state.board.turn == chess.BLACK and chess.square_rank(chess_square) == 0):
|
| | move = chess.Move(self.selected_square, chess_square, promotion=chess.QUEEN)
|
| |
|
| | if move in self.game_state.board.legal_moves:
|
| | self.game_state.board.push(move)
|
| | self.selected_square = None
|
| | self.draw_board()
|
| | self.master.event_generate("<<MoveMade>>")
|
| | else:
|
| | piece = self.game_state.board.piece_at(chess_square)
|
| | if piece and piece.color == self.game_state.board.turn:
|
| | self.selected_square = chess_square
|
| | self.draw_board()
|
| | else:
|
| | self.selected_square = None
|
| | self.draw_board()
|
| |
|
| | def draw_arrow(self, start_sq, end_sq, color="#00FF00", width=4):
|
| |
|
| | start_file = chess.square_file(start_sq)
|
| | start_rank = chess.square_rank(start_sq)
|
| | end_file = chess.square_file(end_sq)
|
| | end_rank = chess.square_rank(end_sq)
|
| |
|
| | start_vf, start_vr = self.get_visual_coords(start_file, start_rank)
|
| | end_vf, end_vr = self.get_visual_coords(end_file, end_rank)
|
| |
|
| | x1 = self.offset_x + start_vf * self.square_size + self.square_size // 2
|
| | y1 = self.offset_y + start_vr * self.square_size + self.square_size // 2
|
| | x2 = self.offset_x + end_vf * self.square_size + self.square_size // 2
|
| | y2 = self.offset_y + end_vr * self.square_size + self.square_size // 2
|
| |
|
| | self.canvas.create_line(x1, y1, x2, y2, fill=color, width=width, arrow="last", arrowshape=(16, 20, 6), tag="arrow")
|
| |
|
| | def display_analysis(self, top_moves, cache=True):
|
| | if cache:
|
| | self.last_analysis_moves = top_moves
|
| |
|
| | self.canvas.delete("arrow")
|
| |
|
| | colors = ["#00FF00", "#00FFFF", "#FFFF00"]
|
| |
|
| | for i, move_data in enumerate(top_moves):
|
| | if i >= len(colors):
|
| | break
|
| | move = move_data["move"]
|
| | self.draw_arrow(move.from_square, move.to_square, color=colors[i], width=6 - i)
|
| |
|