コード例 #1
0
class TWCGameMaker:
    def __init__(self, config):
        self.config = config
        self.data = TWCData(config)
        self.maker = GameMaker(config.game_options)
        self.num_games = 0

    def reset(self):
        self.maker = GameMaker(self.config.game_options)

    def make_game(self):
        rng_grammar = self.config.rngs["grammar"]
        self.maker.grammar = textworld.generator.make_grammar(self.maker.options.grammar, rng=rng_grammar)

        self.place_rooms()

        placed_objects = []

        while len(placed_objects) < self.config.objects:
            if self.config.verbose:
                print()
                print("====== Placing furniture ======")

            furniture = self.place_furniture()
            if not furniture:
                print()
                print(f"Could not generate the game with the provided configuration")
                sys.exit(-1)

            if self.config.verbose:
                print()
                print("====== Placing objects ======")

            placed_objects += self.place_objects()
            assert len(placed_objects) == len(set(placed_objects))

        if self.config.verbose:
            print()
            print("====== Shuffling objects ======")

        self.move_objects(placed_objects)

        if self.config.verbose and self.config.distractors:
            print()
            print("====== Placing distractors ======")

        self.place_distractors()

        self.set_container_properties()
        self.limit_inventory_size()
        self.maker.quests = self.generate_quests(placed_objects)

        self.check_properties()

        uuid = self.generate_uuid()
        game = self.maker.build()
        self.num_games += 1

        self.set_metadata(game, placed_objects)

        if self.config.verbose:
            print()
            print("====== Goal Locations ======")
            for obj, locations in game.metadata["goal_locations"].items():
                print(f'{obj} ->', ", ".join(locations))

        self.config.game_options.path = pjoin(self.config.output_dir, uuid)

        result = textworld.generator.compile_game(game, self.config.game_options)
        self.reset()
        return result

    def place_rooms(self):
        rng = self.config.rngs["map"]
        assert self.config.rooms <= len(self.data.rooms)
        initial_room = self.config.initial_room or rng.choice(self.data.rooms)
        rooms_to_place = self.pick_rooms(initial_room)
        if self.config.verbose:
            print("Rooms:", rooms_to_place)
        self.create_map(rooms_to_place)
        room = self.maker.find_by_name(initial_room)
        self.maker.set_player(room)

    def pick_name(self, names):
        rng = self.config.rngs["objects"]
        names = list(names)
        rng.shuffle(names)
        for name in names:
            if self.maker.find_by_name(name) is None:
                return name
        assert False

    def pick_rooms(self, initial_room):
        assert self.config.rooms <= len(self.data.rooms)
        rng = self.config.rngs["map"]
        visited = {initial_room}
        neighbors = set(self.data.map[initial_room])
        neighbors -= visited

        while len(visited) < self.config.rooms:
            room = rng.choice(list(neighbors))
            visited.add(room)
            neighbors |= set(self.data.map[room])
            neighbors -= visited

        return list(visited)

    def pick_correct_location(self, locations):
        rng = self.config.rngs["objects"]
        locations = list(locations)
        rng.shuffle(locations)
        for location in locations:
            holder = None
            if "." in location:
                room_name = location.split(".")[0]
                holder_name = location.split(".")[1]
                room = self.maker.find_by_name(room_name)
                if room is not None:
                    holder = next((e for e in room.content if e.infos.name == holder_name), None)
            else:
                holder = self.maker.find_by_name(location)
            if holder:
                return holder
        return None

    def pick_wrong_object_location(self, object_name, prefer_correct_room=None):
        rng = self.config.rngs["objects"]
        correct_locations = self.data.objects[object_name]["locations"]
        rng.shuffle(correct_locations)

        holder_names = {location.split(".")[-1] for location in correct_locations}
        forbidden = illegal_locations(self.data.objects[object_name])
        holder_names |= forbidden

        if prefer_correct_room is None:
            prefer_correct_room = self.config.isolated_rooms

        assert prefer_correct_room in [True, False]
        correct_room = self.find_correct_room(object_name)

        # Try to pick location in correct room
        if correct_room and prefer_correct_room:
            room_furniture = [e for e in correct_room.content if e.infos.type in ["c", "s"]]
            wrong_holders = [e for e in room_furniture if e.infos.name not in holder_names]
            if FLOOR not in holder_names:
                wrong_holders.append(correct_room)
            rng.shuffle(wrong_holders)
            if len(wrong_holders) > 0:
                return wrong_holders[0]

        # Pick a random supporter or container
        all_supporters = list(self.maker.findall("s"))
        all_containers = list(self.maker.findall("c"))
        all_rooms = self.maker.rooms

        all_holders = all_supporters + all_containers
        if FLOOR not in holder_names:
            all_holders += all_rooms

        rng.shuffle(all_holders)
        wrong_holders = [e for e in all_holders if e.infos.name not in holder_names]

        if len(wrong_holders) > 0:
            return wrong_holders[0]

        # No wrong location found. Create new furniture
        pool = [f for f in self.data.locations.keys() if f not in holder_names]
        return self.place_random_entity(pool)

    def find_correct_room(self, object_name):
        correct_locations = self.data.objects[object_name]["locations"]
        holder_names = {location.split(".")[-1] for location in correct_locations}

        for location in correct_locations:
            if "." in location:
                room_name = location.split(".")[0]
                return self.maker.find_by_name(room_name)
        for holder_name in holder_names:
            holder = self.maker.find_by_name(holder_name)
            if holder:
                return holder.parent

    def place_at(self, name, holder):
        entity = self.maker.new(type=self.data.entities[name]["type"], name=name)
        entity.infos.noun = name
        if "adjs" in self.data.entities[name] and self.data.entities[name]["adjs"]:
            entity.infos.adj = self.data.entities[name]["adjs"][0]
        if "desc" in self.data.entities[name]:
            entity.infos.desc = self.data.entities[name]["desc"][0]
        if "indefinite" in self.data.entities[name]:
            entity.infos.indefinite = self.data.entities[name]["indefinite"]
        for property_ in self.data.entities[name]["properties"]:
            entity.add_property(property_)
        holder.add(entity)
        self.log_entity_placement(entity, holder)
        return entity

    def log_entity_placement(self, entity, holder):
        name = entity.infos.name
        if self.config.verbose:
            if self.data.entities[name]["category"] == "furniture":
                print(f"{entity.infos.name} added to the {holder.infos.name}")
            elif holder.type == "r":
                print(f"{entity.infos.name} added to the floor in the {holder.infos.name}")
            else:
                print(f"{entity.infos.name} added to the {holder.infos.name} in the {holder.parent.infos.name}")

    def attempt_place_entity(self, name):
        if self.maker.find_by_name(name):
            return
        holder = self.pick_correct_location(self.data.entities[name]["locations"])
        if holder is None:
            return None
        return self.place_at(name, holder)

    def place_entities(self, names):
        return [self.attempt_place_entity(name) for name in names]

    def place_random_entities(self, nb_entities, pool=None):
        rng = self.config.rngs["objects"]
        if pool is None:
            pool = list(self.data.entities.keys())
        if len(pool) == 0:
            return []
        seen = set(e.name for e in self.maker._entities.values())
        candidates = [name for name in pool if name not in seen]
        rng.shuffle(candidates)
        entities = []
        for candidate in candidates:
            if len(entities) >= nb_entities:
                break
            entity = self.attempt_place_entity(candidate)
            if entity:
                entities.append(entity)

        return entities

    def place_random_entity(self, pool):
        entities = self.place_random_entities(1, pool)
        return entities[0] if entities else None

    def place_random_furniture(self, nb_furniture):
        return self.place_random_entities(nb_furniture, self.data.locations.keys())

    def make_graph_map(self, rooms, size=(5, 5)):
        rng = self.config.rngs["map"]
        walker = RandomWalk(neighbors=self.data.map, size=size, rng=rng)
        return walker.place_rooms(rooms)

    def create_map(self, rooms_to_place):
        graph = self.make_graph_map(rooms_to_place)
        rooms = self.maker.import_graph(graph)

        for infos in self.data.doors:
            room1 = self.maker.find_by_name(infos["path"][0])
            room2 = self.maker.find_by_name(infos["path"][1])
            if room1 is None or room2 is None:
                continue  # This door doesn't exist in this world.
            path = self.maker.find_path(room1, room2)
            if path:
                assert path.door is None
                name = self.pick_name(infos["names"])
                door = self.maker.new_door(path, name)
                door.add_property("closed")
        return rooms

    def find_correct_locations(self, obj):
        name = obj.infos.name
        locations = self.data.objects[name]["locations"]
        result = []
        for location in locations:
            if "." in location:
                room_name = location.split(".")[0]
                holder_name = location.split(".")[1]
                room = self.maker.find_by_name(room_name)
                if room is not None:
                    result += [e for e in room.content if e.infos.name == holder_name]
            else:
                holder = self.maker.find_by_name(location)
                if holder:
                    result.append(holder)
        return result

    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_goal_locations(self, objs):
        result = {obj.infos.name: [] for obj in objs}
        for obj in objs:
            locations = self.find_correct_locations(obj)
            for loc in locations:
                result[obj.infos.name].append(loc.infos.name)
        return result

    def generate_quests(self, objs):
        return [q for obj in objs for q in self.generate_quest(obj)]

    def set_metadata(self, game, placed_objects):
        game.objective = INTRO + " " + GOAL
        config = dict(vars(self.config))
        del config['game_options']
        del config['rngs']
        metadata = {
            "seeds": self.maker.options.seeds,
            "config": config,
            "entities": [e.name for e in self.maker._entities.values() if e.name],
            "max_score": sum(quest.reward for quest in game.quests),
            "goal": GOAL,
            "goal_locations": self.generate_goal_locations(placed_objects),
            "uuid": self.generate_uuid()
        }
        game.metadata = metadata

    def generate_uuid(self):
        uuid = "tw-iqa-cleanup-{specs}-{seeds}"
        seeds = self.maker.options.seeds
        uuid = uuid.format(specs=prettify_config(self.config),
                           seeds=encode_seeds([seeds[k] + self.num_games for k in sorted(seeds)]))
        return uuid

    def check_properties(self):
        for entity in self.maker._entities.values():
            if entity.type in ["c", "d"] and 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))

    def limit_inventory_size(self):
        inventory_limit = self.config.objects * 2
        nb_objects_in_inventory = self.config.objects - self.config.take
        if self.config.drop:
            inventory_limit = max(1, nb_objects_in_inventory)
        for i in range(inventory_limit):
            slot = self.maker.new(type="slot", name="")
            if i < len(self.maker.inventory.content):
                slot.add_property("used")
            else:
                slot.add_property("free")
            self.maker.nowhere.append(slot)

    def set_container_properties(self):
        if not self.config.open:
            for entity in self.maker._entities.values():
                if entity.has_property("closed"):
                    entity.remove_property("closed")
                    entity.add_property("open")

    def place_distractors(self):
        rng_objects = self.config.rngs["objects"]
        nb_objects = self.config.objects
        if self.config.distractors:
            nb_distractors = max(0, int(rng_objects.randn(1) * 3 + nb_objects))
            self.place_random_entities(nb_distractors, pool=list(self.data.objects.keys()))

    def move_objects(self, placed_objects):
        rng_quest = self.config.rngs["quest"]
        nb_objects_in_inventory = self.config.objects - self.config.take
        shuffled_objects = list(placed_objects)
        rng_quest.shuffle(shuffled_objects)

        for obj in shuffled_objects[:nb_objects_in_inventory]:
            self.maker.move(obj, self.maker.inventory)

        for obj in shuffled_objects[nb_objects_in_inventory:]:
            wrong_location = self.pick_wrong_object_location(obj.infos.name)
            self.maker.move(obj, wrong_location)
            self.log_entity_placement(obj, wrong_location)

        return nb_objects_in_inventory

    def objects_by_furniture(self, furniture):
        result = []
        for o in self.data.objects:
            locations = [loc.split(".")[-1] for loc in self.data.objects[o]["locations"]]
            if furniture in locations:
                result.append(o)
        return result

    def evenly_place_objects(self):
        all_supporters = list(self.maker.findall("s"))
        all_containers = list(self.maker.findall("c"))
        furniture = all_supporters + all_containers

        objects_per_furniture = self.config.objects // len(furniture)

        placed = []
        for holder in furniture:
            pool = self.objects_by_furniture(holder.infos.name)
            placed += self.place_random_entities(objects_per_furniture, pool)

        remainder = self.config.objects - len(placed)
        placed += self.place_random_entities(remainder, list(self.data.objects.keys()))
        return placed

    def place_objects(self, distribute_evenly=True):
        rng = self.config.rngs["objects"]
        if distribute_evenly is None:
            distribute_evenly = rng.choice([True, False])
        if distribute_evenly:
            return self.evenly_place_objects()
        placed_objects = self.place_random_entities(self.config.objects, list(self.data.objects.keys()))
        return placed_objects

    def evenly_place_furniture(self, nb_furniture):
        furniture_per_room = nb_furniture // self.config.rooms
        placed = []
        for room in self.maker.rooms:
            room_name = room.infos.name
            pool = [k for k, v in self.data.locations.items() if room_name in v["locations"]]
            placed += self.place_random_entities(furniture_per_room, pool)

        remainder = nb_furniture - len(placed)
        placed += self.place_random_furniture(remainder)
        return placed

    def place_furniture(self, distribute_evenly=True):
        rng = self.config.rngs["objects"]
        if distribute_evenly is None:
            distribute_evenly = rng.choice([True, False])
        self.place_entities(DEFAULT_FURNITURE)
        upper_bound = max(2 * len(self.maker.rooms), 0.33 * self.config.objects)
        nb_furniture = rng.randint(len(self.maker.rooms), min(upper_bound, len(self.data.locations) + 1))
        if distribute_evenly:
            return self.evenly_place_furniture(nb_furniture)
        else:
            return self.place_random_furniture(nb_furniture)
コード例 #2
0
def make_example_game(args):
    """
    This game takes place in a typical house and consists in
    three puzzles:
    1) Escape the room;
    2) Find the missing ingredient;
    3) Finish preparing the dinner.

    Here's the map of the house.
                Bathroom
                    +
                    |
                    +
    Bedroom +--+ Kitchen +----+ Backyard
                    +              +
                    |              |
                    +              +
               Living Room       Garden
    """
    # Make the generation process reproducible.
    g_rng.set_seed(2018)

    M = GameMaker()
    # 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)

    # - The bedroom's door is locked
    bedroom_kitchen.door.add_property("locked")

    # - and the key is in the drawer.
    bedroom_key = M.new(type='k', name='old key')
    drawer.add(bedroom_key)
    drawer.add_property("closed")
    M.add_fact("match", bedroom_key, bedroom_kitchen.door)

    # - Describe the room.
    bedroom.desc = ""

    # 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 note on the kitchen island.
    objective = "The dinner is almost ready! It's only missing a grilled carrot."
    note = M.new(type='o', name='note', desc=objective)
    kitchen_island.add(note)

    # - 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')
    carrot = M.new(type='f', name='carrot')
    lettuce = M.new(type='f', name='lettuce')
    garden.add(shovel, tomato, carrot, lettuce)

    # Close all containers
    for container in M.findall(type='c'):
        container.add_property("closed")

    # Set uncooked property for to all food items.
    # NOT IMPLEMENTED YET
    # for food in M.findall(type='f'):
    #     food.add_property("edible")
    #     food.add_property("raw")

    # The player starts in the bedroom.
    M.set_player(bedroom)

    # To define a quest we are going to record it by playing the game.
    # print("******* Recording a quest. Hit 'Ctrl + C' when done. *******")
    # M.record_quest()
    commands = [
        "open chest drawer", "take old key from chest drawer",
        "unlock wooden door with old key", "open wooden door", "go east"
    ]

    if args.type in ["short", "medium", "long", "last", "human"]:
        commands.append("open screen door")

    if args.type in ["medium", "long", "last", "human"]:
        commands.extend(["go east", "go south", "take carrot"])

    if args.type in ["long", "last", "human"]:
        commands.extend([
            "go north",
            "go west",
            "put carrot on stove",
            # "cook carrot"  # Not supported yet.
        ])

    quest = M.set_quest_from_commands(commands)

    # TODO: Provide better API to specify failing conditions.
    quest.set_failing_conditions([Proposition("eaten", [carrot.var])])

    if args.type == "human":
        # Use a very high-level description of the objective.
        quest.desc = ""

    elif args.type == "last":
        # Use a very high-level description of the objective.
        quest.desc = objective

    print(quest.desc)

    game_file = M.compile(args.filename)
    print("*** Game created: {}".format(game_file))
    return game_file