Writing Chess for Two in Pygame

It was evening, there was nothing to do. I decided to program chess for two. We will do it in Pygame, and I will tell you how later. I must say that I am a real teapot in Python and Pygame, so the code and my explanations are far from ideal. Let's get down to development.

Let's start with the standard actions like importing the necessary libraries, creating a window and a game loop.

import pygame
from pygame import *
import pygame as pg
import math

wind=display.set_mode((640,640))
display.set_caption('Chess') 
clock=time.Clock()
font.init()
game=1
while game: #цикл активен пока игра не закрыта
    for e in event.get():
        if e.type==QUIT:
            game=0 #при нажатии на крестик игра закрывается
    display.update()
    clock.tick(60)

Now let's draw the board

RectList=[] #список прямоугольников, из которых состоит доска
for i in range(8):
    for n in range(4):
        RectList.append(pygame.Rect((n*160+(i%2)*80,i*80, 80, 80))) #добавляем белые клетки, расположенные в шахматном порядке, в список

def DrawBg(): #функция, рисующая доску
    pygame.draw.rect(wind, (181, 136, 99), (0, 0, 640, 640)) #рисуем большую черную клетку на весь экран
    for R in RectList:
        pygame.draw.rect(wind, ((240, 217, 181)), R) #на большой черной клетке рисуем маленькие белые клетки из нашего списка

It's time to create a matrix that will store data about the figures. Each figure must be given its own value. I have the following:

Empty cell – .
King-K
Queen-Q
Rook-R (Rock)
Horse-H
Elephant-B (Bishop)
White Pawn-P (Pawn)
Black Pawn-p (Pawn)

It should be said that the differences between the white and black pawns are based on their differences in attack and moves. White attacks and moves up, and black down.
To denote color, let's take 0 for white and 1 for black.

So, our matrix is:

Board=[
    ['R1','H1','B1','Q1','K1','B1','H1','R1'],
    ['p1','p1','p1','p1','p1','p1','p1','p1'],
    ['.','.','.','.','.','.','.','.'],
    ['.','.','.','.','.','.','.','.'],
    ['.','.','.','.','.','.','.','.'],
    ['.','.','.','.','.','.','.','.'],
    ['P0','P0','P0','P0','P0','P0','P0','P0'],
    ['R0','H0','B0','Q0','K0','B0','H0','R0']]

Let's draw the figures too. For this purpose, I added pictures with the necessary figures to the game folder. The pictures are named according to the figure's encoding in the matrix. For example: black knight – H1.png, white pawn – P0.png, black queen – Q1.png, etc.

def DrawPieces():
    y=0
    for Brd in Board:
        x=0
        for B in Brd:
            if Board[y][x]!='.': #если рассматриваемая клетка не пуста
                wind.blit(transform.scale(pygame.image.load(Board[y][x]+'.png'),(70,70)),(5+x*80,5+y*80))#добавить картинку нужной фигуры в соответствующие координаты
            x+=1
        y+=1

Now let's move on to the technical part of the code. I decided to start with such an important thing as a check for check. After all, according to the rules, a move after which the king of the mover is in check is not allowed. For this, we will create a dictionary AttackDict.

AttackDict={'R':[[0,1],[1,0],[0,-1],[-1,0],1],
            'B':[[1,1],[-1,-1],[1,-1],[-1,1],1],
            'Q':[[1,1],[-1,-1],[1,-1],[-1,1],[0,1],[1,0],[0,-1],[-1,0],1],
            'H':[[1,2],[2,1],[-1,-2],[-2,-1],[-1,2],[-2,1],[1,-2],[2,-1],0],
            'P':[[-1,-1],[1,-1],0],
            'p':[[-1,1],[1,1],0],
            'K':[[1,1],[-1,-1],[1,-1],[-1,1],[0,1],[1,0],[0,-1],[-1,0],0]
            }

Let me explain: each figure has a corresponding list. All values, down to the last one, show the direction of the attack, i.e. where the shift goes along X and Y relative to the figure.
The last value is 0 or 1. 1 means that the piece attacks the entire length of the field, like a queen or rook. 0 means that the attack is “single”, like a king, pawn, knight.

Now let's create a function that will check all the pieces, the cells they capture, and understand whether the king is in check.

def CheckShah(B_W): #аргумент B_W принимает значение 0 или 1. 0-Если интересует шах белого короля, 1-если черного
    y=0
    for Brd in Board: #проверка каждой строки
        x=0
        for B in Brd: #проверка каждой клетки строки, теперь B-проверяемая фигура
            if B!='.': #если клетка не пуста
                if B[1]!=B_W: #если найденная фигура противоположного цвета с проверяемым королём и, соответственно, может его атаковать
                    
                    for shift in AttackDict[B[0]][0:-1]: #shift-направление атаки, числа показывающие сдвиг по X и Y
                        pos=[x,y] #позиция найденной фигуры
                        for i in range(AttackDict[B[0]][-1]*6+1): #если атака во всё поле, то цикл повторится 7 раз, иначе - 1 раз.
                            pos[0]+=shift[0]
                            pos[1]+=shift[1]#сместим рассматриваемую позицию в соответствии с shift
                            if pos[0]>7 or pos[0]<0 or pos[1]>7 or pos[1]<0: break #если X или Y рассматриваемой позиции выходит за пределы поля, то остановить проверку этого направления атаки
                            if Board[pos[1]][pos[0]]!='.':
                                if Board[pos[1]][pos[0]]!='K'+B_W: break #если поле не пустое и на нём не стоит вражеский король, то остановить проверку этого направления атаки
                                else: return True #если король в клетке всё же есть - вернуть True. Король действительно под шахом
            x+=1
        y+=1
    return False #если шах так и не был обнаружен - вернуть False

Next we can add a function that will determine what moves are available to the selected figure.

def ShowVariants(x,y): #x,y-координаты фигуры, для которой нужно определить ходы
    global Variants
    Variants=[] #список вариантов ходов
    B=Board[y][x] #B-фигура, для которой нужно определить ходы
    for shift in AttackDict[B[0]][0:-1]:#уже знакомый shift-сдвиг
        pos=[x,y] #а так же знакомая позиция фигуры-pos
        for i in range(AttackDict[B[0]][-1]*6+1): #если атака во всё поле, то цикл повторится 7 раз, иначе-1 раз.
            pos[0]+=shift[0]
            pos[1]+=shift[1]#опять смещаем позицию с помощью shift
            if pos[0]>7 or pos[0]<0 or pos[1]>7 or pos[1]<0: break #если X или Y рассматриваемой позиции выходит за пределы поля, то остановить проверку этого направления
            if Board[pos[1]][pos[0]]!='.': #если клетка не пуста
                if Board[pos[1]][pos[0]][1]!=Board[y][x][1]: Variants.append([pos[0],pos[1]]) #если клетку занимает вражеская фигура то добавить её как вариант хода
                else: break #если же клетку заняла дружеская фигура, то остановить эту линию ходов
            elif B[0]!='p' and B[0]!='P': #если клетка пуста, а рассматриваемая фигура не пешка, то добавить её как вариант хода. (Пешка не может ходить на пустую клетку по диагонали)
                Variants.append([pos[0],pos[1]])
    
    if B[0]=='P': #если рассматриваемая фигура-белая пешка, то добавим стандартные ходы пешек, производимые без взятия
        pos=[x,y]
        for i in range((y==6)+1): #если пешка на 6-ой линии то цикл повторится 2 раза, иначе-1. (По правилам пешки могут ходить на две клетки, если они на "родной линии")
            pos[1]-=1
            if pos[1]<0: break #если вышли за пределы-стоп
            if Board[pos[1]][pos[0]]!='.':break #если клетка впереди не пуста, а занята - тоже стоп
            Variants.append([pos[0],pos[1]])#если цикл ещё не остановлен, то добавим рассматриваемую клетку как вариант хода

    if B[0]=='p':#все тоже самое для черной пешки
        pos=[x,y]
        for i in range((y==1)+1):
            pos[1]+=1
            if pos[1]>7: break
            if Board[pos[1]][pos[0]]!='.':break
            Variants.append([pos[0],pos[1]])
    
    #Теперь дело за малым - откинуть все ходы, которые ставят своего короля под шах
    
    ForDeletion=[] #список вариантов на удаление
    Board[y][x]='.' #временно уберем рассматриваемую фигуру со стола
    for V in Variants: #переберем все варианты
        remember=Board[V[1]][V[0]] #запоминаем клетку, на которую сейчас поставим фигуру
        Board[V[1]][V[0]]=B #ставим фигуру на это место
        if CheckShah(B[1]): ForDeletion.append(V) #если король под шахом - добавим этот вариант в список на удаление
        Board[V[1]][V[0]]=remember #возвращаем клетку которую запомнили
    Board[y][x]=B #вернём рассматриваемую фигуру на стол
    for Del in ForDeletion: #удалим все недопустимые варианты
        Variants.remove(Del)

The hardest part is over. You can add a check for mate or pat

def CheckCheckMate(B_W): #аргумент B_W - как обычно, 0 - интересует мат/пат белых, 1 - черных
    global Variants
    y=0
    for Brd in Board: #проверка каждой строки
        x=0
        for B in Brd: #проверка каждого элемента строки
            if B[-1]==B_W: #если найдена фигура нужного цвета то проверить, есть ли для неё хоть один вариант хода. Если да - вернуть 0, мата или пата нет
                ShowVariants(x,y)
                if len(Variants)>0:Variants=[];return 0
            x+=1
        y+=1
    #если дошли до этой строки, то это значит, что ни одна фигура нужного цвета не может сделать ход. Это означает, что поставлен мат или пат
    if CheckShah(B_W): Variants=[];return 1 #король под шахом - значит мат, возвращаем 1
    else: Variants=[];return 2 #король не под шахом - пат, возвращаем 2
    #обратите внимание, что перед тем, как вернуть значение, необходимо очистить список Variants, чтобы избежать багов

Finally, we will add the ability to move and check for checkmate or stalemate. For clarity, I will note that the player will “drag” the pieces. That is, select the piece by pressing, and releasing the button – move it.

Now the game loop looks something like this:

Variants=[]
DrawBg()
DrawPieces()
Turn=0
game=1
while game:
    for e in event.get():
        if e.type==QUIT:
            game=0
        
        if e.type==pg.MOUSEBUTTONDOWN and e.button==1: #если нажата ЛКМ
            x,y=(e.pos) #x,y-положение мыши
            x,y=math.floor(x/80),math.floor(y/80) #поделив x,y на 80 получаем клетку, на которую нажал игрок
            if Board[y][x]!='.': #если она не пуста
                if Board[y][x][1]==str(Turn): #и равна переменной Turn - очередь. Turn меняется каждый ход
                    ShowVariants(x,y) #получаем список доступных ходов
                    remember=[x,y] #запомним клетку, на которую нажали
                    for V in Variants:
                        pygame.draw.circle(wind, (200,200,200), (V[0]*80+40, V[1]*80+40), 10) #отрисовка кружочков показывающих, куда можно сходить
        
        if e.type==pg.MOUSEBUTTONUP and e.button==1: #если ОТжата ЛКМ
            x,y=(e.pos)
            x,y=math.floor(x/80),math.floor(y/80) #получаем клетку, в которой находится мышка
            if Variants.count([x,y]): #если эта клетка есть в списке возможных ходов
                Board[y][x]=Board[remember[1]][remember[0]] #заменяем выбранную клетку на ту, что запомнили при нажатии
                Board[remember[1]][remember[0]]='.' #клетку, с которой ушли, оставляем пустой
                Turn=1-Turn #очередь меняется с 0 на 1 или наоборот
                
                #после смены очереди надо проверить наличие мата или пата
                check=CheckCheckMate(str(Turn)) #check примет 1 если объявлен мат, 2 - если пат, 0 - в ином случае
                if check==1: #если мат
                    DrawBg()#рисуем доску напоследок
                    DrawPieces()
                    if Turn==0:#и в зависимости от того, чья очередь, объявляем победителя
                        wind.blit(pygame.font.SysFont(None,30).render('BLACK WON', False,(30, 30, 30)),(260,310))
                    if Turn==1:
                        wind.blit(pygame.font.SysFont(None,30).render('WHITE WON', False,(30, 30, 30)),(260,310))
                if check==2: #если пат, то объявляем ничью
                    wind.blit(pygame.font.SysFont(None,30).render('DRAW', False,(30, 30, 30)),(290,310))
                Variants=[]
            if check==0: #доска отрисуется только если не объявлен мат или пат
                DrawBg()
                DrawPieces()
            Variants=[] #очистим список вариантов, во избежание багов
    display.update()
    clock.tick(60)

At this stage, you can already play, but there are two important things missing. First, a pawn, upon reaching the opposite edge of the board, should be promoted to a knight, bishop, rook or queen at the player's discretion. Second, there is no castling. Let's start with the promotion of pawns.

To do this, add the following lines to the game loop:

if Board[0].count('P0') and Turn==1: #если в нулевой строке найдена белая пешка
        Turn=-1 #то временно меняем очередь на -1. Это значит, что белые выбирают фигуру для замены пешки
        PawnX=Board[0].index('P0') #зададим переменной PawnX значение x, где располагается белая пешка
        wind.blit(transform.scale(image.load('Q0.png'),(40,40)),(PawnX*80,0))
        wind.blit(transform.scale(image.load('R0.png'),(40,40)),(40+PawnX*80,0)) #нарисуем фигуры, на которые нужно будет нажать игроку
        wind.blit(transform.scale(image.load('B0.png'),(40,40)),(PawnX*80,40))
        wind.blit(transform.scale(image.load('H0.png'),(40,40)),(40+PawnX*80,40))
    

Now let's add to the mouse click event the case when Turn=-1

if e.type==pg.MOUSEBUTTONDOWN and e.button==1: #если нажата ЛКМ
    if Turn==-1:
        x,y=(e.pos) #координаты мыши
        if PawnX+1>x/80>=PawnX and y<80: #если нажали на клетку где стоит пешка
        x=x%80
        if 40>x>=0 and 40>y>=0:Board[0][PawnX]='Q0' #в зависимости от того, в какой угол нажал игрок (королева,ладья,слон и конь нарисованы по углам клетки с пешкой), превратим его пешку в соответствующую фигуру
        elif 40>x>=0 and 80>y>=40:Board[0][PawnX]='B0'
        elif 80>x>=40 and 40>y>=0:Board[0][PawnX]='R0'
        elif 80>x>=40 and 80>y>=40:Board[0][PawnX]='H0'
        Turn=1 #вернём черным ход
        DrawBg()
        DrawPieces() #отрисуем доску
        check=CheckCheckMate('1')
        if check==1: wind.blit(pygame.font.SysFont(None,30).render('WHITE WON', False,(30, 30, 30)),(260,310)) #и напоследок проверим наличие мата или пата
        if check==2: wind.blit(pygame.font.SysFont(None,30).render('DRAW', False,(30, 30, 30)),(290,310))

We will write similar lines for black. There is nothing to look at here.
Oh yeah, I almost forgot to add a check to the if that is triggered when raising the LMB to check that Turn is not equal to -1 or -2 (-2 is the value when turning a black pawn)

if e.type==pg.MOUSEBUTTONUP and e.button==1 and Turn!=-1 and Turn!=-2: #если ОТжата ЛКМ

The final spurt: let's add castling. Let me remind those who don't know or have forgotten. Castling is a move that consists of horizontally moving the king towards the rook of its color by 2 squares and then moving the rook to the square adjacent to the king on the other side of the king. In simple terms, you move the rook and the king at the same time. Castling is impossible if the rook or king has already made a move, if there are pieces between the rook and the king. It is also impossible if the king is in check or if during castling it passes through a square that is under attack.

Let's start as usual with adding castling for white, and there is no need to show similar lines for black. So, I added the variable CastlingL0 and CastlingR0 – they are responsible for the possibility of castling with the left white rook and the right white rook, respectively (in the variable name for black, I will replace 0 with 1)

Let's add the following lines to the if statement that is triggered when the mouse is released:

if Board[7][0]!='R0': castlingL0=False #если левая ладья не на месте, запретить делать с ней рокировку
if Board[7][7]!='R0': castlingR0=False #если правая ладья не на месте, запретить делать с ней рокировку
if Board[7][4]!='K0': castlingL0=False;castlingR0=False #если король не на месте, запретить делать рокировку впринципе

Now let's add the following lines to the very end of the function that finds move options for the pieces:

if Board[y][x]=='K0': #если рассматриваем ходы для белого короля
        global castlingL0, castlingR0
        if Board[7][0:5]==['R0','.','.','.','K0'] and castlingL0: #если между левой ладьёй и королём пусто, а рокировка с левой ладьёй не запрещена
            Board[7][2],Board[7][3]='K0','K0' #временно поставим два короля в клетки через которые пройдёт король
            if CheckShah('0')==0: #если эти короли не получают шаха, то это значит, что все условия для рокировки есть и можно добавлять ход-рокировку
                Variants.append([2,7])
            Board[7][2],Board[7][3]='.','.' #уберём временных королей
        
        if Board[7][4:8]==['K0','.','.','R0'] and castlingR0: #все тоже самое для рокировки с правой ладьёй
            Board[7][5],Board[7][6]='K0','K0'
            if CheckShah('0')==0:
                Variants.append([6,7])
            Board[7][5],Board[7][6]='.','.'

Now the king can move 2 squares if all conditions are met, all that's left is to start moving the rook. We'll add the following lines of code to the if statement that triggers when the mouse is released.

Board[y][x]=Board[remember[1]][remember[0]] 
Board[remember[1]][remember[0]]='.' #старые строчки кода которые перемещают фигуру во время совершения хода

if remember==[4,7] and Board[y][x]=='K0': #если фигура, которую мы переместили, была белым королём, находящимся в клетке 4,7 (стандартная позиция короля)
    if [x,y]==[2,7]: Board[7][0]='.';Board[7][3]='R0' #если этот король перешёл в клетку 2,7, то переставить левую ладью
    if [x,y]==[6,7]: Board[7][7]='.';Board[7][5]='R0' #если этот король перешёл в клетку 6,7, то переставить правую ладью

That's it! All that's left is to do the same for black and the game is ready!

Link to Yandex Disk with the game.

Follow the link >>> Click the download all button >>> save Chess.zip. >>> Open it >>> drag the Chess folder to your desktop or anywhere in Explorer >>> open this folder >>> open Chess.py >>> you can play.

Please leave criticism or information about errors in the comments if there are any.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *