py_stepik/mod_graph/maze.py

446 lines
15 KiB
Python
Raw Normal View History

2024-03-27 13:55:45 +00:00
"""
Создание лабиринтов в игре
https://stepik.org/lesson/502494/step/3?unit=494196
и https://stepik.org/lesson/569367/step/2?unit=563878
2024-03-27 13:55:45 +00:00
Задача:
Сделайте "догонялку" в лабиринте. Придумайте дизайн лабиринта, рисунки для стенки и проходов. Через стенки Ваш герой не должен
передвигаться.
И ещё задача:
Попробуйте создать свою игру на основе знаний, которые Вы получили на предыдущих уроках.
2024-03-27 13:55:45 +00:00
Решение:
Игра за призрака, нужно из входа в лабиринт "догнать" выход, по пути собирая монетки.
2024-03-27 13:55:45 +00:00
Управление стрелками и WASD, выход по Esc или Q.
Чтобы пройти игру, нужно дойти до выхода, для хорошей концовки - собрать все монетки.
2024-03-27 13:55:45 +00:00
Картинки берутся из папки assets, если их нет то автоматически скачиваются из репозитория.
Если скачать не получилось то будут нарисованы заглушки.
2024-03-27 13:55:45 +00:00
Зависимости: graph pillow
Библиотека graph: https://kpolyakov.spb.ru/download/pygraph.zip
"""
2024-03-27 12:08:54 +00:00
import os
import shutil
2024-03-27 12:08:54 +00:00
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
2024-03-27 12:08:54 +00:00
def make_stub_image(path, name):
"""Создание пустой картинки, на случай если скачать не получилось"""
2024-03-27 12:08:54 +00:00
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)
2024-03-27 13:10:57 +00:00
@contextmanager
def get_assets():
"""Получение соответствия с расположением файлов картинок
2024-03-27 13:10:57 +00:00
Размер картинок нужно менять поэтому они всегда сохраняются во временные файлы.
"""
assets_dir = "assets"
assests = [
"bg1k.png",
"ghost.png",
"brick.png",
"coin.png",
"win.png",
]
2024-03-27 13:10:57 +00:00
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
2024-03-27 13:10:57 +00:00
# передача управления
yield files
# очистка
for _, filename in files.items():
try:
os.remove(filename)
except FileNotFoundError:
pass
2024-03-27 12:08:54 +00:00
class Coords(NamedTuple):
2024-03-27 12:08:54 +00:00
"""
Вспомогательный класс для упрощения работы с координатами
2024-03-27 12:08:54 +00:00
"""
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
2024-03-27 21:39:34 +00:00
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
2024-03-27 12:08:54 +00:00
def transform(self, ref: "Coords"):
return self * ref
2024-03-27 12:08:54 +00:00
# состояние игры
2024-03-27 12:08:54 +00:00
data = {
"hero_pos": Coords(0, 0),
2024-03-27 12:08:54 +00:00
"finished": False,
"hero_moved": False,
2024-03-27 22:00:46 +00:00
"right": True,
"last_right": True,
2024-03-27 12:08:54 +00:00
"maze": [],
"callbacks": [],
"box_sz": Coords(0, 0),
"coins": {},
"coins_collected": 0,
"got_coin": False,
2024-03-27 12:08:54 +00:00
}
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):
"""Перемещает объект к центру"""
bounds = bbox(obj)
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
p = scene_sz // 2 - Coords(width, height) // 2
moveObjectTo(obj, *p)
def make_level(scene_sz, assets):
"""Создание уровня"""
2024-03-27 21:39:34 +00:00
data["maze"] = maze_gen(6, 6)
2024-03-27 13:10:57 +00:00
# размер ячейки
maze_sz = Coords(len(data["maze"][0]), len(data["maze"]))
2024-03-27 21:39:34 +00:00
box_sz = scene_sz // maze_sz
data["box_sz"] = box_sz
2024-03-27 12:08:54 +00:00
2024-03-27 22:00:46 +00:00
# установка начальной позиции
2024-03-27 12:08:54 +00:00
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)
2024-03-27 22:00:46 +00:00
# загрузка изображения героя
hero_img = PIL.Image.open(assets["ghost.png"])
2024-03-27 22:00:46 +00:00
# создаём зеркальную копию
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"])
2024-03-27 22:00:46 +00:00
# сразу поворачиваем героя на право
changeProperty(hero, image=hero_img_right)
2024-03-27 22:00:46 +00:00
# отрисовка стен и добавление мест для монеток
free_points = []
2024-03-27 12:08:54 +00:00
for i, row in enumerate(data["maze"]):
for j, item in enumerate(row):
p = Coords(j, i)
2024-03-27 12:08:54 +00:00
if item < 1:
if data["hero_pos"] != p:
free_points.append(p)
2024-03-27 12:08:54 +00:00
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 = ("монетку", "монетки", "монет")
2024-03-27 12:08:54 +00:00
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
2024-03-27 12:08:54 +00:00
# отрабатываем изменение направления
if data["right"] != data["last_right"]:
img = hero_img_right if data["right"] else hero_img_left
changeProperty(hero, image=img)
2024-03-27 12:08:54 +00:00
data["last_right"] = data["right"]
# отрисовка движения
if data["hero_moved"]:
2024-03-27 13:55:45 +00:00
new_pos = data["hero_pos"].transform(box_sz)
moveObjectTo(hero, *new_pos)
2024-03-27 12:08:54 +00:00
data["hero_moved"] = False
# конец лабиринта|игры
2024-03-27 12:08:54 +00:00
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)
2024-03-27 12:08:54 +00:00
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)
2024-03-27 12:08:54 +00:00
raise Exception("Не найдено начальное положение")
def move_hero(data, delta):
2024-03-27 13:10:57 +00:00
"""проверка и выполнение движения героя в лабиринте"""
maze_sz = Coords(len(data["maze"]), len(data["maze"][0]))
2024-03-27 12:08:54 +00:00
new_pos = data["hero_pos"] + delta
2024-03-27 22:00:46 +00:00
# проерка границ карты
2024-03-27 12:08:54 +00:00
if (
new_pos.x < 0
or new_pos.y < 0
or new_pos.x >= maze_sz.x
or new_pos.y >= maze_sz.y
2024-03-27 12:08:54 +00:00
):
return
2024-03-27 22:00:46 +00:00
# проверка на наличие стен
2024-03-27 12:08:54 +00:00
if data["maze"][new_pos.y][new_pos.x] > 0:
return
2024-03-27 22:00:46 +00:00
# применение новой позиции
2024-03-27 12:08:54 +00:00
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
2024-03-27 22:00:46 +00:00
# проверка на конец лабиринта
if new_pos.x >= maze_sz.x - 1:
2024-03-27 12:08:54 +00:00
data["finished"] = True
def keyPressed(event):
2024-03-27 21:39:34 +00:00
dx = dy = 0
2024-03-27 13:55:45 +00:00
match event.keysym:
case "Left" | "a":
2024-03-27 21:39:34 +00:00
dx = -1
2024-03-27 12:08:54 +00:00
data["right"] = False
2024-03-27 13:55:45 +00:00
case "Right" | "d":
2024-03-27 21:39:34 +00:00
dx = 1
2024-03-27 22:00:46 +00:00
data["right"] = True
2024-03-27 13:55:45 +00:00
case "Up" | "w":
2024-03-27 21:39:34 +00:00
dy = -1
2024-03-27 13:55:45 +00:00
case "Down" | "s":
2024-03-27 21:39:34 +00:00
dy = 1
2024-03-27 13:55:45 +00:00
case "Escape" | "q":
2024-03-27 12:08:54 +00:00
killTimer(data["update_timer"])
close()
2024-03-27 21:39:34 +00:00
if dx or dy:
move_hero(data, Coords(dx, dy))
2024-03-27 12:08:54 +00:00
def update():
for callback in data["callbacks"]:
callback()
data["delta"] = Coords(0, 0)
2024-03-27 12:08:54 +00:00
def maze_gen(row=4, col=4):
"""генератор карты
взял с коментария
https://stepik.org/lesson/502494/step/3?discussion=6527620&unit=494196
2024-03-27 12:08:54 +00:00
"""
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):
2024-03-27 22:00:46 +00:00
# рисуем фон
scene_sz = Coords(1000, 1000)
2024-03-27 12:08:54 +00:00
canvasSize(*scene_sz)
resize_img(assets, "bg1k.png", scene_sz)
image(0, 0, assets["bg1k.png"])
2024-03-27 12:08:54 +00:00
2024-03-27 22:00:46 +00:00
# создаём уровень
make_level(scene_sz, assets)
2024-03-27 12:08:54 +00:00
2024-03-27 22:00:46 +00:00
# цепляем обработчики событий
2024-03-27 12:08:54 +00:00
data["update_timer"] = onTimer(update, 40)
onKey(keyPressed)
2024-03-27 22:00:46 +00:00
# запускаем
2024-03-27 12:08:54 +00:00
run()
2024-03-27 13:10:57 +00:00
def main():
with get_assets() as assets:
app(assets)
2024-03-27 13:10:57 +00:00
2024-03-27 12:08:54 +00:00
main()