1. Введение

1.1 Идея проекта

Всё началось с подготовки к финалу RuCode – масштабному соревнованию для всех увлечённых алгоритмическим программированием. Погружаясь в разбор заданий прошлых лет, мне кое-что совершенно случайно попало в руки, интересная задача: реализовать шахматы с "туманом войны" в консоли

Идея показалась мне настолько вдохновляющей, что я решил пойти дальше, а что если превратить эту консольную головоломку в настоящую игру с графикой и той самой атмосферой тумана войны, который так знаком игрокам в стратегиях

Идея зацепила сразу, представьте классические шахматы, но с элементом неожиданности. Вы не видите все фигуры противника – только те клетки, куда могут ступить ваши фигуры. Каждый ход превращается в тактическую загадку, что скрывается в темноте? Где спрятался вражеский ферзь? и т.д.

Так обычная подготовка к соревнованиям превратилась в увлекательный эксперимент, результатом которого стала эта статья и полная реализация шахмат с туманом войны на Python

как это выглядело в задачи rucode
как это выглядело в задачи rucode

1.2 Подготовка к разработке

Для работы потребуются библиотеки Python, и поэтому перед тем, как приступить к написанию, убедитесь, что у вас установлен Python (версия 3.12 или выше), если нет, то вы можете скачать её с официального сайта python.org. После скачивания python, нужно установить библиотеку:

pip install pygame

Структура проекта:

Cкачать фотографии фигур можно с GitHub

Chess_The_fog_of_war/
├── main.py          # Основной игровой цикл
├── chess_pieces.py  # Классы шахматных фигур
├── 0K.png       # Белый король
├── 0Q.png       # Белый ферзь
├── ...          # Остальные белые фигуры
├── 1K.png       # Черный король
└── ...          # Остальные черные фигуры

2. Основная часть

2.1 Реализация шахматных фигур

Каждая шахматная фигура реализована как отдельный класс, наследуемый от базового класса ChessPiece, такой объектно-ориентированный подход обеспечивает простоту расширения и поддержки кода. Каждый класс фигуры содержит метод get_valid_moves(), который возвращает список допустимых ходов для данной фигуры из текущей позиции

import pygame


class ChessPiece:
    """Базовый класс для всех шахматных фигур"""

    def __init__(self, color: int, symbol: str) -> None:
        """
        Инициализация шахматной фигуры

        Args:
            color: 0 - для белых, 1 - для черных
            symbol: символ фигуры (например: K - король)
        """
        self.color = color
        self.symbol = symbol
        self.has_moved = False

    def get_image_path(self) -> str:
        """Получить путь к изображению фигуры"""
        return f"{self.color}{self.symbol}.png"

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """Получить допустимые ходы для фигуры должен быть реализован в подклассах"""
        return []

    def is_opponent(self, other_piece) -> bool:
        """Проверить, является ли другая фигура фигурой противника"""
        return (other_piece is not None) and (other_piece.color != self.color)

    def is_empty_or_opponent(self, board: list, x: int, y: int) -> bool:
        """Проверить, является ли клетка пустой или содержит фигуру противника"""
        if not (0 <= x < 8 and 0 <= y < 8):
            return False
        target_piece = board[y][x]
        return target_piece is None or self.is_opponent(target_piece)

2.1.1 Пешка (Pawn)

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

Белая пешка движется вверх по доске (уменьшение координаты Y), а черная – вниз (увеличение координаты Y). Со стартовой позиции пешка может сделать двойной ход на две клетки вперед, если путь свободен. Для взятия фигур противника пешка движется по диагонали на одну клетку вперед. Особый случай - "взятие на проходе" - позволяет пешке взять пешку противника, которая только что сделала двойной ход и переместилась на соседнюю вертикаль

все варианты хода пешки
все варианты хода пешки
class Pawn(ChessPiece):
    """Класс пешки"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'P')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для пешки

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        moves = []
        direction = -1 if self.color == 0 else 1  # Белые двигаются вверх, черные вниз

        # Ход вперед (на одну клетку)
        if 0 <= y + direction < 8 and board[y + direction][x] is None:
            moves.append((x, y + direction))

            # Двойной ход с начальной позиции
            start_row = 6 if self.color == 0 else 1
            if y == start_row and board[y + 2 * direction][x] is None:
                moves.append((x, y + 2 * direction))

        # Взятия
        for dx in [-1, 1]:
            new_x, new_y = x + dx, y + direction
            if 0 <= new_x < 8 and 0 <= new_y < 8:
                target = board[new_y][new_x]
                if target is not None and self.is_opponent(target):
                    moves.append((new_x, new_y))

                # Взятие на проходе
                if en_passant == (new_x, new_y):
                    moves.append((new_x, new_y))

        return moves

    def should_promote(self, y: int) -> bool:
        """
        Проверить, должна ли пешка превратиться в другую фигуру

        Args:
            y: текущая координата y пешки

        Returns:
            bool: True, если пешка достигла последней горизонтали и False, если нет
        """
        # Белая пешка достигла верхнего края (y=0)
        if self.color == 0 and y == 0:
            return True
        # Черная пешка достигла нижнего края (y=7)
        if self.color == 1 and y == 7:
            return True
        return False

    def promote(self, piece_type: str) -> ChessPiece:
        """
        Превратить пешку в другую фигуру

        Args:
            piece_type: тип фигуры для превращения ('Q', 'R', 'B', 'N')

        Returns:
            ChessPiece: новая фигура
        """
        piece_classes = {
            'Q': Queen,
            'R': Rook,
            'B': Bishop,
            'N': Knight
        }

        if piece_type in piece_classes:
            return piece_classes[piece_type](self.color)
        else:
            return Queen(self.color)

2.1.2 Конь (Knight)

Конь единственная фигура, способная перепрыгивать через другие фигуры, его движение описывается характерной буквой "Г", он перемещается на две клетки по одной оси и на одну клетку по другой. Это создает восемь возможных направлений движения из любой точки доски

все возможные ходы коня
class Knight(ChessPiece):
    """Класс коня (обозначается как N в шахматах)"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'N')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для коня

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        moves = []
        knight_moves = [
            (2, 1), (2, -1), (-2, 1), (-2, -1),
            (1, 2), (1, -2), (-1, 2), (-1, -2)
        ]

        for dx, dy in knight_moves:
            new_x, new_y = x + dx, y + dy
            if self.is_empty_or_opponent(board, new_x, new_y):
                moves.append((new_x, new_y))

        return moves

2.1.3 Слон (Bishop)

Слон перемещается исключительно по диагоналям на любое количество клеток до тех пор, пока не встретит препятствие. Каждый слон остается на клетках одного цвета на протяжении всей игры

все возможные ходы слона
все возможные ходы слона
class Bishop(ChessPiece):
    """Класс слона"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'B')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для слона

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        moves = []
        directions = [(1, 1), (1, -1), (-1, 1), (-1, -1)]

        for dx, dy in directions:
            for i in range(1, 8):
                new_x, new_y = x + i * dx, y + i * dy
                if not (0 <= new_x < 8 and 0 <= new_y < 8):
                    break

                if board[new_y][new_x] is None:
                    moves.append((new_x, new_y))
                elif self.is_opponent(board[new_y][new_x]):
                    moves.append((new_x, new_y))
                    break
                else:
                    break

        return moves

2.1.4 Ладья (Rook)

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

все возможные ходы ладьи
все возможные ходы ладьи
class Rook(ChessPiece):
    """Класс ладьи"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'R')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для ладьи

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        moves = []
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]

        for dx, dy in directions:
            for i in range(1, 8):
                new_x, new_y = x + i * dx, y + i * dy
                if not (0 <= new_x < 8 and 0 <= new_y < 8):
                    break

                if board[new_y][new_x] is None:
                    moves.append((new_x, new_y))
                elif self.is_opponent(board[new_y][new_x]):
                    moves.append((new_x, new_y))
                    break
                else:
                    break

        return moves

2.1.5 Ферзь (Queen)

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

все возможные ходы ферзя
все возможные ходы ферзя
class Queen(ChessPiece):
    """Класс ферзя"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'Q')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для ферзя

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        # Ферзь ходит как ладья + слон
        rook_moves = Rook(self.color).get_valid_moves(board, x, y)
        bishop_moves = Bishop(self.color).get_valid_moves(board, x, y)
        return rook_moves + bishop_moves

2.1.6 Король (King)

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

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

все возможные ходы короля
все возможные ходы короля
class King(ChessPiece):
    """Класс короля"""

    def __init__(self, color: int) -> None:
        super().__init__(color, 'K')

    def get_valid_moves(self, board: list, x: int, y: int, en_passant: tuple = None) -> list:
        """
        Получить допустимые ходы для короля

        Args:
            board: шахматная доска
            x: текущая координата x
            y: текущая координата y
            en_passant: координаты для взятия на проходе

        Returns:
            list: список допустимых ходов
        """
        moves = []
        king_moves = [
            (1, 0), (-1, 0), (0, 1), (0, -1),
            (1, 1), (1, -1), (-1, 1), (-1, -1)
        ]

        for dx, dy in king_moves:
            new_x, new_y = x + dx, y + dy
            if self.is_empty_or_opponent(board, new_x, new_y):
                moves.append((new_x, new_y))

        # Рокировка
        if not self.has_moved:
            # Короткая рокировка
            if (board[y][x + 1] is None and board[y][x + 2] is None and
                    isinstance(board[y][x + 3], Rook) and not board[y][x + 3].has_moved):
                moves.append((x + 2, y))

            # Длинная рокировка
            if (board[y][x - 1] is None and board[y][x - 2] is None and
                    board[y][x - 3] is None and
                    isinstance(board[y][x - 4], Rook) and not board[y][x - 4].has_moved):
                moves.append((x - 2, y))

        return moves

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

2.2 Основной игровой механизм

2.2.1 Класс ChessGame - архитектурное ядро приложения

Управление состоянием и инициализация: Проект начинается с инициализации игрового движка Pygame и настройки основных параметров отображения. Определяются ключевые константы, такие как размер окна, количество клеток на доске и цветовая схема, которая включает как традиционные шахматные цвета клеток, так и специальные цвета для подсветки и эффектов

import pygame
from chess_pieces import Pawn, Knight, Bishop, Rook, Queen, King

# Инициализация pygame
pygame.init()

# Константы
window_size = 640
board_size = 8
square_size = window_size // board_size
fps = 60

# Цвета
dark_square = (181, 136, 99)
light_square = (240, 217, 181)
highlight = (100, 249, 83, 150)
move_highlight = (200, 200, 200, 100)
check_highlight = (255, 0, 0, 150)
fog_of_war = (0, 0, 0)
promotion_background = (50, 50, 50, 200)

# Настройка дисплея
screen = pygame.display.set_mode((window_size, window_size))
pygame.display.set_caption('Шахматы')
clock = pygame.time.Clock()

# Инициализация шрифта
pygame.font.init()


class ChessGame:
    """Основной класс шахматной игры"""

    def __init__(self) -> None:
        """Инициализация шахматной игры"""
        self.board = [[None for _ in range(board_size)] for _ in range(board_size)]
        self.selected_piece = None
        self.valid_moves = []
        self.current_player = 0  # 0 для белых, 1 для черных
        self.game_over = False
        self.check = False
        self.en_passant = None
        self.promotion_pending = None
        self.castling_rights = {
            'white_king_side': True,
            'white_queen_side': True,
            'black_king_side': True,
            'black_queen_side': True
        }
        self.initialize_board()

    def initialize_board(self) -> None:
        """Начальная расстановка фигур на доске"""
        # Пешки
        for i in range(board_size):
            self.board[1][i] = Pawn(1)  # Черные пешки
            self.board[6][i] = Pawn(0)  # Белые пешки

        # Остальные фигуры
        back_row_order = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]

        # Черные фигуры (верх)
        for i, piece_class in enumerate(back_row_order):
            self.board[0][i] = piece_class(1)

        # Белые фигуры (низ)
        for i, piece_class in enumerate(back_row_order):
            self.board[7][i] = piece_class(0)

Использование матрицы 8×8 обеспечивает интуитивный доступ к фигурам через координаты [строка][столбец]

2.2.2 Визуализация игрового процесса

Метод draw_board() создает шахматную доску с чередующимися светлыми и темными клетками, используя вычисление (row + col) % 2 для определения цвета каждой клетки

Метод draw_pieces() проходит по всем клеткам доски и отображает фигуры, используя заранее подготовленные изображения ( важный момент, тк их нужно скачать или найти самому похожие). Важной особенностью является обработка исключений, если изображение недоступно, система автоматически переключается на текстовое представление фигур, обеспечивая надежность работы приложения

    def draw_board(self) -> None:
        """Отрисовка шахматной доски"""
        # Отрисовка клеток
        for row in range(board_size):
            for col in range(board_size):
                color = light_square if (row + col) % 2 == 0 else dark_square
                pygame.draw.rect(screen, color,
                                 (col * square_size, row * square_size,
                                  square_size, square_size))

    def draw_pieces(self) -> None:
        """Отрисовка всех фигур на доске"""
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece:
                    try:
                        image = pygame.image.load(piece.get_image_path())
                        image = pygame.transform.scale(image, (square_size - 10, square_size - 10))
                        screen.blit(image, (col * square_size + 5, row * square_size + 5))
                    except pygame.error:
                        # Запасной вариант если картинок нет
                        font = pygame.font.SysFont(None, 36)
                        text_color = (255, 255, 255) if piece.color == 1 else (0, 0, 0)
                        text = font.render(piece.symbol, True, text_color)
                        screen.blit(text, (col * square_size + 20, row * square_size + 20))

Отрисовка меню превращения пешки2.2.3 Система превращения пешки

Особое внимание уделено механизму превращения пешки. При достижении последней горизонтали игра переходит в специальное состояние, активируя меню выбора фигуры. Метод draw_promotion_menu() создает интуитивный интерфейс с крупными иконками фигур и подписями

    def draw_promotion_menu(self) -> None:
        """Отрисовка меню превращения пешки"""
        if not self.promotion_pending:
            return

        x, y = self.promotion_pending
        color = self.board[y][x].color

        menu_width = square_size * 2
        menu_height = square_size * 4
        menu_x = (window_size - menu_width) // 2
        menu_y = (window_size - menu_height) // 2

        # Рисуем фон меню
        menu_bg = pygame.Surface((menu_width, menu_height), pygame.SRCALPHA)
        menu_bg.fill((240, 240, 240, 240))
        screen.blit(menu_bg, (menu_x, menu_y))

        # Заголовок меню
        font_title = pygame.font.SysFont(None, 32)
        title_text = "Выберите фигуру для превращения"
        title_surface = font_title.render(title_text, True, (255, 255, 255))
        title_width = title_surface.get_width() + 20
        title_height = title_surface.get_height() + 10
        title_bg = pygame.Surface((title_width, title_height), pygame.SRCALPHA)
        title_bg.fill((0, 0, 0, 200))
        title_rect = title_surface.get_rect(center=(window_size // 2, menu_y - 25))
        title_bg_rect = title_bg.get_rect(center=title_rect.center)
        screen.blit(title_bg, title_bg_rect)
        screen.blit(title_surface, title_rect)

        # Фигуры для превращения
        pieces = [Queen, Rook, Bishop, Knight]
        piece_names = ["Ферзь", "Ладья", "Слон", "Конь"]

        for i, (piece_class, piece_name) in enumerate(zip(pieces, piece_names)):
            piece_y = menu_y + i * (menu_height // 4)

            # Подсветка при наведении
            mouse_x, mouse_y = pygame.mouse.get_pos()
            if (menu_x <= mouse_x <= menu_x + menu_width and
                    piece_y <= mouse_y <= piece_y + menu_height // 4):
                highlight_surface = pygame.Surface((menu_width, menu_height // 4), pygame.SRCALPHA)
                highlight_surface.fill((100, 150, 255, 100))
                screen.blit(highlight_surface, (menu_x, piece_y))

            # Разделительная линия
            if i > 0:
                pygame.draw.line(screen, (150, 150, 150),
                                 (menu_x, piece_y),
                                 (menu_x + menu_width, piece_y), 2)

            # Отображение фигуры
            temp_piece = piece_class(color)

            try:
                image_size = (menu_height // 4 - 20, menu_height // 4 - 20)
                image = pygame.image.load(temp_piece.get_image_path())
                image = pygame.transform.scale(image, image_size)
                screen.blit(image, (menu_x + 15, piece_y + 10))
            except pygame.error:
                font_symbol = pygame.font.SysFont(None, 48)
                symbol_color = (0, 0, 0) if color == 0 else (255, 255, 255)
                symbol_bg_color = (255, 255, 255) if color == 1 else (0, 0, 0)

                # Фон для символа
                symbol_bg = pygame.Surface((40, 40), pygame.SRCALPHA)
                symbol_bg.fill(symbol_bg_color)
                screen.blit(symbol_bg, (menu_x + 10, piece_y + 5))

                symbol_surface = font_symbol.render(temp_piece.symbol, True, symbol_color)
                symbol_rect = symbol_surface.get_rect(center=(menu_x + 30, piece_y + menu_height // 8))
                screen.blit(symbol_surface, symbol_rect)

            # Отображаем название фигуры
            font_text = pygame.font.SysFont(None, 28)
            name_surface = font_text.render(piece_name, True, (0, 0, 0))
            name_rect = name_surface.get_rect(midleft=(menu_x + 80, piece_y + menu_height // 8))
            screen.blit(name_surface, name_rect)

2.2.4 Интерактивные элементы интерфейса

Система подсветки реализована через метод draw_highlights(), который визуально выделяет выбранную фигуру зеленым прямоугольником и отображает возможные ходы в виде серых кружков. Дополнительно реализована подсветка клетки под курсором мыши

Туман войны, уникальная особенность данной реализации, рисуется методом draw_fog_of_war(). Алгоритм создает черную поверхность, покрывающую всю доску, а затем "вырезает" в ней области, соответствующие позициям фигур текущего игрока и всем возможным ходам этих фигур. Это создает эффект ограниченной видимости, характерный для тактических игр

    def draw_highlights(self) -> None:
        """Отрисовка подсветки выбранной фигуры и допустимых ходов"""
        if self.promotion_pending:
            return

        # Подсветка выбранной фигуры
        if self.selected_piece:
            x, y = self.selected_piece
            highlight_surface = pygame.Surface((square_size, square_size), pygame.SRCALPHA)
            highlight_surface.fill(highlight)
            screen.blit(highlight_surface, (x * square_size, y * square_size))

            # Подсветка позиции курсора
            mouse_x, mouse_y = pygame.mouse.get_pos()
            grid_x, grid_y = mouse_x // square_size, mouse_y // square_size
            if 0 <= grid_x < board_size and 0 <= grid_y < board_size:
                cursor_surface = pygame.Surface((square_size, square_size), pygame.SRCALPHA)
                cursor_surface.fill(move_highlight)
                screen.blit(cursor_surface, (grid_x * square_size, grid_y * square_size))

        # Отрисовка допустимых ходов
        for x, y in self.valid_moves:
            pygame.draw.circle(screen, (200, 200, 200),
                               (x * square_size + square_size // 2,
                                y * square_size + square_size // 2),
                               10)

    def draw_fog_of_war(self) -> None:
        """Отрисовка тумана войны - показываются все клетки, куда могут пойти фигуры текущего игрока за один ход"""
        # Создаем поверхность для тумана войны
        fog_surface = pygame.Surface((window_size, window_size))
        fog_surface.fill(fog_of_war)

        # Определяем видимые области для текущего игрока
        visible_areas = []

        # Добавляем позиции всех фигур текущего игрока
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece is not None and piece.color == self.current_player:
                    visible_areas.append((col * square_size, row * square_size, square_size, square_size))

                    # Показываем возможные ходы из этой позиции
                    moves = piece.get_valid_moves(self.board, col, row, self.en_passant)
                    for move_x, move_y in moves:
                        visible_areas.append((move_x * square_size, move_y * square_size, square_size, square_size))

        # Вырезаем видимые области из тумана войны
        for area in visible_areas:
            pygame.draw.rect(fog_surface, (255, 255, 255), area)

        fog_surface.set_colorkey((255, 255, 255))
        screen.blit(fog_surface, (0, 0))

2.2.5 Система проверки шахов и матов

Одной из ключевых особенностей реализации является система обнаружения шаха. Метод draw_check_indicator() визуально выделяет короля, находящегося под шахом, с помощью красной подсветки. Этот визуальный сигнал помогает игрокам быстро идентифицировать ситуацию на доске

    def draw_check_indicator(self) -> None:
        """Подсветка короля при шаге"""
        # Если идет превращение пешки, не отображаем подсветку шаха
        if self.promotion_pending:
            return

        king_pos = self.find_king(self.current_player)
        if king_pos and self.is_in_check(self.current_player):
            x, y = king_pos
            check_surface = pygame.Surface((square_size, square_size), pygame.SRCALPHA)
            check_surface.fill(check_highlight)
            screen.blit(check_surface, (x * square_size, y * square_size))

    def find_king(self, color: int) -> tuple:
        """
        Найти короля указанного цвета

        Args:
            color: цвет короля (0 - белый, 1 - черный)

        Returns:
            tuple: координаты короля (x, y) или None если не найден
        """
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if isinstance(piece, King) and piece.color == color:
                    return (col, row)
        return None

    def is_in_check(self, color: int) -> bool:
        """
        Проверить, находится ли король указанного цвета под шахом

        Args:
            color: цвет короля (0 - белый, 1 - черный)

        Returns:
            bool: True если король под шахом
        """
        king_pos = self.find_king(color)
        if not king_pos:
            return False

        # Проверить, может ли любая фигура противника атаковать короля
        opponent_color = 1 - color
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece is not None and piece.color == opponent_color:
                    moves = piece.get_valid_moves(self.board, col, row, self.en_passant_target)
                    if king_pos in moves:
                        return True

        return False

Метод find_king() выполняет поиск короля указанного цвета путем перебора всех клеток доски. Этот алгоритм гарантирует точное определение позиции монарха, что является фундаментальным для последующих проверок игрового состояния

2.2.6 Механизм проверки шаховой ситуации

Сердце системы безопасности короля метод is_in_check(). Алгоритм работает в два этапа: сначала определяется позиция короля, затем проверяются все фигуры противника на возможность атаковать эту позицию. Для каждой вражеской фигуры вычисляются все возможные ходы, и если хотя бы один из них совпадает с позицией короля, констатируется шах

    def is_in_check(self, color: int) -> bool:
        """
        Проверить, находится ли король указанного цвета под шахом

        Args:
            color: цвет короля (0 - белый, 1 - черный)

        Returns:
            bool: True если король под шахом
        """
        king_pos = self.find_king(color)
        if not king_pos:
            return False

        opponent_color = 1 - color
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece is not None and piece.color == opponent_color:
                    moves = piece.get_valid_moves(self.board, col, row, self.en_passant)
                    if king_pos in moves:
                        return True

        return False

2.2.7 Система валидации ходов

Метод get_valid_moves_for_piece() обеспечивает корректное определение допустимых ходов для каждой фигуры. Алгоритм сначала получает все теоретически возможные ходы фигуры, затем фильтрует их, исключая те, которые оставляют короля под шахом

    def get_valid_moves_for_piece(self, x: int, y: int, include_checks: bool = True) -> list:
        """
        Получить допустимые ходы для фигуры в позиции (x, y)

        Args:
            x: координата x фигуры
            y: координата y фигуры
            include_checks: учитывать ли проверку шаха

        Returns:
            list: список допустимых ходов
        """
        piece = self.board[y][x]
        if piece is None:
            return []

        # Получить базовые ходы
        moves = piece.get_valid_moves(self.board, x, y, self.en_passant)

        if not include_checks:
            return moves

        # Отфильтровать ходы, которые ставят/оставляют короля под шахом
        valid_moves = []
        for move_x, move_y in moves:
            if self.is_move_valid((x, y), (move_x, move_y)):
                valid_moves.append((move_x, move_y))

        return valid_moves

    def is_move_valid(self, start_pos: tuple, end_pos: tuple) -> bool:
        """
        Проверить, является ли ход допустимым (не оставляет короля под шахом)

        Args:
            start_pos: начальная позиция (x, y)
            end_pos: конечная позиция (x, y)

        Returns:
            bool: True если ход допустим
        """
        start_x, start_y = start_pos
        end_x, end_y = end_pos

        # Временное выполнение хода
        temp_piece = self.board[end_y][end_x]
        moving_piece = self.board[start_y][start_x]

        self.board[end_y][end_x] = moving_piece
        self.board[start_y][start_x] = None

        # Проверить, находится ли король под шахом после хода
        in_check = self.is_in_check(moving_piece.color)

        # Отменить временный ход
        self.board[start_y][start_x] = moving_piece
        self.board[end_y][end_x] = temp_piece

        return not in_check

Метод is_move_valid() реализует временное выполнение хода для проверки его безопасности. Алгоритм сохраняет состояние клеток, выполняет пробный ход, проверяет наличие шаха, а затем восстанавливает исходное состояние доски. Этот подход гарантирует, что ни один ход не поставит короля под угрозу

2.2.8 Обработка превращения пешки

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

    def handle_promotion_click(self, pos: tuple) -> None:
        """Обработка клика в увеличенном меню превращения пешки"""
        if not self.promotion_pending:
            return

        click_x, click_y = pos

        # Размеры и позиция увеличенного меню
        menu_width = square_size * 2
        menu_height = square_size * 4
        menu_x = (window_size - menu_width) // 2
        menu_y = (window_size - menu_height) // 2

        # Проверяем, был ли клик в области меню
        if not (menu_x <= click_x <= menu_x + menu_width and
                menu_y <= click_y <= menu_y + menu_height):
            return

        x, y = self.promotion_pending
        pawn = self.board[y][x]

        # Определяем, какую фигуру выбрал игрок
        relative_y = click_y - menu_y
        piece_index = relative_y // (menu_height // 4)

        piece_types = ['Q', 'R', 'B', 'N']

        if 0 <= piece_index < len(piece_types):
            new_piece = pawn.promote(piece_types[piece_index])
            self.board[y][x] = new_piece
            self.promotion_pending = None

            # Следующий ход
            self.current_player = 1 - self.current_player

            # Проверяем состояние игры после превращения
            self.check = self.is_in_check(self.current_player)
            if self.is_checkmate():
                self.game_over = True
            elif self.is_stalemate():
                self.game_over = True

2.2.9 Управление игровым процессом

Метод handle_click() является центральным диспетчером пользовательского ввода. Алгоритм различает несколько состояний: выбор фигуры, выполнение хода, превращение пешки и завершение игры. Для каждого состояния предусмотрена своя логика обработки, что обеспечивает плавный и предсказуемый игровой процесс

Основная логика выполнения ходов сосредоточена в методе make_move(). Этот комплексный алгоритм обрабатывает все специальные правила шахмат, включая взятие на проходе, рокировку и превращение пешек

    def handle_click(self, pos: tuple) -> None:
        """
        Обработка клика мыши

        Args:
            pos: позиция клика (x, y)
        """
        if self.promotion_pending:
            self.handle_promotion_click(pos)
            return

        if self.game_over:
            return

        x, y = pos
        grid_x, grid_y = x // square_size, y // square_size

        if not (0 <= grid_x < board_size and 0 <= grid_y < board_size):
            return

        # Если фигура уже выбрана, попытаться сделать ход
        if self.selected_piece:
            start_x, start_y = self.selected_piece

            # Проверить, кликнули ли на допустимый ход
            if (grid_x, grid_y) in self.valid_moves:
                self.make_move((start_x, start_y), (grid_x, grid_y))
                self.selected_piece = None
                self.valid_moves = []
            else:
                # Кликнули на другую фигуру текущего игрока выбрать ее вместо текущей
                piece = self.board[grid_y][grid_x]
                if piece is not None and piece.color == self.current_player:
                    self.selected_piece = (grid_x, grid_y)
                    self.valid_moves = self.get_valid_moves_for_piece(grid_x, grid_y)
                else:
                    self.selected_piece = None
                    self.valid_moves = []
        else:
            # Фигура еще не выбрана
            piece = self.board[grid_y][grid_x]
            if piece is not None and piece.color == self.current_player:
                self.selected_piece = (grid_x, grid_y)
                self.valid_moves = self.get_valid_moves_for_piece(grid_x, grid_y)


    def make_move(self, start_pos: tuple, end_pos: tuple) -> None:
        """
        Выполнить ход на доске

        Args:
            start_pos: начальная позиция (x, y)
            end_pos: конечная позиция (x, y)
        """
        start_x, start_y = start_pos
        end_x, end_y = end_pos

        moving_piece = self.board[start_y][start_x]

        # Обработка взятия на проходе
        if isinstance(moving_piece, Pawn) and end_pos == self.en_passant:
            # Удалить взятую пешку
            capture_y = end_y + 1 if moving_piece.color == 0 else end_y - 1
            self.board[capture_y][end_x] = None

        # Обработка рокировки
        if isinstance(moving_piece, King) and abs(end_x - start_x) == 2:
            # Короткая рокировка
            if end_x > start_x:
                rook = self.board[start_y][7]
                self.board[start_y][5] = rook
                self.board[start_y][7] = None
                if rook:
                    rook.has_moved = True
            # Длинная рокировка
            else:
                rook = self.board[start_y][0]
                self.board[start_y][3] = rook
                self.board[start_y][0] = None
                if rook:
                    rook.has_moved = True

        # Обновление позиции фигуры
        self.board[end_y][end_x] = moving_piece
        self.board[start_y][start_x] = None
        moving_piece.has_moved = True

        # Проверка на превращение пешки
        if isinstance(moving_piece, Pawn) and moving_piece.should_promote(end_y):
            self.promotion_pending = (end_x, end_y)
            return

        # Установка цели для взятия на проходе
        if (isinstance(moving_piece, Pawn) and
                abs(end_y - start_y) == 2):
            self.en_passant = (start_x, (start_y + end_y) // 2)
        else:
            self.en_passant = None

        # Смена игрока
        self.current_player = 1 - self.current_player

        # Проверка окончания игры
        self.check = self.is_in_check(self.current_player)
        if self.is_checkmate():
            self.game_over = True
        elif self.is_stalemate():
            self.game_over = True

2.2.10 Определение окончания игры

Методы is_checkmate() и is_stalemate() реализуют формальную проверку условий завершения партии. Алгоритм мата проверяет, находится ли король под шахом и существуют ли какие-либо легальные ходы для его спасения. Алгоритм пата аналогичен, но требует отсутствия шаха при невозможности сделать ход

    def is_checkmate(self) -> bool:
        """
        Проверить, находится ли текущий игрок в мате

        Returns:
            bool: True если мат
        """
        if not self.is_in_check(self.current_player):
            return False

        # Проверить, может ли любой ход вывести из-под шаха
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece is not None and piece.color == self.current_player:
                    moves = self.get_valid_moves_for_piece(col, row)
                    if moves:
                        return False

        return True

    def is_stalemate(self) -> bool:
        """
        Проверить, находится ли текущий игрок в пате

        Returns:
            bool: True если пат
        """
        if self.is_in_check(self.current_player):
            return False

        # Проверить, существует ли любой допустимый ход
        for row in range(board_size):
            for col in range(board_size):
                piece = self.board[row][col]
                if piece is not None and piece.color == self.current_player:
                    moves = self.get_valid_moves_for_piece(col, row)
                    if moves:
                        return False

        return True

2.2.11 Визуализация игрового состояния

Метод draw_game_state() отображает текстовые сообщения о текущем состоянии игры. В зависимости от ситуации, игроки видят уведомления о шаге, мате или пате

    def draw_game_state(self) -> None:
        """Отрисовка текста состояния игры"""
        if self.promotion_pending:
            return

        if self.game_over:
            if self.is_checkmate():
                winner = "Черные" if self.current_player == 0 else "Белые"
                text = f"{winner} побеждают матом!"
            else:
                text = "Пат - Ничья!"

            font = pygame.font.SysFont(None, 36)
            text_surface = font.render(text, True, (255, 255, 255))
            text_rect = text_surface.get_rect(center=(window_size // 2, window_size // 2))

            # Отрисовка фона для текста
            pygame.draw.rect(screen, (0, 0, 0),
                             (text_rect.x - 10, text_rect.y - 10,
                              text_rect.width + 20, text_rect.height + 20))
            screen.blit(text_surface, text_rect)

        elif self.check:
            font = pygame.font.SysFont(None, 24)
            text = f"{'Белые' if self.current_player == 0 else 'Черные'} под шахом!"
            text_surface = font.render(text, True, (255, 0, 0))
            screen.blit(text_surface, (10, 10))

2.2.12 Главный игровой цикл

Функция main() координирует весь игровой процесс, объединяя обработку событий, обновление состояния и отрисовку

def main() -> None:
    """Главная функция игры"""
    game = ChessGame()
    running = True

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:  
                    game.handle_click(event.pos)

        # Отрисовка всего (основные элементы)
        game.draw_board()
        game.draw_pieces()
        game.draw_highlights()
        game.draw_fog_of_war()
        game.draw_check_indicator()
        game.draw_game_state()

        if game.promotion_pending:
            game.draw_promotion_menu()

        pygame.display.flip()
        clock.tick(fps)

    pygame.quit()

if __name__ == "__main__":
    main()

3. Итоги и перспективы развития проекта

3.1 Что мы получили

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

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

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

пример игры со стороны игрока играющего за черных в шахматы
пример игры со стороны игрока играющего за черных в шахматы
пример игры со стороны игрока играющего за белых в шахматы
пример игры со стороны игрока играющего за белых в шахматы

3.2 Потенциал для дальнейшего развития

Когда основная механика была готова, я понял, что этот проект, кажется,уже полностью готовым, но всегда найдётся, куда двигаться дальше. Архитектура получилась гибкой, и это открывает кучу возможностей для улучшений, например:

☆  Одним из перспективных направлений является реализация сетевой игры через интернет или локальную сеть

☆  Создание или подключение искусственного интеллекта различного уровня сложности представляет собой интересную вычислительную задачу.

☆  Для повышения соревновательного элемента можно добавить систему контроля времени с различными форматами. Классические шахматы с длительным обдумыванием (5-10 минут) или блиц (1-3 минуты)

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

Визуальная демонстрация возможностей

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

пример запуска кода
пример запуска кода

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

демонстрация шаха
демонстрация шаха

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

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

переход пешки в другую фигуру (кружок и стрелка нарисована отдельно от кода)
переход пешки в другую фигуру (кружок и стрелка нарисована отдельно от кода)

Полный код доступен в репозитории GitHub.

Пишите идеи в комментарии, ваш вариант кода может попасть в обновлённую версию статьи или станет поводом написать новую

P.S. Если найдёте баги — сообщите, исправлю

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


  1. tenzink
    21.10.2025 08:09

    Проверку генерации ходов можно сделать с помощью https://www.chessprogramming.org/Perft теста. Есть преподсчитанные значения для разных глубин из разных позиций. Генерация ходов вещь непростая - там есть где ошибиться.

    Кажется fog of war есть на chess.com. можно пойти дальше и написать своего бота. Но тут для более-менее сильной игры потребуется не один раз пересмотреть структуры данных и потратить много времени.


    1. Laborant_Code Автор
      21.10.2025 08:09

      Спасибо за ссылку на Perft-тесты, я как раз искал способ для комплексной проверки генерации ходов, ведь в шахматах так много нюансов, которые приходилось проверять вручную. Обязательно изучу этот подход

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


  1. GlukKazan
    21.10.2025 08:09

    1. Laborant_Code Автор
      21.10.2025 08:09

      Красиво, именно такую игру с туманом войны я и представлял, как в этой версии против ИИ

      Но в этом проекте, как будто не хватает последнего шага, чтобы была ещё кнопка "мультиплеер" для игры с живым соперником (Хотя бы в локальной сети), тогда бы получилась идеальная версия


      1. GlukKazan
        21.10.2025 08:09

        Требуется авторизация: https://games.dtco.ru/launch/30/64


  1. avshkol
    21.10.2025 08:09

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

    Интересно, есть ли способ сделать офлайн шахматы/шашки (в виде настольной игры) с таким туманом?

    Возможно, для этого нужны специальные очки у каждого игрока,, и поле с тач-экраном...


    1. xSVPx
      21.10.2025 08:09

      Нормальный морской бой устанавливается на стол между сидящими лицом друг к другу игроками и каждый видит своё поле.

      Ныне нет проблемы каждому в его планшете что нужно показывать...


  1. GidraVydra
    21.10.2025 08:09

    Забавная игра. Туман войны у вас, я так понял, однорозовый, как в Героях 3? Было бы интереснее реализовать его как в четверке, возобновляемым.

    Но я, хоть убей, не понял где тут алгоритмическое программирование. Описана и реализована совершенно прикладная задача, причем код максимально наивный (в математическом смысле этого слова). По крайней мере я под алгоритмическим программированием понимаю что-то более...неочевидное, что ли.