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