Architecture
Overview
escapy follows a strict Model → Command → Event → UI pipeline:
Player Action
│
▼
┌──────────┐
│ Game │ (model – mutable state)
└────┬─────┘
│ dispatches to object behaviours
▼
┌──────────┐
│ Commands │ (pure-ish functions: GameProtocol → list[Event])
└────┬─────┘
│ returns events
▼
┌──────────┐
│ Events │ (immutable dataclasses)
└────┬─────┘
│ consumed by
▼
┌──────────┐
│ UI │ (renders, plays sounds, shows messages)
└──────────┘
Key design decisions
Commands are first-class values
A Command is simply Callable[[GameProtocol], list[Event]]. Object
behaviours are expressed by assigning command instances at construction time
rather than overriding methods. This makes objects highly composable
without deep inheritance hierarchies.
Protocols over base classes
All contracts (GameProtocol, Interactable, Unlockable, …) are
defined as typing.Protocol classes. This enables structural (duck)
typing — any object with the right attributes and methods satisfies the
protocol, regardless of its class hierarchy.
Events decouple logic from presentation
Commands never call the UI directly. They return event dataclasses. The UI layer receives the event list and decides how to react (display a message, play a sound, show an overlay, etc.).
Game loop
The canonical game loop is:
ui.init(game)
while ui.is_running:
ui.tick() # regulate frame rate
events = ui.input() # poll and dispatch player actions
ui.handle(events) # update UI state (messages, overlays)
ui.render() # draw current frame
ui.quit()
Note that ui.input() internally calls game.interact(),
game.interact_inventory(), or game.insert_code() — so events are
produced during the input phase, not in a separate update step.
Module dependency graph
types ◄── events ◄── protocols ◄── commands
▲ │
│ ▼
mixins ◄── objects
▲
│
game
▲
│
pygame_ui
▲
│
messages
The core library (escapy) has no runtime dependency on pygame.
The escapy.pygame subpackage is the only module that imports pygame.