phantomcastle: universal version + split
This commit is contained in:
parent
6ee4776711
commit
f0d60d50a8
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,3 +3,7 @@ ext/
|
|||||||
.vscode/
|
.vscode/
|
||||||
pygame-wasm/phantomcastle/build/
|
pygame-wasm/phantomcastle/build/
|
||||||
pygame-wasm/phantomcastle/assets/
|
pygame-wasm/phantomcastle/assets/
|
||||||
|
.venv*/
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
@ -9,7 +9,7 @@ assets/%.png: assets $(../../assets/*.png)
|
|||||||
cp ../../$@ assets/
|
cp ../../$@ assets/
|
||||||
|
|
||||||
build/web: Makefile main.py $(assets_files)
|
build/web: Makefile main.py $(assets_files)
|
||||||
pygbag --width 1000 --height 1000 --can_close 1 --title "Призрачный лабиринт: сокровища небесного замка" --build .
|
pygbag --width 1000 --height 1000 --can_close 1 --title "Призрачный лабиринт: сокровища небесного замка" --icon assets/ghost.png --build .
|
||||||
sed -i -e "s|https://pygame-web.github.io/archives/0.9/|https://b4tman.ru/phantomcastle/pygbag/0.9/|g" build/web/index.html
|
sed -i -e "s|https://pygame-web.github.io/archives/0.9/|https://b4tman.ru/phantomcastle/pygbag/0.9/|g" build/web/index.html
|
||||||
sed -i -e "s|en-us|ru-RU|g" build/web/index.html
|
sed -i -e "s|en-us|ru-RU|g" build/web/index.html
|
||||||
|
|
||||||
|
91
pygame-wasm/phantomcastle/common.py
Normal file
91
pygame-wasm/phantomcastle/common.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
FONT_NAME = "Arial"
|
||||||
|
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
IS_WASM = platform == "emscripten"
|
||||||
|
|
||||||
|
import pygame
|
||||||
|
|
||||||
|
from coords import Coords
|
||||||
|
|
||||||
|
|
||||||
|
class SurfaceWithRect(NamedTuple):
|
||||||
|
surface: pygame.Surface
|
||||||
|
rect: pygame.Rect
|
||||||
|
|
||||||
|
def draw_to(self, target: pygame.Surface):
|
||||||
|
target.blit(self.surface, self.rect)
|
||||||
|
|
||||||
|
|
||||||
|
class DrawableGameObject(ABC):
|
||||||
|
"""обобщение игрового элемента"""
|
||||||
|
|
||||||
|
coords = property(
|
||||||
|
lambda self: self.get_coords(), lambda self, c: self.set_coords(c)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coords: Coords,
|
||||||
|
parent: Optional["DrawableGameObject"] = 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
|
||||||
|
self._mask = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.__class__.__name__}({self.coords})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def surface(self) -> pygame.Surface | None:
|
||||||
|
return self._surface or (self.parent.surface if self.parent else None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mask(self) -> pygame.Mask | None:
|
||||||
|
if not self._mask:
|
||||||
|
self._mask = pygame.mask.from_surface(self.surface.convert_alpha())
|
||||||
|
return self._mask
|
||||||
|
|
||||||
|
def overlap(self, rect: pygame.Rect, mask: pygame.Mask) -> bool:
|
||||||
|
if not self.rect.colliderect(rect):
|
||||||
|
return False
|
||||||
|
|
||||||
|
offset = Coords(*self.rect.topleft) - Coords(*rect.topleft)
|
||||||
|
overlap = mask.overlap(self.mask, offset)
|
||||||
|
return overlap is not 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
|
||||||
|
|
||||||
|
|
||||||
|
class EventHandler(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def handle_keys(self, keys_pressed):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def handle_event(self):
|
||||||
|
pass
|
91
pygame-wasm/phantomcastle/coords.py
Normal file
91
pygame-wasm/phantomcastle/coords.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def dir_norm(self):
|
||||||
|
"""нормализация вектора, но только для получения направления
|
||||||
|
x, y могут быть только -1, 0, 1
|
||||||
|
может быть только направление по горизонтали либо по вертикали
|
||||||
|
либо без направления
|
||||||
|
"""
|
||||||
|
src = self
|
||||||
|
if self.x and self.y:
|
||||||
|
src = (
|
||||||
|
self.__class__(self.x, 0)
|
||||||
|
if abs(self.x) > abs(self.y)
|
||||||
|
else self.__class__(0, self.y)
|
||||||
|
)
|
||||||
|
return self.__class__(*((n > 0) - (n < 0) for n in src))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def zero(cls):
|
||||||
|
return cls(0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
class Direction(Enum):
|
||||||
|
LEFT = 1
|
||||||
|
RIGHT = 2
|
||||||
|
UP = 3
|
||||||
|
DOWN = 4
|
||||||
|
|
||||||
|
def as_coords(self):
|
||||||
|
match self:
|
||||||
|
case Direction.LEFT:
|
||||||
|
return Coords(-1, 0)
|
||||||
|
case Direction.RIGHT:
|
||||||
|
return Coords(1, 0)
|
||||||
|
case Direction.UP:
|
||||||
|
return Coords(0, -1)
|
||||||
|
case Direction.DOWN:
|
||||||
|
return Coords(0, 1)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_coords(coords: Coords) -> Optional["Direction"]:
|
||||||
|
match coords.dir_norm():
|
||||||
|
case Coords(-1, 0):
|
||||||
|
return Direction.LEFT
|
||||||
|
case Coords(1, 0):
|
||||||
|
return Direction.RIGHT
|
||||||
|
case Coords(0, -1):
|
||||||
|
return Direction.UP
|
||||||
|
case Coords(0, 1):
|
||||||
|
return Direction.DOWN
|
5
pygame-wasm/phantomcastle/game/__init__.py
Normal file
5
pygame-wasm/phantomcastle/game/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .scene import Scene
|
||||||
|
from .hero import Hero
|
||||||
|
from .wall import Walls
|
||||||
|
from .coins import Coins
|
||||||
|
from .endlevelmenu import EndLevelMenu
|
108
pygame-wasm/phantomcastle/game/coins.py
Normal file
108
pygame-wasm/phantomcastle/game/coins.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
from random import sample
|
||||||
|
import pygame
|
||||||
|
from coords import Coords
|
||||||
|
from common import DrawableGameObject, FONT_NAME
|
||||||
|
from maze import get_maze_sz
|
||||||
|
|
||||||
|
|
||||||
|
class Coin(DrawableGameObject):
|
||||||
|
"""объект монетки"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coords: Coords,
|
||||||
|
parent: DrawableGameObject,
|
||||||
|
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)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounding_rect(self) -> pygame.Rect:
|
||||||
|
new_rect = self.surface.get_bounding_rect()
|
||||||
|
new_rect.center = self.rect.center
|
||||||
|
return new_rect
|
||||||
|
|
||||||
|
|
||||||
|
class Coins(DrawableGameObject):
|
||||||
|
"""объект коллекции монеток"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: DrawableGameObject,
|
||||||
|
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(FONT_NAME, 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) -> int:
|
||||||
|
return self._capacity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coins_left(self) -> int:
|
||||||
|
return len(self.coins)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coins_collected(self) -> int:
|
||||||
|
return self.capacity - self.coins_left
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_collected(self) -> int:
|
||||||
|
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, actor: DrawableGameObject):
|
||||||
|
mined = [*filter(lambda coin: coin.overlap(actor.rect, actor.mask), self.coins)]
|
||||||
|
for coin in mined:
|
||||||
|
self.coins.remove(coin)
|
||||||
|
self.add_to_collected(coin)
|
97
pygame-wasm/phantomcastle/game/endlevelmenu.py
Normal file
97
pygame-wasm/phantomcastle/game/endlevelmenu.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import pygame
|
||||||
|
from coords import Coords
|
||||||
|
from common import DrawableGameObject, EventHandler, FONT_NAME, IS_WASM, SurfaceWithRect
|
||||||
|
|
||||||
|
|
||||||
|
class EndLevelMenu(DrawableGameObject, EventHandler):
|
||||||
|
def __init__(self, scene: DrawableGameObject):
|
||||||
|
super().__init__(Coords.zero(), scene, scene.assets)
|
||||||
|
self._surface, self.rect = self._create_end_game_label()
|
||||||
|
self.win_image = self._create_win_image()
|
||||||
|
self.win_label = self._create_win_label()
|
||||||
|
self.keys_hint = self._create_keys_hint()
|
||||||
|
self.stats_label = None
|
||||||
|
self.active = False
|
||||||
|
|
||||||
|
def _create_end_game_label(self) -> SurfaceWithRect:
|
||||||
|
"""Надпись завершения игры"""
|
||||||
|
font = pygame.font.SysFont(FONT_NAME, 70)
|
||||||
|
text = "Конец игры!"
|
||||||
|
surface = font.render(text, 1, "#1b10a8c4")
|
||||||
|
rect = surface.get_rect()
|
||||||
|
rect.center = Coords(*self.parent.rect.center) + Coords(0, 38)
|
||||||
|
return SurfaceWithRect(surface, rect)
|
||||||
|
|
||||||
|
def _create_keys_hint(self) -> SurfaceWithRect:
|
||||||
|
"""Совет по кнопкам"""
|
||||||
|
hint_text = "Для новой игры нажмите N"
|
||||||
|
if not IS_WASM:
|
||||||
|
hint_text += ", для выхода Q"
|
||||||
|
font_hint = pygame.font.SysFont(FONT_NAME, 27)
|
||||||
|
surface = font_hint.render(hint_text, 1, "#24053da4")
|
||||||
|
rect = surface.get_rect()
|
||||||
|
rect.center = self.parent.rect.center
|
||||||
|
rect = rect.move(Coords(0, 220))
|
||||||
|
return SurfaceWithRect(surface, rect)
|
||||||
|
|
||||||
|
def _create_win_label(self) -> SurfaceWithRect:
|
||||||
|
"""Надпись для хорошего финала"""
|
||||||
|
font = pygame.font.SysFont(FONT_NAME, 33)
|
||||||
|
text = "Все монетки собраны!"
|
||||||
|
surface = font.render(text, 1, "#96081ba4")
|
||||||
|
rect = surface.get_rect()
|
||||||
|
rect.center = self.parent.rect.center
|
||||||
|
rect.move_ip(Coords(0, -200))
|
||||||
|
return SurfaceWithRect(surface, rect)
|
||||||
|
|
||||||
|
def _create_win_image(self) -> SurfaceWithRect:
|
||||||
|
"""Картинка для хорошего финала"""
|
||||||
|
surface = pygame.image.load(self.scene.assets["win.png"])
|
||||||
|
rect = surface.get_rect()
|
||||||
|
rect.center = self.parent.rect.center
|
||||||
|
return SurfaceWithRect(surface, rect)
|
||||||
|
|
||||||
|
def _create_stats_label(self) -> SurfaceWithRect:
|
||||||
|
"""Общая статистика игры"""
|
||||||
|
stats_text = f"Всего пройдено уровней: {self.scene.total_levels}, собрано монет: {self.scene.total_coins}"
|
||||||
|
stats_font = pygame.font.SysFont(FONT_NAME, 27)
|
||||||
|
surface = stats_font.render(stats_text, 1, "#031f03a4")
|
||||||
|
rect = surface.get_rect()
|
||||||
|
rect.center = Coords(*self.scene.rect.center) + Coords(0, 350)
|
||||||
|
return SurfaceWithRect(surface, rect)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
if self.scene.coins.all_collected:
|
||||||
|
self.win_image.draw_to(self.parent.surface)
|
||||||
|
self.win_label.draw_to(self.parent.surface)
|
||||||
|
|
||||||
|
self.parent.surface.blit(self.surface, self.rect)
|
||||||
|
self.keys_hint.draw_to(self.parent.surface)
|
||||||
|
|
||||||
|
# статистика
|
||||||
|
if self.stats_label is None:
|
||||||
|
self.stats_label = self._create_stats_label()
|
||||||
|
self.stats_label.draw_to(self.parent.surface)
|
||||||
|
|
||||||
|
def request_new_level(self):
|
||||||
|
self.scene.want_new_level = True
|
||||||
|
self.scene.done = True
|
||||||
|
|
||||||
|
def handle_keys(self, keys_pressed):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
if keys_pressed[pygame.K_n]:
|
||||||
|
self.request_new_level()
|
||||||
|
|
||||||
|
def handle_mouse_event(self, event: pygame.event.Event):
|
||||||
|
if event.type == pygame.MOUSEBUTTONDOWN and self.keys_hint.rect.collidepoint(
|
||||||
|
event.pos
|
||||||
|
):
|
||||||
|
self.request_new_level()
|
||||||
|
|
||||||
|
def handle_event(self, event: pygame.event.Event):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
self.handle_mouse_event(event)
|
138
pygame-wasm/phantomcastle/game/hero.py
Normal file
138
pygame-wasm/phantomcastle/game/hero.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import pygame
|
||||||
|
from coords import Coords, Direction
|
||||||
|
from common import DrawableGameObject, EventHandler
|
||||||
|
|
||||||
|
|
||||||
|
class Hero(DrawableGameObject, EventHandler):
|
||||||
|
"""объект главного героя"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coords: Coords,
|
||||||
|
parent: DrawableGameObject,
|
||||||
|
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._speed = 1
|
||||||
|
self.direction = Direction.RIGHT
|
||||||
|
self.mouse_active = 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, self.mask)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speed(self):
|
||||||
|
return max(self._speed, 1)
|
||||||
|
|
||||||
|
@speed.setter
|
||||||
|
def speed(self, value):
|
||||||
|
self._speed = min(value, 15)
|
||||||
|
|
||||||
|
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:
|
||||||
|
super().set_coords(coords)
|
||||||
|
self.scene.coins.collect(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
# уменьшение шага
|
||||||
|
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)
|
||||||
|
self.scene.coins.collect(self)
|
||||||
|
|
||||||
|
def flip(self):
|
||||||
|
self.looking_right = not self.looking_right
|
||||||
|
self._surface = pygame.transform.flip(self.surface, flip_x=True, flip_y=False)
|
||||||
|
|
||||||
|
def update_direction(self, direction: Direction):
|
||||||
|
if direction in (Direction.LEFT, Direction.RIGHT):
|
||||||
|
going_right = direction == Direction.RIGHT
|
||||||
|
if self.looking_right != going_right:
|
||||||
|
self.flip()
|
||||||
|
|
||||||
|
if direction != self.direction:
|
||||||
|
self.speed = 0
|
||||||
|
self.direction = direction
|
||||||
|
else:
|
||||||
|
self.speed += 1
|
||||||
|
|
||||||
|
def move(self, direction: Direction, step: int = 1):
|
||||||
|
self.update_direction(direction)
|
||||||
|
self.coords += direction.as_coords() * step * self.speed // 3
|
||||||
|
|
||||||
|
def handle_mouse_event(self, event: pygame.event.Event):
|
||||||
|
if event.type not in (
|
||||||
|
pygame.MOUSEBUTTONDOWN,
|
||||||
|
pygame.MOUSEBUTTONUP,
|
||||||
|
pygame.MOUSEMOTION,
|
||||||
|
):
|
||||||
|
return
|
||||||
|
match event.type:
|
||||||
|
case pygame.MOUSEBUTTONDOWN:
|
||||||
|
self.mouse_active = self.rect.collidepoint(event.pos)
|
||||||
|
case pygame.MOUSEBUTTONUP:
|
||||||
|
self.mouse_active = False
|
||||||
|
case pygame.MOUSEMOTION if self.mouse_active:
|
||||||
|
rel = Coords(*event.rel)
|
||||||
|
direction = Direction.from_coords(rel)
|
||||||
|
if direction:
|
||||||
|
self.update_direction(direction)
|
||||||
|
self.coords += rel
|
||||||
|
|
||||||
|
def handle_event(self, event: pygame.event.Event):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
self.handle_mouse_event(event)
|
||||||
|
|
||||||
|
def handle_keys(self, keys_pressed):
|
||||||
|
if not self.active:
|
||||||
|
return
|
||||||
|
|
||||||
|
wide, short = 3, 1
|
||||||
|
if keys_pressed[pygame.K_UP]:
|
||||||
|
self.move(Direction.UP, wide)
|
||||||
|
if keys_pressed[pygame.K_DOWN]:
|
||||||
|
self.move(Direction.DOWN, wide)
|
||||||
|
if keys_pressed[pygame.K_LEFT]:
|
||||||
|
self.move(Direction.LEFT, wide)
|
||||||
|
if keys_pressed[pygame.K_RIGHT]:
|
||||||
|
self.move(Direction.RIGHT, wide)
|
||||||
|
if keys_pressed[pygame.K_w]:
|
||||||
|
self.move(Direction.UP, short)
|
||||||
|
if keys_pressed[pygame.K_s]:
|
||||||
|
self.move(Direction.DOWN, short)
|
||||||
|
if keys_pressed[pygame.K_a]:
|
||||||
|
self.move(Direction.LEFT, short)
|
||||||
|
if keys_pressed[pygame.K_d]:
|
||||||
|
self.move(Direction.RIGHT, short)
|
129
pygame-wasm/phantomcastle/game/scene.py
Normal file
129
pygame-wasm/phantomcastle/game/scene.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import asyncio
|
||||||
|
import pygame
|
||||||
|
from maze import maze_gen, get_maze_sz
|
||||||
|
from coords import Coords
|
||||||
|
from common import DrawableGameObject, EventHandler, IS_WASM
|
||||||
|
|
||||||
|
from game.hero import Hero
|
||||||
|
from game.wall import Walls
|
||||||
|
from game.coins import Coins
|
||||||
|
from game.endlevelmenu import EndLevelMenu
|
||||||
|
|
||||||
|
|
||||||
|
class Scene(DrawableGameObject, EventHandler):
|
||||||
|
"""основной игровой объект"""
|
||||||
|
|
||||||
|
# кнопки для выхода из игры
|
||||||
|
exit_keys = (pygame.K_ESCAPE, pygame.K_q)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
assets: dict,
|
||||||
|
screen_sz: Coords,
|
||||||
|
maze_sz: Coords,
|
||||||
|
coins_count: int,
|
||||||
|
fps: 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)
|
||||||
|
|
||||||
|
self.total_levels, self.total_coins = 0, 0
|
||||||
|
|
||||||
|
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 = EndLevelMenu(self)
|
||||||
|
self.end.active = False
|
||||||
|
self.want_new_level = False
|
||||||
|
self.exit_rect = self.get_exit_rect()
|
||||||
|
self.fps = fps
|
||||||
|
|
||||||
|
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 = 1, self.box_sz.y
|
||||||
|
return rect.move((self.box_sz.x, 0))
|
||||||
|
|
||||||
|
def check_level_completed(self):
|
||||||
|
level_completed = self.exit_rect.colliderect(self.hero.rect)
|
||||||
|
if level_completed and not self.level_completed:
|
||||||
|
self.total_coins += self.coins.coins_collected
|
||||||
|
self.total_levels += 1
|
||||||
|
self.end.active = True
|
||||||
|
self.hero.active = False
|
||||||
|
self.level_completed = True
|
||||||
|
|
||||||
|
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_keys(self, keys_pressed):
|
||||||
|
if self.done:
|
||||||
|
return
|
||||||
|
if not self.done:
|
||||||
|
self.hero.handle_keys(keys_pressed)
|
||||||
|
self.check_level_completed()
|
||||||
|
self.end.handle_keys(keys_pressed)
|
||||||
|
|
||||||
|
def handle_exit(self, event: pygame.event.Event):
|
||||||
|
if IS_WASM:
|
||||||
|
return
|
||||||
|
if (
|
||||||
|
event.type == pygame.QUIT
|
||||||
|
or event.type == pygame.KEYDOWN
|
||||||
|
and event.key in self.exit_keys
|
||||||
|
):
|
||||||
|
self.done = True
|
||||||
|
|
||||||
|
def handle_event(self, event: pygame.event.Event):
|
||||||
|
self.handle_exit(event)
|
||||||
|
if self.done:
|
||||||
|
return
|
||||||
|
self.hero.handle_event(event)
|
||||||
|
self.check_level_completed()
|
||||||
|
self.end.handle_event(event)
|
||||||
|
|
||||||
|
async def event_loop(self):
|
||||||
|
clock = pygame.time.Clock()
|
||||||
|
while not self.done:
|
||||||
|
for event in pygame.event.get():
|
||||||
|
self.handle_event(event)
|
||||||
|
self.handle_keys(pygame.key.get_pressed())
|
||||||
|
self.draw()
|
||||||
|
pygame.display.flip()
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
clock.tick(self.fps)
|
55
pygame-wasm/phantomcastle/game/wall.py
Normal file
55
pygame-wasm/phantomcastle/game/wall.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import pygame
|
||||||
|
from coords import Coords
|
||||||
|
from common import DrawableGameObject
|
||||||
|
|
||||||
|
|
||||||
|
class WallBlock(DrawableGameObject):
|
||||||
|
"""объект элемента стены"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coords: Coords,
|
||||||
|
parent: DrawableGameObject,
|
||||||
|
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)
|
||||||
|
self._mask = pygame.mask.Mask(self.rect.size, fill=True)
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
self.parent.surface.blit(self.surface, self.rect)
|
||||||
|
|
||||||
|
|
||||||
|
class Walls(DrawableGameObject):
|
||||||
|
"""объект стен"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: DrawableGameObject,
|
||||||
|
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, mask: pygame.Mask) -> bool:
|
||||||
|
for block in self.blocks:
|
||||||
|
if block.overlap(rect, mask):
|
||||||
|
return True
|
||||||
|
return False
|
@ -1,6 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Игра "Призрачный лабиринт: сокровища небесного замка"
|
Игра "Призрачный лабиринт: сокровища небесного замка"
|
||||||
версия для сборки в виде WASM (для браузера) игры с помощью pygbag
|
|
||||||
|
Призрак в лабиринте, управление стрелками и WASD, выход Esc или Q.
|
||||||
|
Чтобы пройти уровень, нужно дойти до выхода, желательно собрав все монетки.
|
||||||
|
После прохождения уровня можно начать новый (кнопкой N) или выйти из игры.
|
||||||
|
Также можно управлять мышкой, перетаскивая героя (зажав ЛКМ) и кликая на надпись,
|
||||||
|
для начала новой игры.
|
||||||
|
|
||||||
|
Это универсальная версия. Подходит для сборки в виде WASM (для браузера) игры,
|
||||||
|
с помощью pygbag и для запуска напрямую.
|
||||||
|
|
||||||
pip install pygame pygbag
|
pip install pygame pygbag
|
||||||
потом make (в Linux/WSL)
|
потом make (в Linux/WSL)
|
||||||
@ -8,15 +16,13 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from enum import Enum
|
from game import Scene
|
||||||
from random import choice, randrange, sample
|
from coords import Coords
|
||||||
from typing import NamedTuple, Optional
|
|
||||||
|
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
FONT_NAME = "Arial"
|
FPS = 30
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@ -26,727 +32,6 @@ def get_assets_direct(names):
|
|||||||
yield {asset: os.path.join(assets_dir, asset) for asset in names}
|
yield {asset: os.path.join(assets_dir, asset) for asset in names}
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def dir_norm(self):
|
|
||||||
"""нормализация вектора, но только для получения направления
|
|
||||||
x, y могут быть только -1, 0, 1
|
|
||||||
может быть только направление по горизонтали либо по вертикали
|
|
||||||
либо без направления
|
|
||||||
"""
|
|
||||||
src = self
|
|
||||||
if self.x and self.y:
|
|
||||||
src = (
|
|
||||||
self.__class__(self.x, 0)
|
|
||||||
if abs(self.x) > abs(self.y)
|
|
||||||
else self.__class__(0, self.y)
|
|
||||||
)
|
|
||||||
return self.__class__(*((n > 0) - (n < 0) for n in src))
|
|
||||||
|
|
||||||
@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 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 pattern, replacement in upd.items():
|
|
||||||
for i in range(len(maze) - len(pattern) + 1):
|
|
||||||
for j in range(len(maze[0]) - len(pattern[0]) + 1):
|
|
||||||
if all(
|
|
||||||
maze[i + k][j : j + len(v)] == list(map(int, v))
|
|
||||||
for k, v in enumerate(pattern)
|
|
||||||
):
|
|
||||||
for k, v in filter(lambda x: x[1], enumerate(replacement)):
|
|
||||||
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 Direction(Enum):
|
|
||||||
LEFT = 1
|
|
||||||
RIGHT = 2
|
|
||||||
UP = 3
|
|
||||||
DOWN = 4
|
|
||||||
|
|
||||||
def as_coords(self):
|
|
||||||
match self:
|
|
||||||
case Direction.LEFT:
|
|
||||||
return Coords(-1, 0)
|
|
||||||
case Direction.RIGHT:
|
|
||||||
return Coords(1, 0)
|
|
||||||
case Direction.UP:
|
|
||||||
return Coords(0, -1)
|
|
||||||
case Direction.DOWN:
|
|
||||||
return Coords(0, 1)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_coords(coords: Coords) -> Optional["Direction"]:
|
|
||||||
match coords.dir_norm():
|
|
||||||
case Coords(-1, 0):
|
|
||||||
return Direction.LEFT
|
|
||||||
case Coords(1, 0):
|
|
||||||
return Direction.RIGHT
|
|
||||||
case Coords(0, -1):
|
|
||||||
return Direction.UP
|
|
||||||
case Coords(0, 1):
|
|
||||||
return Direction.DOWN
|
|
||||||
|
|
||||||
|
|
||||||
class SurfaceWithRect(NamedTuple):
|
|
||||||
surface: pygame.Surface
|
|
||||||
rect: pygame.Rect
|
|
||||||
|
|
||||||
def draw_to(self, target: pygame.Surface):
|
|
||||||
target.blit(self.surface, self.rect)
|
|
||||||
|
|
||||||
|
|
||||||
class DrawableGameObject(ABC):
|
|
||||||
"""обобщение игрового элемента"""
|
|
||||||
|
|
||||||
coords = property(
|
|
||||||
lambda self: self.get_coords(), lambda self, c: self.set_coords(c)
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coords: Coords,
|
|
||||||
parent: Optional["DrawableGameObject"] = 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
|
|
||||||
self._mask = None
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.__class__.__name__}({self.coords})"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def surface(self) -> pygame.Surface | None:
|
|
||||||
return self._surface or (self.parent.surface if self.parent else None)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mask(self) -> pygame.Mask | None:
|
|
||||||
if not self._mask:
|
|
||||||
self._mask = pygame.mask.from_surface(self.surface.convert_alpha())
|
|
||||||
return self._mask
|
|
||||||
|
|
||||||
def overlap(self, rect: pygame.Rect, mask: pygame.Mask) -> bool:
|
|
||||||
if not self.rect.colliderect(rect):
|
|
||||||
return False
|
|
||||||
|
|
||||||
offset = Coords(*self.rect.topleft) - Coords(*rect.topleft)
|
|
||||||
overlap = mask.overlap(self.mask, offset)
|
|
||||||
return overlap is not 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
|
|
||||||
|
|
||||||
|
|
||||||
class EventHandler(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def handle_keys(self, keys_pressed):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def handle_event(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Hero(DrawableGameObject, EventHandler):
|
|
||||||
"""объект главного героя"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coords: Coords,
|
|
||||||
parent: DrawableGameObject,
|
|
||||||
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._speed = 1
|
|
||||||
self.direction = Direction.RIGHT
|
|
||||||
self.mouse_active = 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, self.mask)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def speed(self):
|
|
||||||
return max(self._speed, 1)
|
|
||||||
|
|
||||||
@speed.setter
|
|
||||||
def speed(self, value):
|
|
||||||
self._speed = min(value, 15)
|
|
||||||
|
|
||||||
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:
|
|
||||||
super().set_coords(coords)
|
|
||||||
self.scene.coins.collect(self)
|
|
||||||
return
|
|
||||||
|
|
||||||
# уменьшение шага
|
|
||||||
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)
|
|
||||||
self.scene.coins.collect(self)
|
|
||||||
|
|
||||||
def flip(self):
|
|
||||||
self.looking_right = not self.looking_right
|
|
||||||
self._surface = pygame.transform.flip(self.surface, flip_x=True, flip_y=False)
|
|
||||||
|
|
||||||
def update_direction(self, direction: Direction):
|
|
||||||
if direction in (Direction.LEFT, Direction.RIGHT):
|
|
||||||
going_right = direction == Direction.RIGHT
|
|
||||||
if self.looking_right != going_right:
|
|
||||||
self.flip()
|
|
||||||
|
|
||||||
if direction != self.direction:
|
|
||||||
self.speed = 0
|
|
||||||
self.direction = direction
|
|
||||||
else:
|
|
||||||
self.speed += 1
|
|
||||||
|
|
||||||
def move(self, direction: Direction, step: int = 1):
|
|
||||||
self.update_direction(direction)
|
|
||||||
self.coords += direction.as_coords() * step * self.speed // 3
|
|
||||||
|
|
||||||
def handle_mouse_event(self, event: pygame.event.Event):
|
|
||||||
if event.type not in (
|
|
||||||
pygame.MOUSEBUTTONDOWN,
|
|
||||||
pygame.MOUSEBUTTONUP,
|
|
||||||
pygame.MOUSEMOTION,
|
|
||||||
):
|
|
||||||
return
|
|
||||||
match event.type:
|
|
||||||
case pygame.MOUSEBUTTONDOWN:
|
|
||||||
self.mouse_active = self.rect.collidepoint(event.pos)
|
|
||||||
case pygame.MOUSEBUTTONUP:
|
|
||||||
self.mouse_active = False
|
|
||||||
case pygame.MOUSEMOTION if self.mouse_active:
|
|
||||||
rel = Coords(*event.rel)
|
|
||||||
direction = Direction.from_coords(rel)
|
|
||||||
if direction:
|
|
||||||
self.update_direction(direction)
|
|
||||||
self.coords += rel
|
|
||||||
|
|
||||||
def handle_event(self, event: pygame.event.Event):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
self.handle_mouse_event(event)
|
|
||||||
|
|
||||||
def handle_keys(self, keys_pressed):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
|
|
||||||
wide, short = 3, 1
|
|
||||||
if keys_pressed[pygame.K_UP]:
|
|
||||||
self.move(Direction.UP, wide)
|
|
||||||
if keys_pressed[pygame.K_DOWN]:
|
|
||||||
self.move(Direction.DOWN, wide)
|
|
||||||
if keys_pressed[pygame.K_LEFT]:
|
|
||||||
self.move(Direction.LEFT, wide)
|
|
||||||
if keys_pressed[pygame.K_RIGHT]:
|
|
||||||
self.move(Direction.RIGHT, wide)
|
|
||||||
if keys_pressed[pygame.K_w]:
|
|
||||||
self.move(Direction.UP, short)
|
|
||||||
if keys_pressed[pygame.K_s]:
|
|
||||||
self.move(Direction.DOWN, short)
|
|
||||||
if keys_pressed[pygame.K_a]:
|
|
||||||
self.move(Direction.LEFT, short)
|
|
||||||
if keys_pressed[pygame.K_d]:
|
|
||||||
self.move(Direction.RIGHT, short)
|
|
||||||
|
|
||||||
|
|
||||||
class WallBlock(DrawableGameObject):
|
|
||||||
"""объект элемента стены"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coords: Coords,
|
|
||||||
parent: DrawableGameObject,
|
|
||||||
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)
|
|
||||||
self._mask = pygame.mask.Mask(self.rect.size, fill=True)
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
self.parent.surface.blit(self.surface, self.rect)
|
|
||||||
|
|
||||||
|
|
||||||
class Walls(DrawableGameObject):
|
|
||||||
"""объект стен"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
parent: DrawableGameObject,
|
|
||||||
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, mask: pygame.Mask) -> bool:
|
|
||||||
for block in self.blocks:
|
|
||||||
if block.overlap(rect, mask):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Coin(DrawableGameObject):
|
|
||||||
"""объект монетки"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coords: Coords,
|
|
||||||
parent: DrawableGameObject,
|
|
||||||
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)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bounding_rect(self) -> pygame.Rect:
|
|
||||||
new_rect = self.surface.get_bounding_rect()
|
|
||||||
new_rect.center = self.rect.center
|
|
||||||
return new_rect
|
|
||||||
|
|
||||||
|
|
||||||
class Coins(DrawableGameObject):
|
|
||||||
"""объект коллекции монеток"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
parent: DrawableGameObject,
|
|
||||||
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(FONT_NAME, 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) -> int:
|
|
||||||
return self._capacity
|
|
||||||
|
|
||||||
@property
|
|
||||||
def coins_left(self) -> int:
|
|
||||||
return len(self.coins)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def coins_collected(self) -> int:
|
|
||||||
return self.capacity - self.coins_left
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all_collected(self) -> int:
|
|
||||||
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, actor: DrawableGameObject):
|
|
||||||
mined = [*filter(lambda coin: coin.overlap(actor.rect, actor.mask), self.coins)]
|
|
||||||
for coin in mined:
|
|
||||||
self.coins.remove(coin)
|
|
||||||
self.add_to_collected(coin)
|
|
||||||
|
|
||||||
|
|
||||||
class EndLevelMenu(DrawableGameObject, EventHandler):
|
|
||||||
def __init__(self, scene: DrawableGameObject):
|
|
||||||
super().__init__(Coords.zero(), scene, scene.assets)
|
|
||||||
self._surface, self.rect = self._create_end_game_label()
|
|
||||||
self.win_image = self._create_win_image()
|
|
||||||
self.win_label = self._create_win_label()
|
|
||||||
self.keys_hint = self._create_keys_hint()
|
|
||||||
self.stats_label = None
|
|
||||||
self.active = False
|
|
||||||
|
|
||||||
def _create_end_game_label(self) -> SurfaceWithRect:
|
|
||||||
"""Надпись завершения игры"""
|
|
||||||
font = pygame.font.SysFont(FONT_NAME, 70)
|
|
||||||
text = "Конец игры!"
|
|
||||||
surface = font.render(text, 1, "#1b10a8c4")
|
|
||||||
rect = surface.get_rect()
|
|
||||||
rect.center = Coords(*self.parent.rect.center) + Coords(0, 38)
|
|
||||||
return SurfaceWithRect(surface, rect)
|
|
||||||
|
|
||||||
def _create_keys_hint(self) -> SurfaceWithRect:
|
|
||||||
"""Совет по кнопкам"""
|
|
||||||
hint_text = "Для новой игры нажмите N"
|
|
||||||
font_hint = pygame.font.SysFont(FONT_NAME, 27)
|
|
||||||
surface = font_hint.render(hint_text, 1, "#24053da4")
|
|
||||||
rect = surface.get_rect()
|
|
||||||
rect.center = self.parent.rect.center
|
|
||||||
rect = rect.move(Coords(0, 220))
|
|
||||||
return SurfaceWithRect(surface, rect)
|
|
||||||
|
|
||||||
def _create_win_label(self) -> SurfaceWithRect:
|
|
||||||
"""Надпись для хорошего финала"""
|
|
||||||
font = pygame.font.SysFont(FONT_NAME, 33)
|
|
||||||
text = "Все монетки собраны!"
|
|
||||||
surface = font.render(text, 1, "#96081ba4")
|
|
||||||
rect = surface.get_rect()
|
|
||||||
rect.center = self.parent.rect.center
|
|
||||||
rect.move_ip(Coords(0, -200))
|
|
||||||
return SurfaceWithRect(surface, rect)
|
|
||||||
|
|
||||||
def _create_win_image(self) -> SurfaceWithRect:
|
|
||||||
"""Картинка для хорошего финала"""
|
|
||||||
surface = pygame.image.load(self.scene.assets["win.png"])
|
|
||||||
rect = surface.get_rect()
|
|
||||||
rect.center = self.parent.rect.center
|
|
||||||
return SurfaceWithRect(surface, rect)
|
|
||||||
|
|
||||||
def _create_stats_label(self) -> SurfaceWithRect:
|
|
||||||
"""Общая статистика игры"""
|
|
||||||
stats_text = f"Всего пройдено уровней: {self.scene.total_levels}, собрано монет: {self.scene.total_coins}"
|
|
||||||
stats_font = pygame.font.SysFont(FONT_NAME, 27)
|
|
||||||
surface = stats_font.render(stats_text, 1, "#031f03a4")
|
|
||||||
rect = surface.get_rect()
|
|
||||||
rect.center = Coords(*self.scene.rect.center) + Coords(0, 350)
|
|
||||||
return SurfaceWithRect(surface, rect)
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
if self.scene.coins.all_collected:
|
|
||||||
self.win_image.draw_to(self.parent.surface)
|
|
||||||
self.win_label.draw_to(self.parent.surface)
|
|
||||||
|
|
||||||
self.parent.surface.blit(self.surface, self.rect)
|
|
||||||
self.keys_hint.draw_to(self.parent.surface)
|
|
||||||
|
|
||||||
# статистика
|
|
||||||
if self.stats_label is None:
|
|
||||||
self.stats_label = self._create_stats_label()
|
|
||||||
self.stats_label.draw_to(self.parent.surface)
|
|
||||||
|
|
||||||
def request_new_level(self):
|
|
||||||
self.scene.want_new_level = True
|
|
||||||
self.scene.done = True
|
|
||||||
|
|
||||||
def handle_keys(self, keys_pressed):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
if keys_pressed[pygame.K_n]:
|
|
||||||
self.request_new_level()
|
|
||||||
|
|
||||||
def handle_mouse_event(self, event: pygame.event.Event):
|
|
||||||
if event.type == pygame.MOUSEBUTTONDOWN and self.keys_hint.rect.collidepoint(
|
|
||||||
event.pos
|
|
||||||
):
|
|
||||||
self.request_new_level()
|
|
||||||
|
|
||||||
def handle_event(self, event: pygame.event.Event):
|
|
||||||
if not self.active:
|
|
||||||
return
|
|
||||||
self.handle_mouse_event(event)
|
|
||||||
|
|
||||||
|
|
||||||
class Scene(DrawableGameObject, EventHandler):
|
|
||||||
"""основной игровой объект"""
|
|
||||||
|
|
||||||
# кнопки для выхода из игры
|
|
||||||
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)
|
|
||||||
|
|
||||||
self.total_levels, self.total_coins = 0, 0
|
|
||||||
|
|
||||||
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 = EndLevelMenu(self)
|
|
||||||
self.end.active = False
|
|
||||||
self.want_new_level = False
|
|
||||||
self.exit_rect = self.get_exit_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 = 1, self.box_sz.y
|
|
||||||
return rect.move((self.box_sz.x, 0))
|
|
||||||
|
|
||||||
def check_level_completed(self):
|
|
||||||
level_completed = self.exit_rect.colliderect(self.hero.rect)
|
|
||||||
if level_completed and not self.level_completed:
|
|
||||||
self.total_coins += self.coins.coins_collected
|
|
||||||
self.total_levels += 1
|
|
||||||
self.end.active = True
|
|
||||||
self.hero.active = False
|
|
||||||
self.level_completed = True
|
|
||||||
|
|
||||||
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_keys(self, keys_pressed):
|
|
||||||
if self.done:
|
|
||||||
return
|
|
||||||
if not self.done:
|
|
||||||
self.hero.handle_keys(keys_pressed)
|
|
||||||
self.check_level_completed()
|
|
||||||
self.end.handle_keys(keys_pressed)
|
|
||||||
|
|
||||||
def handle_event(self, event: pygame.event.Event):
|
|
||||||
if self.done:
|
|
||||||
return
|
|
||||||
self.hero.handle_event(event)
|
|
||||||
self.check_level_completed()
|
|
||||||
self.end.handle_event(event)
|
|
||||||
|
|
||||||
async def event_loop(self):
|
|
||||||
clock = pygame.time.Clock()
|
|
||||||
while not self.done:
|
|
||||||
for event in pygame.event.get():
|
|
||||||
self.handle_event(event)
|
|
||||||
self.handle_keys(pygame.key.get_pressed())
|
|
||||||
self.draw()
|
|
||||||
pygame.display.flip()
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
clock.tick(30)
|
|
||||||
|
|
||||||
|
|
||||||
async def game(assets):
|
async def game(assets):
|
||||||
screen_sz = Coords(1000, 1000)
|
screen_sz = Coords(1000, 1000)
|
||||||
maze_sz = Coords(6, 6)
|
maze_sz = Coords(6, 6)
|
||||||
@ -757,7 +42,7 @@ async def game(assets):
|
|||||||
total_coins = 0
|
total_coins = 0
|
||||||
want_new_level = True
|
want_new_level = True
|
||||||
while want_new_level:
|
while want_new_level:
|
||||||
scene = Scene(assets, screen_sz, maze_sz, coins_count)
|
scene = Scene(assets, screen_sz, maze_sz, coins_count, FPS)
|
||||||
scene.total_levels, scene.total_coins = total_levels, total_coins
|
scene.total_levels, scene.total_coins = total_levels, total_coins
|
||||||
await scene.event_loop()
|
await scene.event_loop()
|
||||||
|
|
||||||
@ -765,6 +50,8 @@ async def game(assets):
|
|||||||
total_levels = scene.total_levels
|
total_levels = scene.total_levels
|
||||||
total_coins = scene.total_coins
|
total_coins = scene.total_coins
|
||||||
|
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
pygame.init()
|
pygame.init()
|
||||||
|
72
pygame-wasm/phantomcastle/maze.py
Normal file
72
pygame-wasm/phantomcastle/maze.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
from random import choice, randrange
|
||||||
|
|
||||||
|
from coords import Coords
|
||||||
|
|
||||||
|
|
||||||
|
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 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 pattern, replacement in upd.items():
|
||||||
|
for i in range(len(maze) - len(pattern) + 1):
|
||||||
|
for j in range(len(maze[0]) - len(pattern[0]) + 1):
|
||||||
|
if all(
|
||||||
|
maze[i + k][j : j + len(v)] == list(map(int, v))
|
||||||
|
for k, v in enumerate(pattern)
|
||||||
|
):
|
||||||
|
for k, v in filter(lambda x: x[1], enumerate(replacement)):
|
||||||
|
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))
|
Loading…
Reference in New Issue
Block a user