ساخت بازی شطرنج با پایتون و Pygame

پروژه‌های بازی‌سازی پایتون

پروژه ۱۱ دقیقه مطالعه
فهرست مطالب
این مطلب از سایت tool.hamidvalad.ir گرفته شده است

معرفی

این پروژه یک بازی شطرنج کامل است که با پایتون و کتابخانه Pygame توسعه یافته است. بازی شامل تمام قوانین استاندارد شطرنج، تشخیص حرکات قانونی، کیش و مات، و انیمیشن‌های حرکتی می‌باشد. پروژه از معماری دو بخشی شامل موتور بازی و رابط کاربری تشکیل شده است.

eggcatcher

نصب و پیش‌نیازها

برای اجرای این پروژه نیاز به پایتون نسخه 3.6 یا بالاتر دارید. کتابخانه‌های مورد نیاز:

  • pygame - برای رابط گرافیکی و مدیریت رویدادها (نیاز به نصب: pip install pygame)
  • os - برای مدیریت مسیر فایل‌ها (پیش‌فرض پایتون)
Terminal
pip install pygame

ساختار فایل‌های پروژه

پروژه از دو فایل اصلی و یک پوشه تصاویر تشکیل شده است:

File Structure
chess-game/
├── ChessGame.py          # فایل اصلی رابط کاربری
├── ChessEngine.py        # موتور بازی و منطق شطرنج
└── images/               # پوشه تصاویر مهره‌ها
    ├── wp.png           # سرباز سفید
    ├── wR.png           # رخ سفید
    ├── wN.png           # اسب سفید
    ├── wB.png           # فیل سفید
    ├── wQ.png           # وزیر سفید
    ├── wK.png           # شاه سفید
    ├── bp.png           # سرباز سیاه
    ├── bR.png           # رخ سیاه
    ├── bN.png           # اسب سیاه
    ├── bB.png           # فیل سیاه
    ├── bQ.png           # وزیر سیاه
    ├── bK.png           # شاه سیاه
    └── icon.ico         # آیکون برنامه

منبع تصاویر: تصاویر مهره‌های شطرنج از وبسایت https://greenchess.net/info.php?item=downloads دانلود شده‌اند.

معماری پروژه

پروژه از دو بخش اصلی تشکیل شده است:

  • ChessGame.py - رابط کاربری گرافیکی و مدیریت رویدادها
  • ChessEngine.py - موتور بازی شامل منطق شطرنج و حرکات

کلاس‌های اصلی شامل:

  • GameState - مدیریت وضعیت بازی و حرکات
  • Move - نمایش و مدیریت حرکات
  • توابع رابط کاربری برای نمایش و انیمیشن

ویژگی‌های اصلی

این بازی شطرنج دارای قابلیت‌های متعددی است:

  • حرکات قانونی تمام مهره‌های شطرنج
  • تشخیص کیش و مات
  • تشخیص پات (تساوی)
  • انیمیشن حرکات مهره‌ها
  • هایلایت خانه‌های قابل حرکت
  • قابلیت undo حرکت (Ctrl+Z)
  • بازنشانی بازی (کلید R)
  • ارتقاء سرباز به وزیر
  • نمایش وضعیت بازی و امتیازات

کدنویسی پروژه

بخش اول: ChessGame.py (رابط کاربری گرافیکی)

این فایل مسئول مدیریت رابط کاربری، رویدادهای ماوس و صفحه‌کلید، و نمایش بصری بازی است. شامل توابع اصلی برای رسم صفحه شطرنج، مهره‌ها، و مدیریت تعامل کاربر می‌باشد.

کد کامل ChessGame.py:

Python
import pygame as p
import ChessEngine
import os

WIDTH = HEIGHT = 512
DIMENSIONS = 8
SQ_SIZE = HEIGHT // DIMENSIONS
MAX_FPS = 15
IMAGES = {}

try:
    icon_path = os.path.join("images", "icon.ico")
    icon = p.image.load(icon_path)
    p.display.set_icon(icon)
except FileNotFoundError:
    print("Warning: Icon file not found. Continuing without icon.")
except Exception as e:
    print(f"Warning: Could not load icon: {e}")

p.display.set_caption("Chess Game")

def loadImages():
    pieces = ['wp', 'wR', 'wN', 'wB', 'wQ', 'wK', 'bp', 'bR', 'bN', 'bB', 'bQ', 'bK']
    for piece in pieces:
        try:
            image_path = os.path.join("images", piece + ".png")
            IMAGES[piece] = p.transform.scale(p.image.load(image_path), (SQ_SIZE, SQ_SIZE))
        except FileNotFoundError:
            print(f"Warning: Image for {piece} not found at {image_path}")
        except Exception as e:
            print(f"Warning: Could not load image for {piece}: {e}")

def main():
    p.init()
    screen = p.display.set_mode((WIDTH, HEIGHT + 50))
    clock = p.time.Clock()
    screen.fill(p.Color("white"))
    gs = ChessEngine.GameState()
    validMoves = gs.getValidMoves()
    moveMade = False
    animate = False
    loadImages()
    running = True
    sqSelected = ()
    playerClicks = []
    gameOver = False

    if not IMAGES:
        print("Error: No game piece images were loaded. The game may not display correctly.")

    while running: 
        for e in p.event.get():
            if e.type == p.QUIT:
                running = False
            elif e.type == p.MOUSEBUTTONDOWN:
                if not gameOver:
                    location = p.mouse.get_pos()
                    col = location[0] // SQ_SIZE
                    row = (location[1] - 50) // SQ_SIZE  
                    if 0 <= row < 8 and 0 <= col < 8:
                        if sqSelected == (row, col):
                            sqSelected = ()
                            playerClicks = []
                        else:
                            sqSelected = (row, col)
                            playerClicks.append(sqSelected)
                        if len(playerClicks) == 1 and (gs.board[row][col] == "--"): 
                            sqSelected = ()
                            playerClicks = []
                        if len(playerClicks) == 2:
                            move = ChessEngine.Move(playerClicks[0], playerClicks[1], gs.board)
                            for i in range(len(validMoves)):
                                if move == validMoves[i]:
                                    gs.makeMove(move)
                                    moveMade = True
                                    animate = True
                                    sqSelected = ()
                                    playerClicks = []
                            if not moveMade:
                                playerClicks = [sqSelected]
            elif e.type == p.KEYDOWN:
                if e.key == p.K_z:
                    gs.undoMove()
                    moveMade = True
                    animate = False
                if e.key == p.K_r:
                    gs = ChessEngine.GameState()
                    validMoves = gs.getValidMoves()
                    sqSelected = ()
                    playerClicks = []
                    moveMade = False
                    animate = False
        if moveMade:
            if animate:
                animatedMoves(gs.moveLog[-1], screen, gs.board, clock)
            validMoves = gs.getValidMoves()
            moveMade = False
            animate = False

        score = calculateScore(gs.board)
        drawScoreAndTurn(screen, score, gs.whiteToMove, gs.checkMate, gs.staleMate)
        drawGameState(screen, gs, validMoves, sqSelected)

        if gs.checkMate:
            gameOver = True
            if gs.whiteToMove:
                drawText(screen, 'Black wins by checkmate')
            else:
                drawText(screen, 'White wins by checkmate')
        elif gs.staleMate:
            gameOver = True
            drawText(screen, 'Stalemate')
        clock.tick(MAX_FPS)
        p.display.flip()

def highlightSquares(screen, gs, validMoves, sqSelected):
    if sqSelected != ():
        r, c = sqSelected
        if gs.board[r][c][0] == ('w' if gs.whiteToMove else 'b'):
            s = p.Surface((SQ_SIZE, SQ_SIZE))
            s.set_alpha(100)
            s.fill(p.Color('blue'))
            screen.blit(s, (c*SQ_SIZE, r*SQ_SIZE + 50))
            s.fill(p.Color("yellow"))
            for moves in validMoves:
                if moves.startRow == r and moves.startCol == c:
                    screen.blit(s, (SQ_SIZE*moves.endCol, SQ_SIZE*moves.endRow + 50))

def drawGameState(screen, gs, validMoves, sqSelected):
    drawBoard(screen)
    highlightSquares(screen, gs, validMoves, sqSelected)
    drawPieces(screen, gs.board)

def drawBoard(screen):
    global colors
    colors = [p.Color("white"), p.Color("grey")]
    for r in range(DIMENSIONS):
        for c in range(DIMENSIONS):
            color = colors[(r+c) % 2]
            p.draw.rect(screen, color, p.Rect(c*SQ_SIZE, r*SQ_SIZE + 50, SQ_SIZE, SQ_SIZE))

def drawPieces(screen, board):
    for r in range(DIMENSIONS):
        for c in range(DIMENSIONS):
            piece = board[r][c]
            if piece != "--" and piece in IMAGES:
                screen.blit(IMAGES[piece], p.Rect(c*SQ_SIZE, r*SQ_SIZE + 50, SQ_SIZE, SQ_SIZE))

def animatedMoves(move, screen, board, clock):
    global colors
    dR = move.endRow - move.startRow
    dC = move.endCol - move.startCol
    framesPerSquare = 5
    frameCount = (abs(dR) + abs(dC)) * framesPerSquare
    for frame in range(frameCount + 1):
        r, c = (move.startRow + dR*frame/frameCount, move.startCol + dC*frame/frameCount)
        drawBoard(screen)
        drawPieces(screen, board)
        color = colors[(move.endRow + move.endCol) % 2]
        endSquare = p.Rect(move.endCol*SQ_SIZE, move.endRow*SQ_SIZE + 50, SQ_SIZE, SQ_SIZE)
        p.draw.rect(screen, color, endSquare)
        if move.pieceCaptured != "--" and move.pieceCaptured in IMAGES:
            screen.blit(IMAGES[move.pieceCaptured], endSquare)
        if move.pieceMoved in IMAGES:
            screen.blit(IMAGES[move.pieceMoved], p.Rect(c*SQ_SIZE, r*SQ_SIZE + 50, SQ_SIZE, SQ_SIZE))
        p.display.flip()
        clock.tick(60)

def drawText(screen, text):
    font = p.font.SysFont("Helvetica", 32, True, False)
    textObject = font.render(text, True, p.Color('Gray'))
    textLocation = p.Rect(0, 50, WIDTH, HEIGHT).move(WIDTH/2 - textObject.get_width()/2, HEIGHT/2 - textObject.get_height()/2)
    screen.blit(textObject, textLocation)
    textObject = font.render(text, True, p.Color("Black"))
    screen.blit(textObject, textLocation.move(2, 2))

def calculateScore(board):
    pieceValues = {
        'p': 1,   # pawn
        'N': 3,   # knight
        'B': 3,   # bishop
        'R': 5,   # rook
        'Q': 9,   # queen
        'K': 0    # king (not counted in material score)
    }
    whiteScore = 0
    blackScore = 0
    for row in board:
        for piece in row:
            if piece != "--":
                color = piece[0]
                pieceType = piece[1]
                value = pieceValues.get(pieceType, 0)
                if color == 'w':
                    whiteScore += value
                else:
                    blackScore += value
    return whiteScore, blackScore

def drawScoreAndTurn(screen, score, whiteToMove, checkMate, staleMate):
    whiteScore, blackScore = score
    scoreDifference = whiteScore - blackScore
    infoHeight = 50
    infoSurface = p.Surface((WIDTH, infoHeight))
    infoSurface.fill(p.Color("lightgray"))
    smallFont = p.font.SysFont("Helvetica", 16)
    boldFont = p.font.SysFont("Helvetica", 16, True)
    scoreText = f"White: {whiteScore}  |  Black: {blackScore}"
    scoreSurface = smallFont.render(scoreText, True, p.Color("black"))
    infoSurface.blit(scoreSurface, (10, 5))
    if scoreDifference > 0:
        advantageText = f"White +{scoreDifference}"
        advantageColor = p.Color("darkblue")
    elif scoreDifference < 0:
        advantageText = f"Black +{abs(scoreDifference)}"
        advantageColor = p.Color("darkred")
    else:
        advantageText = "Material equal"
        advantageColor = p.Color("darkgreen")
    advantageSurface = boldFont.render(advantageText, True, advantageColor)
    infoSurface.blit(advantageSurface, (10, 25))
    if not checkMate and not staleMate:
        turnText = "White's turn" if whiteToMove else "Black's turn"
        turnColor = p.Color("white")
        turnSurface = smallFont.render(turnText, True, turnColor)
        turnBgWidth = turnSurface.get_width() + 20
        turnBgHeight = turnSurface.get_height() + 10
        turnBg = p.Surface((turnBgWidth, turnBgHeight))
        turnBg.fill(p.Color("darkgreen" if whiteToMove else "darkred"))
        turnBgX = WIDTH - turnBgWidth - 10
        turnBgY = (infoHeight - turnBgHeight) // 2
        infoSurface.blit(turnBg, (turnBgX, turnBgY))
        infoSurface.blit(turnSurface, (turnBgX + 10, turnBgY + 5))
    screen.blit(infoSurface, (0, 0))

if __name__ == "__main__":
    main()

توضیح بخش‌های اصلی ChessGame.py:

۱. تنظیمات اولیه و بارگذاری تصاویر: در ابتدای برنامه، پارامترهای اصلی بازی مانند ابعاد صفحه (512x512 پیکسل)، اندازه خانه‌ها (64 پیکسل)، و نرخ فریم (15 فریم بر ثانیه) تنظیم می‌شوند. تابع loadImages تصاویر تمام مهره‌ها را از پوشه images بارگذاری کرده و به اندازه مناسب مقیاس می‌دهد. برای مدیریت صحیح مسیر فایل‌ها در سیستم‌عامل‌های مختلف از os.path.join استفاده شده است.

۲. تابع اصلی و حلقه بازی: تابع main نقطه شروع برنامه است که حلقه اصلی بازی را اجرا می‌کند. این حلقه به طور مداوم سه نوع رویداد را پردازش می‌کند:

  • رویداد خروج (QUIT): زمانی که کاربر پنجره را می‌بندد.
  • کلیک‌های ماوس (MOUSEBUTTONDOWN): برای انتخاب و حرکت مهره‌ها با سیستم دو کلیکی.
  • فشار کلیدهای صفحه‌کلید (KEYDOWN): کلید Z برای بازگشت به حرکت قبلی (undo) و کلید R برای بازنشانی بازی (reset).

۳. منطق انتخاب مهره: زمانی که کاربر روی صفحه کلیک می‌کند، موقعیت کلیک به مختصات شطرنجی (ردیف و ستون) تبدیل می‌شود. سیستم از یک لیست به نام playerClicks برای ذخیره دو کلیک متوالی استفاده می‌کند: کلیک اول خانه مبدا (مهره انتخاب شده) و کلیک دوم خانه مقصد را مشخص می‌کند.

۴. توابع رسم و نمایش: مجموعه‌ای از توابع گرافیکی مسئول نمایش بازی هستند:

  • drawBoard: صفحه شطرنج را با الگوی شطرنجی سفید و خاکستری رسم می‌کند.
  • drawPieces: مهره‌ها را بر اساس وضعیت فعلی بازی از کلاس GameState نمایش می‌دهد.
  • highlightSquares: خانه انتخاب شده و خانه‌های قابل حرکت را با رنگ‌های آبی و زرد هایلایت می‌کند.
  • drawGameState: توابع فوق را ترکیب کرده و حالت کامل بازی را رسم می‌کند.

۵. انیمیشن حرکات: تابع animatedMoves حرکت مهره‌ها را به صورت روان و پیوسته نمایش می‌دهد. این تابع با محاسبه موقعیت‌های میانی بین نقطه شروع و پایان، حرکت را به چندین فریم تقسیم کرده و در هر فریم مهره را در موقعیت جدیدی رسم می‌کند. تعداد فریم‌ها بر اساس فاصله حرکت تعیین می‌شود تا سرعت حرکت در تمام مسافت‌ها یکسان باشد.

۶. سیستم امتیازدهی و نوار اطلاعات: این بخش در بالای صفحه شطرنج نمایش داده می‌شود و شامل:

  • امتیاز هر بازیکن بر اساس ارزش مهره‌های باقی‌مانده (سرباز: ۱، اسب/فیل: ۳، رخ: ۵، وزیر: ۹)
  • تفاوت امتیاز و نشان دادن مزیت مادی هر طرف
  • نشانگر نوبت بازیکن با رنگ سبز برای سفید و قرمز برای سیاه
  • پیام‌های پایان بازی (کیش و مات یا پات)

۷. مدیریت حالت‌های پایان بازی: بازی دو حالت پایان دارد: کیش و مات (وقتی یک بازیکن در کیش است و هیچ حرکت قانونی ندارد) و پات (وقتی بازیکن در کیش نیست اما هیچ حرکت قانونی ندارد). در هر دو حالت، بازی متوقف شده و پیام مناسب نمایش داده می‌شود.

بخش دوم: ChessEngine.py (موتور بازی و منطق شطرنج)

این فایل شامل تمام منطق بازی شطرنج از جمله قوانین حرکات مهره‌ها، تشخیص کیش و مات، و مدیریت وضعیت بازی است. این بخش هسته اصلی برنامه است که مستقل از رابط کاربری عمل می‌کند.

کد کامل ChessEngine.py:

Python

class GameState: 
        
        def __init__(self):
                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.moveFunctions = {'p': self.getPawnMoves, 'R': self.getRookMoves, 'N': self.getKnightMoves,
                                      'B': self.getBishopMoves, 'Q': self.getQueenMoves, 'K': self.getKingMoves}
                self.whiteToMove = True,
                self.moveLog = []
                self.whiteKingLocation = (7, 4)
                self.blackKingLocation = (0, 4)
                self.checkMate = False
                self.staleMate = False

        def makeMove(self, move):
                self.board[move.startRow][move.startCol] = "--"
                self.board[move.endRow][move.endCol] = move.pieceMoved
                self.moveLog.append(move)
                self.whiteToMove = not self.whiteToMove
                if move.pieceMoved == "wK":
                        self.whiteKingLocation = (move.endRow, move.endCol)
                elif move.pieceMoved == "bK":
                        self.blackKingLocation = (move.endRow, move.endCol)

                if move.isPawnPromotion:
                        self.board[move.endRow][move.endCol] = move.pieceMoved[0] + "Q"


        def undoMove(self):
                if len(self.moveLog) != 0:
                        move = self.moveLog.pop()
                        self.board[move.startRow][move.startCol] = move.pieceMoved
                        self.board[move.endRow][move.endCol] = move.pieceCaptured
                        self.whiteToMove = not self.whiteToMove
                        if move.pieceMoved == "wK":
                                self.whiteKingLocation = (move.startRow, move.startCol)
                        if move.pieceMoved == "bK":
                                self.blackKingLocation = (move.startRow, move.startCol)
        """
        All move considering checks
        """
        def getValidMoves(self):
                moves = self.getAllPossibleMoves()
                for i in range(len(moves)-1, -1, -1):
                        self.makeMove(moves[i])
                        self.whiteToMove = not self.whiteToMove
                        if self.inCheck():
                                moves.remove(moves[i])
                        self.whiteToMove = not self.whiteToMove
                        self.undoMove()
                if len(moves) == 0:
                        if self.inCheck():
                                self.checkMate = True
                        else:
                                self.staleMate = True
                else:
                        self.checkMate = False
                        self.staleMate = False

                return moves 

        def inCheck(self):
                if self.whiteToMove:
                        return self.squareUnderAttack(self.whiteKingLocation[0], self.whiteKingLocation[1])
                else:
                        return self.squareUnderAttack(self.blackKingLocation[0], self.blackKingLocation[1])

        def squareUnderAttack(self, r, c):
                self.whiteToMove = not self.whiteToMove
                oppMoves = self.getAllPossibleMoves()
                self.whiteToMove  = not self.whiteToMove
                for move in oppMoves:
                        if move.endRow == r and move.endCol == c:
                                return True
                return False




        """
        All move without considering checks
        """
        def getAllPossibleMoves(self):
                moves = []
                for r in range(len(self.board)):
                        for c in range(len(self.board[r])):
                                turn = self.board[r][c][0] # b or w based on turn
                                if(turn == 'w' and self.whiteToMove) or (turn == 'b' and not self.whiteToMove):
                                        piece = self.board[r][c][1]
                                        self.moveFunctions[piece](r,c, moves)
                return moves


        def getPawnMoves(self, r, c, moves):
                if self.whiteToMove:
                        if self.board[r-1][c] == "--":
                                moves.append(Move((r, c),(r-1, c), self.board))
                                if r == 6 and self.board[r-2][c] == "--":
                                        moves.append(Move((r, c),(r-2, c), self.board))
                        if c-1 >= 0:
                                if self.board[r-1][c-1][0] == 'b':
                                        moves.append(Move((r, c),(r-1, c-1), self.board))
                        if c+1 <= 7:
                                if self.board[r-1][c+1][0] == 'b':
                                        moves.append(Move((r, c),(r-1, c+1), self.board))
                
                else:
                        if self.board[r+1][c] == "--":
                                moves.append(Move((r, c),(r+1, c), self.board))
                                if r == 1 and self.board[r+2][c] == "--":
                                        moves.append(Move((r, c),(r+2, c), self.board))
                        if c-1 >= 0:
                                if self.board[r+1][c-1][0] == 'w':
                                        moves.append(Move((r, c),(r+1, c-1), self.board))
                        if c+1 <= 7:
                                if self.board[r+1][c+1][0] == 'w':
                                        moves.append(Move((r, c),(r+1, c+1), self.board))

        def getRookMoves(self, r, c, moves):
                directions = ((-1, 0), (0, -1), (1, 0), (0, 1))
                enemyColor = "b" if self.whiteToMove else "w"
                for d in directions:
                        for i in range(1, 8):
                                endRow = r + d[0] * i
                                endCol = c + d[1] * i
                                if 0 <= endRow < 8 and 0 <= endCol < 8:
                                        endPiece = self.board[endRow][endCol]
                                        if endPiece == "--":
                                                moves.append(Move((r,c), (endRow, endCol), self.board))
                                        elif endPiece[0] == enemyColor:
                                                moves.append(Move((r,c), (endRow, endCol), self.board))
                                                break
                                        else:
                                                break
                                else:
                                        break

        def getKnightMoves(self, r,c,moves):
                knightMoves = ((-2, -1), (-2, 1), (-1, -2), (-1, 2), (1, -2), (1, 2), (2, -1), (2,1))
                allyColor = "w" if self.whiteToMove else "b"
                for m in knightMoves:
                        endRow = r + m[0]
                        endCol = c + m[1]
                        if 0 <= endRow < 8 and 0 <= endCol < 8:
                                endPiece = self.board[endRow][endCol]
                                if endPiece[0] != allyColor:
                                        moves.append(Move((r,c), (endRow, endCol), self.board))

        def getBishopMoves(self, r,c,moves):
                directions = ((-1, -1), (-1, 1), (1, -1), (1, 1))
                enemyColor = "b" if self.whiteToMove else "w"
                for d in directions:
                        for i in range(1, 8):
                                endRow = r + d[0] * i
                                endCol = c + d[1] * i
                                if 0 <= endRow < 8 and 0 <= endCol < 8:
                                        endPiece = self.board[endRow][endCol]
                                        if endPiece == "--":
                                                moves.append(Move((r,c), (endRow, endCol), self.board))
                                        elif endPiece[0] == enemyColor:
                                                moves.append(Move((r,c), (endRow, endCol), self.board))
                                                break
                                        else:
                                                break
                                else:
                                        break

        def getQueenMoves(self, r,c,moves):
                self.getRookMoves(r, c, moves)
                self.getBishopMoves(r, c, moves)

        def getKingMoves(self, r,c,moves):
                kingMoves = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1,1) )
                allyColor = "w" if self.whiteToMove else "b"
                for i in range(8):
                        endRow = r + kingMoves[i][0]
                        endCol = c + kingMoves[i][1]
                        if 0 <= endRow < 8 and 0 <= endCol < 8:
                                endPiece = self.board[endRow][endCol]
                                if endPiece[0] != allyColor:
                                        moves.append(Move((r,c), (endRow, endCol), self.board))
class Move():

        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]
                self.isPawnPromotion = False
                if (self.pieceMoved == 'wp' and self.endRow == 0) or (self.pieceMoved == 'bp' and self.endRow == 7):
                        self.isPawnPromotion = True
                self.moveID = self.startRow * 1000 + self.startCol * 100 + self.endRow * 10 + self.endCol

        def __eq__(self, other):
                if isinstance(other, Move):
                        return self.moveID  == other.moveID
                return False


        def getChessNotation(self):
                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]

توضیح بخش‌های اصلی ChessEngine.py:

۱. کلاس GameState (مدیریت وضعیت بازی): این کلاس وضعیت کامل بازی را در هر لحظه نگهداری می‌کند. مهم‌ترین attributeهای آن عبارتند از:

  • board: یک آرایه دو بعدی 8×8 که موقعیت تمام مهره‌ها را ذخیره می‌کند.
  • whiteToMove: یک مقدار بولین که نشان می‌دهد نوبت کدام بازیکن است.
  • moveLog: لیستی از تمام حرکات انجام شده در بازی.
  • whiteKingLocation و blackKingLocation: موقعیت فعلی شاه‌ها برای تشخیص سریع کیش.
  • checkMate و staleMate: نشان‌دهنده وضعیت پایان بازی.

۲. ساختار داده‌ای صفحه: صفحه شطرنج به صورت یک آرایه دو بعدی از رشته‌ها پیاده‌سازی شده است. هر خانه می‌تواند یکی از مقادیر زیر را داشته باشد:

  • "wp": سرباز سفید (white pawn)
  • "wR": رخ سفید (white rook)
  • "wN": اسب سفید (white knight)
  • "wB": فیل سفید (white bishop)
  • "wQ": وزیر سفید (white queen)
  • "wK": شاه سفید (white king)
  • مقادیر مشابه با پیشوند "b" برای مهره‌های سیاه
  • "--": خانه خالی

۳. متدهای مدیریت حرکات:

  • makeMove: یک حرکت معتبر را روی صفحه اعمال می‌کند، وضعیت بازی را به روز می‌کند، و حرکت را به تاریخچه اضافه می‌کند.
  • undoMove: آخرین حرکت را از تاریخچه حذف کرده و وضعیت بازی را به حالت قبل برمی‌گرداند.
  • هر دو متد موقعیت شاه‌ها را در صورت جابجایی به‌روزرسانی می‌کنند و ارتقاء سرباز را مدیریت می‌کنند.

۴. سیستم تشخیص حرکات معتبر و کیش: این بخش پیچیده‌ترین قسمت موتور بازی است:

  • getValidMoves: از الگوریتم "تولید و آزمایش" استفاده می‌کند. ابتدا تمام حرکات ممکن تولید می‌شوند، سپس هر حرکت به طور موقت اجرا شده و بررسی می‌شود که آیا بازیکن را در وضعیت کیش قرار می‌دهد یا خیر.
  • inCheck: بررسی می‌کند که آیا بازیکن فعلی در کیش است.
  • squareUnderAttack: بررسی می‌کند که آیا یک خانه خاص مورد حمله مهره‌های حریف است.
  • اگر پس از فیلتر کردن حرکات منجر به کیش، هیچ حرکت معتبری باقی نماند، بازی به وضعیت کیش و مات (اگر بازیکن در کیش باشد) یا پات (اگر در کیش نباشد) می‌رسد.

۵. تولید حرکات پایه برای هر مهره: هر نوع مهره تابع حرکت مخصوص به خود را دارد:

  • سرباز (Pawn): می‌تواند یک خانه به جلو حرکت کند (یا دو خانه در حرکت اول). فقط به صورت مورب می‌تواند حمله کند. جهت حرکت بستگی به رنگ مهره دارد (سفید به سمت بالا، سیاه به سمت پایین).
  • رخ (Rook): در چهار جهت اصلی (بالا، پایین، چپ، راست) به صورت خطی حرکت می‌کند تا زمانی که به مانع برخورد کند.
  • فیل (Bishop): در چهار جهت مورب حرکت می‌کند.
  • وزیر (Queen): ترکیبی از رخ و فیل، در هشت جهت حرکت می‌کند.
  • اسب (Knight): با الگوی "L" حرکت می‌کند (دو خانه در یک جهت و یک خانه در جهت عمود بر آن). تنها مهره‌ای است که می‌تواند از روی مهره‌های دیگر بپرد.
  • شاه (King): یک خانه در هر جهت (هشت خانه اطراف خود) حرکت می‌کند.

۶. کلاس Move (نمایش و مدیریت حرکات): این کلاس تمام اطلاعات مربوط به یک حرکت شطرنج را encapsulate می‌کند:

  • مختصات شروع و پایان
  • مهره حرکت داده شده و مهره captured (در صورت وجود)
  • آیا حرکت یک ارتقاء سرباز است یا خیر
  • یک شناسه یکتا برای مقایسه حرکات
  • متدهایی برای تبدیل مختصات عددی به نوتاسیون شطرنج (مانند (0,0) به "a8")

۷. سیستم نوتاسیون شطرنج: کلاس Move شامل دیکشنری‌هایی برای تبدیل بین سیستم مختصات عددی (0-7 برای سطر و ستون) و نوتاسیون استاندارد شطرنج (a-h برای ستون‌ها و 1-8 برای سطرها) است. این امکان نمایش حرکات به فرمت قابل خواندن مانند "e2e4" را فراهم می‌کند.

۸. ارتقاء سرباز: زمانی که یک سرباز به آخرین ردیف صفحه حریف می‌رسد، به طور خودکار به وزیر ارتقاء می‌یابد. این منطق در متد makeMove و سازنده کلاس Move پیاده‌سازی شده است.

نحوه اجرا

برای اجرای بازی ابتدا مطمئن شوید تمام فایل‌ها در پوشه مناسب قرار دارند، سپس فایل اصلی را اجرا کنید:

Terminal
python ChessGame.py

کنترل‌های بازی:

  • کلیک ماوس برای انتخاب و حرکت مهره‌ها
  • Ctrl+Z برای بازگشت به حرکت قبلی
  • کلید R برای بازنشانی بازی

توسعه و بهبود

ایده‌هایی برای توسعه بیشتر بازی:

  • افزودن هوش مصنوعی برای بازی کامپیوتر
  • قابلیت ذخیره و بارگذاری بازی
  • افزودن تایمر برای هر بازیکن
  • پشتیبانی از حرکات ویژه (قلعه‌گیری، آنپاسان)
  • افزودن صدا و موسیقی متن
  • ایجاد حالت‌های مختلف بازی
  • قابلیت بازی شبکه‌ای

خلاصه پروژه

این پروژه یک پیاده‌سازی کامل و کاربردی از بازی شطرنج است که با پایتون و Pygame توسعه یافته است. ویژگی‌های برجسته این پروژه شامل:

  • معماری تمیز با جداسازی منطق بازی و رابط کاربری
  • پیاده‌سازی کامل تمام حرکات قانونی شطرنج
  • سیستم تشخیص کیش، مات و پات
  • انیمیشن‌های روان برای حرکات مهره‌ها
  • رابط کاربری intuitive با هایلایت خانه‌های قابل حرکت
  • قابلیت‌های undo و reset بازی
  • مدیریت صحیح ارتقاء سرباز
  • سیستم نوتاسیون شطرنج برای حرکات
  • سیستم امتیازدهی و نمایش وضعیت بازی

این پروژه نمونه‌ای عالی از ترکیب برنامه‌نویسی شی‌گرا، الگوریتم‌های بازی و طراحی رابط کاربری در پایتون است.

منابع و مستندات

منبع : https://greenchess.net/info.php?item=downloads

منبع : https://www.pygame.org/docs/

منبع : https://docs.python.org/3/library/os.html