Написать игру Сапёр (Mines) на Python с использованием библиотеки Tkinter оказалось не сложно. В этой статье покажем все этапы создания этой игры, которая массово была представлена публике в Windows 95. Следует отметить, что в Windows 3.11 игра winmine уже была. Но кто теперь это вспомнит?

Создадим поле из кнопок 16х16=256 кнопок.

from tkinter import *
from  random import choice
frm = []; btn = []                                # Списки с фреймами и кнопками

tk = Tk()
tk.title('Achtung, Minen!')

for i in range(0, 16):
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, 16):
        btn.append(Button(frm[i], text=' ',
                          font=('mono', 16, 'bold'),
                          width=2, height=1,))
        btn[i*16+j].pack(side=LEFT, expand=NO, fill=Y)

mainloop()

Кнопки размещаются в 16 горизонтальных фреймах. Список btn состоит из 256 кнопок.

Создадим список playArea - игровое поле и переменную счётчик ходов — nMoves. Игровое поле playArea содержит 256 элементов (целые числа). Где число -1 мина, числа 0 ... 8 количество мин вокруг клетки. Будем ставить мины на поле после первого хода игрока, с тем, чтобы первый ход не закончился взрывом.

Каждый ход игрока выводит на нажатую кнопку соответствующее значение из списка playArea и деактивирует кнопку.

from tkinter import *
from  random import choice
frm = []; btn = []                              # Списки с фреймами и кнопками
playArea = []; nMoves = 0                       # Игровое поле и счётчик ходов

def play(n):                                    # n - номер нажатой кнопки
    global nMoves
    nMoves += 1
    if nMoves == 1:                             # Если это первый ход игрока,
        i = 0
        while i<40:                             # поставим мины,
            j = choice(range(0, 256))
            if j != n and playArea[j] != -1:
                playArea[j] = -1
                i += 1
        for i in range(0, 256):                 # подсчитаем количесво мин вокруг каждой клетки
            if playArea[i] != -1:
                k = 0
                if i not in range(0, 256, 16):
                    if playArea[i-1] == -1: k += 1          # слева
                    if i > 15:
                        if playArea[i-17] == -1: k += 1     # слева сверху
                    if i < 240:
                        if playArea[i+15] == -1: k += 1     # слева снизу
                if i not in range(-1, 256, 16):
                    if playArea[i+1] == -1: k += 1          # справа
                    if i > 15:
                        if playArea[i-15] == -1: k += 1     # справа сверху
                    if i < 240:
                        if playArea[i+17] == -1: k += 1     # справа снизу
                if i > 15:
                    if playArea[i-16] == -1: k += 1         # сверху
                if i < 240:
                    if playArea[i+16] == -1: k += 1         # снизу
                playArea[i] = k
    
    btn[n].config(text=playArea[n], state=DISABLED)         # Отображаем игровую ситуацию

tk = Tk()
tk.title('Achtung, Minen!')

for i in range(0, 16):                          # Размещаем кнопки
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, 16):
        btn.append(Button(frm[i], text=' ',
                          font=('mono', 16, 'bold'),
                          width=2, height=1,
                          command=lambda n=i*16+j: play(n)))
        btn[i*16+j].pack(side=LEFT, expand=NO, fill=Y)
        playArea.append(0)                      # Создаём элементы списка playArea

mainloop()

С такой программой уже можно играть в сапёра, но нет, ставших нам уже привычными, некоторых мелочей:

  • Вместо "0" должна отображаться пустая кнопка с изменённым цветом.
  • Вместо "-1" необходимо вывести изображение мины.
  • Если мы нарвались на мину:
    • необходимо вывести на экран все мины и
    • необходимо вывести на экран "Game Over"
    • в дальнейшем не допустить вывод на экран сообщения "You win!"
  • Если игрок открыл все поля не занятые минами, вывести на экран сообщение "You win!"
  • Необходимо добавить возможность с помощью правой кнопки мыши помечать клетку под которой возможно скрывается мина.
from tkinter import *
from  random import choice
frm = []; btn = []                              # Списки с фреймами и кнопками
playArea = []; nMoves = 0                       # Игровое поле и счётчик ходов

def play(n):                                    # n - номер нажатой кнопки
    global nMoves
    nMoves += 1
    if nMoves == 1:                             # Если это первый ход игрока,
        i = 0
        while i<40:                             # поставим мины,
            j = choice(range(0, 256))
            if j != n and playArea[j] != -1:
                playArea[j] = -1
                i += 1
        for i in range(0, 256):                 # подсчитаем количесво мин вокруг каждой клетки
            if playArea[i] != -1:
                k = 0
                if i not in range(0, 256, 16):
                    if playArea[i-1] == -1: k += 1          # слева
                    if i > 15:
                        if playArea[i-17] == -1: k += 1     # слева сверху
                    if i < 240:
                        if playArea[i+15] == -1: k += 1     # слева снизу
                if i not in range(-1, 256, 16):
                    if playArea[i+1] == -1: k += 1          # справа
                    if i > 15:
                        if playArea[i-15] == -1: k += 1     # справа сверху
                    if i < 240:
                        if playArea[i+17] == -1: k += 1     # справа снизу
                if i > 15:
                    if playArea[i-16] == -1: k += 1         # сверху
                if i < 240:
                    if playArea[i+16] == -1: k += 1         # снизу
                playArea[i] = k
                
    btn[n].config(text=playArea[n], state=DISABLED)         # Отображаем игровую ситуацию
    if playArea[n] == 0:
        btn[n].config(text=' ', bg='#ccb')
    elif playArea[n] == -1:
        if nMoves < (256 - 40):                 # Если игрок ещё не выиграл, то проиграл
            tk.title('Your game is over.')
            nMoves = 256                        # Если проиграл, то уже не выиграет
        for i in range(0, 256):
            if playArea[i] == -1:
                btn[i].config(text='\u2665')
    if nMoves == (256 - 40):                    # Если все клетки открыты, это победа
        tk.title('You win!')

def marker(n):                                  # помечаем клетку под которой возможно скрывается мина.
    btn[n].config(text='\u2661')

tk = Tk()
tk.title('Achtung, Minen!')

for i in range(0, 16):                          # Размещаем кнопки
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, 16):
        btn.append(Button(frm[i], text=' ',
                          font=('mono', 16, 'bold'),
                          width=2, height=1,
                          command=lambda n=i*16+j: play(n)))
        btn[i*16+j].pack(side=LEFT, expand=NO, fill=Y)
        btn[i*16+j].bind('<Button-3>', lambda event, n=i*16+j: marker(n))
        playArea.append(0)                      # Создаём элементы списка playArea

mainloop()

Добавим кнопку "New game" и взрывы мин в случае поражения.

# Mines
# This is my version of the game, known as mines or Minesweeper.
#
# Created on June 25, 2021.
# Author: Diorditsa A.
# I thank Sergey Polozkov for checking the code for hidden errors.
#
# mines.py is distributed in the hope that it will be useful, but
# WITHOUT WARRANTY OF ANY KIND; not even an implied warranty
# MARKETABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See. See the GNU General Public License for more information.
# You can get a copy of the GNU General Public License
# by link http://www.gnu.org/licenses/

from tkinter import *
from  random import choice
frm = []; btn = []                              # Списки с фреймами и кнопками
playArea = []; nMoves = 0; mrk=40                # Игровое поле, счётчик ходов и маркеров

def play(n):                                    # n - номер нажатой кнопки
    global nMoves, mrk
    nMoves += 1
    if nMoves == 1:                             # Если это первый ход игрока,
        tk.title('Achtung, '+str(mrk)+' Minen!')
        i = 0
        while i<40:                             # поставим мины,
            j = choice(range(0, 256))
            if j != n and playArea[j] != -1:
                playArea[j] = -1
                i += 1
        for i in range(0, 256):                 # подсчитаем количесво мин вокруг каждой клетки
            if playArea[i] != -1:
                k = 0
                if i not in range(0, 256, 16):
                    if playArea[i-1] == -1: k += 1          # слева
                    if i > 15:
                        if playArea[i-17] == -1: k += 1     # слева сверху
                    if i < 240:
                        if playArea[i+15] == -1: k += 1     # слева снизу
                if i not in range(-1, 256, 16):
                    if playArea[i+1] == -1: k += 1          # справа
                    if i > 15:
                        if playArea[i-15] == -1: k += 1     # справа сверху
                    if i < 240:
                        if playArea[i+17] == -1: k += 1     # справа снизу
                if i > 15:
                    if playArea[i-16] == -1: k += 1         # сверху
                if i < 240:
                    if playArea[i+16] == -1: k += 1         # снизу
                playArea[i] = k
                
    btn[n].config(text=playArea[n], state=DISABLED)         # Отображаем игровую ситуацию
    if playArea[n] == 0:
        btn[n].config(text=' ', bg='#ccb')
    elif playArea[n] == -1:
        btn[n].config(text='\u2665')
        if nMoves <= (256 - 40):                # Если игрок ещё не выиграл, то проиграл
            tk.title('Your game is over.')
            nMoves = 256                        # Если проиграл, то уже не выиграет
            chainReaction(0)                    # Цепная реакция
    if nMoves == (256 - 40):                    # Если все клетки открыты, это победа
        tk.title('You win!')
        winner(0)

def chainReaction(j):                           # Цепная реакция
    for i in range(j, 256):
        if playArea[i] == -1 and btn[i].cget('text') == ' ':
            btn[i].config(text='\u2665')
            btn[i].flash()
            tk.bell()
            tk.after(50, chainReaction, i + 1)
            break

def winner(j):
    for i in range(j, 256):
        if playArea[i] == 0:
            btn[i].config(state=NORMAL, text='☺')
            btn[i].flash()
            tk.bell()
            btn[i].config(text=' ', state=DISABLED)
            tk.after(50, winner, i + 1)
            break

def marker(n):                                  # помечаем клетку под которой возможно скрывается мина.
    global mrk
    if (btn[n].cget('state')) != 'disabled':
        if btn[n].cget('text') == '\u2661':
            btn[n].config(text=' ')
            mrk += 1
        else:
            btn[n].config(text='\u2661')
            mrk -= 1
        tk.title('Achtung, '+str(mrk)+' Minen!')

def newGame():                                  # Чистим переменную nMoves, mrk и список playArea и кнопки
    global nMoves, btnBG, mrk
    nMoves = 0; mrk = 40
    for i in range(0, 256):
        playArea[i] = 0
        btn[i].config(text=' ', state=NORMAL, bg=btnBG)

tk = Tk()

for i in range(0, 16):                          # Размещаем кнопки
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, 16):
        btn.append(Button(frm[i], text=' ',
                          font=('mono', 16, 'bold'),
                          width=2, height=1,
                          command=lambda n=i*16+j: play(n)))
        btn[i*16+j].pack(side=LEFT, expand=NO, fill=Y)
        btn[i*16+j].bind('<Button-3>', lambda event, n=i*16+j: marker(n))
        playArea.append(0)                      # Создаём элементы списка playArea
        tk.update()

Button(tk, text='New game', font=(16),          # Создаём кнопку "New game"
       command=newGame).pack(side=LEFT, expand=YES, fill=Y)

btnBG = btn[0].cget('bg')                       # Запоминаем цвет кнопки по умолчанию

mainloop()

Рис. Игра Сапёр.

Очередная версия:

# Mines
# This is my version of the game, known as mines or Minesweeper.
#
# Created on June 25, 2021.
# Author: Diorditsa A.
# I thank Sergey Polozkov for checking the code for hidden errors?
#
# mines.py is distributed in the hope that it will be useful, but
# WITHOUT WARRANTY OF ANY KIND; not even an implied warranty
# MARKETABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See. See the GNU General Public License for more information.
# You can get a copy of the GNU General Public License
# by link http://www.gnu.org/licenses/

from tkinter import *
from  random import choice
import time

frm = []; btn = []                              # Списки с фреймами и кнопками
xBtn = 16; yBtn = 16                            # Размеры поля (количество кнопок)
playTime = 0                                    # Время игры
mines = xBtn * yBtn * 10 // 64                  # Количество мин
imgMark = '\u2661'; imgMine = '\u2665'          # Символ маркера и мины
playArea = []; nMoves = 0; mrk=0                # Игровое поле, счётчик ходов и маркеров
tk = Tk()
tk.title('Achtung, Minen!')
tk.geometry(str(44*xBtn)+'x'+str(44*yBtn+10))

def play(n):                                    # n - номер нажатой кнопки
    global xBtn, yBtn, mines, nMoves, mrk, playTime
    if len(playArea) < xBtn*yBtn:               # Если поле ещё не создано
        return()
    nMoves += 1
    if nMoves == 1:                             # Если это первый ход игрока,
        playTime = time.time()
        i = 0
        while i<mines:                          # поставим мины,
            j = choice(range(0, xBtn*yBtn))
            if j != n and playArea[j] != -1:
                playArea[j] = -1
                i += 1
        for i in range(0, xBtn*yBtn):           # подсчитаем количесво мин вокруг каждой клетки
            if playArea[i] != -1:
                k = 0
                if i not in range(0, xBtn*yBtn, xBtn):
                    if playArea[i-1] == -1: k += 1              # слева
                    if i > xBtn-1:
                        if playArea[i-xBtn-1] == -1: k += 1     # слева сверху
                    if i < xBtn*yBtn-xBtn:
                        if playArea[i+xBtn-1] == -1: k += 1     # слева снизу
                if i not in range(-1, xBtn*yBtn, xBtn):
                    if playArea[i+1] == -1: k += 1              # справа
                    if i > xBtn-1:
                        if playArea[i-xBtn+1] == -1: k += 1     # справа сверху
                    if i < xBtn*yBtn-xBtn:
                        if playArea[i+xBtn+1] == -1: k += 1     # справа снизу
                if i > xBtn-1:
                    if playArea[i-xBtn] == -1: k += 1           # сверху
                if i < xBtn*yBtn-xBtn:
                    if playArea[i+xBtn] == -1: k += 1           # снизу
                playArea[i] = k
    if btn[n].cget('text') == imgMark:                          # Если поле было промаркировано
        mrk -= 1
        tk.title('Achtung, '+str(mines-mrk)+' Minen!')
    btn[n].config(text=playArea[n], state=DISABLED, bg='white') # Отображаем игровую ситуацию
    if playArea[n] == 0:                                        # Пустое поле без соседей
        btn[n].config(text=' ', bg='#ccb')
    elif playArea[n] == -1:                                     # Ой мина!
        btn[n].config(text=imgMine)
        if nMoves <= (xBtn*yBtn - mines) or mines >= mrk:       # Если игрок ещё не выиграл, то проиграл
            nMoves = 32000                                      # Если проиграл, то уже не выиграет
            chainReaction(0)                                    # Цепная реакция
            tk.title('Your game is over.')
    if nMoves == (xBtn*yBtn - mines) and mines == mrk:          # Если все клетки открыты и мины помечены
        tk.title('You win! '+str(int(time.time() - playTime))+' сек')
        winner(0)

def chainReaction(j):                               # Цепная реакция
    if j <= len(playArea):                          # Если не запустили новую игру
        for i in range(j, xBtn*yBtn):
            if playArea[i] == -1 and btn[i].cget('text') == ' ':
                btn[i].config(text=imgMine)
                btn[i].flash()
                tk.bell()
                tk.after(50, chainReaction, i + 1)
                break

def winner(j):                                      # Победа
    if j <= len(playArea):                          # Если не запустили новую игру
        for i in range(j, xBtn*yBtn):
            if playArea[i] == 0:
                btn[i].config(state=NORMAL, text='☺')
                btn[i].flash()
                tk.bell()
                btn[i].config(text=' ', state=DISABLED)
                tk.after(50, winner, i + 1)
                break

def marker(n):                                      # помечаем то, где возможно скрывается мина.
    global mrk, mines, playTime
    if (btn[n].cget('state')) != 'disabled':
        if btn[n].cget('text') == imgMark:
            btn[n].config(text=' ')
            mrk -= 1
        else:
            btn[n].config(text=imgMark, fg='blue')
            mrk += 1
        tk.title('Achtung, '+str(mines-mrk)+' Minen!')
    if nMoves == (xBtn*yBtn - mines) and mines == mrk:          # Если все клетки открыты и мины помечены
        tk.title('You win! '+str(int(time.time() - playTime))+' сек')
        winner(0)

def newGame():
    global xBtn, yBtn, mines, nMoves, mrk
    mines = xBtn * yBtn * 10 // 64
    nMoves = 0; mrk=0
    playArea.clear()
    if len(btn) != 0:
        for i in range (0, len(btn)):
            btn[i].destroy()
        btn.clear()
        for i in range (0, len(frm)):
            frm[i].destroy()
        frm.clear()
    playground()
    tk.title('Achtung, '+str(mines-mrk)+' Minen!')

def set5x5():
    global xBtn, yBtn
    xBtn = 5; yBtn = 5
    newGame()

def set8x8():
    global xBtn, yBtn
    xBtn = 8; yBtn = 8
    newGame()

def set10x14():
    global xBtn, yBtn
    xBtn = 10; yBtn = 14
    newGame()

def set16x16():
    global xBtn, yBtn
    xBtn = 16; yBtn = 16
    newGame()

def set32x32():
    global xBtn, yBtn
    xBtn = 32; yBtn = 32
    newGame()

def playground():
    global xBtn, yBtn
    for i in range(0, yBtn):
        frm.append(Frame())
        frm[i].pack(expand=YES, fill=BOTH)
        for j in  range(0, xBtn):
            btn.append(Button(frm[i], text=' ',font=('mono', 16, 'bold'),
                              width=1, height=1, padx=0, pady=0))
    for i in  range(0, xBtn*yBtn):
        if xBtn*yBtn > 256:
            btn[i].config(font=('mono', 8, 'normal'))
        btn[i].config(command=lambda n=i: play(n))
        btn[i].bind('<Button-3>', lambda event, n=i: marker(n))
        btn[i].pack(side=LEFT, expand=YES, fill=BOTH, padx=0, pady=0)
        btn[i].update()
        playArea.append(0)                      # Создаём элементы списка playArea

frmTop = Frame()                                # Создаём кнопки "New game"
frmTop.pack(expand=YES, fill=BOTH)
Label(frmTop, text=' Новая игра:  ').pack(side=LEFT, expand=NO, fill=X, anchor=N)
Button(frmTop, text='5x5', font=(16),
       command=set5x5).pack(side=LEFT, expand=YES, fill=X, anchor=N)
Button(frmTop, text='8x8', font=(16),
       command=set8x8).pack(side=LEFT, expand=YES, fill=X, anchor=N)
Button(frmTop, text='10x14', font=(16),
       command=set10x14).pack(side=LEFT, expand=YES, fill=X, anchor=N)
Button(frmTop, text='16x16', font=(16),
       command=set16x16).pack(side=LEFT, expand=YES, fill=X, anchor=N)
Button(frmTop, text='32x32', font=(16),
       command=set32x32).pack(side=LEFT, expand=YES, fill=X, anchor=N)

mainloop()

Рис. Игра Сапёр с полем 32х32