py_stepik/mod_graph/maze.py
Dmitry f0d2949ce5 maze v2
- added coins
- removed hardcoded images
- added image download
- Points -> Coords
- ...
2024-03-28 14:26:27 +03:00

446 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Создание лабиринтов в игре
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):
"""Перемещает объект к центру"""
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):
"""Создание уровня"""
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()