Hidden Room¶
Problem¶
You want a secret exit that's invisible until the player uses a search command to discover it.
Solution¶
Setting Up the Hidden Exit¶
MAID's ExitInfo has a built-in hidden flag. Hidden exits don't appear in room descriptions or standard movement until revealed.
Builder commands:
@dig east = Secret Treasury
@set here/exits/east/hidden = true
@set here/exits/east/door = true
@set here/exits/east/door_name = concealed passage
Programmatic setup:
from uuid import UUID
from maid_engine.core.world import World
from maid_stdlib.components import (
DescriptionComponent,
ExitMetadataComponent,
ExitInfo,
)
def create_hidden_passage(
world: World, room_id: UUID, secret_room_id: UUID
) -> None:
"""Add a hidden exit to an existing room."""
room = world.get_entity(room_id)
if not room:
return
# Wire the exit route in room data so movement works once revealed
room_data = world.get_room(room_id)
if room_data is not None:
if isinstance(room_data, dict):
room_data.setdefault("exits", {})["east"] = secret_room_id
elif hasattr(room_data, "exits"):
room_data.exits["east"] = secret_room_id
# Mark the exit as hidden so it's invisible until discovered
exit_meta = room.try_get(ExitMetadataComponent)
if exit_meta:
exit_meta.exits["east"] = ExitInfo(
hidden=True,
door=True,
door_name="concealed passage",
)
exit_meta.notify_mutation()
else:
room.add(ExitMetadataComponent(
exits={
"east": ExitInfo(
hidden=True,
door=True,
door_name="concealed passage",
),
},
))
The Search Command¶
import random
from uuid import UUID
from maid_engine.commands.decorators import command
from maid_engine.commands import CommandContext
from maid_stdlib.components import ExitMetadataComponent
@command(
name="search",
aliases=["examine walls", "look around"],
category="exploration",
help_text="Search the room for hidden passages",
)
async def cmd_search(ctx: CommandContext) -> bool:
"""Search the current room for hidden exits."""
room_id = ctx.world.get_entity_room(ctx.player_id)
if not room_id:
return False
room = ctx.world.get_entity(room_id)
if not room:
return False
exit_meta = room.try_get(ExitMetadataComponent)
if not exit_meta:
await ctx.session.send("You search but find nothing unusual.\n")
return True
found_something = False
for direction, exit_info in exit_meta.exits.items():
if not exit_info.hidden:
continue
# Skill check — adjust success chance as desired
search_chance = 0.6 # 60% base chance
if random.random() < search_chance:
exit_info.hidden = False
exit_meta.notify_mutation()
name = exit_info.door_name or "passage"
await ctx.session.send(
f"You discover a {name} to the {direction}!\n"
)
found_something = True
if not found_something:
await ctx.session.send(
"You search carefully but find nothing unusual.\n"
)
return True
Skill-Based Discovery¶
Integrate with a skill system for more depth:
from maid_classic_rpg.components import CharacterInfoComponent
@command(
name="search",
category="exploration",
help_text="Search for hidden passages (Perception check)",
)
async def cmd_search_skilled(ctx: CommandContext) -> bool:
"""Search with skill-based success chance."""
room_id = ctx.world.get_entity_room(ctx.player_id)
if not room_id:
return False
room = ctx.world.get_entity(room_id)
if not room:
return False
exit_meta = room.try_get(ExitMetadataComponent)
if not exit_meta:
await ctx.session.send("You search but find nothing unusual.\n")
return True
# Get player's perception/search skill
player = ctx.world.get_entity(ctx.player_id)
if not player:
return False
info = player.try_get(CharacterInfoComponent)
player_level = info.level if info else 1
found_something = False
for direction, exit_info in exit_meta.exits.items():
if not exit_info.hidden:
continue
# Higher level = better search chance
difficulty = 10 # Base DC
roll = random.randint(1, 20) + player_level
if roll >= difficulty:
exit_info.hidden = False
exit_meta.notify_mutation()
name = exit_info.door_name or "passage"
await ctx.session.send(
f"[Perception {roll} vs DC {difficulty}] "
f"You discover a {name} to the {direction}!\n"
)
found_something = True
else:
await ctx.session.send(
f"[Perception {roll} vs DC {difficulty}] "
f"You feel like something is off, but can't quite tell...\n"
)
if not found_something:
await ctx.session.send("You search carefully but find nothing.\n")
return True
Re-hiding on Reset¶
If you want the passage to re-hide itself (e.g., on area reset):
from maid_engine.core.ecs import System
from maid_engine.core.world import World
from maid_stdlib.components import ExitMetadataComponent
class HiddenExitResetSystem(System):
"""Periodically re-hides discovered hidden exits."""
priority = 250
def __init__(self, world: World, reset_interval: float = 3600.0) -> None:
super().__init__(world)
self.reset_interval = reset_interval
self._timer: float = reset_interval
self._originally_hidden: dict[str, list[str]] = {} # entity_id -> directions
async def update(self, delta: float) -> None:
self._timer -= delta
if self._timer > 0:
return
self._timer = self.reset_interval
for entity in self.entities.with_components(ExitMetadataComponent):
exit_meta = entity.get(ExitMetadataComponent)
key = str(entity.id)
if key in self._originally_hidden:
for direction in self._originally_hidden[key]:
exit_info = exit_meta.exits.get(direction)
if exit_info and not exit_info.hidden:
exit_info.hidden = True
exit_meta.notify_mutation()
How It Works¶
ExitInfo.hidden = Trueremoves the exit from room descriptions and blocks standard movement- The
searchcommand iterates over exits, checks forhidden, and reveals with a random/skill check notify_mutation()marks the entity dirty so persistence picks up the state change- Setting
hidden = Falseimmediately allows normal movement through that exit
Variations¶
- Detect magic: A separate
detectcommand that only finds magically hidden passages - Item-triggered: Require a specific item (torch, magnifying glass) to search effectively
- One-player reveal: Track per-player discovery so each player must find the exit themselves
- Trap: The search reveals a trap instead — deal damage before showing the exit
- Progressive hints: First search says "You notice scratch marks on the wall", second reveals the passage
See Also¶
- Locked Door — Exits gated by items
- Level Gating — Exits gated by level
- Building Commands —
@set,@dig