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-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-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
|
|
|
|
|
self._coords = coords
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
def get_coords(self) -> Coords:
|
|
|
|
|
return self._coords
|
|
|
|
|
|
|
|
|
|
def set_coords(self, coords: Coords):
|
|
|
|
|
if self.parent:
|
|
|
|
|
if self.parent.surface:
|
|
|
|
|
if not (
|
|
|
|
|
coords.x >= 0
|
|
|
|
|
and coords.x < self.parent.surface.get_width()
|
|
|
|
|
and coords.y >= 0
|
|
|
|
|
and coords.y < self.parent.surface.get_height()
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
self._coords = coords
|
|
|
|
|
|
|
|
|
|
@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)
|
|
|
|
|
screen_sz = Coords(*parent.surface.get_size())
|
|
|
|
|
ghost_sz = screen_sz // 10
|
|
|
|
|
resize_img(self.assets, "ghost.png", ghost_sz)
|
|
|
|
|
|
|
|
|
|
self._surface = pygame.image.load(self.assets["ghost.png"])
|
|
|
|
|
self.rect = self.surface.get_rect()
|
|
|
|
|
self.coords = coords
|
|
|
|
|
|
|
|
|
|
def set_coords(self, coords: Coords):
|
|
|
|
|
super().set_coords(coords)
|
|
|
|
|
self.rect.topleft = self.coords
|
|
|
|
|
|
|
|
|
|
def draw(self):
|
|
|
|
|
self.parent.surface.blit(self.surface, self.rect)
|
|
|
|
|
|
|
|
|
|
def handle_event(self, event: pygame.event.Event):
|
|
|
|
|
delta = 10
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Scene(GameObject):
|
|
|
|
|
"""основной игровой объект"""
|
|
|
|
|
|
|
|
|
|
# кнопки для выхода из игры
|
|
|
|
|
exit_keys = (pygame.K_ESCAPE, pygame.K_q)
|
|
|
|
|
|
|
|
|
|
def __init__(self, assets: dict, sz: Coords):
|
|
|
|
|
super().__init__(Coords.zero(), None, assets)
|
|
|
|
|
self._surface = pygame.display.set_mode(sz)
|
|
|
|
|
self.hero = Hero(Coords(100, 100), self)
|
|
|
|
|
resize_img(assets, "bg1k.png", sz)
|
|
|
|
|
self.background = pygame.image.load(self.assets["bg1k.png"])
|
|
|
|
|
self.done = False
|
|
|
|
|
|
|
|
|
|
def draw(self):
|
|
|
|
|
if self.done:
|
|
|
|
|
return
|
|
|
|
|
self.surface.blit(self.background, self.coords)
|
|
|
|
|
self.hero.draw()
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
pygame.display.set_caption("Движение рисунка на Pygame")
|
2024-03-29 13:54:28 +00:00
|
|
|
|
|
2024-03-29 19:29:54 +00:00
|
|
|
|
scene = Scene(assets, screen_sz)
|
|
|
|
|
scene.event_loop()
|
2024-03-29 13:54:28 +00:00
|
|
|
|
|
|
|
|
|
pygame.quit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
assets = [
|
|
|
|
|
"bg1k.png",
|
|
|
|
|
"ghost.png",
|
|
|
|
|
]
|
|
|
|
|
with get_assets(assets) as assets:
|
|
|
|
|
game(assets)
|
|
|
|
|
|
2024-03-29 19:29:54 +00:00
|
|
|
|
|
|
|
|
|
main()
|