diff --git a/.gitignore b/.gitignore
index 537f58e..e590358 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 bank.json
 ext/
 .vscode/
+pygame-wasm/phantomcastle/build/
+pygame-wasm/phantomcastle/assets/
diff --git a/pygame-wasm/phantomcastle/Makefile b/pygame-wasm/phantomcastle/Makefile
new file mode 100644
index 0000000..f867a5c
--- /dev/null
+++ b/pygame-wasm/phantomcastle/Makefile
@@ -0,0 +1,12 @@
+assets_files = assets/bg1k.png assets/brick.png assets/coin.png assets/ghost.png assets/win.png
+
+all: build/web.zip
+
+assets:
+	mkdir assets
+
+assets/%.png: assets $(../../assets/*.png)
+	cp ../../$@ assets/
+
+build/web.zip: Makefile main.py $(assets_files)
+	pygbag --width 1000 --height 1000 --build --archive .
diff --git a/pygame-wasm/phantomcastle/main.py b/pygame-wasm/phantomcastle/main.py
new file mode 100644
index 0000000..b9bc98e
--- /dev/null
+++ b/pygame-wasm/phantomcastle/main.py
@@ -0,0 +1,673 @@
+"""
+    Игра "Призрачный лабиринт: сокровища небесного замка"
+    версия для сборки в виде WASM (для браузера) игры с помощью pygbag
+
+    pip install pygame pygbag
+    потом make (в Linux/WSL)
+"""
+
+import asyncio
+import os
+from abc import ABC, abstractmethod
+from contextlib import contextmanager
+from enum import Enum
+from random import choice, randrange, sample
+from typing import NamedTuple, Optional
+
+import pygame
+
+FONT_NAME = "Arial"
+
+
+@contextmanager
+def get_assets_direct(names):
+    """передача файлов картинок 1 в 1"""
+    assets_dir = "assets"
+    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
+
+    @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)
+
+
+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
+
+    @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
+
+
+class KeysHandler(ABC):
+    @abstractmethod
+    def handle_keys(self, keys_pressed):
+        pass
+
+
+class Hero(DrawableGameObject, KeysHandler):
+    """объект главного героя"""
+
+    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.flip()
+
+    def draw(self):
+        self.parent.surface.blit(self.surface, self.rect)
+
+    def _check_collision(self, coords):
+        """Проверка пересечения со стенами"""
+        new_rect = self.surface.get_bounding_rect()
+        new_rect.topleft = coords
+        new_rect.scale_by_ip(0.99, 0.99)
+        return self.scene.walls.check_collision(new_rect)
+
+    @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:
+            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, 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
+        self.scene.coins.collect(self.rect)
+
+    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)
+
+    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) -> bool:
+        for block in self.blocks:
+            if block.rect.colliderect(rect):
+                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, rect: pygame.Rect):
+        mined = [*filter(lambda coin: coin.bounding_rect.colliderect(rect), self.coins)]
+        for coin in mined:
+            self.coins.remove(coin)
+            self.add_to_collected(coin)
+
+
+class EndLevelMenu(DrawableGameObject, KeysHandler):
+    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 handle_keys(self, keys_pressed):
+        if not self.active:
+            return
+        if keys_pressed[pygame.K_n]:
+            self.parent.want_new_level = True
+            self.parent.done = True
+
+
+class Scene(DrawableGameObject, KeysHandler):
+    """основной игровой объект"""
+
+    # кнопки для выхода из игры
+    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)
+
+    async def event_loop(self):
+        clock = pygame.time.Clock()
+        while not self.done:
+            pygame.event.pump()
+            self.handle_keys(pygame.key.get_pressed())
+            self.draw()
+            pygame.display.flip()
+            await asyncio.sleep(0)
+            clock.tick(30)
+
+
+async def game(assets):
+    screen_sz = Coords(1000, 1000)
+    maze_sz = Coords(6, 6)
+    coins_count = 10
+    pygame.display.set_caption("Призрачный лабиринт: сокровища небесного замка")
+
+    total_levels = 0
+    total_coins = 0
+    want_new_level = True
+    while want_new_level:
+        scene = Scene(assets, screen_sz, maze_sz, coins_count)
+        scene.total_levels, scene.total_coins = total_levels, total_coins
+        await scene.event_loop()
+
+        want_new_level = scene.want_new_level
+        total_levels = scene.total_levels
+        total_coins = scene.total_coins
+
+
+async def main():
+    pygame.init()
+    required_assets = [
+        "bg1k.png",
+        "ghost.png",
+        "brick.png",
+        "win.png",
+        "coin.png",
+    ]
+    with get_assets_direct(required_assets) as assets:
+        await game(assets)
+
+
+asyncio.run(main())