От простого к сложному, пишем на Python игру-головоломку в стиле Puzzle. По русски - пятнашки. Программируем графический интерфейс (GUI) с применением библиотеки Tkinter.

План работы:

  1. Создание графического интерфейса. 
  2. Программирование движений. 
  3. Программирование движения по правилам
  4. Перемешивание кнопок. 

Программирование графического интерфейса

from tkinter import *

Button().pack()
Button().pack()
Button().pack()
Button().pack()
 
mainloop()

Прог. 1. Создание кнопок для игры.

Рис. 1. Создание оконного графического интерфейса (GUI) для игры "Пятнашки".

from tkinter import *

Button().pack(side=LEFT, expand=YES, fill=BOTH)
Button().pack(side=LEFT, expand=YES, fill=BOTH)
Button().pack(side=LEFT, expand=YES, fill=BOTH)
Button().pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 2. Размещение кнопок в один ряд. 

Рис. 2. Создание оконного графического интерфейса (GUI) для игры "Пятнашки".

from tkinter import *

frm = Frame()
frm.pack(expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 3. Размещение 4-х кнопок в одном фрейме.

Задание: В программе 3 скопируйте 6 строк, начиная со строки frm... и вставьте эту копию перед строкой mainloop() 3 раза. Запустите программу и посмотрите что у Вас получилось. 

from tkinter import *

frm = Frame()
frm.pack(expand=YES, fill=BOTH)
for j in  range(0, 4):
    Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 4. Размещение 4-х кнопок в одном фрейме с использованием цикла for.

from tkinter import *

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        Button(frm).pack(side=LEFT, expand=YES, fill=BOTH)
 
mainloop()

Прог. 5.  Размещение 16-ти кнопок в 4-х фреймах с использованием вложенных циклов for.

Рис. 3. Создание оконного графического интерфейса (GUI) для игры "Пятнашки".

from tkinter import *

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        Button(frm, text=i*4+j).pack(side=LEFT, expand=YES, fill=BOTH)

mainloop()

Прог. 6. Размещение на кнопках их номеров.

Рис. 4. Создание оконного графического интерфейса (GUI) для игры "Пятнашки".

from tkinter import *

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        Button(frm, text=i*4+j, font=('mono', 20, 'bold'),
               width=3, height=2).pack(side=LEFT, expand=YES, fill=BOTH)

mainloop()

Прог. 7. Добавлен стиль кнопок.

Рис. 5. Создан оконный графический интерфейс (GUI) для игры "Пятнашки".

Программирование движений кнопок

from tkinter import *
btn = []

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

print(*btn, sep='\n')
mainloop()

Прог. 8. Создан список кнопок.

====== RESTART: ~/puzle06.py ======
.!frame.!button
.!frame.!button2
.!frame.!button3
.!frame.!button4
.!frame2.!button
.!frame2.!button2
.!frame2.!button3
.!frame2.!button4
.!frame3.!button
.!frame3.!button2
.!frame3.!button3
.!frame3.!button4
.!frame4.!button
.!frame4.!button2
.!frame4.!button3
.!frame4.!button4

Листинг 1. Список кнопок.

from tkinter import *
btn = []

def play(n):
    print(n)

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        btn += [Button(frm, text=i*4+j, font=('mono', 20, 'bold'),
               width=3, height=2, command=lambda n=i*4+j: play(n))]
        btn[i*4+j].pack(side=LEFT, expand=YES, fill=BOTH)

mainloop()

Прог. 9. Создана функция play() и функция play() зарегистрирована как обработчик события "нажатие на кнопку".

======== RESTART: ~/puzzle.py ========
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Листинг 2. Номера кнопок.

Если 0 — это пустая клетка, то мы уже имеем нерешаемую игровую ситуацию. Пустая клетка должна быть в конце поля — в правом нижнем углу.

from tkinter import *
btn = []
playArea = list(range(1, 16))
playArea.append(0)

def play(n):
    print(n)

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        btn += [Button(frm, text=playArea[i*4+j], font=('mono', 20, 'bold'),
                       width=3, height=2,
                       command=lambda n=i*4+j: play(n))]
        btn[i*4+j].pack(side=LEFT, expand=YES, fill=BOTH)

mainloop()

Прог. 10. Создан список playArea. 

Рис. 6. Кнопки расположены а правильном для игры "Пятнашки" порядке. 

В списке playArea = list(range(1, 16), индекс этого списка — номер кнопки, элемент списка — цифра на кнопке. Последний элемент этого списка мы добавили со значением 0.

Для написания функции play() нам осталось:

  • Найти m = playArea.index(0) — номер пустой кнопки.
  • Определить условие при котором кнопка играет
  • Поменять местами кнопку с цифрой и пустую кнопку playArea[m] = playArea[n]; playArea[n] = 0 Или playArea[m], playArea[n] = playArea[n], playArea[m]
  • Изменить свойство text у тех кнопок которые сыграли
from tkinter import *
btn = []
playArea = list(range(1, 16))
playArea.append(0)

def play(n):
    m = playArea.index(0)
    playArea[m], playArea[n] = playArea[n], playArea[m]
    btn[m].config(text=playArea[m])
    btn[n].config(text=" ")

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        btn += [Button(frm, text=playArea[i*4+j], font=('mono', 20, 'bold'),
                       width=3, height=2,
                       command=lambda n=i*4+j: play(n))]
        btn[i*4+j].pack(side=LEFT, expand=YES, fill=BOTH)

btn[15].config(text=" ")
mainloop()

Прог. 11. Создана функция play(), без учёта правил игры и удалён символ "0" с соответствующей кнопки.

Рис. 7. На кнопке №16 цифра не отображается.

from tkinter import *
btn = []
playArea = list(range(1, 16))
playArea.append(0)

def play(n):
    m = playArea.index(0)
    if abs(m - n) == 1 and n//4 == m//4 or abs(m - n) == 4:
        playArea[m], playArea[n] = playArea[n], playArea[m]
        btn[m].config(text=playArea[m])
        btn[n].config(text=" ")

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        btn += [Button(frm, text=playArea[i*4+j], font=('mono', 20, 'bold'),
                       width=3, height=2,
                       command=lambda n=i*4+j: play(n))]
        btn[i*4+j].pack(side=LEFT, expand=YES, fill=BOTH)

btn[15].config(text=" ")
mainloop()

Прог. 12. Учтены правила игры.

Неплохо бы, в начале игры случайным образом понажимать кнопочки несколько тысяч раз.

#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# Puzzle.py
# Copyright (C) 2021 Aleksandr Diorditsa, see <https://adior.ru>
# I thank Sergey Polozkov for checking the code for hidden errors.
# I want to thank all my students for the inspiration they give me.
#
# brownian-motion.py is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# Puzzle.py is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

from tkinter import *
from  random import choice
btn = []
playArea = list(range(1, 16))
playArea.append(0)

def play(n):
    m = playArea.index(0)
    if abs(m - n) == 1 and n//4 == m//4 or abs(m - n) == 4:
        playArea[m], playArea[n] = playArea[n], playArea[m]
        btn[m].config(text=playArea[m])
        btn[n].config(text=" ")

for i in range(0, 4):
    frm = Frame()
    frm.pack(expand=YES, fill=BOTH)
    for j in  range(0, 4):
        btn += [Button(frm, text=playArea[i*4+j], font=('mono', 20, 'bold'),
                       width=3, height=2,
                       command=lambda n=i*4+j: play(n))]
        btn[i*4+j].pack(side=LEFT, expand=YES, fill=BOTH)

for i in range(0, 3000):
    play(choice(range(0, 16)))
mainloop()

Прог. 14. Кнопочки перемешаны в случайном порядке.

Рис. 8. Игра "Пятнашки", кнопки перемешаны. 

И ещё один пример программы Puzzle, с использованием класса. Этот вариант игры Puzzle позволяет выбрать размер поля с помощью константы SIZE.

#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# puzzle.py
# Copyright (C) 2021 Aleksandr Diorditsa <a-dior ? yandex.ru>
# I thank Sergey Polozkov for checking the code for hidden errors.
#
# OS.py is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# puzzle.py is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

from tkinter import *
from  random import choice
SIZE = 7
frm = []; btn = []
playArea = list(range(1, SIZE*SIZE)) + [0]

class puzzle(Button):
    def __init__(self, number=0, parent=None, **config):
        self.number = number
        Button.__init__(self, parent, **config)
        self.pack(side=LEFT, expand=YES, fill=BOTH)
        self.config(font=('mono', 20, 'bold'), width=3, height=2, command=self.play)
 
    def play(self):
        m = playArea.index(0)
        if (abs(m - self.number) + abs(self.number//SIZE - m//SIZE)) == 1 or abs(m - self.number) == SIZE:
            playArea[m], playArea[self.number] = playArea[self.number], playArea[m]
            btn[m].config(text=playArea[m])
            self.config(text=" ")
 
for i in range(0, SIZE):
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, SIZE):
        btn.append(puzzle((i*SIZE+j), frm[i]))

for i in range(0, SIZE**6):
    btn[choice(range(0, SIZE*SIZE))].play()

mainloop()

Оживить игру можно анимацией созданной с помощью функции after() и для азарта добавим счётчик времени из библиотеки time.

#!/usr/bin/env python
# -*- Mode: Python; coding: utf-8; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- 
#
# puzzle.py
# Copyright (C) 2021 Aleksandr Diorditsa <a-dior ? yandex.ru>
# I thank Sergey Polozkov for checking the code for hidden errors.
#
# OS.py is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# puzzle.py is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License along
# with this program.  If not, see <http://www.gnu.org/licenses/>.

from tkinter import *
from  random import choice
from  time import time
SIZE = 4
frm = []; btn = []
playArea = list(range(1, SIZE*SIZE)) + [0]
game = False
timeGame = time()

class puzzle(Button):
    def __init__(self, number=0, parent=None, **config):
        self.number = number
        Button.__init__(self, parent, **config)
        self.pack(side=LEFT, expand=YES, fill=BOTH)
        self.config(font=('mono', 20, 'bold'), width=3, height=2, command=self.play)
 
    def play(self):
        m = playArea.index(0)
        if (abs(m - self.number) + abs(self.number//SIZE - m//SIZE)) == 1 or abs(m - self.number) == SIZE:
            playArea[m], playArea[self.number] = playArea[self.number], playArea[m]
            btn[m].config(text=playArea[m])
            self.config(text=" ")
            global game
            if game:
                for i in range(0, SIZE*SIZE):
                    if playArea[i] != i+1:
                        break
                    if i+2 == SIZE*SIZE:
                        tk.title("You Win! "+str(int(timeGame-time()))+"сек.")                    
                        game = False
                        global sort
                        sort = 0
                        tk.after(5000, newGame)

tk = Tk()
for i in range(0, SIZE):
    frm.append(Frame())
    frm[i].pack(expand=YES, fill=BOTH)
    for j in  range(0, SIZE):
        btn.append(puzzle((i*SIZE+j), frm[i]))

sort = 0
def newGame():
    global sort
    sort += 1
    if sort < SIZE**5:
        m = playArea.index(0)
        n = choice(range(0, SIZE*SIZE))
        if (abs(m - n) + abs(n//SIZE - m//SIZE)) == 1 or abs(m - n) == SIZE:
            btn[n].play()
            tk.after(1, newGame)
        else:
            newGame()
    else:
        tk.title("Puzzle " + str(SIZE))
        global game
        game = True
        timeGame = time()

newGame()