pythonpygamedrag-and-dropchess

How to implement drag and drop to basic pygame chess engine


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

edit: I've simplified as much as possible unfortunately the ChessMain_2_simple.py can't be reduced any further as I want to see precisely how drag and drop can coexist with this content (which consists of selecting a piece and then selecting the box where you want to move it)

ChessMain_2_simple.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_2_simple

WIDTH = HEIGHT = 512 #400 is another option
DIMENSION = 4
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', 'wP']
    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_2_simple.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
            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_2_simple.Move(playerClicks[0], playerClicks[1], gs.board)
                    gs.makeMove(move)
                    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('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_2_simple.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 4x4 2d list, each element of the list has 2 characters
        #The first character represent the color of the piece: 'b' or 'w'
        #The second character represent the type of the piece: 'P'
        #'--' represents an empty square with no piece
        self.board = [
            ["bP", "bP", "bP", "bP"],
            ["--", "--", "--", "--"],
            ["--", "--", "--", "--"],
            ["wP", "wP", "wP", "wP"],
        ]

    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


class Move():
    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]

The images

enter image description here

enter image description here


Solution

  • Let me point you towards drag&drop for your chess game. First, you need some members to store if and what is dragged and where it currently is in your game state:

    self.pieceBeingDragged = None # Track the piece being dragged
    self.draggingPosition  = None # Track the position of the piece being dragged
    

    You want to set/update/reset these in your main loop depending on the mouse actions:

    # mouse handler
    elif e.type == p.MOUSEBUTTONDOWN:
        location = p.mouse.get_pos()      # (x, y) location of the mouse
        col      = location[0] // SQ_SIZE # column of the square clicked
        row      = location[1] // SQ_SIZE # row of the square clicked
    
        sqSelected           = (row, col)
        gs.pieceBeingDragged = gs.board[row][col]  # Track the piece being dragged
        gs.draggingPosition  = location
    
    elif e.type == p.MOUSEMOTION:
        if sqSelected:  # If a piece is being dragged
            gs.draggingPosition = p.mouse.get_pos()  # Update the position of the dragged piece
    
    elif e.type == p.MOUSEBUTTONUP:
        if sqSelected:  # Ensure a piece was selected
            location = p.mouse.get_pos()
            col      = location[0] // SQ_SIZE
            row      = location[1] // SQ_SIZE
    
            move = ChessEngine_3.Move(sqSelected, (row, col), gs.board)
            print(move.getChessNotation())
            gs.makeMove(move)
    
        sqSelected           = None  # Reset selection
        gs.pieceBeingDragged = None  # Reset the piece being dragged
        gs.draggingPosition  = None  # Reset the dragging position
    
    # key handler
    

    Finally, you need to update your drawGameState method to visualize the dragged piece. I suggest to update drawPiece as well such that it skips the sqSelected when drawing the pieces.

    def drawGameState(screen, gs, sqSelected):
        drawBoard(screen) # draw squares on the board
        #add in piece highlighting or move suggestions (later)
        drawPiece(screen, gs.board, sqSelected) # Draw pieces on top of those squares, skip selected square
    
        if gs.draggingPosition:  # If a piece is being dragged
            highlightColorFrom = p.Color('yellow')
            highlightColorTo   = p.Color('green')
    
            (r, c) = sqSelected  # Highlight the selected square
            rectToHighlight = p.Rect(c * SQ_SIZE, r * SQ_SIZE, SQ_SIZE, SQ_SIZE)
            p.draw.rect(screen, highlightColorFrom, rectToHighlight)
    
            # Highlight current square
            (r, c) = gs.draggingPosition[1] // SQ_SIZE, gs.draggingPosition[0] // SQ_SIZE
            rectToHighlight = p.Rect(c * SQ_SIZE, r * SQ_SIZE, SQ_SIZE, SQ_SIZE)
            p.draw.rect(screen, highlightColorTo, rectToHighlight)
    
            # Draw the piece being dragged
            piece = gs.pieceBeingDragged
            rectWhereToDrawPiece = p.Rect(gs.draggingPosition[0] - SQ_SIZE//2, gs.draggingPosition[1] - SQ_SIZE//2, SQ_SIZE, SQ_SIZE)
            screen.blit(IMAGES[piece], rectWhereToDrawPiece)  # Draw the piece at the new position
    

    This should get you started. If you still need the two-clicks movement, you can add a check into the MOUSEBUTTONUP case and collect clicks if the user has not moved from the original square.

    Chess boards with drag&drop