tale/base.py
class ParseResult:
    class WhoInfo:
    class WhoInfoOrderedDict(OrderedDict):
class MudObjRegistry:
class MudObject:
class Item(MudObject):
class Weapon(Item):
class Armour(Item):
class Location(MudObject):
class Stats:
class Living(MudObject):
class Container(Item):
class Exit(MudObject):
class Door(Exit):
class Key(Item):
class Soul:
        def __init__(self, seqnr: int = 0) -> None:
        def __str__(self) -> str:
        def __missing__(self, key):
    def __init__(self, verb: str, *, adverb: str = "", message: str = "", bodypart: str = "", qualifier: str = "",
                 args: List[str] = None, who_info: WhoInfoOrderedDict = None,
                 unrecognized: List[str] = None, unparsed: str = "", who_list: List = None) -> None:
    def who_1(self) -> ParsedWhoType:
    def who_12(self) -> Tuple[ParsedWhoType, ParsedWhoType]:
    def who_123(self) -> Tuple[ParsedWhoType, ParsedWhoType, ParsedWhoType]:
    def who_last(self) -> Optional[ParsedWhoType]:
    def __str__(self) -> str:
    def track_vnum(instance: Any, fix_clones: bool=False):
    def create_object(cls, objclass: Type, *vargs, **kwargs) -> Any:
    def __new__(cls, *args, **kwargs):
    def __init__(self, name: str, title: str = "", *, descr: str = "", short_descr: str = "") -> None:
    def init(self) -> None:
    def title(self) -> str:
    def title(self, value: str) -> None:
    def description(self) -> str:
    def description(self, value: str) -> None:
    def short_description(self) -> str:
    def short_description(self, value: str) -> None:
    def extra_desc(self) -> Dict[str, str]:
    def extra_desc(self, value: Dict[str, str]) -> None:
    def init_names(self, name: str, title: str, descr: str, short_descr: str) -> None:
    def _check_title(self, title: str) -> None:
    def add_extradesc(self, keywords: Set[str], description: str) -> None:
    def __repr__(self):
    def destroy(self, ctx: Optional[util.Context]) -> None:
    def wiz_clone(self, actor: 'Living') -> 'MudObject':
    def wiz_destroy(self, actor: 'Living', ctx: util.Context) -> None:
    def show_inventory(self, actor: 'Living', ctx: util.Context) -> None:
    def activate(self, actor: 'Living') -> None:
    def deactivate(self, actor: 'Living') -> None:
    def manipulate(self, verb: str, actor: 'Living') -> None:
    def move(self, target: ContainingType, actor: 'Living'=None,
             *, silent: bool=False, is_player: bool=False, verb: str="move", direction_names: Sequence[str]=None) -> None:
    def read(self, actor: 'Living') -> None:
    def insert(self, obj: Union['Living', 'Item'], actor: Optional['Living']) -> None:
    def remove(self, obj: Union['Living', 'Item'], actor: Optional['Living']) -> None:
    def handle_verb(self, parsed: ParseResult, actor: 'Living') -> bool:
    def notify_action(self, parsed: ParseResult, actor: 'Living') -> None:
    def __init__(self, name: str, title: str = "", *, descr: str = "", short_descr: str = "") -> None:
    def init(self) -> None:
    def __contains__(self, item: 'Item') -> bool:
    def location(self) -> Optional['Location']:
    def location(self, value: 'Location') -> None:
    def inventory(self) -> FrozenSet['Item']:
    def inventory_size(self) -> int:
    def move(self, target: ContainingType, actor: 'Living'=None,
             *, silent: bool=False, is_player: bool=False, verb: str="move", direction_names: Sequence[str]=None) -> None:
    def notify_moved(self, source_container: ContainingType, target_container: ContainingType, actor: Optional['Living']) -> None:
    def allow_item_move(self, actor: Optional['Living'], verb: str="move") -> None:
    def open(self, actor: 'Living', item: 'Item'=None) -> None:
    def close(self, actor: 'Living', item: 'Item'=None) -> None:
    def lock(self, actor: 'Living', item: 'Item'=None) -> None:
    def unlock(self, actor: 'Living', item: 'Item'=None) -> None:
    def combine(self, other: List['Item'], actor: 'Living') -> Optional['Item']:
    def wiz_clone(self, actor: 'Living', make_clone: bool=True) -> 'Item':
    def wiz_destroy(self, actor: 'Living', ctx: util.Context) -> None:
    def show_inventory(self, actor: 'Living', ctx: util.Context) -> None:
    def search_item(name: str, collection: Iterable['Item']) -> Optional['Item']:
    def clone(self) -> 'Item':
    def __init__(self, name: str, descr: str="") -> None:
    def __contains__(self, obj: Union['Living', Item]) -> bool:
    def init_inventory(self, objects: Iterable[Union[Item, 'Living']]) -> None:
    def destroy(self, ctx: Optional[util.Context]) -> None:
    def add_exits(self, exits: Iterable['Exit']) -> None:
    def get_wiretap(self) -> pubsub.Topic:
    def tell(self, room_msg: str, exclude_living: 'Living'=None, specific_targets: Set[Union[ParsedWhoType]]=None,
             specific_target_msg: str="") -> None:
    def message_nearby_locations(self, message: str) -> None:
    def nearby(self, no_traps: bool=True) -> Iterable['Location']:
    def look(self, exclude_living: 'Living'=None, short: bool=False) -> Sequence[str]:
    def search_living(self, name: str) -> Optional['Living']:
    def insert(self, obj: Union['Living', Item], actor: Optional['Living']) -> None:
    def remove(self, obj: Union['Living', Item], actor: Optional['Living']) -> None:
    def handle_verb(self, parsed: ParseResult, actor: 'Living') -> bool:
    def _notify_action_all(self, parsed: ParseResult, actor: 'Living') -> None:
    def notify_action(self, parsed: ParseResult, actor: 'Living') -> None:
    def notify_npc_arrived(self, npc: 'Living', previous_location: 'Location') -> None:
    def notify_npc_left(self, npc: 'Living', target_location: 'Location') -> None:
    def notify_player_arrived(self, player, previous_location: 'Location') -> None:
    def notify_player_left(self, player, target_location: 'Location') -> None:
    def __init__(self) -> None:
    def __repr__(self):
    def from_race(cls: type, race: builtins.str, gender: builtins.str='n') -> 'Stats':
    def set_stats_from_race(self) -> None:
    def __init__(self, name: str, gender: str, *, race: str="human",
                 title: str="", descr: str="", short_descr: str="") -> None:
    def init_gender(self, gender: str) -> None:
    def init_inventory(self, items: Iterable[Item]) -> None:
    def __contains__(self, item: Union['Living', Item, Location]) -> bool:
    def inventory_size(self) -> int:
    def inventory(self) -> FrozenSet[Item]:
    def insert(self, item: Union['Living', Item], actor: Optional['Living']) -> None:
    def remove(self, item: Union['Living', Item], actor: Optional['Living']) -> None:
    def destroy(self, ctx: Optional[util.Context]) -> None:
    def wiz_clone(self, actor: 'Living', make_clone: bool=True) -> 'Living':
    def wiz_destroy(self, actor: 'Living', ctx: util.Context) -> None:
    def show_inventory(self, actor: 'Living', ctx: util.Context) -> None:
    def get_wiretap(self) -> pubsub.Topic:
    def tell(self, message: str, *, end: bool=False, format: bool=True) -> 'Living':
    def tell_later(self, message: str) -> None:
    def tell_others(self, message: str, target: Optional['Living']=None) -> None:
    def parse(self, commandline: str, external_verbs: Set[str]=set()) -> ParseResult:
    def validate_socialize_targets(self, parsed: ParseResult) -> None:
    def remember_previous_parse(self) -> None:
    def do_socialize(self, cmdline: str, external_verbs: Set[str]=set()) -> None:
    def do_command_verb(self, cmdline: str, ctx: util.Context) -> None:
    def do_socialize_cmd(self, parsed: ParseResult) -> None:
    def do_forced_cmd(self, actor: 'Living', parsed: ParseResult, ctx: util.Context) -> None:
    def move(self, target: Union[Location, 'Container', 'Living'], actor: 'Living'=None,
             *, silent: bool=False, is_player: bool=False, verb: str="move", direction_names: Sequence[str]=None) -> None:
        def display_direction(directions: Sequence[str]) -> str:
    def search_item(self, name: str, include_inventory: bool=True,
                    include_location: bool=True, include_containers_in_inventory: bool=True) -> Optional[Item]:
    def locate_item(self, name: str, include_inventory: bool=True, include_location: bool=True,
                    include_containers_in_inventory: bool=True) -> Tuple[Optional[Item], Optional[ContainingType]]:
    def start_attack(self, victim: 'Living') -> None:
    def allow_give_money(self, amount: float, actor: Optional['Living']) -> None:
    def allow_give_item(self, item: Item, actor: Optional['Living']) -> None:
    def _handle_verb_base(self, parsed: ParseResult, actor: 'Living') -> bool:
    def handle_verb(self, parsed: ParseResult, actor: 'Living') -> bool:
    def _notify_action_all(self, parsed: ParseResult, actor: 'Living') -> None:
    def notify_action(self, parsed: ParseResult, actor: 'Living') -> None:
    def look(self, short: Optional[bool]=None) -> None:
    def select_random_move(self) -> Optional['Exit']:
    def _move_follow(self, target_location: Location, ctx: util.Context) -> None:
    def init(self) -> None:
    def init_inventory(self, items: Iterable[Item]) -> None:
    def inventory(self) -> FrozenSet[Item]:
    def inventory_size(self) -> int:
    def __contains__(self, item: Item) -> bool:
    def destroy(self, ctx: Optional[util.Context]) -> None:
    def insert(self, item: Union[Living, Item], actor: Optional[Living]) -> None:
    def remove(self, item: Union[Living, Item], actor: Optional[Living]) -> None:
    def __init__(self, directions: Union[str, Sequence[str]], target_location: Union[str, Location],
                 short_descr: str, long_descr: str="", *, enter_msg: str="") -> None:
    def __repr__(self):
    def names(self):
    def connect(cls, from_loc: Location, directions: Union[str, Sequence[str]], short_descr: str, long_descr: str,
                to_loc: Location, return_directions: Union[str, Sequence[str]],
                return_short_descr: str, return_long_descr: str) -> Tuple['Exit', 'Exit']:
    def bind(self, location: Location) -> None:
    def _bind_target(self, game_zones_module: ModuleType) -> None:
    def allow_passage(self, actor: Living) -> None:
    def open(self, actor: Living, item: Item=None) -> None:
    def close(self, actor: Living, item: Item=None) -> None:
    def lock(self, actor: Living, item: Item=None) -> None:
    def unlock(self, actor: Living, item: Item=None) -> None:
    def manipulate(self, verb: str, actor: Living) -> None:
    def __init__(self, directions: Union[str, Sequence[str]], target_location: Union[str, Location],
                 short_descr: str, long_descr: str="", *, enter_msg: str="",
                 locked: bool=False, opened: bool=False, key_code: str="") -> None:
    def connect(cls, from_loc: Location, directions: Union[str, Sequence[str]], short_descr: str, long_descr: str,
                to_loc: Location, return_directions: Union[str, Sequence[str]], return_short_descr: str, return_long_descr: str,
                locked: bool=False, opened: bool=False, key_code: str="") -> Tuple['Door', 'Door']:
    def reverse_door(self, directions: Union[str, Sequence[str]], returning_location: Location,
                     short_description: str, long_description: str="") -> 'Door':
    def description(self) -> str:
    def description(self, value: str) -> None:
    def __repr__(self):
    def allow_passage(self, actor: Living) -> None:
    def open(self, actor: Living, item: Item=None) -> None:
    def close(self, actor: Living, item: Item=None) -> None:
    def lock(self, actor: Living, item: Item=None) -> None:
    def unlock(self, actor: Living, item: Item=None) -> None:
    def check_key(self, item: Item) -> bool:
    def search_key(self, actor: Living) -> Optional[Item]:
    def insert(self, item: Union[Living, Item], actor: Optional[Living]) -> None:
    def init(self) -> None:
    def key_for(self, door: Door=None, code: str="") -> None:
    def __init__(self) -> None:
    def is_verb(self, verb: str) -> bool:
    def process_verb(self, player: Living, commandstring: str, external_verbs: Set[str]=set()) \
    def process_verb_parsed(self, player: Living, parsed: ParseResult) -> Tuple[Set[ParsedWhoType], str, str, str]:
        def result_messages(action: str, action_room: str) -> Tuple[Set[ParsedWhoType], str, str, str]:
    def parse(self, player: Living, cmd: str, external_verbs: Set[str]=set()) -> ParseResult:
    def remember_previous_parse(self, parsed: ParseResult) -> None:
    def match_previously_parsed(self, player: Living, pronoun: str) -> List[Tuple[Any, str]]:
    def poss_replacement(actor: Living, target: Union[MudObject, None], observer: Optional[Living]) -> str:
    def spacify(self, string: str) -> str:
    def who_replacement(self, actor: Living, target: MudObject, observer: Optional[Living]) -> str:
    def check_person(self, action: str, parsed: ParseResult) -> bool:
    def check_name_with_spaces(self, words: Sequence[str], startindex: int, all_livings: Dict[str, Living],
                               all_items: Dict[str, Item], all_exits: Dict[str, Exit]) \
tale/story.py
class StoryConfig:
    """
    Story configuration settings.
    The reason this is in a separate class, is that these settings are all simple values
    and are serializable, so they can be saved to disk as part of a save game file.
    """
    def __init__(self) -> None:
        self.name = ""                       # the name of the story
        self.author = ""                     # the story's author name
        self.author_address = ""             # the author's email address
        self.version = "1.10"                # arbitrary but is used to check savegames for compatibility
        self.requires_tale = "4.0"           # tale library required to run the game
        self.supported_modes = {GameMode.IF}    # what driver modes (if/mud) are supported by this story
        self.player_name = ""                # set a name to create a prebuilt player, None to use the character builder
        self.player_gender = ""              # m/f (n is technically possible, but not preferred)
        self.player_race = "human"           # default is "human" ofcourse, but you can select something else if you want
        self.player_money = 0.0              # starting money
        self.playable_races = set()          # type: Set[str]  # if specified, specify a subset of the allowed playable races
        self.money_type = MoneyType.NOTHING  # money type modern/fantasy/nothing
        self.server_tick_method = TickMethod.COMMAND   # command (waits for player entry) or timer (async timer driven)
        self.server_tick_time = 5.0          # time between server ticks (in seconds) (usually 1.0 for 'timer' tick method)
        self.gametime_to_realtime = 1        # meaning: game time is X times real time (only used with "timer" tick method) (>=0)
        self.max_wait_hours = 2              # the max. number of hours (gametime) the player is allowed to wait (>=0)
        self.display_gametime = False        # enable/disable display of the game time at certain moments
        self.display_race = False            # display race and bodytype when examining a creature?
        self.epoch = None                    # type: Optional[datetime.datetime]  # start date/time of the game clock
        self.startlocation_player = ""       # name of the location where a player starts the game in
        self.startlocation_wizard = ""       # name of the location where a wizard player starts the game in
        self.savegames_enabled = True        # allow savegames?
        self.show_exits_in_look = True       # with the look command, also show exit descriptions automatically?
        self.license_file = ""               # game license file, if applicable
        self.mud_host = ""                   # for mud mode: hostname to bind the server on. Use "[...]" for IPV6 connectivity.
        self.mud_port = 0                    # for mud mode: port number to bind the server on
        self.zones = []                      # type: List[str]  # names of zone modules to load, in this order
        self.server_mode = GameMode.IF       # the actual game mode the server is operating in (will be set at startup time)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, StoryConfig) and vars(self) == vars(other)
tale/driver.py
class Deferred:
    """
    Represents a callable action that will be invoked (with the given arguments) sometime in the future.
    This object captures the action that must be invoked in a way that is serializable.
    That means that you can't pass all types of callables, there are a few that are not
    serializable (lambda's and scoped functions). They will trigger an error if you use those.
    If you set a (low_seconds, high_seconds) periodical tuple, the deferred will be called periodically
    where the next trigger time is randomized within the given interval.
    The due time is given in Game Time, not in real/wall time!
    Note that the vargs/kwargs should be serializable or savegames are impossible!
    """
    def __init__(self, due_gametime: datetime.datetime, action: Callable, vargs: Sequence[Any], kwargs: Dict[str, Any],
                 *, periodical: Tuple[float, float]=None) -> None:
        assert isinstance(due_gametime, datetime.datetime)
        assert callable(action)
        assert kwargs is None or "ctx" not in kwargs, "ctx will be provided by the driver when calling this"
        if periodical:
            if not len(periodical) == 2:
                raise ValueError("periodical arg must be None or a tuple(float,float)")
            if periodical[0] < 0.1 or periodical[1] < 0.1:
                raise ValueError("periodial interval values must be > 0.1")
        self.due_gametime = due_gametime   # in game time
        self.owner = getattr(action, "__self__", None)
        if isinstance(self.owner, ModuleType):
            # encode a module simply by its name
            self.owner = "module:" + self.owner.__name__
        if self.owner is None:
            action_module = getattr(action, "__module__", None)
            if action_module:
                if hasattr(sys.modules[action_module], action.__name__):
                    self.owner = "module:" + action_module
                else:
                    # a callable was passed that we cannot serialize.
                    raise ValueError("cannot use scoped functions or lambdas as deferred: " + str(action))
            else:
                raise ValueError("cannot determine action's owner object: " + str(action))
        self.action = action.__name__    # store name instead of object, to make this serializable
        self.vargs = vargs
        self.kwargs = kwargs
        self.periodical = periodical

    def __eq__(self, other):
        if self.__class__ == other.__class__:
            return self.due_gametime == other.due_gametime and self.owner.__class__ == other.owner.__class__ \
                and self.action == other.action and self.vargs == other.vargs and self.kwargs == other.kwargs
        return NotImplemented

    def __lt__(self, other):
        if self.__class__ == other.__class__:
            return self.due_gametime < other.due_gametime   # deferreds must be sortable
        return NotImplemented

    def when_due(self, game_clock: util.GameDateTime, realtime: bool=False) -> datetime.timedelta:
        """
        In what time is this deferred due to occur? (timedelta)
        Normally it is in terms of game-time, but if you pass realtime=True,
        you will get the real-time timedelta.
        """
        secs = (self.due_gametime - game_clock.clock).total_seconds()
        if realtime:
            secs = int(secs / game_clock.times_realtime)
        return datetime.timedelta(seconds=secs)

    def __call__(self, *args: Any, **kwargs: Any) -> None:
        self.kwargs = self.kwargs or {}
        if callable(self.action):
            func = self.action
        else:
            # deferred action is stored as the name of the function to call,
            # so we need to obtain the actual function from the owner object.
            if isinstance(self.owner, str):
                if self.owner.startswith("module:"):
                    # the owner refers to a module
                    self.owner = sys.modules[self.owner[7:]]
                else:
                    raise RuntimeError("invalid owner specifier: " + self.owner)
            func = getattr(self.owner, self.action)
        if self.periodical and hasattr(func, "_tale_periodically") and not func._tale_periodically:     # type: ignore
            return  # no longer marked as periodical
        if "ctx" in inspect.signature(func).parameters:
            self.kwargs["ctx"] = kwargs["ctx"]  # add a 'ctx' keyword argument to the call for convenience
        func(*self.vargs, **self.kwargs)
        if self.periodical and (not hasattr(func, "_tale_periodically") or func._tale_periodically):    # type: ignore
            # reschedule the same call!
            assert self.periodical[0] > 0 and self.periodical[1] > 0
            due = random.uniform(self.periodical[0], self.periodical[1])
            self.due_gametime = mud_context.driver.game_clock.plus_realtime(datetime.timedelta(seconds=due))
            if "ctx" in self.kwargs:
                del self.kwargs["ctx"]    # will be passed in again next call by driver, and required to remove because not serializable
            mud_context.driver._enqueue_deferred(self)  # reschedule!
            # note: when owner is deleted/destroyed, it must make sure that any deferreds from it are removed from the queue!
        else:
            # our lifetime has ended, remove references asap:
            del self.owner
            del self.action
            del self.kwargs
            del self.vargs
tale/player.py
class Player(base.Living, pubsub.Listener):
    """
    Player controlled entity.
    Has a Soul for social interaction.
    """
    def __init__(self, name: str, gender: str, *, race: str="human", descr: str="", short_descr: str="") -> None:
        title = lang.capital(name)
        super().__init__(name, gender, race=race, title=title, descr=descr, short_descr=short_descr)
        self.turns = 0
        self.screen_width = DEFAULT_SCREEN_WIDTH
        self.screen_indent = DEFAULT_SCREEN_INDENT
        self.screen_styles_enabled = True
        self.smartquotes_enabled = True
        self.prompt_toolkit_enabled = True
        self.output_line_delay = 50   # milliseconds.
        self.brief = 0  # 0=off, 1=short descr. for known locations, 2=short descr. for all locations
        self.known_locations = set()   # type: Set[base.Location]
        self.last_input_time = time.time()
        self.init_nonserializables()

    def init_nonserializables(self) -> None:
        # these things cannot be serialized or have to be reinitialized
        # call this function after deserialization.
        self._input = queue.Queue()   # type: queue.Queue[str]
        self.input_is_available = Event()
        self.transcript = None   # type: Optional[IO[str]]
        self._output = TextBuffer()

    def init_names(self, name: str, title: str, descr: str, short_descr: str) -> None:
        title = lang.capital(title or name)  # make sure the title of a player remains capitalized
        super().init_names(name, title, descr, short_descr)

    def __repr__(self):
        return "<%s '%s' #%d @ 0x%x, privs:%s>" % (self.__class__.__name__, self.name, self.vnum,
                                                   id(self), ",".join(self.privileges) or "-")

    def set_screen_sizes(self, indent: int, width: int) -> None:
        self.screen_indent = indent
        self.screen_width = width

    def tell(self, message: str, *, end: bool=False, format: bool=True) -> base.Living:
        """
        Sends a message to a player, meant to be printed on the screen.
        Message will be converted to str if required.
        If you want to output a paragraph separator, either set end=True or tell a single newline.
        If you provide format=False, this paragraph of text won't be formatted when it is outputted,
        and whitespace is untouched. Empty strings aren't outputted at all.
        The player object is returned so you can chain calls.
        """
        msg = str(message)
        super().tell(msg)
        if msg == "\n":
            self._output.p()
        else:
            self._output.print(msg, end=end, format=format)
        return self

    def tell_text_file(self, file_resource: Resource, reformat=True) -> None:
        """
        Show the contents of the given text file resource to the player.
        """
        if reformat:
            for paragraph in file_resource.text.split("\n\n"):
                paragraph = "\n".join(line.strip() for line in paragraph.splitlines())
                self.tell(paragraph, end=True)
        else:
            self.tell(file_resource.text, format=False)

    def look(self, short: Optional[bool]=None) -> None:
        """look around in your surroundings (it excludes the player himself from livings)"""
        if short is None:
            if self.brief == 2:
                short = True
            elif self.brief == 1:
                short = self.location in self.known_locations
            else:
                short = False
        if self.location:
            self.known_locations.add(self.location)
            look_paragraphs = self.location.look(exclude_living=self, short=short)
            for paragraph in look_paragraphs:
                self.tell(paragraph, end=True)
        else:
            self.tell("You see nothing.")

    def move(self, target: base.ContainingType, actor: base.Living=None,
             *, silent: bool=False, is_player: bool=True, verb: str="move", direction_names: Sequence[str]=None) -> None:
        """
        Delegate to Living but with is_player set to True.
        Moving the player is only supported to a target Location.
        """
        super().move(target, actor, silent=silent, is_player=True, verb=verb, direction_names=direction_names)

    def create_wiretap(self, target: Union[base.Location, base.Living]) -> None:
        if "wizard" not in self.privileges:
            raise ActionRefused("wiretap requires wizard privilege")
        tap = target.get_wiretap()
        tap.subscribe(self)

    def pubsub_event(self, topicname: pubsub.TopicNameType, event: Tuple[base.MudObject, str]) -> None:
        sender, message = event
        self.tell("[wiretapped from `%s': %s]" % (sender, message), end=True)

    def clear_wiretaps(self) -> None:
        # clear all wiretaps that this player has
        pubsub.unsubscribe_all(self)

    def destroy(self, ctx: Optional[util.Context]) -> None:
        self.clear_wiretaps()
        self.deactivate_transcript()
        super().destroy(ctx)

    def allow_give_money(self, amount: float, actor: Optional[base.Living]) -> None:
        """Do we accept money? Raise ActionRefused if not. For Player, the default is that we accept."""
        pass

    def allow_give_item(self, item: base.Item, actor: Optional[base.Living]) -> None:
        """Do we accept given items? Raise ActionRefused if not. For Player, the default is that we accept."""
        pass

    def get_pending_input(self) -> Sequence[str]:
        """return the full set of lines in the input buffer (if any)"""
        result = []
        self.input_is_available.clear()
        try:
            while True:
                result.append(self._input.get_nowait())
        except queue.Empty:
            return result

    def store_input_line(self, cmd: str) -> None:
        """store a line of entered text in the input command buffer"""
        cmd = cmd.strip()
        self._input.put(cmd)
        if self.transcript:
            self.transcript.write("\n\n>> %s\n" % cmd)
        self.input_is_available.set()
        self.last_input_time = time.time()

    @property
    def idle_time(self) -> float:
        return time.time() - self.last_input_time

    def tell_object_location(self, obj: base.MudObject, known_container: Union[base.Living, base.Item, base.Location, None],
                             print_parentheses: bool=True) -> None:
        """Tells the player some details about the location of the given object."""
        if known_container is None:
            if print_parentheses:
                self.tell("(It's not clear where %s is)." % obj.name)
            else:
                self.tell("It's not clear where %s is." % obj.name)
            return
        elif known_container in self:
            if print_parentheses:
                self.tell("(%s was found in %s, in your inventory)." % (obj.name, known_container.title))
            else:
                self.tell("%s was found in %s, in your inventory." % (lang.capital(obj.name), known_container.title))
        elif known_container is self.location:
            if print_parentheses:
                self.tell("(%s was found in your current location)." % obj.name)
            else:
                self.tell("%s was found in your current location." % lang.capital(obj.name))
        elif known_container is self:
            if print_parentheses:
                self.tell("(%s was found in your inventory)." % obj.name)
            else:
                self.tell("%s was found in your inventory." % lang.capital(obj.name))
        else:
            if print_parentheses:
                self.tell("(%s was found in %s)." % (obj.name, known_container.name))
            else:
                self.tell("%s was found in %s." % (lang.capital(obj.name), known_container.name))

    def activate_transcript(self, file: str, vfs: VirtualFileSystem) -> None:
        if self.transcript:
            raise ActionRefused("There's already a transcript being made to " + self.transcript.name)
        self.transcript = vfs.open_write("transcripts/" + file, mimetype="text/plain", append=True)
        self.tell("Transcript is being written to " + self.transcript.name)
        self.transcript.write("\n*Transcript starting at %s*\n\n" % time.ctime())

    def deactivate_transcript(self) -> None:
        if self.transcript:
            self.transcript.write("\n*Transcript ending at %s*\n\n" % time.ctime())
            self.transcript.close()
            self.transcript = None
            self.tell("Transcript ended.")

    def search_extradesc(self, keyword: str, include_inventory: bool=True, include_containers_in_inventory: bool=False) -> str:
        """
        Searches the extradesc keywords for an location/living/item within the 'visible' world around the player,
        including their inventory.  If there's more than one hit, just return the first extradesc description text.
        """
        assert keyword
        keyword = keyword.lower()
        desc = self.location.extra_desc.get(keyword)
        if desc:
            return desc
        for item in self.location.items:
            desc = item.extra_desc.get(keyword)
            if desc:
                return desc
        for living in self.location.livings:
            desc = living.extra_desc.get(keyword)
            if desc:
                return desc
        if include_inventory:
            for item in self.inventory:
                desc = item.extra_desc.get(keyword)
                if desc:
                    return desc
        if include_containers_in_inventory:
            for container in self.inventory:
                try:
                    inventory = container.inventory
                except ActionRefused:
                    continue    # no access to inventory, just skip this item silently
                else:
                    for item in inventory:
                        desc = item.extra_desc.get(keyword)
                        if desc:
                            return desc
        return ""

    def test_peek_output_paragraphs(self) -> Sequence[Sequence[str]]:
        """
        Returns a copy of the output paragraphs that sit in the buffer so far
        This is for test purposes. No text styles are included.
        """
        paragraphs = self._output.get_paragraphs(clear=False)
        return [strip_text_styles(paragraph_text) for paragraph_text, formatted in paragraphs]

    def test_get_output_paragraphs(self) -> Sequence[Sequence[str]]:
        """
        Gets the accumulated output paragraphs in raw form.
        This is for test purposes. No text styles are included.
        """
        paragraphs = self._output.get_paragraphs(clear=True)
        return [strip_text_styles(paragraph_text) for paragraph_text, formatted in paragraphs]
tale/shop.py
class Shopkeeper(Living):
    def init(self) -> None:
        super().init()
        self.privileges.add("shopkeeper")   # allow for some item transfers (buy/sell) that would otherwise be blocked
        self.shop = ShopBehavior()
        self.verbs = {
            "shop": "Go shopping! This shows some information about the shop, and what it has for sale.",
            "list": "Go shopping! This shows some information about the shop, and what it has for sale.",
            "sell": "Sell something you carry with you: sell <name>",
            "buy": "Buy something from the shop: buy <item-for-sale> or buy #<itemnumber> (see list)",
            "purchase": "Buy something from the shop: buy <item-for-sale> or buy #<itemnumber> (see list)",
            "haggle": "Try to haggle.",
            "value": "Ask the shopkeeper about what he or she's willing to pay for an item.",
            "appraise": "Ask the shopkeeper about what he or she's willing to pay for an item",
            "info": "Ask about an item on sale. Name the item or give its list number.",
            "inquire": "Ask about an item on sale. Name the item or give its list number.",
            "ask": "Ask about an item on sale. Name the item or give its list number."      # overrides default 'ask'
        }

    def set_shop(self, shop: ShopBehavior) -> None:
        if any(item not in self for item in shop.forsale):
            raise ValueError("not all items from shop.forsale are in the shopkeeper's inventory")
        self.shop = shop
        if self.shop.banks_money:
            self.money = min(self.money, banking_money_limit)   # make sure we don't have surplus cash

    def validate_open_hours(self, actor: Living=None, current_time: datetime.time=None) -> None:
        if actor and "wizard" in actor.privileges:
            return   # for wizards, shops are always open
        if current_time is None:
            current_time = mud_context.driver.game_clock.clock.time()
        assert isinstance(current_time, datetime.time)
        for from_hr, to_hr in self.shop.open_hours:
            from_t = datetime.time(from_hr)
            to_t = datetime.time(to_hr)
            if from_hr < to_hr:
                if from_t <= current_time < to_t:  # normal order such as 9..17
                    return  # we're open!
            else:
                if from_t <= current_time or current_time < to_t:  # reversed order, passes midnight, such as 20..3
                    return  # we're open!
        raise ActionRefused("The shop is currently closed! Come back another time, during opening hours.")

    def _parse_item(self, parsed: ParseResult, actor: Living) -> Tuple[Item, str]:
        if parsed.who_count != 1:
            raise ParseError("I don't understand what single item you're talking about.")
        item, info = parsed.who_info.popitem()
        if item not in actor:
            raise ActionRefused(self.shop.msg_playercantsell or "You don't have that.")
        if not isinstance(item, Item):
            raise ActionRefused("You can't sell %s, %s is not trade goods!" % (item.objective, item.subjective))
        designator = info.previous_word or ""
        return item, designator

    def _get_from_list(self, number: int) -> Item:
        shoplist = list(sorted_by_title(self.inventory))
        try:
            return shoplist[number - 1]
        except IndexError:
            raise ActionRefused("That number doesn't appear on the list of items that are for sale.")

    def allow_give_item(self, item: Item, actor: Optional['Living']) -> None:
        """Do we accept given items? Raise ActionRefused if not. Shopkeeper can only be sold items to!"""
        raise ActionRefused("You can't give stuff to %s just like that, try selling it instead." % self.title)

    def notify_action(self, parsed: ParseResult, actor: Living) -> None:
        if actor is self or parsed.verb in self.verbs:
            return  # avoid reacting to ourselves, or reacting to verbs we already have a handler for
        # react to some things people might say such as "ask about <item>/<number>"
        unparsed = parsed.unparsed.split()
        if self in parsed.who_info or self.name in unparsed or lang.capital(self.name) in unparsed \
                or parsed.verb in ("hi", "hello", "greet", "wave") \
                or (parsed.verb == "say" and ("hello" in unparsed or "hi" in unparsed)):
            # someone referred to us
            if random.random() < 0.2:
                self.do_socialize("smile at " + actor.name)
            elif random.random() < 0.2:
                self.do_socialize("wave at " + actor.name)
            elif random.random() < 0.2:
                self.do_socialize("nod at " + actor.name)
            elif random.random() < 0.2:
                self.do_socialize("say \"Hello, how may I help you?\"")

    def handle_verb(self, parsed: ParseResult, actor: Living) -> bool:
        if self.shop.banks_money:
            self.money = min(self.money, banking_money_limit)   # make sure we don't have surplus cash
        self.validate_open_hours(actor)
        if parsed.verb in ("shop", "list"):
            return self.shop_list(parsed, actor)
        elif parsed.verb in ("info", "inquire", "ask"):
            return self.shop_inquire(parsed, actor)
        elif parsed.verb in ("value", "appraise"):
            return self.shop_appraise(parsed, actor)
        elif parsed.verb in ("buy", "purchase"):
            return self.shop_buy(parsed, actor)
        elif parsed.verb == "sell":
            return self.shop_sell(parsed, actor)
        elif parsed.verb == "haggle":
            self.do_socialize("exclaim No haggling")
            return True
        else:
            return False  # unrecognised verb

    def shop_list(self, parsed: ParseResult, actor: Living) -> bool:
        open_hrs = lang.join(["%d to %d" % hours for hours in self.shop.open_hours])
        actor.tell("%s says: \"Welcome. Our opening hours are: %s" % (lang.capital(self.title), open_hrs))
        if "wizard" in actor.privileges:
            actor.tell(" (but for wizards, we're always open)")
        if self.shop.willbuy:
            actor.tell(", and we specialize in " + lang.join(lang.pluralize(word) for word in self.shop.willbuy))
        actor.tell("\"\n", end=True)
        # don't show shop.forsale, it is for the code to know what items have limitless supply
        if self.inventory_size == 0:
            actor.tell("%s apologizes, \"I'm sorry, but our stuff is all gone for the moment. Come back later.\"" %
                       lang.capital(self.subjective))
            self.do_socialize("shrug at " + actor.name)
        else:
            actor.tell("%s shows you a list of what is in stock at the moment:" % lang.capital(self.subjective), end=True)
            txt = ["<ul>  # <dim>|</><ul>  item                        <dim>|</><ul> price     </>"]
            for i, item in enumerate(sorted_by_title(self.inventory), start=1):
                price = item.value * self.shop.sellprofit
                txt.append("%3d. %-30s  %s" % (i, item.title, mud_context.driver.moneyfmt.display(price)))
            actor.tell("\n".join(txt), format=False)
        return True

    def shop_inquire(self, parsed: ParseResult, actor: Living) -> bool:
        item = None
        if parsed.who_count == 2:
            # 'ask lucy about clock/#5/5'
            item = parsed.who_12[0]
            if not isinstance(item, Item):
                item = parsed.who_12[1]
                if not isinstance(item, Item):
                    item = None
        elif parsed.who_count == 1:
            # 'ask about clock/#5/5'
            item = parsed.who_1
            if not isinstance(item, Item):
                item = None
        if item:
            # the parser found an item, check if there's one in the shop too with the same name.
            shop_item = Item.search_item(item.name, self.inventory)
            if shop_item:
                item = shop_item
        if not item:
            # no items in the question, try to extract name/number and look in the shop list #
            for word in parsed.unrecognized:
                if word in ("#", "about", "over", "concerning"):
                    continue
                if word.startswith("#"):
                    word = word[1:]
                try:
                    number = int(word)
                    if number <= 0:
                        continue
                except ValueError:
                    # not a number, search by name
                    item = Item.search_item(word, self.inventory)
                    if not item:
                        continue
                else:
                    # got a number in the shop
                    item = self._get_from_list(number)
        if item and isinstance(item, Item):
            # got an item, inquire about it
            if item not in self:
                raise ActionRefused("That is not something from the shop. You can examine the %s as usual." % item.name)
            actor.tell("The shop sells %s." % lang.a(item.title))
            if item.name in item.extra_desc:
                actor.tell(lang.fullstop(item.extra_desc[item.name]))
            elif item.description:
                actor.tell(lang.fullstop(item.description))
            if random.random() < 0.1:
                actor.tell("\"Would you like to buy something?\", %s asks." % self.title)
            elif random.random() < 0.1:
                actor.tell("\"Take your time\", %s says." % self.title)
            return True
        if parsed.verb == "ask":
            raise RetrySoulVerb
        else:
            raise ParseError("It's unclear what item you want to inquire about.")

    def shop_appraise(self, parsed: ParseResult, actor: Living) -> bool:
        item, designator = self._parse_item(parsed, actor)
        if designator:
            raise ParseError("It's not clear what item you mean.")
        if item.value <= 0:
            actor.tell("%s tells you it's worthless." % lang.capital(self.title))
            return True
        if isinstance(item, Trash) or Item.search_item(item.title, self.shop.forsale):
            actor.tell("%s tells you: \"%s\"" % (lang.capital(self.title), self.shop.msg_shopdoesnotbuy))
            if self.shop.action_temper:
                self.do_socialize("%s %s" % (self.shop.action_temper, actor.name))
            return True
        # @todo charisma bonus/penalty
        price = item.value * self.shop.buyprofit
        value_str = mud_context.driver.moneyfmt.display(price)
        actor.tell("%s appraises the %s." % (lang.capital(self.title), item.name))
        actor.tell("%s tells you: \"I'll give you %s for it.\"" % (lang.capital(self.subjective), value_str))
        return True

    def shop_buy(self, parsed: ParseResult, actor: Living) -> bool:
        if len(parsed.args) != 1:    # @todo support amount to buy?
            raise ParseError("I don't understand what you want to buy.")
        item = None
        name = parsed.args[0]
        if name[0] == '#':
            # it's the Nth from the list
            try:
                num = int(name[1:])
                if num <= 0:
                    raise ValueError("num must be 1 or higher")
                item = list(sorted_by_title(self.inventory))[num - 1]
                if Item.search_item(item.title, self.shop.forsale):
                    item = item.clone()  # make a clone and sell that, the forsale items should never run out
            except ValueError:
                raise ParseError("What number on the list do you mean?")
            except IndexError:
                raise ParseError("That number is not on the list.")
        if not item:
            item = Item.search_item(name, self.shop.forsale)
            if item:
                item = item.clone()  # make a clone and sell that, the forsale items should never run out
            else:
                # search inventory
                item = self.search_item(name, include_inventory=True, include_location=False, include_containers_in_inventory=False)
        if not item:
            actor.tell("%s says: \"%s\"" % (lang.capital(self.title), self.shop.msg_playercantbuy))
            return True
        # sell the item to the customer
        # @todo charisma bonus/penalty
        price = item.value * self.shop.sellprofit
        if price > actor.money:
            actor.tell("%s tells you: \"%s\"" % (lang.capital(self.title), self.shop.msg_playercantafford))
            if self.shop.action_temper:
                self.do_socialize("%s %s" % (self.shop.action_temper, actor.name))
            return True
        item.move(actor, self)   # works, because self has 'shopkeeper' privileges
        actor.money -= price
        self.money += price
        assert actor.money >= 0.0
        if self.shop.msg_shopsolditem:
            if "%d" in self.shop.msg_shopsolditem:
                # old-style (circle) message with just a numeric value for the money
                sold_msg = self.shop.msg_shopsolditem % price
            else:
                # new-style (tale) message with a %s placeholder for the money text
                sold_msg = self.shop.msg_shopsolditem % mud_context.driver.moneyfmt.display(price)
            actor.tell("%s says: \"%s\"" % (lang.capital(self.title), sold_msg))
        else:
            actor.tell("You paid %s for it." % mud_context.driver.moneyfmt.display(price))
        self.do_socialize("thank " + actor.name)
        actor.tell("You've bought the %s!" % item.name)
        if self.shop.banks_money:
            # shopkeeper puts money over a limit in the bank
            if self.money > banking_money_limit:
                self.tell_others("Swiftly, {actor} puts some excess money away in a secret stash somewhere. "
                                 "You failed to see where it went.")
                self.money = banking_money_limit
        return True

    def shop_sell(self, parsed: ParseResult, actor: Living) -> bool:
        item, designator = self._parse_item(parsed, actor)
        if designator:
            raise ParseError("It's not clear what item you want to sell.")
        if item.value <= 0 or isinstance(item, Trash):
            actor.tell("%s tells you: \"%s\"" % (lang.capital(self.title), self.shop.msg_shopdoesnotbuy))
            if self.shop.action_temper:
                self.do_socialize("%s %s" % (self.shop.action_temper, actor.name))
            return True
        if Item.search_item(item.title, self.shop.forsale):
            # if the item is on the forsale list, don't buy it (we already have an endless supply)
            actor.tell("%s tells you: \"%s\"" % (lang.capital(self.title), self.shop.msg_shopdoesnotbuy))
            return True
        # @todo check wontdealwith
        # @todo check item type
        # check money  # @todo charisma bonus/penalty
        price = item.value * self.shop.buyprofit
        limit = self.money * 0.75   # shopkeeper should not spend more than 75% of his money on a single sale
        if price >= limit:
            actor.tell("%s says: \"%s\"" % (lang.capital(self.title), self.shop.msg_shopcantafford))
            return True
        item.move(self, actor)
        actor.money += price
        self.money -= price
        assert self.money >= 0.0
        actor.tell("You've sold the %s." % item.name)
        if self.shop.msg_shopboughtitem:
            if "%d" in self.shop.msg_shopboughtitem:
                # old-style (circle) message with just a numeric value for the money
                bought_msg = self.shop.msg_shopboughtitem % price
            else:
                # new-style (tale) message with a %s placeholder for the money text
                bought_msg = self.shop.msg_shopboughtitem % mud_context.driver.moneyfmt.display(price)
            actor.tell("%s says: \"%s\"" % (lang.capital(self.title), bought_msg))
        else:
            actor.tell("%s gave you %s for it." % (lang.capital(self.title), mud_context.driver.moneyfmt.display(price)))
        self.do_socialize("thank " + actor.name)
        return True
tale/player.py
class PlayerConnection:
    """
    Represents a player and the i/o connection that is used for him/her.
    Provides high level i/o operations to input commands and write output for the player.
    Other code should not have to call the i/o adapter directly.
    """
    dummy_player_name = "dummy-player-for-initial-connection"
    __dummy_player = Player(dummy_player_name, "n")     # will be registered in MudObjRegistry so beware
    __dummy_io = IoAdapterBase(None)

    def __init__(self, player: Player=__dummy_player, io: IoAdapterBase=__dummy_io) -> None:
        self.player = player
        self.io = io
        self.need_new_input_prompt = True

    def get_output(self) -> str:
        """
        Gets the accumulated output lines, formats them nicely, and clears the buffer.
        If there is nothing to be outputted, empty string is returned.
        """
        paragraphs = self.player._output.get_paragraphs()
        if paragraphs:
            formatted = self.io.render_output(paragraphs, width=self.player.screen_width, indent=self.player.screen_indent)
            if formatted and self.player.transcript:
                self.player.transcript.write(formatted)
            return formatted or ""
        return ""

    @property
    def last_output_line(self) -> str:
        return self.io.last_output_line

    @property
    def idle_time(self) -> float:
        return self.player.idle_time

    def write_output(self) -> None:
        """print any buffered output to the player's screen"""
        if not self.io:
            return
        output = self.get_output()
        if output:
            # (re)set a few io parameters because they can be changed dynamically
            self.io.do_styles = self.player.screen_styles_enabled
            self.io.do_smartquotes = self.player.smartquotes_enabled
            self.io.do_prompt_toolkit = self.player.prompt_toolkit_enabled
            if mud_context.config.server_mode == GameMode.IF and self.player.output_line_delay > 0:
                if os.name == "nt" and self.io.do_prompt_toolkit:
                    line_delay = 0.0    # on windows, when using prompt_toolkit, printing individual lines is already very slow
                else:
                    line_delay = self.player.output_line_delay / 1000.0
                for line in output.rstrip().splitlines():
                    self.io.output(line)
                    if line_delay > 0:
                        time.sleep(line_delay)  # delay the output for a short period
            else:
                self.io.output(output.rstrip())

    def output(self, *lines: str) -> None:
        """directly writes the given text to the player's screen, without buffering and formatting/wrapping"""
        self.io.output(*lines)

    def output_no_newline(self, line: str) -> None:
        """similar to output() but writes a single line, without newline at the end"""
        self.io.output_no_newline(self.io.smartquotes(line))

    def input_direct(self, prompt: str) -> str:
        """
        Writes any pending output and prompts for input directly. Returns stripped result.
        The driver does NOT use this for the regular game loop!
        This call is *blocking* and will not work in a multi user situation.
        """
        assert self.io.supports_blocking_input
        self.write_output()
        if not prompt.endswith(" "):
            prompt += " "
        self.output_no_newline(prompt)
        self.player.input_is_available.wait()   # blocking wait
        self.need_new_input_prompt = True
        return self.player.get_pending_input()[0].strip()   # use just the first line, strip whitespace

    def write_input_prompt(self) -> None:
        # only actually write a prompt when the flag is set.
        # this avoids writing a prompt on every server tick even when nothing is entered.
        if self.need_new_input_prompt:
            self.io.write_input_prompt()
            self.need_new_input_prompt = False

    def clear_screen(self) -> None:
        self.io.clear_screen()

    def break_pressed(self) -> None:
        self.io.break_pressed()

    def critical_error(self) -> None:
        self.io.critical_error()

    def singleplayer_mainloop(self) -> None:
        self.io.singleplayer_mainloop(self)   # this does not return, unless game is closed

    def pause(self, unpause: bool=False) -> None:
        self.io.pause(unpause)

    def destroy(self) -> None:
        ctx = None
        if self.io and self.player:
            ctx = util.Context.from_global(player_connection=self)
        if self.io:
            self.io.stop_main_loop = True
            self.io.destroy()
            if self.player and mud_context.config.server_mode == GameMode.IF:
                self.player.destroy(ctx)
                self.io.abort_all_input(self.player)
                self.player = None      # type: ignore
            self.io = None              # type: ignore
        if self.player:
            self.player.destroy(ctx)
            self.player = None          # type: ignore
tale/story.py
class TickMethod(enum.Enum):
    COMMAND = "command"
    TIMER = "timer"
tale/util.py
class GameDateTime:
    """
    The datetime class that tracks game time.
    times_realtime means how much faster the game time is running than real time.
    The internal 'clock' tracks the time in game-time (not real-time).
    """
    def __init__(self, date_time: datetime.datetime, times_realtime: float=1) -> None:
        assert times_realtime >= 0
        self.times_realtime = times_realtime
        self.clock = date_time

    def __str__(self) -> str:
        return str(self.clock)

    def add_gametime(self, timedelta: datetime.timedelta) -> None:
        """advance the game clock by a time delta expressed in game time"""
        assert isinstance(timedelta, datetime.timedelta)
        self.clock += timedelta

    def sub_gametime(self, timedelta: datetime.timedelta) -> None:
        """rewind the game clock by a time delta expressed in game time"""
        assert isinstance(timedelta, datetime.timedelta)
        self.clock -= timedelta

    def plus_realtime(self, timedelta: datetime.timedelta) -> datetime.datetime:
        """return the game clock plus a time delta expressed in real time"""
        assert isinstance(timedelta, datetime.timedelta)
        return self.clock + timedelta * self.times_realtime

    def minus_realtime(self, timedelta: datetime.timedelta) -> datetime.datetime:
        """return the game clock minus a time delta expressed in real time"""
        assert isinstance(timedelta, datetime.timedelta)
        return self.clock - timedelta * self.times_realtime

    def add_realtime(self, timedelta: datetime.timedelta) -> None:
        """advance the game clock by a time delta expressed in real time"""
        assert isinstance(timedelta, datetime.timedelta)
        self.clock += timedelta * self.times_realtime

    def sub_realtime(self, timedelta: datetime.timedelta) -> None:
        """rewind the game clock by a time delta expressed in real time"""
        assert isinstance(timedelta, datetime.timedelta)
        self.clock -= timedelta * self.times_realtime
tale/errors.py
class TaleError(Exception):
    """base class for tale related errors"""
    pass
tale/errors.py
class ActionRefused(TaleFlowControlException):
    """The action that was tried was refused by the situation or target object"""
    pass
tale/story.py
class GameMode(enum.Enum):
    IF = "if"
    MUD = "mud"
tale/story.py
class MoneyType(enum.Enum):
    FANTASY = "fantasy"
    MODERN = "modern"
    NOTHING = None  # type: str

    def __bool__(self):
        return bool(self.value)
tale/shop.py
class ShopBehavior:
    """the data describing the behavior of a particular shop"""
    def __init__(self) -> None:
        self.shopkeeper_vnum = -1   # used for circle data to designate the shopkeeper belonging to this shop
        self.banks_money = False
        self.will_fight = False
        self._buyprofit = 0.3     # price factor when shop buys item
        self._sellprofit = 1.6    # price factor when shop sells item
        self.open_hours = [(9, 17), (18, 22)]
        # items the shop always sells no matter how many are bought (should be in shopkeeper's inventory as well!):
        self.forsale = set()    # type: Set[Item]
        self.msg_playercantafford = "No cash, no goods!"
        self.msg_playercantbuy = "We don't sell that."
        self.msg_playercantsell = "I don't think you have that."
        self.msg_shopboughtitem = "Thank-you very much.  Here are your %s as payment."
        self.msg_shopcantafford = "I can't afford to buy anything, I'm only a poor peddler."
        self.msg_shopdoesnotbuy = "I don't buy that stuff.  Try another shop."
        self.msg_shopsolditem = "Here you go.  That'll be... %s."
        self.action_temper = "smoke"
        self.willbuy = set()   # type: Set[str]
        self.wontdealwith = set()   # type: Set[str]   # @todo implement

    @property
    def buyprofit(self) -> float:
        return self._buyprofit

    @buyprofit.setter
    def buyprofit(self, value: float) -> None:
        assert value <= 1.0
        self._buyprofit = value

    @property
    def sellprofit(self) -> float:
        return self._sellprofit

    @sellprofit.setter
    def sellprofit(self, value: float) -> None:
        assert value >= 1.0
        self._sellprofit = value


Question: import datetime
import importlib
import gzip
import serpent
from typing import Any, Tuple, List, Optional, Dict, Type, Sequence, Union
from .base import Item, Location, Living, Exit, Door, MudObject, MudObjRegistry, Stats, _limbo
from .story import StoryConfig, MoneyType, GameMode, TickMethod
from .player import Player, PlayerConnection
from .errors import TaleError, ActionRefused
from .driver import Deferred
from .util import GameDateTime
from .shop import ShopBehavior, Shopkeeper



def mudobj_ref(mudobj: MudObject) -> Optional[Tuple[int, str, str, str]]:
    """generate a serializable reference (vnum, name, classname, baseclassname) for a MudObject"""
    if mudobj:
        return mudobj.vnum, mudobj.name, qual_classname(mudobj), qual_baseclassname(mudobj)
    return None


def qual_classname(obj: Any, cls: bool=False) -> str:
    if not cls:
        obj = obj.__class__
    return obj.__module__ + "." + obj.__name__


def qual_baseclassname(obj: MudObject) -> str:
    mro = obj.__class__.__mro__
    if Location in mro:
        return qual_classname(Location, cls=True)
    elif Exit in mro:
        return qual_classname(Exit, cls=True)
    elif Player in mro:
        return qual_classname(Player, cls=True)
    elif Living in mro:
        return qual_classname(Living, cls=True)
    elif Item in mro:
        return qual_classname(Item, cls=True)
    else:
        raise ValueError("cannot determine Tale base class", obj)


class TaleSerializer:
    xor_key = 0x5c    # please do not hack the save files

    def __init__(self):
        serpent.register_class(Player, self.serialize_player)
        serpent.register_class(ShopBehavior, self.serialize_shopbehavior)
        serpent.register_class(Location, self.serialize_location)
        serpent.register_class(Stats, self.serialize_stats)
        serpent.register_class(Item, self.serialize_item)
        serpent.register_class(Living, self.serialize_living)
        serpent.register_class(Exit, self.serialize_exit)
        serpent.register_class(Deferred, self.serialize_deferred)
        self.serializer = serpent.Serializer(indent=True, module_in_classname=True)

    def serialize(self, story: StoryConfig, player: Player, items: Sequence[Item], livings: Sequence[Living],
                  locations: Sequence[Location], exits: Sequence[Exit],
                  deferreds: Sequence[Deferred], clock: GameDateTime):
        # only serialize livings that are not the current player, and also not the dummy player used for new connections
        livings = [l for l in livings if l is not player and l.name != PlayerConnection.dummy_player_name]
        if _limbo not in locations:
            locations = list(locations)
            locations.append(_limbo)
        if any(i not in items for i in player.inventory):
            raise ValueError("missing item (from player inventory)")
        if any(i not in items for living in livings for i in living.inventory):
            raise ValueError("missing item (from living inventory)")
        if any(i not in items for loc in locations for i in loc.items):
            raise ValueError("missing item (from locations)")
        if any(l is not player and l not in livings for loc in locations for l in loc.livings):
            raise ValueError("missing living (from locations)")
        if any(living.location is not None and living.location not in locations for living in livings):
            raise ValueError("missing location (from livings)")
        if player.location is not None and player.location not in locations:
            raise ValueError("missing location (from player)")
        if any(e not in exits for loc in locations for e in loc.exits.values()):
            raise ValueError("missing exit (from location)")
        data = {
            # "story_version": story.version,
            # "tale_version_required": story.requires_tale,
            "story_config": story,
            "clock": clock,
            "items": items,
            "livings": livings,
            "locations": locations,
            "exits": exits,
            "deferreds": deferreds,
            "player": player,
        }
        serialized = self.serializer.serialize(data)
        return self.obfuscate(serialized)

    def obfuscate(self, data: bytes) -> bytes:
        data = gzip.compress(data)
        return b"TALESAVE1" + bytes(b ^ self.xor_key for b in data)

    def add_basic_properties(self, state: Dict[str, Any], obj: MudObject) -> None:
        state["__class__"] = qual_classname(obj)
        state["__base_class__"] = qual_baseclassname(obj)
        state["title"] = obj.title
        state["descr"] = obj.description
        state["short_descr"] = obj.short_description
        state["extra_desc"] = obj.extra_desc

    def add_inventory_property(self, state: Dict[str, Any], obj: MudObject) -> None:
        try:
            inv = obj.inventory     # type: ignore

Answer:         except (AttributeError, ActionRefused):