| 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)
|
|
|