class Event: """ Event happening in TextWorld. An event gets triggered when its set of conditions become all statisfied. Attributes: actions: Actions to be performed to trigger this event commands: Human readable version of the actions. condition: :py:class:`textworld.logic.Action` that can only be applied when all conditions are statisfied. """ def __init__( self, actions: Iterable[Action] = (), conditions: Iterable[Proposition] = (), commands: Iterable[str] = () ) -> None: """ Args: actions: The actions to be performed to trigger this event. If an empty list, then `conditions` must be provided. conditions: Set of propositions which need to be all true in order for this event to get triggered. commands: Human readable version of the actions. """ self.actions = actions self.commands = commands self.condition = self.set_conditions(conditions) @property def actions(self) -> Iterable[Action]: return self._actions @actions.setter def actions(self, actions: Iterable[Action]) -> None: self._actions = tuple(actions) @property def commands(self) -> Iterable[str]: return self._commands @commands.setter def commands(self, commands: Iterable[str]) -> None: self._commands = tuple(commands) def is_triggering(self, state: State) -> bool: """ Check if this event would be triggered in a given state. """ return state.is_applicable(self.condition) def set_conditions(self, conditions: Iterable[Proposition]) -> Action: """ Set the triggering conditions for this event. Args: conditions: Set of propositions which need to be all true in order for this event to get triggered. Returns: Action that can only be applied when all conditions are statisfied. """ if not conditions: if len(self.actions) == 0: raise UnderspecifiedEventError() # The default winning conditions are the postconditions of the # last action in the quest. conditions = self.actions[-1].postconditions variables = sorted(set([v for c in conditions for v in c.arguments])) event = Proposition("event", arguments=variables) self.condition = Action("trigger", preconditions=conditions, postconditions=list(conditions) + [event]) return self.condition def __hash__(self) -> int: return hash((self.actions, self.commands, self.condition)) def __eq__(self, other: Any) -> bool: return (isinstance(other, Event) and self.actions == other.actions and self.commands == other.commands and self.condition == other.condition) @classmethod def deserialize(cls, data: Mapping) -> "Event": """ Creates an `Event` from serialized data. Args: data: Serialized data with the needed information to build a `Event` object. """ actions = [Action.deserialize(d) for d in data["actions"]] condition = Action.deserialize(data["condition"]) event = cls(actions, condition.preconditions, data["commands"]) return event def serialize(self) -> Mapping: """ Serialize this event. Results: `Event`'s data serialized to be JSON compatible. """ data = {} data["commands"] = self.commands data["actions"] = [action.serialize() for action in self.actions] data["condition"] = self.condition.serialize() return data def copy(self) -> "Event": """ Copy this event. """ return self.deserialize(self.serialize())
class Quest: """ Quest presentation in TextWorld. A quest is a sequence of :py:class:`Action <textworld.logic.Action>` undertaken with a goal. """ def __init__(self, actions: Optional[Iterable[Action]] = None, winning_conditions: Optional[Collection[Proposition]] = None, failing_conditions: Optional[Collection[Proposition]] = None, desc: Optional[str] = None) -> None: """ Args: actions: The actions to be performed to complete the quest. If `None` or an empty list, then `winning_conditions` must be provided. winning_conditions: Set of propositions that need to be true before marking the quest as completed. Default: postconditions of the last action. failing_conditions: Set of propositions that if are all true means the quest is failed. Default: can't fail the quest. desc: A text description of the quest. """ self.actions = tuple(actions) if actions else () self.desc = desc self.commands = gen_commands_from_actions(self.actions) self.reward = 1 self.win_action = self.set_winning_conditions(winning_conditions) self.fail_action = self.set_failing_conditions(failing_conditions) def set_winning_conditions(self, winning_conditions: Optional[Collection[Proposition]]) -> Action: """ Sets wining conditions for this quest. Args: winning_conditions: Set of propositions that need to be true before marking the quest as completed. Default: postconditions of the last action. Returns: An action that is only applicable when the quest is finished. """ if winning_conditions is None: if len(self.actions) == 0: raise UnderspecifiedQuestError() # The default winning conditions are the postconditions of the # last action in the quest. winning_conditions = self.actions[-1].postconditions # TODO: Make win propositions distinguishable by adding arguments? win_fact = Proposition("win") self.win_action = Action("win", preconditions=winning_conditions, postconditions=list(winning_conditions) + [win_fact]) return self.win_action def set_failing_conditions(self, failing_conditions: Optional[Collection[Proposition]]) -> Optional[Action]: """ Sets the failing conditions of this quest. Args: failing_conditions: Set of propositions that if are all true means the quest is failed. Default: can't fail the quest. Returns: An action that is only applicable when the quest has failed or `None` if the quest can be failed. """ self.fail_action = None if failing_conditions is not None: # TODO: Make fail propositions distinguishable by adding arguments? fail_fact = Proposition("fail") self.fail_action = Action("fail", preconditions=failing_conditions, postconditions=list(failing_conditions) + [fail_fact]) return self.fail_action def __hash__(self) -> int: return hash((self.actions, self.win_action, self.fail_action, self.desc, tuple(self.commands))) def __eq__(self, other: Any) -> bool: return (isinstance(other, Quest) and self.actions == other.actions and self.win_action == other.win_action and self.fail_action == other.fail_action and self.desc == other.desc and self.reward == other.reward and self.commands == other.commands) @classmethod def deserialize(cls, data: Mapping) -> "Quest": """ Creates a `Quest` from serialized data. Args: data: Serialized data with the needed information to build a `Quest` object. """ actions = [Action.deserialize(d) for d in data["actions"]] win_action = Action.deserialize(data["win_action"]) failing_conditions = None if data["fail_action"] is not None: fail_action = Action.deserialize(data["fail_action"]) failing_conditions = fail_action.preconditions desc = data["desc"] quest = cls(actions, win_action.preconditions, failing_conditions, desc=desc) quest.commands = data["commands"] quest.reward = data.get("reward", 1) return quest def serialize(self) -> Mapping: """ Serialize this quest. Results: Quest's data serialized to be JSON compatible """ data = {} data["desc"] = self.desc data["reward"] = self.reward data["commands"] = self.commands data["actions"] = [action.serialize() for action in self.actions] data["win_action"] = self.win_action.serialize() data["fail_action"] = self.fail_action if self.fail_action is not None: data["fail_action"] = self.fail_action.serialize() return data def copy(self) -> "Quest": """ Copy this quest. """ return self.deserialize(self.serialize()) def __str__(self) -> str: return " -> ".join(map(str, self.actions)) def __repr__(self) -> str: txt = "Quest({!r}, winning_conditions={!r}, failing_conditions={!r} desc={!r})" failing_conditions = None if self.fail_action is not None: failing_conditions = self.fail_action.preconditions return txt.format(self.actions, self.win_action.preconditions, failing_conditions, self.desc)