Skip to content

Extended Room Features Guide

Overview

The MAID Extended Room System provides dynamic, context-aware room descriptions that change based on time of day, season, weather, and other game state. This creates immersive environments where rooms feel alive and responsive to the game world.

Key features: - Time-Based Descriptions: Different descriptions for day, night, dawn, dusk - Season Variations: Descriptions that change with the seasons - Weather Effects: Overlay text during rain, snow, fog, storms - Conditional Details: Show details based on player state or game conditions - Random Details: Weighted random selection from detail pools - Dynamic Exits: Exit descriptions that change based on state - Caching: Efficient description caching for performance

Quick Start

from maid_stdlib.components.extended_room import (
    ExtendedRoomComponent,
    ExtendedDescriptions,
    TimeOfDay,
    Season,
    Weather,
    RoomDetail,
)

# Create dynamic room descriptions
descriptions = ExtendedDescriptions(
    base_description="A cozy tavern with wooden beams and a crackling fireplace.",
    time_variants={
        TimeOfDay.NIGHT: "The tavern is quiet, with only a few patrons nursing drinks.",
        TimeOfDay.NOON: "The tavern bustles with the lunch crowd.",
    },
    season_variants={
        Season.WINTER: "Frost clings to the windows.",
    },
    weather_effects={
        Weather.RAIN: "Rain drums steadily on the roof above.",
        Weather.STORM: "Thunder rattles the windowpanes.",
    },
)

# Create component
room_component = ExtendedRoomComponent(descriptions=descriptions)

# Render description with context
context = {
    "time": TimeOfDay.NIGHT,
    "season": Season.WINTER,
    "weather": Weather.RAIN,
}
description = room_component.get_description(context)
# "A cozy tavern... The tavern is quiet... Frost clings... Rain drums..."

Time of Day

TimeOfDay Enum

from maid_stdlib.components.extended_room import TimeOfDay

# Time periods and their hour ranges
TimeOfDay.MIDNIGHT  # 0:00 - 0:59
TimeOfDay.NIGHT     # 1:00 - 4:59, 22:00 - 23:59
TimeOfDay.DAWN      # 5:00 - 6:59
TimeOfDay.MORNING   # 7:00 - 11:59
TimeOfDay.NOON      # 12:00 - 12:59
TimeOfDay.AFTERNOON # 13:00 - 16:59
TimeOfDay.DUSK      # 17:00 - 18:59
TimeOfDay.EVENING   # 19:00 - 21:59

# Get time from hour
time = TimeOfDay.from_hour(14)  # AFTERNOON

# Check if dark
if TimeOfDay.NIGHT.is_dark:
    print("It's dark outside")

# Get light level (0.0 - 1.0)
light = TimeOfDay.NOON.light_level  # 1.0
light = TimeOfDay.NIGHT.light_level  # 0.1

Light Levels

Time Light Level
MIDNIGHT 0.0
NIGHT 0.1
DAWN 0.4
MORNING 0.8
NOON 1.0
AFTERNOON 0.9
DUSK 0.4
EVENING 0.3

Seasons

Season Enum

from maid_stdlib.components.extended_room import Season

# Seasons
Season.SPRING
Season.SUMMER
Season.AUTUMN
Season.WINTER

# Get season from month (Northern Hemisphere)
season = Season.from_month(7)  # SUMMER

# Temperature modifier
modifier = Season.WINTER.temperature_modifier  # 0.5

# Cycle through seasons
next_season = Season.SPRING.next()  # SUMMER

Temperature Modifiers

Season Modifier
WINTER 0.5
SPRING 0.8
SUMMER 1.2
AUTUMN 0.9

Weather

Weather Enum

from maid_stdlib.components.extended_room import Weather

# Weather types
Weather.CLEAR
Weather.CLOUDY
Weather.RAIN
Weather.STORM
Weather.SNOW
Weather.FOG
Weather.WIND

# Visibility modifier (0.0 - 1.0)
visibility = Weather.FOG.visibility_modifier  # 0.2

# Movement modifier
movement = Weather.STORM.movement_modifier  # 0.7

# Check if outdoor-only
if Weather.RAIN.outdoor_only:
    print("Only affects outdoor rooms")

# Check valid transitions
if Weather.CLOUDY.can_transition_to(Weather.RAIN):
    print("Weather can change to rain")

Weather Effects

Weather Visibility Movement
CLEAR 1.0 1.0
CLOUDY 0.9 1.0
RAIN 0.6 0.9
STORM 0.3 0.7
SNOW 0.5 0.6
FOG 0.2 0.8
WIND 0.8 0.9

Valid Weather Transitions

CLEAR -> CLOUDY, WIND, FOG
CLOUDY -> CLEAR, RAIN, SNOW, FOG
RAIN -> STORM, CLOUDY, CLEAR
STORM -> RAIN
SNOW -> CLOUDY, CLEAR, STORM
FOG -> CLEAR, CLOUDY, RAIN
WIND -> CLEAR, CLOUDY, STORM, RAIN

Extended Descriptions

Basic Configuration

from maid_stdlib.components.extended_room import (
    ExtendedDescriptions,
    TimeOfDay,
    Season,
    Weather,
)

descriptions = ExtendedDescriptions(
    base_description="The town square is paved with worn cobblestones.",

    # Time variants
    time_variants={
        TimeOfDay.NIGHT: "The square is empty under the starlight.",
        TimeOfDay.MORNING: "Merchants are setting up their stalls.",
        TimeOfDay.NOON: "The square is crowded with shoppers.",
        TimeOfDay.EVENING: "The last merchants pack up their wares.",
    },
    time_mode="append",  # or "replace"

    # Season variants
    season_variants={
        Season.SPRING: "Cherry blossoms scatter across the stones.",
        Season.SUMMER: "Heat shimmers rise from the cobblestones.",
        Season.AUTUMN: "Fallen leaves crunch underfoot.",
        Season.WINTER: "Snow blankets the square in white.",
    },
    season_mode="append",  # or "replace"

    # Weather effects
    weather_effects={
        Weather.RAIN: "Puddles form between the cobblestones.",
        Weather.SNOW: "Fresh snow accumulates on every surface.",
        Weather.FOG: "Mist obscures the far end of the square.",
    },

    # Mood and atmosphere
    mood="bustling",
    atmosphere_text="The sounds of commerce fill the air.",
)

Time and Season Modes

Append Mode (default): Variants are added to the base description.

time_mode="append"
# Result: "Base description. Time variant text."

Replace Mode: Variants replace the base description.

time_mode="replace"
# Result: "Time variant text."  (base description hidden)

Room Details

RoomDetail

Individual details that can be shown conditionally.

from maid_stdlib.components.extended_room import RoomDetail, TimeOfDay

detail = RoomDetail(
    text="A cat dozes on the windowsill.",
    weight=1.0,  # Selection weight
    conditions={
        "time": TimeOfDay.AFTERNOON,  # Only show in afternoon
    },
)

# Check if conditions pass
context = {"time": TimeOfDay.AFTERNOON}
if detail.check_conditions(context):
    print(detail.text)

Condition Types

# Equality
RoomDetail(text="...", conditions={"time": TimeOfDay.NIGHT})

# List membership
RoomDetail(text="...", conditions={
    "weather": [Weather.RAIN, Weather.STORM],
})

# Comparison tuple
RoomDetail(text="...", conditions={
    "player_level": (">=", 10),  # >, >=, <, <=, ==, !=
})

# Callable (receives context value)
RoomDetail(text="...", conditions={
    "perception": lambda x: x >= 15,
})

# Callable (receives full context if key missing)
RoomDetail(text="...", conditions={
    "custom": lambda ctx: ctx.get("has_lantern", False),
})

Conditional Details

Details shown when their conditions match:

descriptions = ExtendedDescriptions(
    base_description="A quiet library.",
    conditional_details=[
        RoomDetail(
            text="Moonlight streams through the windows.",
            conditions={"time": TimeOfDay.NIGHT},
        ),
        RoomDetail(
            text="You notice a hidden book behind the shelf.",
            conditions={"perception": (">=", 15)},
        ),
        RoomDetail(
            text="Your guild insignia glows faintly.",
            conditions={"guild": "mages"},
        ),
    ],
)

Random Details

Details selected randomly with weights:

descriptions = ExtendedDescriptions(
    base_description="A forest clearing.",
    random_details=[
        RoomDetail(text="A deer grazes nearby.", weight=2.0),
        RoomDetail(text="Birds sing in the trees.", weight=3.0),
        RoomDetail(text="A butterfly flutters past.", weight=1.0),
        RoomDetail(text="You hear wolves in the distance.", weight=0.5),
    ],
    max_random_details=2,  # Select up to 2
)

Higher weights mean more likely selection. Details can also have conditions:

random_details=[
    RoomDetail(
        text="Fireflies dance in the darkness.",
        weight=2.0,
        conditions={"time": TimeOfDay.NIGHT},
    ),
    RoomDetail(
        text="Sunbeams pierce the canopy.",
        weight=2.0,
        conditions={"time": [TimeOfDay.MORNING, TimeOfDay.AFTERNOON]},
    ),
]

Detail Collections

DetailCollection

For managing groups of details:

from maid_stdlib.components.extended_room import DetailCollection, RoomDetail

collection = DetailCollection(max_details=3)

# Add details
collection.add(RoomDetail(text="A painting hangs on the wall.", weight=1.0))
collection.add(RoomDetail(text="Candles flicker on the table.", weight=1.5))

# Remove detail
collection.remove("A painting hangs on the wall.")
# or
collection.remove(detail_object)

# Get all matching details
context = {"time": TimeOfDay.EVENING}
matching = collection.get_matching(context)

# Weighted random selection
from random import Random
rng = Random(42)
selected = collection.select_weighted(context, count=2, rng=rng)

# Convenience method (uses max_details)
selected = collection.select(context, rng=rng)

Extended Exit Descriptions

ExtendedExitDescription

Dynamic exit descriptions based on state:

from maid_stdlib.components.extended_room import (
    ExtendedExitDescription,
    ExitState,
    TimeOfDay,
)

exit_desc = ExtendedExitDescription(
    direction="north",
    base_description="A sturdy oak door leads north.",
    state_variants={
        ExitState.OPEN: "The oak door stands open.",
        ExitState.CLOSED: "A sturdy oak door blocks the way north.",
        ExitState.LOCKED: "The oak door is locked tight.",
        ExitState.HIDDEN: "There appears to be a door hidden behind the bookshelf.",
    },
    time_variants={
        TimeOfDay.NIGHT: "Moonlight seeps through cracks in the door.",
    },
    hidden_until={"perception": (">=", 12)},  # Reveal conditions
)

# Render with state and context
context = {"time": TimeOfDay.NIGHT, "perception": 15}
text = exit_desc.render(state=ExitState.CLOSED, context=context)

Exit States

from maid_stdlib.components.extended_room import ExitState

ExitState.OPEN    # Exit is open and passable
ExitState.CLOSED  # Exit is closed but visible
ExitState.LOCKED  # Exit is locked, requires key
ExitState.HIDDEN  # Exit not visible until conditions met

Hidden Exit Conditions

# Hidden until perception check passes
hidden_until={"perception": (">=", 15)}

# Hidden until player has specific item
hidden_until={"has_key": True}

# Hidden until quest complete
hidden_until={"quest_complete": "find_secret_door"}

# Multiple conditions (all must pass)
hidden_until={
    "perception": (">=", 12),
    "class": ["rogue", "ranger"],
}

ExtendedRoomComponent

Basic Usage

from maid_stdlib.components.extended_room import (
    ExtendedRoomComponent,
    ExtendedDescriptions,
    ExtendedExitDescription,
    ExitState,
)

component = ExtendedRoomComponent(
    descriptions=ExtendedDescriptions(
        base_description="A stone chamber.",
        # ...
    ),
    exit_descriptions={
        "north": ExtendedExitDescription(
            direction="north",
            base_description="A passage leads north.",
        ),
        "east": ExtendedExitDescription(
            direction="east",
            base_description="A locked iron gate blocks the east.",
            state_variants={
                ExitState.LOCKED: "A locked iron gate blocks the east.",
                ExitState.OPEN: "The iron gate stands open.",
            },
        ),
    },
)

Getting Descriptions

# Get room description
context = {
    "time": TimeOfDay.NIGHT,
    "season": Season.WINTER,
    "weather": Weather.CLEAR,
    "player_level": 15,
}
description = component.get_description(context)

# Get exit description
exit_text = component.get_exit_description(
    direction="east",
    state=ExitState.LOCKED,
    context=context,
)

# Invalidate cache when room changes
component.invalidate_cache()

Caching

The component caches descriptions for performance:

# First call generates and caches
desc1 = component.get_description(context)

# Second call returns cached result
desc2 = component.get_description(context)

# Force regeneration
desc3 = component.get_description(context, use_cache=False)

# Cache is keyed on time, season, weather
# Changing these invalidates the cache

Complete Example

from maid_stdlib.components.extended_room import (
    ExtendedRoomComponent,
    ExtendedDescriptions,
    ExtendedExitDescription,
    RoomDetail,
    TimeOfDay,
    Season,
    Weather,
    ExitState,
)

# Create a tavern with full dynamic descriptions
tavern = ExtendedRoomComponent(
    descriptions=ExtendedDescriptions(
        base_description=(
            "The Rusty Tankard is a warm and welcoming tavern. "
            "Wooden tables fill the common room, and a large fireplace "
            "dominates the north wall."
        ),

        time_variants={
            TimeOfDay.MORNING: "Early morning light streams through dusty windows. The tavern is nearly empty.",
            TimeOfDay.NOON: "The lunch crowd fills every table. Servers weave between patrons.",
            TimeOfDay.EVENING: "The tavern is alive with chatter and laughter as regulars gather.",
            TimeOfDay.NIGHT: "Only a few night owls remain, nursing their drinks in the dim light.",
            TimeOfDay.MIDNIGHT: "The tavern is silent and dark, chairs upturned on tables.",
        },
        time_mode="append",

        season_variants={
            Season.WINTER: "A roaring fire in the hearth fights back the winter chill.",
            Season.SUMMER: "Windows stand open to catch any hint of breeze.",
        },

        weather_effects={
            Weather.RAIN: "Rain patters against the windows, making the inside feel even cozier.",
            Weather.STORM: "Thunder rumbles outside, and patrons glance nervously at the door.",
        },

        conditional_details=[
            RoomDetail(
                text="The bartender gives you a knowing nod.",
                conditions={"reputation_tavern": (">=", 50)},
            ),
            RoomDetail(
                text="You notice a suspicious figure in the corner booth.",
                conditions={"perception": (">=", 12)},
            ),
        ],

        random_details=[
            RoomDetail(text="A bard tunes a lute by the fireplace.", weight=2.0),
            RoomDetail(text="Two dwarves argue loudly about mining rights.", weight=1.5),
            RoomDetail(text="A serving girl drops a tray of mugs with a crash.", weight=0.5),
            RoomDetail(text="The smell of fresh bread wafts from the kitchen.", weight=3.0),
        ],
        max_random_details=2,

        mood="cozy",
        atmosphere_text="The warmth of good company fills the air.",
    ),

    exit_descriptions={
        "north": ExtendedExitDescription(
            direction="north",
            base_description="A sturdy door leads to the street.",
            time_variants={
                TimeOfDay.NIGHT: "The door is barred for the night.",
            },
        ),
        "up": ExtendedExitDescription(
            direction="up",
            base_description="Stairs lead up to the guest rooms.",
        ),
        "down": ExtendedExitDescription(
            direction="down",
            base_description="A trapdoor leads to the cellar.",
            state_variants={
                ExitState.HIDDEN: "",  # Invisible until found
                ExitState.LOCKED: "The cellar door is padlocked.",
            },
            hidden_until={"perception": (">=", 15)},
        ),
    },
)

# Render for a specific context
context = {
    "time": TimeOfDay.EVENING,
    "season": Season.WINTER,
    "weather": Weather.RAIN,
    "perception": 16,
    "reputation_tavern": 60,
}

description = tavern.get_description(context)
print(description)

Builder Commands

MAID provides in-game commands for extended room management (BUILDER access level):

@room time <target> <period> <description>     - Set time variant
@room season <target> <season> <description>   - Set season variant
@room weather <target> <weather> <description> - Set weather effect
@room detail <target> add <text> [weight]      - Add random detail
@room detail <target> remove <text>            - Remove detail
@room detail <target> list                     - List all details
@room exit <target> <direction> <description>  - Set exit description
@room mood <target> <mood>                     - Set room mood
@room atmosphere <target> <text>               - Set atmosphere text

Integration with Game Systems

Time System Integration

# In your game tick or time system
def update_world_time():
    hour = get_game_hour()
    time_of_day = TimeOfDay.from_hour(hour)

    # Update context for room rendering
    context["time"] = time_of_day

    # Invalidate caches if time period changed
    if time_of_day != previous_time:
        for room in rooms_with_extended_descriptions:
            room.invalidate_cache()

Weather System Integration

# In your weather system
def change_weather(new_weather: Weather, region):
    old_weather = region.current_weather

    if old_weather.can_transition_to(new_weather):
        region.current_weather = new_weather

        # Update context
        for room in region.outdoor_rooms:
            context = {"weather": new_weather}
            room.invalidate_cache()

            # Apply weather effects
            if new_weather.outdoor_only and not room.is_outdoor:
                continue  # Skip indoor rooms

            # Modify visibility/movement
            visibility = new_weather.visibility_modifier
            movement = new_weather.movement_modifier

Season System Integration

# In your calendar system
def advance_month():
    month = (current_month % 12) + 1
    current_month = month

    new_season = Season.from_month(month)
    if new_season != current_season:
        current_season = new_season
        # Update all room caches
        for room in world.rooms:
            room.invalidate_cache()