[Python] How to implement drag and drop to basic pygame chess engine

Discussão em 'Python' iniciado por Stack, Abril 22, 2025.

  1. Stack

    Stack Membro Participativo

    I'm building a chess engine using pygame as a personal project. I was following a tutorial but I want to go further.

    For the moment I have the chessboard, the pieces in their initial position and the possibility to undo the last move (key z). Also, all pieces can be moved by clicking on them and then clicking on the arrival square. Without any verification: you can eat your own king with a pawn on the 1st turn, and ignore the alternation of players. The only condition for ending the game is the event that closes its window.

    I'd just like to make it possible to drag&drop pieces, but I can't do it. I haven't found a tutorial that shows a practical example. It's always on a Rect object in a minimalist main.py with no design constraints.

    In my code, the management of player actions and the display are well separated. Could you please help me find a way to implement drag&drop that respects this as much as possible? Or explain why this isn't possible.

    thanks for reading

    ChessMain_3.py

    """
    This is our main driver file. It will be responsible for handling user input and displaying the current GameState object.
    """

    import pygame as p
    import ChessEngine_3

    WIDTH = HEIGHT = 512 #400 is another option
    DIMENSION = 8 #dimension of a chess board is 8x8
    SQ_SIZE = HEIGHT // DIMENSION
    MAX_FPS = 15 #for animations later on
    IMAGES = {} #dictionnary of images

    """
    Initialize a global dictionnary of images. It will be called exactly once in the main
    """
    def loadImage():
    pieces = ['bP', 'bR', 'bN', 'bB', 'bQ', 'bK', 'wP', 'wR', 'wN', 'wB', 'wQ', 'wK']
    for piece in pieces:
    unscaledImage = p.image.load('images/' + piece + '.png')
    IMAGES[piece] = p.transform.scale(unscaledImage, (SQ_SIZE, SQ_SIZE))
    #note: now can access an image by saying "IMAGES['bP']"


    """
    The main driver for our code. This will handle user input and update graphics
    """
    def main():
    p.init()
    screen = p.display.set_mode((WIDTH, HEIGHT))
    clock = p.time.Clock()
    screen.fill(p.Color('white'))

    gs = ChessEngine_3.GameState()

    loadImage() #only do this once before the while loop

    running = True
    sqSelected = () #no square is selected initially, keep track of the last click of the user (tuple: (row, col))
    playerClicks = [] #keep track of player clicks (two tuples: [(6, 4), (4, 4)] )
    while running:
    for e in p.event.get():
    if e.type == p.QUIT:
    running = False
    #mouse handler
    elif e.type == p.MOUSEBUTTONDOWN:
    location = p.mouse.get_pos() #(x, y) location of the mouse
    #so make sure to take it in account when adding a side panel
    col = location[0] // SQ_SIZE
    row = location[1] // SQ_SIZE
    if sqSelected == (row, col): #the user clicked the same square twice
    sqSelected = () #deselect
    playerClicks = [] #clear the player clicks
    else:
    sqSelected = (row, col)
    playerClicks.append(sqSelected) #append for both 1st and 2nd clicks
    if len(playerClicks) == 2: #after 2nd click
    move = ChessEngine_3.Move(playerClicks[0], playerClicks[1], gs.board)
    print(move.getChessNotation())
    gs.makeMove(move)
    sqSelected = () #reset user clicks
    playerClicks = []
    #key handler
    elif e.type == p.KEYDOWN: #user wants to undo last move
    if e.key == p.K_z: #undo when z is pressed
    gs.undoMove()
    sqSelected = () #reset user clicks
    playerClicks = []

    drawGameState(screen, gs)
    clock.tick(MAX_FPS)
    p.display.flip()

    """
    Responsible for all the graphics within the current game state.
    """
    def drawGameState(screen, gs):
    drawBoard(screen) #draw squares on the board
    #add in piece highlighting or move suggestions (later)
    drawPiece(screen, gs.board) #draw pieces on top of those squares

    """
    Draw the squares on the board.
    """
    def drawBoard(screen):
    # colors = [p.Color('white'), p.Color('black')]
    # colors = [p.Color('light yellow'), p.Color('brown')]
    # colors = [p.Color('antiquewhite1'), p.Color('burlywood4')]
    # colors = [p.Color('burlywood1'), p.Color('tan3')]
    colors = [p.Color('antiquewhite1'), p.Color('burlywood3')]
    for r in range(DIMENSION):
    for c in range(DIMENSION):
    color = colors[((r+c) % 2)]
    rectToDraw = p.Rect(c*SQ_SIZE, r*SQ_SIZE, SQ_SIZE, SQ_SIZE) #position then dimensions
    p.draw.rect(screen, color, rectToDraw)


    """
    Draw the pieces on the board using the current GameState.board
    """
    def drawPiece(screen, board):
    for r in range(DIMENSION):
    for c in range(DIMENSION):
    piece = board[r][c]
    if piece != '--': #not empty square
    rectWhereToDrawPiece = p.Rect(c*SQ_SIZE, r*SQ_SIZE, SQ_SIZE, SQ_SIZE) #position then dimensions
    screen.blit(IMAGES[piece], rectWhereToDrawPiece)


    if __name__ == '__main__': #google to understand why that "if" statement
    main()


    ChessEngine_3.py

    """
    This class is responsible for storing all the information about the current state of a chess game. It will also be responsible
    for determining the valid moves at the current state. It will also keep a move log.
    """

    class GameState():
    def __init__(self):
    #Board is 8x8 2d list, each element of the list has 2 characters
    #The first character represent the color of the piece: 'b' or 'w'
    #The secind character represent the type of the piece: 'P', 'R', 'N', ...
    #'--' represents an empty square with no piece
    self.board = [
    ["bR", "bN", "bB", "bQ", "bK", "bB", "bN", "bR"],
    ["bP", "bP", "bP", "bP", "bP", "bP", "bP", "bP"],
    ["--", "--", "--", "--", "--", "--", "--", "--"],
    ["--", "--", "--", "--", "--", "--", "--", "--"],
    ["--", "--", "--", "--", "--", "--", "--", "--"],
    ["--", "--", "--", "--", "--", "--", "--", "--"],
    ["wP", "wP", "wP", "wP", "wP", "wP", "wP", "wP"],
    ["wR", "wN", "wB", "wQ", "wK", "wB", "wN", "wR"]
    ]
    self.whiteToMove = True
    self.moveLog = []


    """
    Takes a move as a parameter and executes it (this will not work for castling, pawn promotion, and en-passant)
    """
    def makeMove(self, move):
    self.board[move.startRow][move.startCol] = '--' #clear the square from which the piece has moved
    self.board[move.endRow][move.endCol] = move.pieceMoved #we've stored what pîece was moved in Move instance before overwrite its square content in Board instance
    self.moveLog.append(move) #log the move so we can undo it later or to display/get the game's history
    self.whiteToMove = not self.whiteToMove #swap players

    """
    Undo the last move made
    """
    def undoMove(self):
    if len(self.moveLog) > 0: #if there is at least 1 move made
    #get the last move and remove it from move log
    lastMove = self.moveLog.pop()

    self.board[lastMove.startRow][lastMove.startCol] = lastMove.pieceMoved
    self.board[lastMove.endRow][lastMove.endCol] = lastMove.pieceCaptured #put back the eaten piece if so

    self.whiteToMove = not self.whiteToMove #swap players


    class Move():
    # maps keys to values
    # key : value
    ranksToRow = {'1' : 7, '2' : 6, '3' : 5, '4' : 4,
    '5' : 3, '6' : 2, '7' : 1, '8' : 0}
    rowsToRanks = {v: k for k, v in ranksToRow.items()}

    filesToCols = {'a' : 0, 'b' : 1, 'c' : 2, 'd' : 3,
    'e' : 4, 'f' : 5, 'g' : 6, 'h' : 7}
    colsToFiles = {v: k for k, v in filesToCols.items()}

    def __init__(self, startSq, endSq, board):
    self.startRow = startSq[0]
    self.startCol = startSq[1]
    self.endRow = endSq[0]
    self.endCol = endSq[1]
    self.pieceMoved = board[self.startRow][self.startCol]
    self.pieceCaptured = board[self.endRow][self.endCol]
    #a special attribute working like a hash to identify a move by a number and allow us to override the "==" operator on Move class
    self.moveID = self.startRow * 1000 + self.startCol * 100 + self.endRow * 10 + self.endCol

    """
    Overriding the equals method
    """
    def __eq__(self, other):
    if isinstance(other, Move):
    return self.moveID == other.moveID
    return False

    def getChessNotation(self):
    # not exactly chess notation but quite close (will be different for pawn moves for example)
    return self.getRankFile(self.startRow, self.startCol) + self.getRankFile(self.endRow, self.endCol)

    def getRankFile(self, r, c):
    return self.colsToFiles[c] + self.rowsToRanks[r]


    The images

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    [​IMG]

    Continue reading...

Compartilhe esta Página