algorembrant commited on
Commit
57f1366
·
verified ·
1 Parent(s): 6273d05

Upload 18 files

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ stockfish.exe/*.exe
2
+ *.exe
3
+ __pycache__/
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 callmerem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CheckerChesser
2
+
3
+ CheckerChesser is a powerful chess utility application that combines a local chess analysis board with real-time screen monitoring capabilities. It is designed to assist players in analyzing games, finding best moves, and testing positions.
4
+
5
+ <img width="1354" height="680" alt="image" src="https://github.com/user-attachments/assets/dc902cf0-a31c-4abf-8f85-9dda475eaac8" />
6
+ <img width="1323" height="719" alt="image" src="https://github.com/user-attachments/assets/1dc5c833-bf2b-4562-a5f7-8630c72fefab" />
7
+ <img width="1347" height="675" alt="image" src="https://github.com/user-attachments/assets/4005f25a-b1ef-42a6-a732-cad55ea27a32" />
8
+
9
+ <img width="587" height="187" alt="image" src="https://github.com/user-attachments/assets/85ca3a62-fdd3-4e7d-ad34-058520587279" />
10
+ <img width="1366" height="681" alt="image" src="https://github.com/user-attachments/assets/3935066d-b436-486a-8c40-813bf5b3bed0" />
11
+ <img width="1363" height="681" alt="image" src="https://github.com/user-attachments/assets/07a27629-3aef-4d9b-a4fc-6e16633dfb7b" />
12
+
13
+ <img width="1366" height="715" alt="image" src="https://github.com/user-attachments/assets/e82defd9-f2fc-4f9b-9172-2b107105dec2" /><br><br>
14
+
15
+ _So yeah, it was so good I got banned_
16
+ <img width="863" height="115" alt="image" src="https://github.com/user-attachments/assets/2803a361-f8dd-4160-97a8-4f4d70756967" /><br><br>
17
+
18
+ **_All that grind for nothing lol!_**
19
+
20
+ <img width="331" height="126" alt="image" src="https://github.com/user-attachments/assets/e4b20a49-4d3b-4424-b24e-5fdafece9185" />
21
+
22
+
23
+
24
+
25
+ ## Core Features
26
+
27
+ ### 1. Local Analysis Board
28
+ - **Full Chess Engine Integration**: Powered by **Stockfish**, one of the strongest chess engines in the world.
29
+ - **Interactive Board**: Move pieces, flip the board, and play against the AI.
30
+ - **Dual Player Mode**: Play locally against a friend or test moves for both sides.
31
+ - **Analysis Visualization**: Visualize top engine moves with colored arrows indicating strength and threat.
32
+
33
+ ### 2. Board Editor
34
+ - **Custom Scenario Setup**: Manually place any piece on the board to recreate specific game states.
35
+ - **Automation Control**: The engine is strictly disabled during editing to prevent interference.
36
+ - **Piece Palette**: Intuitive drag-and-drop style interface (click-to-place) for all piece types.
37
+
38
+ ### 3. Screen Analysis (Vision)
39
+ - **Real-time Monitoring**: Define a region on your screen to monitor a live chess board (e.g., from a website or video).
40
+ - **Auto-Calibration**: The application automatically detects the board structure from a standard starting position.
41
+ - **Live suggestions**: As moves are played on the screen, the app updates the internal board and suggests the best response instantly.
42
+
43
+ ### 4. Turn Swapping & Customization
44
+ - **First Move Control**: Choose whether White or Black moves first (useful for variants or playing as Black from the start).
45
+ - **Play As**: Switch sides to play as Black or White against the engine.
46
+
47
+ ---
48
+
49
+ ## Technical Architecture & Machine Learning
50
+
51
+ ### Is it based on Machine Learning?
52
+ **Yes and No.**
53
+
54
+ 1. **Chess Intelligence (Yes)**: The application utilizes **Stockfish**, which employs **NNUE (Efficiently Updatable Neural Network)** technology. This is a form of machine learning where a neural network attempts to evaluate positions, providing superior performance over classical hand-crafted evaluation functions.
55
+ 2. **Vision System (No)**: The screen analysis feature uses **Computer Vision** techniques (specifically Template Matching and Mean Squared Error comparisons), not Deep Learning. It requires a clear view of a 2D chess board to "calibrate" and match pieces against a known template.
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ 1. **Prerequisites**:
62
+ - Python 3.8+
63
+ - Stockfish Engine (Executable must be placed in the project folder or specified in config).
64
+
65
+ 2. **Dependencies**:
66
+ Install the required Python packages:
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ ```
70
+ *(Requires: customtkinter, python-chess, mss, opencv-python, numpy)*
71
+
72
+ 3. **Setup (Stockfish Guide)**:
73
+ This application requires the **Stockfish Chess Engine** to function.
74
+
75
+ **Step-by-Step Guide:**
76
+ 1. **Download**: Visit the official Stockfish website: [https://stockfishchess.org/download/](https://stockfishchess.org/download/)
77
+ 2. **Select Version**: Download the version suitable for your system (usually "Windows (AVX2)" or "Windows (x64-modern)").
78
+ 3. **Extract**: Unzip the downloaded folder. Inside, you will find an executable file (e.g., `stockfish-windows-x86-64-avx2.exe`).
79
+ 4. **Rename**: Rename this file to simply `stockfish.exe`.
80
+ 5. **Place File**: Move `stockfish.exe` directly into the `CheckerChesser` project folder.
81
+
82
+ **Correct Folder Structure:**
83
+ ```text
84
+ CheckerChesser/
85
+ ├── src/
86
+ │ ├── __pycache__/
87
+ │ ├── __init__.py
88
+ │ ├── board_ui.py
89
+ │ ├── engine.py
90
+ │ ├── game_state.py
91
+ │ ├── gui.py
92
+ │ ├── mirror.py
93
+ │ ├── overlay.py
94
+ │ └── vision.py
95
+ ├── tests/
96
+ ├── main.py
97
+ ├── requirements.txt
98
+ ├── stockfish.exe/ <-- PLACE HERE (folder named btw)
99
+ ├── LICENSE
100
+ └── README.md
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Usage Guide
106
+
107
+ ### Starting the App
108
+ Run the main script:
109
+ ```bash
110
+ python main.py
111
+ ```
112
+
113
+ ### Modes
114
+
115
+ #### Local Game
116
+ - **Play**: Drag and drop or click squares to move pieces.
117
+ - **Analysis Mode**: Toggle `Analysis Mode` switch to see Best Move arrows overlaid on the board.
118
+ - **Two Player**: Toggle `Two Player Mode` to control both sides manually.
119
+ - **Flip Board**: Click `⟳ Flip Board` to rotate the view.
120
+
121
+ #### Board Editor
122
+ 1. Toggle `Edit Mode` switch to **ON**.
123
+ 2. A palette of pieces (White/Black) and a Trash Bin will appear.
124
+ 3. **Left Click** a piece in the palette to select it.
125
+ 4. **Left Click** any square on the board to place that piece.
126
+ 5. Select the **Trash Bin** and click a square to clear it.
127
+ 6. Toggle `Edit Mode` **OFF** to resume play.
128
+ > **Note**: AI logic is paused while Edit Mode is active.
129
+
130
+ #### Screen Analysis
131
+ 1. Click `Screen Analysis` in the sidebar.
132
+ 2. An overlay window will appear. Drag to select the area of the chess board you want to monitor.
133
+ 3. **Important**: Ensure the board on screen is at the **Starting Position** when you start monitoring. The tool needs this to calibrate piece textures.
134
+ 4. Once calibrated, the app will track moves and display the best engine move in real-time.
135
+
136
+ ---
137
+
138
+ ## Troubleshooting
139
+
140
+ - **Engine Not Found**: Ensure `stockfish.exe` is in the folder.
141
+ - **Vision Not Working**: Make sure the board on screen is not obstructed and matches standard 2D chess pieces. Glare or unusual piece sets may confuse the template matcher.
142
+ - **Performance**: High `Best Moves` count or deep analysis may use significant CPU resources. Lower the number of moves if the app lags.
143
+
144
+
145
+
146
+ ## Citation
147
+
148
+ ```bibtex
149
+ @misc{CheckerChesser,
150
+ author = {algorembrant},
151
+ title = {CheckerChesser},
152
+ year = {2026},
153
+ publisher = {GitHub},
154
+ journal = {GitHub repository},
155
+ howpublished = {\url{https://github.com/algorembrant/CheckerChesser}},
156
+ }
157
+ ```
STRUCTURE.md ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Structure
2
+
3
+ ```text
4
+ CheckerChesser/
5
+ ├── src/
6
+ │ ├── __init__.py
7
+ │ ├── board_ui.py
8
+ │ ├── engine.py
9
+ │ ├── game_state.py
10
+ │ ├── gui.py
11
+ │ ├── mirror.py
12
+ │ ├── overlay.py
13
+ │ └── vision.py
14
+ ├── tests/
15
+ │ ├── test_engine.py
16
+ │ ├── test_engine_manual.py
17
+ │ └── test_mirror.py
18
+ ├── .gitignore
19
+ ├── LICENSE
20
+ ├── main.py
21
+ ├── README.md
22
+ ├── requirements.txt
23
+ └── TECHSTACK.md
24
+ ```
TECHSTACK.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Techstack
2
+
3
+ Audit of **CheckerChesser** project files (excluding environment and cache):
4
+
5
+ | File Type | Count | Size (KB) |
6
+ | :--- | :--- | :--- |
7
+ | Python (.py) | 12 | 55.9 |
8
+ | (no extension) | 2 | 1.1 |
9
+ | Markdown (.md) | 1 | 7.1 |
10
+ | Plain Text (.txt) | 1 | 0.1 |
11
+ | **Total** | **16** | **64.2** |
main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import customtkinter as ctk
2
+ from src.gui import ChessApp
3
+
4
+ if __name__ == "__main__":
5
+ ctk.set_appearance_mode("Dark")
6
+ ctk.set_default_color_theme("blue")
7
+
8
+ app = ChessApp()
9
+ app.mainloop()
10
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ customtkinter
2
+ python-chess
3
+ opencv-python
4
+ pillow
5
+ mss
6
+ numpy
7
+ paramiko
8
+ pyglet
9
+ pywin32
10
+ pyautogui
src/__init__.py ADDED
File without changes
src/board_ui.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import customtkinter as ctk
2
+ import chess
3
+
4
+ class BoardUI(ctk.CTkFrame):
5
+ def __init__(self, master, game_state, **kwargs):
6
+ super().__init__(master, **kwargs)
7
+ self.game_state = game_state
8
+ self.canvas = ctk.CTkCanvas(self, bg="#302E2B", highlightthickness=0)
9
+ self.canvas.pack(fill="both", expand=True)
10
+
11
+ self.square_size = 50 # Will be recalculated on resize
12
+ self.selected_square = None
13
+ self.pieces = {}
14
+ self.flipped = False # Board orientation
15
+ self.edit_mode = False
16
+ self.selected_edit_piece = None # Piece to place in edit mode (None = Delete)
17
+ self.last_analysis_moves = [] # cache for redrawing arrows
18
+
19
+ self.canvas.bind("<Button-1>", self.on_click)
20
+ self.canvas.bind("<Configure>", self.on_resize)
21
+
22
+ def on_resize(self, event):
23
+ # Calculate square size based on available space (use smaller dimension)
24
+ size = min(event.width, event.height)
25
+ self.square_size = size // 8
26
+ self.draw_board()
27
+
28
+ def get_visual_coords(self, file, rank):
29
+ """Convert chess file/rank to visual coordinates accounting for flip."""
30
+ if self.flipped:
31
+ visual_file = 7 - file
32
+ visual_rank = rank # When flipped, rank 0 (row 1) is at top
33
+ else:
34
+ visual_file = file
35
+ visual_rank = 7 - rank # Normal: rank 7 (row 8) is at top
36
+ return visual_file, visual_rank
37
+
38
+ def get_chess_square_from_visual(self, visual_file, visual_rank):
39
+ """Convert visual coordinates to chess square."""
40
+ if self.flipped:
41
+ file = 7 - visual_file
42
+ rank = visual_rank
43
+ else:
44
+ file = visual_file
45
+ rank = 7 - visual_rank
46
+ return chess.square(file, rank)
47
+
48
+ def draw_board(self):
49
+ self.canvas.delete("all")
50
+ colors = ["#EBECD0", "#779556"] # Light, Dark squares
51
+
52
+ # Center the board if window is not square
53
+ canvas_w = self.canvas.winfo_width()
54
+ canvas_h = self.canvas.winfo_height()
55
+ board_size = self.square_size * 8
56
+ offset_x = (canvas_w - board_size) // 2
57
+ offset_y = (canvas_h - board_size) // 2
58
+
59
+ self.offset_x = max(0, offset_x)
60
+ self.offset_y = max(0, offset_y)
61
+
62
+ for visual_rank in range(8):
63
+ for visual_file in range(8):
64
+ color = colors[(visual_rank + visual_file) % 2]
65
+ x1 = self.offset_x + visual_file * self.square_size
66
+ y1 = self.offset_y + visual_rank * self.square_size
67
+ x2 = x1 + self.square_size
68
+ y2 = y1 + self.square_size
69
+
70
+ self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline="")
71
+
72
+ # Get chess square for this visual position
73
+ chess_square = self.get_chess_square_from_visual(visual_file, visual_rank)
74
+ piece = self.game_state.board.piece_at(chess_square)
75
+
76
+ if piece:
77
+ self.draw_piece(x1, y1, piece)
78
+
79
+ # Highlight King if in Check
80
+ if self.game_state.board.is_check():
81
+ self.highlight_king_check()
82
+
83
+ if self.selected_square is not None:
84
+ self.highlight_square(self.selected_square)
85
+
86
+ # Redraw arrows if they exist
87
+ if self.last_analysis_moves:
88
+ self.display_analysis(self.last_analysis_moves, cache=False)
89
+
90
+ def draw_piece(self, x, y, piece):
91
+ symbol = piece.unicode_symbol()
92
+ font_size = int(self.square_size * 0.7)
93
+ fill_color = "#1a1a1a" if piece.color == chess.BLACK else "#ffffff"
94
+
95
+ # Add shadow for better visibility
96
+ shadow_offset = max(1, int(self.square_size * 0.02))
97
+ self.canvas.create_text(
98
+ x + self.square_size/2 + shadow_offset,
99
+ y + self.square_size/2 + shadow_offset,
100
+ text=symbol,
101
+ font=("Segoe UI Symbol", font_size, "bold"),
102
+ fill="gray30"
103
+ )
104
+ self.canvas.create_text(
105
+ x + self.square_size/2,
106
+ y + self.square_size/2,
107
+ text=symbol,
108
+ font=("Segoe UI Symbol", font_size, "bold"),
109
+ fill=fill_color
110
+ )
111
+
112
+ def highlight_king_check(self):
113
+ """Highlight the King's square in red if in check."""
114
+ king_square = self.game_state.board.king(self.game_state.board.turn)
115
+ if king_square is not None:
116
+ file = chess.square_file(king_square)
117
+ rank = chess.square_rank(king_square)
118
+ visual_file, visual_rank = self.get_visual_coords(file, rank)
119
+
120
+ x1 = self.offset_x + visual_file * self.square_size
121
+ y1 = self.offset_y + visual_rank * self.square_size
122
+ x2 = x1 + self.square_size
123
+ y2 = y1 + self.square_size
124
+
125
+ # Create a red glow/border
126
+ self.canvas.create_oval(x1+2, y1+2, x2-2, y2-2, outline="#FF0000", width=4, tag="check")
127
+ self.canvas.create_rectangle(x1, y1, x2, y2, fill="red", stipple="gray25", outline="")
128
+
129
+ def highlight_square(self, square):
130
+ file = chess.square_file(square)
131
+ rank = chess.square_rank(square)
132
+ visual_file, visual_rank = self.get_visual_coords(file, rank)
133
+
134
+ x1 = self.offset_x + visual_file * self.square_size
135
+ y1 = self.offset_y + visual_rank * self.square_size
136
+ x2 = x1 + self.square_size
137
+ y2 = y1 + self.square_size
138
+
139
+ self.canvas.create_rectangle(x1, y1, x2, y2, fill="yellow", stipple="gray50", outline="gold", width=2)
140
+
141
+ def on_click(self, event):
142
+ # Adjust for offset
143
+ adj_x = event.x - getattr(self, 'offset_x', 0)
144
+ adj_y = event.y - getattr(self, 'offset_y', 0)
145
+
146
+ if adj_x < 0 or adj_y < 0:
147
+ return
148
+
149
+ visual_file = int(adj_x // self.square_size)
150
+ visual_rank = int(adj_y // self.square_size)
151
+
152
+ if visual_file < 0 or visual_file > 7 or visual_rank < 0 or visual_rank > 7:
153
+ return
154
+
155
+ chess_square = self.get_chess_square_from_visual(visual_file, visual_rank)
156
+
157
+ if self.edit_mode:
158
+ # Edit Mode Logic
159
+ self.game_state.set_piece(chess_square, self.selected_edit_piece)
160
+ self.draw_board()
161
+ return
162
+
163
+ # Normal Play Logic
164
+ if self.selected_square is None:
165
+ piece = self.game_state.board.piece_at(chess_square)
166
+ if piece and piece.color == self.game_state.board.turn:
167
+ self.selected_square = chess_square
168
+ self.draw_board()
169
+ else:
170
+ # Try to move
171
+ move = chess.Move(self.selected_square, chess_square)
172
+ # Check for promotion
173
+ piece_at_start = self.game_state.board.piece_at(self.selected_square)
174
+ if piece_at_start and piece_at_start.piece_type == chess.PAWN:
175
+ if (self.game_state.board.turn == chess.WHITE and chess.square_rank(chess_square) == 7) or \
176
+ (self.game_state.board.turn == chess.BLACK and chess.square_rank(chess_square) == 0):
177
+ move = chess.Move(self.selected_square, chess_square, promotion=chess.QUEEN)
178
+
179
+ if move in self.game_state.board.legal_moves:
180
+ self.game_state.board.push(move)
181
+ self.selected_square = None
182
+ self.draw_board()
183
+ self.master.event_generate("<<MoveMade>>")
184
+ else:
185
+ piece = self.game_state.board.piece_at(chess_square)
186
+ if piece and piece.color == self.game_state.board.turn:
187
+ self.selected_square = chess_square
188
+ self.draw_board()
189
+ else:
190
+ self.selected_square = None
191
+ self.draw_board()
192
+
193
+ def draw_arrow(self, start_sq, end_sq, color="#00FF00", width=4):
194
+ # Get visual coordinates for arrow
195
+ start_file = chess.square_file(start_sq)
196
+ start_rank = chess.square_rank(start_sq)
197
+ end_file = chess.square_file(end_sq)
198
+ end_rank = chess.square_rank(end_sq)
199
+
200
+ start_vf, start_vr = self.get_visual_coords(start_file, start_rank)
201
+ end_vf, end_vr = self.get_visual_coords(end_file, end_rank)
202
+
203
+ x1 = self.offset_x + start_vf * self.square_size + self.square_size // 2
204
+ y1 = self.offset_y + start_vr * self.square_size + self.square_size // 2
205
+ x2 = self.offset_x + end_vf * self.square_size + self.square_size // 2
206
+ y2 = self.offset_y + end_vr * self.square_size + self.square_size // 2
207
+
208
+ self.canvas.create_line(x1, y1, x2, y2, fill=color, width=width, arrow="last", arrowshape=(16, 20, 6), tag="arrow")
209
+
210
+ def display_analysis(self, top_moves, cache=True):
211
+ if cache:
212
+ self.last_analysis_moves = top_moves
213
+
214
+ self.canvas.delete("arrow")
215
+
216
+ colors = ["#00FF00", "#00FFFF", "#FFFF00"]
217
+
218
+ for i, move_data in enumerate(top_moves):
219
+ if i >= len(colors):
220
+ break
221
+ move = move_data["move"]
222
+ self.draw_arrow(move.from_square, move.to_square, color=colors[i], width=6 - i)
src/engine.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chess
2
+ import chess.engine
3
+ import os
4
+ import threading
5
+
6
+ class EngineHandler:
7
+ def __init__(self, engine_path="stockfish.exe"):
8
+ self.engine_path = engine_path
9
+ self.engine = None
10
+ self.lock = threading.Lock() # Prevent concurrent engine access
11
+
12
+ def initialize_engine(self):
13
+ # 1. Check if path exists
14
+ if not os.path.exists(self.engine_path):
15
+ return False, f"Engine not found at {self.engine_path}. Please place 'stockfish.exe' in the project folder."
16
+
17
+ # 2. If it is a directory, search for an executable inside
18
+ final_path = self.engine_path
19
+ if os.path.isdir(self.engine_path):
20
+ found_exe = None
21
+ for root, dirs, files in os.walk(self.engine_path):
22
+ for file in files:
23
+ if "stockfish" in file.lower() and file.lower().endswith(".exe"):
24
+ found_exe = os.path.join(root, file)
25
+ break
26
+ if found_exe:
27
+ break
28
+
29
+ if found_exe:
30
+ final_path = found_exe
31
+ else:
32
+ return False, f"Directory found at {self.engine_path}, but no 'stockfish' executable was found inside."
33
+
34
+ try:
35
+ self.engine = chess.engine.SimpleEngine.popen_uci(final_path)
36
+ return True, f"Engine initialized successfully ({os.path.basename(final_path)})."
37
+ except PermissionError:
38
+ return False, f"Permission denied accessing {self.engine_path}. Try running as Administrator or check file properties."
39
+ except Exception as e:
40
+ return False, f"Failed to initialize engine: {e}"
41
+
42
+ def get_best_move(self, fen, time_limit=1.0):
43
+ if not self.engine:
44
+ return None
45
+
46
+ with self.lock:
47
+ try:
48
+ board = chess.Board(fen)
49
+ result = self.engine.play(board, chess.engine.Limit(time=time_limit))
50
+ return result.move
51
+ except Exception as e:
52
+ import traceback
53
+ traceback.print_exc()
54
+ print(f"Error getting best move: {e!r}")
55
+ # Try to reinitialize engine if it died
56
+ self._try_reinit()
57
+ return None
58
+
59
+ def _try_reinit(self):
60
+ """Attempt to reinitialize the engine if it crashed."""
61
+ try:
62
+ if self.engine:
63
+ try:
64
+ self.engine.quit()
65
+ except:
66
+ pass
67
+ self.engine = None
68
+ self.initialize_engine()
69
+ except:
70
+ pass
71
+
72
+ def get_top_moves(self, fen, limit=3, time_limit=1.0):
73
+ if not self.engine:
74
+ return []
75
+
76
+ with self.lock:
77
+ try:
78
+ board = chess.Board(fen)
79
+ info = self.engine.analyse(board, chess.engine.Limit(time=time_limit), multipv=limit)
80
+
81
+ if isinstance(info, dict):
82
+ info = [info]
83
+
84
+ top_moves = []
85
+ for i, line in enumerate(info):
86
+ if "pv" in line:
87
+ move = line["pv"][0]
88
+ score = line["score"].relative.score(mate_score=10000)
89
+ top_moves.append({
90
+ "rank": i + 1,
91
+ "move": move,
92
+ "score": score,
93
+ "pv": line["pv"]
94
+ })
95
+ return top_moves
96
+
97
+ except Exception as e:
98
+ print(f"Error analyzing: {e}")
99
+ self._try_reinit()
100
+ return []
101
+
102
+ def quit(self):
103
+ if self.engine:
104
+ try:
105
+ self.engine.quit()
106
+ except:
107
+ pass
108
+
109
+ def get_evaluation(self, fen):
110
+ if not self.engine:
111
+ return None
112
+
113
+ with self.lock:
114
+ try:
115
+ board = chess.Board(fen)
116
+ info = self.engine.analyse(board, chess.engine.Limit(depth=15))
117
+ return info["score"].relative.score(mate_score=10000)
118
+ except Exception as e:
119
+ print(f"Error in evaluation: {e}")
120
+ return None
src/game_state.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import chess
2
+
3
+ class GameState:
4
+ def __init__(self):
5
+ self.board = chess.Board()
6
+
7
+ def reset(self):
8
+ self.board.reset()
9
+
10
+ def make_move(self, uci_move):
11
+ try:
12
+ move = chess.Move.from_uci(uci_move)
13
+ if move in self.board.legal_moves:
14
+ self.board.push(move)
15
+ return True
16
+ return False
17
+ except:
18
+ return False
19
+
20
+ def set_piece(self, square, piece):
21
+ """Set a piece on the board directly."""
22
+ self.board.set_piece_at(square, piece)
23
+
24
+ def get_fen(self):
25
+ return self.board.fen()
26
+
27
+ def is_game_over(self):
28
+ return self.board.is_game_over()
src/gui.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import customtkinter as ctk
2
+ import threading
3
+ import chess
4
+ import time
5
+ from src.game_state import GameState
6
+ from src.engine import EngineHandler
7
+ from src.board_ui import BoardUI
8
+ from src.overlay import SelectionOverlay, ProjectionOverlay
9
+ from src.vision import VisionHandler
10
+ from src.mirror import MirrorHandler
11
+
12
+ class ChessApp(ctk.CTk):
13
+ def __init__(self):
14
+ super().__init__()
15
+
16
+ self.title("CheckerChesser")
17
+ self.geometry("900x700")
18
+ self.minsize(600, 500) # Minimum window size
19
+
20
+ # Initialize Logic
21
+ self.game_state = GameState()
22
+ self.engine = EngineHandler()
23
+ self.vision = VisionHandler()
24
+ self.mirror = MirrorHandler()
25
+
26
+ # Screen Mirroring State
27
+ self.mirroring = False
28
+ self.mirror_region = None
29
+ self.projection_overlay = None
30
+
31
+ self.grid_rowconfigure(0, weight=1)
32
+ self.grid_columnconfigure(0, weight=0) # Sidebar
33
+ self.grid_columnconfigure(1, weight=1) # Main Content
34
+
35
+ # Sidebar
36
+ self.sidebar = ctk.CTkFrame(self, width=200, corner_radius=0)
37
+ self.sidebar.grid(row=0, column=0, sticky="nsew")
38
+
39
+ self.logo_label = ctk.CTkLabel(self.sidebar, text="CheckerChesser", font=ctk.CTkFont(size=20, weight="bold"))
40
+ self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
41
+
42
+ self.new_game_btn = ctk.CTkButton(self.sidebar, text="New Local Game", command=self.start_local_game)
43
+ self.new_game_btn.grid(row=1, column=0, padx=20, pady=10)
44
+
45
+ self.mirror_btn = ctk.CTkButton(self.sidebar, text="Screen Mirroring", command=self.start_screen_mirroring)
46
+ self.mirror_btn.grid(row=2, column=0, padx=20, pady=10)
47
+
48
+ self.stop_btn = ctk.CTkButton(self.sidebar, text="Stop Mirroring", command=self.stop_mirroring, fg_color="red", hover_color="darkred")
49
+ self.stop_btn.grid(row=3, column=0, padx=20, pady=10)
50
+ self.stop_btn.grid_remove() # Hidden by default
51
+
52
+ self.status_label = ctk.CTkLabel(self.sidebar, text="Status: Idle", anchor="w")
53
+ self.status_label.grid(row=4, column=0, padx=20, pady=(20, 0), sticky="ew")
54
+
55
+ # Content Area
56
+ self.content_frame = ctk.CTkFrame(self, corner_radius=0, fg_color="transparent")
57
+ self.content_frame.grid(row=0, column=1, sticky="nsew")
58
+ self.content_frame.grid_rowconfigure(0, weight=1)
59
+ self.content_frame.grid_columnconfigure(0, weight=1)
60
+
61
+ # Init Engine
62
+ # Defer execution to ensure mainloop is running before thread tries to callback
63
+ self.after(200, self.init_engine_thread)
64
+
65
+ # Sidebar Toggle State
66
+ self.sidebar_visible = True
67
+
68
+ # Sidebar Toggle Button (Floating)
69
+ self.sidebar_toggle_btn = ctk.CTkButton(self, text="☰", width=30, height=30,
70
+ command=self.toggle_sidebar,
71
+ fg_color="gray20", hover_color="gray30")
72
+ self.sidebar_toggle_btn.place(x=10, y=10)
73
+
74
+ # Current Board View
75
+ self.board_ui = None
76
+ self.start_local_game()
77
+
78
+ def toggle_sidebar(self):
79
+ if self.sidebar_visible:
80
+ self.sidebar.grid_remove()
81
+ self.sidebar_visible = False
82
+ self.sidebar_toggle_btn.configure(fg_color="transparent") # Blend in more when closed? Or keep visible?
83
+ # When closed, content takes full width.
84
+ # self.grid_columnconfigure(0, weight=0) is already set for sidebar column.
85
+ # But the sidebar frame is gone, so column 0 collapses.
86
+ else:
87
+ self.sidebar.grid()
88
+ self.sidebar_visible = True
89
+ self.sidebar_toggle_btn.configure(fg_color="gray20")
90
+
91
+ def init_engine_thread(self):
92
+ def _init():
93
+ success, msg = self.engine.initialize_engine()
94
+ self.after(0, lambda: self.status_label.configure(text="Engine: Ready" if success else "Engine: Not Found"))
95
+ if not success:
96
+ print(msg)
97
+ threading.Thread(target=_init, daemon=True).start()
98
+
99
+ def start_local_game(self):
100
+ self.stop_mirroring() # Stop any active mirroring
101
+
102
+ # Clear content
103
+ for widget in self.content_frame.winfo_children():
104
+ widget.destroy()
105
+
106
+ self.game_state.reset()
107
+
108
+ # Controls Frame (horizontal layout for controls)
109
+ self.controls_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
110
+ self.controls_frame.grid(row=1, column=0, pady=10, sticky="ew")
111
+
112
+ # Left controls
113
+ left_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
114
+ left_frame.pack(side="left", padx=20)
115
+
116
+ # Play As selector
117
+ self.play_as_var = ctk.StringVar(value="White")
118
+ play_as_label = ctk.CTkLabel(left_frame, text="Play as:")
119
+ play_as_label.pack(side="left", padx=(0, 5))
120
+ self.play_as_menu = ctk.CTkOptionMenu(left_frame, variable=self.play_as_var,
121
+ values=["White", "Black"],
122
+ command=self.on_play_as_change,
123
+ width=80)
124
+ self.play_as_menu.pack(side="left", padx=5)
125
+
126
+ # First Move selector
127
+ self.first_move_var = ctk.StringVar(value="White")
128
+ first_move_label = ctk.CTkLabel(left_frame, text="First Move:")
129
+ first_move_label.pack(side="left", padx=(10, 5))
130
+ self.first_move_menu = ctk.CTkOptionMenu(left_frame, variable=self.first_move_var,
131
+ values=["White", "Black"],
132
+ command=self.on_first_move_change,
133
+ width=80)
134
+ self.first_move_menu.pack(side="left", padx=5)
135
+
136
+ # Flip Board button
137
+ self.flip_btn = ctk.CTkButton(left_frame, text="⟳ Flip Board", command=self.flip_board, width=100)
138
+ self.flip_btn.pack(side="left", padx=10)
139
+
140
+ # Right controls
141
+ right_frame = ctk.CTkFrame(self.controls_frame, fg_color="transparent")
142
+ right_frame.pack(side="right", padx=20)
143
+
144
+ # Best Moves count
145
+ moves_label = ctk.CTkLabel(right_frame, text="Show Best Moves:")
146
+ moves_label.pack(side="left", padx=(0, 5))
147
+ self.best_moves_var = ctk.StringVar(value="3")
148
+ self.best_moves_menu = ctk.CTkOptionMenu(right_frame, variable=self.best_moves_var,
149
+ values=["1", "2", "3"],
150
+ command=self.on_best_moves_change,
151
+ width=60)
152
+ self.best_moves_menu.pack(side="left", padx=5)
153
+
154
+ # Board UI
155
+ self.board_ui = BoardUI(self.content_frame, self.game_state)
156
+ self.board_ui.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
157
+
158
+ # Toggles Frame
159
+ toggles_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
160
+ toggles_frame.grid(row=2, column=0, pady=5)
161
+
162
+ # Analysis Toggle
163
+ self.analysis_var = ctk.BooleanVar(value=False)
164
+ self.analysis_switch = ctk.CTkSwitch(toggles_frame, text="Analysis Mode",
165
+ variable=self.analysis_var, command=self.toggle_analysis)
166
+ self.analysis_switch.pack(side="left", padx=15)
167
+
168
+ # Two Player Mode Toggle
169
+ self.two_player_var = ctk.BooleanVar(value=False)
170
+ self.two_player_switch = ctk.CTkSwitch(toggles_frame, text="Two Player Mode",
171
+ variable=self.two_player_var, command=self.toggle_two_player)
172
+ self.two_player_switch.pack(side="left", padx=15)
173
+
174
+ # Force Move Button (Initially hidden or shown based on mode? Best to just have it handy)
175
+ self.force_move_btn = ctk.CTkButton(left_frame, text="⚡ Force Move", command=self.force_ai_move, width=100, fg_color="orange", hover_color="darkorange")
176
+ self.force_move_btn.pack(side="left", padx=10)
177
+
178
+ # Edit Mode Toggle
179
+ self.edit_mode_var = ctk.BooleanVar(value=False)
180
+ self.edit_mode_switch = ctk.CTkSwitch(toggles_frame, text="Edit Mode",
181
+ variable=self.edit_mode_var, command=self.toggle_edit_mode)
182
+ self.edit_mode_switch.pack(side="left", padx=15)
183
+
184
+ # Piece Palette (Hidden by default)
185
+ self.palette_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
186
+ self.palette_frame.grid(row=3, column=0, pady=5)
187
+ self.palette_frame.grid_remove()
188
+
189
+ self.init_palette()
190
+
191
+ # Score display label
192
+ self.score_label = ctk.CTkLabel(self.content_frame, text="", font=("Arial", 12))
193
+ self.score_label.grid(row=4, column=0, pady=5)
194
+
195
+ # Bind move event
196
+ self.bind("<<MoveMade>>", self.on_move_made)
197
+ self.status_label.configure(text="Mode: vs AI (White)")
198
+
199
+ def start_screen_mirroring(self):
200
+ self.status_label.configure(text="Select Region to Mirror to...")
201
+ self.withdraw() # Hide main win
202
+
203
+ # Function called when region is selected
204
+ def on_selection(region):
205
+ self.deiconify() # Show main win
206
+ self.begin_mirroring(region)
207
+
208
+ SelectionOverlay(self, on_selection)
209
+
210
+ def begin_mirroring(self, region):
211
+ """
212
+ Start mirroring moves to the selected region.
213
+ """
214
+ self.mirror_region = region
215
+ self.mirroring = True
216
+
217
+ self.stop_btn.grid() # Show stop button
218
+
219
+ # Clear content and show mirroring UI
220
+ # We can actually KEEP the board UI for mirroring, so the user can play!
221
+ # The original analysis mode cleared everything. Mirroring implies playing locally.
222
+
223
+ # Just update status
224
+ self.status_label.configure(text="Mode: Screen Mirroring Active")
225
+ self.stop_btn.grid()
226
+
227
+ # Optional: Show overlay on the target region to confirm?
228
+ self.projection_overlay = ProjectionOverlay(region)
229
+ # Maybe draw a box or something? For now just keep it simple.
230
+
231
+ def stop_mirroring(self):
232
+ self.mirroring = False
233
+ self.mirror_region = None
234
+
235
+ if self.projection_overlay:
236
+ self.projection_overlay.destroy()
237
+ self.projection_overlay = None
238
+
239
+ self.stop_btn.grid_remove()
240
+ self.status_label.configure(text="Mirroring Stopped")
241
+
242
+ # Restore normal status text if game is ongoing
243
+ if not self.game_state.is_game_over():
244
+ turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
245
+ self.status_label.configure(text=f"Your Turn ({turn_str})")
246
+
247
+ def toggle_analysis(self):
248
+ if self.analysis_var.get():
249
+ self.update_analysis()
250
+ else:
251
+ self.board_ui.canvas.delete("arrow")
252
+
253
+ def toggle_two_player(self):
254
+ if self.two_player_var.get():
255
+ self.status_label.configure(text="Mode: Two Player")
256
+ turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
257
+ self.status_label.configure(text=f"{turn}'s Turn")
258
+ # Clear AI thinking text if any
259
+ if "Thinking" in self.status_label.cget("text"):
260
+ self.status_label.configure(text=f"{turn}'s Turn")
261
+ else:
262
+ self.status_label.configure(text="Mode: vs AI (White)")
263
+ if self.game_state.board.turn == chess.WHITE:
264
+ self.status_label.configure(text="Your Turn (White)")
265
+
266
+ def flip_board(self):
267
+ """Flip the board orientation."""
268
+ if hasattr(self, 'board_ui') and self.board_ui:
269
+ self.board_ui.flipped = not getattr(self.board_ui, 'flipped', False)
270
+ self.board_ui.draw_board()
271
+
272
+ def on_play_as_change(self, value):
273
+ """Handle play as color change."""
274
+ if self.edit_mode_var.get():
275
+ return # Do not reset if editing
276
+ self.reset_game()
277
+
278
+ def init_palette(self):
279
+ """Initialize the piece palette for editing."""
280
+ pieces = [
281
+ (chess.PAWN, chess.WHITE, "♙"), (chess.KNIGHT, chess.WHITE, "♘"), (chess.BISHOP, chess.WHITE, "♗"),
282
+ (chess.ROOK, chess.WHITE, "♖"), (chess.QUEEN, chess.WHITE, "♕"), (chess.KING, chess.WHITE, "♔"),
283
+ (chess.PAWN, chess.BLACK, "♟"), (chess.KNIGHT, chess.BLACK, "♞"), (chess.BISHOP, chess.BLACK, "♝"),
284
+ (chess.ROOK, chess.BLACK, "♜"), (chess.QUEEN, chess.BLACK, "♛"), (chess.KING, chess.BLACK, "♚")
285
+ ]
286
+
287
+ # White pieces row
288
+ for i, (pt, color, symbol) in enumerate(pieces[:6]):
289
+ btn = ctk.CTkButton(self.palette_frame, text=symbol, width=40, font=("Segoe UI Symbol", 24),
290
+ command=lambda p=chess.Piece(pt, color): self.select_palette_piece(p))
291
+ btn.grid(row=0, column=i, padx=2, pady=2)
292
+
293
+ # Black pieces row
294
+ for i, (pt, color, symbol) in enumerate(pieces[6:]):
295
+ btn = ctk.CTkButton(self.palette_frame, text=symbol, width=40, font=("Segoe UI Symbol", 24),
296
+ command=lambda p=chess.Piece(pt, color): self.select_palette_piece(p))
297
+ btn.grid(row=1, column=i, padx=2, pady=2)
298
+
299
+ # Trash / Delete
300
+ trash_btn = ctk.CTkButton(self.palette_frame, text="🗑", width=40, font=("Segoe UI Symbol", 20), fg_color="red", hover_color="darkred",
301
+ command=lambda: self.select_palette_piece(None))
302
+ trash_btn.grid(row=0, column=6, rowspan=2, padx=5, sticky="ns")
303
+
304
+ def select_palette_piece(self, piece):
305
+ self.board_ui.selected_edit_piece = piece
306
+
307
+ def toggle_edit_mode(self):
308
+ is_edit = self.edit_mode_var.get()
309
+ self.board_ui.edit_mode = is_edit
310
+ self.board_ui.selected_square = None # Clear selection
311
+ self.board_ui.draw_board()
312
+
313
+ if is_edit:
314
+ self.palette_frame.grid()
315
+ self.status_label.configure(text="Edit Mode: Select piece to place")
316
+ else:
317
+ self.palette_frame.grid_remove()
318
+ self.status_label.configure(text="Edit Mode Disabled")
319
+ # Resume game logic state
320
+ turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
321
+ self.status_label.configure(text=f"Your Turn ({turn_str})")
322
+
323
+ def on_first_move_change(self, value):
324
+ """Handle first move color change."""
325
+ if self.edit_mode_var.get():
326
+ return # Do not reset if editing
327
+ self.reset_game()
328
+
329
+ def reset_game(self):
330
+ """Reset the game state based on current controls."""
331
+ self.game_state.reset()
332
+
333
+ # Set turn based on First Move selection
334
+ first_move_color = chess.BLACK if self.first_move_var.get() == "Black" else chess.WHITE
335
+ self.game_state.board.turn = first_move_color
336
+
337
+ play_as = self.play_as_var.get()
338
+
339
+ if play_as == "Black":
340
+ # Flip board so black is at bottom
341
+ self.board_ui.flipped = True
342
+ else:
343
+ self.board_ui.flipped = False
344
+
345
+ # Check if AI should move
346
+ ai_color = chess.WHITE if play_as == "Black" else chess.BLACK
347
+
348
+ if self.game_state.board.turn == ai_color and not self.two_player_var.get():
349
+ self.status_label.configure(text="AI Thinking...")
350
+ threading.Thread(target=self.make_ai_move, daemon=True).start()
351
+ else:
352
+ turn_str = "White" if self.game_state.board.turn == chess.WHITE else "Black"
353
+ self.status_label.configure(text=f"Your Turn ({turn_str})")
354
+
355
+ self.board_ui.draw_board()
356
+
357
+ def on_best_moves_change(self, value):
358
+ """Handle best moves count change."""
359
+ if self.analysis_var.get():
360
+ self.update_analysis()
361
+
362
+ def update_analysis(self):
363
+ if not self.analysis_var.get():
364
+ return
365
+
366
+ def _analyze():
367
+ fen = self.game_state.get_fen()
368
+ limit = int(self.best_moves_var.get()) if hasattr(self, 'best_moves_var') else 3
369
+ top_moves = self.engine.get_top_moves(fen, limit=limit)
370
+ self.after(0, lambda: self.display_analysis_results(top_moves))
371
+
372
+ threading.Thread(target=_analyze, daemon=True).start()
373
+
374
+ def display_analysis_results(self, top_moves):
375
+ """Display analysis results with scores."""
376
+ if hasattr(self, 'board_ui') and self.board_ui:
377
+ self.board_ui.display_analysis(top_moves)
378
+
379
+ # Update score label
380
+ if hasattr(self, 'score_label') and top_moves:
381
+ score_texts = []
382
+ found_mate = False
383
+ for i, move_data in enumerate(top_moves):
384
+ move = move_data["move"]
385
+ score = move_data["score"]
386
+ # Convert score to more readable format
387
+ if abs(score) >= 9000:
388
+ # Mate score
389
+ found_mate = True
390
+ mate_in = (10000 - abs(score))
391
+ score_str = f"M{mate_in}" if score > 0 else f"-M{mate_in}"
392
+
393
+ if i == 0: # If top move is mate
394
+ if score > 0:
395
+ self.status_label.configure(text=f"White wins in {mate_in}!")
396
+ else:
397
+ self.status_label.configure(text=f"Black wins in {mate_in}!")
398
+ else:
399
+ score_str = f"{score/100:+.2f}"
400
+
401
+ colors = ["🟢", "🔵", "🟡"]
402
+ score_texts.append(f"{colors[i]} {move}: {score_str}")
403
+
404
+ self.score_label.configure(text=" | ".join(score_texts))
405
+
406
+ # If no mate found in top moves, ensure status doesn't stick (unless editing/thinking)
407
+ if not found_mate and not self.mirroring and not self.edit_mode_var.get() and "Thinking" not in self.status_label.cget("text"):
408
+ # Restore turn status
409
+ turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
410
+ if self.two_player_var.get():
411
+ self.status_label.configure(text=f"{turn}'s Turn")
412
+ # Else AI turn text is handled by 'AI Thinking' or 'Your Turn'
413
+
414
+ elif hasattr(self, 'score_label'):
415
+ self.score_label.configure(text="")
416
+
417
+ def on_move_made(self, event=None):
418
+ # Check for game over
419
+ if self.game_state.is_game_over():
420
+ result = self.game_state.board.result()
421
+ self.status_label.configure(text=f"Game Over: {result}")
422
+ # Do NOT return here, we might still want to mirror the last move that caused game over?
423
+ # Actually if game is over, we still want to update UI.
424
+ # But let's act normal.
425
+
426
+ # MIRROR LOGIC
427
+ if self.mirroring and event:
428
+ # Just made a move.
429
+ # The event might not carry the move info directly if it's a generic binding,
430
+ # but we can get the last move from the board stack.
431
+ try:
432
+ if self.game_state.board.move_stack:
433
+ last_move = self.game_state.board.peek()
434
+ # If it's a move made by the player (or AI if we want to mirror AI too)
435
+ # For now, let's mirror ALL moves (Player and AI) so the external board stays in sync?
436
+ # User said "whatever i move ... it will mirroe".
437
+ # If AI moves on local board, user probably wants that on external board too if playing against external opponent?
438
+ # OR if playing vs AI locally, and mirroring to analysis board.
439
+ # Let's mirror everything.
440
+
441
+ # Need to know if we are 'flipped' on the external board?
442
+ # User selects one region. We assume it matches our orientation?
443
+ # User request: "make the mirror doenst matter if i am black of white... it will mirror i move on the right"
444
+ # This implies they want the Target Orientation to MATCH the Local Orientation.
445
+ # If I am flipped (Black), Target is flipped (Black).
446
+ is_flipped = getattr(self.board_ui, 'flipped', False)
447
+
448
+ threading.Thread(target=self.mirror.execute_move, args=(last_move, self.mirror_region, is_flipped), daemon=True).start()
449
+ except Exception as e:
450
+ print(f"Mirror error: {e}")
451
+
452
+ if self.analysis_var.get():
453
+ self.update_analysis()
454
+
455
+ # Check if two player mode
456
+ if self.two_player_var.get():
457
+ # Two player mode - just update turn indicator
458
+ turn = "White" if self.game_state.board.turn == chess.WHITE else "Black"
459
+ self.status_label.configure(text=f"{turn}'s Turn")
460
+ else:
461
+ # vs AI mode
462
+ if self.game_state.board.turn == chess.BLACK:
463
+ # Trigger AI move
464
+ if not self.edit_mode_var.get() and not self.two_player_var.get():
465
+ self.status_label.configure(text="AI Thinking...")
466
+ threading.Thread(target=self.make_ai_move, daemon=True).start()
467
+ else:
468
+ self.status_label.configure(text="Your Turn (White)")
469
+
470
+ def make_ai_move(self):
471
+ if self.edit_mode_var.get():
472
+ return
473
+
474
+ # AI plays best move
475
+ best_move = self.engine.get_best_move(self.game_state.get_fen())
476
+ if best_move:
477
+ self.game_state.board.push(best_move)
478
+ self.after(0, self.update_board_after_ai)
479
+ else:
480
+ self.after(0, lambda: self.status_label.configure(text="Engine Error"))
481
+
482
+ def update_board_after_ai(self):
483
+ self.board_ui.draw_board()
484
+ self.status_label.configure(text="Your Turn")
485
+
486
+ if self.analysis_var.get():
487
+ self.update_analysis()
488
+
489
+ if self.game_state.is_game_over():
490
+ self.status_label.configure(text=f"Game Over: {self.game_state.board.result()}")
491
+
492
+ def force_ai_move(self):
493
+ """Force the AI to make a move for the current side."""
494
+ if self.game_state.is_game_over():
495
+ return
496
+
497
+ self.status_label.configure(text="Forcing AI Move...")
498
+ threading.Thread(target=self.make_ai_move, daemon=True).start()
src/mirror.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pyautogui
2
+ import chess
3
+ import time
4
+ import math
5
+
6
+ class MirrorHandler:
7
+ def __init__(self):
8
+ # Configure pyautogui to be a bit safer/slower if needed
9
+ pyautogui.FAILSAFE = True
10
+ pyautogui.PAUSE = 0.1
11
+
12
+ def execute_move(self, move, region, is_flipped=False):
13
+ """
14
+ Execute a chess move on the screen region using mouse drag.
15
+
16
+ Args:
17
+ move (chess.Move): The move to execute.
18
+ region (dict): {'left', 'top', 'width', 'height'} of the target board.
19
+ is_flipped (bool): If True, board is viewed from Black's perspective (rank 1 at top).
20
+ """
21
+ if not region or not move:
22
+ return
23
+
24
+ start_sq = move.from_square
25
+ end_sq = move.to_square
26
+
27
+ # Calculate coordinates
28
+ start_x, start_y = self._get_square_center(start_sq, region, is_flipped)
29
+ end_x, end_y = self._get_square_center(end_sq, region, is_flipped)
30
+
31
+ # Save current position to restore later?
32
+ # Actually user said "just use my mouse", implies they expect it to take over.
33
+ # But maybe restoring it is nice? Let's just do the move for now.
34
+
35
+ # Perform the drag
36
+ # Move to start
37
+ pyautogui.moveTo(start_x, start_y)
38
+ # Drag to end
39
+ pyautogui.dragTo(end_x, end_y, button='left')
40
+
41
+ # Check for promotion
42
+ if move.promotion:
43
+ # Assumes auto-queen or standard promotion UI (usually Queen is closest/first)
44
+ # This is tricky for generic sites.
45
+ # Often, clicking the target square again acts as "Question" or immediate select.
46
+ # For now, let's just click the target square again to select Queen if it pops up.
47
+ time.sleep(0.1)
48
+ pyautogui.click()
49
+
50
+ def _get_square_center(self, square, region, is_flipped):
51
+ """
52
+ Calculate center x, y for a given square index (0-63).
53
+ """
54
+ file_idx = chess.square_file(square)
55
+ rank_idx = chess.square_rank(square)
56
+
57
+ if is_flipped:
58
+ # Board is black options (rank 8 at bottom? Wait.)
59
+ # Standard: White at bottom (Rank 0 is bottom).
60
+ # Flipped: Black at bottom (Rank 7 is bottom, Rank 0 is top).
61
+
62
+ # If Flipped (Black at bottom):
63
+ # File 0 (a) is on the Right? No, usually board just rotates 180.
64
+ # Visual:
65
+ # White bottom: a1 bottom-left.
66
+ # Black bottom: h8 bottom-left.
67
+
68
+ # Let's assume Standard Rotation (180 degrees):
69
+ # a1 (0,0) becomes top-right.
70
+ # h8 (7,7) becomes bottom-left.
71
+
72
+ # Let's map visual col/row (0-7 from top-left)
73
+ # Normal:
74
+ # col = file_idx
75
+ # row = 7 - rank_idx
76
+
77
+ # Flipped:
78
+ # col = 7 - file_idx
79
+ # row = rank_idx
80
+
81
+ col = 7 - file_idx
82
+ row = rank_idx
83
+ else:
84
+ # Standard (White at bottom)
85
+ # a1 is bottom-left
86
+ col = file_idx
87
+ row = 7 - rank_idx
88
+
89
+ sq_w = region['width'] / 8
90
+ sq_h = region['height'] / 8
91
+
92
+ center_x = region['left'] + (col * sq_w) + (sq_w / 2)
93
+ center_y = region['top'] + (row * sq_h) + (sq_h / 2)
94
+
95
+ return center_x, center_y
src/overlay.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import customtkinter as ctk
2
+ import tkinter as tk
3
+ import chess
4
+ import win32gui
5
+ import win32con
6
+ import win32api
7
+
8
+ class SelectionOverlay(ctk.CTkToplevel):
9
+ def __init__(self, master, on_select_callback):
10
+ super().__init__(master)
11
+
12
+ self.on_select_callback = on_select_callback
13
+
14
+ self.attributes("-alpha", 0.3)
15
+ self.attributes("-fullscreen", True)
16
+ self.attributes("-topmost", True)
17
+ self.configure(bg="black")
18
+
19
+ self.start_x = None
20
+ self.start_y = None
21
+ self.rect = None
22
+
23
+ self.canvas = ctk.CTkCanvas(self, cursor="cross", bg="grey15", highlightthickness=0)
24
+ self.canvas.pack(fill="both", expand=True)
25
+
26
+ self.canvas.bind("<ButtonPress-1>", self.on_press)
27
+ self.canvas.bind("<B1-Motion>", self.on_drag)
28
+ self.canvas.bind("<ButtonRelease-1>", self.on_release)
29
+
30
+ # Exit on Escape
31
+ self.bind("<Escape>", lambda e: self.destroy())
32
+
33
+ def on_press(self, event):
34
+ self.start_x = event.x
35
+ self.start_y = event.y
36
+ if self.rect:
37
+ self.canvas.delete(self.rect)
38
+ self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y, outline="red", width=2)
39
+
40
+ def on_drag(self, event):
41
+ cur_x, cur_y = (event.x, event.y)
42
+ self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y)
43
+
44
+ def on_release(self, event):
45
+ end_x, end_y = (event.x, event.y)
46
+
47
+ # Calculate region
48
+ left = min(self.start_x, end_x)
49
+ top = min(self.start_y, end_y)
50
+ width = abs(self.start_x - end_x)
51
+ height = abs(self.start_y - end_y)
52
+
53
+ if width > 50 and height > 50:
54
+ self.on_select_callback({'left': left, 'top': top, 'width': width, 'height': height})
55
+ self.destroy()
56
+ else:
57
+ # Too small, just reset
58
+ pass
59
+
60
+ class ProjectionOverlay(ctk.CTkToplevel):
61
+ def __init__(self, region):
62
+ """
63
+ region: {'left': int, 'top': int, 'width': int, 'height': int}
64
+ """
65
+ super().__init__()
66
+
67
+ self.region = region
68
+ # Position exactly over the board
69
+ self.geometry(f"{region['width']}x{region['height']}+{region['left']}+{region['top']}")
70
+
71
+ # Remove title bar
72
+ self.overrideredirect(True)
73
+ self.attributes("-topmost", True)
74
+
75
+ # Transparency setup for Windows
76
+ # We use a specific color as the transparent key.
77
+ self.transparent_color = "#000001" # Very nearly black
78
+ self.configure(bg=self.transparent_color)
79
+ self.wm_attributes("-transparentcolor", self.transparent_color)
80
+
81
+ # Canvas for drawing
82
+ self.canvas = tk.Canvas(self, bg=self.transparent_color, highlightthickness=0)
83
+ self.canvas.pack(fill="both", expand=True)
84
+
85
+ # Make click-through
86
+ self.make_click_through()
87
+
88
+ def make_click_through(self):
89
+ hwnd = win32gui.GetParent(self.winfo_id())
90
+ # If GetParent returns 0, try the window id directly (sometimes needed for Toplevels without parents)
91
+ if hwnd == 0:
92
+ hwnd = self.winfo_id()
93
+
94
+ styles = win32gui.GetWindowLong(hwnd, win32con.GWL_EXSTYLE)
95
+ styles = styles | win32con.WS_EX_LAYERED | win32con.WS_EX_TRANSPARENT
96
+ win32gui.SetWindowLong(hwnd, win32con.GWL_EXSTYLE, styles)
97
+
98
+ def draw_best_move(self, move):
99
+ """
100
+ Draw an arrow for the best move.
101
+ move: python-chess Move object (e.g. e2e4)
102
+ """
103
+ self.canvas.delete("all")
104
+
105
+ if not move:
106
+ return
107
+
108
+ # Calculate coordinates
109
+ # File 0-7 (a-h), Rank 0-7 (1-8)
110
+ # 0,0 is bottom-left (a1)
111
+
112
+ sq_w = self.region['width'] / 8
113
+ sq_h = self.region['height'] / 8
114
+
115
+ start_sq = move.from_square
116
+ end_sq = move.to_square
117
+
118
+ # Convert to x,y (top-left origin for drawing)
119
+ # File (x) is same
120
+ # Rank (y) needs flip: Rank 0 is at bottom (y moves down) -> Rank 7 is top (y=0)
121
+ # Rank r -> visual row (7-r)
122
+
123
+ x1 = (chess.square_file(start_sq) + 0.5) * sq_w
124
+ y1 = ((7 - chess.square_rank(start_sq)) + 0.5) * sq_h
125
+
126
+ x2 = (chess.square_file(end_sq) + 0.5) * sq_w
127
+ y2 = ((7 - chess.square_rank(end_sq)) + 0.5) * sq_h
128
+
129
+ # Draw Arrow
130
+ self.canvas.create_line(x1, y1, x2, y2, fill="#00ff00", width=6, arrow=tk.LAST, arrowshape=(16, 20, 6), tag="arrow")
131
+
132
+ # Draw Circle on target
133
+ r = min(sq_w, sq_h) * 0.2
134
+ self.canvas.create_oval(x2-r, y2-r, x2+r, y2+r, outline="#00ff00", width=4, tag="arrow")
135
+
136
+ def clear(self):
137
+ self.canvas.delete("all")
src/vision.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import mss
4
+
5
+ class VisionHandler:
6
+ def __init__(self):
7
+ self.templates = {}
8
+ self.is_calibrated = False
9
+
10
+ def capture_screen(self, region):
11
+ """
12
+ Capture a specific region of the screen.
13
+ region: {'top': int, 'left': int, 'width': int, 'height': int}
14
+ """
15
+ with mss.mss() as sct:
16
+ # mss requires int for all values
17
+ monitor = {
18
+ "top": int(region["top"]),
19
+ "left": int(region["left"]),
20
+ "width": int(region["width"]),
21
+ "height": int(region["height"])
22
+ }
23
+ screenshot = sct.grab(monitor)
24
+ img = np.array(screenshot)
25
+ return cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
26
+
27
+ def split_board(self, board_image):
28
+ """
29
+ Split board image into 64 squares.
30
+ Assumption: board_image is cropped exactly to the board edges.
31
+ """
32
+ h, w, _ = board_image.shape
33
+ sq_h, sq_w = h // 8, w // 8
34
+ squares = []
35
+ # Row-major order (rank 8 down to rank 1)
36
+ # Standard FEN order: Rank 8 (index 0) to Rank 1 (index 7)
37
+ for r in range(8):
38
+ row_squares = []
39
+ for c in range(8):
40
+ # Add a small crop margin to avoid border noise
41
+ margin_h = int(sq_h * 0.1)
42
+ margin_w = int(sq_w * 0.1)
43
+
44
+ y1 = r * sq_h + margin_h
45
+ y2 = (r + 1) * sq_h - margin_h
46
+ x1 = c * sq_w + margin_w
47
+ x2 = (c + 1) * sq_w - margin_w
48
+
49
+ square = board_image[y1:y2, x1:x2]
50
+ row_squares.append(square)
51
+ squares.append(row_squares)
52
+ return squares
53
+
54
+ def calibrate(self, board_image):
55
+ """
56
+ Calibrate by learning piece appearance from a starting position board image.
57
+ Standard Starting Position:
58
+ r n b q k b n r
59
+ p p p p p p p p
60
+ . . . . . . . .
61
+ . . . . . . . .
62
+ . . . . . . . .
63
+ . . . . . . . .
64
+ P P P P P P P P
65
+ R N B Q K B N R
66
+ """
67
+ squares = self.split_board(board_image)
68
+ self.templates = {}
69
+
70
+ # Dictionary mapping piece chars to list of coordinates (row, col) in start pos
71
+ # We'll take the average or just the first instance as template
72
+ # 0-indexed rows: 0=BlackBack, 1=BlackPawn, ... 6=WhitePawn, 7=WhiteBack
73
+
74
+ piece_map = {
75
+ 'r': [(0, 0), (0, 7)],
76
+ 'n': [(0, 1), (0, 6)],
77
+ 'b': [(0, 2), (0, 5)],
78
+ 'q': [(0, 3)],
79
+ 'k': [(0, 4)],
80
+ 'p': [(1, i) for i in range(8)],
81
+ 'R': [(7, 0), (7, 7)],
82
+ 'N': [(7, 1), (7, 6)],
83
+ 'B': [(7, 2), (7, 5)],
84
+ 'Q': [(7, 3)],
85
+ 'K': [(7, 4)],
86
+ 'P': [(6, i) for i in range(8)],
87
+ '.': [] # Empty squares
88
+ }
89
+
90
+ # Add empty squares (rows 2, 3, 4, 5)
91
+ for r in range(2, 6):
92
+ for c in range(8):
93
+ piece_map['.'].append((r, c))
94
+
95
+ # Store one template per piece type (simplest approach)
96
+ # Better: Store all samples and use KNN, but simple template match might suffice for consistent graphics
97
+
98
+ for p_char, coords in piece_map.items():
99
+ if not coords:
100
+ continue
101
+
102
+ # Use the first coordinate as the primary template
103
+ r, c = coords[0]
104
+ template = squares[r][c]
105
+
106
+ # We convert to grayscale for simpler matching
107
+ gray_template = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
108
+
109
+ # Store just one template for now
110
+ self.templates[p_char] = gray_template
111
+
112
+ self.is_calibrated = True
113
+ print("Calibration complete.")
114
+
115
+ def match_square(self, square_img):
116
+ if not self.templates:
117
+ return '.'
118
+
119
+ gray_sq = cv2.cvtColor(square_img, cv2.COLOR_BGR2GRAY)
120
+
121
+ best_score = float('inf')
122
+ best_piece = '.'
123
+
124
+ # Compare against all templates using MSE
125
+ # Note: Templates must be same size. If slightly different due to rounding, resize.
126
+
127
+ target_h, target_w = gray_sq.shape
128
+
129
+ for p_char, template in self.templates.items():
130
+ # Resize template to match square if needed (should be identical if grid is uniform)
131
+ if template.shape != gray_sq.shape:
132
+ template = cv2.resize(template, (target_w, target_h))
133
+
134
+ # Mean Squared Error
135
+ err = np.sum((gray_sq.astype("float") - template.astype("float")) ** 2)
136
+ err /= float(gray_sq.shape[0] * gray_sq.shape[1])
137
+
138
+ if err < best_score:
139
+ best_score = err
140
+ best_piece = p_char
141
+
142
+ return best_piece
143
+
144
+ def get_fen_from_image(self, board_image):
145
+ if not self.is_calibrated:
146
+ # Fallback or error
147
+ return None
148
+
149
+ squares = self.split_board(board_image)
150
+ fen_rows = []
151
+
152
+ for r in range(8):
153
+ empty_count = 0
154
+ row_str = ""
155
+ for c in range(8):
156
+ piece = self.match_square(squares[r][c])
157
+
158
+ if piece == '.':
159
+ empty_count += 1
160
+ else:
161
+ if empty_count > 0:
162
+ row_str += str(empty_count)
163
+ empty_count = 0
164
+ row_str += piece
165
+
166
+ if empty_count > 0:
167
+ row_str += str(empty_count)
168
+
169
+ fen_rows.append(row_str)
170
+
171
+ # Join with '/'
172
+ fen_board = "/".join(fen_rows)
173
+
174
+ # Add default game state info (w KQkq - 0 1)
175
+ # TODO: Detect turn based on external logic or diff?
176
+ # For now, we might alternate or assume it's always the user's turn to move?
177
+ # Actually, if we are just showing analysis, we want the engine to evaluate for the SIDE TO MOVE.
178
+ # But we don't know who is to move just from the static board.
179
+ # We can try to infer from previous state or just ask the user.
180
+ # For this version, let's return the board part, and handle the rest in logic.
181
+
182
+ return f"{fen_board} w KQkq - 0 1"
tests/test_engine.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import unittest
4
+
5
+ # Add src to path
6
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
7
+
8
+ from src.engine import EngineHandler
9
+ from src.game_state import GameState
10
+
11
+ class TestChessLogic(unittest.TestCase):
12
+ def test_game_state_init(self):
13
+ gs = GameState()
14
+ self.assertEqual(gs.get_fen(), "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
15
+
16
+ def test_make_move(self):
17
+ gs = GameState()
18
+ # e2e4
19
+ success = gs.make_move("e2e4")
20
+ self.assertTrue(success)
21
+ self.assertNotEqual(gs.get_fen(), "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
22
+
23
+ def test_engine_missing(self):
24
+ # Should handle missing engine gracefully
25
+ engine = EngineHandler("non_existent_stockfish.exe")
26
+ success, msg = engine.initialize_engine()
27
+ self.assertFalse(success)
28
+ self.assertIn("not found", msg)
29
+
30
+ if __name__ == '__main__':
31
+ unittest.main()
tests/test_engine_manual.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import sys
3
+ import os
4
+
5
+ # Add src to path
6
+ sys.path.append(os.path.join(os.getcwd()))
7
+
8
+ from src.engine import EngineHandler
9
+
10
+ def test_engine():
11
+ print("Testing EngineHandler...")
12
+ engine = EngineHandler()
13
+ success, msg = engine.initialize_engine()
14
+
15
+ if not success:
16
+ print(f"FAILED: {msg}")
17
+ return
18
+
19
+ print(f"SUCCESS: {msg}")
20
+
21
+ # Test FEN (Start Position)
22
+ fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
23
+
24
+ print("\nTesting get_top_moves(limit=3)...")
25
+ top_moves = engine.get_top_moves(fen, limit=3)
26
+
27
+ if len(top_moves) == 0:
28
+ print("FAILED: No moves returned. (Check stockfish path?)")
29
+ else:
30
+ print(f"SUCCESS: Returned {len(top_moves)} moves.")
31
+ for m in top_moves:
32
+ print(f"Rank {m['rank']}: {m['move']} (Score: {m['score']})")
33
+
34
+ engine.quit()
35
+
36
+ if __name__ == "__main__":
37
+ test_engine()
tests/test_mirror.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import unittest
2
+ import chess
3
+ from src.mirror import MirrorHandler
4
+
5
+ class TestMirrorLogic(unittest.TestCase):
6
+ def setUp(self):
7
+ self.mirror = MirrorHandler()
8
+ self.region = {'left': 100, 'top': 100, 'width': 800, 'height': 800}
9
+
10
+ def test_coordinate_mapping_standard(self):
11
+ # Test a1 (start square)
12
+ # Standard: a1 is bottom-left (0,0) in cartesian, but for screen pixels:
13
+ # col 0, row 7 (since y increases downwards)
14
+ # region top=100. height=800. square height=100.
15
+ # row 7 y range: 100 + 700 = 800 to 900. Center = 850.
16
+ # col 0 x range: 100 + 0 = 100 to 200. Center = 150.
17
+
18
+ sq = chess.A1
19
+ x, y = self.mirror._get_square_center(sq, self.region, is_flipped=False)
20
+ self.assertEqual(x, 150)
21
+ self.assertEqual(y, 850)
22
+
23
+ # Test h8 (top-right)
24
+ # col 7, row 0
25
+ # x: 100 + 700 + 50 = 850
26
+ # y: 100 + 0 + 50 = 150
27
+ sq = chess.H8
28
+ x, y = self.mirror._get_square_center(sq, self.region, is_flipped=False)
29
+ self.assertEqual(x, 850)
30
+ self.assertEqual(y, 150)
31
+
32
+ def test_coordinate_mapping_flipped(self):
33
+ # Flipped: Black at bottom.
34
+ # a1 (0,0) becomes top-right?
35
+ # Let's re-verify logic:
36
+ # Flipped: col = 7 - file_idx, row = rank_idx
37
+
38
+ # a1: file 0, rank 0
39
+ # col = 7, row = 0
40
+ # x: 850, y: 150 (Top-Right) -> Correct for rotated 180.
41
+
42
+ sq = chess.A1
43
+ x, y = self.mirror._get_square_center(sq, self.region, is_flipped=True)
44
+ self.assertEqual(x, 850)
45
+ self.assertEqual(y, 150)
46
+
47
+ # h8: file 7, rank 7
48
+ # col = 0, row = 7
49
+ # x: 150, y: 850 (Bottom-Left)
50
+ sq = chess.H8
51
+ x, y = self.mirror._get_square_center(sq, self.region, is_flipped=True)
52
+ self.assertEqual(x, 150)
53
+ self.assertEqual(y, 850)
54
+
55
+ if __name__ == '__main__':
56
+ unittest.main()