├── 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 | Main Board Screenshot 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 | Chessboard GUI 78 | Move Highlighting 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 | --------------------------------------------------------------------------------