Хотите создать свою первую игру, но не знаете, с чего начать? «Сапёр» — идеальный проект для этого! В нём есть простая, но интересная логика, работа с пользовательским вводом и графика, что делает его отличной стартовой точкой для любого начинающего разработчика.
В этом пошаговом руководстве мы вместе напишем полностью рабочую игру «Сапёр» с помощью языка 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.
alex0008
Автор, ждал разбора момента и не увидел как генератор минного поля учитывает, что мины могут создать кольцо, в которое невозможно будет попасть? Простая рандомная расстановка может привести к такому
enamored_poc Автор
В статье этот момент был опущен сознательно по одной причине: сохранение низкого порога входа для начинающих.
Целью руководства было научить основам:
Работе с классами (ООП).
Базовой логике 2D-массивов.
Основам Pygame (игровой цикл, отрисовка, обработка событий).
Реализации простого рекурсивного алгоритма.
Добавление алгоритма проверки поля на логическую решаемость — это задача на порядок сложнее, которая увела бы нас от этих основных целей.
Xiran
Фу нейронка
alex0008
а это было бы интересно разобрать - как узнать что область не замкнута и в неё есть доступ? Когда это лучше делать, при постановке новой мины проверять не замкнёт ли она область, или пытаться сперва расставить а потом проверить и, допустим, перенести одну из них, чтобы разомкнуть контур. А ведь это коснётся и задачи и на поиск пути - такие алгоритмы используются во множестве игр.
Понимаю, что статья для начинающих - но чуть-чуть материала на развитие тоже будет интересно
HemulGM
Что это за ситуация? И почему это вообще является проблемой?
alex0008
Если мины образуют кольцо вокруг ячейки без мины, скажем слева (мина это X, а 5 ячейка без неё) то разгадывая поле справа туда никак не попадёшь:
XX
5X
XX
HemulGM
Верхний правый Х и нижний правый Х легко помогают определить, где нет мины
А если будет полный круг, то можно методом исключения
alex0008
Я имею ввиду, что начиная, например, справа сверху как мы узнаем, есть в центре мина или нет? Мы не можем вычислить это никак - нет просвета и информация из чисел вокруг не помогает тут
HemulGM
Это не страшно. Оставляем на будущее (под вопрос) и дальше идём. В конце будет понятно
Проблема может возникнуть когда уже две будет такие ситуации, а не найденная мина одна
HemulGM
Кстати, можно попробовать в моем исполнении поиграть. Классическая игра, без рекламы и с разными размерами полей и сложностью.
https://play.google.com/store/apps/details?id=com.embarcadero.MineSweeperFMX
Написано на Delphi
nvv1970
Чат гпт убедил меня, что это действительно бэд дизайн играх. Но мы же говорим об оригинальной игре в винде? Так подобных ситуаций валом.
При чем гпт забавно объяснял. Спрашиваю, про угадывание ли речь? Нет, говорит, ты "не можешь пройти" через мины)
alex0008
А всё решается просто, вместо случайной расстановки мин делать связную область без мин и генерацию начинать с первого клика игрока(иначе он может подорваться с первого же хода) - именно так сделан сапёр в винде
А вот как - тут и стоит подумать, в том и интерес)