445 lines
15 KiB
Python
445 lines
15 KiB
Python
"""
|
||
Создание лабиринтов в игре
|
||
https://stepik.org/lesson/502494/step/3?unit=494196
|
||
и https://stepik.org/lesson/569367/step/2?unit=563878
|
||
|
||
Задача:
|
||
Сделайте "догонялку" в лабиринте. Придумайте дизайн лабиринта, рисунки для стенки и проходов. Через стенки Ваш герой не должен
|
||
передвигаться.
|
||
И ещё задача:
|
||
Попробуйте создать свою игру на основе знаний, которые Вы получили на предыдущих уроках.
|
||
|
||
Решение:
|
||
Игра за призрака, нужно из входа в лабиринт "догнать" выход, по пути собирая монетки.
|
||
Управление стрелками и WASD, выход по Esc или Q.
|
||
Чтобы пройти игру, нужно дойти до выхода, для хорошей концовки - собрать все монетки.
|
||
|
||
Картинки берутся из папки assets, если их нет то автоматически скачиваются из репозитория.
|
||
Если скачать не получилось то будут нарисованы заглушки.
|
||
Зависимости: graph pillow
|
||
|
||
Библиотека graph: https://kpolyakov.spb.ru/download/pygraph.zip
|
||
"""
|
||
|
||
import os
|
||
import shutil
|
||
import tempfile
|
||
import urllib.request
|
||
from contextlib import contextmanager
|
||
from random import choice, randrange, sample
|
||
from typing import NamedTuple
|
||
|
||
import PIL
|
||
from graph import (
|
||
bbox,
|
||
canvasSize,
|
||
changeFillColor,
|
||
changeProperty,
|
||
close,
|
||
deleteObject,
|
||
image,
|
||
killTimer,
|
||
moveObjectTo,
|
||
onKey,
|
||
onTimer,
|
||
penColor,
|
||
run,
|
||
text,
|
||
)
|
||
from PIL import Image, ImageDraw
|
||
|
||
|
||
def download_asset(asset, path):
|
||
"""
|
||
Загрузка картинок из репозитория
|
||
"""
|
||
prefix = "https://gitea.b4tman.ru/temp/py_stepik/raw/branch/master/assets/"
|
||
print("Качаю картинку", asset, end=" ... ", flush=True)
|
||
try:
|
||
urllib.request.urlretrieve(prefix + asset, path)
|
||
except Exception:
|
||
print("не смог :(")
|
||
return False
|
||
print("скачал!")
|
||
return True
|
||
|
||
|
||
def make_stub_image(path, name):
|
||
"""Создание пустой картинки, на случай если скачать не получилось"""
|
||
|
||
img = PIL.Image.new("RGBA", (200, 200))
|
||
draw = ImageDraw.Draw(img)
|
||
draw.rectangle([(50, 50), (150, 150)], outline="black", width=2)
|
||
draw.line((50, 50, 150, 150), fill="red", width=2)
|
||
draw.line((50, 150, 150, 50), fill="red", width=2)
|
||
draw.text((50, 170), name, fill="blue")
|
||
img.save(path)
|
||
|
||
|
||
@contextmanager
|
||
def get_assets():
|
||
"""Получение соответствия с расположением файлов картинок
|
||
|
||
Размер картинок нужно менять поэтому они всегда сохраняются во временные файлы.
|
||
"""
|
||
|
||
assets_dir = "assets"
|
||
assests = [
|
||
"bg1k.png",
|
||
"ghost.png",
|
||
"brick.png",
|
||
"coin.png",
|
||
"win.png",
|
||
]
|
||
files = {}
|
||
# поиск файлов (загрузка если их нет) и создание временных
|
||
for asset in assests:
|
||
_, ext = os.path.splitext(asset)
|
||
temppath = tempfile.mktemp(suffix=ext)
|
||
filepath = os.path.join(assets_dir, asset)
|
||
if os.path.isfile(filepath):
|
||
shutil.copyfile(filepath, temppath)
|
||
else:
|
||
if not download_asset(asset, temppath):
|
||
make_stub_image(temppath, asset)
|
||
files[asset] = temppath
|
||
|
||
# передача управления
|
||
yield files
|
||
# очистка
|
||
for _, filename in files.items():
|
||
try:
|
||
os.remove(filename)
|
||
except FileNotFoundError:
|
||
pass
|
||
|
||
|
||
class Coords(NamedTuple):
|
||
"""
|
||
Вспомогательный класс для упрощения работы с координатами
|
||
"""
|
||
|
||
x: int | float
|
||
y: int | float
|
||
|
||
def __add__(self, other):
|
||
if isinstance(other, self.__class__):
|
||
return self.__class__(self.x + other.x, self.y + other.y)
|
||
if isinstance(other, (int, float)):
|
||
return self.__class__(self.x + other, self.y + other)
|
||
return NotImplemented
|
||
|
||
def __sub__(self, other):
|
||
if isinstance(other, self.__class__):
|
||
return self.__class__(self.x - other.x, self.y - other.y)
|
||
if isinstance(other, (int, float)):
|
||
return self.__class__(self.x - other, self.y - other)
|
||
return NotImplemented
|
||
|
||
def __floordiv__(self, other):
|
||
if isinstance(other, self.__class__):
|
||
return self.__class__(self.x // other.x, self.y // other.y)
|
||
if isinstance(other, (int, float)):
|
||
return self.__class__(self.x // other, self.y // other)
|
||
return NotImplemented
|
||
|
||
def __mul__(self, other):
|
||
if isinstance(other, self.__class__):
|
||
return self.__class__(self.x * other.x, self.y * other.y)
|
||
if isinstance(other, (int, float)):
|
||
return self.__class__(self.x * other, self.y * other)
|
||
return NotImplemented
|
||
|
||
def transform(self, ref: "Coords"):
|
||
return self * ref
|
||
|
||
|
||
# состояние игры
|
||
data = {
|
||
"hero_pos": Coords(0, 0),
|
||
"finished": False,
|
||
"hero_moved": False,
|
||
"right": True,
|
||
"last_right": True,
|
||
"maze": [],
|
||
"callbacks": [],
|
||
"box_sz": Coords(0, 0),
|
||
"coins": {},
|
||
"coins_collected": 0,
|
||
"got_coin": False,
|
||
}
|
||
|
||
|
||
def resize_img(assets: dict, name: str, sz: Coords):
|
||
"""
|
||
Изменение размера картинки и сохранение в файл
|
||
"""
|
||
img = PIL.Image.open(assets[name])
|
||
if img.size != sz:
|
||
img = img.resize(sz)
|
||
img.save(assets[name])
|
||
|
||
|
||
def choose_plural(amount, declensions):
|
||
"""Возвращает количество объектов в виде строки
|
||
например 5 копеек, 1 копейка и т.д.
|
||
"""
|
||
a = amount % 100
|
||
i = 2
|
||
if 10 < a < 20:
|
||
pass
|
||
elif a % 10 == 1:
|
||
i = 0
|
||
elif 1 < a % 10 < 5:
|
||
i = 1
|
||
return f"{amount} {declensions[i]}"
|
||
|
||
|
||
def move_to_center(obj, scene_sz):
|
||
"""Перемещает объект к центру"""
|
||
x1, y1, x2, y2 = bbox(obj)
|
||
obj_sz = Coords(x2, y2) - Coords(x1, y1)
|
||
p = scene_sz // 2 - obj_sz // 2
|
||
moveObjectTo(obj, *p)
|
||
|
||
|
||
def make_level(scene_sz, assets):
|
||
"""Создание уровня"""
|
||
|
||
data["maze"] = maze_gen(6, 6)
|
||
|
||
# размер ячейки
|
||
maze_sz = Coords(len(data["maze"][0]), len(data["maze"]))
|
||
box_sz = scene_sz // maze_sz
|
||
data["box_sz"] = box_sz
|
||
|
||
# установка начальной позиции
|
||
data["hero_pos"] = find_start(data)
|
||
data["hero_moved"] = True
|
||
|
||
resize_img(assets, "ghost.png", box_sz)
|
||
resize_img(assets, "brick.png", box_sz)
|
||
resize_img(assets, "coin.png", box_sz)
|
||
|
||
# загрузка изображения героя
|
||
hero_img = PIL.Image.open(assets["ghost.png"])
|
||
# создаём зеркальную копию
|
||
hero_img_right = hero_img.transpose(Image.FLIP_LEFT_RIGHT)
|
||
# записываем изображения
|
||
hero_img_right = PIL.ImageTk.PhotoImage(hero_img_right)
|
||
hero_img_left = PIL.ImageTk.PhotoImage(hero_img)
|
||
hero = image(0, 0, assets["ghost.png"])
|
||
# сразу поворачиваем героя на право
|
||
changeProperty(hero, image=hero_img_right)
|
||
|
||
# отрисовка стен и добавление мест для монеток
|
||
free_points = []
|
||
for i, row in enumerate(data["maze"]):
|
||
for j, item in enumerate(row):
|
||
p = Coords(j, i)
|
||
if item < 1:
|
||
if data["hero_pos"] != p:
|
||
free_points.append(p)
|
||
continue
|
||
image(*p.transform(box_sz), assets["brick.png"])
|
||
|
||
# расстановка монеток
|
||
all_coins = 10
|
||
coin_points = sample(free_points, all_coins)
|
||
for point in coin_points:
|
||
data["coins"][point] = image(*point.transform(box_sz), assets["coin.png"])
|
||
|
||
# надпись со счетчиком монет
|
||
penColor("#1e11ce")
|
||
coins_label = text("", 0, 5, font=("Arial", 20, "bold"))
|
||
coin_forms = ("монетку", "монетки", "монет")
|
||
|
||
def update():
|
||
# отрисовка счетчика монеток
|
||
if data["got_coin"]:
|
||
collected = data["coins_collected"]
|
||
if collected == all_coins:
|
||
coins_text = "Молодец, собрал вообще все монетки!"
|
||
changeFillColor(coins_label, "#062706")
|
||
else:
|
||
coins_text = f"Собрал {choose_plural(collected, coin_forms)}"
|
||
changeProperty(coins_label, text=coins_text)
|
||
data["got_coin"] = False
|
||
|
||
# отрабатываем изменение направления
|
||
if data["right"] != data["last_right"]:
|
||
img = hero_img_right if data["right"] else hero_img_left
|
||
changeProperty(hero, image=img)
|
||
data["last_right"] = data["right"]
|
||
# отрисовка движения
|
||
if data["hero_moved"]:
|
||
new_pos = data["hero_pos"].transform(box_sz)
|
||
moveObjectTo(hero, *new_pos)
|
||
data["hero_moved"] = False
|
||
# конец лабиринта|игры
|
||
if data["finished"]:
|
||
data["callbacks"].remove(update)
|
||
if data["coins_collected"] == all_coins: # победа
|
||
move_to_center(image(0, 0, assets["win.png"]), scene_sz)
|
||
penColor("blue")
|
||
move_to_center(text("Победа!", 0, 0, font=("Arial", 30)), scene_sz)
|
||
else: # плохая концовка
|
||
move_to_center(text("Конец игры!", 0, 0, font=("Arial", 40)), scene_sz)
|
||
|
||
data.setdefault("callbacks", []).append(update)
|
||
|
||
|
||
def find_start(data):
|
||
for i in range(len(data["maze"])):
|
||
if data["maze"][i][0] == 0:
|
||
return Coords(0, i)
|
||
raise Exception("Не найдено начальное положение")
|
||
|
||
|
||
def move_hero(data, delta):
|
||
"""проверка и выполнение движения героя в лабиринте"""
|
||
maze_sz = Coords(len(data["maze"]), len(data["maze"][0]))
|
||
new_pos = data["hero_pos"] + delta
|
||
# проерка границ карты
|
||
if (
|
||
new_pos.x < 0
|
||
or new_pos.y < 0
|
||
or new_pos.x >= maze_sz.x
|
||
or new_pos.y >= maze_sz.y
|
||
):
|
||
return
|
||
# проверка на наличие стен
|
||
if data["maze"][new_pos.y][new_pos.x] > 0:
|
||
return
|
||
# применение новой позиции
|
||
data["hero_pos"] = new_pos
|
||
data["hero_moved"] = True
|
||
|
||
# сбор монеток
|
||
if new_pos in data["coins"]:
|
||
data["coins_collected"] += 1
|
||
deleteObject(data["coins"][new_pos])
|
||
del data["coins"][new_pos]
|
||
data["got_coin"] = True
|
||
|
||
# проверка на конец лабиринта
|
||
if new_pos.x >= maze_sz.x - 1:
|
||
data["finished"] = True
|
||
|
||
|
||
def keyPressed(event):
|
||
dx = dy = 0
|
||
match event.keysym:
|
||
case "Left" | "a":
|
||
dx = -1
|
||
data["right"] = False
|
||
case "Right" | "d":
|
||
dx = 1
|
||
data["right"] = True
|
||
case "Up" | "w":
|
||
dy = -1
|
||
case "Down" | "s":
|
||
dy = 1
|
||
case "Escape" | "q":
|
||
killTimer(data["update_timer"])
|
||
close()
|
||
if dx or dy:
|
||
move_hero(data, Coords(dx, dy))
|
||
|
||
|
||
def update():
|
||
for callback in data["callbacks"]:
|
||
callback()
|
||
data["delta"] = Coords(0, 0)
|
||
|
||
|
||
def maze_gen(row=4, col=4):
|
||
"""генератор карты
|
||
взял с коментария
|
||
https://stepik.org/lesson/502494/step/3?discussion=6527620&unit=494196
|
||
"""
|
||
row = max(2 * row + 1, 3)
|
||
col = max(2 * col + 1, 3)
|
||
maze = [[2] * col]
|
||
maze.extend([[2] + [1] * (col - 2) + [2] for _ in range(row - 2)])
|
||
maze.append(maze[0])
|
||
|
||
curr = (randrange(1, len(maze) - 1, 2), randrange(1, len(maze[0]) - 1, 2))
|
||
path = [curr]
|
||
maze[curr[0]][curr[1]] = 0
|
||
|
||
while path:
|
||
nexts = [
|
||
(r1, c1, r2, c2)
|
||
for r, c in zip((1, 0, -1, 0), (0, 1, 0, -1))
|
||
if (
|
||
(r1 := curr[0] + r) is None
|
||
or (c1 := curr[1] + c) is None
|
||
or (r2 := curr[0] + 2 * r) is None
|
||
or (c2 := curr[1] + 2 * c) is None
|
||
or 1 == maze[r1][c1] == maze[r2][c2]
|
||
)
|
||
]
|
||
if len(nexts):
|
||
r1, c1, r2, c2 = choice(nexts)
|
||
maze[r1][c1] = maze[r2][c2] = 0
|
||
path.append((r2, c2))
|
||
else:
|
||
curr = path.pop()
|
||
|
||
upd = {
|
||
("22", "20"): (None, "00"),
|
||
("02", "22"): ("00", None),
|
||
("11101", "00101", "11111", "10100", "10111"): (
|
||
None,
|
||
None,
|
||
None,
|
||
"10001",
|
||
None,
|
||
),
|
||
("10111", "10100", "11111", "00101", "11101"): (
|
||
None,
|
||
"10001",
|
||
None,
|
||
None,
|
||
None,
|
||
),
|
||
}
|
||
for key in upd:
|
||
for i in range(len(maze) - len(key) + 1):
|
||
for j in range(len(maze[0]) - len(key[0]) + 1):
|
||
if all(
|
||
maze[i + k][j : j + len(v)] == list(map(int, v))
|
||
for k, v in enumerate(key)
|
||
):
|
||
for k, v in filter(lambda x: x[1], enumerate(upd[key])):
|
||
maze[i + k][j : j + len(v)] = list(map(int, v))
|
||
|
||
return maze
|
||
|
||
|
||
def app(assets):
|
||
# рисуем фон
|
||
scene_sz = Coords(1000, 1000)
|
||
canvasSize(*scene_sz)
|
||
resize_img(assets, "bg1k.png", scene_sz)
|
||
image(0, 0, assets["bg1k.png"])
|
||
|
||
# создаём уровень
|
||
make_level(scene_sz, assets)
|
||
|
||
# цепляем обработчики событий
|
||
data["update_timer"] = onTimer(update, 40)
|
||
onKey(keyPressed)
|
||
|
||
# запускаем
|
||
run()
|
||
|
||
|
||
def main():
|
||
with get_assets() as assets:
|
||
app(assets)
|
||
|
||
|
||
main()
|