phantomcastle: universal version + split
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user