Implémentation d'un jeu d'échecs sur Python

Tags :
  • POK
  • 2023-2024
  • temps 2
  • Pygame
  • Jeux
  • Échecs
Auteurs :
  • Duc DANG VU

Ce POK a pour objectif d'implémenter un jeu d'échecs sur Python et de créer un code qui peut résoudre des problèmes simples.

Prérequis

Connaissance basique de la programmation objet avec Python et des règles du jeu d'échecs.

Il est possible de tester l'implémentation du jeu sur le lien suivant: Jeu d'échecs avec Python Tout les codes présentés sont disponibles dans le dossier Git du POK2: codes du POK2

Introduction

Le jeu d'échecs est un jeu classique, mais relativement complexe à implémenter. Ce POK utilisera les connaissances acquises lors du premier MON du temps 2, pour implémenter l'interface du jeu. Afin d'éviter tout chevauchement entre le MON et ce POK, cette partie du POK sera réalisée lors du deuxième sprint. Le premier sprint sera dédié à l'implémentation du code pour résoudre des problèmes simples.

Backlog du premier sprint

Ce premier sprint aura donc pour objectif d'implémenter le code de résolution de problèmes simples. Voici les fonctionnalités que devront être développées, ainsi que leurs complexités:

Implémentation du code permettant de résoudre des problèmes simples

La première partie de ce POK a pour objectif de créer une méthode qui permettra de résoudre des problèmes simples du jeu des échecs. Autrement dit, pour une position donnée, le programme devra donner le coup gagnant qui mène à un échec et mat (en 1, 2, 3 voire 4 coups). Avant d'arriver à cette méthode, plusieurs étapes intermédiaires sont nécessaires, notamment pour représenter le plateau d'échecs et ses pièces.

Toute la suite de ce POK considérera que le lecteur connaît les règles des échecs, ainsi que son vocabulaire de base. Les termes un peu plus techniques (clouages, pat etc...) seront expliqués lorsqu'ils seront introduits.

Représentation des éléments du plateau d'échecs

Implémentation des cases

Les premiers éléments à implémenter sont les 64 cases du plateau. On crée une classe Cell, qui contient les attributs suivants:

Ces cases vont être incluses dans la liste représentant le plateau d'échecs.

Cliquer pour voir le code de la classe Cell

class Cell():
    def __init__(self, row, column, promote = None):
        self.name = chr(column + 97) + f"{row+1}"
        self.row = row
        self.column = column
        self.promote = promote

Implémentations des pièces

Il faut maintenant implémenter les différentes pièces du jeu, ainsi que leurs mouvements. On crée une classe Piece qui contient les attributs suivants:

Cliquer pour voir le code de la classe Piece

class Piece():
    def __init__(self, IsWhite, value, letter, current_cell):
        self.possible_moves = []
        self.current_cell = current_cell
        self.IsWhite = IsWhite
        self.value = value
        self.letter = letter

A présent, il faut implémenter chaque pièce. On crée donc 6 classes qui correspond à chaque pièce (Pawn, Bishop, Knight, Rook, Queen, King). Ces classes héritent de la classe Piece, et ont une méthode get_possible_moves. Cette méthode va ajouter à l'attribut possible_moves toutes les cases vers lesquelles la pièce peut aller. Voilà comment fonctionnent ces méthodes:

Cliquer pour voir le code des 6 classes correspondantes au pièces du jeu

class King(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 1000, "K", current_cell)
        else:
            super().__init__(IsWhite, -1000, "k", current_cell)

    def get_possible_moves(self, board):
        self.possible_moves = []
        column = self.current_cell.column
        row = self.current_cell.row
        for k in [-1, 0, 1]:
            for l in [-1, 0, 1]:
                if not (k == 0 and l == 0):
                    new_column = column + k
                    new_row = row + l
                    if new_row <= 7 and new_column <= 7 and new_row >= 0 and new_column >= 0 :
                        if board[new_row][new_column][1] is None: 
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                        else:
                            obstacle_color = board[new_row][new_column][1].IsWhite
                            if obstacle_color ^ self.IsWhite:
                                new_cell = Cell(new_row, new_column)
                                self.possible_moves.append(new_cell)

class Rook(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 5, "R", current_cell)
        else:
            super().__init__(IsWhite, -5, "r", current_cell)
    
    def get_possible_moves(self, board):
        self.possible_moves = []
        for k in [-1, 1]:
            new_column = self.current_cell.column + k
            new_row = self.current_cell.row
            while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                if board[new_row][new_column][1] is None: 
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                else:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                    break
                new_column = new_column + k
        for l in [-1, 1]:
            new_column = self.current_cell.column
            new_row = self.current_cell.row + l
            while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                if board[new_row][new_column][1] is None: 
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                else:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                    break
                new_row = new_row + l

class Knight(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 3, "N", current_cell)
        else:
            super().__init__(IsWhite, -3, "n", current_cell)

    def get_possible_moves(self, board):
        self.possible_moves = []
        column = self.current_cell.column
        row = self.current_cell.row
        L = [-1, -2, 1, 2]
        for k in L:
            temp_L = [-1, -2, 1, 2]
            temp_L.remove(k)
            temp_L.remove(-k)
            for l in temp_L:
                new_column = column + k
                new_row = row + l
                if new_row <= 7 and new_column <= 7 and new_row >= 0 and new_column >= 0:
                    if board[new_row][new_column][1] is None: 
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                    else:
                        obstacle_color = board[new_row][new_column][1].IsWhite
                        if obstacle_color ^ self.IsWhite:
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
        
class Bishop(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 3, "B", current_cell)
        else:
            super().__init__(IsWhite, -3, "b", current_cell)
    
    def get_possible_moves(self, board):
        self.possible_moves = []
        for k in [-1, 1]:
            for l in [-1, 1]:
                new_column = self.current_cell.column + k
                new_row = self.current_cell.row + l
                while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                    if board[new_row][new_column][1] is None: 
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                    else:
                        obstacle_color = board[new_row][new_column][1].IsWhite
                        if obstacle_color ^ self.IsWhite:
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                        break
                    new_column = new_column + k
                    new_row = new_row + l

class Queen(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 9, "Q", current_cell)
        else:
            super().__init__(IsWhite, -9, "q", current_cell)
    
    def get_possible_moves(self, board):
        self.possible_moves = []
        for k in [-1, 1]:
            new_column = self.current_cell.column + k
            new_row = self.current_cell.row
            while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                if board[new_row][new_column][1] is None: 
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                else:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                    break
                new_column = new_column + k
        for l in [-1, 1]:
            new_column = self.current_cell.column
            new_row = self.current_cell.row + l
            while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                if board[new_row][new_column][1] is None: 
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                else:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
                    break
                new_row = new_row + l
        for k in [-1, 1]:
            for l in [-1, 1]:
                new_column = self.current_cell.column + k
                new_row = self.current_cell.row + l
                while new_column >= 0 and new_column <= 7 and new_row >= 0 and new_row <= 7:
                    if board[new_row][new_column][1] is None: 
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                    else:
                        obstacle_color = board[new_row][new_column][1].IsWhite
                        if obstacle_color ^ self.IsWhite:
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                        break
                    new_column = new_column + k
                    new_row = new_row + l

class Pawn(Piece):
    def __init__(self, IsWhite, current_cell):
        if IsWhite:
            super().__init__(IsWhite, 1, "", current_cell)
        else:
            super().__init__(IsWhite, -1, "", current_cell)
    
    
    def get_possible_moves(self, board):
        if self.IsWhite:
            move = 1
        else:
            move = -1
        self.possible_moves = []
        if self.current_cell.row == 6 and self.IsWhite:
            new_column = self.current_cell.column + 0
            new_row = self.current_cell.row + move
            if new_column >= 0 and new_column <= 7:
                self.possible_moves.append(Cell(new_row, new_column, "Q"))
                self.possible_moves.append(Cell(new_row, new_column, "N"))
                self.possible_moves.append(Cell(new_row, new_column, "B"))
                self.possible_moves.append(Cell(new_row, new_column, "R"))
            for k in [-1, 1]:
                new_column = self.current_cell.column + k
                new_row = self.current_cell.row + move
                if new_column >= 0 and new_column <= 7:
                    if board[new_row][new_column][1] is not None:
                        obstacle_color = board[new_row][new_column][1].IsWhite
                        if obstacle_color ^ self.IsWhite:
                            self.possible_moves.append(Cell(new_row, new_column, "Q"))
                            self.possible_moves.append(Cell(new_row, new_column, "N"))
                            self.possible_moves.append(Cell(new_row, new_column, "B"))
                            self.possible_moves.append(Cell(new_row, new_column, "R"))

        elif self.current_cell.row == 1 and not self.IsWhite:
            new_column = self.current_cell.column + 0
            new_row = self.current_cell.row + move
            if new_column >= 0 and new_column <= 7:
                self.possible_moves.append(Cell(new_row, new_column, "q"))
                self.possible_moves.append(Cell(new_row, new_column, "n"))
                self.possible_moves.append(Cell(new_row, new_column, "b"))
                self.possible_moves.append(Cell(new_row, new_column, "r"))
            for k in [-1, 1]:
                new_column = self.current_cell.column + k
                new_row = self.current_cell.row + move
                if new_column >= 0 and new_column <= 7:
                    if board[new_row][new_column][1] is not None:
                        obstacle_color = board[new_row][new_column][1].IsWhite
                        if obstacle_color ^ self.IsWhite:
                            self.possible_moves.append(Cell(new_row, new_column, "q"))
                            self.possible_moves.append(Cell(new_row, new_column, "n"))
                            self.possible_moves.append(Cell(new_row, new_column, "b"))
                            self.possible_moves.append(Cell(new_row, new_column, "r"))
        else:
            if self.current_cell.row == 1 or self.current_cell.row == 6:
                for k in [1, 2]:
                    new_row = self.current_cell.row + k * move
                    new_column = self.current_cell.column + 0
                    if new_row >= 0 and new_row <= 7:
                        if board[new_row][new_column][1] is None: 
                            new_cell = Cell(new_row, new_column)
                            self.possible_moves.append(new_cell)
                        else:
                            break
                    else:
                        break
            else:
                new_row = self.current_cell.row + move
                new_column = self.current_cell.column + 0
                if new_row >= 0 and new_row <= 7:
                    if board[new_row][new_column][1] is None: 
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)
            new_row = self.current_cell.row + move
            new_column = self.current_cell.column - 1
            if new_column >= 0 and new_row <= 7 and new_row >= 0:
                if board[new_row][new_column][1] is not None:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)

            new_column = self.current_cell.column + 1
            if new_column <= 7 and new_row <= 7 and new_row >= 0:
                if board[new_row][new_column][1] is not None:
                    obstacle_color = board[new_row][new_column][1].IsWhite
                    if obstacle_color ^ self.IsWhite:
                        new_cell = Cell(new_row, new_column)
                        self.possible_moves.append(new_cell)

Implémentation de la classe Chessboard

Il faut maintenant implémenter une classe qui va permettre de représenter et manipuler un plateau d'échecs. Ce plateau sera un attribut de cette classe, et sera un tableau de 8 lignes et 8 colonnes. Chaque élément de ce tableau sera une liste contenant un objet de la classe Cell, correspondant à la case de l'échiquier, et une objet de la classe Piece, qui sera le contenu de cette case. Si la case est vide, il y aura un objet None.

Cliquer pour voir l'initialisation de la classe Chessboard

class Chessboard():
    def __init__(self):
        self.board = [ [None for j in range(8)] for i in range(8) ]
        for i in range(8):
            for j in range(8):
                self.board[i][j] = [Cell(i, j), None]

Récupérer une position quelconque

Implémentons à présent une méthode qui permettra de récupérer n'importe quelle position d'échecs. Pour cela, on va utiliser une notation appelée la Notation de Forsyth-Edwards. Cette notation est celle utilisée dans le monde des échecs pour représenter une position. Pour faire court, chaque rangée de l'échiquier est décrite de haut en bas, et chaque pièce est représentée par sa lettre. En lisant les lettres une par une, il n'est pas difficile de représenter la position dans notre code.

Cliquer pour voir le code de la méthode get_position_from_fen

def get_position_from_fen(self, fen_position):
        new_board = [ [None for j in range(8)] for i in range(8) ]
        for i in range(8):
            for j in range(8):
                new_board[i][j] = [Cell(i, j), None]
        row_list = []
        word = ''
        count = 0
        for char in fen_position:
            if char != "/" and char != ' ':
                word += char
            elif char == "/":
                row_list.append(word)
                word = ''
            elif char == ' ':
                row_list.append(word)
                if fen_position[count + 1] == 'w':
                    IsWhiteToPlay = True
                else:
                    IsWhiteToPlay = False
                break
            count += 1
        nb_row = 7
        for row in row_list:
            counter = 0
            for k in range(len(row)):
                if row[k].isdigit():
                    digit = int(row[k])
                    counter += digit
                else:
                    if row[k] == 'r' or row[k] == 'R':
                        new_board[nb_row][counter][1] = Rook(row[k].isupper(), Cell(nb_row, counter))
                    elif row[k] == 'n' or row[k] == 'N':
                        new_board[nb_row][counter][1] = Knight(row[k].isupper(), Cell(nb_row, counter))
                    elif row[k] == 'b' or row[k] == 'B':
                        new_board[nb_row][counter][1] = Bishop(row[k].isupper(), Cell(nb_row, counter))
                    elif row[k] == 'k' or row[k] == 'K':
                        new_board[nb_row][counter][1] = King(row[k].isupper(), Cell(nb_row, counter))
                    elif row[k] == 'q' or row[k] == 'Q':
                        new_board[nb_row][counter][1] = Queen(row[k].isupper(), Cell(nb_row, counter))
                    elif row[k] == 'p' or row[k] == 'P':
                        new_board[nb_row][counter][1] = Pawn(row[k].isupper(), Cell(nb_row, counter))
                    counter += 1
            nb_row -= 1
        return(new_board, IsWhiteToPlay)

Effectuer un mouvement sur l'échiquier

La méthode move prend en argument une case de départ et une case d'arrivée, et modifie l'échiquier en conséquence. Pour cela, on récupère la pièce sur la case de départ et on vérifie si elle peut aller dans sa case d'arrivée grâce à l'attribut possible_moves. Ensuite, on supprime la pièce de la case de départ et on la place sur la case d'arrivée. Enfin, on actualise les attributs possible_moves de chaque pièce en utilisant les méthodes get_possible_moves.

Cliquer pour voir les codes des méthodes move et update_possible_moves

def move(self, current_cell, final_cell):
    piece = self.board[current_cell.row][current_cell.column][1]
    if piece is None:
        print("Il n'y a pas de pièce dans la case indiquée")
        return()
    if (final_cell.row, final_cell.column) not in [(cell.row, cell.column) for cell in piece.possible_moves]:
        print("La pièce ne peut pas aller jusqu'à la case indiquée")
        return()
    self.board[current_cell.row][current_cell.column][1] = None
    if piece.letter == "" and (current_cell.row == 6 and piece.IsWhite):
        if final_cell.promote is None:
            print("Problème : pas de promotion")
            return()
        if final_cell.promote in ["Q", "q"]:
            self.board[final_cell.row][final_cell.column][1] = Queen(piece.IsWhite, final_cell)
        elif final_cell.promote in ["N", "n"]:
            self.board[final_cell.row][final_cell.column][1] = Knight(piece.IsWhite, final_cell)
        elif final_cell.promote in ["B", "b"]:
            self.board[final_cell.row][final_cell.column][1] = Bishop(piece.IsWhite, final_cell)
        elif final_cell.promote in ["R", "r"]:
            self.board[final_cell.row][final_cell.column][1] = Rook(piece.IsWhite, final_cell)

    elif piece.letter == "" and (current_cell.row == 1 and not piece.IsWhite):
        if final_cell.promote is None:
            print("Problème : pas de promotion", piece.letter, current_cell.name, final_cell.name, final_cell.promote)
            return()
        if final_cell.promote in ["Q", "q"]:
            self.board[final_cell.row][final_cell.column][1] = Queen(piece.IsWhite, final_cell)
        elif final_cell.promote in ["N", "n"]:
            self.board[final_cell.row][final_cell.column][1] = Knight(piece.IsWhite, final_cell)
        elif final_cell.promote in ["B", "b"]:
            self.board[final_cell.row][final_cell.column][1] = Bishop(piece.IsWhite, final_cell)
        elif final_cell.promote in ["R", "r"]:
            self.board[final_cell.row][final_cell.column][1] = Rook(piece.IsWhite, final_cell) 
    elif piece.letter in ["K", "k"]:
        self.board[final_cell.row][final_cell.column][1] = King(piece.IsWhite, final_cell)
    elif piece.letter in ["R", "r"]:
        self.board[final_cell.row][final_cell.column][1] = Rook(piece.IsWhite, final_cell)
    elif piece.letter in ["B", "b"]:
        self.board[final_cell.row][final_cell.column][1] = Bishop(piece.IsWhite, final_cell)
    elif piece.letter in ["N", "n"]:
        self.board[final_cell.row][final_cell.column][1] = Knight(piece.IsWhite, final_cell)
    elif piece.letter in ["Q", "q"]:
        self.board[final_cell.row][final_cell.column][1] = Queen(piece.IsWhite, final_cell)
    elif piece.letter in [""]:
        self.board[final_cell.row][final_cell.column][1] = Pawn(piece.IsWhite, final_cell)
    self.update_possible_moves()

def update_possible_moves(self):
    for row in self.board:
        for cell in row:
            if cell[1] is not None:
                cell[1].get_possible_moves(self.board)

Le cas de la promotion sera expliqué plus tard, car c'est un cas qui a été traité en dernier lors de ce sprint.

Déterminer si une pièce peut être capturée par ou pas

Cette méthode est très importante, car elle est utilisée dans beaucoup de méthodes ci-après. Pour cela, on récupère tout les mouvements possibles du plateau, et on vérifie si les coordonnées de la pièce en question est dans cette liste.

Cliquer pour voir le code de la méthode CanBeCaptured

def CanBeCaptured(self, piece):
    captured_by = []
    opponent_moves = [[cell[0], cell[1]] for cell in self.get_all_possible_moves2(not piece.IsWhite)]
    for k in range(len(opponent_moves)):
        for i in range(len(opponent_moves[k][1])):
            current_cell = opponent_moves[k][1][i]
            if piece.current_cell.name == current_cell.name:
                captured_by.append(opponent_moves[k][0])
                break
    if len(captured_by) == 0:
        return(False, captured_by)
    else:
        return(True, captured_by)

Récupérer tout les mouvements possibles d'une position

Il faut ensuite récupérer tout les coups possibles d'une position. Pour cela, on parcourt le plateau et quand on rencontre une pièce, on rajoute dans une liste la case de départ et les cases d'arrivée. Une chose importante à faire est de trier cette liste finale. On place en premier les mouvements qui résultent en un échec. Ceci est important car ces mouvements sont plus susceptibles de résoudre le problème, et cela va considérablement réduire le temps d'exécution de l'algorithme.

Cliquer pour voir le code de la méthode get_all_possible_moves

def get_all_possible_moves(self, IsWhiteToPlay):
    all_possible_moves = []
    initial_board = copy.deepcopy(self.board[:])
    for row in self.board:
        for cell in row:
            if cell[1] is not None:
                self.board = copy.deepcopy(initial_board[:])
                piece = copy.deepcopy(cell[1])
                if piece.IsWhite == IsWhiteToPlay:
                    current_moves = []
                    index = -1
                    # killer_moves = []
                    for move in piece.possible_moves:
                        piece_opponent = copy.deepcopy(self.board[move.row][move.column][1])
                        self.move(piece.current_cell, move)
                        if not self.IsCheck(IsWhiteToPlay):
                            if self.IsCheck(not IsWhiteToPlay):
                                current_moves = [move] + current_moves
                            elif piece_opponent != None and ((piece_opponent.IsWhite) ^ (piece.IsWhite)):
                                current_moves = [move] + current_moves
                            elif piece.letter not in [""] and index != -1:
                                current_moves = current_moves[:index] + [move] + current_moves[index:]
                            else:
                                current_moves.append(move)
                            self.board = copy.deepcopy(initial_board[:])
                            index += 1
                        else:
                            self.board = copy.deepcopy(initial_board[:])
                    # current_moves = killer_moves + current_moves
                    all_possible_moves.append([piece.current_cell, current_moves])
    return(all_possible_moves)

Notons que cette méthode gère les "clouages". Ce concept a lieu quand une pièce ne peut pas bouger car si elle bouge, son roi serait en échec.

Déterminer si la position est un échec et mat ou pas

Cette méthode a été la plus longue à implémenter. Ceci est dû au fait que qu'il y a plusieurs cas à vérifier. En effet, pour parer un échec, il y a plusieurs possibilités:

La méthode IsCheckmate gère ces cas un par un, et renvoie un booléen qui indique s'il y a uun échec et mat ou pas. Il est inutile d'expliquer en détail comment cette fonction gère ces cas, mais il est intéressant de noter que cette méthode utilise la plupart des méthodes ci-avant.

Cliquer pour voir le code de la méthode IsCheckmate

def IsCheckmate(self, IsWhiteToPlay):
    initial_board = copy.deepcopy(self.board)
    count = 0
    for row in self.board:
        for cell in row:
            if cell[1] is not None:
                piece2 = copy.deepcopy(cell[1])
                # print(piece2.letter, piece2.current_cell.name)
                if (piece2.value == 1000 or piece2.value == -1000) and (piece2.IsWhite == IsWhiteToPlay):
                    list_captured = copy.deepcopy(self.CanBeCaptured(piece2)[1])
                    if self.CanBeCaptured(piece2)[0]: #Vérifie si le roi est en échec
                        current_row = piece2.current_cell.row
                        current_column = piece2.current_cell.column
                        current_cell = Cell(current_row,current_column)
                        for move in piece2.possible_moves:
                            self.move(current_cell, move)
                            new_piece = self.board[move.row][move.column][1]
                            can_be_captured = self.CanBeCaptured(new_piece)[0]
                            # print(new_piece.current_cell.name, [cell.name for cell in self.CanBeCaptured(new_piece)[1]])
                            if can_be_captured:
                                count += 1
                            self.board = copy.deepcopy(initial_board)
                            self.update_possible_moves()
                        if count != len(piece2.possible_moves): #Vérifie si le roi peut s'échapper ou pas
                            return(False)
                        else: #Le roi ne peut pas s'échapper
                            if len(list_captured) == 1: #Une pièce met en échec
                                cell = list_captured[0]
                                piece_opponent = self.board[cell.row][cell.column][1]
                                (can_be_captured2, list_captured2) = self.CanBeCaptured(piece_opponent)
                                if can_be_captured2 and (len(list_captured2) > 1 or self.board[list_captured2[0].row][list_captured2[0].column][1].letter not in ["k", "K"]): #La pièce peut être prise par une pièce autre que le roi
                                    index = None
                                    for m in range(len(list_captured2)):
                                        if self.board[list_captured2[m].row][list_captured2[m].column][1].letter in ["K", "k"]:
                                            index = m
                                            break
                                    if index != None:
                                        list_captured2.pop(index)
                                    c = 0
                                    for cell_opponent in list_captured2:
                                        c += 1
                                        self.move(cell_opponent, piece_opponent.current_cell)
                                        if not self.IsCheck(IsWhiteToPlay): # Au moins une pièce peut prendre la pièce qui met en échec
                                            self.board = copy.deepcopy(initial_board)
                                            return(False)
                                        self.board = copy.deepcopy(initial_board)
                                    return(True)
                                else: #La pièce ne peut pas être prise par une pièce autre que le roi
                                    cells_inbetween = []
                                    piece_opponent = self.board[list_captured[0].row][list_captured[0].column][1]
                                    row_king = piece2.current_cell.row
                                    column_king = piece2.current_cell.column
                                    if piece_opponent.letter in ["R", "r", "q", "Q"]:
                                        if piece_opponent.current_cell.column == column_king and piece_opponent.current_cell.row > row_king: #la Tour est au-dessus du roi
                                            current_column = piece_opponent.current_cell.column
                                            current_row = piece_opponent.current_cell.row - 1
                                            while current_row != row_king and current_row >= 0:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row -= 1
                                        elif piece_opponent.current_cell.column == column_king and piece_opponent.current_cell.row < row_king: #la Tour est en dessous du roi
                                            current_column = piece_opponent.current_cell.column
                                            current_row = piece_opponent.current_cell.row + 1
                                            while current_row != row_king and current_row <= 7:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row += 1
                                        elif piece_opponent.current_cell.row == row_king and piece_opponent.current_cell.column > column_king: #la Tour est à droite du roi
                                            current_column = piece_opponent.current_cell.column - 1
                                            current_row = piece_opponent.current_cell.row
                                            while current_column != column_king and current_column >= 0:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_column -= 1
                                        elif piece_opponent.current_cell.row == row_king and piece_opponent.current_cell.column < column_king: #la Tour est à gauche du roi
                                            current_column = piece_opponent.current_cell.column + 1
                                            current_row = piece_opponent.current_cell.row
                                            while current_column != column_king and current_column <= 7:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_column += 1
                                    if piece_opponent.letter in ["B", "b", "Q", "q"]:
                                        if piece_opponent.current_cell.column > column_king and piece_opponent.current_cell.row > row_king: #le Fou est en haut à droite du roi
                                            current_column = piece_opponent.current_cell.column - 1
                                            current_row = piece_opponent.current_cell.row - 1
                                            while current_row != row_king and current_column >= 0 and current_row >= 0:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row -= 1
                                                current_column -= 1
                                        elif piece_opponent.current_cell.column < column_king and piece_opponent.current_cell.row < row_king: #le Fou est en bas à gauche du roi
                                            current_column = piece_opponent.current_cell.column + 1
                                            current_row = piece_opponent.current_cell.row + 1
                                            while current_row != row_king and current_row <= 7 and current_column <= 7:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row += 1
                                                current_column += 1
                                        elif piece_opponent.current_cell.column > column_king and piece_opponent.current_cell.row < row_king: #le Fou est en bas à droite du roi
                                            current_column = piece_opponent.current_cell.column - 1
                                            current_row = piece_opponent.current_cell.row + 1
                                            while current_row != row_king and current_row <= 7 and current_column >= 0:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row += 1
                                                current_column -= 1
                                        elif piece_opponent.current_cell.column < column_king and piece_opponent.current_cell.row > row_king: #le Fou est en haut à gauche du roi
                                            current_column = piece_opponent.current_cell.column + 1
                                            current_row = piece_opponent.current_cell.row - 1
                                            while current_row != row_king and current_row >= 0 and current_column <= 7:
                                                cells_inbetween.append(self.board[current_row][current_column][0])
                                                current_row -= 1
                                                current_column += 1
                                    if piece_opponent.letter not in ["R", "r", "B", "b", "Q", "q"]: #La pièce qui met en échec n'est ni un fou, ni une tour, ni une Dame
                                        return(True)
                                    cells_inbetween = [cell.name for cell in cells_inbetween]
                                    all_possible_moves = copy.deepcopy(self.get_all_possible_moves(IsWhiteToPlay))
                                    ending_cells = []
                                    for n in range(len(all_possible_moves)):
                                        current_cell = all_possible_moves[n][0]
                                        if self.board[current_cell.row][current_cell.column][1].letter not in ['k', 'K']:
                                            ending_cells += all_possible_moves[n][1]
                                    ending_cells = [cell.name for cell in ending_cells]
                                    for cell in ending_cells:
                                        if cell in cells_inbetween:
                                            return(False)
                                    return(True)
                            else: #2 pièces ou plus mettent en échec
                                return(True)
                    else:
                        return(False)

Fonction d'évaluation

Il faut maintenant une méthode qui évalue la position d'une position donnée. Nous allons faire très simple, étant donné que l'objectif est simplement de résoudre des échecs et mat en quelques coups: l'évaluation d'une position sera donc la somme des valeurs des pièces présentes dans le plateau. S'il y a un échec et mat, on renvoie un score de 1000000.

Cliquer pour voir le code de la méthode evaluate

def evaluation(self):
    eval = 0
    if self.IsCheckmate(True):
        return(-1000000)
    elif self.IsCheckmate(False):
        return(1000000)
    for row in self.board:
        for cell in row:
            if cell[1] is not None:
                eval += cell[1].value
    return(eval)

Notons que cette fonction d'évaluation est centrale dans les meilleurs robots d'échecs. Elle est ici loin d'être complète, mais comme on s'intéresse seulement à des échecs et mat, il est inutile de pousser plus loin cette méthode.

L'algorithme de résolution

Nous arrivons enfin à l'algorithme de résolution. Cet algorithme va utiliser toutes les méthodes précédemment présentées pour trouver un échec et mat en quelques coups. Cette méthode prend en entrée une position en notation FEN, un nombre de coups à explorer (qui est la profondeur de calcul) et renvoie une liste de coups qui correspond à l'échec et mat trouvé. Cet algorithme, bien connu dans la théorie des jeux, s'appelle l'algorithme Minimax. Il est inutiles d'expliquer comment fonctionne cet algorithme en détail car beaucoup de documentation est disponible sur ce sujet, mais le principe général repose sur le fait que l'algorithme trouve les mouvements qui permet de minimiser le score pour un joueur et maximiser ce même score pour l'autre joueur. Ce score est trouvé grâce à la méthode evaluate. Notons également que j'ai également implémenter l'élagage "Alpha-Beta" qui permet de réduire considérablement le temps d'exécution, en coupant les branches de l'arbre qui sont inutiles à explorer.

Cliquer pour voir le code de la méthode solve

def solve(self, fen_position, IsWhiteToPlay, depth):
    self.c = 0 
    self.tot = 0
    self.best_moves = [None for i in range(depth)]
    variations = []
    (self.board, IsWhiteToPlay) = self.get_position_from_fen(fen_position)
    self.update_possible_moves()
    time_begin = time.time()
    result = self.solve_assist(IsWhiteToPlay, -9999999999, 9999999999, depth)
    time_final = time.time()
    tot_time = round(time_final - time_begin, 1)
    tot_min = int((tot_time // 60) % 60)
    tot_hours = int(tot_time // 3600)
    tot_sec = round(tot_time % 60,1)
    print(f"Durée de l'exécution: {tot_hours} heures, {tot_min} minutes et {tot_sec} secondes")
    return(result)

def solve_assist(self, IsWhiteToPlay, alpha, beta, depth):
    # print(depth, self.IsCheckmate(True))
    self.c += 1
    print(f"{self.c}/{self.tot}", self.evaluation())
    if depth == 0 or self.IsCheckmate(True) or self.IsCheckmate(False):
        return(self.evaluation(), self.best_moves)
    if IsWhiteToPlay:
        best = -99999999999
        all_possible_moves = self.get_all_possible_moves(IsWhiteToPlay)
        flag = False
        IsStalemate = True
        for k in range(len(all_possible_moves)):
            self.tot += len(all_possible_moves[k][1])
        for move in all_possible_moves:
            for i in range(len(move[1])):
                initial_board = copy.deepcopy(self.board[:])
                initial_best_moves = copy.deepcopy(self.best_moves[:])
                self.move(move[0], move[1][i])
                IsStalemate = False
                val = self.solve_assist(not IsWhiteToPlay, alpha, beta, depth - 1)[0]
                self.board = copy.deepcopy(initial_board[:])
                if val > best:
                    best = val
                    next_move = ""
                    if move[1][i].promote != None:
                        if self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + "x" + f"{move[1][i].name}" + "=" + f"{move[1][i].promote}"
                        else:
                            next_move = f"{move[1][i].name}" + "=" + f"{move[1][i].promote}"
                    else:
                        if self.board[move[0].row][move[0].column][1].letter == "" and self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + next_move
                        if self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{self.board[move[0].row][move[0].column][1].letter}" + "x" + f"{move[1][i].name}"
                        else:
                            next_move = f"{self.board[move[0].row][move[0].column][1].letter}" + f"{move[1][i].name}"
                        if self.board[move[0].row][move[0].column][1].letter == "" and self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + next_move
                    self.move(move[0], move[1][i])
                    if self.IsCheckmate(not IsWhiteToPlay):
                        next_move = next_move + "#"
                    elif self.IsCheck(not IsWhiteToPlay):
                        next_move = next_move + "+"
                    self.board = copy.deepcopy(initial_board[:])
                    self.best_moves[-depth] = next_move
                else:
                    self.best_moves = copy.deepcopy(initial_best_moves)
                alpha = max(alpha, val)
                if beta <= alpha:
                    flag = True
                    break
            if flag:
                break
        if IsStalemate:
            return(0, self.best_moves)
    else:
        best = 9999999999
        all_possible_moves = self.get_all_possible_moves(IsWhiteToPlay)
        flag = False
        IsStalemate = True
        for k in range(len(all_possible_moves)):
            self.tot += len(all_possible_moves[k][1])
        for move in all_possible_moves:
            for i in range(len(move[1])):
                initial_board = copy.deepcopy(self.board[:])
                initial_best_moves = copy.deepcopy(self.best_moves[:])
                self.move(move[0], move[1][i])
                IsStalemate = False
                val = self.solve_assist(not IsWhiteToPlay, alpha, beta, depth - 1)[0]
                self.board = copy.deepcopy(initial_board[:])
                if val < best:
                    best = val
                    next_move = ""
                    if move[1][i].promote != None:
                        if self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + "x" + f"{move[1][i].name}" + "=" + f"{move[1][i].promote}"
                        else:
                            next_move = f"{move[1][i].name}" + "=" + f"{move[1][i].promote}"
                    else:
                        if self.board[move[0].row][move[0].column][1].letter == "" and self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + next_move
                        if self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{self.board[move[0].row][move[0].column][1].letter}" + "x" + f"{move[1][i].name}"
                        else:
                            next_move = f"{self.board[move[0].row][move[0].column][1].letter}" + f"{move[1][i].name}"
                        if self.board[move[0].row][move[0].column][1].letter == "" and self.board[move[1][i].row][move[1][i].column][1] != None:
                            next_move = f"{chr(move[0].column + 97)}" + next_move
                    self.move(move[0], move[1][i])
                    if self.IsCheckmate(not IsWhiteToPlay):
                        next_move = next_move + "#"
                    elif self.IsCheck(not IsWhiteToPlay):
                        next_move = next_move + "+"
                    self.board = copy.deepcopy(initial_board[:])
                    self.best_moves[-depth] = next_move
                else:
                    self.best_moves = copy.deepcopy(initial_best_moves)
                beta = min(beta, val)
                if beta <= alpha:
                    flag = True
                    break
            if flag:
                break
        if IsStalemate:
            return(0, self.best_moves)
    return(best, self.best_moves)

Démonstration du code

Pour vérifier si ce code fonctionne correctement, j'ai cherché des problèmes de mat en 2 et de mat en 3 sur Internet, et j'ai fait tourner l'algorithme sur ces positions. Voici un exemple sur cette position:

Image1

L'exécution de l'algorithme sur cette position donne la ligne suivante: ['Qd5+', 'nxd5', 'Rxc6#'] en 55.5 secondes. D'après l'ordinateur le plus puissant du monde "Stockfish", c'est bien la solution! (Notons le brillant sacrifice de Dame). Plusieurs lignes sont possibles, celle qui est donnée n'en est qu'un exemple. Voici un autre exemple d'un mat en 3 sur la position suivante:

Image2

L'algorithme fournit la ligne suivante: ['Ke2', 'nc1+', 'Ke3', 'na2', 'd4#'] en 24 minutes. Encore une fois, c'est la bonne réponse. On remarque cependant que la résolution a été bien plus longue. Ceci est dû au fait qu'il y a beaucoup plus de pièces sur l'échiquier, et le nombre de mouvements explorés est également beaucoup plus élevé que le cas précédent. Pour finir, voici l'exemple de résolution d'un mat en 4 sur la position suivante:

Image3

L'algorithme résout ce problème en 12 minutes et 57 secondes: ['Bh5', 'kxh5', 'Kg7', 'h6', 'Kf6', 'kh4', 'Kg6#']. Notons que l'exécution est gérable grâce au fait qu'il y ait peu de pièces sur l'échiquier, et que les coups des Noirs soient forcés après le mouvement Fou H5. En réalité, j'ai essayé de faire tourner l'algorithme sur d'autres problèmes de Mat en 4 pendant plusieurs heures, sans avoir de résultat. Le mat en 4 est donc la limite, ce qui traduit le manque d'optimisation de l'algorithme.

Gestion de la promotion des pions

De nombreux problèmes repose sur une règle des échecs, nommée la "promotion". Cette règle stipule qu'un pion a le droit de se transformer en n'importe quelle pièce lorsqu'elle arrive au bout du plateau. Pour implémenter cela dans le code, on crée un attribut promote dans la classe Cell. Cet attribut est par défaut None. Si un pion se trouve sur les rangées d'indice 1 ou 6, on change cet attribue en une pièce (Tour, Dame, Fou ou Cavalier). Ensuite, dans la méthode move, lorsque l'attribut promote n'est pas None on avance le pion et on le change en la pièce correspondante.

Pour tester cette implémentation, on fait tourner l'algorithme sur le problème suivant, qui est un mat en 2 qui nécessite deux promotions (Trait aux noirs):

Image4

La ligne donnée est: ['e1=n+', 'Kh2', 'f1=n#'], qui est bien la solution. L'algorithme a bien compris qu'il fallait faire deux promotions en cavalier.

Bilan du premier sprint et prévision du deuxième sprint

J'ai réussi à développer tout les objectifs du backlog présenté plus haut. J'ai parfois sous estimé certaines complexités, mais finalement tout s'est bien passé. L'algorithme que j'ai créé n'est pas du tout optimal, et manque cruellement de techniques pour rendre plus rapide l'exécution. Les meilleurs robots d'échecs explorent une profondeur de plus de 15 en seulement quelques secondes, tandis que mon programme met plusieurs heures pour une profondeur de 7... Cependant, le programme marche parfaitement pour des profondeurs faibles, ce qui me contente pour l'instant.

Il faut à présent décrire le backlog du deuxième sprint, qui aura pour objectif d'utiliser Pygame pour implémenter le jeu d'échecs pour qu'on puisse y jouer. Voici ce backlog, avec les complexités pour chaque fonctionnalités:

Implémentation du jeu d'échecs avec Pygame

Nous allons à présent implémenter le jeu d'échecs en utilisant le module Pygame (ce module est expliqué dans mon MON2.1). On créé donc une nouvelle classe Chess_game, qui hérite de la classe précédemment créée Chessboard. Nous allons en effet utiliser toutes les méthodes déjà créées pour implémenter l'interface du jeu d'échecs, comme par exemple les méthodes pour manipuler la représentation du plateau d'échecs, vérifier s'il y a un échec etc...

Initialisation de la classe

Lors de l'initialisation de la classe Chess_game, nous allons introduire plusieurs variables qui nous seront utiles:

Cliquer pour voir le code de l'initialisation de la classe Chess_game

def __init__(self):
    pygame.init()
    super().__init__()
    self.set_starting_board()
    self.screen = pygame.display.set_mode((900, 550))
    self.chessboard_surface = pygame.image.load('chessboard.png')
    self.size = 480
    self.cell_size = self.size // 8
    self.chessboard_surface = pygame.transform.scale(self.chessboard_surface, (self.size, self.size))
    self.delta_x = 150
    self.delta_y = 30
    self.background_color = "White"
    self.screen.fill(self.background_color)
    self.font = pygame.font.Font(None, 30)
    pygame.display.set_caption("Jeu d'échecs")
    self.clock = pygame.time.Clock()
    self.black_rook_surface = pygame.image.load('Chess_pieces_png/black_rook.png')
    self.black_rook_surface = pygame.transform.scale(self.black_rook_surface, (self.cell_size, self.cell_size))
    self.black_knight_surface = pygame.image.load('Chess_pieces_png/black_knight.png')
    self.black_knight_surface = pygame.transform.scale(self.black_knight_surface, (self.cell_size, self.cell_size))
    self.black_bishop_surface = pygame.image.load('Chess_pieces_png/black_bishop.png')
    self.black_bishop_surface = pygame.transform.scale(self.black_bishop_surface, (self.cell_size, self.cell_size))
    self.black_king_surface = pygame.image.load('Chess_pieces_png/black_king.png')
    self.black_king_surface = pygame.transform.scale(self.black_king_surface, (self.cell_size, self.cell_size))
    self.black_queen_surface = pygame.image.load('Chess_pieces_png/black_queen.png')
    self.black_queen_surface = pygame.transform.scale(self.black_queen_surface, (self.cell_size, self.cell_size))
    self.black_pawn_surface = pygame.image.load('Chess_pieces_png/black_pawn.png')
    self.black_pawn_surface = pygame.transform.scale(self.black_pawn_surface, (self.cell_size, self.cell_size))

    self.white_rook_surface = pygame.image.load('Chess_pieces_png/white_rook.png')
    self.white_rook_surface = pygame.transform.scale(self.white_rook_surface, (self.cell_size, self.cell_size))
    self.white_knight_surface = pygame.image.load('Chess_pieces_png/white_knight.png')
    self.white_knight_surface = pygame.transform.scale(self.white_knight_surface, (self.cell_size, self.cell_size))
    self.white_bishop_surface = pygame.image.load('Chess_pieces_png/white_bishop.png')
    self.white_bishop_surface = pygame.transform.scale(self.white_bishop_surface, (self.cell_size, self.cell_size))
    self.white_king_surface = pygame.image.load('Chess_pieces_png/white_king.png')
    self.white_king_surface = pygame.transform.scale(self.white_king_surface, (self.cell_size, self.cell_size))
    self.white_queen_surface = pygame.image.load('Chess_pieces_png/white_queen.png')
    self.white_queen_surface = pygame.transform.scale(self.white_queen_surface, (self.cell_size, self.cell_size))
    self.white_pawn_surface = pygame.image.load('Chess_pieces_png/white_pawn.png')
    self.white_pawn_surface = pygame.transform.scale(self.white_pawn_surface, (self.cell_size, self.cell_size))
    
    self.state = "piece_not_selected"
    self.coord_piece_selected = None
    self.cells_selected = []
    self.IsWhiteToPlay = True

    self.scrolled = False
    self.move_list = []
    self.move_count = 1
    self.scroll_count = 0
    self.font_moves_size = 27
    self.limit = 18
    self.font_moves = pygame.font.Font(None, self.font_moves_size)

    self.en_passant_cell = None

    self.pieces_taken_by_white = []
    self.pieces_taken_by_black = []
    self.pieces_taken_size = 25
    self.black_rook_surface2 = pygame.image.load('Chess_pieces_png/black_rook.png')
    self.black_rook_surface2 = pygame.transform.scale(self.black_rook_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.black_knight_surface2 = pygame.image.load('Chess_pieces_png/black_knight.png')
    self.black_knight_surface2 = pygame.transform.scale(self.black_knight_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.black_bishop_surface2 = pygame.image.load('Chess_pieces_png/black_bishop.png')
    self.black_bishop_surface2  = pygame.transform.scale(self.black_bishop_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.black_king_surface2 = pygame.image.load('Chess_pieces_png/black_king.png')
    self.black_king_surface2 = pygame.transform.scale(self.black_king_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.black_queen_surface2 = pygame.image.load('Chess_pieces_png/black_queen.png')
    self.black_queen_surface2 = pygame.transform.scale(self.black_queen_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.black_pawn_surface2 = pygame.image.load('Chess_pieces_png/black_pawn.png')
    self.black_pawn_surface2 = pygame.transform.scale(self.black_pawn_surface2, (self.pieces_taken_size, self.pieces_taken_size))

    self.white_rook_surface2 = pygame.image.load('Chess_pieces_png/white_rook.png')
    self.white_rook_surface2 = pygame.transform.scale(self.white_rook_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.white_knight_surface2 = pygame.image.load('Chess_pieces_png/white_knight.png')
    self.white_knight_surface2 = pygame.transform.scale(self.white_knight_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.white_bishop_surface2 = pygame.image.load('Chess_pieces_png/white_bishop.png')
    self.white_bishop_surface2 = pygame.transform.scale(self.white_bishop_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.white_king_surface2 = pygame.image.load('Chess_pieces_png/white_king.png')
    self.white_king_surface2 = pygame.transform.scale(self.white_king_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.white_queen_surface2 = pygame.image.load('Chess_pieces_png/white_queen.png')
    self.white_queen_surface2 = pygame.transform.scale(self.white_queen_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.white_pawn_surface2 = pygame.image.load('Chess_pieces_png/white_pawn.png')
    self.white_pawn_surface2 = pygame.transform.scale(self.white_pawn_surface2, (self.pieces_taken_size, self.pieces_taken_size))
    self.font_pieces_taken = pygame.font.Font(None, 25)


    self.move_sound = pygame.mixer.Sound('Sounds/move.ogg')
    self.capture_sound = pygame.mixer.Sound('Sounds/capture.ogg')
    self.check_sound = pygame.mixer.Sound('Sounds/check.ogg')
    self.castle_sound = pygame.mixer.Sound('Sounds/castle.ogg')

Gestion de l'affichage

Il faut tout d'abord implémenter des méthodes pour afficher les différents éléments sur l'écran. Ces éléments sont les suivants:

Certaines de ces méthodes d'affichage utilisent une autre méthode draw_rect qui permettent de colorer une case d'une certaine couleur.

Cliquer pour voir les méthodes dédiées à l'affichage

def display_position(self):
    if self.state == "piece_selected":
        nb_row, nb_column = self.coord_piece_selected
        if self.cells_selected[1] != []:
            self.draw_rect(nb_row, nb_column, "steelblue4")
    if self.cells_selected != []:
        for cell in self.cells_selected[1]:
            self.draw_rect(cell.row, cell.column, "lightblue3")
    if self.IsCheck(self.IsWhiteToPlay):
        self.draw_check()
    for row in self.board:
        for cell in row:
            if cell[1] is not None:
                piece = cell[1]
                if piece.IsWhite:
                    coord_x, coord_y = self.get_coordinates(piece.current_cell.row, piece.current_cell.column)
                    if piece.letter == "R":
                        white_rook_rect = self.white_rook_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_rook_surface, white_rook_rect)
                    elif piece.letter == "N":
                        white_knight_rect = self.white_knight_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_knight_surface, white_knight_rect)
                    elif piece.letter == "B":
                        white_bishop_rect = self.white_bishop_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_bishop_surface, white_bishop_rect)
                    elif piece.letter == "Q":
                        white_queen_rect = self.white_queen_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_queen_surface, white_queen_rect)
                    elif piece.letter == "K":
                        white_king_rect = self.white_king_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_king_surface, white_king_rect)
                    elif piece.letter == "":
                        white_pawn_rect = self.white_pawn_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.white_pawn_surface, white_pawn_rect)
                else:
                    coord_x, coord_y = self.get_coordinates(piece.current_cell.row, piece.current_cell.column)
                    if piece.letter == "r":
                        black_rook_rect = self.black_rook_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_rook_surface, black_rook_rect)
                    elif piece.letter == "n":
                        black_knight_rect = self.black_knight_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_knight_surface, black_knight_rect)
                    elif piece.letter == "b":
                        black_bishop_rect = self.black_bishop_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_bishop_surface, black_bishop_rect)
                    elif piece.letter == "q":
                        black_queen_rect = self.black_queen_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_queen_surface, black_queen_rect)
                    elif piece.letter == "k":
                        black_king_rect = self.black_king_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_king_surface, black_king_rect)
                    elif piece.letter == "":
                        black_pawn_rect = self.black_pawn_surface.get_rect(center = (coord_x, coord_y))
                        self.screen.blit(self.black_pawn_surface, black_pawn_rect)
def display_pieces_taken(self):
    white_rect = pygame.Rect(0, 0, self.delta_x, self.delta_y + self.size)
    pygame.draw.rect(self.screen, self.background_color, white_rect)
    score = self.evaluation()

    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 1.5*self.cell_size
    black_pawn_rect = self.black_pawn_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.black_pawn_surface2, black_pawn_rect)
    text_black_pawn_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_white.count('')}", False, "Black")
    text_rect = text_black_pawn_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_black_pawn_taken, text_rect)

    coord_x += 55
    black_knight_rect = self.black_knight_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.black_knight_surface2, black_knight_rect)
    text_black_knight_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_white.count('n')}", False, "Black")
    text_rect = text_black_knight_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_black_knight_taken, text_rect)

    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 1*self.cell_size
    black_bishop_rect = self.black_bishop_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.black_bishop_surface2, black_bishop_rect)
    text_black_bishop_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_white.count('b')}", False, "Black")
    text_rect = text_black_bishop_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_black_bishop_taken, text_rect)

    coord_x += 55
    black_rook_rect = self.black_rook_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.black_rook_surface2, black_rook_rect)
    text_black_rook_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_white.count('r')}", False, "Black")
    text_rect = text_black_rook_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_black_rook_taken, text_rect)

    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 0.5*self.cell_size
    black_queen_rect = self.black_queen_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.black_queen_surface2, black_queen_rect)
    text_black_queen_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_white.count('q')}", False, "Black")
    text_rect = text_black_queen_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_black_queen_taken, text_rect)
    
    if score == 1000000 or score == -1000000:
        coord_x += 45
        text_score = self.font_pieces_taken.render("#", False, "Black")
        text_rect = text_score.get_rect(midtop = (coord_x + 23, coord_y + 5))
        self.screen.blit(text_score, text_rect)
    elif score > 0:
        coord_x += 45
        text_score = self.font_pieces_taken.render(f"+{score}", False, "Black")
        text_rect = text_score.get_rect(midtop = (coord_x + 23, coord_y + 5))
        self.screen.blit(text_score, text_rect)


    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 8*self.cell_size
    white_pawn_rect = self.white_pawn_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.white_pawn_surface2, white_pawn_rect)
    text_white_pawn_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_black.count('')}", False, "Black")
    text_rect = text_white_pawn_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_white_pawn_taken, text_rect)

    coord_x += 55
    white_knight_rect = self.white_knight_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.white_knight_surface2, white_knight_rect)
    text_white_knight_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_black.count('N')}", False, "Black")
    text_rect = text_white_knight_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_white_knight_taken, text_rect)

    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 7.5*self.cell_size
    white_bishop_rect = self.white_bishop_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.white_bishop_surface2, white_bishop_rect)
    text_white_bishop_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_black.count('B')}", False, "Black")
    text_rect = text_white_bishop_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_white_bishop_taken, text_rect)

    coord_x += 55
    white_rook_rect = self.white_rook_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.white_rook_surface2, white_rook_rect)
    text_white_rook_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_black.count('R')}", False, "Black")
    text_rect = text_white_rook_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_white_rook_taken, text_rect)

    coord_x = self.delta_x - 100
    coord_y = self.delta_y + self.size - 7*self.cell_size
    white_queen_rect = self.white_queen_surface2.get_rect(midtop = (coord_x, coord_y))
    self.screen.blit(self.white_queen_surface2, white_queen_rect)
    text_white_queen_taken = self.font_pieces_taken.render(f"x{self.pieces_taken_by_black.count('Q')}", False, "Black")
    text_rect = text_white_queen_taken.get_rect(midtop = (coord_x + 23, coord_y + 5))
    self.screen.blit(text_white_queen_taken, text_rect)


    if score == 1000000 or score == -1000000:
        coord_x += 45
        text_score = self.font_pieces_taken.render("#", False, "Black")
        text_rect = text_score.get_rect(midtop = (coord_x + 23, coord_y + 5))
        self.screen.blit(text_score, text_rect)
    elif score < 0:
        coord_x += 45
        text_score = self.font_pieces_taken.render(f"+{-score}", False, "Black")
        text_rect = text_score.get_rect(midtop = (coord_x + 23, coord_y + 5))
        self.screen.blit(text_score, text_rect)

def display_restart_button(self):
    font_restart = pygame.font.Font(None, 27)
    coord_x = self.delta_x - 75
    coord_y = self.delta_y + self.size//2 - 20
    text_restart = font_restart.render("Recommencer", False, "Black")
    text_rect = text_restart.get_rect(center = (coord_x, coord_y))
    rect_restart = pygame.Rect(self.delta_x - 143, coord_y - 15, 135, 30)
    pygame.draw.rect(self.screen, "Gray", rect_restart)
    self.screen.blit(text_restart, text_rect)

 def display_who_to_play(self):
    self.remove_text()
    if self.IsWhiteToPlay:
        text_white_to_play = self.font.render("Au tour des Blancs", False, "Black")
        text_rect = text_white_to_play.get_rect(center = (self.size//2 + self.delta_x, self.delta_y//2))
        self.screen.blit(text_white_to_play, text_rect)
    else:
        text_black_to_play = self.font.render("Au tour des Noirs", False, "Black")
        text_rect = text_black_to_play.get_rect(center = (self.size//2 + self.delta_x, self.delta_y//2))
        self.screen.blit(text_black_to_play, text_rect)

def display_winner(self, is_white_winner):
    self.remove_text()
    if is_white_winner:
        text_white_winner = self.font.render("Les Blancs ont gagné !", False, "Black")
        text_rect = text_white_winner.get_rect(center = (self.size//2 + self.delta_x, self.delta_y//2))
        self.screen.blit(text_white_winner, text_rect)
    if not is_white_winner:
        text_black_winner = self.font.render("Les Noirs ont gagné !", False, "Black")
        text_rect = text_black_winner.get_rect(center = (self.size//2 + self.delta_x, self.delta_y//2))
        self.screen.blit(text_black_winner, text_rect)

def display_moves(self):
    self.remove_moves()
    delta_move_x = 100
    delta_move_y = 10
    if len(self.move_list) > self.limit * 2:
        max_scroll_count = math.ceil(len(self.move_list)/2) * 2 - self.limit * 2
        if (-1 * self.scroll_count * 2) == max_scroll_count or not self.scrolled:
            list_move_to_display = self.move_list[max_scroll_count:]
            if len(list_move_to_display) % 2 == 1:
                list_move_to_display.append(["", ""])
        else:
            index_start = - self.scroll_count * 2
            index_end = index_start + self.limit * 2
            list_move_to_display = self.move_list[index_start:index_end]
    else:
        list_move_to_display = self.move_list[:]
        self.scroll_count = 0
    for k in range(len(list_move_to_display)):
        coord_y = self.delta_y + (k // 2) * self.font_moves_size + delta_move_y
        if k % 2 == 0:
            coord_x = self.delta_x + self.size + delta_move_x
            text_move = self.font_moves.render(list_move_to_display[k][1], False, "Black")
            text_rect = text_move.get_rect(center = (coord_x, coord_y))
            self.screen.blit(text_move, text_rect)

            coord_x2 = self.delta_x + self.size + delta_move_x - 50
            text_move2 = self.font_moves.render(str(list_move_to_display[k][0]) + ".", False, "Black")
            text_rect2 = text_move2.get_rect(center = (coord_x2, coord_y))
            self.screen.blit(text_move2, text_rect2)
        if k % 2 == 1:
            coord_x = self.delta_x + self.size + delta_move_x + 70
            text_move = self.font_moves.render(list_move_to_display[k][1], False, "Black")
            text_rect = text_move.get_rect(center = (coord_x, coord_y))
            self.screen.blit(text_move, text_rect)

Affichage des mouvements possibles

Il faut à présent créer une méthode qui permet d'afficher les cases où la pièce sélectionnée peut aller. Il faut d'abord déterminer quelle case a été sélectionnée par l'utilisateur. Pour cela, on utilise la méthode get_row_column qui renvoie le numéro de ligne et colonne de la case sélectionnée par le joueur.

Cliquer pour voir la méthode get_row_column

def get_row_column(self):
    pos = pygame.mouse.get_pos()
    pos_x = pos[0] - self.delta_x
    pos_y = pos[1] - self.delta_y
    if (pos_x in range(0, self.size + 1)) and (pos_y in range(0, self.size + 1)):
        nb_row = 7 - (pos_y // self.cell_size)
        nb_column = pos_x // self.cell_size
        return(nb_row, nb_column)
    else:
        return(None)

Ensuite, il faut récupérer l'attribut possible_moves qui est a été défini plus tôt, et qui contient toutes les cases possibles sur lesquelles chaque pièce peut aller. On peut ainsi récupérer les numéros de lignes et colonnes de chaque cases, et ainsi les colorer de la bonne couleur. Notons que c'est dans cette méthode que l'on gère la prise en passant.

Cliquer pour voir le code de la méthode update_cells_selected

def update_cells_selected(self, piece):
    valid_moves = []
    current_cell = copy.deepcopy(piece.current_cell)
    initial_board = copy.deepcopy(self.board[:])
    if self.en_passant_cell != None and piece.letter == "":
        adjacent_column1 = self.en_passant_cell.column - 1
        adjacent_column2 = self.en_passant_cell.column + 1
        if piece.current_cell.column in [adjacent_column1, adjacent_column2]:
            if piece.current_cell.row + 1 == self.en_passant_cell.row and piece.IsWhite:
                piece.en_passant = self.en_passant_cell
                piece.possible_moves.append(self.en_passant_cell)
                valid_moves.append(self.en_passant_cell)
            elif piece.current_cell.row - 1 == self.en_passant_cell.row and not piece.IsWhite:
                piece.en_passant = self.en_passant_cell
                piece.possible_moves.append(self.en_passant_cell)
                valid_moves.append(self.en_passant_cell)
    for cell in piece.possible_moves:
        if cell.short_castle:
            if self.IsWhiteToPlay:
                row = 0
            else:
                row = 7
            if not self.IsCheck(self.IsWhiteToPlay):
                self.move(current_cell, Cell(row, 5))
                if not self.IsCheck(self.IsWhiteToPlay):
                    self.board = copy.deepcopy(initial_board)
                    self.move(current_cell, Cell(row, 6))
                    if not self.IsCheck(self.IsWhiteToPlay):
                        valid_moves.append(copy.deepcopy(cell))
                        self.board = copy.deepcopy(initial_board)
                    else:
                        self.board = copy.deepcopy(initial_board)
                else:
                    self.board = copy.deepcopy(initial_board)
        elif cell.long_castle:
            if self.IsWhiteToPlay:
                row = 0
            else:
                row = 7
            if not self.IsCheck(self.IsWhiteToPlay):
                self.move(current_cell, Cell(row, 2))
                if not self.IsCheck(self.IsWhiteToPlay):
                    self.board = copy.deepcopy(initial_board)
                    self.move(current_cell, Cell(row, 3))
                    if not self.IsCheck(self.IsWhiteToPlay):
                        valid_moves.append(copy.deepcopy(cell))
                        self.board = copy.deepcopy(initial_board)
                    else:
                        self.board = copy.deepcopy(initial_board)
                else:
                    self.board = copy.deepcopy(initial_board)
        else:
            self.move(current_cell, cell)
            if not self.IsCheck(self.IsWhiteToPlay):
                valid_moves.append(copy.deepcopy(cell))
            self.board = copy.deepcopy(initial_board)
    self.cells_selected = [piece.current_cell, copy.deepcopy(valid_moves)]

Test du programme

Voici quelques captures d'écran de ce que le programme produit:

Image partie normale Image d'une partie en cours, avec une pièce sélectionnée

Image échec Image d'une situation d'échec par les Blancs

Image échec et mat Image d'une situation de fin de partie (échec et mat)

Bilan du deuxième sprint

Tout les éléments du backlog ont été réalisés, excepté la fonctionnalité du retournement de plateau. En effet, j'ai largement sous-estimé la difficulté de cette fonctionnalité, car elle implique d'inverser toutes les coordonnées présentes dans le code et cela prendrait un temps considérable.