"""
States represent the sequence of states the room goes through.
This module includes the BaseState class and the StateHandler
for managing states on the room.
The state handler operates on an Evscaperoom and changes
its state from one to another.
A given state is given as a module in states/ package. The
state is identified by its module name.
"""
from functools import wraps
from django.conf import settings
from evennia import logger, utils
from .objects import EvscaperoomObject
from .utils import create_evscaperoom_object, msg_cinematic, parse_for_things
# state setup
if hasattr(settings, "EVSCAPEROOM_STATE_PACKAGE"):
_ROOMSTATE_PACKAGE = settings.EVSCAPEROOM_STATE_PACKAGE
else:
_ROOMSTATE_PACKAGE = "evennia.contrib.full_systems.evscaperoom.states"
if hasattr(settings, "EVSCAPEROOM_START_STATE"):
_FIRST_STATE = settings.EVSCAPEROOM_START_STATE
else:
_FIRST_STATE = "state_001_start"
_GA = object.__getattribute__
# handler for managing states on room
[docs]class StateHandler(object):
"""
This sits on the room and is used to progress through the states.
"""
[docs] def __init__(self, room):
self.room = room
self.current_state_name = room.db.state or _FIRST_STATE
self.prev_state_name = room.db.prev_state
self.current_state = None
self.current_state = self.load_state(self.current_state_name)
[docs] def load_state(self, statename):
"""
Load state without initializing it
"""
try:
mod = utils.mod_import(f"{_ROOMSTATE_PACKAGE}.{statename}")
except Exception as err:
logger.log_trace()
self.room.msg_room(None, f"|rBUG: Could not load state {statename}: {err}!")
self.room.msg_room(None, f"|rBUG: Falling back to {self.current_state_name}")
return
state = mod.State(self, self.room)
return state
[docs] def init_state(self):
"""
Initialize a new state
"""
self.current_state.init()
[docs] def next_state(self, next_state=None):
"""
Check if the current state is finished. This should be called whenever
the players do actions that may affect the state of the room.
Args:
next_state (str, optional): If given, override the next_state given
by the current state's check() method with this - this allows
for branching paths (but the current state must still first agree
that the check passes).
Returns:
state_changed (bool): True if the state changed, False otherwise.
"""
# allows the state to enforce/customize what the next state should be
next_state_name = self.current_state.next(next_state)
if next_state_name:
# we are ready to move on!
next_state = self.load_state(next_state_name)
if not next_state:
raise RuntimeError(f"Could not load new state {next_state_name}!")
self.prev_state_name = self.current_state_name
self.current_state_name = next_state_name
self.current_state.clean()
self.prev_state = self.current_state
self.current_state = next_state
self.init_state()
self.room.db.prev_state = self.prev_state_name
self.room.db.state = self.current_state_name
return True
return False
# base state class
[docs]class BaseState(object):
"""
Base object holding all callables for a state. This is here to
allow easy overriding for child states.
"""
next_state = "unset"
# a sequence of hints to describe this state.
hints = []
[docs] def __init__(self, handler, room):
"""
Initializer.
Args:
room (EvscapeRoom): The room tied to this state.
handler (StateHandler): Back-reference to the handler
storing this state.
"""
self.handler = handler
self.room = room
# the name is derived from the name of the module
self.name = self.__class__.__module__
def __str__(self):
return self.__class__.__module__
def __repr__(self):
return str(self)
def _catch_errors(self, method):
"""
Wrapper handling state method errors.
"""
@wraps(method)
def decorator(*args, **kwargs):
try:
return method(*args, **kwargs)
except Exception:
logger.log_trace(f"Error in State {__name__}")
self.room.msg_room(
None,
f"|rThere was an unexpected error in State {__name__}. "
"Please |wreport|r this as an issue.|n",
)
raise # TODO
return decorator
def __getattribute__(self, key):
"""
Always wrap all callables in the error-handler
"""
val = _GA(self, key)
if callable(val):
return _GA(self, "_catch_errors")(val)
return val
[docs] def get_hint(self):
"""
Get a hint for how to solve this state.
"""
hint_level = self.room.attributes.get("state_hint_level", default=-1)
next_level = hint_level + 1
if next_level < len(self.hints):
# return the next hint in the sequence.
self.room.db.state_hint_level = next_level
self.room.db.stats["hints_used"] += 1
self.room.log(
f"HINT: {self.name.split('.')[-1]}, level {next_level + 1} "
f"(total used: {self.room.db.stats['hints_used']})"
)
return self.hints[next_level]
else:
# no more hints for this state
return None
# helpers
[docs] def msg(self, message, target=None, borders=False, cinematic=False):
"""
Display messsage to everyone in room, or given target.
"""
if cinematic:
message = msg_cinematic(message, borders=borders)
if target:
options = target.attributes.get("options", category=self.room.tagcategory, default={})
style = options.get("things_style", 2)
# we assume this is a char
target.msg(parse_for_things(message, things_style=style))
else:
self.room.msg_room(None, message)
[docs] def cinematic(self, message, target=None):
"""
Display a 'cinematic' sequence - centered, with borders.
"""
self.msg(message, target=target, borders=True, cinematic=True)
[docs] def create_object(self, typeclass=None, key="testobj", location=None, **kwargs):
"""
This is a convenience-wrapper for quickly building EvscapeRoom objects.
Keyword Args:
typeclass (str): This can take just the class-name in the evscaperoom's
objects.py module. Otherwise, a full path or the actual class
is needed (for custom state objects, just give the class directly).
key (str): Name of object.
location (Object): If not given, this will be the current room.
kwargs (any): Will be passed into create_object.
Returns:
new_obj (Object): The newly created object, if any.
"""
if not location:
location = self.room
return create_evscaperoom_object(
typeclass=typeclass,
key=key,
location=location,
tags=[("room", self.room.tagcategory.lower())],
**kwargs,
)
[docs] def get_object(self, key):
"""
Find a named *non-character* object for this state in this room.
Args:
key (str): Object to search for.
Returns:
obj (Object): Object in the room.
"""
match = EvscaperoomObject.objects.filter_family(
db_key__iexact=key, db_tags__db_category=self.room.tagcategory.lower()
)
if not match:
logger.log_err(f"get_object: No match for '{key}' in state ")
return None
return match[0]
# state methods
[docs] def init(self):
"""
Initializes the state (usually by modifying the room in some way)
"""
pass
[docs] def clean(self):
"""
Any cleanup operations after the state ends.
"""
self.room.db.state_hint_level = -1
[docs] def next(self, next_state=None):
"""
Get the next state after this one.
Args:
next_state (str, optional): This allows the calling code
to redirect to a different state than the 'default' one
(creating branching paths in the game). Override this method
to customize (by default the input will always override default
set on the class)
Returns:
state_name (str or None): Name of next state to switch to. None
to remain in this state. By default we check the room for the
"finished" flag be set.
"""
return next_state or self.next_state
[docs] def character_enters(self, character):
"""
Called when character enters the room in this state.
"""
pass
[docs] def character_leaves(self, character):
"""
Called when character is whisked away (usually because of
quitting). This method cannot influence the move itself; it
happens just before room.character_cleanup()
"""
pass