Хотите создать свою первую игру, но не знаете, с чего начать? «Сапёр» — идеальный проект для этого! В нём есть простая, но интересная логика, работа с пользовательским вводом и графика, что делает его отличной стартовой точкой для любого начинающего разработчика.
В этом пошаговом руководстве мы вместе напишем полностью рабочую игру «Сапёр» с помощью языка Python и популярной библиотеки для создания игр Pygame. Вам не нужен опыт в разработке игр — только базовые знания Python. Мы пройдем весь путь от пустого файла до финального результата, и я подробно объясню каждый шаг.
Вот наш план:
Настроим рабочее окружение: создадим отдельное пространство для нашего проекта.
Создадим «мозг» игры: напишем код, который будет отвечать за мины и цифры на поле.
Нарисуем игровое поле: выведем нашу игру на экран.
Научим игру реагировать: добавим обработку кликов мыши.
Добавим условия победы и поражения: чтобы в игру было интересно играть!
В конце у вас будет готовый проект, которым можно поделиться с друзьями, и четкое понимание, как устроены простые 2D-игры.
Ну что, готовы? Начнем
Шаг 0. Подготовка рабочего места (самый важный шаг!)
Прежде чем написать хотя бы одну строчку кода, нам нужно подготовить для нашего проекта чистое и организованное рабочее пространство. Мы сделаем это с помощью виртуального окружения.
Что это такое простыми словами? Представьте, что для каждого своего проекта вы создаете отдельную, изолированную "коробку". В эту коробку вы складываете только те инструменты (библиотеки), которые нужны именно для этого проекта. Это защищает ваши проекты от конфликтов и считается золотым стандартом в Python-разработке.
Давайте создадим такую "коробку" для нашего «Сапёра».
1. Создаем папку проекта
Сначала нам нужна папка, где будут лежать все файлы нашей игры. Для этого откройте терминал (или командную строку в Windows). Это программа, где мы можем давать компьютеру команды текстом.
Введите следующие команды по очереди, нажимая Enter после каждой:
# Создаем папку с именем minesweeper mkdir minesweeper # Заходим внутрь этой папки cd minesweeper
Теперь все дальнейшие действия мы будем выполнять внутри папки minesweeper.
2. Создаем виртуальное окружение
Находясь в папке minesweeper, введите в терминал следующую команду:
python -m venv venv
Эта команда создаст внутри minesweeper новую папку с именем venv. В ней будет храниться "чистая" копия Python и все библиотеки, которые мы установим для нашей игры.
3. Активируем окружение
Мы создали "коробку", а теперь её нужно "открыть" или активировать. Команды для этого немного отличаются в зависимости от вашей операционной системы.
Если у вас Windows:
venv\Scripts\activateЕсли у вас macOS или Linux:
source venv/bin/activate
Если все прошло успешно, вы увидите, что в начале строки терминала появилось (venv). Это наш сигнал — виртуальное окружение активно!
# Пример того, как это будет выглядеть: (venv) C:\Users\YourName\minesweeper>
4. Устанавливаем Pygame
Теперь, когда наша "коробка" открыта, мы можем положить в неё наш главный инструмент — библиотеку Pygame. pip — это стандартный менеджер пакетов Python, который скачает и установит её для нас.
Выполните в терминале одну простую команду:
pip install pygame
pip установит Pygame именно в наше виртуальное окружение, не затрагивая основную систему.
Шаг 1. "Мозг" игры — создаем логику поля
Прежде чем мы начнем что-либо рисовать, нам нужно создать правила и внутреннее устройство нашей игры. Эту часть часто называют логикой или моделью. Представьте, что мы создаем «Сапёра», в которого можно было бы играть с закрытыми глазами, просто называя координаты.
Наш план для этого шага:
Создать "кирпичик" — объект для одной ячейки поля.
Собрать из этих "кирпичиков" целое игровое поле.
Научить поле расставлять мины.
Научить поле считать цифры вокруг мин.
Давайте начнем! Создайте в папке minesweeper новый файл с именем main.py и пишите весь код из этого шага туда.
1.1. "Кирпичик" для нашего поля: класс Cell
Каждый квадратик на поле «Сапёра» должен что-то о себе "знать": есть ли в нем мина, открыт ли он и так далее. Чтобы удобно хранить эту информацию, мы создадим для ячейки собственный класс-шаблон.
Добавьте этот код в ваш файл main.py:
class Cell: def __init__(self): self.is_mine = False self.is_open = False self.is_flagged = False self.adjacent_mines = 0
Этот простой класс — наш "строительный блок". Каждый раз, когда нам понадобится новая ячейка на поле, мы будем создавать её по этому шаблону.
1.2. Собираем поле в одно целое: класс GameBoard
Теперь, когда у нас есть "кирпичик", давайте построим из них стену — наше игровое поле. Для этого создадим еще один класс, GameBoard, который будет управлять всеми ячейками.
Добавьте этот код в main.py под классом Cell:
import random # Класс Cell, который мы написали выше, должен быть здесь... class GameBoard: def __init__(self, width, height, mines_count): self.width = width self.height = height self.mines_count = mines_count # Создаем сетку (список списков) и заполняем её нашими "кирпичиками" self.board = [[Cell() for _ in range(width)] for _ in range(height)]
Здесь мы создаем сетку (двумерный список), заполненную объектами Cell. Теперь у нас есть структура поля, но оно пока пустое. Давайте это исправим.
1.3. Расставляем мины
Нам нужно случайным образом разместить на поле заданное количество мин. Для этого добавим новый метод (функцию) _place_mines внутрь нашего класса GameBoard.
def _place_mines(self): # Создаем список всех возможных координат (строка, столбец) all_coords = [(r, c) for r in range(self.height) for c in range(self.width)] # Выбираем из списка случайные уникальные координаты для мин mine_coords = random.sample(all_coords, self.mines_count) # В ячейках по этим координатам ставим мины for r, c in mine_coords: self.board[r][c].is_mine = True
Мы использовали random.sample — это удобный способ выбрать несколько случайных элементов из списка, при этом он гарантирует, что все они будут уникальными. Так мы избежим ситуации, когда две мины попали в одну и ту же ячейку.
1.4. Считаем цифры вокруг мин
Это самая важная часть логики. Нам нужно пройти по каждой ячейке поля. Если в ней нет мины, мы должны посчитать, сколько мин находится в 8 соседних ячейках.
Добавьте следующий метод _calculate_adjacent_mines в класс GameBoard:
def _calculate_adjacent_mines(self): # Проходим по каждой ячейке поля for r in range(self.height): for c in range(self.width): # Если в ячейке уже есть мина, считать ничего не нужно if self.board[r][c].is_mine: continue mine_count = 0 # Проверяем всех 8 соседей for dr in [-1, 0, 1]: # Смещение по строке for dc in [-1, 0, 1]: # Смещение по столбцу # Пропускаем саму текущую ячейку if dr == 0 and dc == 0: continue # Вычисляем координаты соседа nr, nc = r + dr, c + dc # ВАЖНО: Проверяем, что сосед не вышел за границы поля if 0 <= nr < self.height and 0 <= nc < self.width: if self.board[nr][nc].is_mine: mine_count += 1 # Записываем результат в ячейку self.board[r][c].adjacent_mines = mine_count
И последнее — давайте сделаем так, чтобы мины и цифры расставлялись автоматически сразу при создании поля. Для этого просто вызовем наши новые методы в __init__.
Обновите метод __init__ в классе GameBoard, добавив в конец две новые строчки:
def __init__(self, width, height, mines_count): self.width = width self.height = height self.mines_count = mines_count self.board = [[Cell() for _ in range(width)] for _ in range(height)] # Добавляем эти две строки: self._place_mines() self._calculate_adjacent_mines()
Шаг 2. Первое окно — рисуем наше игровое поле
Итак, "мозг" нашей игры готов, но пока он работает невидимо. Пора дать ему "тело" — графическое отображение! В этом шаге мы создадим окно игры и нарисуем в нём сетку, которая представляет наше игровое поле.
Наш план:
Написать базовый код для запуска окна Pygame.
Задать основные параметры: размеры ячеек, цвета.
Создать функцию, которая будет рисовать сетку на основе данных из нашего
GameBoard.Собрать всё вместе в главный игровой цикл.
Продолжаем работать в нашем файле main.py.
2.1. Константы и базовый запуск Pygame
Хорошая практика в программировании — выносить значения, которые могут часто меняться (вроде цветов или размеров), в отдельные переменные в начале файла. Их называют константами (и часто пишут ЗАГЛАВНЫМИ_БУКВАМИ).
Добавьте этот код в самый низ вашего файла main.py, после всех классов.
import pygame # --- Константы --- # Размеры поля в ячейках BOARD_WIDTH = 20 BOARD_HEIGHT = 15 MINES_COUNT = 30 # Размер одной ячейки в пикселях CELL_SIZE = 30 # Рассчитываем размер окна в пикселях SCREEN_WIDTH = BOARD_WIDTH * CELL_SIZE SCREEN_HEIGHT = BOARD_HEIGHT * CELL_SIZE # Цвета (в формате RGB) BG_COLOR = (192, 192, 192) # Серый LINE_COLOR = (128, 128, 128) # Темно-серый # --- Инициализация Pygame и создание окна --- pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Сапёр") # --- Создание игрового поля --- game_board = GameBoard(BOARD_WIDTH, BOARD_HEIGHT, MINES_COUNT)
Что мы здесь сделали:
Импортировали
pygame.Задали все основные настройки нашей игры. Теперь, если вы захотите поле побольше или поменьше, достаточно будет поменять цифры здесь.
Инициализировали Pygame и создали окно нужного размера.
Создали один экземпляр нашего "мозга" —
game_board. Именно его мы и будем рисовать.
2.2. Функция для отрисовки поля
Давайте создадим отдельную функцию, которая будет отвечать только за рисование. Это делает код более чистым и организованным.
Добавьте эту функцию в main.py после блока с константами:
def draw_board(board_obj): # Заливаем весь экран фоновым цветом screen.fill(BG_COLOR) # Проходим по каждой ячейке for r in range(board_obj.height): for c in range(board_obj.width): # Рассчитываем координаты ячейки на экране (в пикселях) x = c * CELL_SIZE y = r * CELL_SIZE # Создаем прямоугольник для ячейки rect = pygame.Rect(x, y, CELL_SIZE, CELL_SIZE) # Рисуем контур ячейки pygame.draw.rect(screen, LINE_COLOR, rect, 1)
Эта функция:
Заливает экран серым цветом, чтобы стереть предыдущий кадр.
В двойном цикле проходит по каждой ячейке.
Вычисляет, где на экране (в пикселях) нужно нарисовать текущую ячейку.
Рисует для каждой ячейки прямоугольник с черным контуром толщиной в 1 пиксель.
2.3. Главный игровой цикл
Любая игра работает внутри бесконечного цикла. На каждом витке этого цикла игра:
Проверяет действия игрока (нажал ли он кнопку, закрыл ли окно).
Обновляет логику игры.
Перерисовывает экран.
Этот цикл — сердце нашей программы. Добавьте его в самый конец файла main.py:
# --- Главный игровой цикл --- running = True while running: # 1. Обработка событий for event in pygame.event.get(): # Если пользователь нажал на "крестик" if event.type == pygame.QUIT: running = False # 2. Отрисовка draw_board(game_board) # 3. Обновление экрана pygame.display.flip() # Корректное завершение работы pygame.quit()
Теперь, если вы запустите ваш файл main.py из терминала (python main.py), вы должны увидеть окно с серой сеткой!
(venv) ...\minesweeper> python main.py

Шаг 3. Оживляем игру — учим ее слушать мышку
Игра без управления — не игра. Сейчас наше окно просто показывает статичную картинку. Давайте научим нашу программу "слышать" клики мыши и понимать, по какой именно ячейке кликнул игрок.
Наш план на этот шаг:
Найти в нашем игровом цикле место, где отлавливаются все действия игрока.
Добавить код для отслеживания именно кликов мыши.
Написать простую формулу для превращения координат клика (в пикселях) в координаты ячейки (номер строки и столбца).
Проверить, что всё работает, выводя результат в терминал.
Мы будем вносить изменения в главный игровой цикл, который находится в самом конце файла main.py.
3.1. Ловим клики мыши
Наш игровой цикл while running: уже содержит цикл for event in pygame.event.get():. Этот цикл — "уши" нашей программы. Он ловит все события: движение мыши, нажатие клавиш, закрытие окна и, конечно, клики.
Давайте добавим проверку на событие клика мыши.
Найдите свой игровой цикл и добавьте в него блок if, как показано ниже:
# --- Главный игровой цикл --- running = True while running: # 1. Обработка событий for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # ДОБАВЛЯЕМ ЭТОТ БЛОК: # Если произошло событие "кнопка мыши нажата" if event.type == pygame.MOUSEBUTTONDOWN: # Тут будет наша логика print("Клик!") # 2. Отрисовка # ... (код отрисовки остается без изменений) ...
Если вы сейчас запустите игру и покликаете по окну, вы увидите, что в терминале, из которого вы запускали скрипт, при каждом клике появляется сообщение "Клик!". Отлично, мы научились их ловить!
3.2. Превращаем пиксели в ячейки
Теперь самое интересное. Pygame сообщает нам, где был клик, в пикселях (например, "клик был в точке X=152, Y=95"). Но нашей игре нужны координаты ячейки (например, "клик был в ячейке: строка 3, столбец 5").
Как это посчитать? Очень просто! Нужно разделить координату в пикселях на размер одной ячейки.
Например, если размер ячейки CELL_SIZE у нас 30 пикселей:
Клик в
X=70пикселей.70 // 30 = 2. Значит, это 3-й столбец (считая с нуля: 0, 1, 2).Клик в
Y=95пикселей.95 // 30 = 3. Значит, это 4-я строка (считая с нуля: 0, 1, 2, 3).
Мы используем целочисленное деление (//), которое отбрасывает остаток — это именно то, что нам нужно.
Давайте напишем это в коде. Обновите ваш блок обработки клика:
if event.type == pygame.MOUSEBUTTONDOWN: # Получаем координаты клика в пикселях mouse_x, mouse_y = pygame.mouse.get_pos() # Превращаем пиксели в координаты ячейки clicked_col = mouse_x // CELL_SIZE clicked_row = mouse_y // CELL_SIZE # Выводим результат в терминал для проверки print(f"Клик по ячейке: строка {clicked_row}, столбец {clicked_col}")
3.3. Собираем всё вместе и проверяем
Вот как теперь должен выглядеть ваш полный игровой цикл:
# --- Главный игровой цикл --- running = True while running: # 1. Обработка событий for event in pygame.event.get(): if event.type == pygame.QUIT: running = False if event.type == pygame.MOUSEBUTTONDOWN: mouse_x, mouse_y = pygame.mouse.get_pos() clicked_col = mouse_x // CELL_SIZE clicked_row = mouse_y // CELL_SIZE print(f"Клик по ячейке: строка {clicked_row}, столбец {clicked_col}") # 2. Отрисовка draw_board(game_board) # 3. Обновление экрана pygame.display.flip() # Корректное завершение работы pygame.quit()
Теперь снова запустите игру (python main.py). Кликайте по разным ячейкам на поле. Вы должны видеть в терминале, как программа безошибочно определяет, на какую строку и столбец вы нажали.

Шаг 4. Собираем всё вместе — логика в действии!
Сейчас у нас есть "мозг" игры (GameBoard) и "уши", которые слышат клики мыши. Пришло время соединить их! Мы будем использовать координаты клика, чтобы вызывать методы нашего "мозга", а затем обновим отрисовку, чтобы показать результат этих действий на экране.
План на этот заключительный шаг:
Дописать в класс
GameBoardметоды для открытия ячеек и установки флагов.Вызывать эти методы из главного игрового цикла в ответ на клики.
Полностью переделать функцию
draw_board, чтобы она отображала мины, цифры и флаги.Добавить логику победы и поражения.
4.1. Дописываем логику в GameBoard
Возвращаемся к нашему классу GameBoard. Нам нужно добавить ему два новых "умения": открывать ячейку и ставить/убирать флажок.
Добавьте эти два метода внутрь класса GameBoard:
def open_cell(self, r, c): # Получаем ячейку, по которой кликнули cell = self.board[r][c] # Нельзя открыть уже открытую ячейку или ячейку с флагом if cell.is_open or cell.is_flagged: return cell.is_open = True # Магия "Сапёра": если ячейка пустая, открываем соседей if cell.adjacent_mines == 0 and not cell.is_mine: # Проходим по всем 8 соседям for dr in [-1, 0, 1]: for dc in [-1, 0, 1]: if dr == 0 and dc == 0: continue nr, nc = r + dr, c + dc # Убеждаемся, что сосед в пределах поля if 0 <= nr < self.height and 0 <= nc < self.width: # И рекурсивно вызываем эту же функцию для соседа! self.open_cell(nr, nc) def toggle_flag(self, r, c): cell = self.board[r][c] # Флаг можно ставить только на закрытые ячейки if not cell.is_open: cell.is_flagged = not cell.is_flagged
Ключевой момент здесь — рекурсия в методе open_cell. Если игрок кликает по пустой ячейке (где 0 мин вокруг), функция вызывает саму себя для всех восьми соседей. Если кто-то из соседей тоже окажется пустым, он, в свою очередь, вызовет эту функцию для своих соседей. Так и получается "цепная реакция", открывающая целую область.
4.2. Обновляем главный игровой цикл
Теперь в главном цикле мы будем не просто печатать координаты, а вызывать наши новые методы. Нам также нужно различать левую и правую кнопки мыши.
event.button == 1— это левая кнопка (открыть ячейку).event.button == 3— это правая кнопка (поставить флаг).
Обновите блок обработки событий в вашем цикле:
if event.type == pygame.MOUSEBUTTONDOWN: mouse_x, mouse_y = pygame.mouse.get_pos() clicked_col = mouse_x // CELL_SIZE clicked_row = mouse_y // CELL_SIZE # Если левая кнопка мыши if event.button == 1: game_board.open_cell(clicked_row, clicked_col) # Если правая кнопка мыши elif event.button == 3: game_board.toggle_flag(clicked_row, clicked_col)
4.3. Большая переделка draw_board
Наша текущая функция draw_board рисует только серую сетку. Пора заставить ее показывать всё: открытые ячейки, цифры, флаги и, конечно, мины! Это потребует самой большой модификации кода.
Сначала нам нужен шрифт для рисования цифр. Добавьте его создание после инициализации Pygame.
# --- Инициализация Pygame и создание окна --- pygame.init() screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Сапёр") # Добавляем создание шрифта font = pygame.font.SysFont('Arial', CELL_SIZE // 2)
Теперь полностью замените вашу старую функцию draw_board на эту, новую версию:
def draw_board(board_obj): screen.fill(BG_COLOR) for r in range(board_obj.height): for c in range(board_obj.width): cell = board_obj.board[r][c] x = c * CELL_SIZE y = r * CELL_SIZE rect = pygame.Rect(x, y, CELL_SIZE, CELL_SIZE) # Рисуем контур ячейки pygame.draw.rect(screen, LINE_COLOR, rect, 1) # Если ячейка открыта if cell.is_open: if cell.is_mine: # Рисуем мину (красный круг) pygame.draw.circle(screen, (255, 0, 0), rect.center, CELL_SIZE // 3) elif cell.adjacent_mines > 0: # Рисуем цифру text = font.render(str(cell.adjacent_mines), True, (0, 0, 0)) text_rect = text.get_rect(center=rect.center) screen.blit(text, text_rect) # Если стоит флаг elif cell.is_flagged: # Рисуем флаг (желтый треугольник) pygame.draw.polygon(screen, (255, 255, 0), [(rect.left + 5, rect.top + 5), (rect.right - 5, rect.centery), (rect.left + 5, rect.bottom - 5)])
4.4. Добавляем финал: победа и поражение
Игра должна заканчиваться! Давайте добавим game_over флаг в наш GameBoard.
В метод __init__ класса GameBoard добавьте self.game_over = False.
В методе open_cell найдите место, где ячейка оказывается миной, и измените его:
cell.is_open = True # Если это была мина - игра окончена if cell.is_mine: self.game_over = True return # Сразу выходим
Теперь в главном цикле мы должны перестать обрабатывать клики, если игра окончена.
if event.type == pygame.MOUSEBUTTONDOWN and not game_board.game_over: # ... остальная логика клика ...
И последнее: давайте покажем сообщение о проигрыше!
# В главном цикле, после отрисовки draw_board(game_board) # Если игра окончена, показываем сообщение if game_board.game_over: # Полупрозрачный черный фон overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 128)) screen.blit(overlay, (0, 0)) # Текст text = font.render("Вы проиграли!", True, (255, 255, 255)) text_rect = text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)) screen.blit(text, text_rect)
Поздравляю!
Вы сделали это! Запустите скрипт python main.py и наслаждайтесь своей собственной, полностью рабочей версией «Сапёра». Вы прошли весь путь от настройки окружения до реализации сложной игровой логики и её визуализации.
Анонс новых статей, полезные материалы, а так же если в процессе написания кода возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Теперь у вас есть прочная база для создания собственных, более сложных проектов с помощью Pygame.
