├── images
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
├── README.md
└── chees_game.py
/images/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nima-Mollaei/Chess_Game/HEAD/images/screenshot1.png
--------------------------------------------------------------------------------
/images/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nima-Mollaei/Chess_Game/HEAD/images/screenshot2.png
--------------------------------------------------------------------------------
/images/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nima-Mollaei/Chess_Game/HEAD/images/screenshot3.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
♟️Chess_Game
3 |
4 |
5 | A complete chess game with rules, move validation, and GUI built using Python & Tkinter.
6 | Supports castling, promotion, en passant, undo, and more.
7 |
8 |
9 |
10 |
11 |
12 |
13 | ---
14 |
15 | ## 🎯 Overview
16 |
17 | This project is a full-featured chess engine and GUI built with **Python** and **Tkinter**. It is built entirely from scratch without external libraries for logic or rendering. It is ideal for learning GUI design, game logic, and chess rules implementation.
18 |
19 | ---
20 |
21 | ## 🎮 Features
22 |
23 | ✅ Classic 8×8 chessboard with Unicode pieces
24 | ✅ All standard rules of chess enforced:
25 | - Legal move validation
26 | - Castling (both sides)
27 | - En passant
28 | - Promotion (default to queen)
29 | - Check & checkmate detection
30 |
31 | ✅ Interactive GUI with:
32 | - Highlighted selected pieces and possible moves
33 | - Undo last move
34 | - Turn indicators
35 | - Error prevention (can’t move enemy pieces or make illegal moves)
36 |
37 | ---
38 |
39 | ## 🚀 Installation
40 |
41 | ### Prerequisites
42 | - Python 3.6+
43 | - No external libraries needed (uses only built-in `tkinter`)
44 |
45 | ### Run the Game
46 | ```bash
47 | git clone https://github.com/your-username/tkinter-chess.git
48 | cd Chess_Game
49 | python chess_game.py
50 | ````
51 |
52 | ---
53 |
54 | ## 🧩 How It Works
55 |
56 | The project consists of:
57 |
58 | * `chess_game.py`: Main script containing all classes for board logic and Tkinter rendering.
59 | * `Board` class handles:
60 |
61 | * Piece setup
62 | * Movement rules
63 | * Special chess rules
64 | * `GUI` layer handles:
65 |
66 | * Drawing the board and pieces
67 | * Event bindings
68 | * Updating board state after moves
69 |
70 | Architecture is modular and easily extensible for future enhancements.
71 |
72 | ---
73 |
74 | ## 🖼 Screenshots
75 |
76 |
77 |
78 |
79 |
80 |
81 | ---
82 |
83 |
84 | ## 🤝 Contributing
85 |
86 | Feel free to contribute to this project! Fork the repository and submit a pull request. If you find any issues or have suggestions, please open an issue.
87 |
88 |
89 |
--------------------------------------------------------------------------------
/chees_game.py:
--------------------------------------------------------------------------------
1 | import tkinter as tk
2 | from tkinter import messagebox, simpledialog
3 |
4 | TILE_SIZE = 64
5 | ROWS, COLS = 8, 8
6 | WHITE, BLACK = 'white', 'black'
7 |
8 | PIECES = {
9 | 'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙',
10 | 'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟'
11 | }
12 |
13 |
14 | class Piece:
15 | def __init__(self, name, color):
16 | self.name = name
17 | self.color = color
18 | self.has_moved = False
19 |
20 | def __str__(self):
21 | return PIECES[self.name.upper() if self.color == WHITE else self.name.lower()]
22 |
23 | def get_moves(self, board, x, y, en_passant_target=None, check_check=True):
24 | moves = []
25 |
26 | def inside_board(nx, ny):
27 | return 0 <= nx < 8 and 0 <= ny < 8
28 |
29 | if self.name == 'P':
30 | dir = -1 if self.color == WHITE else 1
31 | start_row = 6 if self.color == WHITE else 1
32 | # Forward moves
33 | if inside_board(x, y+dir) and board[y+dir][x] is None:
34 | moves.append((x, y+dir))
35 | if y == start_row and board[y+2*dir][x] is None:
36 | moves.append((x, y+2*dir))
37 | # Captures
38 | for dx in [-1, 1]:
39 | nx, ny = x + dx, y + dir
40 | if inside_board(nx, ny):
41 | target = board[ny][nx]
42 | if target and target.color != self.color:
43 | moves.append((nx, ny))
44 | elif en_passant_target == (nx, ny):
45 | moves.append((nx, ny))
46 |
47 | elif self.name == 'R':
48 | directions = [(0,1),(1,0),(0,-1),(-1,0)]
49 | for dx, dy in directions:
50 | nx, ny = x + dx, y + dy
51 | while inside_board(nx, ny):
52 | target = board[ny][nx]
53 | if target is None:
54 | moves.append((nx, ny))
55 | elif target.color != self.color:
56 | moves.append((nx, ny))
57 | break
58 | else:
59 | break
60 | nx += dx
61 | ny += dy
62 |
63 | elif self.name == 'B':
64 | directions = [(1,1),(1,-1),(-1,1),(-1,-1)]
65 | for dx, dy in directions:
66 | nx, ny = x + dx, y + dy
67 | while inside_board(nx, ny):
68 | target = board[ny][nx]
69 | if target is None:
70 | moves.append((nx, ny))
71 | elif target.color != self.color:
72 | moves.append((nx, ny))
73 | break
74 | else:
75 | break
76 | nx += dx
77 | ny += dy
78 |
79 | elif self.name == 'Q':
80 | directions = [(0,1),(1,0),(0,-1),(-1,0),(1,1),(1,-1),(-1,1),(-1,-1)]
81 | for dx, dy in directions:
82 | nx, ny = x + dx, y + dy
83 | while inside_board(nx, ny):
84 | target = board[ny][nx]
85 | if target is None:
86 | moves.append((nx, ny))
87 | elif target.color != self.color:
88 | moves.append((nx, ny))
89 | break
90 | else:
91 | break
92 | nx += dx
93 | ny += dy
94 |
95 | elif self.name == 'N':
96 | jumps = [(2,1),(1,2),(-1,2),(-2,1),(-2,-1),(-1,-2),(1,-2),(2,-1)]
97 | for dx, dy in jumps:
98 | nx, ny = x+dx, y+dy
99 | if inside_board(nx, ny):
100 | target = board[ny][nx]
101 | if target is None or target.color != self.color:
102 | moves.append((nx, ny))
103 |
104 | elif self.name == 'K':
105 | king_moves = [(x+dx, y+dy) for dx in [-1,0,1] for dy in [-1,0,1] if dx != 0 or dy != 0]
106 | for nx, ny in king_moves:
107 | if inside_board(nx, ny):
108 | target = board[ny][nx]
109 | if target is None or target.color != self.color:
110 | moves.append((nx, ny))
111 |
112 | # Castling
113 | if not self.has_moved:
114 | row = y
115 | # Kingside castling
116 | if self.can_castle(board, x, y, kingside=True):
117 | moves.append((x+2, y))
118 | # Queenside castling
119 | if self.can_castle(board, x, y, kingside=False):
120 | moves.append((x-2, y))
121 |
122 | # If check_check=True, filter out moves that leave king in check
123 | if check_check:
124 | valid_moves = []
125 | for move in moves:
126 | if not self.move_puts_king_in_check(board, (x, y), move, en_passant_target):
127 | valid_moves.append(move)
128 | return valid_moves
129 |
130 | return moves
131 |
132 | def can_castle(self, board, x, y, kingside):
133 | # Check if squares between king and rook are empty and not attacked
134 | rook_x = 7 if kingside else 0
135 | direction = 1 if kingside else -1
136 | rook = board[y][rook_x]
137 |
138 | if rook is None or rook.name != 'R' or rook.color != self.color or rook.has_moved:
139 | return False
140 |
141 | # Squares between king and rook must be empty
142 | range_start = x + direction
143 | range_end = rook_x
144 | step = direction
145 | for nx in range(range_start, range_end, step):
146 | if board[y][nx] is not None:
147 | return False
148 |
149 | # King may not be in check, nor pass through or end on attacked squares
150 | # We'll check squares: current, one step, two steps in direction
151 | squares_to_check = [x, x + direction, x + 2*direction]
152 | game_dummy = ChessGame.dummy_board(board)
153 | for sq_x in squares_to_check:
154 | if self.square_attacked(game_dummy, sq_x, y, self.color):
155 | return False
156 |
157 | return True
158 |
159 | def move_puts_king_in_check(self, board, src, dst, en_passant_target):
160 | # Simulate move and check if own king is in check
161 | x1, y1 = src
162 | x2, y2 = dst
163 | piece = board[y1][x1]
164 | target = board[y2][x2]
165 | board_copy = [row[:] for row in board]
166 | # Deep copy pieces (to avoid reference issues)
167 | board_copy = []
168 | for row in board:
169 | board_copy.append([p if p is None else Piece(p.name, p.color) for p in row])
170 |
171 | # Apply move on copy
172 | board_copy[y2][x2] = board_copy[y1][x1]
173 | board_copy[y1][x1] = None
174 |
175 | # Remove captured pawn for en passant
176 | if piece.name == 'P' and dst == en_passant_target:
177 | capture_row = y1
178 | board_copy[capture_row][x2] = None
179 |
180 | return self.square_attacked(board_copy, *self.find_king(board_copy, piece.color), piece.color)
181 |
182 | @staticmethod
183 | def square_attacked(board, x, y, color):
184 | # Check if square (x,y) is attacked by opponent of color
185 | for j in range(8):
186 | for i in range(8):
187 | p = board[j][i]
188 | if p is not None and p.color != color:
189 | moves = p.get_moves(board, i, j, check_check=False)
190 | if (x, y) in moves:
191 | return True
192 | return False
193 |
194 | @staticmethod
195 | def find_king(board, color):
196 | for y in range(8):
197 | for x in range(8):
198 | p = board[y][x]
199 | if p and p.name == 'K' and p.color == color:
200 | return (x, y)
201 | return (-1, -1)
202 |
203 | class ChessGame:
204 | def __init__(self, root):
205 | self.root = root
206 | self.canvas = tk.Canvas(root, width=COLS*TILE_SIZE+200, height=ROWS*TILE_SIZE+40)
207 | self.canvas.pack()
208 | self.canvas.bind("", self.on_click)
209 |
210 | self.status_var = tk.StringVar()
211 | self.status_label = tk.Label(root, textvariable=self.status_var, font=('Arial', 14))
212 | self.status_label.pack()
213 |
214 | self.selected = None
215 | self.highlighted = []
216 | self.turn = WHITE
217 | self.history = []
218 | self.captured = []
219 | self.board = [[None]*8 for _ in range(8)]
220 | self.en_passant_target = None
221 | self.in_check_flag = False
222 | self.game_over = False
223 |
224 | self.setup_board()
225 | self.draw()
226 |
227 | btn_frame = tk.Frame(root)
228 | btn_frame.pack()
229 | tk.Button(btn_frame, text="Undo", command=self.undo).pack(side='left')
230 | tk.Button(btn_frame, text="Reset", command=self.reset).pack(side='left')
231 |
232 | def setup_board(self):
233 | placement = ['R','N','B','Q','K','B','N','R']
234 | for i in range(8):
235 | self.board[1][i] = Piece('P', BLACK)
236 | self.board[6][i] = Piece('P', WHITE)
237 | self.board[0][i] = Piece(placement[i], BLACK)
238 | self.board[7][i] = Piece(placement[i], WHITE)
239 |
240 | def draw(self):
241 | self.canvas.delete("all")
242 | for y in range(8):
243 | for x in range(8):
244 | fill = '#EEE' if (x+y)%2 == 0 else '#666'
245 |
246 | # Highlight selected and possible moves
247 | if self.selected == (x, y):
248 | fill = '#8f8'
249 | elif (x, y) in self.highlighted:
250 | fill = '#8cf'
251 |
252 | piece = self.board[y][x]
253 | # Highlight king in check
254 | if piece and piece.name == 'K' and self.is_in_check(piece.color):
255 | fill = '#f88'
256 |
257 | self.canvas.create_rectangle(x*TILE_SIZE, y*TILE_SIZE,
258 | (x+1)*TILE_SIZE, (y+1)*TILE_SIZE,
259 | fill=fill)
260 | if piece:
261 | self.canvas.create_text((x+0.5)*TILE_SIZE, (y+0.5)*TILE_SIZE,
262 | text=str(piece), font=("Arial", 32))
263 |
264 | # Captured pieces display
265 | self.canvas.create_text(8*TILE_SIZE+10, 20, text="Captured:", anchor='nw')
266 | white_caps = ''.join(str(p) for p in self.captured if p.color == BLACK)
267 | black_caps = ''.join(str(p) for p in self.captured if p.color == WHITE)
268 | self.canvas.create_text(8*TILE_SIZE+10, 50, text=f"White: {white_caps}", anchor='nw')
269 | self.canvas.create_text(8*TILE_SIZE+10, 80, text=f"Black: {black_caps}", anchor='nw')
270 |
271 | # Status message
272 | turn_text = "White" if self.turn == WHITE else "Black"
273 | if self.game_over:
274 | self.status_var.set(f"Game Over! {turn_text} lost.")
275 | else:
276 | check_text = " (Check!)" if self.is_in_check(self.turn) else ""
277 | self.status_var.set(f"Turn: {turn_text}{check_text}")
278 |
279 | def on_click(self, event):
280 | if self.game_over:
281 | return
282 | if event.x >= 8*TILE_SIZE or event.y >= 8*TILE_SIZE:
283 | return
284 | x, y = event.x // TILE_SIZE, event.y // TILE_SIZE
285 | piece = self.board[y][x]
286 |
287 | # If selected and clicked on valid move
288 | if self.selected and (x, y) in self.highlighted:
289 | self.move_piece(self.selected, (x, y))
290 | self.selected = None
291 | self.highlighted = []
292 | self.draw()
293 | self.check_game_end()
294 | return
295 |
296 | # Select piece if belongs to current player
297 | if piece and piece.color == self.turn:
298 | self.selected = (x, y)
299 | self.highlighted = piece.get_moves(self.board, x, y, self.en_passant_target)
300 | else:
301 | self.selected = None
302 | self.highlighted = []
303 |
304 | self.draw()
305 |
306 | def move_piece(self, src, dst):
307 | x1, y1 = src
308 | x2, y2 = dst
309 | piece = self.board[y1][x1]
310 | target = self.board[y2][x2]
311 | captured = None
312 | special = None # for special moves: castling, en passant, promotion
313 |
314 | # Save state for undo
315 | state = {
316 | 'board': [[p if p is None else Piece(p.name, p.color) for p in row] for row in self.board],
317 | 'turn': self.turn,
318 | 'en_passant_target': self.en_passant_target,
319 | 'captured': self.captured[:],
320 | }
321 |
322 | # Handle castling
323 | if piece.name == 'K' and abs(x2 - x1) == 2:
324 | special = 'castling'
325 | if x2 > x1:
326 | # kingside
327 | rook_src = (7, y1)
328 | rook_dst = (x2 -1, y1)
329 | else:
330 | # queenside
331 | rook_src = (0, y1)
332 | rook_dst = (x2 +1, y1)
333 | rook = self.board[rook_src[1]][rook_src[0]]
334 | self.board[rook_dst[1]][rook_dst[0]] = rook
335 | self.board[rook_src[1]][rook_src[0]] = None
336 | rook.has_moved = True
337 |
338 | # Handle en passant capture
339 | if piece.name == 'P' and dst == self.en_passant_target:
340 | special = 'en_passant'
341 | capture_row = y1
342 | captured = self.board[capture_row][x2]
343 | self.board[capture_row][x2] = None
344 |
345 | # Normal capture
346 | if target and target.color != piece.color:
347 | captured = target
348 |
349 | # Move piece
350 | self.board[y2][x2] = piece
351 | self.board[y1][x1] = None
352 |
353 | # Promotion
354 | if piece.name == 'P' and (y2 == 0 or y2 == 7):
355 | special = 'promotion'
356 | promoted_piece = self.ask_promotion(piece.color)
357 | self.board[y2][x2] = Piece(promoted_piece, piece.color)
358 |
359 | # Update flags
360 | piece.has_moved = True
361 |
362 | # Update en passant target
363 | if piece.name == 'P' and abs(y2 - y1) == 2:
364 | self.en_passant_target = (x1, (y1 + y2)//2)
365 | else:
366 | self.en_passant_target = None
367 |
368 | # Save move history for undo
369 | self.history.append({
370 | 'state': state,
371 | 'move': (src, dst),
372 | 'special': special,
373 | 'captured': captured,
374 | })
375 |
376 | if captured:
377 | self.captured.append(captured)
378 |
379 | # Change turn
380 | self.turn = BLACK if self.turn == WHITE else WHITE
381 |
382 | def ask_promotion(self, color):
383 | choices = {'Q': 'Queen', 'R': 'Rook', 'B': 'Bishop', 'N': 'Knight'}
384 | while True:
385 | choice = simpledialog.askstring("Promotion", "Promote to (Q, R, B, N):").upper()
386 | if choice in choices:
387 | return choice
388 |
389 | def undo(self):
390 | if not self.history:
391 | return
392 | last = self.history.pop()
393 | # Restore board
394 | self.board = [[p if p is None else Piece(p.name, p.color) for p in row] for row in last['state']['board']]
395 | self.turn = last['state']['turn']
396 | self.en_passant_target = last['state']['en_passant_target']
397 | self.captured = last['state']['captured'][:]
398 | self.selected = None
399 | self.highlighted = []
400 | self.game_over = False
401 | self.draw()
402 |
403 | def is_in_check(self, color):
404 | king_pos = Piece.find_king(self.board, color)
405 | if king_pos == (-1, -1):
406 | return False
407 | return Piece.square_attacked(self.board, *king_pos, color)
408 |
409 | def check_game_end(self):
410 | if self.is_in_check(self.turn):
411 | # Check if no legal moves => checkmate
412 | if not self.has_legal_moves(self.turn):
413 | self.game_over = True
414 | loser = "White" if self.turn == WHITE else "Black"
415 | messagebox.showinfo("Checkmate", f"Checkmate! {loser} loses.")
416 | return
417 | else:
418 | # Check stalemate
419 | if not self.has_legal_moves(self.turn):
420 | self.game_over = True
421 | messagebox.showinfo("Stalemate", "Stalemate! Draw.")
422 | return
423 | self.draw()
424 |
425 | def has_legal_moves(self, color):
426 | for y in range(8):
427 | for x in range(8):
428 | p = self.board[y][x]
429 | if p and p.color == color:
430 | moves = p.get_moves(self.board, x, y, self.en_passant_target)
431 | if moves:
432 | return True
433 | return False
434 |
435 | def reset(self):
436 | self.board = [[None]*8 for _ in range(8)]
437 | self.captured = []
438 | self.history = []
439 | self.selected = None
440 | self.highlighted = []
441 | self.turn = WHITE
442 | self.en_passant_target = None
443 | self.game_over = False
444 | self.setup_board()
445 | self.draw()
446 |
447 | @staticmethod
448 | def dummy_board(board):
449 | # For checking attacked squares in castling: shallow copy pieces without has_moved etc
450 | dummy = []
451 | for row in board:
452 | dummy.append([p if p is None else Piece(p.name, p.color) for p in row])
453 | return dummy
454 |
455 |
456 | if __name__ == "__main__":
457 | root = tk.Tk()
458 | root.title("Chess")
459 | game = ChessGame(root)
460 | root.mainloop()
461 |
--------------------------------------------------------------------------------