Getting Started

Requirements

  • Python 3.14+ (uses type statement syntax)

  • pygame 2.0+ (optional — only needed if you use escapy.pygame)

Installation

From source (development)

pyenv install         # or any other way to install Python 3.14
python -m venv .venv
source .venv/bin/activate

pip install .                        # runtime deps
# or, for development:
pip install -e .[dev]                   # includes ruff, pytest, pre-commit
pre-commit install

As a dependency

pip install escapy                      # core only
pip install escapy[pygame]              # core + pygame UI

Project layout

src/escapy/
├── __init__.py          # Public re-exports
├── types.py             # Position, Room
├── events.py            # All event dataclasses
├── commands.py          # Command factory functions
├── game.py              # Game engine (state + dispatch)
├── messages.py          # MessageProvider utilities
├── mixins.py            # UnlockableMixin, DecodableMixin
├── objects.py           # Ready-made game-object classes
├── protocols/
│   ├── __init__.py      # Protocol re-exports
│   ├── game.py          # GameProtocol, Command type
│   ├── objects.py       # Interactable, Placeable, Unlockable, Decodable
│   └── ui.py            # GameUiProtocol
└── pygame/
    ├── __init__.py      # Re-exports PyGameUi
    └── pygame_ui.py     # Pygame-based UI implementation

Your first game

  1. Define objects — choose from the built-in classes or create your own:

from escapy import PickableObject, SelfKeyLock, MoveToRoom, Position, no_op, reveal

objects = {
    "knife": PickableObject("knife", width=0.05, height=0.05),
    "poster": SelfKeyLock(
        id="poster",
        key_id="knife",
        on_unlock=reveal("hidden-key", "room1", Position(x=0.75, y=0.75)),
        width=0.15,
        height=0.25,
    ),
    "hidden-key": PickableObject("hidden-key", width=0.03, height=0.03),
    "door": MoveToRoom(room_id="room2", width=0.1, height=0.2),
}
  1. Define rooms — a room maps object IDs to positions:

rooms = {
    "room1": {
        "knife": Position(x=0.2, y=0.2),
        "poster": Position(x=0.7, y=0.1),
        "door": Position(x=0.9, y=0.4),
    },
    "room2": {},
}
  1. Create the game:

from escapy import Game

game = Game(objects=objects, rooms=rooms, inventory=[], first_room_id="room1")
  1. Set up messages (optional — maps events to display text):

from escapy import dict_message_provider
from escapy.events import PickedUpEvent, InteractedWithLockedEvent

messages = {
    repr(PickedUpEvent("knife")): "You picked up the knife.",
    repr(InteractedWithLockedEvent("poster")): "It won't budge without a sharp edge.",
}
message_provider = dict_message_provider(messages)
  1. Run with the pygame UI:

from escapy.pygame import PyGameUi

config_ui = {
    "title": "My Escape Room",
    "width": 1024,
    "height": 768,
    "fps": 30,
    "assets_dir": "assets",
    "rooms": {"room1": "room1.png", "room2": "room2.png"},
    "objects": {
        "knife": "knife.png",
        "poster:locked": "poster-locked.png",
        "poster:unlocked": "poster-unlocked.png",
        "hidden-key": "key.png",
        "door": "door.png",
    },
}

ui = PyGameUi(config_ui, message_provider)
ui.init(game)

while ui.is_running:
    ui.tick()
    events = ui.input()
    ui.handle(events)
    ui.render()

ui.quit()

Running tests

pytest -q