Недавно я выложил свою первую статью, в которой рассказал об опыте создания шахмат на двоих на pygame. Там я встретил большое количество объективной критики: ошибок было действительно много. И поэтому сегодня попытаюсь исправить все свои косяки, написав чистый и структурированный код с учётом всей критики. Приступим
Создадим матрицу соответствующую доске, где точка - пустая клетка
Board=[['.']*8 for y in range(8)]
Теперь создадим класс фигур
class ChessPiece(): def __init__(self, name, color): self.color=color self.already_moved=False self.name=name def __str__(self): return self.name+self.color
Для создания экземпляра класса будем вписывать имя и цвет. Имя - латинское обозначение фигуры в стандартной шахматной нотации (исключение: P-пешка). Для обозначения цвета примем 0 за белый, 1 - за чёрный.
Мы уже можем добавлять в матрицу доски экземпляры класса ChessPiece, но сделаем это позже.
Хорошо бы создать в классе метод, возвращающий список доступных фигуре ходов. Для этого создадим словарь attack_dict. Сейчас объясню, зачем он
attack_dict={'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], 'N':[[1,2],[2,1],[-1,-2],[-2,-1],[-1,2],[-2,1],[1,-2],[2,-1],0], 'K':[[1,1],[-1,-1],[1,-1],[-1,1],[0,1],[1,0],[0,-1],[-1,0],0],}
Объясняю: введя имя фигуры в качестве ключа для словаря получим список. Все значения кроме последнего - направление атаки, они показывают смещение по X и Y которое нужно совершить для получения клетки, которую атакует фигура. Последнее же значение показывает длину атаки. Если 1, то фигура атакует на всё поле, если 0, то единожды.
Пора добавить в класс метод get_moves, он вернёт список доступных фигуре ходов.
Этот же метод в последующем будем использовать для проверки на шах. Значит, если обнаружим, что атакуем вражеского короля, то вернём True вместо списка ходов, ведь возможность срубить короля в игре всё равно не представляется
def get_moves(self, x, y): moves=[] piece=Board[y][x] attack=attack_dict[piece.name][0:-1] for shift in attack: #shift - смещение о котором говорил ранее pos=[x,y] for i in range(attack_dict[piece.name][-1]*6+1): #если атака на всё поле, то цикл повторится 7 раз, иначе - 1 pos[0]+=shift[0] pos[1]+=shift[1] if pos[0]>7 or pos[0]<0 or pos[1]>7 or pos[1]<0: break #вышли за поле-стоп under_attack=Board[pos[1]][pos[0]] if under_attack!='.': if under_attack.name=='K' and under_attack.color!=piece.color:return True #если бьём короля, вернём True elif under_attack.color!=piece.color: moves.append(pos[:]) break moves.append(pos[:]) return moves
Обратите внимание, что пешки - существа странные. Их ходы и необычные свойства слишком выбиваются, относительно остальных фигур. По этой причине их нет в attack_dict. Значит, для пешек мы создадим свой подкласс Pawn и свой метод get_moves
class Pawn(ChessPiece): def __init__(self, name, color): self.color=color self.already_moved=False self.name=name def get_moves(self, x, y): moves=[] pos=[x,y] if self.color=='1': y+=1 else: y-=1 x-=1 #сместимся по диагонали for i in range(2): if 7>=y>=0 and 7>=x>=0: if Board[y][x]!='.' and Board[y][x].color!=self.color: moves.append([x,y][:]) #если в клетке стоит враг-добавить, как вариант хода if Board[y][x].name=='K': return True #проверка на шах x+=2 #проверим другую диагональ x,y=pos[0], pos[1] #вернём x и y старые значения for i in range(2-self.already_moved): #добавим ходы без взятия (если пешку ещё не трогали, то 2 клетки сразу) if self.color=='1': y+=1 else: y-=1 if y>7 or y<0: break if Board[y][x]!='.': break moves.append([x,y][:]) return moves
Сделаем функцию - проверку на шах
def check_shah(B_W): #если B_W равен 0, то интересует шах белым, 1-чёрным for y in range(8): for x in range(8): if Board[y][x]!='.' and Board[y][x].color!=B_W: if Board[y][x].get_moves(x,y)==True: return True return False
Но не все полученные ходы с функции get_moves приемлемы. Нужно отбросить те, после которых король под шахом. Напишем соответствующую функцию
def filter_moves(x,y): piece=Board[y][x] moves=piece.get_moves(x,y) Board[y][x]='.' #уберем фигуру с поля for_deletion=[] for move in moves: remember=Board[move[1]][move[0]] #запомним клетку, куда сейчас поставим фигуру Board[move[1]][move[0]]=piece #ставим if check_shah(piece.color): for_deletion.append(move) #если король под шахом-записать этот ход на удаление Board[move[1]][move[0]]=remember #возвращаем всё как было Board[y][x]=piece for delet in for_deletion: #удалим лишние ходы moves.remove(delet) return moves
На очереди функция проверки на мат или пат
def checkmate_stalemate(B_W): for y in range(8): for x in range(8): if Board[y][x]!='.' and Board[y][x].color==B_W: if filter_moves(x,y)!=[]: return None #ходы есть, значит мата/пата нет if check_shah(B_W): return 1 #мат return 0 #пат
Основные функции почти готовы. Давайте займемся импортом библиотек, созданием окна и игрового цикла
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=True while game: for e in event.get(): if e.type==QUIT: game=False clock.tick(60)
Не хватает отрисовки доски. Добавим в папку с игрой картинки, называющиеся в соответствии с названием и цветом фигуры. Пример: N0.png - белый конь, Q1.png - черная королева, P1.png-чёрная пешка и т.д.
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 draw_board(): pygame.draw.rect(wind, (181, 136, 99), (0, 0, 640, 640)) #одна большая черная клетка for R in RectList: pygame.draw.rect(wind, ((240, 217, 181)), R) #много маленьких белых клеток for y in range(8): for x in range(8): if Board[y][x]!='.': wind.blit(transform.scale(pygame.image.load(Board[y][x].__str__()+'.png'),(70,70)),(5+x*80,5+y*80)) #рисуем фигуры display.update()
Ещё немного декора: добавим функцию, рисующую кружочки-подсказки для хода
def draw_circles(moves): for circle in moves: pygame.draw.circle(wind, (200,200,200), (circle[0]*80+40, circle[1]*80+40), 10) display.update()
Теперь функция, выводящая на экран победителя или ничью
def try_print_winner(turn): check=checkmate_stalemate(str(turn)) if check!=None: #если мат или пат, выведем это на экран draw_board() if check==1 and turn==0: wind.blit(pygame.font.SysFont(None,30).render('BLACK WON', False,(30, 30, 30)),(260,310)) elif check==1 and turn==1: wind.blit(pygame.font.SysFont(None,30).render('WHITE WON', False,(30, 30, 30)),(260,310)) else: wind.blit(pygame.font.SysFont(None,30).render('DRAW', False,(30, 30, 30)),(290,310)) display.update()
Займемся перемещением фигур игроком. Для простоты будем хватать фигуру нажатием мыши и перемещать отжатием. Вот как выглядит игровой цикл сейчас:
turn=0 #turn показывает, чья очередь draw_board() game=True while game: for e in event.get(): if e.type==QUIT: game=False if e.type==pg.MOUSEBUTTONDOWN and e.button==1: #Если нажата ЛКМ x_from,y_from=(e.pos) x_from,y_from=math.floor(x_from/80),math.floor(y_from/80) if Board[y_from][x_from]!='.' and Board[y_from][x_from].color==str(turn): moves=filter_moves(x_from,y_from) #получаем ходы для фигуры draw_circles(moves) else: x_from=-1 else: x_from=-1 if e.type==pg.MOUSEBUTTONUP and e.button==1 and x_from!=-1: #если отжата ЛКМ x_to,y_to=(e.pos) x_to,y_to=math.floor(x_to/80),math.floor(y_to/80) if moves.count([x_to,y_to]): #если ход приемлем, переставляем фигуру Board[y_to][x_to]=Board[y_from][x_from] Board[y_to][x_to].already_moved=True Board[y_from][x_from]='.' turn=1-turn #меняем ход draw_board() try_print_winner(turn) clock.tick(60)
Цикл тяжеловато читается. Исправим, раскинув строчки по функциям
def grab_piece(x,y): #взять фигуру piece=Board[y][x] moves=[] if piece=='.' or piece.color!=str(turn): return [] moves=filter_moves(x,y) return moves def put_piece(x_to,y_to,x_from,y_from,moves): #поставить фигуру if moves.count([x_to,y_to]): #если ход приемлем, переставляем фигуру Board[y_to][x_to]=Board[y_from][x_from] Board[y_to][x_to].already_moved=True Board[y_from][x_from]='.' return True #флаг, который подскажет сменить ход
А вот и простой, понятный игровой цикл:
while game: for e in event.get(): if e.type==QUIT: game=False if e.type==pg.MOUSEBUTTONDOWN and e.button==1: #Если нажата ЛКМ x_from,y_from=(e.pos) x_from,y_from=math.floor(x_from/80),math.floor(y_from/80) moves=grab_piece(x_from,y_from) draw_circles(moves) if e.type==pg.MOUSEBUTTONUP and e.button==1: #если отжата ЛКМ x_to,y_to=(e.pos) x_to,y_to=math.floor(x_to/80),math.floor(y_to/80) if put_piece(x_to,y_to,x_from,y_from,moves): turn=1-turn #меняем ход draw_board() try_print_winner(turn) clock.tick(60)
Кстати, помните функцию checkmate_stalemate? Это проверка на мат или пат. Так вот, она использует другую функцию - filter_moves для получения списка ходов. Я заменю filter_moves на функцию grab_piece (сейчас она делает почти тоже самое) так как скоро мы добавим в grab_piece ещё ходы и filter_moves потеряет актуальность.
Это было важное отступление, а сейчас добавим все фигуры на доску
for i in range(8): Board[0][i]=ChessPiece('RNBQKBNR'[i],'1') Board[1][i]=Pawn('P','1') Board[7][i]=ChessPiece('RNBQKBNR'[i],'0') Board[6][i]=Pawn('P','0')
Игра готова за исключением трёх вещей. Не хватает рокировки, взятия на проходе и превращения пешки по достижению края доски. Рекомендую почитать, как совершаются эти ходы по ссылке, если вы не в курсе.
Рокировка-ход королём, да при том весьма необычный. Так что давайте создадим отдельный подкласс для короля, за одно напишем функцию, добавляющую рокировку
class King(ChessPiece): def add_castling(self): castlings_list=[] if self.already_moved==False: y=7-int(self.color)*7 rooks_cond=[False,False] #проверка ладей for x in range(2): check_piece=Board[y][x*7] if check_piece!='.': if check_piece.name==('R') and check_piece.color==self.color and check_piece.already_moved==False: rooks_cond[x]=True reach_cond=[False,False] #проверка места между королём и ладьями for x in range(2): if x==0 and Board[y][1:4]==['.','.','.']: reach_cond[0]=True elif x==1 and Board[y][5:7]==['.','.']: reach_cond[1]=True shah_cond=[False,False] #проверка шаха for x in range(2): #будем ставить временных королей, если они под шахом-условие не соблюдено if x==0 and reach_cond[x]: Board[y][2],Board[y][3]=King('K',self.color),King('K',self.color) if check_shah(self.color)==False: shah_cond[x]=True Board[y][2],Board[y][3]='.','.' elif x==1 and reach_cond[x]: Board[y][5],Board[y][6]=King('K',self.color),King('K',self.color) if check_shah(self.color)==False: shah_cond[x]=True Board[y][5],Board[y][6]='.','.' all_cond=(shah_cond[0] and rooks_cond[0],shah_cond[1] and rooks_cond[1]) if all_cond[0]: castlings_list.append([2,y]) if all_cond[1]: castlings_list.append([6,y]) return castlings_list
Пока что, метод add_castling проверяет все условия для рокировки, затем возвращает клетки, куда нужно сходить королю для рокировки. Давайте дадим королю возможность ходить на эти клетки. Для этого добавим в конец функции grab_piece следующие строчки:
if piece.name=='K': global castlings #castlings ещё понадобится, поэтому создадим его перед игр. циклом, а тут глобализуем castlings=piece.add_castling() for c in castlings: moves.append(c)
Теперь надо заставить двигаться ладью. Для этого добавим королю метод move_rook. Его будем вызывать перед тем, как двигать короля
def move_rook(self,x,y): #x,y-координаты точки, куда ходит король if x==2: #если рокировка влево Board[y][0]='.' Board[y][3]=ChessPiece('R',self.color) elif x==6: #если рокировка вправо Board[y][7]='.' Board[y][5]=ChessPiece('R',self.color)
Осталось всё связать в функции put_piece, переставляющей фигуры
def put_piece(x_to,y_to,x_from,y_from,moves): if moves.count([x_to,y_to]): #новые строчки if Board[y_from][x_from].name=='K': global castlings if castlings.count([x_to, y_to]): #если ход является рокировкой Board[y_from][x_from].move_rook(x_to,y_to) #переставим ладью Board[y_to][x_to]=Board[y_from][x_from] Board[y_to][x_to].already_moved=True Board[y_from][x_from]='.' return True
Кстати, поскольку мы вынесли короля в отдельный класс, то не забудем сделать это и при создании данной фигуры:
for i in range(8): #создание всех фигур кроме королей Board[0][i]=ChessPiece('RNBQ.BNR'[i],'1') Board[1][i]=Pawn('P','1') Board[7][i]=ChessPiece('RNBQ.BNR'[i],'0') Board[6][i]=Pawn('P','0') Board[0][4]=King('K','1') #создание двух королей Board[7][4]=King('K','0')
Рокировка готова! На очереди взятие на проходе.
Как же его реализовать? Сейчас попытаюсь объяснить свою идею. Для этого рассмотрим следующую ситуацию:

После хода белой пешки, вызовем некий метод check_en_passant класса Pawn, который, если пешка сходила на 2 клетки, запишет клетку справа и слева в список en_passant_pos. На следующем ходу вражеские пешки на этих клетках (если они там есть) смогут совершить взятие на проходе.
В en_passant_pos надо добавить ещё кое-что: клетку, отмеченную красной точкой (туда будем ходить при взятии) и клетку, где сейчас стоит белая пешка (её будем рубить)
en_passant_pos создали перед игровым циклом, а метод check_en_passant - сейчас:
def check_en_passant(x_from,y_from,x_to,y_to): if y_from-y_to==2 or y_from-y_to==-2:#если ходим на 2 клетки global en_passant_pos en_passant_pos=[[x_to-1,y_to],[x_to+1,y_to]] #клетки слева и справа от пешки en_passant_pos.append(x_to, (y_from+y_to)//2) #куда ходим при взятии? #(найдя среднее между Y до и после хода пешки, получим Y нужной клетки) en_passant_pos.append(x_to,y_to) #клетка где стоит наша пешка
По-моему, если получать значения из en_passant_pos по индексу, то это будет тяжело для восприятия, так что заменим его на словарь. Значение будем получать по "кодовому слову".
Вот как теперь выглядит check_en_passant:
def check_en_passant(self, x_from,y_from,x_to,y_to): if y_from-y_to==2 or y_from-y_to==-2:#если ходим на 2 клетки global en_passant_pos en_passant_pos['left']=[x_to-1,y_to] #клетка слева ��т пешки en_passant_pos['right']=[x_to+1,y_to] #справа en_passant_pos['move']=[x_to, (y_from+y_to)//2] #куда ходим при взятии? en_passant_pos['fellt']=[x_to,y_to] #клетка где стоит наша пешка
Теперь подумаем о том, как проверить ходы-взятия на шах после совершения. Давайте создадим метод en_passant_filt. Он, благодаря всем данным из en_passant_pos, с имитирует взятие на проходе, и проверит короля на шах. Но перед этим пара важных для понимания вещей:
Для совершения взятия на проходе нужно 3 действия: 1 - убрать рубящую пешку, 2 - поставить рубящую пешку в другую точку, 3 - убрать срубленную пешку.
При чём порядок выполнения неважен. Понимание этого сейчас пригодится, ведь мы сначала выполним пункт 2 и 3, и только после этого - 1Напомню, что en_passant_pos хранит координаты клеток, пешкам в которых можно совершать взятие. Так вот, е��ли нужно запретить данное действие, то будем заменять координаты клетки на [-1, -1] (несуществующие)
А вот и функция en_passant_filt:
def en_passant_filt(self): pos=en_passant_pos['fellt'] Board[pos[1]][pos[0]]='.' #уберём нашу пешку (пункт 3) pos=en_passant_pos['move'] enemy_color=str(1-int(self.color)) Board[pos[1]][pos[0]]=Pawn('P',enemy_color) #поставим вражескую пешку (пункт 2) piece=Board[pos[1]][pos[0]] for cell in ['left','right']: #здесь выполним пункт 1 pos=en_passant_pos[cell] piece=Board[pos[1]][pos[0]] if piece=='.': en_passant_pos[cell]=[-1,-1]; continue if piece.name!='P' or piece.color==self.color: en_passant_pos[cell]=[-1,-1]; continue #если в проверяемой клетке не вражеская пешка-запрещаем взятие Board[pos[1]][pos[0]]='.' #срубим пешку (пункт 1) if check_shah(enemy_color): en_passant_pos[cell]=[-1,-1] #если после пункта 1 шах-запрещаем взятие #если все этапы проверки пройдены, то не трогаем проверяемую клетку (она может совершить взятие) Board[pos[1]][pos[0]]=Pawn('P', enemy_color) #вернём фигуру Board[pos[1]][pos[0]].already_moved=True pos=en_passant_pos['fellt'] #вернём всё как было Board[pos[1]][pos[0]]=Pawn('P',self.color) Board[pos[1]][pos[0]].already_moved=True pos=en_passant_pos['move'] Board[pos[1]][pos[0]]='.'
Теперь начнём вызывать en_passant_filt в конце check_en_passant:
self.en_passant_filt()
Поздравляю, самое сложное позади. Теперь надо начать вызывать check_en_passant после каждого хода пешкой. Для этого допишем в конец функции put_piece следующее:
global en_passant_pos en_passant_pos={} #будем очищать en_passant_pos каждый ход if Board[y_to][x_to].name=='P': Board[y_to][x_to].check_en_passant(x_from,y_from,x_to,y_to)
По взятию на проходе осталось всего ничего. Надо добавить пешке, которая получит разрешение на взятие, возможность его совершить. Допишем следующее в grab_piece:
if piece.name=='P': global en_passant_pos if len(en_passant_pos)>0: if en_passant_pos['left']==[x,y] or en_passant_pos['right']==[x,y]: moves.append(en_passant_pos['move'])
Сейчас пешки просто научились ходить по диагонали на пустые клетки, но взятие на проходе не зря называется взятием, начнём рубить пешку. Для этого надо дописать чуть-чуть кода в put_piece. Сделаем это:
def put_piece(x_to,y_to,x_from,y_from,moves): if moves.count([x_to,y_to]): if Board[y_from][x_from].name=='K': global castlings if castlings.count([x_to, y_to]): Board[y_from][x_from].move_rook(x_to,y_to) Board[y_to][x_to]=Board[y_from][x_from] Board[y_to][x_to].already_moved=True Board[y_from][x_from]='.' global en_passant_pos #новые строчки if Board[y_to][x_to].name=='P': if len(en_passant_pos)>0: if en_passant_pos['move']==[x_to,y_to]: pos=en_passant_pos['fellt'] Board[pos[1]][pos[0]]='.' en_passant_pos={} if Board[y_to][x_to].name=='P': Board[y_to][x_to].check_en_passant(x_from,y_from,x_to,y_to) return True
Наконец-то мы разобрались со взятием. Давайте подумаем о превращении пешки. Когда пешка будет доходить до края доски, нужно как-то останавливать цикл смены ходов и запускать выбор фигуры для превращения.
Для этого вспомним про переменную turn. Она принимает 0 или 1 в зависимости от того, чей ход. Так вот, предлагаю, для события превращения пешки, задавать turn значение -1. Тогда никто не сможет совершить ход, а мы реализуем свои коварные планы.
Находить пешку на краю доски и задавать turn значение -1 будем при помощи функции find_border_pawn:
def find_border_pawn(): y=-1 for x in range(8): #ищем пешку на краю доски if Board[0][x]!='.' and Board[0][x].name=='P': y=0; break if Board[7][x]!='.' and Board[7][x].name=='P': y=7; break if y!=-1: global turn turn=-1 #по углам клетки, на которой стоит пешка, нарисуем ферзя, ладью, сл��на и коня if Board[y][x].color=='0': wind.blit(transform.scale(pygame.image.load('Q0.png'),(40,40)),(x*80,y*80)) wind.blit(transform.scale(pygame.image.load('R0.png'),(40,40)),(x*80+40,y*80)) wind.blit(transform.scale(pygame.image.load('B0.png'),(40,40)),(x*80,y*80+40)) wind.blit(transform.scale(pygame.image.load('N0.png'),(40,40)),(x*80+40,y*80+40)) if Board[y][x].color=='1': wind.blit(transform.scale(pygame.image.load('Q1.png'),(40,40)),(x*80,y*80)) wind.blit(transform.scale(pygame.image.load('R1.png'),(40,40)),(x*80+40,y*80)) wind.blit(transform.scale(pygame.image.load('B1.png'),(40,40)),(x*80,y*80+40)) wind.blit(transform.scale(pygame.image.load('N1.png'),(40,40)),(x*80+40,y*80+40)) display.update() return x,y
Кстати, пока пешка на краю доски, событие отжатия мыши нам не нужно. Поэтому я добавил в него условие, что turn не равен -1. Тут смотреть не на что.
Итак, сейчас мы имеем функцию, которая находит пешку на краю доски, рисует вокруг неё нужные фигуры, задаёт turn значение -1 и возвращает координаты пешки. Я немного поработал с этими координатами и вот как теперь выглядит игровой цикл:
while game: for e in event.get(): if e.type==QUIT: game=False if e.type==pg.MOUSEBUTTONDOWN and e.button==1: x_from,y_from=(e.pos) x_from,y_from=math.floor(x_from/80),math.floor(y_from/80) if turn==-1 and (x_pawn,y_pawn)==(x_from,y_from): #если нажали на пешку на краю доски replace_pawn(x_pawn,y_pawn) #эту функцию ещё не показал moves=grab_piece(x_from,y_from) draw_circles(moves) if e.type==pg.MOUSEBUTTONUP and e.button==1 and turn!=-1: x_to,y_to=(e.pos) x_to,y_to=math.floor(x_to/80),math.floor(y_to/80) if put_piece(x_to,y_to,x_from,y_from,moves): turn=1-turn draw_board() try_print_winner(turn) #новая строчка-запоминаем координаты пешки x_pawn,y_pawn=find_border_pawn() clock.tick(60)
Функция replace_pawn - вот финальный кусок кода, который я вам покажу:
def replace_pawn(x_pawn,y_pawn): x,y=(e.pos) x,y=math.floor((x%80)/40), math.floor((y%80)/40) #теперь x и y отображают угол в который нажали, по ним поймём во что превратить пешку piece_dict={ (0,0): 'Q', (1,0): 'R', (0,1): 'B', (1,1): 'N'} piece_name=piece_dict[(x,y)] piece_color=Board[y_pawn][x_pawn].color Board[y_pawn][x_pawn]=ChessPiece(piece_name,piece_color) global turn turn=int(piece_color) turn=1-turn draw_board()
Вот и всё! У нас получились полностью функционирующие шахматы на двоих. Надеюсь, что положительные изменения относительно прошлой статьи заметны. Если у вас есть мысли по улучшению кода или информация об ошибках, обязательно напишите об этом в комментариях
Ссылка на Яндекс Диск с игрой
Переходите по ссылке >>> нажимайте на кнопку скачать всё >>> сохраняйте HabrChess.zip >>> открывайте его >>> перетаскивайте папку Chess себе на рабочий стол или в любое место проводника >>> открываете эту папку >>> открываете Chess.py >>> можно играть
