File size: 23,378 Bytes
57f1366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
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)  # Minimum window size
        
        # Initialize Logic
        self.game_state = GameState()
        self.engine = EngineHandler()
        self.vision = VisionHandler()
        self.mirror = MirrorHandler()
        
        # Screen Mirroring State
        self.mirroring = False
        self.mirror_region = None
        self.projection_overlay = None
        
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=0) # Sidebar
        self.grid_columnconfigure(1, weight=1) # Main Content
        
        # Sidebar
        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() # Hidden by default

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

        # Content Area
        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)

        # Init Engine
        # Defer execution to ensure mainloop is running before thread tries to callback
        self.after(200, self.init_engine_thread)
        
        # Sidebar Toggle State
        self.sidebar_visible = True
        
        # Sidebar Toggle Button (Floating)
        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)

        # Current Board View
        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") # Blend in more when closed? Or keep visible?
            # When closed, content takes full width. 
            # self.grid_columnconfigure(0, weight=0) is already set for sidebar column.
            # But the sidebar frame is gone, so column 0 collapses.
        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() # Stop any active mirroring
        
        # Clear content
        for widget in self.content_frame.winfo_children():
            widget.destroy()
            
        self.game_state.reset()
        
        # Controls Frame (horizontal layout for controls)
        self.controls_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
        self.controls_frame.grid(row=1, column=0, pady=10, sticky="ew")
        
        # Left controls
        left_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
        left_frame.pack(side="left", padx=20)
        
        # Play As selector
        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)

        # First Move selector
        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)
        
        # Flip Board button
        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 controls
        right_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
        right_frame.pack(side="right", padx=20)
        
        # Best Moves count
        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)
        
        # Board UI
        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
        toggles_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
        toggles_frame.grid(row=2, column=0, pady=5)
        
        # Analysis Toggle
        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)
        
        # Two Player Mode Toggle
        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)
        
        # Force Move Button (Initially hidden or shown based on mode? Best to just have it handy)
        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)

        # Edit Mode Toggle
        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)
        
        # Piece Palette (Hidden by default)
        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()

        # Score display label
        self.score_label = ctk.CTkLabel(self.content_frame, text="", font=("Arial", 12))
        self.score_label.grid(row=4, column=0, pady=5)
        
        # Bind move event
        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() # Hide main win
        
        # Function called when region is selected
        def on_selection(region):
            self.deiconify() # Show main win
            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() # Show stop button
        
        # Clear content and show mirroring UI
        # We can actually KEEP the board UI for mirroring, so the user can play!
        # The original analysis mode cleared everything. Mirroring implies playing locally.
        
        # Just update status
        self.status_label.configure(text="Mode: Screen Mirroring Active")
        self.stop_btn.grid()
        
        # Optional: Show overlay on the target region to confirm?
        self.projection_overlay = ProjectionOverlay(region)
        # Maybe draw a box or something? For now just keep it simple.

    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")
        
        # Restore normal status text if game is ongoing
        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")
            # Clear AI thinking text if any
            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 # Do not reset if editing
        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, "β™š")
        ]
        
        # White pieces row
        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)
            
        # Black pieces row
        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 / Delete
        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 # Clear selection
        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")
            # Resume game logic state
            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 # Do not reset if editing
        self.reset_game()

    def reset_game(self):
        """Reset the game state based on current controls."""
        self.game_state.reset()
        
        # Set turn based on First Move selection
        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":
            # Flip board so black is at bottom
            self.board_ui.flipped = True
        else:
            self.board_ui.flipped = False
            
        # Check if AI should move
        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)
        
        # Update score label
        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"]
                # Convert score to more readable format
                if abs(score) >= 9000:
                    # Mate score
                    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 top move is mate
                         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 no mate found in top moves, ensure status doesn't stick (unless editing/thinking)
            if not found_mate and not self.mirroring and not self.edit_mode_var.get() and "Thinking" not in self.status_label.cget("text"):
                 # Restore turn status
                 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")
                 # Else AI turn text is handled by 'AI Thinking' or 'Your Turn'
                 
        elif hasattr(self, 'score_label'):
            self.score_label.configure(text="")

    def on_move_made(self, event=None):
        # Check for game over
        if self.game_state.is_game_over():
            result = self.game_state.board.result()
            self.status_label.configure(text=f"Game Over: {result}")
            # Do NOT return here, we might still want to mirror the last move that caused game over?
            # Actually if game is over, we still want to update UI.
            # But let's act normal.
            
        # MIRROR LOGIC
        if self.mirroring and event:
             # Just made a move.
             # The event might not carry the move info directly if it's a generic binding, 
             # but we can get the last move from the board stack.
             try:
                 if self.game_state.board.move_stack:
                     last_move = self.game_state.board.peek()
                     # If it's a move made by the player (or AI if we want to mirror AI too)
                     # For now, let's mirror ALL moves (Player and AI) so the external board stays in sync?
                     # User said "whatever i move ... it will mirroe".
                     # If AI moves on local board, user probably wants that on external board too if playing against external opponent?
                     # OR if playing vs AI locally, and mirroring to analysis board.
                     # Let's mirror everything.
                     
                     # Need to know if we are 'flipped' on the external board?
                     # User selects one region. We assume it matches our orientation?
                     # User request: "make the mirror doenst matter if i am black of white... it will mirror i move on the right"
                     # This implies they want the Target Orientation to MATCH the Local Orientation.
                     # If I am flipped (Black), Target is flipped (Black).
                     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()

        # Check if two player mode
        if self.two_player_var.get():
            # Two player mode - just update turn indicator
            turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
            self.status_label.configure(text=f"{turn}'s Turn")
        else:
            # vs AI mode
            if self.game_state.board.turn == chess.BLACK:
                # Trigger AI move
                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
            
        # AI plays best move
        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()