| | import customtkinter as ctk
|
| | import threading
|
| | import chess
|
| | import time
|
| | from src.game_state import GameState
|
| | from src.engine import EngineHandler
|
| | from src.board_ui import BoardUI
|
| | from src.overlay import SelectionOverlay, ProjectionOverlay
|
| | from src.vision import VisionHandler
|
| | from src.mirror import MirrorHandler
|
| |
|
| | class ChessApp(ctk.CTk):
|
| | def __init__(self):
|
| | super().__init__()
|
| |
|
| | self.title("CheckerChesser")
|
| | self.geometry("900x700")
|
| | self.minsize(600, 500)
|
| |
|
| |
|
| | self.game_state = GameState()
|
| | self.engine = EngineHandler()
|
| | self.vision = VisionHandler()
|
| | self.mirror = MirrorHandler()
|
| |
|
| |
|
| | self.mirroring = False
|
| | self.mirror_region = None
|
| | self.projection_overlay = None
|
| |
|
| | self.grid_rowconfigure(0, weight=1)
|
| | self.grid_columnconfigure(0, weight=0)
|
| | self.grid_columnconfigure(1, weight=1)
|
| |
|
| |
|
| | self.sidebar = ctk.CTkFrame(self, width=200, corner_radius=0)
|
| | self.sidebar.grid(row=0, column=0, sticky="nsew")
|
| |
|
| | self.logo_label = ctk.CTkLabel(self.sidebar, text="CheckerChesser", font=ctk.CTkFont(size=20, weight="bold"))
|
| | self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
|
| |
|
| | self.new_game_btn = ctk.CTkButton(self.sidebar, text="New Local Game", command=self.start_local_game)
|
| | self.new_game_btn.grid(row=1, column=0, padx=20, pady=10)
|
| |
|
| | self.mirror_btn = ctk.CTkButton(self.sidebar, text="Screen Mirroring", command=self.start_screen_mirroring)
|
| | self.mirror_btn.grid(row=2, column=0, padx=20, pady=10)
|
| |
|
| | self.stop_btn = ctk.CTkButton(self.sidebar, text="Stop Mirroring", command=self.stop_mirroring, fg_color="red", hover_color="darkred")
|
| | self.stop_btn.grid(row=3, column=0, padx=20, pady=10)
|
| | self.stop_btn.grid_remove()
|
| |
|
| | self.status_label = ctk.CTkLabel(self.sidebar, text="Status: Idle", anchor="w")
|
| | self.status_label.grid(row=4, column=0, padx=20, pady=(20, 0), sticky="ew")
|
| |
|
| |
|
| | self.content_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
|
| | self.content_frame.grid(row=0, column=1, sticky="nsew")
|
| | self.content_frame.grid_rowconfigure(0, weight=1)
|
| | self.content_frame.grid_columnconfigure(0, weight=1)
|
| |
|
| |
|
| |
|
| | self.after(200, self.init_engine_thread)
|
| |
|
| |
|
| | self.sidebar_visible = True
|
| |
|
| |
|
| | self.sidebar_toggle_btn = ctk.CTkButton(self, text="☰", width=30, height=30,
|
| | command=self.toggle_sidebar,
|
| | fg_color="gray20", hover_color="gray30")
|
| | self.sidebar_toggle_btn.place(x=10, y=10)
|
| |
|
| |
|
| | self.board_ui = None
|
| | self.start_local_game()
|
| |
|
| | def toggle_sidebar(self):
|
| | if self.sidebar_visible:
|
| | self.sidebar.grid_remove()
|
| | self.sidebar_visible = False
|
| | self.sidebar_toggle_btn.configure(fg_color="transparent")
|
| |
|
| |
|
| |
|
| | else:
|
| | self.sidebar.grid()
|
| | self.sidebar_visible = True
|
| | self.sidebar_toggle_btn.configure(fg_color="gray20")
|
| |
|
| | def init_engine_thread(self):
|
| | def _init():
|
| | success, msg = self.engine.initialize_engine()
|
| | self.after(0, lambda: self.status_label.configure(text="Engine: Ready" if success else "Engine: Not Found"))
|
| | if not success:
|
| | print(msg)
|
| | threading.Thread(target=_init, daemon=True).start()
|
| |
|
| | def start_local_game(self):
|
| | self.stop_mirroring()
|
| |
|
| |
|
| | for widget in self.content_frame.winfo_children():
|
| | widget.destroy()
|
| |
|
| | self.game_state.reset()
|
| |
|
| |
|
| | self.controls_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
|
| | self.controls_frame.grid(row=1, column=0, pady=10, sticky="ew")
|
| |
|
| |
|
| | left_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
|
| | left_frame.pack(side="left", padx=20)
|
| |
|
| |
|
| | self.play_as_var = ctk.StringVar(value="White")
|
| | play_as_label = ctk.CTkLabel(left_frame, text="Play as:")
|
| | play_as_label.pack(side="left", padx=(0, 5))
|
| | self.play_as_menu = ctk.CTkOptionMenu(left_frame, variable=self.play_as_var,
|
| | values=["White", "Black"],
|
| | command=self.on_play_as_change,
|
| | width=80)
|
| | self.play_as_menu.pack(side="left", padx=5)
|
| |
|
| |
|
| | self.first_move_var = ctk.StringVar(value="White")
|
| | first_move_label = ctk.CTkLabel(left_frame, text="First Move:")
|
| | first_move_label.pack(side="left", padx=(10, 5))
|
| | self.first_move_menu = ctk.CTkOptionMenu(left_frame, variable=self.first_move_var,
|
| | values=["White", "Black"],
|
| | command=self.on_first_move_change,
|
| | width=80)
|
| | self.first_move_menu.pack(side="left", padx=5)
|
| |
|
| |
|
| | self.flip_btn = ctk.CTkButton(left_frame, text="⟳ Flip Board", command=self.flip_board, width=100)
|
| | self.flip_btn.pack(side="left", padx=10)
|
| |
|
| |
|
| | right_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
|
| | right_frame.pack(side="right", padx=20)
|
| |
|
| |
|
| | moves_label = ctk.CTkLabel(right_frame, text="Show Best Moves:")
|
| | moves_label.pack(side="left", padx=(0, 5))
|
| | self.best_moves_var = ctk.StringVar(value="3")
|
| | self.best_moves_menu = ctk.CTkOptionMenu(right_frame, variable=self.best_moves_var,
|
| | values=["1", "2", "3"],
|
| | command=self.on_best_moves_change,
|
| | width=60)
|
| | self.best_moves_menu.pack(side="left", padx=5)
|
| |
|
| |
|
| | self.board_ui = BoardUI(self.content_frame, self.game_state)
|
| | self.board_ui.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
|
| |
|
| |
|
| | toggles_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
|
| | toggles_frame.grid(row=2, column=0, pady=5)
|
| |
|
| |
|
| | self.analysis_var = ctk.BooleanVar(value=False)
|
| | self.analysis_switch = ctk.CTkSwitch(toggles_frame, text="Analysis Mode",
|
| | variable=self.analysis_var, command=self.toggle_analysis)
|
| | self.analysis_switch.pack(side="left", padx=15)
|
| |
|
| |
|
| | self.two_player_var = ctk.BooleanVar(value=False)
|
| | self.two_player_switch = ctk.CTkSwitch(toggles_frame, text="Two Player Mode",
|
| | variable=self.two_player_var, command=self.toggle_two_player)
|
| | self.two_player_switch.pack(side="left", padx=15)
|
| |
|
| |
|
| | self.force_move_btn = ctk.CTkButton(left_frame, text="⚡ Force Move", command=self.force_ai_move, width=100, fg_color="orange", hover_color="darkorange")
|
| | self.force_move_btn.pack(side="left", padx=10)
|
| |
|
| |
|
| | self.edit_mode_var = ctk.BooleanVar(value=False)
|
| | self.edit_mode_switch = ctk.CTkSwitch(toggles_frame, text="Edit Mode",
|
| | variable=self.edit_mode_var, command=self.toggle_edit_mode)
|
| | self.edit_mode_switch.pack(side="left", padx=15)
|
| |
|
| |
|
| | self.palette_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
|
| | self.palette_frame.grid(row=3, column=0, pady=5)
|
| | self.palette_frame.grid_remove()
|
| |
|
| | self.init_palette()
|
| |
|
| |
|
| | self.score_label = ctk.CTkLabel(self.content_frame, text="", font=("Arial", 12))
|
| | self.score_label.grid(row=4, column=0, pady=5)
|
| |
|
| |
|
| | self.bind("<<MoveMade>>", self.on_move_made)
|
| | self.status_label.configure(text="Mode: vs AI (White)")
|
| |
|
| | def start_screen_mirroring(self):
|
| | self.status_label.configure(text="Select Region to Mirror to...")
|
| | self.withdraw()
|
| |
|
| |
|
| | def on_selection(region):
|
| | self.deiconify()
|
| | self.begin_mirroring(region)
|
| |
|
| | SelectionOverlay(self, on_selection)
|
| |
|
| | def begin_mirroring(self, region):
|
| | """
|
| | Start mirroring moves to the selected region.
|
| | """
|
| | self.mirror_region = region
|
| | self.mirroring = True
|
| |
|
| | self.stop_btn.grid()
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | self.status_label.configure(text="Mode: Screen Mirroring Active")
|
| | self.stop_btn.grid()
|
| |
|
| |
|
| | self.projection_overlay = ProjectionOverlay(region)
|
| |
|
| |
|
| | def stop_mirroring(self):
|
| | self.mirroring = False
|
| | self.mirror_region = None
|
| |
|
| | if self.projection_overlay:
|
| | self.projection_overlay.destroy()
|
| | self.projection_overlay = None
|
| |
|
| | self.stop_btn.grid_remove()
|
| | self.status_label.configure(text="Mirroring Stopped")
|
| |
|
| |
|
| | if not self.game_state.is_game_over():
|
| | turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | self.status_label.configure(text=f"Your Turn ({turn_str})")
|
| |
|
| | def toggle_analysis(self):
|
| | if self.analysis_var.get():
|
| | self.update_analysis()
|
| | else:
|
| | self.board_ui.canvas.delete("arrow")
|
| |
|
| | def toggle_two_player(self):
|
| | if self.two_player_var.get():
|
| | self.status_label.configure(text="Mode: Two Player")
|
| | turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | self.status_label.configure(text=f"{turn}'s Turn")
|
| |
|
| | if "Thinking" in self.status_label.cget("text"):
|
| | self.status_label.configure(text=f"{turn}'s Turn")
|
| | else:
|
| | self.status_label.configure(text="Mode: vs AI (White)")
|
| | if self.game_state.board.turn == chess.WHITE:
|
| | self.status_label.configure(text="Your Turn (White)")
|
| |
|
| | def flip_board(self):
|
| | """Flip the board orientation."""
|
| | if hasattr(self, 'board_ui') and self.board_ui:
|
| | self.board_ui.flipped = not getattr(self.board_ui, 'flipped', False)
|
| | self.board_ui.draw_board()
|
| |
|
| | def on_play_as_change(self, value):
|
| | """Handle play as color change."""
|
| | if self.edit_mode_var.get():
|
| | return
|
| | self.reset_game()
|
| |
|
| | def init_palette(self):
|
| | """Initialize the piece palette for editing."""
|
| | pieces = [
|
| | (chess.PAWN, chess.WHITE, "♙"), (chess.KNIGHT, chess.WHITE, "♘"), (chess.BISHOP, chess.WHITE, "♗"),
|
| | (chess.ROOK, chess.WHITE, "♖"), (chess.QUEEN, chess.WHITE, "♕"), (chess.KING, chess.WHITE, "♔"),
|
| | (chess.PAWN, chess.BLACK, "♟"), (chess.KNIGHT, chess.BLACK, "♞"), (chess.BISHOP, chess.BLACK, "♝"),
|
| | (chess.ROOK, chess.BLACK, "♜"), (chess.QUEEN, chess.BLACK, "♛"), (chess.KING, chess.BLACK, "♚")
|
| | ]
|
| |
|
| |
|
| | for i, (pt, color, symbol) in enumerate(pieces[:6]):
|
| | btn = ctk.CTkButton(self.palette_frame, text=symbol, width=40, font=("Segoe UI Symbol", 24),
|
| | command=lambda p=chess.Piece(pt, color): self.select_palette_piece(p))
|
| | btn.grid(row=0, column=i, padx=2, pady=2)
|
| |
|
| |
|
| | for i, (pt, color, symbol) in enumerate(pieces[6:]):
|
| | btn = ctk.CTkButton(self.palette_frame, text=symbol, width=40, font=("Segoe UI Symbol", 24),
|
| | command=lambda p=chess.Piece(pt, color): self.select_palette_piece(p))
|
| | btn.grid(row=1, column=i, padx=2, pady=2)
|
| |
|
| |
|
| | trash_btn = ctk.CTkButton(self.palette_frame, text="🗑", width=40, font=("Segoe UI Symbol", 20), fg_color="red", hover_color="darkred",
|
| | command=lambda: self.select_palette_piece(None))
|
| | trash_btn.grid(row=0, column=6, rowspan=2, padx=5, sticky="ns")
|
| |
|
| | def select_palette_piece(self, piece):
|
| | self.board_ui.selected_edit_piece = piece
|
| |
|
| | def toggle_edit_mode(self):
|
| | is_edit = self.edit_mode_var.get()
|
| | self.board_ui.edit_mode = is_edit
|
| | self.board_ui.selected_square = None
|
| | self.board_ui.draw_board()
|
| |
|
| | if is_edit:
|
| | self.palette_frame.grid()
|
| | self.status_label.configure(text="Edit Mode: Select piece to place")
|
| | else:
|
| | self.palette_frame.grid_remove()
|
| | self.status_label.configure(text="Edit Mode Disabled")
|
| |
|
| | turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | self.status_label.configure(text=f"Your Turn ({turn_str})")
|
| |
|
| | def on_first_move_change(self, value):
|
| | """Handle first move color change."""
|
| | if self.edit_mode_var.get():
|
| | return
|
| | self.reset_game()
|
| |
|
| | def reset_game(self):
|
| | """Reset the game state based on current controls."""
|
| | self.game_state.reset()
|
| |
|
| |
|
| | first_move_color = chess.BLACK if self.first_move_var.get() == "Black" else chess.WHITE
|
| | self.game_state.board.turn = first_move_color
|
| |
|
| | play_as = self.play_as_var.get()
|
| |
|
| | if play_as == "Black":
|
| |
|
| | self.board_ui.flipped = True
|
| | else:
|
| | self.board_ui.flipped = False
|
| |
|
| |
|
| | ai_color = chess.WHITE if play_as == "Black" else chess.BLACK
|
| |
|
| | if self.game_state.board.turn == ai_color and not self.two_player_var.get():
|
| | self.status_label.configure(text="AI Thinking...")
|
| | threading.Thread(target=self.make_ai_move, daemon=True).start()
|
| | else:
|
| | turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | self.status_label.configure(text=f"Your Turn ({turn_str})")
|
| |
|
| | self.board_ui.draw_board()
|
| |
|
| | def on_best_moves_change(self, value):
|
| | """Handle best moves count change."""
|
| | if self.analysis_var.get():
|
| | self.update_analysis()
|
| |
|
| | def update_analysis(self):
|
| | if not self.analysis_var.get():
|
| | return
|
| |
|
| | def _analyze():
|
| | fen = self.game_state.get_fen()
|
| | limit = int(self.best_moves_var.get()) if hasattr(self, 'best_moves_var') else 3
|
| | top_moves = self.engine.get_top_moves(fen, limit=limit)
|
| | self.after(0, lambda: self.display_analysis_results(top_moves))
|
| |
|
| | threading.Thread(target=_analyze, daemon=True).start()
|
| |
|
| | def display_analysis_results(self, top_moves):
|
| | """Display analysis results with scores."""
|
| | if hasattr(self, 'board_ui') and self.board_ui:
|
| | self.board_ui.display_analysis(top_moves)
|
| |
|
| |
|
| | if hasattr(self, 'score_label') and top_moves:
|
| | score_texts = []
|
| | found_mate = False
|
| | for i, move_data in enumerate(top_moves):
|
| | move = move_data["move"]
|
| | score = move_data["score"]
|
| |
|
| | if abs(score) >= 9000:
|
| |
|
| | found_mate = True
|
| | mate_in = (10000 - abs(score))
|
| | score_str = f"M{mate_in}" if score > 0 else f"-M{mate_in}"
|
| |
|
| | if i == 0:
|
| | if score > 0:
|
| | self.status_label.configure(text=f"White wins in {mate_in}!")
|
| | else:
|
| | self.status_label.configure(text=f"Black wins in {mate_in}!")
|
| | else:
|
| | score_str = f"{score/100:+.2f}"
|
| |
|
| | colors = ["🟢", "🔵", "🟡"]
|
| | score_texts.append(f"{colors[i]} {move}: {score_str}")
|
| |
|
| | self.score_label.configure(text=" | ".join(score_texts))
|
| |
|
| |
|
| | if not found_mate and not self.mirroring and not self.edit_mode_var.get() and "Thinking" not in self.status_label.cget("text"):
|
| |
|
| | turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | if self.two_player_var.get():
|
| | self.status_label.configure(text=f"{turn}'s Turn")
|
| |
|
| |
|
| | elif hasattr(self, 'score_label'):
|
| | self.score_label.configure(text="")
|
| |
|
| | def on_move_made(self, event=None):
|
| |
|
| | if self.game_state.is_game_over():
|
| | result = self.game_state.board.result()
|
| | self.status_label.configure(text=f"Game Over: {result}")
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | if self.mirroring and event:
|
| |
|
| |
|
| |
|
| | try:
|
| | if self.game_state.board.move_stack:
|
| | last_move = self.game_state.board.peek()
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | is_flipped = getattr(self.board_ui, 'flipped', False)
|
| |
|
| | threading.Thread(target=self.mirror.execute_move, args=(last_move, self.mirror_region, is_flipped), daemon=True).start()
|
| | except Exception as e:
|
| | print(f"Mirror error: {e}")
|
| |
|
| | if self.analysis_var.get():
|
| | self.update_analysis()
|
| |
|
| |
|
| | if self.two_player_var.get():
|
| |
|
| | turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
|
| | self.status_label.configure(text=f"{turn}'s Turn")
|
| | else:
|
| |
|
| | if self.game_state.board.turn == chess.BLACK:
|
| |
|
| | if not self.edit_mode_var.get() and not self.two_player_var.get():
|
| | self.status_label.configure(text="AI Thinking...")
|
| | threading.Thread(target=self.make_ai_move, daemon=True).start()
|
| | else:
|
| | self.status_label.configure(text="Your Turn (White)")
|
| |
|
| | def make_ai_move(self):
|
| | if self.edit_mode_var.get():
|
| | return
|
| |
|
| |
|
| | best_move = self.engine.get_best_move(self.game_state.get_fen())
|
| | if best_move:
|
| | self.game_state.board.push(best_move)
|
| | self.after(0, self.update_board_after_ai)
|
| | else:
|
| | self.after(0, lambda: self.status_label.configure(text="Engine Error"))
|
| |
|
| | def update_board_after_ai(self):
|
| | self.board_ui.draw_board()
|
| | self.status_label.configure(text="Your Turn")
|
| |
|
| | if self.analysis_var.get():
|
| | self.update_analysis()
|
| |
|
| | if self.game_state.is_game_over():
|
| | self.status_label.configure(text=f"Game Over: {self.game_state.board.result()}")
|
| |
|
| | def force_ai_move(self):
|
| | """Force the AI to make a move for the current side."""
|
| | if self.game_state.is_game_over():
|
| | return
|
| |
|
| | self.status_label.configure(text="Forcing AI Move...")
|
| | threading.Thread(target=self.make_ai_move, daemon=True).start()
|
| |
|