«Где ест уж» — логическая игра-головоломка на поле 3 х 3 клетки. В начале игры на поле, в клетках размещены  буквы, составляющие текст, ГДЕ ЕСТ ЖУ. Цель игры: переставить местами буквы Ж и У. Напишем программу игры Где ест уж на Python с библиотекой tkinter.

Создадим поле из 9 кнопок для игры «Где ест уж».

from tkinter import *

for i in range(9):
    Button().pack()

mainloop()

Лист. 1.

В программе листинг 1, в первой строке мы импортируем все функции из библиотеки tkinter. Графическая библиотека tkinter входит в стандартный дистрибутив Python версии 3 и старше и служит для создания графического интерфейса программ на Pyton. В версии 2.7 имеется библиотека Tkinter с тем же функционалом.

Ключевое слово for создаёт цикл, в котором переменная i последовательно принимает значения из диапазона range(9).

Функция range() создаёт диапазон значений. Параметрами функции range() могут быть границы диапазона и приращение (правая граница не включается в диапазон). 

Цикл for создаётся с помощью ключевых слов for и in, определение цикла заканчивается двоеточием :. После двоеточия начинается тело цикла. Каждый оператор тела  цикла, если он пишется с новой строки, должен иметь отступ слева.

Для выделения блока операторов в python, (например, тела цикла) рекомендуется добавлять перед каждым оператором блока 4 пробела. 

Цикл for i in range(9): выполняется 9 раз, при этом переменная i принимает значения от 0 до 9 с шагом 1. 0 включительно, 9 исключительно. Так как мы указали только один параметр диапазона range(), диапазон создан с левой границей 0 и шагом 1 по умолчанию. 9 — правая граница диапазона. 

В цикле for выполняется конструктор Button() с методом pack(). 

Конструктор Button() создаёт объект кнопка. Метод pack() отображает объект кнопку в окне программы. Окно программы, создаётся библиотекой tkinter по умолчанию, в тот момент, когда мы создаём в программе визуальный объект (кнопку, например). Конструктор Button() выполняется в цикле 9 раз и наша программа, листинг 1, создаёт 9 кнопок в окне нашего приложения, см. рис. 1.

Рис. 1.

from tkinter import *

for i in range(9):
    Button().pack(expand=YES, fill=BOTH)

mainloop()

Лист. 2

В программе листинг 2 в методе pack() мы добавили два параметра.

Параметр expand со значением YES применяется для того, чтобы визуальный объект мог занять всё свободное пространство в контейнере. В нашем случае, кнопка (визуальный объект) размещается в окне программы (контейнер).

Свойство fill со значением BOTH позволяет визуальному объекту растягиваться в обоих направлениях, по X и по Y до размера контейнера.

Результат работы программы лист 2 смотрите на рисунке 2.

Рис. 2.

from tkinter import *

Tk().title('Переставьте местами Ж и У')

for i in range(9):
    Button().pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 3.

В программе листинг 3 параметр side со значением LEFT в методе pack() прижимает визуальный объект, при размещении в контейнере, влево. Смотрите рисунок 3. Кроме того, мы явно создали окно приложения конструктором Tk() и методом title() вывели цель игры в заголовке этого окна.

Рис. 3.

from tkinter import *

Tk().title('Переставьте местами Ж и У')

f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    Button(f).pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 4.

Рис. 4.

from tkinter import *

Tk().title('Переставьте местами Ж и У')

f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    Button(f).pack(expand=YES, fill=BOTH, side=LEFT)

f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    Button(f).pack(expand=YES, fill=BOTH, side=LEFT)
    
f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    Button(f).pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 5.

Рис. 5.

from tkinter import *
btn = []

Tk().title('Переставьте местами Ж и У')

f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    btn += [Button(f)]

f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    btn += [Button(f)]
    
f = Frame()
f.pack(expand=YES, fill=BOTH)
for i in range(3):
    btn += [Button(f)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 6.

В программе листинг 6 мы создали пустой список btn, в него будем добавлять кнопки.

from tkinter import *
btn = []

Tk().title('Переставьте местами Ж и У')

for i in range(3):
    f = Frame()
    f.pack(expand=YES, fill=BOTH)
    for i in range(3):
        btn += [Button(f)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 7.

В программе листинг 7 мы создали пустой список btn, в него будем добавлять кнопки.

from tkinter import *

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)

mainloop()

Лист. 8.

В программе листинг 8 мы создали список с именем frm, содержащий три фрейма, созданных конструктором Frame(). Фрейм — это контейнер для других визуальных объектов. По умолчанию фрейм невидим и прозрачен.

В цикле for i in frm, переменная i, по очереди, получает значение элементов списка frm. То есть, i в этом цикле является указателем на каждый из трёх фреймов, по очереди. В теле этого цикла мы размещаем невидимые фреймы на экране растянутыми по горизонтали методом pack() с соответствующими параметрами (expand=YES, fill=BOTH). Фреймы занимают место в окне программы так же, как его занимали кнопки на рис. 2, с той лишь разницей, что фреймов у нас 3.

В том же цикле for i in frm, в каждой итерации цикла с помощью конструктора Button() мы создаём по три кнопки. Параметр i конструктора Button() указывает в каком фрейме должны быть размещены очередные три кнопки. Так как в цикле for i in frm по три раза выполняются функции и операторы тела цикла, создаётся 9 кнопок. Все кнопки (как объекты) добавляются в список btn оператором +=.

В следующем цикле for i in range(9), переменная i, по очереди, получает значение из диапазона созданного функцией range(). Таким образом создан цикл из 9-ти повторений. В теле этого цикла всего одна строка, и в ней, к каждой кнопке применяется метод pack(). Переменная i используется в теле цикла как индекс элемента в списке btn. То есть i - это номер кнопки в списке btn, а btn[i] - это конкретная кнопка (объект) в этом списке.

В дальнейшем,  у нас возникнет необходимость обращаться к кнопкам, как к объектам, по имени. Поэтому, в программе листинг 6 мы создали, сначала, пустой список btn в который добавили 9 кнопок. Теперь к любой кнопке, как к объекту, мы можем обращаться как к элементу списка btn с индексом в квадратных скобках. Например, btn[3] - четвёртая кнопка, которая в окне программы находится в среднем ряду слева.

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i])

mainloop()

Лист. 9.

В программе листинг 9 мы создали список letter состоящий из 9-ти элементов. Элементами этого списка являются строки. Каждая строка содержит по одному символу (буква в кавычках). Последний элемент этого списка, символ пробела в кавычках. 

В цикле for i in range(9), мы размещаем на каждой кнопке, методом config() по одному элементу из списка letter. Для этого, в методе config изменяем свойство кнопки text. В результате на кнопках появляются буквы и пробобел см. рис. 5.

Рис. 5.

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

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)

mainloop()

Лист. 10.

В программе, листинг 10, методом config() мы меняем свойство кнопки font и width. Свойство width=2 задаёт ширину кнопки в два символа. Свойство font принимает сразу несколько параметров в кортеже. Элементами кортежа со свойствами font являются: имя или тип шрифта в операционной системе как строковое выражение, размер шрифта - целое число, начертание шрифта как строковое выражение. Результат работы программы 6 представлен на рисунке 6.

Рис. 6

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    print(n)

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 11.

В программе листинг 11 с помощью ключевого слова  def мы создали функцию play(). Новые функции, созданные программистом в программе называют функциями пользователя. Новые функции, обычно, создают в начале программы после определения глобальных переменных, до их первого упоминания в коде программы.

В теле функции play() мы прописали метод config() применяемый к кнопке по которой кликнул игрок. Параметр text метода config() выводит на кнопку надпись. Функция str() превращает число в строку. Параметр n, передаваемый в функцию play() это номер кнопки по которой кликнул игрок.

Функция play() будет зарегистрирована как обработчик события "клик мышью по кнопке". Обработчик события по умолчанию для кнопки можно зарегистрировать методом config(). Параметр command метода config() регистрирует обработчик события. Так как значением параметра command должно быть имя функции без скобок, мы для передачи параметра в функцию play в случае возникновения события, воспользовались lambda функцией

Первый параметр lambda функции n вычисляется в процессе создания объекта кнопка, а второй параметр, указанный в lambda функции после двоеточия, передаётся для регистрации в качестве обработчика события. А в lambda функции мы указываем функцию play() с круглыми скобками и передаваемым параметром n. Причём, параметр n вычисляется на этапе создания кнопки и сохраняется в свойстве command как значение. То есть, функция play, как обработчик события будет вызвана с параметром номер кнопки, который был вычислен на этапе создания кнопки. 

=========== RESTART: /home/dior/Python/Tkinter/Где ест уж/tk017.py ===========
0
1
2
3
4
5
6
7
8

Рис. 7.

На рис. 7 вы видите результат работы программы листинг 7. Программист кликает по кнопкам, a функция print() в функции play() выводит в консоль Python номер объекта (кнопки) в списке btn. 

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    letter[m], letter[n] = letter[n], letter[m]
    print(letter)

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 12.

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

=========== RESTART: /home/dior/Python/Tkinter/Где ест уж/tk017.py ===========
['Г', 'Д', 'Е', 'Е', 'С', ' ', 'Ж', 'У', 'Т']
['Г', 'Д', 'Е', 'Е', 'С', ' ', 'Ж', 'У', 'Т']
['Г', 'Д', 'Е', 'Е', ' ', 'С', 'Ж', 'У', 'Т']
['Г', 'Д', 'Е', ' ', 'Е', 'С', 'Ж', 'У', 'Т']
[' ', 'Д', 'Е', 'Г', 'Е', 'С', 'Ж', 'У', 'Т']
['Д', ' ', 'Е', 'Г', 'Е', 'С', 'Ж', 'У', 'Т']
['Д', 'Е', ' ', 'Г', 'Е', 'С', 'Ж', 'У', 'Т']
>>> 

Рис. 8.

На рис. 8 вы видите результат работы программы листинг 8. Программист кликает по кнопкам, a функция print() в функции play() выводит в консоль список letter. Здесь можно убедиться, что когда должны меняться буквы на кнопках, меняются буквы в списке letter. 

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    letter[m], letter[n] = letter[n], letter[m]
    btn[m].config(text=letter[m])
    btn[n].config(text=' ')

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 14.

Добавим в игру правила, по которым можно двигать буквы.

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    if m==8 and (n==7 or n==5) or m==7 and(n==6 or n==8 or n==4):
        letter[m], letter[n] = letter[n], letter[m]
        btn[m].config(text=letter[m])
        btn[n].config(text=' ')

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 15.

Немного упростим логическое выражение в функции play()

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    if (m in [1, 4, 7] or n in [1, 4, 7]) and (n-m==1 or m-n==1) or (m in [3, 4, 5] or n in [3, 4, 5]) and (n-m==3 or m-n==3):
        letter[m], letter[n] = letter[n], letter[m]
        btn[m].config(text=letter[m])
        btn[n].config(text=' ')

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 16

Ещё раз упростим логическое выражение в функции play()

from tkinter import *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    if (m in [1, 4, 7] or n in [1, 4, 7]) and abs(n-m)==1 or (m in [3, 4, 5] or n in [3, 4, 5]) and abs(n-m)==3:
        letter[m], letter[n] = letter[n], letter[m]
        btn[m].config(text=letter[m])
        btn[n].config(text=' ')

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 17.

Заново перепишем логическое выражение в функции play()

#!/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 want to thank Sergey Polozkov for checking the code for hidden errors.
# I want to thank Sergey Polozkov for the idea of creating this puzzle.
# I want to thank all my students for the inspiration they give me.
#
# puzzle.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 *

letter = ['Г', 'Д', 'Е', 'Е', 'С', 'Т', 'Ж', 'У', ' ']

def play(n):
    m = letter.index(' ')
    if (abs(m - n) + abs(n//3 - m//3)) == 1 or abs(m - n) == 3:
        letter[m], letter[n] = letter[n], letter[m]
        btn[m].config(text=letter[m])
        btn[n].config(text=' ')

Tk().title('Переставьте местами Ж и У')

btn = []
frm = [Frame(), Frame(), Frame()]
for i in frm:
    i.pack(expand=YES, fill=BOTH)
    btn += [Button(i), Button(i), Button(i)]

for i in range(9):
    btn[i].pack(expand=YES, fill=BOTH, side=LEFT)
    btn[i].config(text=letter[i], font=('mono', 60, 'bold'), width=2)
    btn[i].config(command=lambda n=i:play(n))

mainloop()

Лист. 18.

В программе листинг 15 добавлены правила игры, строка

if (abs(m - n) + abs(n//3 - m//3)) == 1 or abs(m - n) == 3:

 Рис. 9.