py_stepik/mod_pygame/moveimg.py

477 lines
15 KiB
Python
Raw Normal View History

2024-03-29 13:54:28 +00:00
"""
3.5 Вставка рисунков. Управление рисунков
https://stepik.org/lesson/937437/step/2?unit=943412
Задача:
Попробуйте сделать управление объектом, который есть не просто прямоугольник, а, например, рисунок.
Зависимости: pygame pillow
"""
import os
import shutil
import tempfile
import urllib.request
2024-03-29 19:29:54 +00:00
from abc import ABC, abstractmethod
2024-03-29 13:54:28 +00:00
from contextlib import contextmanager
2024-03-30 13:17:42 +00:00
from random import choice, randrange
2024-03-29 19:29:54 +00:00
from typing import NamedTuple, Optional
2024-03-29 13:54:28 +00:00
import PIL
2024-03-29 19:29:54 +00:00
import pygame
from PIL import ImageDraw
2024-03-29 13:54:28 +00:00
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(names):
"""Получение соответствия с расположением файлов картинок
Размер картинок нужно менять поэтому они всегда сохраняются во временные файлы.
"""
assets_dir = "assets"
files = {}
# поиск файлов (загрузка если их нет) и создание временных
for asset in names:
_, 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
2024-03-29 19:29:54 +00:00
@classmethod
def zero(cls):
return cls(0, 0)
2024-03-29 13:54:28 +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]}"
2024-03-30 13:17:42 +00:00
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 get_maze_sz(maze: list[list[int]]) -> Coords:
return Coords(len(maze[0]), len(maze))
2024-03-29 19:29:54 +00:00
class GameObject(ABC):
"""обобщение игрового элемента"""
coords = property(
lambda self: self.get_coords(), lambda self, c: self.set_coords(c)
)
def __init__(
self,
coords: Coords,
parent: Optional["GameObject"] = None,
assets: dict | None = None,
):
self.parent = parent
2024-03-30 13:17:42 +00:00
self.rect = pygame.Rect(coords, coords)
2024-03-29 19:29:54 +00:00
self.assets = assets or (parent.assets if parent else None)
self._surface = None
@property
def surface(self) -> pygame.Surface | None:
return self._surface or (self.parent.surface if self.parent else None)
2024-03-30 13:17:42 +00:00
@property
def scene(self):
return self.parent.scene if self.parent else self
2024-03-29 19:29:54 +00:00
def get_coords(self) -> Coords:
2024-03-30 13:17:42 +00:00
return Coords(*self.rect.topleft)
2024-03-29 19:29:54 +00:00
def set_coords(self, coords: Coords):
if self.parent:
if self.parent.surface:
if not (
coords.x >= 0
2024-03-30 13:17:42 +00:00
and coords.x + self.rect.width <= self.parent.surface.get_width()
2024-03-29 19:29:54 +00:00
and coords.y >= 0
2024-03-30 13:17:42 +00:00
and coords.y + self.rect.height < self.parent.surface.get_height()
2024-03-29 19:29:54 +00:00
):
return
2024-03-30 13:17:42 +00:00
self.rect.topleft = coords
2024-03-29 19:29:54 +00:00
@abstractmethod
def draw(self):
pass
2024-03-29 13:54:28 +00:00
2024-03-29 19:29:54 +00:00
@abstractmethod
def handle_event(self, event: pygame.event.Event):
pass
2024-03-29 13:54:28 +00:00
2024-03-29 19:29:54 +00:00
class Hero(GameObject):
"""объект главного героя"""
def __init__(
self,
coords: Coords,
parent: GameObject,
assets: dict | None = None,
):
super().__init__(coords, parent, assets)
self._surface = pygame.image.load(self.assets["ghost.png"])
self.rect = self.surface.get_rect()
2024-03-30 13:17:42 +00:00
self.rect.topleft = coords
2024-03-29 19:29:54 +00:00
def draw(self):
self.parent.surface.blit(self.surface, self.rect)
2024-03-30 13:17:42 +00:00
def _check_collision(self, coords):
"""Проверка пересечения со стенами"""
new_rect = self.rect.copy()
new_rect.topleft = coords
return self.scene.walls.check_collision(new_rect)
def _reduce_step(self, coords):
"""Уменьшение шага движения, с целью подойти вплотную к стене"""
delta = coords - self.coords
dx, dy = 0, 0
if abs(delta.x) > 1:
dx = 1 * (delta.x < 0 or -1)
if abs(delta.y) > 1:
dy = 1 * (delta.y < 0 or -1)
return coords + Coords(dx, dy)
def set_coords(self, coords: Coords):
# проверка колизии
has_collision = self._check_collision(coords)
if not has_collision:
return super().set_coords(coords)
# уменьшение шага
while has_collision and coords != self.coords:
coords_new = self._reduce_step(coords)
if coords_new == coords:
return # не могу уменьшить шаг
coords = coords_new
has_collision = self._check_collision(coords)
super().set_coords(coords)
2024-03-29 19:29:54 +00:00
def handle_event(self, event: pygame.event.Event):
2024-03-30 13:17:42 +00:00
delta = 30
2024-03-29 19:29:54 +00:00
if event.type == pygame.KEYDOWN:
match event.key:
case pygame.K_UP | pygame.K_w:
self.coords += Coords(0, -1) * delta
case pygame.K_DOWN | pygame.K_s:
self.coords += Coords(0, 1) * delta
case pygame.K_LEFT | pygame.K_a:
self.coords += Coords(-1, 0) * delta
case pygame.K_RIGHT | pygame.K_d:
self.coords += Coords(1, 0) * delta
2024-03-30 13:17:42 +00:00
class WallBlock(GameObject):
"""объект елемента стены"""
def __init__(
self,
coords: Coords,
parent: GameObject,
assets: dict | None = None,
):
super().__init__(coords, parent, assets)
self._surface = pygame.image.load(self.assets["brick.png"])
self.rect = self.surface.get_rect()
self.rect.topleft = coords
def draw(self):
self.parent.surface.blit(self.surface, self.rect)
def handle_event(self, event: pygame.event.Event):
...
class Walls(GameObject):
"""объект стен"""
def __init__(
self,
parent: GameObject,
maze: list[list[int]],
box_sz: Coords,
assets: dict | None = None,
):
super().__init__(Coords.zero(), parent, assets)
self.box_sz = box_sz
self.blocks = [
WallBlock(Coords(j, i).transform(box_sz), self, self.assets)
for i, row in enumerate(maze)
for j, item in enumerate(row)
if item > 0
]
def draw(self):
for block in self.blocks:
block.draw()
def check_collision(self, rect: pygame.Rect) -> bool:
for block in self.blocks:
if block.rect.colliderect(rect):
return True
return False
def handle_event(self, event: pygame.event.Event):
for block in self.blocks:
block.handle_event(event)
2024-03-29 19:29:54 +00:00
class Scene(GameObject):
"""основной игровой объект"""
# кнопки для выхода из игры
exit_keys = (pygame.K_ESCAPE, pygame.K_q)
2024-03-30 13:17:42 +00:00
def __init__(self, assets: dict, screen_sz: Coords, maze_sz: Coords):
2024-03-29 19:29:54 +00:00
super().__init__(Coords.zero(), None, assets)
2024-03-30 13:17:42 +00:00
self.maze = maze_gen(*maze_sz)
maze_sz = get_maze_sz(self.maze)
box_sz = screen_sz // get_maze_sz(self.maze)
self.box_sz = box_sz
resize_img(self.assets, "brick.png", box_sz)
hero_sz = Coords(*map(int, box_sz * 0.8))
resize_img(self.assets, "ghost.png", hero_sz)
hero_y_offset = (box_sz.y - hero_sz.y) // 2 + box_sz.y
self._surface = pygame.display.set_mode(screen_sz)
self.rect = self._surface.get_rect()
self.hero = Hero(Coords(0, hero_y_offset), self)
resize_img(assets, "bg1k.png", screen_sz)
2024-03-29 19:29:54 +00:00
self.background = pygame.image.load(self.assets["bg1k.png"])
self.done = False
2024-03-30 13:17:42 +00:00
self.maze = maze_gen(6, 6)
self.walls = Walls(self, self.maze, box_sz)
print(self.get_exit_rect())
print(self.rect)
def get_exit_rect(self) -> pygame.Rect:
maze_sz = get_maze_sz(self.maze)
coords = (maze_sz - Coords(1, 2)) * self.box_sz
rect = pygame.Rect(coords, coords)
rect.width, rect.height = self.box_sz
return rect
2024-03-29 19:29:54 +00:00
def draw(self):
if self.done:
return
self.surface.blit(self.background, self.coords)
2024-03-30 13:17:42 +00:00
pygame.draw.rect(self._surface, pygame.Color("#42c53d25"), self.get_exit_rect())
2024-03-29 19:29:54 +00:00
self.hero.draw()
2024-03-30 13:17:42 +00:00
self.walls.draw()
2024-03-29 19:29:54 +00:00
def handle_event(self, event: pygame.event.Event):
if self.done:
return
if (
event.type == pygame.QUIT
or event.type == pygame.KEYDOWN
and event.key in self.exit_keys
):
self.done = True
if not self.done:
self.hero.handle_event(event)
def event_loop(self):
while not self.done:
event = pygame.event.wait()
self.handle_event(event)
self.draw()
pygame.display.flip()
2024-03-29 13:54:28 +00:00
2024-03-29 19:29:54 +00:00
def game(assets):
pygame.init()
screen_sz = Coords(1000, 1000)
2024-03-30 13:17:42 +00:00
maze_sz = Coords(6, 6)
2024-03-29 19:29:54 +00:00
pygame.display.set_caption("Движение рисунка на Pygame")
2024-03-29 13:54:28 +00:00
2024-03-30 13:17:42 +00:00
scene = Scene(assets, screen_sz, maze_sz)
2024-03-29 19:29:54 +00:00
scene.event_loop()
2024-03-29 13:54:28 +00:00
pygame.quit()
def main():
assets = [
"bg1k.png",
"ghost.png",
2024-03-30 13:17:42 +00:00
"brick.png",
2024-03-29 13:54:28 +00:00
]
with get_assets(assets) as assets:
game(assets)
2024-03-29 19:29:54 +00:00
main()