Source code for escapy.objects

# Copyright (C) 2026 Matteo Zeccoli Marazzini
#
# This file is part of escapy.
#
# escapy is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# escapy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
# more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with escapy. If not, see <https://www.gnu.org/licenses/>.

"""Ready-made game-object classes for common escape-room mechanics.

Each class composes protocol implementations and mixin behaviour to
provide a complete, reusable game object that can be placed in rooms,
interacted with, and managed by the inventory system.
"""

from .commands import (
    Command,
    add_to_inventory,
    ask_for_code,
    chain,
    combine,
    cond,
    inspect,
    key_lock,
    locked,
    move_to_room,
    pick,
    put_in_hand,
    simple_lock,
)
from .events import UnlockedEvent
from .mixins import DecodableMixin, UnlockableMixin
from .protocols import Decodable, Interactable, InventoryInteractable, Placeable, Unlockable


[docs] class PickableObject(Interactable, InventoryInteractable, Placeable): """An object that can be picked up from a room and held in-hand. Clicking the object in the room picks it up (removes it from the room and adds it to inventory). Clicking it in the inventory puts it in-hand. Args: id: Unique object identifier. width: Normalised width (fraction of the game area). height: Normalised height (fraction of the game area). """ def __init__( self, id: str, width: float, height: float, ): self.interact = pick(id) self.interact_inventory = put_in_hand(id) self.width = width self.height = height
[docs] class SelfSimpleLock(UnlockableMixin, Interactable, Unlockable, Placeable): """A lock that can be opened with a simple click (no key required). Args: id: Unique object identifier. on_unlock: Command to execute when the lock is opened. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, on_unlock: Command, width: float, height: float): self.interact = simple_lock(id) self.state = "locked" self.on_unlock = on_unlock self.width = width self.height = height
[docs] class SelfKeyLock(UnlockableMixin, Interactable, Unlockable, Placeable): """A lock that requires a specific key held in-hand to open. If the player interacts without the correct key, an :class:`~escapy.events.InteractedWithLockedEvent` is emitted instead. Args: id: Unique object identifier. key_id: Identifier of the key object that opens this lock. on_unlock: Command to execute when the lock is opened. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, key_id: str, on_unlock: Command, width: float, height: float): self.interact = chain( (lambda _events: True, key_lock(id, key_id=key_id)), ( lambda events: ( self.state == "locked" and not any(isinstance(e, UnlockedEvent) and e.object_id == id for e in events) ), locked(id), ), ) self.state = "locked" self.on_unlock = on_unlock self.width = width self.height = height
[docs] class SelfAskCodeLock(UnlockableMixin, DecodableMixin, Interactable, Unlockable, Decodable, Placeable): """A lock that prompts the player to enter a numeric/text code. When locked, interaction triggers an :class:`~escapy.events.AskedForCodeEvent`. If the submitted code matches, the lock opens and ``on_unlock`` fires. Args: id: Unique object identifier. on_unlock: Command to execute when decoded. code: The correct code string. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, on_unlock: Command, code: str, width: float, height: float): self.interact = cond((lambda: self.state == "locked", ask_for_code(id))) self.state = "locked" self.on_unlock = on_unlock self.code = code self.on_decode = lambda game: self.unlock()(game) self.width = width self.height = height
[docs] class MoveToRoom(Interactable, Placeable): """A clickable area that transports the player to another room. Args: room_id: Destination room identifier. width: Normalised width. height: Normalised height. """ def __init__(self, room_id: str, width: float, height: float): self.interact = move_to_room(room_id) self.width = width self.height = height
[docs] class WinMachine(DecodableMixin, InventoryInteractable, Decodable, Placeable): """A special object that ends (wins) the game when the correct code is entered. Interacting with it from the inventory triggers a code prompt. A correct code moves the player to the designated win room. Args: id: Unique object identifier. code: The winning code string. win_room_id: Room to transition to upon success. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, code: str, win_room_id: str, width: float, height: float): self.interact_inventory = ask_for_code(id) self.code = code self.on_decode = move_to_room(win_room_id) self.width = width self.height = height
[docs] class InspectableObject(Interactable, Placeable): """An object that can be inspected (zoomed-in view) when clicked. Args: id: Unique object identifier. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, width: float, height: float): self.interact = inspect(id) self.width = width self.height = height
[docs] class PickableInspectableObject(Interactable, InventoryInteractable, Placeable): """An object that can be picked up from a room and inspected from the inventory. Args: id: Unique object identifier. width: Normalised width. height: Normalised height. """ def __init__(self, id: str, width: float, height: float): self.interact = pick(id) self.interact_inventory = inspect(id) self.width = width self.height = height
[docs] class MoveToRoomAndAddToInventoryObject(Interactable, Placeable): """A clickable area that moves the player to another room and adds an object to the inventory. Args: room_id: Destination room identifier. object_id: Object to add to the inventory on interaction. width: Normalised width. height: Normalised height. """ def __init__(self, room_id: str, object_id: str, width: float, height: float): self.interact = combine(move_to_room(room_id), add_to_inventory(object_id)) self.width = width self.height = height