py_stepik/mod_pygame/moveimg.py
2024-03-30 23:54:51 +03:00

685 lines
23 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.

"""
3.5 Вставка рисунков. Управление рисунков
https://stepik.org/lesson/937437/step/2?unit=943412
и
https://stepik.org/lesson/937437/step/3?unit=943412
Задача:
Попробуйте сделать управление объектом, который есть не просто прямоугольник, а, например, рисунок.
и
Сделайте программу, чтобы при движении влево было одно изображение, а при движении вправо другое изображение героя.
Решение:
Игра "Призрачный лабиринт: сокровища небесного замка".
Призрак в лабиринте, управление стрелками и WASD, выход Esc или Q.
Чтобы пройти уровень, нужно дойти до выхода, желательно собрав все монетки.
После прохождения уровня можно начать новый или выйти из игры.
Картинки берутся из папки assets, если их нет то автоматически скачиваются из репозитория.
Если скачать не получилось то будут нарисованы заглушки.
Зависимости: pygame
"""
import os
import shutil
import tempfile
import urllib.request
from abc import ABC, abstractmethod
from contextlib import contextmanager
from random import choice, randrange, sample
from typing import NamedTuple, Optional
import pygame
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 = pygame.surface.Surface((200, 200), flags=pygame.SRCALPHA)
img.fill((255, 255, 255, 0))
pygame.draw.line(img, "#ff000065", (5, 5), (195, 195), 2)
pygame.draw.line(img, "#ff000065", (195, 5), (5, 195), 2)
rect = pygame.Rect(5, 5, 190, 190)
pygame.draw.rect(img, "black", rect, 3)
font = pygame.font.SysFont("Arial", 44)
text1 = font.render(name, True, "blue")
text1_rect = text1.get_rect()
text1_rect.center = img.get_rect().center
img.blit(text1, text1_rect)
pygame.image.save(img, 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
@classmethod
def zero(cls):
return cls(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 get_maze_sz(maze: list[list[int]]) -> Coords:
return Coords(len(maze[0]), len(maze))
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.rect = pygame.Rect(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)
@property
def scene(self):
return self.parent.scene if self.parent else self
def get_coords(self) -> Coords:
return Coords(*self.rect.topleft)
def set_coords(self, coords: Coords):
new_rect = self.rect.copy()
new_rect.topleft = coords
if self.parent:
if self.parent.rect:
if not self.parent.rect.contains(new_rect):
return
self.rect = new_rect
@abstractmethod
def draw(self):
pass
def handle_event(self, event: pygame.event.Event):
pass
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()
sf = Coords(0.8, 0.8)
self._surface, self.rect = self.scene.scale_box(self.surface, self.rect, sf)
self.rect.topleft = coords
self.active = True
self.looking_right = False
# картинка изначально влево, а надо бы начинать со взгляда вправо
self.flip()
def draw(self):
self.parent.surface.blit(self.surface, self.rect)
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)
def flip(self):
self.looking_right = not self.looking_right
self._surface = pygame.transform.flip(self.surface, True, False)
def move(self, direction: Coords, step: int = 1):
if direction.x != 0:
going_right = direction.x > 0
if self.looking_right != going_right:
self.flip()
self.coords += direction * step
self.scene.coins.collect(self.rect)
def move_left(self, step: int = 1):
self.move(Coords(-1, 0), step)
def move_right(self, step: int = 1):
self.move(Coords(1, 0), step)
def move_up(self, step: int = 1):
self.move(Coords(0, -1), step)
def move_down(self, step: int = 1):
self.move(Coords(0, 1), step)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return
wide, short = 30, 5
if event.type == pygame.KEYDOWN:
match event.key:
case pygame.K_UP:
self.move_up(wide)
case pygame.K_DOWN:
self.move_down(wide)
case pygame.K_LEFT:
self.move_left(wide)
case pygame.K_RIGHT:
self.move_right(wide)
case pygame.K_w:
self.move_up(short)
case pygame.K_s:
self.move_down(short)
case pygame.K_a:
self.move_left(short)
case pygame.K_d:
self.move_right(short)
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
# уменьшаем размер монетки
sf = Coords(1, 1)
self._surface, self.rect = self.scene.scale_box(self.surface, self.rect, sf)
def draw(self):
self.parent.surface.blit(self.surface, self.rect)
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
class Coin(GameObject):
"""объект монетки"""
def __init__(
self,
coords: Coords,
parent: GameObject,
assets: dict | None = None,
):
super().__init__(coords, parent, assets)
self._surface = pygame.image.load(self.assets["coin.png"])
self.rect = self.surface.get_rect()
self.rect.topleft = coords
# уменьшаем размер монетки
sf = Coords(0.7, 0.7)
self._surface, self.rect = self.scene.scale_box(self.surface, self.rect, sf)
def draw(self):
self.parent.surface.blit(self.surface, self.rect)
class Coins(GameObject):
"""объект коллекции монеток"""
def __init__(
self,
parent: GameObject,
maze: list[list[int]],
box_sz: Coords,
count: int,
assets: dict | None = None,
):
super().__init__(Coords.zero(), parent, assets)
self.box_sz = box_sz
self._capacity = count
free_points = []
excluded = Coords(0, 1), get_maze_sz(maze) - Coords(1, 2)
for i, row in enumerate(maze):
for j, item in enumerate(row):
p = Coords(j, i)
if item < 1 and p not in excluded:
free_points.append(p)
continue
coin_points = sample(free_points, min(count, len(free_points)))
self.coins = [
Coin(point.transform(box_sz), self, self.assets) for point in coin_points
]
self.collected_coins = []
# Надпись, если все монетки собраны
font = pygame.font.SysFont("Arial", 30)
text = "Все монетки собраны!"
self.done_txt = font.render(text, 1, "#050366e3")
self.done_txt_rect = self.done_txt.get_rect()
self.done_txt_rect.topleft = Coords(10, 10)
@property
def capacity(self):
return self._capacity
@property
def coins_left(self):
return len(self.coins)
@property
def coins_collected(self):
return self.capacity - self.coins_left
@property
def all_collected(self):
return self.coins_left == 0
def draw(self):
for coin in self.collected_coins:
coin.draw()
for coin in self.coins:
coin.draw()
if self.all_collected:
self.parent.surface.blit(self.done_txt, self.done_txt_rect)
def add_to_collected(self, coin: Coin):
last_pos = Coords(10, 10)
if self.collected_coins:
last_pos = Coords(*self.collected_coins[-1].rect.topright)
last_pos -= Coords(coin.rect.width // 2, 0)
coin.coords = last_pos
self.collected_coins.append(coin)
def collect(self, rect: pygame.Rect):
mined = [*filter(lambda coin: coin.rect.colliderect(rect), self.coins)]
for coin in mined:
self.coins.remove(coin)
self.add_to_collected(coin)
class EndLevel(GameObject):
def __init__(self, scene: GameObject):
super().__init__(Coords.zero(), scene, scene.assets)
self.image = pygame.image.load(scene.assets["win.png"])
self.active = False
# надпись завершения игры
font = pygame.font.SysFont("Arial", 70)
text = "Конец игры!"
self._surface = font.render(text, 1, "#1b10a8c4")
self.rect = self._surface.get_rect()
self.rect.center = self.parent.rect.center
# совет по кнопкам
hint = "Для новой игры нажмите N, для выхода Q"
font_hint = pygame.font.SysFont("Arial", 27)
self.hint = font_hint.render(hint, 1, "#24053da4")
self.hint_rect = self.hint.get_rect()
self.hint_rect.center = self.parent.rect.center
self.hint_rect = self.hint_rect.move(Coords(0, 300))
# Надпись для хорошего финала
text = "Все монетки собраны!"
self.goodtxt = font_hint.render(text, 1, "#96081ba4")
self.goodtxt_rect = self.goodtxt.get_rect()
self.goodtxt_rect.center = self.parent.rect.center
self.goodtxt_rect = self.goodtxt_rect.move(Coords(0, -100))
def draw(self):
if not self.active:
return
if self.scene.coins.all_collected:
self.parent.surface.blit(self.image, self.rect)
self.parent.surface.blit(self.goodtxt, self.goodtxt_rect)
self.parent.surface.blit(self.surface, self.rect)
self.parent.surface.blit(self.hint, self.hint_rect)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_n:
self.parent.want_new_level = True
self.parent.done = True
class Scene(GameObject):
"""основной игровой объект"""
# кнопки для выхода из игры
exit_keys = (pygame.K_ESCAPE, pygame.K_q)
def __init__(
self, assets: dict, screen_sz: Coords, maze_sz: Coords, coins_count: int
):
super().__init__(Coords.zero(), None, assets)
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
self._surface = pygame.display.set_mode(screen_sz)
self.surface.fill("white")
self.rect = self._surface.get_rect()
self.background = pygame.image.load(self.assets["bg1k.png"])
self.background = pygame.transform.scale(self.background, self.rect.size)
hero_sz = Coords(*map(int, box_sz * 0.8))
hero_y_offset = (box_sz.y - hero_sz.y) // 2 + box_sz.y
self.hero = Hero(Coords(0, hero_y_offset), self)
self.done = False
self.level_completed = False
self.maze = maze_gen(6, 6)
self.walls = Walls(self, self.maze, box_sz)
self.coins = Coins(self, self.maze, box_sz, coins_count)
self.end = EndLevel(self)
self.end.active = False
self.want_new_level = False
self.exit_rect = self.get_exit_rect()
# #для тестирования экрана конца уровня
# self.hero.coords = Coords(*self.exit_rect.topleft) + Coords(
# -self.box_sz.x // 2, 5
# )
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
# уменьшаем размер клетки и перемещаем её вправо
rect.width = self.box_sz.x // 4
rect = rect.move(Coords(self.box_sz.x // 2, 0))
return rect
def check_level_completed(self):
self.level_completed = self.exit_rect.colliderect(self.hero.rect)
if self.level_completed:
self.end.active = True
self.hero.active = False
def draw(self):
if self.done:
return
self.surface.fill("white")
self.surface.blit(self.background, self.coords)
if self.level_completed:
self.end.draw()
else:
self.hero.draw()
self.walls.draw()
self.coins.draw()
def scale_box(
self, surface: pygame.Surface, rect: pygame.Rect, scale_factor: Coords
):
rect.size = self.box_sz
rect.scale_by_ip(*scale_factor)
surface = pygame.transform.scale(surface, rect.size)
return surface, rect
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)
self.check_level_completed()
self.end.handle_event(event)
def event_loop(self):
while not self.done:
event = pygame.event.wait()
self.handle_event(event)
self.draw()
pygame.display.flip()
def game(assets):
screen_sz = Coords(1000, 1000)
maze_sz = Coords(6, 6)
coins_count = 10
pygame.display.set_caption("Призрачный лабиринт: сокровища небесного замка")
want_new_level = True
while want_new_level:
scene = Scene(assets, screen_sz, maze_sz, coins_count)
scene.event_loop()
want_new_level = scene.want_new_level
pygame.quit()
def main():
pygame.init()
assets = [
"bg1k.png",
"ghost.png",
"brick.png",
"win.png",
"coin.png",
]
with get_assets(assets) as assets:
game(assets)
main()