├── FreeSerif-4aeK.ttf ├── README.md ├── chess_piece_king.png ├── main.py └── pieces.py /FreeSerif-4aeK.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnemigoPython/chess/e867229aa4c32904c0fcdae5562c180c37b4fc0d/FreeSerif-4aeK.ttf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chess 2 | Local chess game with full ruleset, made in Pygame. 3 | 4 | # controls: 5 | SPACE: flip board 6 | 7 | R: reset/new game 8 | 9 | A: auto-flip board after each turn 10 | 11 | 1-4: change promotion (1 = Queen, 2 = Knight, 3 = Rook, 4 = Bishop) 12 | 13 | P: print game transcript to terminal 14 | 15 | # files: 16 | FreeSerif-4aeK.ttf: font file that is used for text and the unicode images of the chess pieces. 17 | 18 | chess_piece_king.png: window icon image 19 | 20 | main.py: main execution file 21 | 22 | pieces.py: class file for the pieces 23 | 24 | # notes: 25 | Functioning as of 23/01/21 but not fully bug tested/more features coming. Please send any feedback: basileagle@gmail.com 26 | 27 | 24/01/21: Fixed a MAJOR bug with 2 words. Line 30 of pieces.py should have read "COORDS not IN legal_moves", not "not legal_moves". Corrected in newest upload. 28 | -------------------------------------------------------------------------------- /chess_piece_king.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EnemigoPython/chess/e867229aa4c32904c0fcdae5562c180c37b4fc0d/chess_piece_king.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pygame as pg 2 | from pygame import freetype 3 | import pieces 4 | import sys 5 | 6 | w = 660 7 | h = 490 8 | BLACK = (0, 0, 0) 9 | WHITE = (255, 255, 255) 10 | GREY = (142, 142, 142) 11 | SILVER = (192, 192, 192) 12 | LIGHT = (252, 204, 116) 13 | DARK = (87, 58, 46) 14 | GREEN = (0, 255, 0) 15 | RED = (215, 0, 0) 16 | ORANGE = (255, 165, 0) 17 | transcript, turn_number = '', 0 18 | 19 | 20 | def coords_to_notation(coords): 21 | return f'{chr(97 + coords[0])}{8 - coords[1]}' 22 | 23 | 24 | def notation_to_coords(notation): 25 | return ord(notation[0]) - 97, 8 - int(notation[1]) 26 | 27 | 28 | def reset_board(with_pieces=True): 29 | def generate_pieces(colour): 30 | return [pieces.Rook(colour), pieces.Knight(colour), pieces.Bishop(colour), pieces.Queen(colour), 31 | pieces.King(colour), pieces.Bishop(colour), pieces.Knight(colour), pieces.Rook(colour)] 32 | 33 | board = [[None for x in range(8)] for x in range(8)] 34 | if with_pieces: 35 | board[0] = generate_pieces("black") 36 | board[7] = generate_pieces("white") 37 | board[1] = [pieces.Pawn("black") for square in board[1]] 38 | board[6] = [pieces.Pawn("white") for square in board[6]] 39 | return board 40 | 41 | 42 | def draw_squares(screen): 43 | colour_dict = {True: LIGHT, False: DARK} 44 | current_colour = True 45 | for row in range(8): 46 | for square in range(8): 47 | pg.draw.rect(screen, colour_dict[current_colour], ((40 + (square * 50)), 40 + (row * 50), 50, 50)) 48 | current_colour = not current_colour 49 | current_colour = not current_colour 50 | 51 | 52 | def draw_coords(screen, font, flipped): 53 | for row in range(8): 54 | if flipped: 55 | font.render_to(screen, (10, 45 + (row * 50)), chr(49 + row)) 56 | else: 57 | font.render_to(screen, (10, 45 + (row * 50)), chr(56 - row)) 58 | for col in range(8): 59 | if flipped: 60 | font.render_to(screen, (45 + (col * 50), 450), chr(72 - col)) 61 | else: 62 | font.render_to(screen, (45 + (col * 50), 450), chr(65 + col)) 63 | 64 | 65 | def draw_pieces(screen, font, board, flipped): 66 | for row, pieces in enumerate(board[::(-1 if flipped else 1)]): 67 | for square, piece in enumerate(pieces[::(-1 if flipped else 1)]): 68 | if piece: 69 | font.render_to(screen, (piece.img_adjust[0] + (square * 50), piece.img_adjust[1] + (row * 50)), 70 | piece.image, BLACK) 71 | 72 | 73 | def find_square(x, y, flipped): 74 | true_target = int((x - 40) / 50), int((y - 40) / 50) 75 | if flipped: 76 | target_square = 7 - true_target[0], 7 - true_target[1] 77 | else: 78 | target_square = true_target 79 | return true_target, target_square 80 | 81 | 82 | def draw_text(screen, font, turn, colour, check, playing, promotion, auto_flip): 83 | counter_colour = BLACK if turn == 'white' else WHITE 84 | pg.draw.rect(screen, BLACK, (450, 45, 205, 130), width=1) 85 | pg.draw.rect(screen, BLACK, (450, 345, 205, 130), width=1) 86 | pg.draw.rect(screen, colour, (450, 190, 200, 140)) 87 | if playing: 88 | font.render_to(screen, (465, 200), f'{turn} to move', counter_colour) 89 | else: 90 | font.render_to(screen, (465, 200), f'{turn} wins', counter_colour) 91 | pg.draw.rect(screen, counter_colour, (465, 230, 20, 20), width=3) 92 | if auto_flip: 93 | font.render_to(screen, (465, 230), '✓', GREEN) 94 | font.render_to(screen, (490, 230), 'auto-rotate', counter_colour) 95 | promote_dict = {'queen': 9813, 'rook': 9814, 'bishop': 9815, 'knight': 9816} 96 | font.render_to(screen, (465, 260), f'promote: {chr(promote_dict[promotion])}', counter_colour) 97 | if check: 98 | font.render_to(screen, (465, 300), ('CHECK' if playing else 'CHECKMATE'), counter_colour if playing else RED) 99 | 100 | 101 | def draw_legal_moves(screen, colour, moves, board, flipped): 102 | for move in moves: 103 | if flipped: 104 | pg.draw.circle(screen, colour, ((65 + ((7 - move[0]) * 50), 65 + ((7 - move[1]) * 50))), 5) 105 | else: 106 | pg.draw.circle(screen, colour, ((65 + (move[0] * 50), 65 + (move[1] * 50))), 5) 107 | 108 | 109 | def draw_captures(screen, font, captures, flipped): 110 | for e, piece in enumerate([i for i in captures if i.colour == ('white' if flipped else 'black')]): 111 | if e < 6: 112 | font.render_to(screen, (400 + piece.img_adjust[0] + (e * 35), 300 + piece.img_adjust[1]), piece.image, 113 | BLACK) 114 | elif e < 12: 115 | font.render_to(screen, (400 + piece.img_adjust[0] + ((e - 6) * 35), 340 + piece.img_adjust[1]), piece.image, 116 | BLACK) 117 | else: 118 | font.render_to(screen, (400 + piece.img_adjust[0] + ((e - 12) * 35), 380 + piece.img_adjust[1]), 119 | piece.image, BLACK) 120 | for e, piece in enumerate([i for i in captures if i.colour == ('black' if flipped else 'white')]): 121 | if e < 6: 122 | font.render_to(screen, (400 + piece.img_adjust[0] + (e * 30), piece.img_adjust[1]), piece.image, BLACK) 123 | elif e < 12: 124 | font.render_to(screen, (400 + piece.img_adjust[0] + ((e - 6) * 35), 40 + piece.img_adjust[1]), piece.image, 125 | BLACK) 126 | else: 127 | font.render_to(screen, (400 + piece.img_adjust[0] + ((e - 12) * 35), 80 + piece.img_adjust[1]), piece.image, 128 | BLACK) 129 | 130 | 131 | def move_piece(board, target, kings, origin, destination, captures, promotion): 132 | global transcript, turn_number 133 | # start transcript 134 | if target.colour == 'white': 135 | turn_number += 1 136 | transcript += f'{turn_number}. ' 137 | 138 | # piece move conditions 139 | for row in board: 140 | for piece in row: 141 | if piece and piece.name == 'pawn' and piece.en_passant: 142 | piece.en_passant = False 143 | promoting = False 144 | if target.name == 'pawn': 145 | if target.double_move: 146 | target.double_move = False 147 | if abs(origin[1] - destination[1]) == 2: 148 | target.en_passant = True 149 | if origin[0] != destination[0] and not board[destination[1]][destination[0]]: 150 | captures.append(board[destination[1] - target.direction][destination[0]]) 151 | board[destination[1] - target.direction][destination[0]] = None 152 | transcript += coords_to_notation(origin)[0] 153 | if destination[1] == (0 if target.colour == 'white' else 7): 154 | promoting = True 155 | piece_dict = {'queen': pieces.Queen(target.colour), 'knight': pieces.Knight(target.colour), 156 | 'rook': pieces.Rook(target.colour), 'bishop': pieces.Bishop(target.colour)} 157 | if target.name == 'king': 158 | kings[int(target.colour == "black")] = destination 159 | if target.castle_rights: 160 | target.castle_rights = False 161 | if destination[0] - origin[0] == 2: 162 | board[target.back_rank][5] = board[target.back_rank][7] 163 | board[target.back_rank][7] = None 164 | transcript += 'O-O ' 165 | if origin[0] - destination[0] == 2: 166 | board[target.back_rank][3] = board[target.back_rank][0] 167 | board[target.back_rank][0] = None 168 | transcript += 'O-O-O ' 169 | if target.name == 'rook' and target.castle_rights: 170 | target.castle_rights = False 171 | 172 | # finish transcript 173 | if transcript[-2] != 'O': 174 | if target.name != 'pawn': 175 | transcript += target.name[0].upper() if target.name != 'knight' else 'N' 176 | elif board[destination[1]][destination[0]]: 177 | transcript += coords_to_notation(origin)[0] 178 | transcript += f'x{coords_to_notation(destination)} ' if board[destination[1]][destination[0]] else f'{coords_to_notation(destination)} ' 179 | 180 | # add any existing piece to captures list 181 | if board[destination[1]][destination[0]]: 182 | captures.append(board[destination[1]][destination[0]]) 183 | 184 | # move piece 185 | if not promoting: 186 | board[destination[1]][destination[0]] = target 187 | else: 188 | board[destination[1]][destination[0]] = piece_dict[promotion] 189 | transcript = transcript[:-1] + f'={promotion[0].upper()} ' if promotion != 'knight' else '=N ' 190 | board[origin[1]][origin[0]] = None 191 | 192 | # any checks with new board status 193 | enemy_king = kings[int(target.colour == "white")] 194 | check = board[enemy_king[1]][enemy_king[0]].in_check(board, enemy_king) 195 | return board, captures, kings, check 196 | 197 | 198 | def draw_check(screen, board, kings, flipped, turn, checkmate): 199 | if checkmate: 200 | king = kings[1 if turn == 'white' else 0] 201 | else: 202 | king = kings[0 if turn == 'white' else 1] 203 | if flipped: 204 | pg.draw.circle(screen, RED if checkmate else ORANGE, ((65 + ((7 - king[0]) * 50), 65 + ((7 - king[1]) * 50))), 205 | 25, width=3) 206 | else: 207 | pg.draw.circle(screen, RED if checkmate else ORANGE, ((65 + (king[0] * 50), 65 + (king[1] * 50))), 25, width=3) 208 | 209 | 210 | def checkmate(board, turn, kings): 211 | global transcript 212 | for y, row in enumerate(board): 213 | for x, square in enumerate(row): 214 | if square and square.colour != turn: 215 | moves = square.find_moves(board, (x, y), kings, True) 216 | if moves: 217 | transcript = transcript[:-1] + '+ ' 218 | return False 219 | transcript = transcript[:-1] + '# ' 220 | return True 221 | 222 | 223 | def main(): 224 | global transcript, turn_number 225 | # window init 226 | pg.init() 227 | clock = pg.time.Clock() 228 | window_logo = pg.image.load('chess_piece_king.png') 229 | pg.display.set_caption('Chess') 230 | pg.display.set_icon(window_logo) 231 | screen = pg.display.set_mode((w, h)) 232 | 233 | # font/pieces init: the piece icons come from the unicode of this font 234 | freetype.init() 235 | font = freetype.Font('FreeSerif-4aeK.ttf', 50) 236 | micro_font = freetype.Font('FreeSerif-4aeK.ttf', 25) 237 | 238 | # board init 239 | board = reset_board() 240 | 241 | # declare vars 242 | playing = True 243 | turn = 'white' 244 | check = False 245 | board_flipped = False 246 | auto_flip = False 247 | kings = [(4, 7), (4, 0)] 248 | promotion = 'queen' 249 | target_square = None 250 | target = None 251 | captures = [] 252 | legal_moves = [] 253 | 254 | while True: 255 | screen.fill(GREY) 256 | COLOUR = SILVER if turn == 'white' else BLACK 257 | draw_squares(screen) 258 | if target_square: 259 | pg.draw.rect(screen, COLOUR, ((40 + (true_target[0] * 50)), 40 + (true_target[1] * 50), 50, 50), width=2) 260 | draw_coords(screen, font, board_flipped) 261 | draw_pieces(screen, font, board, board_flipped) 262 | for event in pg.event.get(): 263 | if event.type == pg.MOUSEBUTTONDOWN: 264 | if playing and 441 > event.pos[0] > 39 and 441 > event.pos[1] > 39: 265 | if event.button != 3: 266 | true_target, target_square = find_square(event.pos[0], event.pos[1], board_flipped) 267 | target = board[target_square[1]][target_square[0]] 268 | if target and turn == target.colour: 269 | legal_moves = target.find_moves(board, target_square, kings, check) 270 | elif target_square and target: 271 | true_target, destination = find_square(event.pos[0], event.pos[1], board_flipped) 272 | if destination in legal_moves: 273 | board, captures, kings, check = move_piece(board, target, kings, target_square, destination, 274 | captures, promotion) 275 | if check and checkmate(board, turn, kings): 276 | playing = False 277 | target_square = None 278 | else: 279 | turn = 'black' if turn == 'white' else 'white' 280 | if auto_flip and board_flipped == (turn == 'white'): 281 | board_flipped = not board_flipped 282 | if target_square: 283 | true_target = 7 - true_target[0], 7 - true_target[1] 284 | legal_moves = [] 285 | else: 286 | target_square = None 287 | else: 288 | target_square = None 289 | else: 290 | target_square = None 291 | if event.type == pg.KEYDOWN: 292 | if event.key == pg.K_SPACE: 293 | board_flipped = not board_flipped 294 | if target_square: 295 | true_target = 7 - true_target[0], 7 - true_target[1] 296 | if event.key == pg.K_r: 297 | board = reset_board() 298 | kings = [(4, 7), (4, 0)] 299 | turn = 'white' 300 | check = False 301 | board_flipped = False 302 | target_square = None 303 | captures = [] 304 | playing = True 305 | transcript, turn_number = '', 0 306 | if event.key == pg.K_a: 307 | auto_flip = not auto_flip 308 | if event.key == pg.K_1: 309 | promotion = 'queen' 310 | if event.key == pg.K_2: 311 | promotion = 'knight' 312 | if event.key == pg.K_3: 313 | promotion = 'rook' 314 | if event.key == pg.K_4: 315 | promotion = 'bishop' 316 | if event.key == pg.K_p: 317 | print(transcript) 318 | if event.type == pg.QUIT: 319 | pg.quit() 320 | sys.exit() 321 | draw_text(screen, micro_font, turn, COLOUR, check, playing, promotion, auto_flip) 322 | if target_square and target and turn == target.colour and legal_moves: 323 | draw_legal_moves(screen, COLOUR, legal_moves, board, board_flipped) 324 | if captures: 325 | draw_captures(screen, font, captures, board_flipped) 326 | if check: 327 | draw_check(screen, board, kings, board_flipped, turn, not playing) 328 | pg.display.update() 329 | clock.tick(60) 330 | 331 | 332 | if __name__ == '__main__': 333 | main() 334 | -------------------------------------------------------------------------------- /pieces.py: -------------------------------------------------------------------------------- 1 | class Piece: 2 | images = ['white_king', 'white_queen', 'white_rook', 'white_bishop', 'white_knight', 'white_pawn', 'black_king', 3 | 'black_queen', 'black_rook', 'black_bishop', 'black_knight', 'black_pawn'] 4 | 5 | def __init__(self, colour, name, img_adjust=(50, 50), unbounded=True): 6 | self.colour = colour 7 | self.name = name 8 | self.image = chr(int('98' + str(self.images.index(f'{colour}_{name}') + 12))) 9 | self.img_adjust = img_adjust 10 | self.unbounded = unbounded 11 | 12 | def find_moves(self, board, location, kings, check): 13 | x, y = location[0], location[1] 14 | legal_moves = [] 15 | additional = set() 16 | if self.name == 'pawn': 17 | additional.update(self.additional_moves(board, x, y)) 18 | for x2, y2 in self.moveset.union(additional): 19 | if any(i < 0 for i in (x + x2, y + y2)): 20 | continue 21 | try: 22 | coords = x + x2, y + y2 23 | square = board[coords[1]][coords[0]] 24 | if self.name != 'pawn' and (square is None or square and square.colour != self.colour) or \ 25 | self.name == 'pawn' and ((x2 == 0 and square is None) or (x2, y2) in additional): 26 | king = kings[int(self.colour == "black")] 27 | king_pos = coords if king == (x, y) else king 28 | if not board[king[1]][king[0]].in_check(board, king_pos, moved_from=location, moved_to=coords): 29 | legal_moves.append(coords) 30 | if square and square.colour != self.colour or coords not in legal_moves and not check: 31 | continue 32 | while self.unbounded or self.name == 'pawn' and self.double_move: 33 | coords = coords[0] + x2, coords[1] + y2 34 | square = board[coords[1]][coords[0]] 35 | if check and board[king[1]][king[0]].in_check(board, king_pos, moved_from=location, 36 | moved_to=coords): 37 | continue 38 | if all(i >= 0 for i in coords) and self.name != 'pawn' and (square is None or square and 39 | square.colour != self.colour) or self.name == 'pawn' and ( 40 | x2 == 0 and square is None): 41 | legal_moves.append(coords) 42 | elif not check: 43 | break 44 | if self.name == 'pawn' or square and square.colour != self.colour: 45 | break 46 | except IndexError: 47 | continue 48 | if self.name == 'king' and not check and self.castle_rights and self.castle(board, x, y): 49 | legal_moves.extend(self.castle(board, x, y)) 50 | return legal_moves 51 | 52 | 53 | class King(Piece): 54 | def __init__(self, colour): 55 | self.back_rank = 7 if colour == 'white' else 0 56 | self.moveset = {(x, y) for x in range(-1, 2) for y in range(-1, 2) if x != 0 or y != 0} 57 | self.castle_rights = True 58 | super().__init__(colour, 'king', unbounded=False) 59 | 60 | def in_check(self, board, location, moved_from=None, moved_to=None): 61 | for move in self.moveset: 62 | coords = location 63 | square = board[coords[1]][coords[0]] 64 | while (coords != moved_to or location == moved_to) and ( 65 | coords == location or coords == moved_from or square is None): 66 | try: 67 | if any(i < 0 or i > 7 for i in (coords[0] + move[0], coords[1] + move[1])): 68 | break 69 | coords = coords[0] + move[0], coords[1] + move[1] 70 | square = board[coords[1]][coords[0]] 71 | except IndexError: 72 | break 73 | if square is None or square.colour == self.colour or coords == moved_to: 74 | continue 75 | if 0 in move and (square.name == 'rook' or square.name == 'queen') or 0 not in move and ( 76 | square.name == 'bishop' or square.name == 'queen' or (square.name == 'pawn' and 77 | location[1] - coords[1] == square.direction)): 78 | return True 79 | for x, y in {(x, y) for x in range(-2, 3) for y in range(-2, 3) if x != 0 and y != 0 and abs(x) != abs(y)}: 80 | try: 81 | coords = location[0] + x, location[1] + y 82 | square = board[coords[1]][coords[0]] 83 | if any(i < 0 for i in (coords[0], coords[1])): 84 | continue 85 | if square and square.colour != self.colour and square.name == 'knight' and coords != moved_to: 86 | return True 87 | except IndexError: 88 | continue 89 | return False 90 | 91 | def castle(self, board, x, y): 92 | moves = [] 93 | if board[self.back_rank][0] and board[self.back_rank][0].name == 'rook' and board[self.back_rank][ 94 | 0].castle_rights: 95 | squares = [(i, self.back_rank) for i in range(1, 4)] 96 | if all(not piece for piece in board[self.back_rank][1:4]) and all( 97 | not self.in_check(board, square) for square in squares): 98 | moves.append((2, self.back_rank)) 99 | if board[self.back_rank][7] and board[self.back_rank][7].name == 'rook' and board[self.back_rank][ 100 | 7].castle_rights: 101 | squares = [(i, self.back_rank) for i in range(5, 7)] 102 | if all(not piece for piece in board[self.back_rank][5:7]) and all( 103 | not self.in_check(board, square) for square in squares): 104 | moves.append((6, self.back_rank)) 105 | return moves 106 | 107 | 108 | class Queen(Piece): 109 | def __init__(self, colour): 110 | self.moveset = {(x, y) for x in range(-1, 2) for y in range(-1, 2) if x != 0 or y != 0} 111 | super().__init__(colour, 'queen', img_adjust=(47, 50)) 112 | 113 | 114 | class Rook(Piece): 115 | def __init__(self, colour): 116 | self.moveset = {(x, y) for x in range(-1, 2) for y in range(-1, 2) if (x == 0 or y == 0) and (x != 0 or y != 0)} 117 | self.castle_rights = True 118 | super().__init__(colour, 'rook', img_adjust=(52, 53)) 119 | 120 | 121 | class Bishop(Piece): 122 | def __init__(self, colour): 123 | self.moveset = {(x, y) for x in range(-1, 2) for y in range(-1, 2) if x != 0 and y != 0} 124 | super().__init__(colour, 'bishop', img_adjust=(49, 49)) 125 | 126 | 127 | class Knight(Piece): 128 | def __init__(self, colour): 129 | self.moveset = {(x, y) for x in range(-2, 3) for y in range(-2, 3) if x != 0 and y != 0 and abs(x) != abs(y)} 130 | super().__init__(colour, 'knight', img_adjust=(50, 52), unbounded=False) 131 | 132 | 133 | class Pawn(Piece): 134 | def __init__(self, colour): 135 | self.direction = -1 if colour == 'white' else 1 136 | self.moveset = {(0, y * self.direction) for y in range(1, 2)} 137 | self.en_passant = False 138 | self.double_move = True 139 | super().__init__(colour, 'pawn', img_adjust=(54, 52), unbounded=False) 140 | 141 | def additional_moves(self, board, x, y): 142 | valid_attacks = set() 143 | for n in range(-1, 2, 2): 144 | try: 145 | square = board[y + self.direction][x + n] 146 | if square and square.colour != self.colour: 147 | valid_attacks.add((n, self.direction)) 148 | else: 149 | square = board[y][x + n] 150 | if square and square.name == 'pawn' and square.en_passant: 151 | valid_attacks.add((n, self.direction)) 152 | except IndexError: 153 | pass 154 | return valid_attacks 155 | --------------------------------------------------------------------------------