I have made a chess game according to a YouTube tutorial, and it does not work as expected. There are some really weird bugs going on, and I have checked the code thousand times and do not have a solution for it.
I mostly suspect that the bug is somewhere within the board.py because that is the place where we calculate all the moves. Now I found some bugs in this game and one of them is that sometimes the pieces that are friendly, can actually capture each other, it never happens at the start of the game, but it happens after one of the kings have been in check, and if for example a piece is pinned(if it moves, the king will be in check) the other pieces that had their moves limited so they can protect the king, they’ll still have their moves and can actually capture the pinned friendly piece. Another similar situation is that for example if the left knight protects the king, it will work fine as expected, but the other knight will start working weird and moves freely regardless of the king’s situation, it can also capture the not the other friendly pieces, but the other knight!
Here are some screenshots to make it clear:
You can see that the queen’s moves are not limited because the king is not in check, I used the bishop to block the way of the white bishop and queen’s last possible move is still active, which is where the black bishop is
Previously I also used the moved knight to protect the king, and moved it to a different location, and the other knight as you can see can somehow capture the moved knight
Because the previous move of this knight was limited to where it can protect the king, that move is still active and it can capture the friendly bishop although in the codes that knight is prevented from adding the squares with friendly pieces in them as the valid moves
So, i am posting the classes that I suspect that one of them is containing the bug here so maybe you guys can help me out with it:
settings.py
WIDTH = HEIGHT = 800
SCREEN_WIDTH = SCREEN_HEIGHT = 800
ROWS = COLS = 8
NAME = 'Chess Game'
SquareSize = WIDTH // COLS
SHOW_MOVES = True
SHOW_TRACES = False
SHOW_HOVER = True
CASTLING = True
PAWN_EN_PASSANT = True
PROMOTION = True
WARN_CHECKS = False
WARN_PINS = False
INSTANT_CHECKMATE = False
TO_QUEEN = True
TO_LOST_PIECES = True
main.py
import pygame as pg
from sys import exit
from settings import *
from game import Game
from square import Square
from move import Move
from piece import *
class Main:
def __init__(self):
pg.init()
self.screen = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption(NAME)
self.game = Game()
def mainloop(self):
game, board, screen, dragger = self.game, self.game.board, self.screen, self.game.dragger
while True:
game.show_bg(screen)
game.show_last_move(screen)
game.show_moves(screen)
game.show_pieces(screen)
if not pg.mouse.get_focused(): game.set_hover(-1, -1)
game.show_hover(screen, game.nextPlayer)
if dragger.dragging:
dragger.update_texture(screen)
for event in pg.event.get():
if event.type == pg.MOUSEBUTTONDOWN: # click
dragger.update_mouse(event.pos)
clicked_row = dragger.mouseY // SquareSize
clicked_col = dragger.mouseX // SquareSize
if isinstance(board.squares[clicked_row][clicked_col].piece, Knight):
pass
print('----------------------------------')
# if the clicked square has a piece:
if board.squares[clicked_row][clicked_col].has_piece():
piece = board.squares[clicked_row][clicked_col].piece
if piece.color == game.nextPlayer: # valid piece (color) ?
board.calculate_moves(piece, clicked_row, clicked_col, True)
dragger.save_initial(event.pos)
dragger.drag_piece(piece)
# show methods
game.show_bg(screen)
game.show_last_move(screen)
game.show_moves(screen)
game.show_pieces(screen)
elif event.type == pg.MOUSEMOTION:
motion_row, motion_col = event.pos[1], event.pos[0]
game.set_hover(motion_row, motion_col)
motion_row, motion_col = motion_row // SquareSize, motion_col // SquareSize
print('r'+str(motion_row)+', '+str(motion_col)+', ('+str(event.pos[1])+', '+str(event.pos[0])+') ', end='r')
if dragger.dragging:
dragger.update_mouse(event.pos)
game.show_bg(screen)
game.show_last_move(screen)
game.show_moves(screen)
game.show_pieces(screen)
game.show_hover(screen, game.nextPlayer)
dragger.update_texture(screen)
elif event.type == pg.MOUSEBUTTONUP:
if dragger.dragging:
dragger.update_mouse(event.pos)
released_row, released_col = dragger.mouseY // SquareSize, dragger.mouseX // SquareSize
initial, final = Square(dragger.initial_row, dragger.initial_col), Square(released_row, released_col)
move = Move(initial, final)
if board.valid_move(dragger.piece, move):
game.playSound(True) if board.squares[released_row][released_col].has_enemy_piece(game.nextPlayer) else game.playSound()
board.move(dragger.piece, move, False)
if PAWN_EN_PASSANT: board.en_passant_on(dragger.piece)
game.show_bg(screen)
game.show_last_move(screen)
game.show_pieces(screen)
game.next_turn()
else: game.playSound(None)
dragger.undrag_piece()
elif event.type == pg.KEYDOWN:
if event.key == pg.K_TAB: game.change_theme()
if event.key == pg.K_r:
game.reset()
game, board, dragger = self.game, self.game.board, self.game.dragger
if event.type == pg.QUIT:
pg.quit()
exit()
pg.display.update()
main = Main()
main.mainloop()
dragger.py
import pygame as pg
from piece import Piece
from settings import *
class Dragger:
def __init__(self):
self.mouseX = self.mouseY = self.initial_row = self.initial_col = self.piece = None
self.dragging = False
# blit method
def update_texture(self, surface: pg.Surface):
#add this method can be turned into a general function
self.piece.set_texture(size = 125)
img = pg.image.load(self.piece.texture)
img_center = self.mouseX, self.mouseY
self.piece.texture_rect = img.get_rect(center = img_center)
surface.blit(img, self.piece.texture_rect)
# other methods
def update_mouse(self, pos):
self.mouseX, self.mouseY = pos
def save_initial(self, pos):
self.initial_row, self.initial_col = pos[1] // SquareSize, pos[0] // SquareSize
def drag_piece(self, piece: Piece): self.piece, self.dragging = piece, True
def undrag_piece(self): self.piece, self.dragging = None, False
board.py
from copy import deepcopy
from settings import *
from square import Square
from move import Move
from sound import Sound
from piece import *
from itertools import permutations, product
class Board:
def __init__(self):
self.squares, self.lastMove = [[None for row in range(ROWS)] for col in range(COLS)], None
self._create()
teams = {'white', 'black'}
for team in teams: self._add_pieces(team)
def move(self, piece: Piece, move: Move, testing: bool = False):
initial = move.initial
final = move.final
en_passant_empty = self.squares[final.row][final.col].isEmpty()
self.squares[initial.row][initial.col].piece = None
self.squares[final.row][final.col].piece = piece
diff = final.col - initial.col
if isinstance(piece, Pawn):
if en_passant_empty and diff != 0:
self.squares[initial.row][initial.col + diff].piece = None
self.squares[final.row][final.col].piece = piece
if not testing: Sound('assets/Audios/capture.wav').play()
else: self.check_promotion(piece, final)
if isinstance(piece, King):
if not testing and abs(initial.col - final.col) == 2:
rook = piece.left_rook if diff < 0 else piece.right_rook
self.move(rook, rook.moves[-1])
Sound('assets/Audios/castling.mp3').play()
if isinstance(piece, Knight) and piece.color == 'black':print(' '+piece.name+' move '+str(move)+'n')
piece.moved = True
piece.clear_moves()
self.lastMove = move
def valid_move(self, piece: Piece, move: Move): return move in piece.moves
def check_promotion(self, piece: Piece, final: Square):
if PROMOTION and (final.row == 0 or final.row == 7):
if TO_LOST_PIECES:
pass
elif TO_QUEEN: self.squares[final.row][final.col].piece = Queen(piece.color)
else: return
Sound('assets/Audios/promotion.mp3').play()
def en_passant_on(self, piece: Piece):
if not PAWN_EN_PASSANT or not isinstance(piece, Pawn) or not piece.en: return
for row in range(ROWS):
for col in range(COLS):
if isinstance(self.squares[row][col].piece, Pawn): self.squares[row][col].piece.en_passant = False
piece.en_passant = True
def in_check(self, piece: Piece, move: Move):
if not INSTANT_CHECKMATE:
virtual_piece = deepcopy(piece)
virtual_board = deepcopy(self)
virtual_board.move(virtual_piece, move, True)
for row in range(ROWS):
for col in range(COLS):
if virtual_board.squares[row][col].has_enemy_piece(piece.color):
p = virtual_board.squares[row][col].piece
virtual_board.calculate_moves(p, row, col, False)
for m in p.moves:
c = None
if m.final.piece != None:
c = m.final.piece.color
print(m.final.has_enemy_piece(c))
if isinstance(m.final.piece, King): return True
return False
def calculate_moves(self, piece: Piece, row: int, col: int, virtual: bool = True):
def all_combos(tpl: tuple, add = False):
tmp = set()
per = set(permutations(tpl))
sign_variations = set(product(*[(x, -x) for x in tpl]))
for variation in sign_variations: tmp.update(permutations(variation))
if add:
res = []
for Row, Col in tmp: res.append((row+Row, col+Col))
return set(res)
return tmp
def pawn_moves():
if isinstance(piece, Pawn):
steps = 1 if piece.moved else 2
piece.en = True if steps == 2 else False
start, end = row + piece.dir, row + (piece.dir * (1 + steps))
for move_row in range(start, end, piece.dir):
if Square.in_range(move_row) and self.squares[move_row][col].isEmpty():
initial, final = Square(row, col), Square(move_row, col)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move):
piece.add_move(move)
else:
piece.add_move(move)
else: break
move_row, move_cols = row + piece.dir, [col-1, col+1]
for move_col in move_cols:
if Square.in_range(move_row, move_col) and self.squares[move_row][move_col].has_enemy_piece(piece.color):
initial = Square(row, col)
final_piece = self.squares[move_row][move_col].piece
final = Square(move_row, move_col, final_piece)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move):
piece.add_move(move)
else:
piece.add_move(move)
r, fr = 3 if piece.color == 'white' else 4, 2 if piece.color == 'white' else 5
if Square.in_range(col-1) and row == r and self.squares[row][col-1].has_enemy_piece(piece.color):
p = self.squares[row][col-1].piece
if isinstance(p, Pawn):
if p.en_passant:
piece.en_passant = False
initial = Square(row, col)
final = Square(fr, col-1, p)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move):
piece.add_move(move)
else:
piece.add_move(move)
if Square.in_range(col+1) and row == r:
if self.squares[row][col+1].has_enemy_piece(piece.color):
p = self.squares[row][col+1].piece
if isinstance(p, Pawn):
if p.en_passant:
initial = Square(row, col)
final = Square(fr, col+1, p)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move):
piece.add_move(move)
else:
piece.add_move(move)
def knight_moves():
possible_moves = all_combos(Knight.move_range, True)
for possible_move in possible_moves:
possible_move_row, possible_move_col = possible_move
if Square.in_range(possible_move_row, possible_move_col) and self.squares[possible_move_row][possible_move_col].isEmpty_or_enemy(piece.color) and not self.squares[possible_move_row][possible_move_col].has_friendly_piece(piece.color):
initial = Square(row, col)
final_piece = self.squares[possible_move_row][possible_move_col].piece
final = Square(possible_move_row, possible_move_col, final_piece)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move):
if piece.color == 'black': print(' '+piece.name+' move '+str(move)+'n')
piece.add_move(move)
else:
if piece.color == 'black': print(' else '+piece.name+' move '+str(move)+'n')
piece.add_move(move)
def straght_moves(incrs: set):
for inc in incrs:
row_incr, col_incr = inc
possible_move_row = row + row_incr
possible_move_col = col + col_incr
while True:
if Square.in_range(possible_move_row, possible_move_col):
initial = Square(row, col)
final_piece = self.squares[possible_move_row][possible_move_col].piece
final = Square(possible_move_row, possible_move_col, final_piece)
move = Move(initial, final)
if self.squares[possible_move_row][possible_move_col].isEmpty():
if virtual:
if not self.in_check(piece, move): piece.add_move(move)
else: piece.add_move(move)
elif self.squares[possible_move_row][possible_move_col].has_enemy_piece(piece.color):
if virtual:
if not self.in_check(piece, move): piece.add_move(move)
else: piece.add_move(move)
break
elif self.squares[possible_move_row][possible_move_col].has_friendly_piece(piece.color): break
else: break
possible_move_row, possible_move_col = possible_move_row + row_incr, possible_move_col + col_incr
def king_moves():
adjs = all_combos(Rook.move_range, True).union(all_combos(Bishop.move_range, True))
for possible_move in adjs:
possible_move_row, possible_move_col = possible_move
if Square.in_range(possible_move_row, possible_move_col) and self.squares[possible_move_row][possible_move_col].isEmpty_or_enemy(piece.color):
initial = Square(row, col)
final = Square(possible_move_row, possible_move_col)
move = Move(initial, final)
if virtual:
if not self.in_check(piece, move): piece.add_move(move)
else: piece.add_move(move)
if CASTLING and not piece.moved:
left_rook = self.squares[row][0].piece
if isinstance(left_rook, Rook):
if not left_rook.moved:
for c in range(1, 4):
if self.squares[row][c].has_piece(): break
if c == 3:
piece.left_rook = left_rook
initial = Square(row, 0)
final = Square(row, 3)
moveR = Move(initial, final)
initial = Square(row, col)
final = Square(row, 2)
moveK = Move(initial, final)
if virtual:
if not self.in_check(piece, moveK) and not self.in_check(left_rook, moveR):
left_rook.add_move(moveR)
piece.add_move(moveK)
else:
left_rook.add_move(moveR)
piece.add_move(moveK)
right_rook = self.squares[row][7].piece
if isinstance(right_rook, Rook):
if not right_rook.moved:
for c in range(5, 7):
if self.squares[row][c].has_piece(): break
if c == 6:
piece.right_rook = right_rook
initial = Square(row, 7)
final = Square(row, 5)
moveR = Move(initial, final)
initial = Square(row, col)
final = Square(row, 6)
moveK = Move(initial, final)
if virtual:
if not self.in_check(piece, moveK) and not self.in_check(right_rook, moveR):
right_rook.add_move(moveR)
piece.add_move(moveK)
else:
right_rook.add_move(moveR)
piece.add_move(moveK)
if isinstance(piece, Pawn): pawn_moves()
elif isinstance(piece, Knight): knight_moves()
elif isinstance(piece, Bishop): straght_moves(all_combos(Bishop.move_range))
elif isinstance(piece, Rook): straght_moves(all_combos(Rook.move_range))
elif isinstance(piece, Queen): straght_moves(all_combos(Bishop.move_range).union(all_combos(Rook.move_range)))
elif isinstance(piece, King): king_moves()
def _create(self):
for row in range(ROWS):
for col in range(COLS): self.squares[row][col] = Square(row, col)
def _add_pieces(self, color):
row_pawn, row_other = (6, 7) if color == 'white' else (1, 0)
for col in range(COLS): self.squares[row_pawn][col] = Square(row_pawn, col, Pawn(color))
for col in (1, 6): self.squares[row_other][col] = Square(row_other, col, Knight(color))
for col in (2, 5): self.squares[row_other][col] = Square(row_other, col, Bishop(color))
for col in (0, 7): self.squares[row_other][col] = Square(row_other, col, Rook(color))
self.squares[row_other][3] = Square(row_other, 3, Queen(color))
self.squares[row_other][4] = Square(row_other, 4, King(color))
piece.py
from move import Move
class Piece:
def __init__(self, name, color, worth, texture = None, texture_rect = None):
self.name, self.color = name, color
worth_sign = 1 if color == 'white' else -1
self.worth = worth * worth_sign
self.moves = []
self.moved = False
self.texture = texture
self.set_texture()
self.texture_rect = texture_rect
def set_texture(self, size = 80): self.texture = f'assets/textures/{size}px/{self.color}-{self.name}-{size}px.png'
def add_move(self, move: Move): self.moves.append(move)
def clear_moves(self): return self.moves.clear()
class Pawn(Piece):
def __init__(self, color):
self.dir, self.en_passant, self.en = -1 if color == 'white' else 1, False, False
super().__init__('pawn', color, 1.0)
class Knight(Piece):
move_range = (1, 2)
def __init__(self, color):
super().__init__('knight', color, 3.0)
class Bishop(Piece):
move_range = (1, 1)
def __init__(self, color):
super().__init__('bishop', color, 3.001)
class Rook(Piece):
move_range = (1, 0)
def __init__(self, color):
super().__init__('rook', color, 5.0)
class Queen(Piece):
def __init__(self, color):
super().__init__('queen', color, 9.0)
class King(Piece):
def __init__(self, color):
self.left_rook = None
self.right_rook = None
from math import inf
super().__init__('king', color, inf)
move.py
# from square import Square
class Move:
def __init__(self, initial, final): self.initial, self.final = initial, final
def __str__(self):
s = ''
s += f'({self.initial.row}, {self.initial.col})'
s += f' -> ({self.final.row}, {self.final.col})'
return s
def __eq__(self, other: 'Move'): return self.initial == other.initial and self.final == other.final
square.py
from piece import Piece
class Square:
def __init__(self, row: int, col: int, piece: Piece = None): self.row, self.col, self.piece = row, col, piece
def __eq__(self, other: 'Square'): return self.row == other.row and self.col == other.col
def has_piece(self): return self.piece != None
def isEmpty(self): return not self.has_piece()
def has_friendly_piece(self, color): return self.has_piece() and self.piece.color == color
def has_enemy_piece(self, color): return self.has_piece() and self.piece.color != color
def isEmpty_or_enemy(self, color): return self.isEmpty() or self.has_enemy_piece(color)
@staticmethod
def in_range(*args):
for arg in args:
if arg < 0 or arg > 7: return False
return True
Hope you understand it and help me fix it, these are the only bugs remaining from the game as far as I see. Thanks in advanced.
user26649650 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
3