Недавно я выложил свою первую статью, в которой рассказал об опыте создания шахмат на двоих на 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, с имитирует взятие на проходе, и проверит короля на шах. Но перед этим пара важных для понимания вещей:

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

  2. Напомню, что 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 >>> можно играть

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


  1. SuharkovMP
    20.08.2024 08:26
    +1

    def check_shah

    шах = check, а у вас сейчас функция проверки персидского короля.. на что? )


    1. RUSTIK1023 Автор
      20.08.2024 08:26

      Спасибо за поправку, не знал. Но я боюсь, что если функция будет называться check_check, то смысл будет понятен ещё меньше


      1. SuharkovMP
        20.08.2024 08:26

        Возьмите как аналог следующую свою функцию

        def checkmate_stalemate(B_W)

        А еще у вас не учтено правило 50 ходов.


        1. andyshark1974
          20.08.2024 08:26

          И еще - недостаточно материала для мата :-)


  1. andyshark1974
    20.08.2024 08:26
    +1

    Я в своем проекте делал что-то подобное, и пришел к выводу что нельзя рассматривать фигуры с отрывом от позиции на доске. Вы кстати на этом же попались когда делали рокировку и взятие на проходе. В итоге (сугубо мое мнение) получилось некрасиво - помесь одного с другим. Т.е. Вы сначала делаете проверку на возможность хода фигуры, потом выделяете короля в отдельную сущность, потом выделяете пешку в отдельную сущность и... Вот это и некрасиво.

    Далее. Вы проверяете только пат. Но в шахматах есть кроме пата еще правила на ничью - трехкратное повторение позиции, недостаточно материала для мата, правило 50 ходов. И если последнее правило Вы можете обойти если не пишите партию, то два остальных как обойти? Это полноценные правила которые Вы игнорируете, а ведь они эквивалентны пату.

    Еще про рокировку. Либо я недоглядел, но где проверка того ходила фигура или нет? Ведь это тоже запрет рокировки.

    Из рекомендаций. Я бы посоветовал хранить FEN позицию, тогда проверка возможности рокировки и взятие на проходе упрощается резко и в разы.


    1. RUSTIK1023 Автор
      20.08.2024 08:26

      Да, рокировка и взятие на проходе, действительно, оформлены не очень красиво, признаю.
      На счёт условий для ничьи Вы правы. Я поленился их добавлять, расчитывая на то, что игроки договорятся сами
      А на счёт условия для рокировки о котором Вы написали, оно есть. У каждой фигуры есть параметр already_moved. Он проверяется для ладьи и короля.
      А так, спасибо за критику, попытаюсь добавить то, о чём вы написали когда появятся силы.


      1. andyshark1974
        20.08.2024 08:26
        +1

        Легче сразу работать по FEN-записи на самом деле. Я у себя ее просто загружаю и разбираю по свойствам внутри объекта "Шахматная доска". Просто нельзя рассматривать фигуры в отрыве от доски, это единое целое именно из-за рокировки и взятия на проходе, увы. И всякие примеры типа "наследования" в шахматной реальности не работают :-) потому что без доски - толку ноль.

        Кстати, если хотите увидеть интересный код, посмотрите реализацию cm-chessboard на js.