Цель:
Подробности указаны далее.
Видимое поведение игры должно соответствовать алгоритму указанному ниже, при этом исходный код программы может работать по другому алгоритму.
Примечание: Переход в состояние - изменяет значение состояние игры на указанное, никаких других действия не происходит; Выполнение действия - выполняет указанное действие, после чего алгоритм продолжает основное действие; Переход к действию - выполняет указанное действие, при этом основное действие прерывается.
Действие Инициализация игры:
Действие Добавление шариков на игровое поле:
Проверяется буфер подсказки. В этом буфере хранятся цвета шариков которые появятся на поле на следующем ходе. Если буфер подсказки пустой, то генерируется 3 шарика случайного цвета и помещаются в буфер.
Из буфера подсказки достаются три шарика и добавляются на игровое поле в случайные места;
Если в процессе добавления очередного шарика на игровое поле для него НЕ оказывается свободного места:
После добавления всех шариков на игровое поле генерируется 3 новых шарика случайного цвета и помещаются в буфер подсказки.
Действие Проверка линий:
В результате хода пользователя может быть создано не более одной независимой линии. В результате добавления 3х рандомных шариков - не более 3х. Будем называть линии зависимыми, если при удалении одной из них остальные разрушаются. Данный этап сильно зависит от алгоритма поиска линий.
Выполняется проверка игрового поля на присутствие линии из шариков одного цвета. Линия из шариков - это ровно 5 шариков расположенных подряд:
Если найдены линии:
Шарики входящие в состав линии удаляются с игрового поля:
Если удаляется выбранный шарик:
Количество очков увеличивается на 2 за каждый удалённый шарик;
На данном этапе все независимые линий должны быть удалены.
Если линии НЕ найдены:
Если игра находится в состоянии Шарик перемещён:
Если игра НЕ находится в состоянии Шарик перемещён, то выполняется проверка наличия хотя бы одной пустой плитки на игровом поле:
Если пользователь щёлкает ЛКМ в состоянии Шарик НЕ выбран по:
Пустой плитке. Состояние игры не изменяется;
Если пользователь щёлкает по шарику:
Если пользователь щёлкает ЛКМ в состоянии Шарик выбран по:
Пустой плитке:
Выполняется проверка доступности плитки. Плитка называется доступной, если можно переместить шарик с текущей плитки на указанную, двигая его любое количество раз вверх, вниз, влево или вправо, при этом в процессе перемещения шарик должен двигаться только по пустым плиткам. Кратчайший путь определять не обязательно;
Если плитка НЕ доступна. Состояние игры не меняется;
Если плитка доступна:
Если пользователь щёлкает по шарику:
Если шарик тот же. Состояние игры не меняется;
Если шарик другой:
Действие Завершение игры:
При старте приложения выполняется переход к действию Инициализация игры.
Если на игровом поле нажата кнопка "Новая игра", то выполняется переход к действию Инициализация игры.
Если на игровом поле нажата кнопка "Сделать ход", то, если игра НЕ находится в состоянии Игра окончена, то:
Дизайн игры должны соответствовать игре представленной по ссылке. Дизайн игра изменять можно, но только в сторону эстетического улучшения (лучше получилось или хуже решает преподаватель :)). При разработке игры можно воспользоваться данными картинками.
Для упрочения задания анимацию появления новых шариков, перемещения шарика и увеличения количества очков реализовывать не обязательно.
Для работы с изображениями понадобится библиотека Pillow
. Т.к. она не входит в состав стандартных, то её необходимо установить. Если у вас уже установлена библиотека PIL
, используйте её или удалите перед установкой Pillow
. Чтобы установить/обновить Pillow
наберите в командной строке:
pip install --upgrade Pillow
Для размещения элементов в окне удобно использовать упаковщик grid
(видео, текст). Изучите образец интерфейса игры и подумайте как можно уложить представленные элементы в сетку (кнопки, подсказку, счёт тоже).
Код демонстрирующий работу упаковщика grid
xxxxxxxxxx
from tkinter import *
root = Tk()
for row in range(2):
for col in range(2):
lbl = Label(root,
text=', '.join([str(row), str(col)]),
bg="sky blue",
borderwidth=50) # Ширина гриницы Lable
lbl.grid(row=row, # Номер строки начиная с 0
column=col, # Номер столбца начиная с 0
padx=1, # Отступ от левого и правого края ячейки сетки
pady=1) # Отступ от верхнего и нижнего края ячейки сетки
root.mainloop()
Для размещения элемента в произвольное место окна удобно использовать упаковщик place
(текст).
Код демонстрирующий работу упаковщика place
xxxxxxxxxx
from tkinter import *
root = Tk()
root.geometry("290x300")
for i in range(4):
lbl = Label(root, text=str(i), bg="sky blue", borderwidth=50)
lbl.place(x=60*i, # Смещение влева на право в пикселах
y= 60*i) # Смещение сверху вниз в пикселах
root.mainloop()
Для загрузки изображения в виджет используйте параметр image
. В примере ниже картинка ball-green.png
должна лежать в папке со скриптом.
Внимание: загруженная картинка обязательно должна быть привязана к какой-нибудь переменной (в примере это img
) иначе она может быть удалена сборщиком мусора и перестанет отображаться или вызовет ошибку.
xxxxxxxxxx
from tkinter import *
from PIL import ImageTk
root = Tk()
img = ImageTk.PhotoImage(file="ball-green.png")
Label(root, image=img, borderwidth=0).pack()
root.mainloop()
Для реакции на щелчок мыши используем метод bind
. В данном примере, по щелчку ЛКМ по лейблу у него должна поменяться картинка на img2.png
. В примере привязываем одну и туже функцию ко всем лейблам, а для того, чтобы знать по какому из них щелкнули используем параметр event
. Данный параметр содержит поле widget
- это и есть лейбл по которому был щелчок. Далее при помощи метода config
меняем его картинку.
xxxxxxxxxx
from tkinter import *
from PIL import ImageTk
def set_img2(event):
event.widget.config(image=img_2)
root = Tk()
img = ImageTk.PhotoImage(file="ball-green.png")
img_2 = ImageTk.PhotoImage(file="ball-blue.png")
for row in range(2):
for col in range(2):
lbl = Label(root, image = img, borderwidth=0)
lbl.bind("<Button-1>", set_img2)
lbl.grid(row=row, column=col, padx=1, pady=1)
root.mainloop()
В предыдущих примерах можно было бы использовать виджет (кнопка) Button
вместо (метка) Label
, но т.к. Button
имеет анимацию нажатия, для игрового поля он не очень подходит. Но для кнопок "Новая игра" и "Сделать ход" можно использовать его.
В случае если изображения даны не в виде отдельных картинок, а в виде нескольких картинок склеенных вместе (tileset) нужно иметь возможность вырезать нужный фрагмент изображения. В данном примере исходный файл cell-bgr.png
содержит изображение выбранной и НЕ выбранной плитки. После загрузки нарезаем картинку при помощи метода crop
. Метод принимает 4 параметра: x и y координату левого верхнего угла нужной части изображения и, x и y координату правого нижнего угла нужной части изображения. Координаты указываются в пикселях и легко определяются в любом графическом редакторе.
xxxxxxxxxx
from tkinter import *
from PIL import Image, ImageTk
def set_img2(event):
event.widget.config(image=img_2)
root = Tk()
tileset = Image.open("cell-bgr.png")
img = ImageTk.PhotoImage(tileset.crop((1, 0, 67, 66)))
img_2 = ImageTk.PhotoImage(tileset.crop((1, 69, 67, 135)))
for row in range(2):
for col in range(2):
lbl = Label(root, image = img, borderwidth=0)
lbl.bind("<Button-1>", set_img2)
lbl.grid(row=row, column=col, padx=1, pady=1)
root.mainloop()
В нашем случае в игре 7 цветов шариков и 2 цвета плитки, итого получается 14 вариантов картинок, но если ещё делать анимацию, то количество вариантов существенно возрастёт. Чтобы решить данную проблему будем накладывать картинку шарика поверх картинки плитки прямо в программе. Это можно сделать двумя способами:
При помощи метода paste
. Плюсом данного метода является возможность наложить друг на друга картинки разных размеров. Для того, чтобы учесть прозрачность накладываемой картинки есть возможность задать маску. В этом как раз и заключается основной минус метода paste
. Маска имеет только 2 уровня: пиксел прозрачный и пиксел НЕ прозрачный, поэтому наложенная картинка шарика будет выглядеть грубовато, т.к. у неё больше уровней прозрачности:
xxxxxxxxxx
from tkinter import *
from PIL import Image, ImageTk
root = Tk()
bgr = Image.open("page-bgr.png").convert('RGBA')
ball = Image.open("ball-blue.png").convert('RGBA')
# Метод paste изменяет изображение-подложку
bgr.paste(ball, # Накладываемая картинка
(0,0), # Сместить картинку на (x, y) пикселей
ball) # Маска прозрачности. Делаем из самого изображения
img = ImageTk.PhotoImage(bgr)
Label(root, image = img, borderwidth=0).pack()
root.mainloop()
При помощи метода alpha_composite
. Плюсом данного метода является возможность наложить друг на друга картинки с учётом альфа-канала (прозрачность). Т.е. полупрозрачные участки изображения (например тени) получатся лучшего качества. Минус данного метода в том, что изображения должны быть одинакового размера, иначе программа падает с ошибкой.
Код приведённый ниже позволяет использовать alpha_composite
с изображениями разного размера.
xxxxxxxxxx
from tkinter import *
from PIL import Image, ImageTk
root = Tk()
bgr = Image.open("page-bgr.png").convert('RGBA')
ball = Image.open("ball-blue.png").convert('RGBA')
pic_size_same_as_bgr = Image.new("RGBA", bgr.size)
pic_size_same_as_bgr.paste(ball, (70,0)) # Маска не нужна
ball_over_bgr = Image.alpha_composite(bgr, pic_size_same_as_bgr)
ball_over_bgr.paste(ball, (0,0), ball) # Для сравнения (можно убрать)
img = ImageTk.PhotoImage(ball_over_bgr)
Label(root, image = img, borderwidth=0).pack()
root.mainloop()
Не забываете, что изображения, которые вы планируете использовать в дальнейшем, должны быть связаны с переменными. Как вариант, можно сгенерировать все необходимые изображения и поместить их в словарь.
Рассмотрим типичную ситуацию происходящую во время игры, а именно: пользователь щелкнул по лейблу (одна из плиток игрового поля). Как узнать координаты плитки, есть ли на ней шарик и т.д. Вариантов как обычно очень много. Несколько вариантов:
Завести словарь, в котором в качестве ключа использовать конкретный объект класса Label
, а в качестве значения другой словарь/класс со всеми необходимым данными;
Воспользоваться ООП и создать класс-наследник от Label
добавив в него все необходимые поля;
Добавить к конкретному объекту класса Label
пользовательские атрибуты. С учётом пройдённого на сегодняшний день материала, рекомендуется выбрать этот вариант. Например:
xxxxxxxxxx
from tkinter import *
from random import randint
def clck(event):
print(event.widget.row, event.widget.col)
if event.widget.is_pig_here:
event.widget.config(bg="green", text="\uD83D\uDC37", font="Arial 50", borderwidth=20)
else:
event.widget.config(bg="red", text="\u0460", font="Arial 40", borderwidth=23)
root = Tk()
for row in range(2):
for col in range(2):
lbl = Label(root, text=', '.join([str(row), str(col)]), bg="sky blue", borderwidth=50)
# Добавляем свои атрибуты: row, col, is_pig_here
lbl.row = row
lbl.col = col
lbl.is_pig_here = randint(0, 1)
lbl.bind("<Button-1>", clck)
lbl.grid(row=row, column=col, padx=1, pady=1, sticky = 'nesw')
root.mainloop()
В процессе разработки игры придется решить 4 основные задачи:
Для решения этих задач может быть удобно структурировать элементы по разному, например для поиска пути и определения линий удобно работать с плитками как с двумерным массивом, а для выбора рандомной пустой плитки удобно собрать все пустые плитки в отдельный одномерный список.
Т.к. в Python переменные, в том числе и элементы списков, являются ссылками, то можно легко поместить объект в несколько разных списков, при этом объект не копируется, а является одним и тем же.
Ниже представлен код, в котором демонстрируется работа с одними и теми же виджетами Label
через двумерный список bord
и одномерный список line
. Так же, при щелчке по плитке, формируется временный одномерный список cand
состоящий только из тех плиток, которые подходят под условие соседства с выбранной.
xxxxxxxxxx
from tkinter import *
from random import shuffle
root = Tk()
bord = []
line = []
N = 4
def is_collect():
for i in range(N*N-1):
if line[i]['text'] != str(i+1): return False
return True
def swap(current, candidate):
for i in candidate:
if i['text'] == '':
current['bg'], i['bg'] = i['bg'], current['bg']
current['text'], i['text'] = i['text'], current['text']
return
def clck(event):
tile = event.widget
cand = []
if tile.row > 0: cand.append(bord[tile.row-1][tile.col])
if tile.col > 0: cand.append(bord[tile.row][tile.col-1])
if tile.row < N-1: cand.append(bord[tile.row+1][tile.col])
if tile.col < N-1: cand.append(bord[tile.row][tile.col+1])
swap(tile, cand)
if is_collect():
bord[N-1][N-1].config(text='\uD83C\uDF81', font="Arial 30", borderwidth=20)
for row in range(N):
bord.append([])
for col in range(N):
lbl = Label(root, bg='sky blue', borderwidth=50)
lbl.row = row
lbl.col = col
lbl.bind('<Button-1>', clck)
lbl.grid(row=row, column=col, padx=1, pady=1, sticky = 'nesw')
bord[row].append(lbl)
line.append(lbl)
nums =list(range(1, N*N))
shuffle(nums)
for i in range(N*N-1):
line[i].config(text=str(nums[i]))
line[N*N-1].config(bg='white')
root.mainloop()
Отчёт по лабораторной работе оформляется в соответствии с указанными в разделе Правила оценивания требованиями.
В отчёте создайте раздел (заголовок второго уровня) Постановка задачи и продублируйте туда соответствующий блок из этого документа.
Создайте раздел (заголовок второго уровня) Выполнение работы и текстом подробно опишите всё, что делали в процессе выполнения. В описании обязательно должны присутствовать:
В папке с лабораторной работой должно быть: