Дело было вечером, делать было нечего. Решил запрограммировать шахматы на двоих. Делать их будем на 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)
zartarn
15.08.2024 10:51+2Кажется Вы забыли про 'взятие на проходе'
RUSTIK1023 Автор
15.08.2024 10:51Да. Я не любитель шахмат и узнал о такой вещи после написания статьи. Но я планирую написать вторую статью, с учётом всей критики
AndyLem
15.08.2024 10:51Я просто оставлю это здесь
https://peps.python.org/pep-0008/
RUSTIK1023 Автор
15.08.2024 10:51Спасибо, прочитаю. Вероятно, в будущем придётся переделать статью с учетом всей критики.
AndyLem
15.08.2024 10:51Пожалуйста. Раз вы только собираетесь читать, то я удивлен вашим выбором языка реализации. Но лучше поздно, чем никогда ☺️
Vegas_Real
15.08.2024 10:51Сразу с импортов... Блин ну почитайте как они пишутся! Просто import pygame as pg и все! Зачем вся остальная чепуха?
LunarBirdMYT
15.08.2024 10:51+1Вечер прошел продуктивно :) А вы планируете дальше улучшать этот код? Местами я вижу очень похожие моменты, возможно можно было бы создать отдельные классы для фигур с какими-нибудь общими методами. Это сделает код более читаемым и вам возможно будет проще понять его логику спустя время. Так можно еще и ООП подтянуть - как идея для второй статейки. Хорошо бы еще в папку на яндексе докинуть файл с зависимостями для скрипта.
RUSTIK1023 Автор
15.08.2024 10:51+1Спасибо. Да, похоже, придётся делать вторую статью. Уже вижу, что мне есть чему научиться
GospodinKolhoznik
Вам просто обязательно нужно освоить декомпозицию функций. Это самый главный ключ к написанию хорошо структурированного кода.
RUSTIK1023 Автор
Спасибо, почитаю на досуге