def __init__(self) -> None: self._state = State() self._entities = OrderedDict() self._rooms = [] self._objects = [] self._update() self._player_room = None
def test_going_through_door(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State() state.add_facts([ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("free", [kitchen, room]), Proposition("free", [room, kitchen]), Proposition("south_of", [room, kitchen]) ]) options = ChainingOptions() options.backward = True options.max_depth = 3 options.subquests = True options.create_variables = True options.rules_per_depth = [ [KnowledgeBase.default().rules["take/c"], KnowledgeBase.default().rules["take/s"]], KnowledgeBase.default().rules.get_matching("go.*"), [KnowledgeBase.default().rules["open/d"]], ] chains = list(get_chains(state, options)) assert len(chains) == 18
def test_parallel_quests(): logic = GameLogic.parse(""" type foo { rules { do_a :: not_a(foo) & $not_c(foo) -> a(foo); do_b :: not_b(foo) & $not_c(foo) -> b(foo); do_c :: $a(foo) & $b(foo) & not_c(foo) -> c(foo); } constraints { a_or_not_a :: a(foo) & not_a(foo) -> fail(); b_or_not_b :: b(foo) & not_b(foo) -> fail(); c_or_not_c :: c(foo) & not_c(foo) -> fail(); } } """) kb = KnowledgeBase(logic, "") state = State([ Proposition.parse("a(foo)"), Proposition.parse("b(foo)"), Proposition.parse("c(foo)"), ]) options = ChainingOptions() options.backward = True options.kb = kb options.max_depth = 3 options.max_breadth = 1 chains = list(get_chains(state, options)) assert len(chains) == 2 options.max_breadth = 2 chains = list(get_chains(state, options)) assert len(chains) == 3 options.min_breadth = 2 chains = list(get_chains(state, options)) assert len(chains) == 1 assert len(chains[0].actions) == 3 assert chains[0].nodes[0].depth == 2 assert chains[0].nodes[0].breadth == 2 assert chains[0].nodes[0].parent == chains[0].nodes[2] assert chains[0].nodes[1].depth == 2 assert chains[0].nodes[1].breadth == 1 assert chains[0].nodes[1].parent == chains[0].nodes[2] assert chains[0].nodes[2].depth == 1 assert chains[0].nodes[2].breadth == 1 assert chains[0].nodes[2].parent == None options.min_breadth = 1 options.create_variables = True chains = list(get_chains(State(), options)) assert len(chains) == 5
def test_count(self): rng = np.random.RandomState(1234) types_counts = {t: rng.randint(2, 10) for t in self.types.variables} state = State() for t in self.types.variables: v = Variable(get_new(t, types_counts), t) state.add_fact(Proposition("dummy", [v])) counts = self.types.count(state) for t in self.types.variables: assert counts[t] == types_counts[t], (counts[t], types_counts[t])
def check_state(self, state: State) -> bool: """Check that a state satisfies the constraints.""" fail = Proposition("fail", []) constraints = state.all_applicable_actions(self.constraints) for constraint in constraints: copy = state.apply_on_copy(constraint) if copy and copy.is_fact(fail): return False return True
def test_room_connections(): room0 = Variable("room0", "r") room1 = Variable("room1", "r") room2 = Variable("room2", "r") # Only one connection can exist between two rooms. # r1 # | # r0 - r1 state = State([ Proposition("north_of", [room1, room0]), Proposition("south_of", [room0, room1]), Proposition("east_of", [room1, room0]), Proposition("west_of", [room0, room1]) ]) assert not check_state(state) # Non Cartesian layout are allowed. # r1 # | # r0 - r2 - r1 state = State([ Proposition("north_of", [room1, room0]), Proposition("south_of", [room0, room1]), Proposition("east_of", [room2, room0]), Proposition("west_of", [room0, room2]), Proposition("east_of", [room1, room2]), Proposition("west_of", [room2, room1]) ]) assert check_state(state) # A room cannot have more than 4 'link' propositions. room3 = Variable("room3", "r") room4 = Variable("room4", "r") room5 = Variable("room5", "r") door1 = Variable("door1", "d") door2 = Variable("door2", "d") door3 = Variable("door3", "d") door4 = Variable("door4", "d") door5 = Variable("door5", "d") state = State([ Proposition("link", [room0, door1, room1]), Proposition("link", [room0, door2, room2]), Proposition("link", [room0, door3, room3]), Proposition("link", [room0, door4, room4]), Proposition("link", [room0, door5, room5]) ]) assert not check_state(state)
def test_all_instantiations(): state = State([ Proposition.parse("at(P, kitchen: r)"), Proposition.parse("in(key: o, kitchen: r)"), Proposition.parse("in(egg: o, kitchen: r)"), Proposition.parse("in(book: o, study: r)"), Proposition.parse("in(book: o, study: r)"), Proposition.parse("in(map: o, I)"), ]) take = Rule.parse("take :: $at(P, r) & in(o, r) -> in(o, I)") actions = set(state.all_instantiations(take)) assert actions == { Action.parse("take :: $at(P, kitchen: r) & in(key: o, kitchen: r) -> in(key: o, I)"), Action.parse("take :: $at(P, kitchen: r) & in(egg: o, kitchen: r) -> in(egg: o, I)"), } drop = take.inverse(name="drop") actions = set(state.all_instantiations(drop)) assert actions == { Action.parse("drop :: $at(P, kitchen: r) & in(map: o, I) -> in(map: o, kitchen: r)"), } state.apply(*actions) actions = set(state.all_instantiations(drop)) assert len(actions) == 0 # The state is no longer aware of the I variable, so there are no solutions actions = set(state.all_instantiations(take)) assert len(actions) == 0
def build_state(locked_door=False): # Set up a world with two rooms and a few objecs. P = Variable("P") I = Variable("I") bedroom = Variable("bedroom", "r") kitchen = Variable("kitchen", "r") rusty_key = Variable("rusty key", "k") small_key = Variable("small key", "k") wooden_door = Variable("wooden door", "d") chest = Variable("chest", "c") cabinet = Variable("cabinet", "c") robe = Variable("robe", "o") state = State([ Proposition("at", [P, bedroom]), Proposition("south_of", [kitchen, bedroom]), Proposition("north_of", [bedroom, kitchen]), Proposition("link", [bedroom, wooden_door, kitchen]), Proposition("link", [kitchen, wooden_door, bedroom]), Proposition("locked" if locked_door else "closed", [wooden_door]), Proposition("in", [rusty_key, I]), Proposition("match", [rusty_key, chest]), Proposition("locked", [chest]), Proposition("at", [chest, kitchen]), Proposition("in", [small_key, chest]), Proposition("match", [small_key, cabinet]), Proposition("locked", [cabinet]), Proposition("at", [cabinet, bedroom]), Proposition("in", [robe, cabinet]), ]) return state
def test_is_sequence_applicable(): state = State([ Proposition.parse("at(P, r_1: r)"), Proposition.parse("empty(r_2: r)"), Proposition.parse("empty(r_3: r)"), ]) assert state.is_sequence_applicable([ Action.parse("go :: at(P, r_1: r) & empty(r_2: r) -> at(P, r_2: r) & empty(r_1: r)"), Action.parse("go :: at(P, r_2: r) & empty(r_3: r) -> at(P, r_3: r) & empty(r_2: r)"), ]) assert not state.is_sequence_applicable([ Action.parse("go :: at(P, r_1: r) & empty(r_2: r) -> at(P, r_2: r) & empty(r_1: r)"), Action.parse("go :: at(P, r_1: r) & empty(r_3: r) -> at(P, r_3: r) & empty(r_1: r)"), ]) assert not state.is_sequence_applicable([ Action.parse("go :: at(P, r_2: r) & empty(r_3: r) -> at(P, r_3: r) & empty(r_2: r)"), Action.parse("go :: at(P, r_3: r) & empty(r_1: r) -> at(P, r_1: r) & empty(r_3: r)"), ])
def check_action(self, state: State, action: Action) -> bool: """ Check if an action should be allowed in this state. The default implementation disallows actions that would create new facts that don't mention any new variables. Args: state: The current state. action: The action being applied. Returns: Whether that action should be allowed. """ for prop in action.preconditions: if not state.is_fact(prop): if all(state.has_variable(var) for var in prop.arguments): # Don't allow creating new predicates without any new variables return False return True
def test_get_reverse_action(): kb = KnowledgeBase.default() for rule in kb.rules.values(): action = maybe_instantiate_variables(rule, kb.types.constants_mapping.copy(), State([])) r_action = kb.get_reverse_action(action) if rule.name.startswith("eat"): assert r_action is None else: assert r_action is not None # Check if that when applying the reverse rule we can re-obtain # the previous state. state = State(action.preconditions) new_state = state.copy() assert new_state.apply(action) r_state = new_state.copy() r_state.apply(r_action) assert state == r_state
def __init__(self) -> None: """ Creates an empty world, with a player and an empty inventory. """ self._entities = {} self.quests = [] self.rooms = [] self.paths = [] self.globals = { } # global variables hack for entities that must be accessible from anywhere self._types_counts = KnowledgeBase.default().types.count(State()) self.player = self.new(type='P') self.inventory = self.new(type='I') self.grammar = tw_textlabs.generator.make_grammar() self._game = None self._distractors_facts = []
def state(self) -> State: """ Current state of the world. """ facts = [] for room in self.rooms: facts += room.facts for path in self.paths: facts += path.facts for g in self.globals.values(): facts += g.facts facts += self.inventory.facts facts += self._distractors_facts return State(facts)
def __init__(self, surface_generator: SurfaceGenerator) -> None: """ Creates an empty world, with a player and an empty inventory. """ self._entities = {} self.quests = [] self.rooms = [] self.paths = [] self._types_counts = KnowledgeBase.default().types.count(State()) self.player = self.new(type='P') self.inventory = self.new(type='I') self.sg = surface_generator self._game = None self._quests_str = [] self._distractors_facts = [] # define global op types self.globals = {} for t in KnowledgeBase.default().types.descendants('toe'): op_type = self.new(type=t) op_type.add_property('initialized') self.globals[t] = op_type
def build_state(door_state="open"): # Set up a world with two rooms and a few objecs. state = State([ Proposition("at", [P, bedroom]), Proposition("south_of", [kitchen, bedroom]), Proposition("north_of", [bedroom, kitchen]), Proposition("link", [bedroom, wooden_door, kitchen]), Proposition("link", [kitchen, wooden_door, bedroom]), Proposition(door_state, [wooden_door]), # Proposition("in", [rusty_key, I]), Proposition("match", [rusty_key, chest]), Proposition("locked", [chest]), Proposition("at", [chest, kitchen]), Proposition("in", [small_key, chest]), # Proposition("match", [small_key, cabinet]), Proposition("locked", [cabinet]), Proposition("at", [cabinet, bedroom]), Proposition("in", [robe, cabinet]), ]) return state
def test_backward_chaining(): P = Variable("P", "P") room = Variable("room", "r") kitchen = Variable("kitchen", "r") state = State([ Proposition("at", [P, room]), Proposition("north_of", [kitchen, room]), Proposition("south_of", [room, kitchen]), ]) options = ChainingOptions() options.backward = True options.max_depth = 2 options.subquests = True options.create_variables = True options.rules_per_depth = [ [KnowledgeBase.default().rules["take/c"], KnowledgeBase.default().rules["take/s"]], [KnowledgeBase.default().rules["open/c"]], ] options.restricted_types = {"d"} chains = list(get_chains(state, options)) assert len(chains) == 3 options = ChainingOptions() options.backward = True options.max_depth = 3 options.subquests = True options.create_variables = True options.rules_per_depth = [ [KnowledgeBase.default().rules["put"]], [KnowledgeBase.default().rules["go/north"]], [KnowledgeBase.default().rules["take/c"]], ] options.restricted_types = {"d"} chains = list(get_chains(state, options)) assert len(chains) == 3
def try_instantiate(self, state: State, partial: _PartialAction) -> Optional[Action]: """ Try to instantiate a partial action, by creating new variables if necessary. """ rule, mapping = partial.rule, partial.mapping if self.create_variables: type_counts = Counter({ ph.type: len(state.variables_of_type(ph.type)) for ph in rule.placeholders }) for ph in rule.placeholders: if mapping.get(ph) is None: var = self.create_variable(state, ph, type_counts) if var: mapping[ph] = var else: return None return rule.instantiate(mapping)
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 test_state(): state = State() P = Variable.parse("P") kitchen = Variable.parse("kitchen: r") study = Variable.parse("study: r") stove = Variable.parse("stove: o") at_kitchen = Proposition.parse("at(P, kitchen: r)") in_kitchen = Proposition.parse("in(stove: o, kitchen: r)") at_study = Proposition.parse("at(P, study: r)") assert not state.is_fact(at_kitchen) assert not state.is_fact(in_kitchen) assert not state.is_fact(at_study) assert len(state.variables) == 0 assert len(state.variables_of_type("P")) == 0 assert len(state.variables_of_type("r")) == 0 assert len(state.variables_of_type("o")) == 0 state.add_fact(at_kitchen) state.add_fact(in_kitchen) assert state.is_fact(at_kitchen) assert state.is_fact(in_kitchen) assert not state.is_fact(at_study) assert set(state.variables) == {P, kitchen, stove} assert state.variables_of_type("P") == {P} assert state.variables_of_type("r") == {kitchen} assert state.variables_of_type("o") == {stove} state.remove_fact(at_kitchen) assert not state.is_fact(at_kitchen) assert state.is_fact(in_kitchen) assert not state.is_fact(at_study) assert set(state.variables) == {kitchen, stove} assert len(state.variables_of_type("P")) == 0 assert state.variables_of_type("r") == {kitchen} assert state.variables_of_type("o") == {stove} state.remove_fact(in_kitchen) assert not state.is_fact(at_kitchen) assert not state.is_fact(in_kitchen) assert not state.is_fact(at_study) assert len(state.variables) == 0 assert len(state.variables_of_type("P")) == 0 assert len(state.variables_of_type("r")) == 0 assert len(state.variables_of_type("o")) == 0 state.add_fact(at_study) assert not state.is_fact(at_kitchen) assert not state.is_fact(in_kitchen) assert state.is_fact(at_study) assert set(state.variables) == {P, study} assert state.variables_of_type("P") == {P} assert state.variables_of_type("r") == {study} assert len(state.variables_of_type("o")) == 0
class World: def __init__(self) -> None: self._state = State() self._entities = OrderedDict() self._rooms = [] self._objects = [] self._update() self._player_room = None @classmethod def from_facts(cls, facts: List[Proposition]) -> "World": world = cls() world.add_facts(facts) return world @classmethod def deserialize(cls, serialized_facts: List) -> "World": return cls.from_facts( [Proposition.deserialize(f) for f in serialized_facts]) def serialize(self) -> List: return [f.serialize() for f in self.facts] @classmethod def from_map(cls, map: networkx.Graph) -> "World": """ Args: map: Graph defining the structure of the world. """ world = cls() names = [ d.get("name", "r_{}".format(i)) for i, (n, d) in enumerate(map.nodes.items()) ] rooms = OrderedDict( (n, Variable(names[i], "r")) for i, n in enumerate(map.nodes())) world.add_facts(graph2state(map, rooms)) return world @property def player_room(self) -> WorldRoom: return self._player_room @property def rooms(self) -> List[WorldRoom]: return self._rooms @property def objects(self) -> List[WorldObject]: return self._objects @property def entities(self) -> ValuesView[WorldEntity]: return self._entities.values() @property def state(self) -> State: return self._state @state.setter def state(self, state: State) -> None: self._state = State() self.add_facts(state.facts) @property def facts(self) -> List[Proposition]: # Sort the facts for deterministic world generation return sorted(self._state.facts) def add_fact(self, fact: Proposition) -> None: self.add_facts([fact]) def add_facts(self, facts: List[Proposition]) -> None: self._state.add_facts(facts) self._update() # Update the internal representation of the world. def _get_entity(self, var: Variable) -> WorldEntity: if var.name not in self._entities: self._entities[var.name] = WorldEntity.create(var) return self._entities[var.name] def _get_room(self, var: Variable) -> WorldRoom: entity = self._get_entity(var) assert isinstance(entity, WorldRoom) return entity def _get_object(self, var: Variable) -> WorldObject: entity = self._get_entity(var) assert isinstance(entity, WorldObject) return entity def _update(self) -> None: """ Update the internal representation of the world. This method will create new entities based on facts. It should be called whenever backing facts are changed. """ self._entities = OrderedDict() # Clear entities. self.player = self._get_entity(Variable("P")) self.inventory = self._get_entity(Variable("I")) self._player_room = None self._process_rooms() self._process_objects() self._rooms = [ entity for entity in self._entities.values() if isinstance(entity, WorldRoom) ] self._objects = [ entity for entity in self._entities.values() if isinstance(entity, WorldObject) ] self._entities_per_type = defaultdict(list) for entity in self._entities.values(): self._entities_per_type[entity.type].append(entity) def _process_rooms(self) -> None: for fact in self.facts: if not KnowledgeBase.default().types.is_descendant_of( fact.arguments[0].type, 'r'): continue # Skip non room facts. room = self._get_room(fact.arguments[0]) room.add_related_fact(fact) if fact.name.endswith("_of"): # Handle room positioning facts. exit = reverse_direction(fact.name.split("_of")[0]) dest = self._get_room(fact.arguments[1]) dest.add_related_fact(fact) assert exit not in room.exits room.exits[exit] = dest # Handle door link facts. for fact in self.facts: if fact.name != "link": continue src = self._get_room(fact.arguments[0]) door = self._get_object(fact.arguments[1]) dest = self._get_room(fact.arguments[2]) door.add_related_fact(fact) src.content.append(door) exit_found = False for exit, room in src.exits.items(): if dest == room: src.doors[exit] = door exit_found = True break if not exit_found: # Need to position both rooms w.r.t. each other. src_free_exits = [ exit for exit in DIRECTIONS if exit not in src.exits ] for exit in src_free_exits: r_exit = reverse_direction(exit) if r_exit not in dest.exits: src.exits[exit] = dest dest.exits[r_exit] = src src.doors[exit] = door exit_found = True break # Relax the Cartesian grid constraint. if not exit_found: # Need to position both rooms w.r.t. each other. src_free_exits = [ exit for exit in DIRECTIONS if exit not in src.exits ] dest_free_exits = [ exit for exit in DIRECTIONS if exit not in dest.exits ] if len(src_free_exits) > 0 and len(dest_free_exits) > 0: exit = src_free_exits[0] r_exit = dest_free_exits[0] src.exits[exit] = dest dest.exits[r_exit] = src src.doors[exit] = door exit_found = True if not exit_found: # If there is still no exit found. raise NoFreeExitError("Cannot connect {} and {}.".format( src, dest)) def _process_objects(self) -> None: for fact in self.facts: if KnowledgeBase.default().types.is_descendant_of( fact.arguments[0].type, 'r'): continue # Skip room facts. obj = self._get_entity(fact.arguments[0]) obj.add_related_fact(fact) if fact.name == "match": other_obj = self._get_entity(fact.arguments[1]) obj.matching_entity_id = fact.arguments[1].name other_obj.matching_entity_id = fact.arguments[0].name if fact.name in [ "in", "on", "at", "potential", "pot_multi_a", "pot_multi_b" ]: holder = self._get_entity(fact.arguments[1]) holder.content.append(obj) if fact.arguments[0].type == "P": self._player_room = holder def get_facts_in_scope(self) -> List[Proposition]: facts = [] facts += [ fact for exit in self.player_room.exits.values() for fact in exit.related_facts ] facts += [ fact for door in self.player_room.doors.values() for fact in door.related_facts ] facts += [ fact for obj in self.get_visible_objects_in(self.player_room) for fact in obj.related_facts ] facts += [ fact for obj in self.get_objects_in_inventory() for fact in obj.related_facts ] return uniquify(facts) def get_visible_objects_in(self, obj: WorldObject) -> List[WorldObject]: if "locked" in obj.properties or "closed" in obj.properties: return [] objects = list(obj.content) for obj in obj.content: objects += self.get_visible_objects_in(obj) return objects def get_all_objects_in(self, obj: WorldObject) -> List[WorldObject]: objects = list(obj.content) for obj in obj.content: objects += self.get_all_objects_in(obj) return objects def get_objects_in_inventory(self) -> List[WorldObject]: return self.inventory.content def get_entities_per_type(self, type: str) -> List[WorldEntity]: """ Get all entities of a certain type. """ return self._entities_per_type.get(type, []) def find_object_by_id(self, id: str) -> Optional[WorldObject]: return self._entities.get(id) def find_room_by_id(self, id: str) -> Optional[WorldRoom]: return self._entities.get(id) def set_player_room(self, start_room: Union[None, WorldRoom, str] = None) -> None: if start_room is None: if len(self.rooms) == 0: start_room = WorldRoom("r_0", "r") else: start_room = self.rooms[0] elif start_room in self._entities: start_room = self._entities[start_room] elif isinstance(start_room, Variable) and start_room.name in self._entities: start_room = self._entities[start_room.name] else: raise ValueError("Unknown room: {}".format(start_room)) self.add_fact(Proposition("at", [self.player, start_room])) def populate_room( self, nb_objects: int, room: Variable, rng: Optional[RandomState] = None, object_types_probs: Optional[Dict[str, float]] = None ) -> List[Proposition]: rng = g_rng.next() if rng is None else rng state = [] types_counts = KnowledgeBase.default().types.count(self.state) inventory = Variable("I", "I") objects_holder = [inventory, room] locked_or_closed_objects = [] lockable_objects = [] for s in self.facts: # Look for containers and supporters to put stuff in/on them. if s.name == "at" and s.arguments[0].type in [ "c", "s" ] and s.arguments[1].name == room.name: objects_holder.append(s.arguments[0]) # Look for containers and doors without a matching key. if s.name == "at" and s.arguments[0].type in [ "c", "d" ] and s.arguments[1].name == room.name: obj_propositions = [ p.name for p in self.facts if s.arguments[0].name in p.names ] if "match" not in obj_propositions and s.arguments[ 0] not in lockable_objects: lockable_objects.append(s.arguments[0]) if "locked" in obj_propositions or "closed" in obj_propositions: locked_or_closed_objects.append(s.arguments[0]) object_id = 0 while object_id < nb_objects: if len(locked_or_closed_objects) > 0: # Prioritize adding key if there are locked or closed things in the room. obj_type = "k" else: obj_type = KnowledgeBase.default().types.sample( parent_type='t', rng=rng, exceptions=["d", "r"], include_parent=False, probs=object_types_probs) if KnowledgeBase.default().types.is_descendant_of(obj_type, "o"): obj_name = get_new(obj_type, types_counts) obj = Variable(obj_name, obj_type) allowed_objects_holder = list(objects_holder) if obj_type == "k": if len(locked_or_closed_objects) > 0: # Look for a *locked* container or a door. rng.shuffle(locked_or_closed_objects) locked_or_closed_obj = locked_or_closed_objects.pop() state.append( Proposition("match", [obj, locked_or_closed_obj])) lockable_objects.remove(locked_or_closed_obj) # Do not place the key in its own matching container. if locked_or_closed_obj in allowed_objects_holder: allowed_objects_holder.remove(locked_or_closed_obj) elif len(lockable_objects) > 0: # Look for a container or a door. rng.shuffle(lockable_objects) lockable_obj = lockable_objects.pop() state.append(Proposition("match", [obj, lockable_obj])) else: continue # Unuseful key is not allowed. elif obj_type == "f": # HACK: manually add the edible property to food items. state.append(Proposition("edible", [obj])) # Place the object somewhere. obj_holder = rng.choice(allowed_objects_holder) if KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "s"): state.append(Proposition("on", [obj, obj_holder])) elif KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "c"): state.append(Proposition("in", [obj, obj_holder])) elif KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "I"): state.append(Proposition("in", [obj, obj_holder])) elif KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "r"): state.append(Proposition("at", [obj, obj_holder])) else: raise ValueError( "Unknown type for object holder: {}".format( obj_holder)) elif KnowledgeBase.default().types.is_descendant_of(obj_type, "s"): supporter_name = get_new(obj_type, types_counts) supporter = Variable(supporter_name, obj_type) state.append(Proposition("at", [supporter, room])) objects_holder.append(supporter) elif KnowledgeBase.default().types.is_descendant_of(obj_type, "c"): container_name = get_new(obj_type, types_counts) container = Variable(container_name, obj_type) state.append(Proposition("at", [container, room])) objects_holder.append(container) container_state = rng.choice(["open", "closed", "locked"]) state.append(Proposition(container_state, [container])) lockable_objects.append(container) if container_state in ["locked", "closed"]: locked_or_closed_objects.append(container) else: raise ValueError("Unknown object type: {}".format(obj_type)) object_id += 1 self.add_facts(state) return state def populate( self, nb_objects: int, rng: Optional[RandomState] = None, object_types_probs: Optional[Dict[str, float]] = None ) -> List[Proposition]: rng = g_rng.next() if rng is None else rng room_names = [room.id for room in self.rooms] nb_objects_per_room = {room_name: 0 for room_name in room_names} indices = np.arange(len(room_names)) for _ in range(nb_objects): idx = rng.choice(indices) nb_objects_per_room[room_names[idx]] += 1 state = [] for room in self.rooms: state += self.populate_room(nb_objects_per_room[room.id], room, rng, object_types_probs) return state def populate_room_with( self, objects: WorldObject, room: WorldRoom, rng: Optional[RandomState] = None) -> List[Proposition]: rng = g_rng.next() if rng is None else rng state = [] objects_holder = [room] locked_or_closed_objects = [] lockable_objects = [] for s in self.facts: # Look for containers and supporters to put stuff in/on them. if s.name == "at" and s.arguments[0].type in [ "c", "s" ] and s.arguments[1].name == room.name: objects_holder.append(s.arguments[0]) # Look for containers and doors without a matching key. if s.name == "at" and s.arguments[0].type in [ "c", "d" ] and s.arguments[1].name == room.name: obj_propositions = [ p.name for p in self.facts if s.arguments[0].name in p.names ] if "match" not in obj_propositions and s.arguments[ 0] not in lockable_objects: lockable_objects.append(s.arguments[0]) if "locked" in obj_propositions or "closed" in obj_propositions: locked_or_closed_objects.append(s.arguments[0]) remaining_objects_id = list(range(len(objects))) rng.shuffle(remaining_objects_id) for idx in remaining_objects_id: obj = objects[idx] obj_type = obj.type if KnowledgeBase.default().types.is_descendant_of(obj_type, "o"): allowed_objects_holder = list(objects_holder) # Place the object somewhere. obj_holder = rng.choice(allowed_objects_holder) if KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "s"): state.append(Proposition("on", [obj, obj_holder])) elif KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "c"): state.append(Proposition("in", [obj, obj_holder])) elif KnowledgeBase.default().types.is_descendant_of( obj_holder.type, "r"): state.append(Proposition("at", [obj, obj_holder])) else: raise ValueError( "Unknown type for object holder: {}".format( obj_holder)) elif KnowledgeBase.default().types.is_descendant_of(obj_type, "s"): supporter = obj state.append(Proposition("at", [supporter, room])) objects_holder.append(supporter) elif KnowledgeBase.default().types.is_descendant_of(obj_type, "c"): container = obj state.append(Proposition("at", [container, room])) objects_holder.append(container) container_state = rng.choice(["open", "closed", "locked"]) state.append(Proposition(container_state, [container])) lockable_objects.append(container) if container_state in ["locked", "closed"]: locked_or_closed_objects.append(container) else: raise ValueError("Unknown object type: {}".format(obj_type)) self.add_facts(state) return state def populate_with(self, objects: List[WorldObject], rng: Optional[RandomState] = None) -> List[Proposition]: rng = g_rng.next() if rng is None else rng room_names = [room.id for room in self.rooms] nb_objects_per_room = {room_name: 0 for room_name in room_names} indices = np.arange(len(room_names)) for _ in range(len(objects)): idx = rng.choice(indices) nb_objects_per_room[room_names[idx]] += 1 state = [] for room in self.rooms: state += self.populate_room_with( objects[:nb_objects_per_room[room.id]], room, rng) objects = objects[nb_objects_per_room[room.id]:] self.add_facts(state) return state def __eq__(self, other: Any) -> bool: return (isinstance(other, World) and self.state == other.state) def __hash__(self) -> int: return hash(frozenset(self.facts))
def state(self, state: State) -> None: self._state = State() self.add_facts(state.facts)
def test_constraints(): # Declare some variables. P = Variable("P", "P") I = Variable("I", "I") bedroom = Variable("bedroom", "r") kitchen = Variable("kitchen", "r") rusty_key = Variable("rusty key", "k") small_key = Variable("small key", "k") wooden_door = Variable("wooden door", "d") glass_door = Variable("glass door", "d") chest = Variable("chest", "c") cabinet = Variable("cabinet", "c") counter = Variable("counter", "s") robe = Variable("robe", "o") # Make sure the number of basic constraints matches the number # of constraints in constraints.txt basic_constraints = [ k for k in KnowledgeBase.default().constraints.keys() if "-" not in k ] # assert len(basic_constraints) == 32 # Doors can only have one state. door_states = ["open", "closed", "locked"] for door_state in door_states: state = State([Proposition(door_state, [wooden_door])]) assert check_state(state) for door_state2 in door_states: if door_state == door_state2: continue state2 = state.copy() state2.add_fact(Proposition(door_state2, [glass_door])) # New door assert check_state(state2) state2.add_fact(Proposition(door_state2, [wooden_door])) assert not check_state(state2) # Containers can only have one state. container_states = ["open", "closed", "locked"] for container_state in container_states: state = State([Proposition(container_state, [chest])]) assert check_state(state) for container_state2 in container_states: if container_state == container_state2: continue state2 = state.copy() state2.add_fact(Proposition(container_state2, [cabinet])) # New container assert check_state(state2) state2.add_fact(Proposition(container_state2, [chest])) assert not check_state(state2) # A player/supporter/container can only be at one place. for obj in [P, chest, counter]: assert check_state(State([Proposition("at", [obj, kitchen])])) assert check_state(State([Proposition("at", [obj, bedroom])])) assert not check_state( State([ Proposition("at", [obj, kitchen]), Proposition("at", [obj, bedroom]) ])) # An object is either in the player's inventory, in a container or on a supporter obj_locations = [ Proposition("in", [robe, I]), Proposition("in", [robe, chest]), Proposition("on", [robe, counter]) ] for obj_location in obj_locations: assert check_state(State([obj_location])) for obj_location2 in obj_locations: if obj_location == obj_location2: break assert not check_state(State([obj_location, obj_location2 ])), "{}, {}".format( obj_location, obj_location2) # Only one key can match a container and vice-versa. assert check_state(State([Proposition("match", [rusty_key, chest])])) assert not check_state( State([ Proposition("match", [small_key, chest]), Proposition("match", [rusty_key, chest]) ])) assert not check_state( State([ Proposition("match", [small_key, cabinet]), Proposition("match", [small_key, chest]) ])) # Only one key can match a door and vice-versa. assert check_state(State([Proposition("match", [rusty_key, chest])])) assert not check_state( State([ Proposition("match", [small_key, wooden_door]), Proposition("match", [rusty_key, wooden_door]) ])) assert not check_state( State([ Proposition("match", [small_key, glass_door]), Proposition("match", [small_key, wooden_door]) ])) # A door can't be used to link more than two rooms. door = Variable("door", "d") room1 = Variable("room1", "r") room2 = Variable("room2", "r") room3 = Variable("room3", "r") assert not check_state( State([ Proposition("link", [room1, door, room2]), Proposition("link", [room1, door, room3]), ])) door1 = Variable("door1", "d") door2 = Variable("door2", "d") room1 = Variable("room1", "r") room2 = Variable("room2", "r") assert not check_state( State([ Proposition("link", [room1, door1, room2]), Proposition("link", [room1, door2, room2]), ]))
def test_parallel_quests_navigation(): logic = GameLogic.parse(""" type P { } type I { } type r { rules { move :: at(P, r) & $free(r, r') -> at(P, r'); } constraints { atat :: at(P, r) & at(P, r') -> fail(); } } type o { rules { take :: $at(P, r) & at(o, r) -> in(o, I); } constraints { inat :: in(o, I) & at(o, r) -> fail(); } } type flour : o { } type eggs : o { } type cake { rules { bake :: in(flour, I) & in(eggs, I) -> in(cake, I) & in(flour, cake) & in(eggs, cake); } constraints { inincake :: in(o, I) & in(o, cake) -> fail(); atincake :: at(o, r) & in(o, cake) -> fail(); } } """) kb = KnowledgeBase(logic, "") state = State([ Proposition.parse("at(P, r3: r)"), Proposition.parse("free(r2: r, r3: r)"), Proposition.parse("free(r1: r, r2: r)"), ]) bake = [kb.logic.rules["bake"]] non_bake = [r for r in kb.logic.rules.values() if r.name != "bake"] options = ChainingOptions() options.backward = True options.create_variables = True options.min_depth = 3 options.max_depth = 3 options.min_breadth = 2 options.max_breadth = 2 options.kb = kb options.rules_per_depth = [bake, non_bake, non_bake] options.restricted_types = {"P", "r"} chains = list(get_chains(state, options)) assert len(chains) == 2