def test_game_with_infinite_max_score(self): M = textworld.GameMaker() museum = M.new_room("Museum") statue = M.new(type="o", name="golden statue") pedestal = M.new(type="s", name="pedestal") pedestal.add(statue) museum.add(pedestal) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', statue, M.inventory)]), ], reward=10, optional=True, repeatable=True), Quest(win_events=[ Event(conditions=[M.new_fact('at', statue, museum)]), ], reward=0) ] M.set_walkthrough( ["take golden statue from pedestal", "look", "drop statue"]) game = M.build() assert game.max_score == np.inf inform7 = Inform7Game(game) game_progress = GameProgression(game) assert len(game_progress.quest_progressions) == len(game.quests) # Following the actions associated to the last quest actually corresponds # to solving the whole game. for action in game_progress.winning_policy: assert not game_progress.done game_progress.update(action) assert game_progress.done assert not game_progress.quest_progressions[ 0].completed # Repeatable quests can never be completed. assert game_progress.quest_progressions[ 1].completed # Mandatory quest. # Solve the game while completing the optional quests. game_progress = GameProgression(game) for command in [ "take golden statue from pedestal", "look", "look", "drop golden statue" ]: _apply_command(command, game_progress, inform7) progressions = game_progress.quest_progressions assert not progressions[0].done # Repeatable quests can never be done. assert progressions[ 0].nb_completions == 3 # They could have been completed a number of times, though. assert progressions[1].done assert game_progress.score == 30
def setUpClass(cls): M = GameMaker() # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R2) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("closed") carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') R1.add(carrot) R1.add(lettuce) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) # The goals commands = [ "open wooden door", "go west", "take carrot", "go east", "drop carrot" ] cls.eventA = M.new_event_using_commands(commands) commands = [ "open wooden door", "go west", "take lettuce", "go east", "insert lettuce into chest" ] cls.eventB = M.new_event_using_commands(commands) cls.losing_eventA = Event(conditions={M.new_fact("eaten", carrot)}) cls.losing_eventB = Event(conditions={M.new_fact("eaten", lettuce)}) cls.questA = Quest(win_events=[cls.eventA], fail_events=[cls.losing_eventA]) cls.questB = Quest(win_events=[cls.eventB], fail_events=[cls.losing_eventB]) cls.questC = Quest(win_events=[], fail_events=[cls.losing_eventA, cls.losing_eventB]) commands = ["open wooden door", "go west", "take carrot", "eat carrot"] cls.eating_carrot = M.new_event_using_commands(commands) commands = [ "open wooden door", "go west", "take lettuce", "eat lettuce" ] cls.eating_lettuce = M.new_event_using_commands(commands) commands = [ "open wooden door", "go west", "take lettuce", "go east", "insert lettuce into chest" ] M.quests = [cls.questA, cls.questB, cls.questC] cls.game = M.build()
def test_generating_quests(self): g_rng.set_seed(2018) map_ = make_small_map(n_rooms=5, possible_door_states=["open"]) world = World.from_map(map_) def _rule_to_skip(rule): # Examine, look and inventory shouldn't be used for chaining. if rule.name.startswith("look"): return True if rule.name.startswith("inventory"): return True if rule.name.startswith("examine"): return True return False for max_depth in range(1, 3): for rule in KnowledgeBase.default().rules.values(): if _rule_to_skip(rule): continue options = ChainingOptions() options.backward = True options.max_depth = max_depth options.max_length = max_depth options.create_variables = True options.rules_per_depth = [[rule]] options.restricted_types = {"r"} chain = sample_quest(world.state, options) # Build the quest by providing the actions. actions = chain.actions assert len(actions) == max_depth, rule.name quest = Quest(win_events=[Event(actions)]) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not quest.is_winning(state) state.apply(action) assert quest.is_winning(state) # Build the quest by only providing the winning conditions. quest = Quest( win_events=[Event(conditions=actions[-1].postconditions)]) tmp_world = World.from_facts(chain.initial_state.facts) state = tmp_world.state for action in actions: assert not quest.is_winning(state) state.apply(action) assert quest.is_winning(state)
def make_game(options: GameOptions) -> Game: """ Make a game (map + objects + quest). Arguments: options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. """ rngs = options.rngs # Generate only the map for now (i.e. without any objects) world = make_world(options.nb_rooms, nb_objects=0, rngs=rngs) # Sample a quest. chaining_options = options.chaining.copy() # Go, examine, look and inventory shouldn't be used for chaining. exclude = ["go.*", "examine.*", "look.*", "inventory.*"] chaining_options.rules_per_depth = [ options.kb.rules.get_matching(".*", exclude=exclude) ] chaining_options.backward = True chaining_options.create_variables = True chaining_options.rng = rngs['quest'] chaining_options.restricted_types = {"r", "d"} chain = sample_quest(world.state, chaining_options) subquests = [] for i in range(1, len(chain.nodes)): if chain.nodes[i].breadth != chain.nodes[i - 1].breadth: event = Event(chain.actions[:i]) subquests.append(Quest(win_events=[event])) event = Event(chain.actions) subquests.append(Quest(win_events=[event])) # Set the initial state required for the quest. world.state = chain.initial_state # Add distractors objects (i.e. not related to the quest) world.populate(options.nb_objects, rng=rngs['objects']) grammar = make_grammar(options.grammar, rng=rngs['grammar']) game = make_game_with(world, subquests, grammar) game.change_grammar(grammar) game.metadata["uuid"] = options.uuid return game
def test_init(self): event = Event(self.actions) assert event.actions == self.actions assert event.condition == self.event.condition assert event.condition.preconditions == self.actions[-1].postconditions assert set(event.condition.preconditions).issuperset(self.conditions) event = Event(conditions=self.conditions) assert len(event.actions) == 0 assert set(event.condition.preconditions) == set(self.conditions) npt.assert_raises(UnderspecifiedEventError, Event, actions=[]) npt.assert_raises(UnderspecifiedEventError, Event, actions=[], conditions=[]) npt.assert_raises(UnderspecifiedEventError, Event, conditions=[])
def build_test_game(): M = GameMaker() # The goal commands = ["go east", "insert carrot into chest"] # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') M.inventory.add(carrot) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) quest1 = M.new_quest_using_commands(commands) quest1.reward = 2 commands = ["go east", "insert carrot into chest", "close chest"] event = M.new_event_using_commands(commands) quest2 = Quest(win_events=[event], fail_events=[Event(conditions={M.new_fact("eaten", carrot)})]) M.quests = [quest1, quest2] game = M.build() return game
def new_event_using_commands(self, commands: List[str]) -> Event: """ Creates a new event using predefined text commands. This launches a `textworld.play` session to execute provided commands. Args: commands: Text commands. Returns: The resulting event. """ with make_temp_directory() as tmpdir: try: game_file = self.compile(pjoin(tmpdir, "record_event.ulx")) recorder = Recorder() agent = textworld.agents.WalkthroughAgent(commands) textworld.play(game_file, agent=agent, wrappers=[recorder], silent=True) except textworld.agents.WalkthroughDone: pass # Quest is done. # Skip "None" actions. actions, commands = zip(*[(a, c) for a, c in zip(recorder.actions, commands) if a is not None]) event = Event(actions=actions, commands=commands) return event
def generate_quest(self, obj): quests = [] locations = self.find_correct_locations(obj) assert len(locations) > 0 conditions = [self.maker.new_fact(preposition_of(location), obj, location) for location in locations] events = [Event(conditions={c}) for c in conditions] place_quest = Quest(win_events=events, reward=self.config.reward) quests.append(place_quest) if self.config.intermediate_reward > 0: current_location = obj.parent if current_location == self.maker.inventory: return quests take_cond = self.maker.new_fact('in', obj, self.maker.inventory) events = [Event(conditions={take_cond})] take_quest = Quest(win_events=events, reward=int(self.config.intermediate_reward)) quests.append(take_quest) return quests
def generate_quest(maker, obj): locations = find_correct_locations(maker, obj) assert len(locations) > 0 conditions = [ maker.new_fact(preposition_of(location), obj, location) for location in locations ] events = [Event(conditions={c}) for c in conditions] return Quest(win_events=events)
def set_quest_from_commands(self, commands: List[str], ask_for_state: bool = False) -> Quest: """ Defines the game's quest using predefined text commands. This launches a `textworld.play` session. Args: commands: Text commands. ask_for_state: If true, the user will be asked to specify which set of facts of the final state are should be true in order to consider the quest as completed. Returns: The resulting quest. """ with make_temp_directory() as tmpdir: try: game_file = self.compile(pjoin(tmpdir, "record_quest.ulx")) recorder = Recorder() agent = textworld.agents.WalkthroughAgent(commands) textworld.play(game_file, agent=agent, wrappers=[recorder], silent=True) except textworld.agents.WalkthroughDone: pass # Quest is done. # Skip "None" actions. actions = [action for action in recorder.actions if action is not None] # Ask the user which quests have important state, if this is set # (if not, we assume the last action contains all the relevant facts) winning_facts = None if ask_for_state and recorder.last_game_state is not None: winning_facts = [ user_query.query_for_important_facts( actions=recorder.actions, facts=recorder.last_game_state.state.facts, varinfos=self._working_game.infos) ] if len(commands) != len(actions): unrecognized_commands = [ c for c, a in zip(commands, recorder.actions) if a is None ] raise QuestError( "Some of the actions were unrecognized: {}".format( unrecognized_commands)) event = Event(actions=actions, conditions=winning_facts) self.quests = [Quest(win_events=[event])] # Calling build will generate the description for the quest. self.build() return self.quests[-1]
def build_and_compile_game(options: GameOptions): M = textworld.GameMaker() # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') M.inventory.add(carrot) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) quest1_cmds = ["go east", "insert carrot into chest"] carrot_in_chest = M.new_event_using_commands(quest1_cmds) eating_carrot = Event(conditions={M.new_fact("eaten", carrot)}) quest1 = Quest(win_events=[carrot_in_chest], fail_events=[eating_carrot], reward=2) quest2_cmds = quest1_cmds + ["close chest"] quest2_actions = M.new_event_using_commands(quest2_cmds).actions chest_closed_with_carrot = Event(conditions={ M.new_fact("in", carrot, chest), M.new_fact("closed", chest) }, actions=quest2_actions) quest2 = Quest(win_events=[chest_closed_with_carrot], fail_events=[eating_carrot]) M.quests = [quest1, quest2] M.set_walkthrough(quest2_cmds) game = M.build() game_file = _compile_test_game(game, options) return game, game_file
def setUpClass(cls): M = GameMaker() room = M.new_room("room") M.set_player(room) carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') room.add(carrot) room.add(lettuce) chest = M.new(type='c', name='chest') chest.add_property("open") room.add(chest) # The goals commands = ["take carrot", "insert carrot into chest"] cls.eventA = M.new_event_using_commands(commands) commands = ["take lettuce", "insert lettuce into chest", "close chest"] event = M.new_event_using_commands(commands) cls.eventB = Event(actions=event.actions, conditions={ M.new_fact("in", lettuce, chest), M.new_fact("closed", chest) }) cls.fail_eventA = Event(conditions={M.new_fact("eaten", carrot)}) cls.fail_eventB = Event(conditions={M.new_fact("eaten", lettuce)}) cls.quest = Quest(win_events=[cls.eventA, cls.eventB], fail_events=[cls.fail_eventA, cls.fail_eventB]) commands = ["take carrot", "eat carrot"] cls.eating_carrot = M.new_event_using_commands(commands) commands = ["take lettuce", "eat lettuce"] cls.eating_lettuce = M.new_event_using_commands(commands) commands = ["take lettuce", "insert lettuce into chest"] M.quests = [cls.quest] cls.game = M.build()
def make_quest(world: Union[World, State], options: Optional[GameOptions] = None): state = getattr(world, "state", world) if options is None: options = GameOptions() # By default, exclude quests finishing with: go, examine, look and inventory. exclude = ["go.*", "examine.*", "look.*", "inventory.*"] options.chaining.rules_per_depth = [ options.kb.rules.get_matching(".*", exclude=exclude) ] options.chaining.rng = options.rngs['quest'] chains = [] for _ in range(options.nb_parallel_quests): chain = sample_quest(state, options.chaining) if chain is None: msg = "No quest can be generated with the provided options." raise NoSuchQuestExistError(msg) chains.append(chain) state = chain.initial_state # State might have changed, i.e. options.create_variable is True. if options.chaining.backward and hasattr(world, "state"): world.state = state quests = [] actions = [] for chain in reversed(chains): for i in range(1, len(chain.nodes)): actions.append(chain.actions[i - 1]) if chain.nodes[i].breadth != chain.nodes[i - 1].breadth: event = Event(actions) quests.append(Quest(win_events=[event])) actions.append(chain.actions[-1]) event = Event(actions) quests.append(Quest(win_events=[event])) return quests
def setUpClass(cls): M = GameMaker() # The goal commands = ["go east", "insert carrot into chest"] # Create a 'bedroom' room. R1 = M.new_room("bedroom") R2 = M.new_room("kitchen") M.set_player(R1) path = M.connect(R1.east, R2.west) path.door = M.new(type='d', name='wooden door') path.door.add_property("open") carrot = M.new(type='f', name='carrot') M.inventory.add(carrot) # Add a closed chest in R2. chest = M.new(type='c', name='chest') chest.add_property("open") R2.add(chest) cls.eventA = M.new_event_using_commands(commands) cls.eventB = Event(conditions={ M.new_fact("at", carrot, R1), M.new_fact("closed", path.door) }) cls.eventC = Event(conditions={M.new_fact("eaten", carrot)}) cls.eventD = Event(conditions={ M.new_fact("closed", chest), M.new_fact("closed", path.door) }) cls.quest = Quest(win_events=[cls.eventA, cls.eventB], fail_events=[cls.eventC, cls.eventD], reward=2) M.quests = [cls.quest] cls.game = M.build() cls.inform7 = Inform7Game(cls.game)
def build_and_compile_playground_game(): M = GameMaker() room_dr = M.new_room("Locker Room") room_mr = M.new_room("Meeting Room") room_fl = M.new_room("Field") team_mates = M.new(type='tm', name = "teammates") captain = M.new(type='tm3', name = "captain") room_dr.add(team_mates) room_mr.add(captain) corridor = M.connect(room_dr.east, room_mr.west) corridor1 = M.connect(room_mr.east, room_fl.west) #corridor2 = M.connect(room_fl.east, new_room.west) M.set_player(room_dr) team_mates_2 = M.new(type='tm2', name = "someone") room_dr.add(team_mates_2) game_gear = M.new(type='ap', name = "game gears") room_dr.add(game_gear) M.add_fact("not_equiped", game_gear) M.add_fact("not_participated", captain) M.add_fact("not_helped", team_mates) M.add_fact("not_disturbed", team_mates) M.add_fact("not_bully", team_mates) room_dr.infos.desc = "You are in the Locker room with your team mates. You will find your game gears here. Collect the game gears and wear them before the meeting. If you need help, ask help from others and be helpful to others as well. \n After your are done here, go to the meeting room." room_mr.infos.desc = "You are in the meeting room. A meeting is going to start to finalize the game plan. Join the meeting and try to share your thought." captain.infos.desc = "The meeting is leading by the captain. Please, Cooperate with him." failed_event_1 = Event(conditions={ M.new_fact("not_equiped", game_gear), M.new_fact("at", M._entities['P'], room_mr)}) failed_quest_1 = Quest(win_events=[], fail_events=[failed_event_1]) #M.quests = [failed_quest_1] game = M.build() game_file = _compile_test_game(game) return game, game_file
def make_quest(world, quest_length, rng=None, rules_per_depth=(), backward=False): state = world if hasattr(world, "state"): state = world.state rng = g_rng.next() if rng is None else rng # Sample a quest according to quest_length. options = ChainingOptions() options.backward = backward options.max_depth = quest_length options.rng = rng options.rules_per_depth = rules_per_depth chain = sample_quest(state, options) event = Event(chain.actions) return Quest(win_events=[event])
def set_quest_from_commands(self, commands: List[str]) -> Quest: """ Defines the game's quest using predefined text commands. This launches a `textworld.play` session. Args: commands: Text commands. Returns: The resulting quest. """ with make_temp_directory() as tmpdir: try: game_file = self.compile(pjoin(tmpdir, "record_quest.ulx")) recorder = Recorder() agent = textworld.agents.WalkthroughAgent(commands) textworld.play(game_file, agent=agent, wrappers=[recorder], silent=True) except textworld.agents.WalkthroughDone: pass # Quest is done. # Skip "None" actions. actions = [action for action in recorder.actions if action is not None] if len(commands) != len(actions): unrecognized_commands = [ c for c, a in zip(commands, recorder.actions) if a is None ] raise QuestError( "Some of the actions were unrecognized: {}".format( unrecognized_commands)) event = Event(actions=actions) self.quests = [Quest(win_events=[event])] # Calling build will generate the description for the quest. self.build() return self.quests[-1]
def record_quest(self, ask_for_state: bool = False) -> Quest: """ Defines the game's quest by recording the commands. This launches a `textworld.play` session. Args: ask_for_state: If true, the user will be asked to specify which set of facts of the final state are should be true in order to consider the quest as completed. Returns: The resulting quest. """ with make_temp_directory() as tmpdir: game_file = self.compile(pjoin(tmpdir, "record_quest.ulx")) recorder = Recorder() agent = textworld.agents.HumanAgent(autocompletion=True) textworld.play(game_file, agent=agent, wrappers=[recorder]) # Skip "None" actions. actions = [action for action in recorder.actions if action is not None] # Ask the user which quests have important state, if this is set # (if not, we assume the last action contains all the relevant facts) winning_facts = None if ask_for_state and recorder.last_game_state is not None: winning_facts = [ user_query.query_for_important_facts( actions=recorder.actions, facts=recorder.last_game_state.state.facts, varinfos=self._working_game.infos) ] event = Event(actions=actions, conditions=winning_facts) self.quests.append(Quest(win_events=[event])) # Calling build will generate the description for the quest. self.build() return self.quests[-1]
def record_quest(self) -> Quest: """ Defines the game's quest by recording the commands. This launches a `textworld.play` session. Returns: The resulting quest. """ with make_temp_directory() as tmpdir: game_file = self.compile(pjoin(tmpdir, "record_quest.ulx")) recorder = Recorder() agent = textworld.agents.HumanAgent(autocompletion=True) textworld.play(game_file, agent=agent, wrappers=[recorder]) # Skip "None" actions. actions = [action for action in recorder.actions if action is not None] # Assume the last action contains all the relevant facts about the winning condition. event = Event(actions=actions) self.quests.append(Quest(win_events=[event])) # Calling build will generate the description for the quest. self.build() return self.quests[-1]
def make_game(mode: str, options: GameOptions) -> textworld.Game: """ Make a Treasure Hunter game. Arguments: mode: Mode for the game where * `'easy'`: rooms are all empty except where the two objects are placed. Also, connections between rooms have no door. * `'medium'`: adding closed doors and containers that might need to be open in order to find the object. * `'hard'`: adding locked doors and containers (necessary keys will in the inventory) that might need to be unlocked (and open) in order to find the object. options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. """ kb = KnowledgeBase.default() metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Treasure Hunter" metadata["mode"] = mode metadata["seeds"] = options.seeds metadata["world_size"] = options.nb_rooms metadata["quest_length"] = options.quest_length rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_quest = rngs['quest'] rng_grammar = rngs['grammar'] modes = ["easy", "medium", "hard"] if mode == "easy": door_states = None n_distractors = 0 elif mode == "medium": door_states = ["open", "closed"] n_distractors = 10 elif mode == "hard": door_states = ["open", "closed", "locked"] n_distractors = 20 # Generate map. map_ = textworld.generator.make_map(n_rooms=options.nb_rooms, rng=rng_map, possible_door_states=door_states) assert len(map_.nodes()) == options.nb_rooms world = World.from_map(map_) # Randomly place the player. starting_room = None if len(world.rooms) > 1: starting_room = rng_map.choice(world.rooms) world.set_player_room(starting_room) # Add object the player has to pick up. types_counts = kb.types.count(world.state) obj_type = kb.types.sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) right_obj = Variable(var_id, obj_type) world.add_fact(Proposition("in", [right_obj, world.inventory])) # Add containers and supporters to the world. types_counts = kb.types.count(world.state) objects = [] distractor_types = uniquify(['c', 's'] + kb.types.descendants('c') + kb.types.descendants('s')) for i in range(n_distractors): obj_type = rng_objects.choice(distractor_types) var_id = get_new(obj_type, types_counts) # This update the types_counts. objects.append(Variable(var_id, obj_type)) world.populate_with(objects, rng=rng_objects) # Add object the player should not pick up. types_counts = kb.types.count(world.state) obj_type = kb.types.sample(parent_type='o', rng=rng_objects, include_parent=True) var_id = get_new(obj_type, types_counts) wrong_obj = Variable(var_id, obj_type) # Place it anywhere in the world. world.populate_with([wrong_obj], rng=rng_objects) # Generate a quest that finishes by taking something (i.e. the right # object since it's the only one in the inventory). options.chaining.rules_per_depth = [kb.rules.get_matching("take.*")] options.chaining.backward = True options.chaining.rng = rng_quest #options.chaining.restricted_types = exceptions #exceptions = ["r", "c", "s", "d"] if mode == "easy" else ["r"] chain = textworld.generator.sample_quest(world.state, options.chaining) # Add objects needed for the quest. world.state = chain.initial_state event = Event(chain.actions) quest = Quest( win_events=[event], fail_events=[ Event(conditions={Proposition("in", [wrong_obj, world.inventory])}) ]) grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) game = textworld.generator.make_game_with(world, [quest], grammar) game.metadata = metadata mode_choice = modes.index(mode) uuid = "tw-treasure_hunter-{specs}-{grammar}-{seeds}" uuid = uuid.format(specs=encode_seeds( (mode_choice, options.nb_rooms, options.quest_length)), grammar=options.grammar.uuid, seeds=encode_seeds( [options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def make_game(mode: str, options: GameOptions) -> textworld.Game: """ Make a Coin Collector game. Arguments: mode: Mode for the game where * `'simple'`: the distractor rooms are only placed orthogonaly to the chain. This means moving off the optimal path leads immediately to a dead end. * `'random'`: the distractor rooms are randomly place along the chain. This means a player can wander for a while before reaching a dead end. options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). .. warning:: This challenge requires `options.grammar.allowed_variables_numbering` to be `True`. Returns: Generated game. """ # Needed for games with a lot of rooms. assert options.grammar.allowed_variables_numbering if mode == "simple" and float(options.nb_rooms) / options.quest_length > 4: msg = ("Total number of rooms must be less than 4 * `quest_length` " "when distractor mode is 'simple'.") raise ValueError(msg) metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Coin Collector" metadata["mode"] = mode metadata["seeds"] = options.seeds metadata["world_size"] = options.nb_rooms metadata["quest_length"] = options.quest_length rngs = options.rngs rng_map = rngs['map'] M = textworld.GameMaker(options) # Generate map. rooms = [] walkthrough = [] for i in range(options.quest_length): r = M.new_room() if i >= 1: # Connect it to the previous rooms. free_exits = [ k for k, v in rooms[-1].exits.items() if v.dest is None ] src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(rooms[-1].exits[src_exit], r.exits[dest_exit]) walkthrough.append("go {}".format(src_exit)) rooms.append(r) M.set_player(rooms[0]) # Add a coin for the player to pick up. coin = M.new(type="o", name="coin") rooms[-1].add(coin) # Add distractor rooms, if needed. chain_of_rooms = list(rooms) while len(rooms) < options.nb_rooms: if mode == "random": src = rng_map.choice(rooms) else: # Add one distractor room per room along the chain. src = chain_of_rooms[len(rooms) % len(chain_of_rooms)] free_exits = [k for k, v in src.exits.items() if v.dest is None] if len(free_exits) == 0: continue dest = M.new_room() src_exit = rng_map.choice(free_exits) dest_exit = reverse_direction(src_exit) M.connect(src.exits[src_exit], dest.exits[dest_exit]) rooms.append(dest) # Generate the quest thats by collecting the coin. quest = Quest( win_events=[Event(conditions={M.new_fact("in", coin, M.inventory)})]) M.quests = [quest] walkthrough.append("take coin") M.set_walkthrough(walkthrough) game = M.build() game.metadata.update(metadata) mode_choice = 0 if mode == "simple" else 1 uuid = "tw-coin_collector-{specs}-{grammar}-{seeds}" uuid = uuid.format(specs=encode_seeds( (mode_choice, options.nb_rooms, options.quest_length)), grammar=options.grammar.uuid, seeds=encode_seeds( [options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def test_serialization(self): data = self.event.serialize() event = Event.deserialize(data) assert event == self.event
def make(settings: Mapping[str, str], options: Optional[GameOptions] = None) -> textworld.Game: """ Make a Cooking game. Arguments: settings: Difficulty settings (see notes). options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. Notes: The settings that can be provided are: * recipe : Number of ingredients in the recipe. * take : Number of ingredients to fetch. It must be less or equal to the value of the `recipe` skill. * open : Whether containers/doors need to be opened. * cook : Whether some ingredients need to be cooked. * cut : Whether some ingredients need to be cut. * drop : Whether the player's inventory has limited capacity. * go : Number of locations in the game (1, 6, 9, or 12). """ options = options or GameOptions() # Load knowledge base specific to this challenge. options.kb = KnowledgeBase.load(KB_PATH) rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_grammar = rngs['grammar'] rng_quest = rngs['quest'] rng_recipe = np.random.RandomState(settings["recipe_seed"]) allowed_foods = list(FOODS) allowed_food_preparations = get_food_preparations(list(FOODS)) if settings["split"] == "train": allowed_foods = list(FOODS_SPLITS['train']) allowed_food_preparations = dict(FOOD_PREPARATIONS_SPLITS['train']) elif settings["split"] == "valid": allowed_foods = list(FOODS_SPLITS['valid']) allowed_food_preparations = get_food_preparations(FOODS_SPLITS['valid']) # Also add food from the training set but with different preparations. allowed_foods += [f for f in FOODS if f in FOODS_SPLITS['train']] allowed_food_preparations.update(dict(FOOD_PREPARATIONS_SPLITS['valid'])) elif settings["split"] == "test": allowed_foods = list(FOODS_SPLITS['test']) allowed_food_preparations = get_food_preparations(FOODS_SPLITS['test']) # Also add food from the training set but with different preparations. allowed_foods += [f for f in FOODS if f in FOODS_SPLITS['train']] allowed_food_preparations.update(dict(FOOD_PREPARATIONS_SPLITS['test'])) if settings.get("cut"): # If "cut" skill is specified, remove all "uncut" preparations. for food, preparations in allowed_food_preparations.items(): allowed_food_preparations[food] = [preparation for preparation in preparations if "uncut" not in preparation] if settings.get("cook"): # If "cook" skill is specified, remove all "raw" preparations. for food, preparations in list(allowed_food_preparations.items()): allowed_food_preparations[food] = [preparation for preparation in preparations if "raw" not in preparation] if len(allowed_food_preparations[food]) == 0: del allowed_food_preparations[food] allowed_foods.remove(food) M = textworld.GameMaker(options) recipe = M.new(type='RECIPE', name='') meal = M.new(type='meal', name='meal') M.add_fact("out", meal, recipe) meal.add_property("edible") M.nowhere.append(recipe) # Out of play object. M.nowhere.append(meal) # Out of play object. options.nb_rooms = settings.get("go", 1) if options.nb_rooms == 1: rooms_to_place = ROOMS[:1] elif options.nb_rooms == 6: rooms_to_place = ROOMS[:2] elif options.nb_rooms == 9: rooms_to_place = ROOMS[:3] elif options.nb_rooms == 12: rooms_to_place = ROOMS[:4] else: raise ValueError("Cooking games can only have {1, 6, 9, 12} rooms.") G = make_graph_world(rng_map, rooms_to_place, NEIGHBORS, size=(5, 5)) rooms = M.import_graph(G) # Add doors for infos in DOORS: room1 = M.find_by_name(infos["path"][0]) room2 = M.find_by_name(infos["path"][1]) if room1 is None or room2 is None: continue # This door doesn't exist in this world. path = M.find_path(room1, room2) if path: assert path.door is None name = pick_name(M, infos["names"], rng_objects) door = M.new_door(path, name) door.add_property("closed") # Find kitchen. kitchen = M.find_by_name("kitchen") # The following predicates will be used to force the "prepare meal" # command to happen in the kitchen. M.add_fact("cooking_location", kitchen, recipe) # Place some default furnitures. place_entities(M, ["table", "stove", "oven", "counter", "fridge", "BBQ", "shelf", "showcase"], rng_objects) # Place some random furnitures. nb_furnitures = rng_objects.randint(len(rooms), len(ENTITIES) + 1) place_random_furnitures(M, nb_furnitures, rng_objects) # Place the cookbook and knife somewhere. cookbook = place_entity(M, "cookbook", rng_objects) cookbook.infos.synonyms = ["recipe"] if rng_objects.rand() > 0.5 or settings.get("cut"): knife = place_entity(M, "knife", rng_objects) start_room = rng_map.choice(M.rooms) M.set_player(start_room) M.grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) # Remove every food preparation with grilled, if there is no BBQ. if M.find_by_name("BBQ") is None: for name, food_preparations in allowed_food_preparations.items(): allowed_food_preparations[name] = [food_preparation for food_preparation in food_preparations if "grilled" not in food_preparation] # Disallow food with an empty preparation list. allowed_foods = [name for name in allowed_foods if allowed_food_preparations[name]] # Decide which ingredients are needed. nb_ingredients = settings.get("recipe", 1) assert nb_ingredients > 0 and nb_ingredients <= 5, "recipe must have {1,2,3,4,5} ingredients." ingredient_foods = place_random_foods(M, nb_ingredients, rng_quest, allowed_foods) # Sort by name (to help differentiate unique recipes). ingredient_foods = sorted(ingredient_foods, key=lambda f: f.name) # Decide on how the ingredients should be processed. ingredients = [] for i, food in enumerate(ingredient_foods): food_preparations = allowed_food_preparations[food.name] idx = rng_quest.randint(0, len(food_preparations)) type_of_cooking, type_of_cutting = food_preparations[idx] ingredients.append((food, type_of_cooking, type_of_cutting)) # ingredient = M.new(type="ingredient", name="") # food.add_property("ingredient_{}".format(i + 1)) # M.add_fact("base", food, ingredient) # M.add_fact(type_of_cutting, ingredient) # M.add_fact(type_of_cooking, ingredient) # M.add_fact("in", ingredient, recipe) # M.nowhere.append(ingredient) # Move ingredients in the player's inventory according to the `take` skill. nb_ingredients_already_in_inventory = nb_ingredients - settings.get("take", 0) shuffled_ingredients = list(ingredient_foods) rng_quest.shuffle(shuffled_ingredients) for ingredient in shuffled_ingredients[:nb_ingredients_already_in_inventory]: M.move(ingredient, M.inventory) # Compute inventory capacity. inventory_limit = 10 # More than enough. if settings.get("drop"): inventory_limit = nb_ingredients if nb_ingredients == 1 and settings.get("cut"): inventory_limit += 1 # So we can hold the knife along with the ingredient. # Add distractors for each ingredient. def _place_one_distractor(candidates, ingredient): rng_objects.shuffle(candidates) for food_name in candidates: distractor = M.find_by_name(food_name) if distractor: if distractor.parent == ingredient.parent: break # That object already exists and is considered as a distractor. continue # That object already exists. Can't used it as distractor. # Place the distractor in the same "container" as the ingredient. distractor = place_food(M, food_name, rng_objects, place_it=False) ingredient.parent.add(distractor) break for ingredient in ingredient_foods: if ingredient.parent == M.inventory and nb_ingredients_already_in_inventory >= inventory_limit: # If ingredient is in the inventory but inventory is full, do not add distractors. continue splits = ingredient.name.split() if len(splits) == 1: continue # No distractors. prefix, suffix = splits[0], splits[-1] same_prefix_list = [f for f in allowed_foods if f.startswith(prefix) if f != ingredient.name] same_suffix_list = [f for f in allowed_foods if f.endswith(suffix) if f != ingredient.name] if same_prefix_list: _place_one_distractor(same_prefix_list, ingredient) if same_suffix_list: _place_one_distractor(same_suffix_list, ingredient) # Add distractors foods. The amount is drawn from N(nb_ingredients, 3). nb_distractors = abs(int(rng_objects.randn(1) * 3 + nb_ingredients)) distractors = place_random_foods(M, nb_distractors, rng_objects, allowed_foods) # If recipe_seed is positive, a new recipe is sampled. if settings["recipe_seed"] > 0: assert settings.get("take", 0), "Shuffle recipe requires the 'take' skill." potential_ingredients = ingredient_foods + distractors rng_recipe.shuffle(potential_ingredients) ingredient_foods = potential_ingredients[:nb_ingredients] # Decide on how the ingredients of the new recipe should be processed. ingredients = [] for i, food in enumerate(ingredient_foods): food_preparations = allowed_food_preparations[food.name] idx = rng_recipe.randint(0, len(food_preparations)) type_of_cooking, type_of_cutting = food_preparations[idx] ingredients.append((food, type_of_cooking, type_of_cutting)) # Add necessary facts about the recipe. for food, type_of_cooking, type_of_cutting in ingredients: ingredient = M.new(type="ingredient", name="") food.add_property("ingredient_{}".format(i + 1)) M.add_fact("base", food, ingredient) M.add_fact(type_of_cutting, ingredient) M.add_fact(type_of_cooking, ingredient) M.add_fact("in", ingredient, recipe) M.nowhere.append(ingredient) # Depending on the skills and how the ingredient should be processed # we change the predicates of the food objects accordingly. for food, type_of_cooking, type_of_cutting in ingredients: if not settings.get("cook"): # Food should already be cooked accordingly. food.add_property(type_of_cooking) food.add_property("cooked") if food.has_property("inedible"): food.add_property("edible") food.remove_property("inedible") if food.has_property("raw"): food.remove_property("raw") if food.has_property("needs_cooking"): food.remove_property("needs_cooking") if not settings.get("cut"): # Food should already be cut accordingly. food.add_property(type_of_cutting) food.remove_property("uncut") if not settings.get("open"): for entity in M._entities.values(): if entity.has_property("closed"): entity.remove_property("closed") entity.add_property("open") walkthrough = [] # Build TextWorld quests. quests = [] consumed_ingredient_events = [] for i, ingredient in enumerate(ingredients): ingredient_consumed = Event(conditions={M.new_fact("consumed", ingredient[0])}) consumed_ingredient_events.append(ingredient_consumed) ingredient_burned = Event(conditions={M.new_fact("burned", ingredient[0])}) quests.append(Quest(win_events=[], fail_events=[ingredient_burned])) if ingredient[0] not in M.inventory: holding_ingredient = Event(conditions={M.new_fact("in", ingredient[0], M.inventory)}) quests.append(Quest(win_events=[holding_ingredient])) win_events = [] if ingredient[1] != TYPES_OF_COOKING[0] and not ingredient[0].has_property(ingredient[1]): win_events += [Event(conditions={M.new_fact(ingredient[1], ingredient[0])})] fail_events = [Event(conditions={M.new_fact(t, ingredient[0])}) for t in set(TYPES_OF_COOKING[1:]) - {ingredient[1]}] # Wrong cooking. quests.append(Quest(win_events=win_events, fail_events=[ingredient_consumed] + fail_events)) win_events = [] if ingredient[2] != TYPES_OF_CUTTING[0] and not ingredient[0].has_property(ingredient[2]): win_events += [Event(conditions={M.new_fact(ingredient[2], ingredient[0])})] fail_events = [Event(conditions={M.new_fact(t, ingredient[0])}) for t in set(TYPES_OF_CUTTING[1:]) - {ingredient[2]}] # Wrong cutting. quests.append(Quest(win_events=win_events, fail_events=[ingredient_consumed] + fail_events)) holding_meal = Event(conditions={M.new_fact("in", meal, M.inventory)}) quests.append(Quest(win_events=[holding_meal], fail_events=consumed_ingredient_events)) meal_burned = Event(conditions={M.new_fact("burned", meal)}) meal_consumed = Event(conditions={M.new_fact("consumed", meal)}) quests.append(Quest(win_events=[meal_consumed], fail_events=[meal_burned])) M.quests = quests G = compute_graph(M) # Needed by the move(...) function called below. # Build walkthrough. current_room = start_room walkthrough = [] # Start by checking the inventory. walkthrough.append("inventory") # 0. Find the kitchen and read the cookbook. walkthrough += move(M, G, current_room, kitchen) current_room = kitchen walkthrough.append("examine cookbook") # 1. Drop unneeded objects. for entity in M.inventory.content: if entity not in ingredient_foods: walkthrough.append("drop {}".format(entity.name)) # 2. Collect the ingredients. for food, type_of_cooking, type_of_cutting in ingredients: if food.parent == M.inventory: continue food_room = food.parent.parent if food.parent.parent else food.parent walkthrough += move(M, G, current_room, food_room) if food.parent.has_property("closed"): walkthrough.append("open {}".format(food.parent.name)) if food.parent == food_room: walkthrough.append("take {}".format(food.name)) else: walkthrough.append("take {} from {}".format(food.name, food.parent.name)) current_room = food_room # 3. Go back to the kitchen. walkthrough += move(M, G, current_room, kitchen) # 4. Process ingredients (cook). if settings.get("cook"): for food, type_of_cooking, _ in ingredients: if type_of_cooking == "fried": stove = M.find_by_name("stove") walkthrough.append("cook {} with {}".format(food.name, stove.name)) elif type_of_cooking == "roasted": oven = M.find_by_name("oven") walkthrough.append("cook {} with {}".format(food.name, oven.name)) elif type_of_cooking == "grilled": toaster = M.find_by_name("BBQ") # 3.a move to the backyard. walkthrough += move(M, G, kitchen, toaster.parent) # 3.b grill the food. walkthrough.append("cook {} with {}".format(food.name, toaster.name)) # 3.c move back to the kitchen. walkthrough += move(M, G, toaster.parent, kitchen) # 5. Process ingredients (cut). if settings.get("cut"): free_up_space = settings.get("drop") and not len(ingredients) == 1 knife = M.find_by_name("knife") if knife: knife_location = knife.parent.name knife_on_the_floor = knife_location == "kitchen" for i, (food, _, type_of_cutting) in enumerate(ingredients): if type_of_cutting == "uncut": continue if free_up_space: ingredient_to_drop = ingredients[(i + 1) % len(ingredients)][0] walkthrough.append("drop {}".format(ingredient_to_drop.name)) # Assume knife is reachable. if knife_on_the_floor: walkthrough.append("take {}".format(knife.name)) else: walkthrough.append("take {} from {}".format(knife.name, knife_location)) if type_of_cutting == "chopped": walkthrough.append("chop {} with {}".format(food.name, knife.name)) elif type_of_cutting == "sliced": walkthrough.append("slice {} with {}".format(food.name, knife.name)) elif type_of_cutting == "diced": walkthrough.append("dice {} with {}".format(food.name, knife.name)) walkthrough.append("drop {}".format(knife.name)) knife_on_the_floor = True if free_up_space: walkthrough.append("take {}".format(ingredient_to_drop.name)) # 6. Prepare and eat meal. walkthrough.append("prepare meal") walkthrough.append("eat meal") cookbook_desc = "You open the copy of 'Cooking: A Modern Approach (3rd Ed.)' and start reading:\n" recipe = textwrap.dedent( """ Recipe #1 --------- Gather all following ingredients and follow the directions to prepare this tasty meal. Ingredients: {ingredients} Directions: {directions} """ ) recipe_ingredients = "\n ".join(ingredient[0].name for ingredient in ingredients) recipe_directions = [] for ingredient in ingredients: cutting_verb = TYPES_OF_CUTTING_VERBS.get(ingredient[2]) if cutting_verb: recipe_directions.append(cutting_verb + " the " + ingredient[0].name) cooking_verb = TYPES_OF_COOKING_VERBS.get(ingredient[1]) if cooking_verb: recipe_directions.append(cooking_verb + " the " + ingredient[0].name) recipe_directions.append("prepare meal") recipe_directions = "\n ".join(recipe_directions) recipe = recipe.format(ingredients=recipe_ingredients, directions=recipe_directions) cookbook.infos.desc = cookbook_desc + recipe # Limit capacity of the inventory. for i in range(inventory_limit): slot = M.new(type="slot", name="") if i < len(M.inventory.content): slot.add_property("used") else: slot.add_property("free") M.nowhere.append(slot) # Sanity checks: for entity in M._entities.values(): if entity.type in ["c", "d"]: if not (entity.has_property("closed") or entity.has_property("open") or entity.has_property("locked")): raise ValueError("Forgot to add closed/locked/open property for '{}'.".format(entity.name)) # M.set_walkthrough(walkthrough) # BUG: having several "slots" causes issues with dependency tree. game = M.build() # Collect infos about this game. metadata = { "seeds": options.seeds, "goal": cookbook.infos.desc, "recipe": recipe, "ingredients": [(food.name, cooking, cutting) for food, cooking, cutting in ingredients], "settings": settings, "entities": [e.name for e in M._entities.values() if e.name], "nb_distractors": nb_distractors, "walkthrough": walkthrough, "max_score": sum(quest.reward for quest in game.quests), } objective = ("You are hungry! Let's cook a delicious meal. Check the cookbook" " in the kitchen for the recipe. Once done, enjoy your meal!") game.objective = objective game.metadata = metadata skills_uuid = "+".join("{}{}".format(k, "" if settings[k] is True else settings[k]) for k in SKILLS if k in settings and settings[k]) uuid = "tw-cooking{split}-{specs}-{seeds}" uuid = uuid.format(split="-{}".format(settings["split"]) if settings.get("split") else "", specs=skills_uuid, seeds=encode_seeds([options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def build_and_compile_super_hero_game(): M = GameMaker() roomA = M.new_room("Room A") alley = M.new_room("Alley") bank1 = M.new_room("Bank1") bank2 = M.new_room("Bank2") bank3 = M.new_room("Bank3") corridor = M.connect(roomA.east, alley.west) corridor1 = M.connect(alley.east, bank1.west) corridor1 = M.connect(alley.north, bank2.south) corridor1 = M.connect(alley.south, bank3.north) M.set_player(roomA) roomA.infos.desc = "You are in a road. Some mobs are planning to rob a bank. You need to stop them. Go east to the alley. You can find a person in the alley who has information about the roberry. Collect information from him and prevent the roberry." alley.infos.desc = "This is an alley. There is a person beside the table. He knows about the bank roberry." bank2.infos.desc = "This is the north bank. Some robbers are going to rob the bank. You can call the police and try to capture them or convince them to surrender. Or you can also shoot them to stop the robbery." money = M.new(type="o", name = 'money') money.infos.desc = "it is money" M.inventory.add(money) person = M.new(type="pr", name = "informant") person.infos.desc = "This person knows about the bank roberry. Do a favor for him. He will help you." M.add_fact("not_asked", person) M.add_fact("not_given", person) alley.add(person) robber = M.new(type="rbr", name = "joker") bank2.add(robber) M.add_fact("not_stopped", robber) M.add_fact("not_conflict", robber) M.add_fact("not_allowed", robber) police = M.new(type="pl", name = "police") bank2.add(police) M.add_fact("not_called", police) # asking quest qst_event_asking = Event(conditions={M.new_fact("asked", person)}) quest_asking = Quest(win_events=[qst_event_asking], reward=2) # the wining quest qst_event_stopped_rob = Event(conditions={M.new_fact("asked", person), M.new_fact("stopped", robber)}) win_quest = Quest(win_events=[qst_event_stopped_rob], reward=2) # 1st failure condition failed_cmds1 = ["go east", "go south"] failed_event1 = M.new_event_using_commands(failed_cmds1) failed_quest_1 = Quest(win_events=[], fail_events=[failed_event1]) # 2nd failure condition failed_cmds2 = ["go east", "go east"] failed_event2 = M.new_event_using_commands(failed_cmds2) failed_quest_2 = Quest(win_events=[], fail_events=[failed_event2]) # 3rd failure condition failed_event3 = Event(conditions={ M.new_fact("not_asked", person), M.new_fact("at", M._entities['P'], bank2)}) failed_quest_3 = Quest(win_events=[], fail_events=[failed_event3]) failed_event4 = Event(conditions={ M.new_fact("allowed", robber)}) failed_quest_4 = Quest(win_events=[], fail_events=[failed_event4]) M.quests = [win_quest, quest_asking, failed_quest_1, failed_quest_2, failed_quest_3, failed_quest_4] game = M.build() game.main_quest = win_quest game_file = _compile_test_game(game) return game, game_file
def make_game(settings: Mapping[str, str], options: Optional[GameOptions] = None) -> textworld.Game: """ Make a simple game. Arguments: settings: Difficulty settings (see notes). options: For customizing the game generation (see :py:class:`textworld.GameOptions <textworld.generator.game.GameOptions>` for the list of available options). Returns: Generated game. Notes: The settings that can be provided are: * rewards : The reward frequency: dense, balanced, or sparse. * goal : The description of the game's objective shown at the beginning of the game: detailed, bried, or none. * test : Whether this game should be drawn from the test distributions of games. """ metadata = {} # Collect infos for reproducibility. metadata["desc"] = "Simple game" metadata["seeds"] = options.seeds metadata["world_size"] = 6 metadata["quest_length"] = None # TBD rngs = options.rngs rng_map = rngs['map'] rng_objects = rngs['objects'] rng_grammar = rngs['grammar'] rng_quest = rngs['quest'] # Make the generation process reproducible. textworld.g_rng.set_seed(2018) M = textworld.GameMaker() M.grammar = textworld.generator.make_grammar(options.grammar, rng=rng_grammar) # Start by building the layout of the world. bedroom = M.new_room("bedroom") kitchen = M.new_room("kitchen") livingroom = M.new_room("living room") bathroom = M.new_room("bathroom") backyard = M.new_room("backyard") garden = M.new_room("garden") # Connect rooms together. bedroom_kitchen = M.connect(bedroom.east, kitchen.west) kitchen_bathroom = M.connect(kitchen.north, bathroom.south) kitchen_livingroom = M.connect(kitchen.south, livingroom.north) kitchen_backyard = M.connect(kitchen.east, backyard.west) backyard_garden = M.connect(backyard.south, garden.north) # Add doors. bedroom_kitchen.door = M.new(type='d', name='wooden door') kitchen_backyard.door = M.new(type='d', name='screen door') kitchen_backyard.door.add_property("closed") # Design the bedroom. drawer = M.new(type='c', name='chest drawer') trunk = M.new(type='c', name='antique trunk') bed = M.new(type='s', name='king-size bed') bedroom.add(drawer, trunk, bed) # Close the trunk and drawer. trunk.add_property("closed") drawer.add_property("closed") # - The bedroom's door is locked bedroom_kitchen.door.add_property("locked") # Design the kitchen. counter = M.new(type='s', name='counter') stove = M.new(type='s', name='stove') kitchen_island = M.new(type='s', name='kitchen island') refrigerator = M.new(type='c', name='refrigerator') kitchen.add(counter, stove, kitchen_island, refrigerator) # - Add some food in the refrigerator. apple = M.new(type='f', name='apple') milk = M.new(type='f', name='milk') refrigerator.add(apple, milk) # Design the bathroom. toilet = M.new(type='c', name='toilet') sink = M.new(type='s', name='sink') bath = M.new(type='c', name='bath') bathroom.add(toilet, sink, bath) toothbrush = M.new(type='o', name='toothbrush') sink.add(toothbrush) soap_bar = M.new(type='o', name='soap bar') bath.add(soap_bar) # Design the living room. couch = M.new(type='s', name='couch') low_table = M.new(type='s', name='low table') tv = M.new(type='s', name='tv') livingroom.add(couch, low_table, tv) remote = M.new(type='o', name='remote') low_table.add(remote) bag_of_chips = M.new(type='f', name='half of a bag of chips') couch.add(bag_of_chips) # Design backyard. bbq = M.new(type='s', name='bbq') patio_table = M.new(type='s', name='patio table') chairs = M.new(type='s', name='set of chairs') backyard.add(bbq, patio_table, chairs) # Design garden. shovel = M.new(type='o', name='shovel') tomato = M.new(type='f', name='tomato plant') pepper = M.new(type='f', name='bell pepper') lettuce = M.new(type='f', name='lettuce') garden.add(shovel, tomato, pepper, lettuce) # Close all containers for container in M.findall(type='c'): container.add_property("closed") # Set uncooked property for to all food items. foods = M.findall(type='f') for food in foods: food.add_property("edible") food_names = [food.name for food in foods] # Shuffle the position of the food items. rng_quest.shuffle(food_names) for food, name in zip(foods, food_names): food.orig_name = food.name food.name = name # The player starts in the bedroom. M.set_player(bedroom) # Quest walkthrough = [] # Part I - Escaping the room. # Generate the key that unlocks the bedroom door. bedroom_key = M.new(type='k', name='old key') M.add_fact("match", bedroom_key, bedroom_kitchen.door) # Decide where to hide the key. if rng_quest.rand() > 0.5: drawer.add(bedroom_key) walkthrough.append("open chest drawer") walkthrough.append("take old key from chest drawer") bedroom_key_holder = drawer else: trunk.add(bedroom_key) walkthrough.append("open antique trunk") walkthrough.append("take old key from antique trunk") bedroom_key_holder = trunk # Unlock the door, open it and leave the room. walkthrough.append("unlock wooden door with old key") walkthrough.append("open wooden door") walkthrough.append("go east") # Part II - Find food item. # 1. Randomly pick a food item to cook. food = rng_quest.choice(foods) if settings["test"]: TEST_FOODS = ["garlic", "kiwi", "carrot"] food.name = rng_quest.choice(TEST_FOODS) # Retrieve the food item and get back in the kitchen. # HACK: handcrafting the walkthrough. if food.orig_name in ["apple", "milk"]: rooms_to_visit = [] doors_to_open = [] walkthrough.append("open refrigerator") walkthrough.append("take {} from refrigerator".format(food.name)) elif food.orig_name == "half of a bag of chips": rooms_to_visit = [livingroom] doors_to_open = [] walkthrough.append("go south") walkthrough.append("take {} from couch".format(food.name)) walkthrough.append("go north") elif food.orig_name in ["bell pepper", "lettuce", "tomato plant"]: rooms_to_visit = [backyard, garden] doors_to_open = [kitchen_backyard.door] walkthrough.append("open screen door") walkthrough.append("go east") walkthrough.append("go south") walkthrough.append("take {}".format(food.name)) walkthrough.append("go north") walkthrough.append("go west") # Part II - Cooking the food item. walkthrough.append("put {} on stove".format(food.name)) # walkthrough.append("cook {}".format(food.name)) # walkthrough.append("eat {}".format(food.name)) # 2. Determine the winning condition(s) of the subgoals. quests = [] bedroom_key_holder if settings["rewards"] == "dense": # Finding the bedroom key and opening the bedroom door. # 1. Opening the container. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("open", bedroom_key_holder)}) ])) # 2. Getting the key. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", bedroom_key, M.inventory)}) ])) # 3. Unlocking the bedroom door. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("closed", bedroom_kitchen.door)}) ])) # 4. Opening the bedroom door. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("open", bedroom_kitchen.door)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Escaping out of the bedroom. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("at", M.player, kitchen)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Opening doors. for door in doors_to_open: quests.append( Quest( win_events=[Event(conditions={M.new_fact("open", door)})])) if settings["rewards"] == "dense": # Moving through places. for room in rooms_to_visit: quests.append( Quest(win_events=[ Event(conditions={M.new_fact("at", M.player, room)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Retrieving the food item. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", food, M.inventory)}) ])) if settings["rewards"] in ["dense", "balanced"]: # Retrieving the food item. quests.append( Quest(win_events=[ Event(conditions={M.new_fact("in", food, M.inventory)}) ])) if settings["rewards"] in ["dense", "balanced", "sparse"]: # Putting the food on the stove. quests.append( Quest( win_events=[Event( conditions={M.new_fact("on", food, stove)})])) # 3. Determine the losing condition(s) of the game. quests.append( Quest(fail_events=[Event(conditions={M.new_fact("eaten", food)})])) # Set the subquest(s). M.quests = quests # - Add a hint of what needs to be done in this game. objective = "The dinner is almost ready! It's only missing a grilled {}." objective = objective.format(food.name) note = M.new(type='o', name='note', desc=objective) kitchen_island.add(note) game = M.build() game.main_quest = M.new_quest_using_commands(walkthrough) game.change_grammar(game.grammar) if settings["goal"] == "detailed": # Use the detailed version of the objective. pass elif settings["goal"] == "brief": # Use a very high-level description of the objective. game.objective = objective elif settings["goal"] == "none": # No description of the objective. game.objective = "" game.metadata = metadata uuid = "tw-simple-r{rewards}+g{goal}+{dataset}-{flags}-{seeds}" uuid = uuid.format(rewards=str.title(settings["rewards"]), goal=str.title(settings["goal"]), dataset="test" if settings["test"] else "train", flags=options.grammar.uuid, seeds=encode_seeds( [options.seeds[k] for k in sorted(options.seeds)])) game.metadata["uuid"] = uuid return game
def test_game_with_optional_and_repeatable_quests(self): M = textworld.GameMaker() museum = M.new_room("Museum") weak_statue = M.new(type="o", name="ivory statue") normal_statue = M.new(type="o", name="stone statue") strong_statue = M.new(type="o", name="granite statue") museum.add(weak_statue) museum.add(normal_statue) museum.add(strong_statue) M.set_player(museum) M.quests = [ Quest(win_events=[ Event(conditions=[M.new_fact('in', weak_statue, M.inventory)]), ], reward=-10, optional=True, repeatable=True), Quest(win_events=[ Event( conditions=[M.new_fact('in', normal_statue, M.inventory)]), ], reward=3, optional=True), Quest( win_events=[ Event(conditions=[ M.new_fact('in', strong_statue, M.inventory) ]), ], reward=5, ) ] M.set_walkthrough( ["take ivory", "take stone", "drop ivory", "take granite"]) game = M.build() inform7 = Inform7Game(game) game_progress = GameProgression(game) assert len(game_progress.quest_progressions) == len(game.quests) # Following the actions associated to the last quest actually corresponds # to solving the whole game. for action in game_progress.winning_policy: assert not game_progress.done game_progress.update(action) assert game_progress.done assert not game_progress.quest_progressions[ 0].completed # Optional, negative quest. assert not game_progress.quest_progressions[ 1].completed # Optional, positive quest. assert game_progress.quest_progressions[ 2].completed # Mandatory quest. # Solve the game while completing the optional quests. game_progress = GameProgression(game) for command in [ "take ivory statue", "look", "take stone statue", "drop ivory statue", "take granite statue" ]: _apply_command(command, game_progress, inform7) progressions = game_progress.quest_progressions assert not progressions[0].done # Repeatable quests can never be done. assert progressions[ 0].nb_completions == 3 # They could have been completed a number of times, though. assert progressions[ 1].done # The nonrepeatable-optional quest can be done. assert progressions[2].done assert game.max_score == 8 assert game_progress.score == -22