Pygame – Why in my Chess game, sometimes friendly pieces can capture each other?

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()

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.

New contributor

user26649650 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật