Дело было вечером, делать было нечего. Решил запрограммировать шахматы на двоих. Делать их будем на Pygame, а как - расскажу далее. Надо сказать, что я в Python и Pygame тот ещё чайник, так что код и мои пояснения далеки от идеала. Давайте приступим к разработке.

Начнём со стандартных действий вроде импортирования нужных библиотек, создания окна и игрового цикла

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)

Теперь нарисуем доску

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) #на большой черной клетке рисуем маленькие белые клетки из нашего списка

Пора создать матрицу, в которой будут храниться данные о фигурах. Каждой фигуре надо дать свое значение. У меня они следующие:

Пустая клетка - .
Король-K (King)
Королева-Q (Queen)
Ладья-R (Rock)
Конь-H (Horse)
Слон-B (Bishop)
Белая пешка-P (Pawn)
Черная пешка-p (Pawn)

Надо сказать, что отличия между белой и черной пешкой обоснованы их различием в атаке и ходах. Белые атакуют и ходят вверх, а черные - вниз.
Для обозначения цвета примем 0 за белый, 1 - за черный.

Итак, наша матрица:

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']]

Давайте нарисуем и фигуры. Для этого я добавил картинки с нужными фигурами в папку с игрой. Картинки называются в соответствии с зашифровкой фигуры в матрице. Например: черный конь - H1.png, белая пешка - P0.png, Черная королева - Q1.png и т.д.

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

Теперь перейдём к технической части кода. Начать я решил с такой важной вещи как проверка на шах. Ведь по правилам ход, после которого король ходившего находится под шахом, недопустим. Для этого создадим словарь 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]
            }

Объясняю: каждой фигуре соответствует список. Все значения до последнего - показывают направление атаки, то есть куда идет смещение по X и Y относительно фигуры.
Последние значение - 0 или 1. 1 означает, что фигура атакует во всю длину поля как королева или ладья. 0 означает что атака "одинарная" как у короля, пешки, коня.

Теперь создадим функцию, которая проверит все фигуры, клетки которые они бьют и поймёт, под шахом ли король.

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

Далее можем добавить функцию, которая определит какие ходы доступны выбранной фигуре

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)

Самое сложное позади. Можно добавить проверку на мат или пат

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, чтобы избежать багов

Наконец добавим возможность ходить и проверку на мат или пат. Для понимания отмечу, что игрок фигуры будет "перетаскивать". То есть выбирать фигуру нажатием, а отпуская кнопку - переставлять.

Теперь игровой цикл выглядит как то так:

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)

На этом этапе уже можно играть, но не хватает 2 важные вещи. Во-первых, пешка, при достижении противоположного края доски должна превращаться в коня, слона, ладью или ферзя на усмотрение игрока. Во-вторых, не хватает рокировки. Начнём с превращения пешек.

для этого добавим в игровой цикл следующие строки:

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

Теперь добавим в событие с нажатием мыши случай, когда 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))

Аналогичные строки напишем для чёрных. Здесь смотреть не на что.
Ах да, чуть не забыл добавить в if срабатывающий при поднятии ЛКМ проверку на то что Turn не равен -1 или -2 (-2 - значение при превращении черной пешки)

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

Последний рывок: добавим рокировку. Давайте напомню тем, кто не знает или забыл. Рокировка - ход, заключающийся в горизонтальном перемещении короля в сторону ладьи своего цвета на 2 клетки и последующем перемещении ладьи на соседнюю с королём клетку по другую сторону от короля. Простыми словами вы переставляете ладью и короля одновременно. Рокировка невозможна если ладья или король уже делали ход, если между ладьёй и королём есть фигуры. Так же она невозможна если король под шахом или во время совершения рокировки пройдет через клетку, находящуюся под ударом.

Начнём как обычно с добавления рокировки для белых, а аналогичные строки для чёрных показывать необходимости нет. Итак, я добавил переменную CastlingL0 и CastlingR0 - они отвечают за возможность совершения рокировки с левой белой ладьёй и правой белой ладьёй соответственно (в названии переменных для чёрных заменю 0 на 1)

Добавим в if срабатывающий при отжатии мыши следующие строки:

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

Теперь добавим в самый конец функции, находящей варианты хода для фигур следующие строчки:

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]='.','.'

Теперь король уже может ходить на 2 клетки при соблюдении всех условий, осталось лишь начать смещать ладью. Следующие строки кода мы добавим в if срабатывающий при отжатии мыши.

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, то переставить правую ладью

Всё! Осталось сделать то же для чёрных и игра готова!

Ссылка на Яндекс Диск с игрой.

Переходите по ссылке >>> Нажимайте на кнопку скачать всё >>> сохраняйте Chess.zip. >>> Открывайте его >>> перетаскивайте папку Chess себе на рабочий стол или в любое место проводника >>> открываете эту папку >>> открываете Chess.py >>> можно играть.

Просьба, оставлять в комментариях критику или информацию об ошибках если таковые имеются.

Комментарии (15)


  1. GospodinKolhoznik
    15.08.2024 10:51
    +1

    Вам просто обязательно нужно освоить декомпозицию функций. Это самый главный ключ к написанию хорошо структурированного кода.


    1. RUSTIK1023 Автор
      15.08.2024 10:51

      Спасибо, почитаю на досуге


  1. zartarn
    15.08.2024 10:51
    +2

    Кажется Вы забыли про 'взятие на проходе'


    1. RUSTIK1023 Автор
      15.08.2024 10:51

      Да. Я не любитель шахмат и узнал о такой вещи после написания статьи. Но я планирую написать вторую статью, с учётом всей критики


  1. AndyLem
    15.08.2024 10:51

    Я просто оставлю это здесь

    https://peps.python.org/pep-0008/


    1. RUSTIK1023 Автор
      15.08.2024 10:51

      Спасибо, прочитаю. Вероятно, в будущем придётся переделать статью с учетом всей критики.


      1. AndyLem
        15.08.2024 10:51

        Пожалуйста. Раз вы только собираетесь читать, то я удивлен вашим выбором языка реализации. Но лучше поздно, чем никогда ☺️


  1. Vegas_Real
    15.08.2024 10:51

    Сразу с импортов... Блин ну почитайте как они пишутся! Просто import pygame as pg и все! Зачем вся остальная чепуха?


    1. RUSTIK1023 Автор
      15.08.2024 10:51

      Спасибо, не знал


  1. LunarBirdMYT
    15.08.2024 10:51
    +1

    Вечер прошел продуктивно :) А вы планируете дальше улучшать этот код? Местами я вижу очень похожие моменты, возможно можно было бы создать отдельные классы для фигур с какими-нибудь общими методами. Это сделает код более читаемым и вам возможно будет проще понять его логику спустя время. Так можно еще и ООП подтянуть - как идея для второй статейки. Хорошо бы еще в папку на яндексе докинуть файл с зависимостями для скрипта.


    1. RUSTIK1023 Автор
      15.08.2024 10:51
      +1

      Спасибо. Да, похоже, придётся делать вторую статью. Уже вижу, что мне есть чему научиться


  1. kOlydeBug
    15.08.2024 10:51
    +1

    Rook - ладья


    1. csl
      15.08.2024 10:51

      и Horse - Knight


      1. RUSTIK1023 Автор
        15.08.2024 10:51

        Я знаю. Заменил, чтобы не совпадало с королём


    1. RUSTIK1023 Автор
      15.08.2024 10:51

      Ой