""" Создание лабиринтов в игре 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()