diff --git a/textworld/envs/glulx/git_glulx_ml.py b/textworld/envs/glulx/git_glulx_ml.py index ea959793..8cd6b9f7 100644 --- a/textworld/envs/glulx/git_glulx_ml.py +++ b/textworld/envs/glulx/git_glulx_ml.py @@ -253,6 +253,11 @@ def command_feedback(self): """ if not hasattr(self, "_command_feedback"): command_feedback = self.feedback + + # On the first move, command_feedback should be empty. + if self.nb_moves == 0: + command_feedback = "" + # Remove room description from command feedback. if len(self.description.strip()) > 0: regex = "\s*" + re.escape(self.description.strip()) + "\s*" @@ -531,7 +536,7 @@ def render(self, mode: str = "human") -> None: # Wrap each paragraph. if mode == "human": paragraphs = msg.split("\n") - paragraphs = ["\n".join(textwrap.wrap(paragraph, width=80)) for paragraph in paragraphs] + paragraphs = ["\n".join(textwrap.wrap(paragraph, width=120)) for paragraph in paragraphs] msg = "\n".join(paragraphs) outfile.write(msg + "\n") diff --git a/textworld/envs/glulx/tests/test_git_glulx_ml.py b/textworld/envs/glulx/tests/test_git_glulx_ml.py index 5e1d706e..ca1a93f9 100644 --- a/textworld/envs/glulx/tests/test_git_glulx_ml.py +++ b/textworld/envs/glulx/tests/test_git_glulx_ml.py @@ -40,10 +40,11 @@ def build_test_game(): path.door.add_property("open") carrot = M.new(type='f', name='carrot') + M.add_fact("edible", carrot) M.inventory.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) diff --git a/textworld/generator/chaining.py b/textworld/generator/chaining.py index 6b5f8ddb..b589a95a 100644 --- a/textworld/generator/chaining.py +++ b/textworld/generator/chaining.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT license. - +import re from collections import Counter from functools import total_ordering from numpy.random import RandomState @@ -103,7 +103,7 @@ def __init__(self): self.subquests = False self.independent_chains = False self.create_variables = False - self.fixed_mapping = data.get_types().constants_mapping + self.fixed_mapping = {Placeholder(t.name): Variable(t.name) for t in data.get_types() if t.constant} self.rng = None self.logic = data.get_logic() self.rules_per_depth = [] @@ -365,9 +365,9 @@ def check_action(self, node: _Node, state: State, action: Action) -> bool: while nav_parent.action is not None and self._is_navigation(nav_parent.action): # HACK: Going through a door is always considered navigation unless the previous action was to open that door. parent = nav_parent.parent - if parent.action is not None and parent.action.name == "open/d": + if parent.action is not None and self._is_open_door(parent.action): break - if self.backward and action.name == "open/d": + if self.backward and self._is_open_door(action): break nav_parent = parent @@ -389,7 +389,10 @@ def check_action(self, node: _Node, state: State, action: Action) -> bool: return self.options.check_action(state, action) def _is_navigation(self, action): - return action.name.startswith("go/") + return re.match("go\(.*\).*", action.name) + + def _is_open_door(self, action): + return re.match("open\(d\).*", action.name) def apply(self, node: _Node, action: Action) -> Optional[State]: """Attempt to apply an action to the given state.""" diff --git a/textworld/generator/data/__init__.py b/textworld/generator/data/__init__.py index fbfe1f8a..696db996 100644 --- a/textworld/generator/data/__init__.py +++ b/textworld/generator/data/__init__.py @@ -10,7 +10,6 @@ from typing import Optional from textworld.logic import GameLogic -from textworld.generator.vtypes import VariableType, VariableTypeTree from textworld.utils import maybe_mkdir, RegexDict BUILTIN_DATA_PATH = os.path.dirname(__file__) @@ -82,17 +81,17 @@ def create_data_files(dest: str = './textworld_data', verbose: bool = False, for INFORM7_ADDONS_CODE = None -def _to_type_tree(types): - vtypes = [] +# def _to_type_tree(types): +# vtypes = [] - for vtype in sorted(types): - if vtype.parents: - parent = vtype.parents[0] - else: - parent = None - vtypes.append(VariableType(vtype.name, vtype.name, parent)) +# for vtype in sorted(types): +# if vtype.parents: +# parent = vtype.parents[0] +# else: +# parent = None +# vtypes.append(VariableType(vtype.name, vtype.name, parent)) - return VariableTypeTree(vtypes) +# return VariableTypeTree(vtypes) def _to_regex_dict(rules): @@ -134,7 +133,8 @@ def load_logic(target_dir: str): logic = GameLogic.load(paths) global _TYPES - _TYPES = _to_type_tree(logic.types) + # _TYPES = _to_type_tree(logic.types) + _TYPES = logic.types global _RULES _RULES = _to_regex_dict(logic.rules.values()) @@ -197,13 +197,57 @@ def get_constraints(): def get_reverse_rules(action): + assert False, "deprecated" # XXX return _REVERSE_RULES(action) +def get_reverse_action(action): + r_action = action.inverse() + for rule in get_rules().values(): + r_action.name = rule.name + if rule.match(r_action): + return r_action + + return None + + def get_types(): return _TYPES + +def sample_type(parent_type, rng, exceptions=[], include_parent=True, probs=None): + """ Sample an object type given the parent's type. """ + import numpy as np + types = [t.name for t in get_types().get(parent_type).descendants] + if include_parent: + types = [parent_type] + types + types = [t for t in types if t not in exceptions] + + if probs is not None: + probs = np.array([probs[t] for t in types], dtype="float") + probs /= np.sum(probs) + + return rng.choice(types, p=probs) + + +def count_types(state): + """ Counts how many objects there are of each type. """ + types_counts = {t.name: 0 for t in get_types()} + for var in state.variables: + if get_types().get(var.type).constant: + continue + + if "_" not in var.name: + continue + + cpt = int(var.name.split("_")[-1]) + var_type = var.type + types_counts[var_type] = max(cpt + 1, types_counts[var_type]) + + return types_counts + + def get_data_path(): return _DATA_PATH diff --git a/textworld/generator/data/logic/box.twl b/textworld/generator/data/logic/box.twl new file mode 100644 index 00000000..50f27f74 --- /dev/null +++ b/textworld/generator/data/logic/box.twl @@ -0,0 +1,31 @@ +# A type of container that is portable. +type box : t, container, portable, openable { + predicates { + reachable_contents(box) = reachable(box) & open(box); + reachable(box) = in(box, I); + reachable(box) = at(P, r) & at(box, r); + reachable(box) = reachable(table) & on(box, table); + reachable(box) = reachable_contents(chest) & in(box, chest); + } + + constraints { + no_nested_boxes :: in(x: box, y: box) -> fail(); + no_box_on_stool :: on(box, stool) -> fail(); + } + + inform7 { + type { + kind :: "box-like"; + } + + code :: """ + [Avoid nesting box-like objects because of some limitation with alias cycles.] + Instead of inserting a box-like (called source) into a box-like (called dest): + say "You cannot insert [the source] in [the dest]!"; + + Instead of putting a box-like (called source) on a stool-like (called dest): + say "You cannot put [the source] on [the dest]!"; + """; + + } +} diff --git a/textworld/generator/data/logic/chest.twl b/textworld/generator/data/logic/chest.twl new file mode 100644 index 00000000..cc6715ab --- /dev/null +++ b/textworld/generator/data/logic/chest.twl @@ -0,0 +1,21 @@ +# A type of container that is fixed in place and lockable. +type chest : t, container, fixed_in_place, lockable { + predicates { + reachable_contents(chest) = reachable(chest) & open(chest); + } + + inform7 { + type { + kind :: "chest-like"; + } + } +} + +## Alias for backward compatibility. +#type c : chest { +# inform7 { +# type { +# kind :: "old-c"; +# } +# } +#} \ No newline at end of file diff --git a/textworld/generator/data/logic/container.twl b/textworld/generator/data/logic/container.twl index 339ef3bd..d4046bac 100644 --- a/textworld/generator/data/logic/container.twl +++ b/textworld/generator/data/logic/container.twl @@ -1,52 +1,17 @@ -# container -type c : t { +# Property of an object that can contain other objects. +type container { predicates { - open(c); - closed(c); - locked(c); - - in(o, c); - } - - rules { - lock/c :: $at(P, r) & $at(c, r) & $in(k, I) & $match(k, c) & closed(c) -> locked(c); - unlock/c :: $at(P, r) & $at(c, r) & $in(k, I) & $match(k, c) & locked(c) -> closed(c); - - open/c :: $at(P, r) & $at(c, r) & closed(c) -> open(c); - close/c :: $at(P, r) & $at(c, r) & open(c) -> closed(c); - } - - reverse_rules { - lock/c :: unlock/c; - open/c :: close/c; - } - - constraints { - c1 :: open(c) & closed(c) -> fail(); - c2 :: open(c) & locked(c) -> fail(); - c3 :: closed(c) & locked(c) -> fail(); + in(portable, container); } inform7 { type { - kind :: "container"; - definition :: "containers are openable, lockable and fixed in place. containers are usually closed."; + kind :: ""; + definition :: "It is a kind of container."; } predicates { - open(c) :: "The {c} is open"; - closed(c) :: "The {c} is closed"; - locked(c) :: "The {c} is locked"; - - in(o, c) :: "The {o} is in the {c}"; - } - - commands { - open/c :: "open {c}" :: "opening the {c}"; - close/c :: "close {c}" :: "closing the {c}"; - - lock/c :: "lock {c} with {k}" :: "locking the {c} with the {k}"; - unlock/c :: "unlock {c} with {k}" :: "unlocking the {c} with the {k}"; + in(portable, container) :: "The {portable} is in the {container}"; } } } diff --git a/textworld/generator/data/logic/door.twl b/textworld/generator/data/logic/door.twl index 54613ab5..a87a21d0 100644 --- a/textworld/generator/data/logic/door.twl +++ b/textworld/generator/data/logic/door.twl @@ -1,31 +1,31 @@ # door -type d : t { +type d : t, lockable { predicates { - open(d); - closed(d); - locked(d); - link(r, d, r); + + north_of/d(r, d, r); + west_of/d(r, d, r); + + south_of/d(r, d, r') = north_of/d(r', d, r); + east_of/d(r, d, r') = west_of/d(r', d, r); + + reachable(d) = at(P, r) & link(r, d, r') & link(r', d, r); } rules { - lock/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & $in(k, I) & $match(k, d) & closed(d) -> locked(d); - unlock/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & $in(k, I) & $match(k, d) & locked(d) -> closed(d); + lock(d, k) :: $reachable(d) & $in(k, I) & $match(k, d) & closed(d) -> locked(d); + unlock(d, k) :: $reachable(d) & $in(k, I) & $match(k, d) & locked(d) -> closed(d); - open/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & closed(d) -> open(d) & free(r, r') & free(r', r); - close/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & open(d) & free(r, r') & free(r', r) -> closed(d); + open(d) :: $reachable(d) & closed(d) -> open(d) & free(r, r') & free(r', r); + close(d) :: $reachable(d) & open(d) & free(r, r') & free(r', r) -> closed(d); } reverse_rules { - lock/d :: unlock/d; - open/d :: close/d; + lock(d, k) :: unlock(d, k); + open(d) :: close(d); } constraints { - d1 :: open(d) & closed(d) -> fail(); - d2 :: open(d) & locked(d) -> fail(); - d3 :: closed(d) & locked(d) -> fail(); - # A door can't be used to link more than two rooms. link1 :: link(r, d, r') & link(r, d, r'') -> fail(); link2 :: link(r, d, r') & link(r'', d, r''') -> fail(); @@ -35,7 +35,7 @@ type d : t { # There cannot be more than four doors in a room. too_many_doors :: link(r, d1: d, r1: r) & link(r, d2: d, r2: r) & link(r, d3: d, r3: r) & link(r, d4: d, r4: r) & link(r, d5: d, r5: r) -> fail(); - + # There cannot be more than four doors in a room. dr1 :: free(r, r1: r) & link(r, d2: d, r2: r) & link(r, d3: d, r3: r) & link(r, d4: d, r4: r) & link(r, d5: d, r5: r) -> fail(); dr2 :: free(r, r1: r) & free(r, r2: r) & link(r, d3: d, r3: r) & link(r, d4: d, r4: r) & link(r, d5: d, r5: r) -> fail(); @@ -52,18 +52,19 @@ type d : t { definition :: "door is openable and lockable."; } - predicates { - open(d) :: "The {d} is open"; - closed(d) :: "The {d} is closed"; - locked(d) :: "The {d} is locked"; - } - commands { - open/d :: "open {d}" :: "opening {d}"; - close/d :: "close {d}" :: "closing {d}"; + open(d) :: "open {d}" :: "opening {d}"; + close(d) :: "close {d}" :: "closing {d}"; - unlock/d :: "unlock {d} with {k}" :: "unlocking {d} with the {k}"; - lock/d :: "lock {d} with {k}" :: "locking {d} with the {k}"; + unlock(d, k) :: "unlock {d} with {k}" :: "unlocking {d} with the {k}"; + lock(d, k) :: "lock {d} with {k}" :: "locking {d} with the {k}"; + } + + predicates { + north_of/d(r, d, r') :: "South of {r} and north of {r'} is a door called {d}"; + south_of/d(r, d, r') :: "North of {r} and south of {r'} is a door called {d}"; + east_of/d(r, d, r') :: "West of {r} and east of {r'} is a door called {d}"; + west_of/d(r, d, r') :: "East of {r} and west of {r'} is a door called {d}"; } } } diff --git a/textworld/generator/data/logic/edible.twl b/textworld/generator/data/logic/edible.twl new file mode 100644 index 00000000..4cb0a845 --- /dev/null +++ b/textworld/generator/data/logic/edible.twl @@ -0,0 +1,31 @@ +# Property of an object that can eaten. +type edible { + predicates { + edible(edible); + eaten(edible); + } + + rules { + eat(edible) :: in(edible, I) & edible(edible) -> eaten(edible); + } + + constraints { + eaten_thing_is_unreachable :: reachable(edible) & eaten(edible) -> fail(); + } + + inform7 { + type { + kind :: ""; + definition :: ""; + } + + predicates { + edible(edible) :: "The {edible} is edible"; + eaten(edible) :: "The {edible} is nowhere"; + } + + commands { + eat(edible) :: "eat {edible}" :: "eating the {edible}"; + } + } +} diff --git a/textworld/generator/data/logic/fixed_in_place.twl b/textworld/generator/data/logic/fixed_in_place.twl new file mode 100644 index 00000000..81eef1f9 --- /dev/null +++ b/textworld/generator/data/logic/fixed_in_place.twl @@ -0,0 +1,17 @@ +# portable +type fixed_in_place { + predicates { + reachable(fixed_in_place) = at(P, r) & at(fixed_in_place, r); + } + + constraints { + single_location :: at(fixed_in_place, r) & at(fixed_in_place, r') -> fail(); + } + + inform7 { + type { + kind :: ""; + definition :: "It is usually fixed in place."; + } + } +} diff --git a/textworld/generator/data/logic/food.twl b/textworld/generator/data/logic/food.twl index 5518c321..8d79151b 100644 --- a/textworld/generator/data/logic/food.twl +++ b/textworld/generator/data/logic/food.twl @@ -1,34 +1,8 @@ -# food -type f : o { - predicates { - edible(f); - eaten(f); - } - - rules { - eat :: in(f, I) -> eaten(f); - } - - constraints { - eaten1 :: eaten(f) & in(f, I) -> fail(); - eaten2 :: eaten(f) & in(f, c) -> fail(); - eaten3 :: eaten(f) & on(f, s) -> fail(); - eaten4 :: eaten(f) & at(f, r) -> fail(); - } - +# Food: a thing that can be eaten and carried by the player. +type f : t, portable, edible { inform7 { type { kind :: "food"; - definition :: "food is edible."; - } - - predicates { - edible(f) :: "The {f} is edible"; - eaten(f) :: "The {f} is nowhere"; - } - - commands { - eat :: "eat {f}" :: "eating the {f}"; } } } diff --git a/textworld/generator/data/logic/inventory.twl b/textworld/generator/data/logic/inventory.twl index 8b2eca09..058a5250 100644 --- a/textworld/generator/data/logic/inventory.twl +++ b/textworld/generator/data/logic/inventory.twl @@ -1,40 +1,40 @@ # Inventory type I { predicates { - in(o, I); + in(portable, I); } rules { - take :: $at(P, r) & at(o, r) -> in(o, I); - drop :: $at(P, r) & in(o, I) -> at(o, r); + take(portable, r) :: $at(P, r) & at(portable, r) -> in(portable, I); + drop(portable, r) :: $at(P, r) & in(portable, I) -> at(portable, r); - take/c :: $at(P, r) & $at(c, r) & $open(c) & in(o, c) -> in(o, I); - insert :: $at(P, r) & $at(c, r) & $open(c) & in(o, I) -> in(o, c); + take(portable, container) :: $reachable_contents(container) & in(portable, container) -> in(portable, I); + insert(portable, container) :: $reachable_contents(container) & in(portable, I) -> in(portable, container); - take/s :: $at(P, r) & $at(s, r) & on(o, s) -> in(o, I); - put :: $at(P, r) & $at(s, r) & in(o, I) -> on(o, s); + take(portable, supporter) :: $reachable(supporter) & on(portable, supporter) -> in(portable, I); + put(portable, supporter) :: $reachable(supporter) & in(portable, I) -> on(portable, supporter); } reverse_rules { - take :: drop; - take/c :: insert; - take/s :: put; + take(portable, r) :: drop(portable, r); + take(portable, container) :: insert(portable, container); + take(portable, supporter) :: put(portable, supporter); } inform7 { predicates { - in(o, I) :: "The player carries the {o}"; + in(portable, I) :: "The player carries the {portable}"; } commands { - take :: "take {o}" :: "taking the {o}"; - drop :: "drop {o}" :: "dropping the {o}"; + take(portable, r) :: "take {portable}" :: "taking the {portable}"; + drop(portable, r) :: "drop {portable}" :: "dropping the {portable}"; - take/c :: "take {o} from {c}" :: "removing the {o} from the {c}"; - insert :: "insert {o} into {c}" :: "inserting the {o} into the {c}"; + take(portable, container) :: "take {portable} from {container}" :: "removing the {portable} from the {container}"; + insert(portable, container) :: "insert {portable} into {container}" :: "inserting the {portable} into the {container}"; - take/s :: "take {o} from {s}" :: "removing the {o} from the {s}"; - put :: "put {o} on {s}" :: "putting the {o} on the {s}"; + take(portable, supporter) :: "take {portable} from {supporter}" :: "removing the {portable} from the {supporter}"; + put(portable, supporter) :: "put {portable} on {supporter}" :: "putting the {portable} on the {supporter}"; inventory :: "inventory" :: "taking inventory"; } diff --git a/textworld/generator/data/logic/key.twl b/textworld/generator/data/logic/key.twl index ff6d0499..7b2208b7 100644 --- a/textworld/generator/data/logic/key.twl +++ b/textworld/generator/data/logic/key.twl @@ -1,15 +1,12 @@ # key type k : o { predicates { - match(k, c); - match(k, d); + match(k, lockable); } constraints { - k1 :: match(k, c) & match(k', c) -> fail(); - k2 :: match(k, c) & match(k, c') -> fail(); - k3 :: match(k, d) & match(k', d) -> fail(); - k4 :: match(k, d) & match(k, d') -> fail(); + k1 :: match(k, lockable) & match(k', lockable) -> fail(); + k2 :: match(k, lockable) & match(k, lockable') -> fail(); } inform7 { @@ -18,8 +15,7 @@ type k : o { } predicates { - match(k, c) :: "The matching key of the {c} is the {k}"; - match(k, d) :: "The matching key of the {d} is the {k}"; + match(k, lockable) :: "The matching key of the {lockable} is the {k}"; } } } diff --git a/textworld/generator/data/logic/lockable.twl b/textworld/generator/data/logic/lockable.twl new file mode 100644 index 00000000..6bc4093d --- /dev/null +++ b/textworld/generator/data/logic/lockable.twl @@ -0,0 +1,36 @@ +# Property of an object that can be locked/unlocked. +type lockable : openable { + predicates { + locked(lockable); + } + + rules { + lock(lockable, k) :: $reachable(lockable) & $in(k, I) & $match(k, lockable) & closed(lockable) -> locked(lockable); + unlock(lockable, k) :: $reachable(lockable) & $in(k, I) & $match(k, lockable) & locked(lockable) -> closed(lockable); + } + + reverse_rules { + lock(lockable, k) :: unlock(lockable, k); + } + + constraints { + open_and_locked :: open(lockable) & locked(lockable) -> fail(); + closed_and_locked :: closed(lockable) & locked(lockable) -> fail(); + } + + inform7 { + type { + kind :: ""; + definition :: "It is lockable. It is usually unlocked."; + } + + predicates { + locked(lockable) :: "The {lockable} is locked"; + } + + commands { + unlock(lockable, k) :: "unlock {lockable} with {k}" :: "unlocking the {lockable} with the {k}"; + lock(lockable, k) :: "lock {lockable} with {k}" :: "locking the {lockable} with the {k}"; + } + } +} diff --git a/textworld/generator/data/logic/object.twl b/textworld/generator/data/logic/object.twl index 19a47080..0a0925f2 100644 --- a/textworld/generator/data/logic/object.twl +++ b/textworld/generator/data/logic/object.twl @@ -1,21 +1,9 @@ # object -type o : t { - constraints { - obj1 :: in(o, I) & in(o, c) -> fail(); - obj2 :: in(o, I) & on(o, s) -> fail(); - obj3 :: in(o, I) & at(o, r) -> fail(); - obj4 :: in(o, c) & on(o, s) -> fail(); - obj5 :: in(o, c) & at(o, r) -> fail(); - obj6 :: on(o, s) & at(o, r) -> fail(); - obj7 :: at(o, r) & at(o, r') -> fail(); - obj8 :: in(o, c) & in(o, c') -> fail(); - obj9 :: on(o, s) & on(o, s') -> fail(); - } +type o : t, portable { inform7 { type { - kind :: "object-like"; - definition :: "object-like is portable."; + kind :: "portable-thing"; } } } diff --git a/textworld/generator/data/logic/openable.twl b/textworld/generator/data/logic/openable.twl new file mode 100644 index 00000000..870ec548 --- /dev/null +++ b/textworld/generator/data/logic/openable.twl @@ -0,0 +1,37 @@ +# Property of an object that can be opened/closed. +type openable { + predicates { + open(openable); + closed(openable); + } + + rules { + open(openable) :: $reachable(openable) & closed(openable) -> open(openable); + close(openable) :: $reachable(openable) & open(openable) -> closed(openable); + } + + reverse_rules { + open(openable) :: close(openable); + } + + constraints { + open_and_closed :: open(openable) & closed(openable) -> fail(); + } + + inform7 { + type { + kind :: ""; + definition :: "It is openable. It is usually closed."; + } + + predicates { + open(openable) :: "The {openable} is open"; + closed(openable) :: "The {openable} is closed"; + } + + commands { + open(openable) :: "open {openable}" :: "opening the {openable}"; + close(openable) :: "close {openable}" :: "closing the {openable}"; + } + } +} diff --git a/textworld/generator/data/logic/portable.twl b/textworld/generator/data/logic/portable.twl new file mode 100644 index 00000000..3d1ea468 --- /dev/null +++ b/textworld/generator/data/logic/portable.twl @@ -0,0 +1,28 @@ +# portable +type portable { + predicates { + reachable(portable) = in(portable, I); + reachable(portable) = at(P, r) & at(portable, r); + reachable(portable) = reachable_contents(container) & in(portable, container); + reachable(portable) = reachable(supporter) & on(portable, supporter); + } + + constraints { + obj1 :: in(portable, I) & in(portable, container) -> fail(); + obj2 :: in(portable, I) & on(portable, supporter) -> fail(); + obj3 :: in(portable, I) & at(portable, r) -> fail(); + obj4 :: in(portable, container) & on(portable, supporter) -> fail(); + obj5 :: in(portable, container) & at(portable, r) -> fail(); + obj6 :: on(portable, supporter) & at(portable, r) -> fail(); + obj7 :: at(portable, r) & at(portable, r') -> fail(); + obj8 :: in(portable, container) & in(portable, container') -> fail(); + obj9 :: on(portable, supporter) & on(portable, supporter') -> fail(); + } + + inform7 { + type { + kind :: ""; + definition :: "It is usually portable."; + } + } +} diff --git a/textworld/generator/data/logic/room.twl b/textworld/generator/data/logic/room.twl index 0393c487..c6936016 100644 --- a/textworld/generator/data/logic/room.twl +++ b/textworld/generator/data/logic/room.twl @@ -7,34 +7,26 @@ type r { north_of(r, r); west_of(r, r); - north_of/d(r, d, r); - west_of/d(r, d, r); - free(r, r); south_of(r, r') = north_of(r', r); east_of(r, r') = west_of(r', r); - - south_of/d(r, d, r') = north_of/d(r', d, r); - east_of/d(r, d, r') = west_of/d(r', d, r); } rules { - go/north :: at(P, r) & $north_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); - go/south :: at(P, r) & $south_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); - go/east :: at(P, r) & $east_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); - go/west :: at(P, r) & $west_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); + go(north) :: at(P, r) & $north_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); + go(south) :: at(P, r) & $south_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); + go(east) :: at(P, r) & $east_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); + go(west) :: at(P, r) & $west_of(r', r) & $free(r, r') & $free(r', r) -> at(P, r'); } reverse_rules { - go/north :: go/south; - go/west :: go/east; + go(north) :: go(south); + go(west) :: go(east); } constraints { r1 :: at(P, r) & at(P, r') -> fail(); - r2 :: at(s, r) & at(s, r') -> fail(); - r3 :: at(c, r) & at(c, r') -> fail(); # An exit direction can only lead to one room. nav_rr1 :: north_of(r, r') & north_of(r'', r') -> fail(); @@ -49,6 +41,9 @@ type r { nav_rrD :: south_of(r, r') & west_of(r, r') -> fail(); nav_rrE :: south_of(r, r') & east_of(r, r') -> fail(); nav_rrF :: west_of(r, r') & east_of(r, r') -> fail(); + + # A room can have up to four exits + free5 :: free(r, r') & free(r, r'') & free(r, r''') & free(r, r'''') & free(r, r''''') -> fail(); } inform7 { @@ -64,18 +59,13 @@ type r { south_of(r, r') :: "The {r} is mapped south of {r'}"; east_of(r, r') :: "The {r} is mapped east of {r'}"; west_of(r, r') :: "The {r} is mapped west of {r'}"; - - north_of/d(r, d, r') :: "South of {r} and north of {r'} is a door called {d}"; - south_of/d(r, d, r') :: "North of {r} and south of {r'} is a door called {d}"; - east_of/d(r, d, r') :: "West of {r} and east of {r'} is a door called {d}"; - west_of/d(r, d, r') :: "East of {r} and west of {r'} is a door called {d}"; } commands { - go/north :: "go north" :: "going north"; - go/south :: "go south" :: "going south"; - go/east :: "go east" :: "going east"; - go/west :: "go west" :: "going west"; + go(north) :: "go north" :: "going north"; + go(south) :: "go south" :: "going south"; + go(east) :: "go east" :: "going east"; + go(west) :: "go west" :: "going west"; } } } diff --git a/textworld/generator/data/logic/stool.twl b/textworld/generator/data/logic/stool.twl new file mode 100644 index 00000000..15c838c2 --- /dev/null +++ b/textworld/generator/data/logic/stool.twl @@ -0,0 +1,29 @@ +# A type of supporter that is portable. +type stool : t, supporter, portable { + predicates { + reachable(stool) = in(stool, I); + reachable(stool) = at(P, r) & at(stool, r); + reachable(stool) = reachable(table) & on(stool, table); + reachable(stool) = reachable_contents(chest) & in(stool, chest); + } + + constraints { + no_nested_stools :: on(x: stool, y: stool) -> fail(); + no_stool_in_box :: in(stool, box) -> fail(); + } + + inform7 { + type { + kind :: "stool-like"; + } + + code :: """ + [Avoid nesting stool-like objects because of some limitation with alias cycles.] + Instead of putting a stool-like (called source) on a stool-like (called dest): + say "You cannot put [the source] on [the dest]!"; + + Instead of inserting a stool-like (called source) into a box-like (called dest): + say "You cannot insert [the source] in [the dest]!"; + """; + } +} diff --git a/textworld/generator/data/logic/supporter.twl b/textworld/generator/data/logic/supporter.twl index e740dd41..962d5756 100644 --- a/textworld/generator/data/logic/supporter.twl +++ b/textworld/generator/data/logic/supporter.twl @@ -1,17 +1,17 @@ -# supporter -type s : t { +# Property of an object that can support other objects. +type supporter { predicates { - on(o, s); + on(portable, supporter); } inform7 { type { - kind :: "supporter"; - definition :: "supporters are fixed in place."; + kind :: ""; + definition :: "It is a kind of supporter."; } predicates { - on(o, s) :: "The {o} is on the {s}"; + on(portable, supporter) :: "The {portable} is on the {supporter}"; } } } diff --git a/textworld/generator/data/logic/table.twl b/textworld/generator/data/logic/table.twl new file mode 100644 index 00000000..b0e85252 --- /dev/null +++ b/textworld/generator/data/logic/table.twl @@ -0,0 +1,17 @@ +# A type of supporter that is fixed in place. +type table : t, supporter, fixed_in_place { + inform7 { + type { + kind :: "table-like"; + } + } +} + +## Alias for backward compatibility. +#type s : table { +# inform7 { +# type { +# kind :: "old-s"; +# } +# } +#} diff --git a/textworld/generator/data/tests/test_box.py b/textworld/generator/data/tests/test_box.py new file mode 100644 index 00000000..924fa561 --- /dev/null +++ b/textworld/generator/data/tests/test_box.py @@ -0,0 +1,188 @@ +from os.path import join as pjoin + +import textworld + +from textworld import g_rng # Global random generator. +from textworld import GameMaker +from textworld.utils import make_temp_directory + + +def test_box(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + + obj1 = M.new("o", name="obj1") + obj2 = M.new("o", name="obj2") + box = M.new("box", name="box") + M.add_fact("open", box) + box.add(obj2) + M.inventory.add(box, obj1) + + with make_temp_directory(prefix="test_box") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + # box in the inventory + # open/close/insert into/take from + # insert/drop/put portable object from the box (not allowed) + # drop box + assert "insert obj1 into box" in game_state.admissible_commands + game_state, _, _ = env.step("insert obj1 into box") + assert " an obj1" in game_state.inventory + + assert "drop obj1" not in game_state.admissible_commands + game_state, _, _ = env.step("drop obj1") + assert " an obj1" in game_state.inventory + + assert "close box" in game_state.admissible_commands + game_state, _, _ = env.step("close box") + assert "close the box" in game_state.feedback + assert " an obj1" not in game_state.inventory + + assert "take obj1 from box" not in game_state.admissible_commands + game_state, _, _ = env.step("take obj1 from box") + assert "obj1" not in game_state.inventory + + assert "open box" in game_state.admissible_commands + game_state, _, _ = env.step("open box") + assert "open the box" in game_state.feedback + + assert "take obj1 from box" in game_state.admissible_commands + game_state, _, _ = env.step("take obj1 from box") + assert " an obj1" not in game_state.inventory + assert " an obj1" in game_state.inventory + + assert "drop box" in game_state.admissible_commands + game_state, _, _ = env.step("drop box") + assert "obj2" not in game_state.inventory + assert "box" not in game_state.inventory + + # box on the floor + # open/close/insert into box/take from the box + # take the box + assert "take obj2 from box" in game_state.admissible_commands + game_state, _, _ = env.step("take obj2 from box") + assert "obj2" in game_state.inventory + + assert "insert obj1 into box" in game_state.admissible_commands + game_state, _, _ = env.step("insert obj1 into box") + assert "obj1" not in game_state.inventory + + assert "close box" in game_state.admissible_commands + game_state, _, _ = env.step("close box") + assert "take obj1 from box" not in game_state.admissible_commands + + assert "take box" in game_state.admissible_commands + game_state, _, _ = env.step("take box") + assert "box" in game_state.inventory + + +def test_box2(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + + obj1 = M.new("o", name="obj1") + table = M.new("table", name="table") + chest = M.new("chest", name="chest") + box = M.new("box", name="box") + M.add_fact("closed", chest) + M.add_fact("closed", box) + chest.add(box) + room.add(chest, table) + box.add(obj1) + + with make_temp_directory(prefix="test_box") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + # box in a fixed in place container aka a chest + # open/close/insert into/take from + assert "open box" not in game_state.admissible_commands + game_state, _, _ = env.step("open box") + assert "can't" in game_state.feedback + + assert "open chest" in game_state.admissible_commands + game_state, _, _ = env.step("open chest") + + assert "open box" in game_state.admissible_commands + game_state, _, _ = env.step("open box") + + game_state, _, _ = env.step("examine box") + assert "obj1" in game_state.feedback + + assert "take obj1 from box" in game_state.admissible_commands + game_state, _, _ = env.step("take obj1 from box") + assert "obj1" in game_state.inventory + + assert "take box from chest" in game_state.admissible_commands + game_state, _, _ = env.step("take box from chest") + assert "box" in game_state.inventory + + assert "put box on table" in game_state.admissible_commands + game_state, _, _ = env.step("put box on table") + assert "box" not in game_state.inventory + + # box in a fixed in place supporter aka a table + # open/close/insert into box/take from the box + # take the box + assert "insert obj1 into box" in game_state.admissible_commands + game_state, _, _ = env.step("insert obj1 into box") + assert "obj1" not in game_state.inventory + + assert "close box" in game_state.admissible_commands + game_state, _, _ = env.step("close box") + assert "take obj1 from box" not in game_state.admissible_commands + + +def test_nested_boxes(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + + doll1 = M.new("box", name="big Russian doll") + doll2 = M.new("box", name="medium Russian doll") + doll3 = M.new("box", name="small Russian doll") + ball = M.new("o", name="ball") + stool = M.new("stool", name="stool") + + M.add_fact("open", doll1) + M.add_fact("open", doll2) + M.add_fact("closed", doll3) + room.add(doll1) + room.add(stool) + M.inventory.add(doll2) + M.inventory.add(doll3) + M.inventory.add(ball) + + with make_temp_directory(prefix="test_nested_boxes") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + assert "insert small Russian doll into medium Russian doll" not in game_state.admissible_commands + game_state, _, _ = env.step("insert small Russian doll into medium Russian doll") + assert "cannot insert" in game_state.feedback + assert "small Russian doll" in game_state.inventory + + assert "insert small Russian doll into big Russian doll" not in game_state.admissible_commands + game_state, _, _ = env.step("insert small Russian doll into big Russian doll") + assert "cannot insert" in game_state.feedback + assert "small Russian doll" in game_state.inventory + + assert "put small Russian doll on stool" not in game_state.admissible_commands + game_state, _, _ = env.step("put small Russian doll on stool") + assert "cannot put" in game_state.feedback + assert "small Russian doll" in game_state.inventory diff --git a/textworld/generator/data/tests/test_chest.py b/textworld/generator/data/tests/test_chest.py new file mode 100644 index 00000000..5f609614 --- /dev/null +++ b/textworld/generator/data/tests/test_chest.py @@ -0,0 +1,48 @@ +from os.path import join as pjoin + +import textworld + +from textworld import g_rng # Global random generator. +from textworld import GameMaker +from textworld.utils import make_temp_directory + + +def test_chest(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + bank = M.new_room("Bank") + M.set_player(bank) + + money = M.new("o", name="gold bar") + safe = M.new("chest", name="safe") + M.add_fact("closed", safe) + bank.add(safe) + safe.add(money) + + with make_temp_directory(prefix="test_chest") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + assert "open safe" in game_state.admissible_commands + game_state, _, _ = env.step("open safe") + assert "You open the" in game_state.feedback + assert "open safe" not in game_state.admissible_commands + assert "close safe" in game_state.admissible_commands + game_state, _, _ = env.step("close safe") + assert "You close the" in game_state.feedback + assert "close safe" not in game_state.admissible_commands + + # Already open/closed. + game_state, _, _ = env.step("open safe") + game_state, _, _ = env.step("open safe") + assert "already open" in game_state.feedback + game_state, _, _ = env.step("close safe") + game_state, _, _ = env.step("close safe") + assert "already close" in game_state.feedback + + game_state, _, _ = env.step("open safe") + game_state, _, _ = env.step("take gold bar from safe") + assert "gold bar" in game_state.inventory diff --git a/textworld/generator/data/tests/test_food.py b/textworld/generator/data/tests/test_food.py new file mode 100644 index 00000000..87386cea --- /dev/null +++ b/textworld/generator/data/tests/test_food.py @@ -0,0 +1,61 @@ +from os.path import join as pjoin + +import textworld + +from textworld import g_rng # Global random generator. +from textworld import GameMaker +from textworld.utils import make_temp_directory + + +def test_food(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + + apple = M.new("f", name="apple") + M.add_fact("edible", apple) + raw_meat = M.new("f", name="raw meat") + table = M.new("table", name="table") + room.add(table) + table.add(apple) + M.inventory.add(raw_meat) + + with make_temp_directory(prefix="test_food") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + assert "eat raw meat" not in game_state.admissible_commands + game_state, _, _ = env.step("eat raw meat") + assert "plainly inedible" in game_state.feedback + + assert "eat apple" not in game_state.admissible_commands + game_state, _, _ = env.step("eat apple") + assert "need to take" in game_state.feedback + + assert "take apple from table" in game_state.admissible_commands + game_state, _, _ = env.step("take apple from table") + + assert "eat apple" in game_state.admissible_commands + game_state, _, _ = env.step("eat apple") + assert "You eat the apple. Not bad" in game_state.feedback + + assert "eat apple" not in game_state.admissible_commands + game_state, _, _ = env.step("eat apple") + assert "can't see" in game_state.feedback + + +def test_food_constraints(): + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + apple = M.new("f", name="apple") + M.add_fact("eaten", apple) + assert M.validate() + + # Eaten food should be nowhere. + M.inventory.add(apple) + assert not M.validate() diff --git a/textworld/generator/data/tests/test_stool.py b/textworld/generator/data/tests/test_stool.py new file mode 100644 index 00000000..ebccedf6 --- /dev/null +++ b/textworld/generator/data/tests/test_stool.py @@ -0,0 +1,87 @@ +from os.path import join as pjoin + +import textworld + +from textworld import g_rng # Global random generator. +from textworld import GameMaker +from textworld.utils import make_temp_directory + + +def test_stool(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + concert = M.new_room("Concert") + M.set_player(concert) + + stage = M.new("table", name="stage") + stool = M.new("stool", name="stool") + water = M.new("o", name="bottle of water") + mic = M.new("o", name="microphone") + concert.add(stage) + stage.add(stool) + stool.add(water) + stool.add(mic) + + with make_temp_directory(prefix="test_stool") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + assert "take microphone from stool" in game_state.admissible_commands + assert "take bottle of water from stool" in game_state.admissible_commands + assert "take stool from stage" in game_state.admissible_commands + game_state, _, _ = env.step("take stool from stage") + assert "stool" in game_state.inventory + assert " a microphone" in game_state.inventory + assert " a bottle of water" in game_state.inventory + + game_state, _, _ = env.step("take microphone from stool") + assert " a microphone" not in game_state.inventory + assert "microphone" in game_state.inventory + + game_state, _, _ = env.step("drop stool") + assert "stool" not in game_state.inventory + assert "bottle of water" not in game_state.inventory + + game_state, _, _ = env.step("take bottle of water from stool") + assert "bottle of water" in game_state.inventory + + game_state, _, _ = env.step("examine stool") + assert "nothing" in game_state.feedback + + game_state, _, _ = env.step("put microphone on stool") + assert "microphone" not in game_state.inventory + + +def test_nested_stools(): + g_rng.set_seed(20180905) # Make the generation process reproducible. + + M = GameMaker() + room = M.new_room("room") + M.set_player(room) + + block1 = M.new("stool", name="large block") + block2 = M.new("stool", name="medium block") + block3 = M.new("stool", name="small block") + + room.add(block1) + M.inventory.add(block2) + M.inventory.add(block3) + + with make_temp_directory(prefix="test_nested_stools") as tmpdir: + game_file = M.compile(pjoin(tmpdir, "game.ulx")) + env = textworld.start(game_file) + env.activate_state_tracking() + game_state = env.reset() + + assert "put small block on medium block" not in game_state.admissible_commands + game_state, _, _ = env.step("put small block on medium block") + assert "cannot put" in game_state.feedback + assert "small block" in game_state.inventory + + assert "put small block on large block" not in game_state.admissible_commands + game_state, _, _ = env.step("put small block on large block") + assert "cannot put" in game_state.feedback + assert "small block" in game_state.inventory diff --git a/textworld/generator/data/text_grammars/basic1_instruction.twg b/textworld/generator/data/text_grammars/basic1_instruction.twg index 47716810..f18cbb06 100644 --- a/textworld/generator/data/text_grammars/basic1_instruction.twg +++ b/textworld/generator/data/text_grammars/basic1_instruction.twg @@ -9,8 +9,8 @@ eat_types:(f) close_open_types:(d|c) #actions take:take the #obj_types#. -insert:insert #obj_types# into (c). -put:put #obj_types# on (s). +insert:insert #obj_types# into (chest). +put:put #obj_types# on (table). drop:drop #obj_types# in (r). eat:eat (f). open:open #close_open_types#. @@ -34,10 +34,10 @@ wait:Wait. #---------------- ig_unlock_open:open locked #lock_types# using (k). ig_unlock_open_take:Open locked #lock_types# using (k) and take #obj_types_no_key#. -ig_open_take:Take the #obj_types# from (c). +ig_open_take:Take the #obj_types# from (chest). ig_take_unlock:Take (k) and use (k) to unlock #lock_types#. -ig_open_insert:Open (c) and place #obj_types# in (c). -ig_insert_close:Place the #obj_types# in (c) and close (c). +ig_open_insert:Open (chest) and place #obj_types# in (chest). +ig_insert_close:Place the #obj_types# in (chest) and close (chest). ig_close_lock:Close #lock_types# and lock #lock_types#. #Flavour Text #---------------- diff --git a/textworld/generator/data/text_grammars/basic1_obj.twg b/textworld/generator/data/text_grammars/basic1_obj.twg index 6a543c6c..dca086cc 100644 --- a/textworld/generator/data/text_grammars/basic1_obj.twg +++ b/textworld/generator/data/text_grammars/basic1_obj.twg @@ -19,12 +19,12 @@ digit::0;1;2;3;4;5;6;7;8;9 #Containers # Each roomType must has specific containers #------------------- -(c):#(c)_adj#|#number##(c)_noun# -(c)_noun:box -(c)_adj:#adj_stripped# -X_(c):#X_(c)_adj#|#number##X_(c)_noun# -X_(c)_noun:box -X_(c)_adj:#adj_stripped# +(chest):#(chest)_adj#|#number##(chest)_noun# +(chest)_noun:box +(chest)_adj:#adj_stripped# +X_(chest):#X_(chest)_adj#|#number##X_(chest)_noun# +X_(chest)_noun:box +X_(chest)_adj:#adj_stripped# #Doors # Each roomType must has specific doors #------------------- @@ -34,12 +34,12 @@ X_(c)_adj:#adj_stripped# #Supporters # Each roomType must has specific supporters #------------------- -(s):#(s)_adj#|#number##(s)_noun# -(s)_noun:stand -(s)_adj:#adj_stripped# -X_(s):#X_(s)_adj#|#number##X_(s)_noun# -X_(s)_noun:stand -X_(s)_adj:#adj_stripped# +(table):#(table)_adj#|#number##(table)_noun# +(table)_noun:stand +(table)_adj:#adj_stripped# +X_(table):#X_(table)_adj#|#number##X_(table)_noun# +X_(table)_noun:stand +X_(table)_adj:#adj_stripped# # Objects # Each roomType must have specific objects #------------------- @@ -68,8 +68,8 @@ X_(o)_adj_type_1:#adj_stripped# #Object Descriptor Functions #--------------------------- (P)_desc:It's you. -(c)_desc:It is what it is -(s)_desc:It is what it is +(chest)_desc:It is what it is +(table)_desc:It is what it is (o)_desc:It is what it is (f)_desc:It is what it is (k)_desc:It is what it is @@ -83,4 +83,4 @@ on_desc:On the (name) is [a list of things on the (obj)]. #---------------- letter:A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z (k<->d)_match:#letter#-#(k)_noun# <-> #letter#-#(d)_noun# -(k<->c)_match:#letter#-#(k)_noun# <-> #letter#-#(c)_noun# \ No newline at end of file +(k<->c)_match:#letter#-#(k)_noun# <-> #letter#-#(chest)_noun# \ No newline at end of file diff --git a/textworld/generator/data/text_grammars/basic1_room.twg b/textworld/generator/data/text_grammars/basic1_room.twg index 812b58a9..b9e14613 100644 --- a/textworld/generator/data/text_grammars/basic1_room.twg +++ b/textworld/generator/data/text_grammars/basic1_room.twg @@ -15,16 +15,16 @@ inform7nounnoa:[if (obj) is locked]locked[else if (obj) is open]unlocked[otherwi dec:You are in the (name). #Container Descriptions #---------------------- -room_desc_(c):#room_desc_(c)_1_name##room_desc_(c)_content# -room_desc_(c)_1_name:#reg-a#; +room_desc_(chest):#room_desc_(chest)_1_name##room_desc_(chest)_content# +room_desc_(chest)_1_name:#reg-a#; reg-a:There is #i7_closed/open# (name) here. -room_desc_(c)_content:[if (obj) is open] #reg-d#[end if] +room_desc_(chest)_content:[if (obj) is open] #reg-d#[end if] reg-d:The (name) contains #i7_list_in#. #Supporter Descriptions #---------------------- -room_desc_(s):#room_desc_(s)_1_name# #room_desc_(s)_content# -room_desc_(s)_1_name:There is a (name) here. -room_desc_(s)_content:The (name) holds #i7_list_on#. +room_desc_(table):#room_desc_(table)_1_name# #room_desc_(table)_content# +room_desc_(table)_1_name:There is a (name) here. +room_desc_(table)_content:The (name) holds #i7_list_on#. #Group Descriptions #------------------ room_desc_group:There are (^) (val) here, (name). diff --git a/textworld/generator/data/text_grammars/basic_instruction.twg b/textworld/generator/data/text_grammars/basic_instruction.twg index 3b748b04..4cce7944 100644 --- a/textworld/generator/data/text_grammars/basic_instruction.twg +++ b/textworld/generator/data/text_grammars/basic_instruction.twg @@ -11,8 +11,8 @@ close_open_types:(d|c) take:take the #obj_types# in (r). take/s:take the #obj_types# from the #on_types#. take/c:take the #obj_types# from the #on_types#. -insert:insert #obj_types# into (c). -put:put #obj_types# on (s). +insert:insert #obj_types# into (chest). +put:put #obj_types# on (table). drop:drop #obj_types# in (r). eat:eat (f). open:open #close_open_types#. @@ -32,10 +32,10 @@ wait:Wait. #---------------- ig_unlock_open:open locked #lock_types# using (k). ig_unlock_open_take:Open locked #lock_types# using (k) and take #obj_types_no_key#. -ig_open_take:Take the #obj_types# from (c). +ig_open_take:Take the #obj_types# from (chest). ig_take_unlock:Take (k) and use (k) to unlock #lock_types#. -ig_open_insert:Open (c) and place #obj_types# in (c). -ig_insert_close:Place the #obj_types# in (c) and close (c). +ig_open_insert:Open (chest) and place #obj_types# in (chest). +ig_insert_close:Place the #obj_types# in (chest) and close (chest). ig_close_lock:Close #lock_types# and lock #lock_types#. #Flavour Text #---------------- diff --git a/textworld/generator/data/text_grammars/basic_obj.twg b/textworld/generator/data/text_grammars/basic_obj.twg index 49075bd5..75bd2dd5 100644 --- a/textworld/generator/data/text_grammars/basic_obj.twg +++ b/textworld/generator/data/text_grammars/basic_obj.twg @@ -19,12 +19,12 @@ digit:0;1;2;3;4;5;6;7;8;9 #Containers # Each roomType must has specific containers #------------------- -(c):#(c)_adj#|#(c)_noun##number# -(c)_noun:box -(c)_adj:#adj_stripped# -X_(c):#X_(c)_adj#|#X_(c)_noun##number# -X_(c)_noun:box -X_(c)_adj:#adj_stripped# +(chest):#(chest)_adj#|#(chest)_noun##number# +(chest)_noun:box +(chest)_adj:#adj_stripped# +X_(chest):#X_(chest)_adj#|#X_(chest)_noun##number# +X_(chest)_noun:box +X_(chest)_adj:#adj_stripped# #Doors # Each roomType must has specific doors #------------------- @@ -34,12 +34,12 @@ X_(c)_adj:#adj_stripped# #Supporters # Each roomType must has specific supporters #------------------- -(s):#(s)_adj#|#(s)_noun##number# -(s)_noun:stand -(s)_adj:#adj_stripped# -X_(s):#X_(s)_adj#|#X_(s)_noun##number# -X_(s)_noun:stand -X_(s)_adj:#adj_stripped# +(table):#(table)_adj#|#(table)_noun##number# +(table)_noun:stand +(table)_adj:#adj_stripped# +X_(table):#X_(table)_adj#|#X_(table)_noun##number# +X_(table)_noun:stand +X_(table)_adj:#adj_stripped# # Objects # Each roomType must have specific objects #------------------- @@ -68,8 +68,8 @@ X_(o)_adj_type_1:#adj_stripped# #Object Descriptor Functions #--------------------------- (P)_desc:It's you. -(c)_desc:It is what it is -(s)_desc:It is what it is +(chest)_desc:It is what it is +(table)_desc:It is what it is (o)_desc:It is what it is (f)_desc:It is what it is (k)_desc:It is what it is @@ -83,4 +83,4 @@ on_desc:On the (name) is [a list of things on the (obj)]. #---------------- letter:A;B;C;D;E;F;G;H;I;J;K;L;M;N;O;P;Q;R;S;T;U;V;W;X;Y;Z (k<->d)_match:#letter#-#(k)_noun# <-> #letter#-#(d)_noun# -(k<->c)_match:#letter#-#(k)_noun# <-> #letter#-#(c)_noun# \ No newline at end of file +(k<->c)_match:#letter#-#(k)_noun# <-> #letter#-#(chest)_noun# \ No newline at end of file diff --git a/textworld/generator/data/text_grammars/basic_room.twg b/textworld/generator/data/text_grammars/basic_room.twg index d68dfa41..e8eb0144 100644 --- a/textworld/generator/data/text_grammars/basic_room.twg +++ b/textworld/generator/data/text_grammars/basic_room.twg @@ -15,16 +15,16 @@ inform7nounnoa:[if (obj) is locked]locked[else if (obj) is open]unlocked[otherwi dec:You are in the (name). #Container Descriptions #---------------------- -room_desc_(c):#room_desc_(c)_1_name##room_desc_(c)_content# -room_desc_(c)_1_name:#reg-a#; +room_desc_(chest):#room_desc_(chest)_1_name##room_desc_(chest)_content# +room_desc_(chest)_1_name:#reg-a#; reg-a:There is #i7_closed/open# (name) here. -room_desc_(c)_content:[if (obj) is open] #reg-d#[end if] +room_desc_(chest)_content:[if (obj) is open] #reg-d#[end if] reg-d:The (name) contains #i7_list_in#. #Supporter Descriptions #---------------------- -room_desc_(s):#room_desc_(s)_1_name# #room_desc_(s)_content# -room_desc_(s)_1_name:There is a (name) here. -room_desc_(s)_content:The (name) holds #i7_list_on#. +room_desc_(table):#room_desc_(table)_1_name# #room_desc_(table)_content# +room_desc_(table)_1_name:There is a (name) here. +room_desc_(table)_content:The (name) holds #i7_list_on#. #Group Descriptions #------------------ room_desc_group:There are (^) (val) here, (name). diff --git a/textworld/generator/data/text_grammars/house_instruction.twg b/textworld/generator/data/text_grammars/house_instruction.twg index 7888718d..54edf828 100644 --- a/textworld/generator/data/text_grammars/house_instruction.twg +++ b/textworld/generator/data/text_grammars/house_instruction.twg @@ -3,13 +3,13 @@ #------------------------- obj_types:(o|k|f) obj_types_no_key:(o|f) -on_types:(c|s) -lock_types:(c|d) +on_types:(chest|table) +lock_types:(chest|d) eat_types:(f) -close_open_types:(d|c) +close_open_types:(d|chest) lock_type_var:#lock_types#;#lock_types# #in_the_(r)# -(s)_var:(s);(s) #in_the_(r)# -(c)_var:(c);(c) #in_the_(r)# +(table)_var:(table);(table) #in_the_(r)# +(chest)_var:(chest);(chest) #in_the_(r)# on_var:#on_types#;#on_types# #in_the_(r)# in_the_(r):in the (r);within the (r);inside the (r) make_syn_u:Make sure;Assure;Make it so;Doublecheck;Look and see;Make absolutely sure @@ -26,12 +26,12 @@ take/c:#take_synonym_1# the #obj_types# from the #on_var#. take:#pick-up_synonym_1# the #obj_types# from the floor of the (r). pick-up_synonym_1:pick-up;retrieve;recover;pick up;lift pick-up_synonym_v:Pick-up;Retrieve;Recover;Pick up;Lift -insert:#insert_syn_1# the #obj_types# #into_syn# the #(c)_var#.;you can #insert_syn_1# the #obj_types# #into_syn# the #(c)_var#. +insert:#insert_syn_1# the #obj_types# #into_syn# the #(chest)_var#.;you can #insert_syn_1# the #obj_types# #into_syn# the #(chest)_var#. insert_syn_u:Insert;Put;Place;Deposit insert_syn_1:insert;put;place;deposit insert_syn_v:inserted;put;placed;deposited begood:good;great;fantastic;a great idea -put:#put_syn_v# the #obj_types# on the #(s)_var#. +put:#put_syn_v# the #obj_types# on the #(table)_var#. put_syn_u:Put;Place;Sit;Rest put_syn_v:put;place;sit;rest on_it_syn:on it;upon it @@ -54,14 +54,10 @@ unlock_no_key:unlock the #lock_type_var#.;#make_syn_v# that the #lock_type_var# lock:#lock_key#;#lock_no_key# lock_key:lock the #lock_type_var# #by_the_syn# (k).;#make_syn_v# that the #lock_type_var# is locked #by_the_syn# (k).;#insert_syn_u# the (k) into the #lock_type_var# to lock it. lock_no_key:lock the #lock_type_var#.;#make_syn_v# the #lock_type_var# is locked.;#make_syn_v# that the #lock_type_var# is locked. -go/north:#go_syn_l# north.;#tryto# #go_syn_l# north. -go/south:#go_syn_l# south.;#tryto# #go_syn_l# south. -go/east:#go_syn_l# east.;#tryto# #go_syn_l# east. -go/west:#go_syn_l# west.;#tryto# #go_syn_l# west. -go/north/d:#go_syn_l# through the north (d).;#tryto# #go_syn_l# through the north (d). -go/south/d:#go_syn_l# through the south (d).;#tryto# #go_syn_l# through the south (d). -go/east/d:#go_syn_l# through the east (d).;#tryto# #go_syn_l# through the east (d). -go/west/d:#go_syn_l# through the west (d).;#tryto# #go_syn_l# through the west (d). +go(north):#go_syn_l# north.;#tryto# #go_syn_l# north. +go(south):#go_syn_l# south.;#tryto# #go_syn_l# south. +go(east):#go_syn_l# east.;#tryto# #go_syn_l# east. +go(west):#go_syn_l# west.;#tryto# #go_syn_l# west. go_syn_u:Go;Head;Venture;Travel;Move;Go to the;Take a trip go_syn_l:go;head;venture;travel;move;go to the;take a trip go_syn_v:visited;travelled;moved;ventured;gone @@ -75,12 +71,10 @@ wait:Wait. #---------------- ig_unlock_open:open the locked #lock_types# using the (k).;unlock and open the #lock_types#.;unlock and open the #lock_types# using the (k).;open the #lock_types# using the (k). ig_unlock_open_take:open the locked #lock_types# using the (k) and take the #obj_types_no_key#.;unlock the #lock_types# and take the #obj_types_no_key#.;unlock the #lock_types# using the (k), and take the #obj_types_no_key#.;take the #obj_types_no_key# from within the locked #lock_types#. -ig_open_take:take the #obj_types# from the (c).;open the (c) and take the #obj_types#.;from in the closed (c), take the #obj_types#. -ig_take/c_unlock:take the (k) and use it to unlock the #lock_types#.;unlock the #lock_types#, with the (k).; -ig_take/s_unlock:take the (k) and use it to unlock the #lock_types#.;unlock the #lock_types#, with the (k).; -ig_take_unlock:#pick-up_synonym_v# the (k) and use it to unlock the #lock_types#.;unlock the #lock_types#, with the (k).; -ig_open_insert:open the (c) and place the #obj_types# in it.;put the #obj_types# in the closed (c).; -ig_insert_close:place the #obj_types# in the (c) and close it.;close (c) after placing the #obj_types# in it. +ig_open_take:take the #obj_types# from the (chest).;open the (chest) and take the #obj_types#.;from in the closed (chest), take the #obj_types#. +ig_take_unlock:get the (k) and use it to unlock the #lock_types#.;unlock the #lock_types# using the (k).; +ig_open_insert:open the (chest) and place the #obj_types# in it.;put the #obj_types# in the closed (chest).; +ig_insert_close:place the #obj_types# in the (chest) and close it.;close (chest) after placing the #obj_types# in it. ig_close_lock:close the #lock_types# and lock it.;close the #lock_types# and lock it with the (k). #Flavour Text #---------------- @@ -101,16 +95,16 @@ action_separator_take: #afterhave# #havetaken# the #obj_types#, ; #after# #takin action_separator_take/s: #afterhave# #havetaken# the #obj_types#, ; #after# #taking# the #obj_types#, ; With the #obj_types#, ; If you can get your hands on the #obj_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_take/c: #afterhave# #havetaken# the #obj_types#, ; #after# #taking# the #obj_types#, ; With the #obj_types#, ; If you can get your hands on the #obj_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_eat: #afterhave# #ate# the #eat_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_separator_insert: #afterhave# #inserted# the #obj_types# into the (c), ; #after# #inserting# the #obj_types# into the (c), ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_separator_put: #afterhave# #put_v# the #obj_types# on the (s), ; #after# #putting# the #obj_types# on the (s), ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_separator_insert: #afterhave# #inserted# the #obj_types# into the (chest), ; #after# #inserting# the #obj_types# into the (chest), ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_separator_put: #afterhave# #put_v# the #obj_types# on the (table), ; #after# #putting# the #obj_types# on the (table), ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_open: #afterhave# #opened# the #close_open_types#, ; #after# #opening# the #close_open_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_unlock: #afterhave# #unlocked# the #lock_types#, ; #after# #unlocking# the #lock_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_lock: #afterhave# #locked# the #lock_types#, ; #after# #locking# the #lock_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_seperator_go: #afterhave# #gone# (dir), ; #after# #getting# (dir), ; once you're (dir), ; once you're in the (dir), ; If you can manage to go (dir), ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_seperator_go/south: #afterhave# gone south, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_seperator_go/north: #afterhave# gone north, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_seperator_go/east: #afterhave# gone east, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# -action_seperator_go/west: #afterhave# gone west, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_seperator_go(south): #afterhave# gone south, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_seperator_go(north): #afterhave# gone north, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_seperator_go(east): #afterhave# gone east, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# +action_seperator_go(west): #afterhave# gone west, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_close: #afterhave# #closed# the #close_open_types#, ; #after# #closing# the #close_open_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# action_separator_drop: #afterhave# #dropped# the #obj_types#, ; #after# #dropping# the #obj_types#, ;#emptyinstruction1#;#emptyinstruction2#;#emptyinstruction3#;#emptyinstruction4#;#emptyinstruction5#;#emptyinstruction6#;#emptyinstruction7#;#emptyinstruction8#;#emptyinstruction9#;#emptyinstruction10# #Separator Symbols diff --git a/textworld/generator/data/text_grammars/house_obj.twg b/textworld/generator/data/text_grammars/house_obj.twg index 738993b3..b86e205f 100644 --- a/textworld/generator/data/text_grammars/house_obj.twg +++ b/textworld/generator/data/text_grammars/house_obj.twg @@ -54,40 +54,73 @@ work_(r)_noun:office;studio;workshop;cubicle;study work_(r)_adj:silent;austere;serious;still hot-adj:super;unreasonably;absurdly;alarmingly;upsettingly -#Containers + +# Chests aka fixed in place containers. # Each roomType must has specific containers -#container descriptions work like room descriptions, except the (r) is a (c) +# Their descriptions work like room descriptions, except the (r) is a (chest) #------------------- -(c):#(c)_adj_noun# -(c)_noun:chest;box;safe;locker -(c)_adj:sturdy;nice;ugly -(c)_adj_noun:#(c)_adj# | #(c)_noun# -clean_(c):#clean_(c)_adj_type_1# | #clean_(c)_noun_type_1#;#clean_(c)_adj_type_2# | #clean_(c)_noun_type_2# +(chest):#(chest)_adj_noun# +(chest)_noun:chest;safe;locker +(chest)_adj:sturdy;nice;ugly +(chest)_adj_noun:#(chest)_adj# | #(chest)_noun# +clean_(chest):#clean_(chest)_adj_type_1# | #clean_(chest)_noun_type_1#;#clean_(chest)_adj_type_2# | #clean_(chest)_noun_type_2# #Type 1 -clean_(c)_noun_type_1:cabinet;basket;box;safe;trunk;case -clean_(c)_adj_type_1:gross;stained;spotless;plain; +clean_(chest)_noun_type_1:cabinet;basket;safe;trunk;case +clean_(chest)_adj_type_1:gross;stained;spotless;plain; #Type 2 -clean_(c)_noun_type_2:drawer;dresser;cabinet -clean_(c)_adj_type_2:#wood_type#wood +clean_(chest)_noun_type_2:drawer;dresser;cabinet +clean_(chest)_adj_type_2:#wood_type#wood + +storage_(chest):#storage_(chest)_adj# | #storage_(chest)_noun# +storage_(chest)_noun:chest;safe;locker;trunk;coffer;cabinet;case;display +storage_(chest)_adj:rusty;neglected;brand new + +cook_(chest):#cook_(chest)_adj# | #cook_(chest)_noun# +cook_(chest)_noun:fridge;refrigerator;freezer;chest;cabinet;case +cook_(chest)_adj:fancy;big;small + +rest_(chest):#rest_(chest)_adj# | #rest_(chest)_noun# +rest_(chest)_noun:dresser;chest;basket;safe;locker;trunk;coffer +rest_(chest)_adj:new;dusty;clean;amazing -storage_(c):#storage_(c)_adj# | #storage_(c)_noun# -storage_(c)_noun:toolbox;chest;safe;locker;trunk;coffer;cabinet;crate;case;suitcase;display -storage_(c)_adj:rusty;neglected;brand new +work_(chest):#work_(chest)_adj# | #work_(chest)_noun# +work_(chest)_noun:cabinet;safe;locker;trunk;bureau;coffer;case;display +work_(chest)_adj:iron;rusty;ancient -cook_(c):#cook_(c)_adj# | #cook_(c)_noun# -cook_(c)_noun:fridge;refrigerator;freezer;chest;cabinet;case -cook_(c)_adj:fancy;big;small +# Boxes aka portable containers. +#------------------- +(box):#(box)_adj_noun# +(box)_noun:chest;box +(box)_adj:sturdy;nice;ugly +(box)_adj_noun:#(box)_adj# | #(box)_noun# +clean_(box):#clean_(box)_adj# | #clean_(box)_noun# +clean_(box)_noun:box;bag +clean_(box)_adj:gross;stained;spotless;plain; + +storage_(box):#storage_(box)_adj# | #storage_(box)_noun# +storage_(box)_noun:toolbox;crate;suitcase +storage_(box)_adj:rusty;neglected;brand new -rest_(c):#rest_(c)_adj# | #rest_(c)_noun# -rest_(c)_noun:dresser;chest;basket;box;safe;locker;trunk;coffer;suitcase;portmanteau -rest_(c)_adj:new;dusty;clean;amazing +cook_(box):#cook_(box)_adj# | #cook_(box)_noun# +cook_(box)_noun:grocery bag;bowl;cooking pot +cook_(box)_adj:fancy;big;small -work_(c):#work_(c)_adj# | #work_(c)_noun# -work_(c)_noun:cabinet;box;safe;locker;trunk;bureau;coffer;case;suitcase;toolbox;portmanteau;display -work_(c)_adj:iron;rusty;ancient +rest_(box):#rest_(box)_adj# | #rest_(box)_noun# +rest_(box)_noun:box;suitcase;bag;laundry basket +rest_(box)_adj:new;dusty;clean;amazing +work_(box):#work_(box)_adj# | #work_(box)_noun# +work_(box)_noun:box;suitcase;toolbox;crate +work_(box)_adj:iron;rusty;ancient +# Stool aka portable supporters. +#------------------- +(stool):#(stool)_adj_noun# +(stool)_noun:step stool;stool +(stool)_adj:sturdy;nice;ugly +(stool)_adj_noun:#(stool)_adj# | #(stool)_noun# + fruit:watermelon;melon;honeydew;strawberry;apple;pear;grape;kiwi;cantaloupe #Doors # Each roomType must has specific doors @@ -101,30 +134,30 @@ wood_type:oak;birch;maple;balsam;beech;mahogany;walnut;cedar;pine; #Supporters # Each roomType must has specific supporters #------------------- -#Like containers, but with a (s) -(s):#(s)_adj# | #(s)_noun# -(s)_noun:shelf;table;pedestal;slab -(s)_adj:#(o)_adj# -clean_(s):#clean_(s)_adj# | #clean_(s)_noun# -clean_(s)_noun:board;shelf;table;counter;rack;bench -clean_(s)_adj:dusty;chipped;shiny -storage_(s):#storage_(s)_adj# | #storage_(s)_noun# -storage_(s)_noun:shelf;table;workbench;counter;rack;stand -storage_(s)_adj:rusty;shoddy;splintery;rough -cook_(s):#cook_(s)_adj# | #cook_(s)_noun# -cook_(s)_noun:counter;board;shelf;table;rack;chair;plate;bowl;pan;platter;saucepan -cook_(s)_adj:greasy;soaped down;filthy;messy -rest_(s):#rest_(s)_adj# | #rest_(s)_noun# -rest_(s)_noun:bed;couch;shelf;bookshelf;desk;bed stand;mantelpiece;mantle;bar;bench;stand;recliner -rest_(s)_adj:comfy;warm;worn-out -work_(s):#work_(s)_adj# | #work_(s)_noun# -work_(s)_noun:stand;bookshelf;table;chair;shelf;desk;mantelpiece;mantle;stand;armchair -work_(s)_adj:stern;solid;worn;gross +#Like containers, but with a (table) +(table):#(table)_adj# | #(table)_noun# +(table)_noun:shelf;table;pedestal;slab +(table)_adj:#(o)_adj# +clean_(table):#clean_(table)_adj# | #clean_(table)_noun# +clean_(table)_noun:board;shelf;table;counter;rack;bench +clean_(table)_adj:dusty;chipped;shiny +storage_(table):#storage_(table)_adj# | #storage_(table)_noun# +storage_(table)_noun:shelf;table;workbench;counter;rack;stand +storage_(table)_adj:rusty;shoddy;splintery;rough +cook_(table):#cook_(table)_adj# | #cook_(table)_noun# +cook_(table)_noun:counter;board;shelf;table;rack;chair;plate;bowl;pan;platter;saucepan +cook_(table)_adj:greasy;soaped down;filthy;messy +rest_(table):#rest_(table)_adj# | #rest_(table)_noun# +rest_(table)_noun:bed;couch;shelf;bookshelf;desk;bed stand;mantelpiece;mantle;bar;bench;stand;recliner +rest_(table)_adj:comfy;warm;worn-out +work_(table):#work_(table)_adj# | #work_(table)_noun# +work_(table)_noun:stand;bookshelf;table;chair;shelf;desk;mantelpiece;mantle;stand;armchair +work_(table)_adj:stern;solid;worn;gross # Objects # Each roomType must have specific objects #------------------- -#(s) --> (o) Very useful to create multiple subtypes to avoid inappropriate or awkward adjective pairing +#(table) --> (o) Very useful to create multiple subtypes to avoid inappropriate or awkward adjective pairing (o):#(o)_adj# | #(o)_noun# (o)_noun:pencil;pen (o)_adj:new;old;used;dusty;clean;large;small;fancy;plain;ornate;antique;contemporary;modern;dirty;elegant;simple;hefty;modest;austere @@ -265,9 +298,9 @@ candy:chocolate bar;gummy bear;candy bar;licorice strip;cookie #Object Descriptor Functions #--------------------------- (P)_desc:It's you. -(c)_desc:The (name) looks strong, and impossible to #force_open#. +(chest)_desc:The (name) looks strong, and impossible to #force_open#. force_open:force open;open;break open;bash open;crack open -(s)_desc:The (name) is #supp_stable#. +(table)_desc:The (name) is #supp_stable#. supp_stable:stable;wobbly;unstable;balanced;durable;reliable;solid;undependable;solidly built;an unstable piece of #garbage#;shaky garbage:garbage;trash;junk (o)_desc:The (name) is #obj_what#.;The (name) #looks_seems# #out_in_place# here @@ -298,4 +331,4 @@ shape:rectangular;cuboid;spherical;formless;non-euclidean colour:red;blue;chartreuse;purple;violet;orange;yellow;green;brown;teal;cyan smell:vanilla;lavender;cake;fudge;fresh laundry;soap (k<->d)_match:#(k)_adj# | #clearancelevel# #(k)_noun# <-> #(d)_adj# | #clearancelevel# #(d)_noun#; #colour# | #(k)_noun# <-> #colour# | #(d)_noun# -(k<->c)_match:#(k)_adj# | #clearancelevel# #(k)_noun# <-> #(c)_adj# | #clearancelevel# #(c)_noun#; #colour# | #(k)_noun# <-> #colour# | #(c)_noun# +(k<->c)_match:#(k)_adj# | #clearancelevel# #(k)_noun# <-> #(chest)_adj# | #clearancelevel# #(chest)_noun#; #colour# | #(k)_noun# <-> #colour# | #(chest)_noun# diff --git a/textworld/generator/data/text_grammars/house_room.twg b/textworld/generator/data/text_grammars/house_room.twg index 18a259fd..3932996d 100644 --- a/textworld/generator/data/text_grammars/house_room.twg +++ b/textworld/generator/data/text_grammars/house_room.twg @@ -94,23 +94,23 @@ sign:sign;placard;notice;signboard;board #---------------------- #Rules for Container Descriptions #Good idea to subdivide these into difficulty levels -#room_desc_(c): generates all container descriptions +#room_desc_(chest): generates all container descriptions #containerdescription: contains all physical exterior descriptions of containers -#room_desc_(c)_1_name: describes the container as an adj+noun -#room_dec_(c)_1_noun: describes the container as a noun -#room_desc_(c)_content: decides if we append a description of the container's contents depending on if the container is open or closed +#room_desc_(chest)_1_name: describes the container as an adj+noun +#room_dec_(chest)_1_noun: describes the container as a noun +#room_desc_(chest)_content: decides if we append a description of the container's contents depending on if the container is open or closed #opencontainer:what is appended if the container is open -#room_desc_(c)_2_adj: adds an adjective and a list of contents (creates doubled adjectives?) -#room_desc_(c)_2: list of contents without an adjective +#room_desc_(chest)_2_adj: adds an adjective and a list of contents (creates doubled adjectives?) +#room_desc_(chest)_2: list of contents without an adjective -room_desc_(c):#containerdescription##room_desc_(c)_content# +room_desc_(chest):#containerdescription##room_desc_(chest)_content# #special_container#. -containerdescription:#room_desc_(c)_1_name#;#room_desc_(c)_1_noun# -room_desc_(c)_content:[if (obj) is open and there is something in the (obj)] #opencontainer##suffix#[end if][if (obj) is open and the (obj) contains nothing] #emptyreaction#[end if] +containerdescription:#room_desc_(chest)_1_name#;#room_desc_(chest)_1_noun# +room_desc_(chest)_content:[if (obj) is open and there is something in the (obj)] #opencontainer##suffix#[end if][if (obj) is open and the (obj) contains nothing] #emptyreaction#[end if] opencontainer:The (name) contains #i7_list_in# -#room_desc_(c)_2_adj#;#room_desc_(c)_2# +#room_desc_(chest)_2_adj#;#room_desc_(chest)_2# emptyreaction:The (name-n) is empty, what a horrible day!;The (name-n) is empty! What a waste of a day!;The (name-n) is empty! This is the worst thing that could possibly happen, ever!;Empty! What kind of nightmare TextWorld is this?;What a letdown! The (name-n) is empty! -room_desc_(c)_1_name:#reg-a#;#normal-a#;#difficult-a#;#moredifficult-a#;#playful-a# +room_desc_(chest)_1_name:#reg-a#;#normal-a#;#difficult-a#;#moredifficult-a#;#playful-a# reg-a:#a1#;#a2# normal-a:#a3#;#a4#;#a5# @@ -118,7 +118,7 @@ difficult-a:#a6# moredifficult-a:#reg-a# playful-a:#reg-a# -room_desc_(c)_1_noun:#reg-b#;#normal-b#;#difficult-b#;#moredifficult-b#;#playful-b# +room_desc_(chest)_1_noun:#reg-b#;#normal-b#;#difficult-b#;#moredifficult-b#;#playful-b# reg-b:#b1#;#b2# normal-b:#b3#;#b4#;#b5# @@ -126,15 +126,15 @@ difficult-b:#reg-b# moredifficult-b:#reg-b# playful-b:#reg-b# -room_desc_(c)_2_adj:#c1#;#c2#;#c3#;#c4#;#c5#;#c6#;#c7#;#c8#;#c9# +room_desc_(chest)_2_adj:#c1#;#c2#;#c3#;#c4#;#c5#;#c6#;#c7#;#c8#;#c9# -room_desc_(c)_2:#d0#;#d1#;#d2#;#d3#;#d4# +room_desc_(chest)_2:#d0#;#d1#;#d2#;#d3#;#d4# -room_desc_(c)_multi_noun:#e1# -room_desc_(c)_multi_open_noun:#f1#;#f2#;#f3#;#f4#;#f5#;#f6# +room_desc_(chest)_multi_noun:#e1# +room_desc_(chest)_multi_open_noun:#f1#;#f2#;#f3#;#f4#;#f5#;#f6# -room_desc_(c)_multi_adj:#g1# -room_desc_(c)_multi_open_adj:#h1#;#h2#;#h3#;#h4# +room_desc_(chest)_multi_adj:#g1# +room_desc_(chest)_multi_open_adj:#h1#;#h2#;#h3#;#h4# @@ -179,7 +179,7 @@ d4:In it, you can see #i7_list_in#. # E # -e1:[if (obj) is open]#room_desc_(c)_multi_open_noun#.[else if (obj) is locked]The (name-n) is locked.[otherwise]The (name-n) is closed.[end if] +e1:[if (obj) is open]#room_desc_(chest)_multi_open_noun#.[else if (obj) is locked]The (name-n) is locked.[otherwise]The (name-n) is closed.[end if] # F # @@ -192,7 +192,7 @@ f6:The (name-n) #reminds_you# the containers #ofyouryouth#. Oh, how they also co # G # -g1:[if (obj) is open]#room_desc_(c)_multi_open_adj#.[else if (obj) is locked]The (name-adj) one is locked.[otherwise]The (name-adj) one is closed.[end if] +g1:[if (obj) is open]#room_desc_(chest)_multi_open_adj#.[else if (obj) is locked]The (name-adj) one is locked.[otherwise]The (name-adj) one is closed.[end if] # H # @@ -223,26 +223,26 @@ ContentsC-:Contents-;Contained within-;Inside are the following-;Inventory is as #Supporter Descriptions #---------------------- #Similar to Container descriptions, but without open/close or lock/unlock -#room_desc_(s): hub -#room_desc_(s)_1_noun : description of supporter without adjective. Paired with--> room_desc_(s)_2_adj -#room_desc_(s)_1_name : same as above, but with an adjective -#room_desc_(s)_2_adj : adjective for supporter plus a list of things on it -#room_desc_(s)_2: +#room_desc_(table): hub +#room_desc_(table)_1_noun : description of supporter without adjective. Paired with--> room_desc_(table)_2_adj +#room_desc_(table)_1_name : same as above, but with an adjective +#room_desc_(table)_2_adj : adjective for supporter plus a list of things on it +#room_desc_(table)_2: -room_desc_(s):#room_desc_(s)_1_noun# #room_desc_(s)_2_adj#;#room_desc_(s)_1_name# #room_desc_(s)_2# +room_desc_(table):#room_desc_(table)_1_noun# #room_desc_(table)_2_adj#;#room_desc_(table)_1_name# #room_desc_(table)_2# -room_desc_(s)_1_noun:#prefix# (name-n)#suffix_(s)_mid# +room_desc_(table)_1_noun:#prefix# (name-n)#suffix_(table)_mid# -room_desc_(s)_1_name:#prefix# (name)#suffix_(s)_mid# +room_desc_(table)_1_name:#prefix# (name)#suffix_(table)_mid# -room_desc_(s)_2_adj:The (name-n) is (name-adj).[if there is something on the (obj)] On the (name) #how_see_u# #i7_list_on##suffix_(s)_end#[end if][if there is nothing on the (obj)] #emptysupporter##suffix_(s)_end_angry#[end if] +room_desc_(table)_2_adj:The (name-n) is (name-adj).[if there is something on the (obj)] On the (name) #how_see_u# #i7_list_on##suffix_(table)_end#[end if][if there is nothing on the (obj)] #emptysupporter##suffix_(table)_end_angry#[end if] #;The (name-n) is (name-adj) [if name has something on it] on the (name) #i7_list_on#[else if the (name) is empty]#emptysupporter#[end if] -room_desc_(s)_2:[if there is something on the (obj)]On the (name) #how_see_u# #i7_list_on##suffix_(s)_end#[end if][if there is nothing on the (obj)]#emptysupporter##suffix_(s)_end_angry#[end if];[if there is something on the (obj)]You see #i7_list_on# on the (name-n)#suffix_(s)_end#[end if][if there is nothing on the (obj)]#emptysupporter##suffix_(s)_end_angry#[end if] +room_desc_(table)_2:[if there is something on the (obj)]On the (name) #how_see_u# #i7_list_on##suffix_(table)_end#[end if][if there is nothing on the (obj)]#emptysupporter##suffix_(table)_end_angry#[end if];[if there is something on the (obj)]You see #i7_list_on# on the (name-n)#suffix_(table)_end#[end if][if there is nothing on the (obj)]#emptysupporter##suffix_(table)_end_angry#[end if] #[if name has something on it] on the (name) #i7_list_on#[else if the (name) is empty]#emptysupporter#[end if] -room_desc_(s)_multi_noun:[if there is something on the (obj)]On the (name-n), you see #i7_list_on##suffix_(s)_end#[end if][if there is nothing on the (obj)]#emptysupporter_multi##suffix_(s)_end_angry#[end if] -room_desc_(s)_multi_adj:[if there is something on the (obj)]On the (name-adj) one, you see #i7_list_on##suffix_(s)_end#[end if][if there is nothing on the (obj)]#emptysupporter_multi##suffix_(s)_end_angry#[end if] +room_desc_(table)_multi_noun:[if there is something on the (obj)]On the (name-n), you see #i7_list_on##suffix_(table)_end#[end if][if there is nothing on the (obj)]#emptysupporter_multi##suffix_(table)_end_angry#[end if] +room_desc_(table)_multi_adj:[if there is something on the (obj)]On the (name-adj) one, you see #i7_list_on##suffix_(table)_end#[end if][if there is nothing on the (obj)]#emptysupporter_multi##suffix_(table)_end_angry#[end if] emptysupporter:But there isn't a thing on it;Unfortunately, there isn't a thing on it;But the thing is empty;But the thing is empty, unfortunately;But the thing hasn't got anything on it;But oh no! there's nothing on this piece of #trash#;The (name-n) appears to be empty;Looks like someone's already been here and taken everything off it, though;However, the (name-n), like an empty (name-n), has nothing on it; emptysupporter_multi:There isn't a thing on the (name-n);The (name-n) is empty;Look at the (name-n). There's nothing on this piece of #trash#;But the (name-n) hasn't got anything on it;What a letdown, there's nothing here; #Supporter Description Templates @@ -329,8 +329,8 @@ unblocked:unblocked;unguarded #To be affixed before object descriptions. Keep away from doors. Prefixes start with a uppercase letter and end with "a" prefix:You see a gleam over in a corner, where you can see a;What's that over there? It looks like it's a;You scan the room, seeing a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;You smell #smelltype# smell, and follow it to a;Were you looking for a (name-n)? Because look over there, it's a;You rest your hand against a wall, but you miss the wall and fall onto a;You scan the room for a (name-n), and you find a;You hear a noise behind you and spin around, but you can't see anything other than a;Look out! It's a- oh, never mind, it's just a;Look over there! a;Oh, great. Here's a;Hey, want to see a (name-n)? Look over there, a;If you haven't noticed it already, there seems to be something there by the wall, it's a;You bend down to tie your shoe. When you stand up, you notice a;Oh wow! Is that what I think it is? It is! It's a;You lean against the wall, inadvertently pressing a secret button. The wall opens up to reveal a;You see a;As if things weren't amazing enough already, you can even see a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a;#how_see# a prefix-multi:You see a gleam over in a corner, where you can see ;What's that over there? It looks like it's ;You scan the room, seeing ;#how_see# ;You bend down to tie your shoe. When you stand up, you notice ;You smell #smelltype# smell, and follow it to ;You can suddenly see ; -prefix_(c):#prefix# -prefix_(s):#prefix# +prefix_(chest):#prefix# +prefix_(table):#prefix# #suffixes #----------- @@ -344,9 +344,9 @@ suffix:.;. You shudder, but continue examining the room.;. You wonder idly who l ###Multi suffixes need to be more flexible than normal ones### suffix-multi:.;. You shudder, but continue examining the room.;. You wonder idly who put this stuff here.;. There's something strange about this stuff being here, but you can't put your finger on it.;. There's something strange about this stuff being here, but you don't have time to worry about that now.;. Huh, weird.;, so there's that.;, so why not take a picture, it'll last longer!;. It doesn't get any more TextWorld than this!;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#;#empty41#;#empty42#;#empty43#;#empty44#;#empty45#; -suffix_(s)_mid:.;. You shudder, but continue examining the (name-n).;. You wonder idly who left that here.;. Now why would someone leave that there?;#suffix_meta#;. Why don't you take a picture of it, it'll last longer!;!;. Wow, isn't TextWorld just the best?;. I guess it's true what they say, if you're looking for a (name-n), go to TextWorld.;. What a coincidence, weren't you just thinking about a (name-n)?;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#; -suffix_(s)_end:.;. You shudder, but continue examining the room.;. There's something strange about this being here, but you can't put your finger on it.;. There's something strange about this thing being here, but you don't have time to worry about that now.;. Huh, weird.;, so there's that.;. Hmmm... what else, what else?;. I mean, just wow! Isn't TextWorld just the best?;. You can't wait to tell the folks at home about this!;. Something scurries by right in the corner of your eye. Probably nothing.;. You idly wonder how they came up with the name TextWorld for this place. It's pretty fitting.;. Suddenly, you bump your head on the ceiling, but it's not such a bad bump that it's going to prevent you from looking at objects and even things.;. Wow! Just like in the movies!;. It doesn't get more TextWorld than this!;. Now that's what I call TextWorld!;. Classic TextWorld.;#suffix#;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#; -suffix_(s)_end_angry:. You move on, clearly #upsetwith# TextWorld.;. You move on, clearly #upsetwith# your TextWorld experience.;. Sometimes, just sometimes, TextWorld can just be the worst.;. What's the point of an empty (name-n)?;. Hopefully this doesn't make you too upset.;. You make a mental note to not get your hopes up the next time you see a (name-n) in a room.;. ;. Hopefully, this discovery doesn't ruin your TextWorld experience!;. You swear loudly.;. This always happens!;. This always happens, here in TextWorld!;. Silly (name-n), silly, empty, good for nothing (name-n).;. You think about smashing the (name-n) to bits, throwing the bits #intheblank#, etc, until you get bored.;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n)! oh well.;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, and here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#;#empty41#;#empty42#;#empty43#;#empty44#;#empty45#;#empty46#;#empty47#;#empty48#;#empty49#;#empty50#; +suffix_(table)_mid:.;. You shudder, but continue examining the (name-n).;. You wonder idly who left that here.;. Now why would someone leave that there?;#suffix_meta#;. Why don't you take a picture of it, it'll last longer!;!;. Wow, isn't TextWorld just the best?;. I guess it's true what they say, if you're looking for a (name-n), go to TextWorld.;. What a coincidence, weren't you just thinking about a (name-n)?;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#; +suffix_(table)_end:.;. You shudder, but continue examining the room.;. There's something strange about this being here, but you can't put your finger on it.;. There's something strange about this thing being here, but you don't have time to worry about that now.;. Huh, weird.;, so there's that.;. Hmmm... what else, what else?;. I mean, just wow! Isn't TextWorld just the best?;. You can't wait to tell the folks at home about this!;. Something scurries by right in the corner of your eye. Probably nothing.;. You idly wonder how they came up with the name TextWorld for this place. It's pretty fitting.;. Suddenly, you bump your head on the ceiling, but it's not such a bad bump that it's going to prevent you from looking at objects and even things.;. Wow! Just like in the movies!;. It doesn't get more TextWorld than this!;. Now that's what I call TextWorld!;. Classic TextWorld.;#suffix#;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#; +suffix_(table)_end_angry:. You move on, clearly #upsetwith# TextWorld.;. You move on, clearly #upsetwith# your TextWorld experience.;. Sometimes, just sometimes, TextWorld can just be the worst.;. What's the point of an empty (name-n)?;. Hopefully this doesn't make you too upset.;. You make a mental note to not get your hopes up the next time you see a (name-n) in a room.;. ;. Hopefully, this discovery doesn't ruin your TextWorld experience!;. You swear loudly.;. This always happens!;. This always happens, here in TextWorld!;. Silly (name-n), silly, empty, good for nothing (name-n).;. You think about smashing the (name-n) to bits, throwing the bits #intheblank#, etc, until you get bored.;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n)! oh well.;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;. Aw, and here you were, all excited for there to be things on it!;. Oh! Why couldn't there just be stuff on it?;. Hm. Oh well;. What, you think everything in TextWorld should have stuff on it?;. It would have been so cool if there was stuff on the (name-n).;#emptymainperiod#;#emptymain#;#empty1#;#empty2#;#empty3#;#empty4#;#empty5#;#empty6#;#empty7#;#empty8#;#empty9#;#empty10#;#empty11#;#empty12#;#empty13#;#empty14#;#empty15#;#empty16#;#empty17#;#empty18#;#empty19#;#empty20#;#empty21#;#empty22#;#empty23#;#empty24#;#empty25#;#empty26#;#empty27#;#empty28#;#empty29#;#empty30#;#empty30#;#empty31#;#empty32#;#empty33#;#empty34#;#empty35#;#empty36#;#empty37#;#empty38#;#empty39#;#empty40#;#empty41#;#empty42#;#empty43#;#empty44#;#empty45#;#empty46#;#empty47#;#empty48#;#empty49#;#empty50#; #fix expandables in #----------- smelltype:an #ansmell#;a #asmell# diff --git a/textworld/generator/game.py b/textworld/generator/game.py index 32d1e418..9744fdbe 100644 --- a/textworld/generator/game.py +++ b/textworld/generator/game.py @@ -10,9 +10,7 @@ from textworld.generator import data from textworld.generator.text_grammar import Grammar from textworld.generator.world import World -from textworld.logic import Action, Proposition, Rule, State -from textworld.generator.vtypes import VariableTypeTree -from textworld.generator.grammar import get_reverse_action +from textworld.logic import Action, Proposition, Rule, State, GameLogic from textworld.generator.graph_networks import DIRECTIONS from textworld.generator.dependency_tree import DependencyTree @@ -272,8 +270,9 @@ def __init__(self, world: World, grammar: Optional[Grammar] = None, self.metadata = {} self._objective = None self._infos = self._build_infos() - self._rules = data.get_rules() - self._types = data.get_types() + self._game_logic = data.get_logic() + # self._rules = data.get_rules() + # self._types = data.get_types() self.change_grammar(grammar) self._main_quest = None @@ -307,8 +306,7 @@ def copy(self) -> "Game": game = Game(self.world, self.grammar, self.quests) game._infos = self.infos game.state = self.state.copy() - game._rules = self._rules - game._types = self._types + game._game_logic = self._game_logic game._objective = self._objective return game @@ -354,9 +352,7 @@ def deserialize(cls, data: Mapping) -> "Game": game._infos = {k: EntityInfo.deserialize(v) for k, v in data["infos"]} game.state = State.deserialize(data["state"]) - game._rules = {k: Rule.deserialize(v) - for k, v in data["rules"]} - game._types = VariableTypeTree.deserialize(data["types"]) + game._game_logic = GameLogic.deserialize(data["game_logic"]) game.metadata = data.get("metadata", {}) game._objective = data.get("objective", None) @@ -375,8 +371,7 @@ def serialize(self) -> Mapping: data["grammar"] = self.grammar.flags.serialize() data["quests"] = [quest.serialize() for quest in self.quests] data["infos"] = [(k, v.serialize()) for k, v in self._infos.items()] - data["rules"] = [(k, v.serialize()) for k, v in self._rules.items()] - data["types"] = self._types.serialize() + data["game_logic"] = self._game_logic.serialize() data["metadata"] = self.metadata data["objective"] = self._objective return data @@ -403,7 +398,7 @@ def directions_names(self) -> List[str]: @property def objects_types(self) -> List[str]: """ All types of objects in this game. """ - return sorted(self._types.types) + return sorted(t.name for t in self._game_logic.types) @property def objects_names(self) -> List[str]: @@ -428,7 +423,7 @@ def verbs(self) -> List[str]: """ Verbs that should be recognized in this game. """ # Retrieve commands templates for every rule. commands = [data.INFORM7_COMMANDS[rule_name] - for rule_name in self._rules] + for rule_name in self._game_logic.rules] verbs = [cmd.split()[0] for cmd in commands] verbs += ["look", "inventory", "examine", "wait"] return sorted(set(verbs)) @@ -527,7 +522,7 @@ def remove(self, action: Action) -> Optional[Action]: super().remove(action) # The last action might have impacted one of the subquests. - reverse_action = get_reverse_action(action) + reverse_action = data.get_reverse_action(action) if reverse_action is not None: self.push(reverse_action) @@ -684,8 +679,10 @@ def __init__(self, game: Game, track_quests: bool = True) -> None: """ self.game = game self.state = game.state.copy() - self._valid_actions = list(self.state.all_applicable_actions(self.game._rules.values(), - self.game._types.constants_mapping)) + from textworld.logic import Placeholder, Variable + self._rules = self.game._game_logic.rules.values() + self._constants_mapping = {Placeholder(t.name): Variable(t.name) for t in self.game._game_logic.types if t.constant} + self._valid_actions = list(self.state.all_applicable_actions(self._rules, self._constants_mapping)) self.quest_progressions = [] if track_quests: @@ -766,8 +763,7 @@ def update(self, action: Action) -> None: self.state.apply(action) # Get valid actions. - self._valid_actions = list(self.state.all_applicable_actions(self.game._rules.values(), - self.game._types.constants_mapping)) + self._valid_actions = list(self.state.all_applicable_actions(self._rules, self._constants_mapping)) # Update all quest progressions given the last action and new state. for quest_progression in self.quest_progressions: diff --git a/textworld/generator/grammar.py b/textworld/generator/grammar.py deleted file mode 100644 index b061ae59..00000000 --- a/textworld/generator/grammar.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT license. - - -from typing import List, Callable -from collections import OrderedDict - -from textworld.utils import RegexDict -from textworld.logic import Placeholder, Rule, TypeHierarchy -from textworld.generator import data - - -def get_reverse_action(action): - rev_rule = data.get_reverse_rules(action) - if rev_rule is None: - return None - - return action.inverse(name=rev_rule.name) diff --git a/textworld/generator/inform7/grammar.py b/textworld/generator/inform7/grammar.py index a612e985..2d6e4672 100644 --- a/textworld/generator/inform7/grammar.py +++ b/textworld/generator/inform7/grammar.py @@ -14,17 +14,25 @@ def define_inform7_kinds(): roots = [type for type in data.get_logic().types if len(type.parents) == 0] for root in roots: for type_ in root.subtypes: - if type_.name not in data.INFORM7_VARIABLES: - continue + parent_types = list(type_.parent_types) + if len(parent_types) == 0: + continue # Skip types without a parent. kind = data.INFORM7_VARIABLES[type_.name] - for parent in type_.parents: - parent_kind = data.INFORM7_VARIABLES[parent] - msg = '{} is a kind of {}.\n'.format(kind, parent_kind) - type_defs += msg - - desc = data.INFORM7_VARIABLES_DESCRIPTION[type_.name] - if desc: - type_defs += desc + '\n' + parent_kind = data.INFORM7_VARIABLES[parent_types[0].name] # The first parent should define the kind of object. + + if kind == "" or parent_kind == "": + continue + + type_defs += "The {kind} is a kind of {parent_kind}.".format(kind=kind, parent_kind=parent_kind) + type_definition = data.INFORM7_VARIABLES_DESCRIPTION[type_.name] + if type_definition: + type_defs += " " + type_definition + + attributes = {vtype for parent_type in parent_types[1:] for vtype in parent_type.supertypes} + for attribute in attributes: + type_defs += " " + data.INFORM7_VARIABLES_DESCRIPTION[attribute.name] + + type_defs += "\n" return type_defs diff --git a/textworld/generator/inform7/tests/test_world2inform7.py b/textworld/generator/inform7/tests/test_world2inform7.py index 81d34941..716980e1 100644 --- a/textworld/generator/inform7/tests/test_world2inform7.py +++ b/textworld/generator/inform7/tests/test_world2inform7.py @@ -103,10 +103,11 @@ def test_quest_losing_condition(): path.door.add_property("open") carrot = M.new(type='f', name='carrot') + M.add_fact("edible", carrot) M.inventory.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) @@ -174,7 +175,7 @@ def test_names_disambiguation(): path = M.connect(roomA.east, roomB.west) gateway = M.new_door(path, name="gateway") - + path = M.connect(roomA.west, roomC.east) rectangular_gateway = M.new_door(path, name="rectangular gateway") @@ -219,13 +220,13 @@ def test_names_disambiguation(): garage = M.new_room("garage") M.set_player(garage) - key = M.new(type="k", name="key") - typeG_safe = M.new(type="c", name="type G safe") - safe = M.new(type="c", name="safe") + key = M.new(type="k", name="key") + typeG_safe = M.new(type="chest", name="type G safe") + safe = M.new(type="chest", name="safe") safe.add(key) garage.add(safe, typeG_safe) - + M.add_fact("open", safe) game = M.build() @@ -242,13 +243,13 @@ def test_names_disambiguation(): garage = M.new_room("garage") M.set_player(garage) - key = M.new(type="k", name="key") - safe = M.new(type="c", name="safe") - typeG_safe = M.new(type="c", name="type G safe") + key = M.new(type="k", name="key") + safe = M.new(type="chest", name="safe") + typeG_safe = M.new(type="chest", name="type G safe") safe.add(key) garage.add(safe, typeG_safe) - + M.add_fact("open", safe) game = M.build() diff --git a/textworld/generator/inform7/world2inform7.py b/textworld/generator/inform7/world2inform7.py index 2223e373..02e0d0cc 100644 --- a/textworld/generator/inform7/world2inform7.py +++ b/textworld/generator/inform7/world2inform7.py @@ -198,14 +198,14 @@ def generate_inform7_source(game, seed=1234, use_i7_description=False): # Declare all objects for vtype in data.get_types(): - if vtype in ["P", "I"]: - continue # Skip player and inventory. + if vtype.constant: + continue # Skip constants like player and inventory. - entities = world.get_entities_per_type(vtype) + entities = world.get_entities_per_type(vtype.name) if len(entities) == 0: continue # No entity of that specific type. - kind = data.INFORM7_VARIABLES[vtype] + kind = data.INFORM7_VARIABLES[vtype.name] names = [entity.id for entity in entities] source += "The " + " and the ".join(names) + " are {}s.\n".format(kind) # All objects are privately-named and we manually define all "Understand as" phrases needed. @@ -269,7 +269,7 @@ def generate_inform7_source(game, seed=1234, use_i7_description=False): test_template = "quest{} completed is true" game_winning_test = " and ".join(test_template.format(i) for i in range(len(quests))) - # Remove square bracket when printing score increases. Square brackets are conflicting with + # Remove square bracket when printing score increases. Square brackets are conflicting with # Inform7's events parser in git_glulx_ml.py. # And add winning conditions for the game. source += textwrap.dedent("""\ @@ -330,6 +330,17 @@ def generate_inform7_source(game, seed=1234, use_i7_description=False): # Replace default banner with a greeting message and the quest description. source += textwrap.dedent("""\ Rule for printing the banner text: + say "[fixed letter spacing]"; + say " ________ ________ __ __ ________ __ __ ______ _______ __ _______ [line break]"; + say "| \| \| \ | \| \| \ _ | \ / \ | \ | \ | \ [line break]"; + say " \$$$$$$$$| $$$$$$$$| $$ | $$ \$$$$$$$$| $$ / \ | $$| $$$$$$\| $$$$$$$\| $$ | $$$$$$$\[line break]"; + say " | $$ | $$__ \$$\/ $$ | $$ | $$/ $\| $$| $$ | $$| $$__| $$| $$ | $$ | $$[line break]"; + say " | $$ | $$ \ >$$ $$ | $$ | $$ $$$\ $$| $$ | $$| $$ $$| $$ | $$ | $$[line break]"; + say " | $$ | $$$$$ / $$$$\ | $$ | $$ $$\$$\$$| $$ | $$| $$$$$$$\| $$ | $$ | $$[line break]"; + say " | $$ | $$_____ | $$ \$$\ | $$ | $$$$ \$$$$| $$__/ $$| $$ | $$| $$_____ | $$__/ $$[line break]"; + say " | $$ | $$ \| $$ | $$ | $$ | $$$ \$$$ \$$ $$| $$ | $$| $$ \| $$ $$[line break]"; + say " \$$ \$$$$$$$$ \$$ \$$ \$$ \$$ \$$ \$$$$$$ \$$ \$$ \$$$$$$$$ \$$$$$$$ [line break]"; + say "[variable letter spacing][line break]"; say "{objective}[line break]". """.format(objective=objective)) @@ -435,7 +446,7 @@ def generate_inform7_source(game, seed=1234, use_i7_description=False): remove the list of doors from L; if the number of entries in L is greater than 0: say "There is [L with indefinite articles] on the floor."; - + """) # Print properties of objects when listing the inventory contents and the room contents. @@ -507,16 +518,16 @@ def generate_inform7_source(game, seed=1234, use_i7_description=False): source += textwrap.dedent("""\ The taking action has an object called previous locale (matched as "from"). - Setting action variables for taking: + Setting action variables for taking: now previous locale is the holder of the noun. - Report taking something from the location: + Report taking something from the location: say "You pick up [the noun] from the ground." instead. - Report taking something: + Report taking something: say "You take [the noun] from [the previous locale]." instead. - Report dropping something: + Report dropping something: say "You drop [the noun] on the ground." instead. """) diff --git a/textworld/generator/logger.py b/textworld/generator/logger.py index 3397e72c..97e1be45 100644 --- a/textworld/generator/logger.py +++ b/textworld/generator/logger.py @@ -101,6 +101,7 @@ def collect(self, game): nb_objects = 0 for type_ in data.get_types(): + type_ = type_.name if type_ in ["I", "P", "t", "r"]: continue diff --git a/textworld/generator/maker.py b/textworld/generator/maker.py index 19dbec2f..923fba3f 100644 --- a/textworld/generator/maker.py +++ b/textworld/generator/maker.py @@ -145,11 +145,13 @@ def add_property(self, name: str) -> None: def add(self, *entities: List["WorldEntity"]) -> None: """ Add children to this entity. """ - if data.get_types().is_descendant_of(self.type, "r"): + if data.get_logic().types.get(self.type).has_supertype_named("r"): name = "at" - elif data.get_types().is_descendant_of(self.type, ["c", "I"]): + elif data.get_logic().types.get(self.type).has_supertype_named("container"): name = "in" - elif data.get_types().is_descendant_of(self.type, "s"): + elif data.get_logic().types.get(self.type).has_supertype_named("I"): + name = "in" + elif data.get_logic().types.get(self.type).has_supertype_named("supporter"): name = "on" else: raise ValueError("Unexpected type {}".format(self.type)) @@ -167,7 +169,7 @@ def has_property(self, name: str) -> bool: Example: >>> from textworld import GameMaker >>> M = GameMaker() - >>> chest = M.new(type="c", name="chest") + >>> chest = M.new(type="chest", name="chest") >>> chest.has_property('closed') False >>> chest.add_property('closed') @@ -276,7 +278,7 @@ def door(self) -> Optional[WorldEntity]: @door.setter def door(self, door: WorldEntity) -> None: - if door is not None and not data.get_types().is_descendant_of(door.type, "d"): + if door is not None and not data.get_types().get(door.type).has_supertype_named("d"): msg = "Expecting a WorldEntity of 'door' type." raise TypeError(msg) @@ -322,7 +324,7 @@ def __init__(self) -> None: self._quests = [] self.rooms = [] self.paths = [] - self._types_counts = data.get_types().count(State()) + self._types_counts = {} self.player = self.new(type='P') self.inventory = self.new(type='I') self.grammar = textworld.generator.make_grammar() @@ -402,7 +404,7 @@ def new(self, type: str, name: Optional[str] = None, * Otherwise, a `WorldEntity` is returned. """ var_id = type - if not data.get_types().is_constant(type): + if not data.get_types().get(type).constant: var_id = get_new(type, self._types_counts) var = Variable(var_id, type) @@ -643,7 +645,7 @@ def set_quest_from_final_state(self, final_state: Collection[Proposition]) -> Qu self.build() return self._quests[0] - def validate(self) -> bool: + def validate(self, raises: bool = False) -> bool: """ Check if the world is valid and can be compiled. A world is valid is the player has been place in a room and @@ -652,11 +654,17 @@ def validate(self) -> bool: """ if self.player not in self: msg = "Player position has not been specified. Use 'M.set_player(room)'." - raise MissingPlayerError(msg) + if raises: + raise MissingPlayerError(msg) + + return False failed_constraints = get_failing_constraints(self.state) if len(failed_constraints) > 0: - raise FailedConstraintsError(failed_constraints) + if raises: + raise FailedConstraintsError(failed_constraints) + + return False return True @@ -673,7 +681,7 @@ def build(self, validate: bool = True) -> Game: Generated game. """ if validate: - self.validate() # Validate the state of the world. + self.validate(raises=True) # Validate the state of the world. world = World.from_facts(self.facts) game = Game(world, quests=self._quests) diff --git a/textworld/generator/testing.py b/textworld/generator/testing.py deleted file mode 100644 index 90a86dfc..00000000 --- a/textworld/generator/testing.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT license. - - -VARIABLES_TXT_CONTENT = """ -# [variables] -thing: t -room: r -container: c -> t -supporter: s -> t -stove : stove -> s -# This is a comment. -oven: oven -> c -door: d -> t -portable_object: o -> t -key: k -> o -food: f -> o - -# [constants] -player: P -inventory: I -""" - -RULES_TXT_CONTENT = """ -# Navigation # -go/north :: at(P, r) & $north_of(r', r) & $south_of(r, r') & $free(r, r') & $free(r', r) -> at(P, r') -go/south :: at(P, r) & $south_of(r', r) & $north_of(r, r') & $free(r, r') & $free(r', r) -> at(P, r') -go/east :: at(P, r) & $east_of(r', r) & $west_of(r, r') & $free(r, r') & $free(r', r) -> at(P, r') -go/west :: at(P, r) & $west_of(r', r) & $east_of(r, r') & $free(r, r') & $free(r', r) -> at(P, r') - -# Doors # -unlock/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & $in(k, I) & $match(k, d) & locked(d) -> closed(d) -lock/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & $in(k, I) & $match(k, d) & closed(d) -> locked(d) -open/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & closed(d) -> open(d) & free(r, r') & free(r', r) -close/d :: $at(P, r) & $link(r, d, r') & $link(r', d, r) & open(d) & free(r, r') & free(r', r) -> closed(d) - -# Containers) & supporters # -open/c :: $at(P, r) & $at(c, r) & closed(c) -> open(c) -close/c :: $at(P, r) & $at(c, r) & open(c) -> closed(c) -insert :: $at(P, r) & $at(c, r) & $open(c) & in(o, I) -> in(o, c) -take/c :: $at(P, r) & $at(c, r) & $open(c) & in(o, c) -> in(o, I) -lock/c :: $at(P, r) & $at(c, r) & $in(k, I) & $match(k, c) & closed(c) -> locked(c) -unlock/c :: $at(P, r) & $at(c, r) & $in(k, I) & $match(k, c) & locked(c) -> closed(c) -put :: $at(P, r) & $at(s, r) & in(o, I) -> on(o, s) -take/s :: $at(P, r) & $at(s, r) & on(o, s) -> in(o, I) -take :: $at(P, r) & at(o, r) -> in(o, I) -drop :: $at(P, r) & in(o, I) -> at(o, r) - -# Misc # -eat :: in(f, I) & edible(f) -> eaten(f) -""" - -CONSTRAINTS_TXT_CONTENT = """ -c1 :: open(c) & closed(c) -> fail() -c2 :: open(c) & locked(c) -> fail() -c3 :: closed(c) & locked(c) -> fail() - -d1 :: open(d) & closed(d) -> fail() -d2 :: open(d) & locked(d) -> fail() -d3 :: closed(d) & locked(d) -> fail() - -obj1 :: in(o, I) & in(o, c) -> fail() -obj2 :: in(o, I) & on(o, s) -> fail() -obj3 :: in(o, I) & at(o, r) -> fail() -obj4 :: in(o, c) & on(o, s) -> fail() -obj5 :: in(o, c) & at(o, r) -> fail() -obj6 :: on(o, s) & at(o, r) -> fail() -obj7 :: at(o, r) & at(o, r') -> fail() -obj8 :: in(o, c) & in(o, c') -> fail() -obj9 :: on(o, s) & on(o, s') -> fail() - -k1 :: match(k, c) & match(k', c) -> fail() -k2 :: match(k, c) & match(k, c') -> fail() -k3 :: match(k, d) & match(k', d) -> fail() -k4 :: match(k, d) & match(k, d') -> fail() - -r1 :: at(P, r) & at(P, r') -> fail() -r2 :: at(s, r) & at(s, r') -> fail() -r3 :: at(c, r) & at(c, r') -> fail() - -# A door can't be used to link more than two rooms. -link1 :: link(r, d, r') & link(r, d, r'') -> fail() -link2 :: link(r, d, r') & link(r'', d, r''') -> fail() - -# There's already a door linking two rooms. -link3 :: link(r, d, r') & link(r, d', r') -> fail() - -# There cannot be more than four doors in a room. -# dr2 :: at(d1: d, r) & at(d2: d, r) & at(d3: d, r) & at(d4: d, r) & at(d5: d, r) -> fail() - -# An exit direction can only lead to one room. -nav_rr1 :: north_of(r, r') & north_of(r'', r') -> fail() -nav_rr2 :: south_of(r, r') & south_of(r'', r') -> fail() -nav_rr3 :: east_of(r, r') & east_of(r'', r') -> fail() -nav_rr4 :: west_of(r, r') & west_of(r'', r') -> fail() - -# Two rooms can only be connected once with each other. -nav_rrA :: north_of(r, r') & south_of(r, r') -> fail() -nav_rrB :: north_of(r, r') & west_of(r, r') -> fail() -nav_rrC :: north_of(r, r') & east_of(r, r') -> fail() -nav_rrD :: south_of(r, r') & west_of(r, r') -> fail() -nav_rrE :: south_of(r, r') & east_of(r, r') -> fail() -nav_rrF :: west_of(r, r') & east_of(r, r') -> fail() - -free1 :: link(r, d, r') & free(r, r') & closed(d) -> fail() -free2 :: link(r, d, r') & free(r, r') & locked(d) -> fail() - -eaten1 :: eaten(f) & in(f, I) -> fail() -eaten2 :: eaten(f) & in(f, c) -> fail() -eaten3 :: eaten(f) & on(f, s) -> fail() -eaten4 :: eaten(f) & at(f, r) -> fail() -""" - -REVERSE_RULES_TXT_CONTENT = """ -# Navigation # -go/north : go/south -go/south : go/north -go/east : go/west -go/west : go/east -go/north/d : go/south/d -go/south/d : go/north/d -go/east/d : go/west/d -go/west/d : go/east/d - -# Doors # -unlock/d : lock/d -lock/d : unlock/d -open/d : close/d -close/d : open/d - -# Containers & supporters # -open/c : close/c -close/c : open/c -insert : take/c -take/c : insert -lock/c : unlock/c -unlock/c : lock/c -put : take/s -take/s : put -take : drop -drop : take - -# Misc # -eat : None -""" diff --git a/textworld/generator/tests/test_chaining.py b/textworld/generator/tests/test_chaining.py index b2b555c5..a8529307 100644 --- a/textworld/generator/tests/test_chaining.py +++ b/textworld/generator/tests/test_chaining.py @@ -2,60 +2,55 @@ # Licensed under the MIT license. -from textworld import testing +import textworld from textworld.generator import data from textworld.generator.chaining import ChainingOptions, get_chains from textworld.logic import GameLogic, Proposition, State, Variable -# noinspection PyPep8Naming 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 + M = textworld.GameMaker() + bedroom = M.new_room("bedroom") + kitchen = M.new_room("kitchen") + rusty_key = M.new("k", name="rusty key") + small_key = M.new("k", name="small key") + chest = M.new("chest", name="chest") + cabinet = M.new("chest", name="cabinet") + robe = M.new("o", name="robe") + path = M.connect(bedroom.south, kitchen.north) + wooden_door = M.new_door(path, name="wooden door") + + M.set_player(bedroom) + M.inventory.add(rusty_key) + if locked_door: + wooden_door.add_property("locked") + else: + wooden_door.add_property("closed") + + chest.add_property("locked") + M.add_fact("match", rusty_key, chest) + kitchen.add(chest) + chest.add(small_key) + + M.add_fact("match", small_key, cabinet) + cabinet.add_property("locked") + bedroom.add(cabinet) + cabinet.add(robe) + + return State(M.facts) def test_chaining(): # The following test depends on the available rules, # so instead of depending on what is in rules.txt, # we define the allowed_rules to used. - allowed_rules = data.get_rules().get_matching("take/.*") - allowed_rules += data.get_rules().get_matching("go.*") - allowed_rules += data.get_rules().get_matching("insert.*", "put.*") - allowed_rules += data.get_rules().get_matching("open.*", "close.*") - allowed_rules += data.get_rules().get_matching("lock.*", "unlock.*") - allowed_rules += data.get_rules().get_matching("eat.*") + allowed_rules = data.get_rules().get_matching(r"take\((o|k|f), (r|table|chest)\).*") + allowed_rules += data.get_rules().get_matching(r"go\(.*\).*") + allowed_rules += data.get_rules().get_matching(r"insert\((o|k|f), chest\).*", r"put\((o|k|f), table\).*") + allowed_rules += data.get_rules().get_matching(r"open\((d|chest)\).*", r"close\((d|chest)\).*") + allowed_rules += data.get_rules().get_matching(r"lock\((d|chest), k\).*", r"unlock\((d|chest), k\).*") + allowed_rules += data.get_rules().get_matching(r"eat\(f\).*") class Options(ChainingOptions): def get_rules(self, depth): @@ -74,12 +69,26 @@ def get_rules(self, depth): state = build_state(locked_door=False) chains = list(get_chains(state, options)) assert len(chains) == 5 + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I + # open/d > go/south > unlock/c > open/c > insert-P-r-c-k-I + # open/d > go/south > unlock/c > open/c > go/north + # open/d > go/south > unlock/c > go/north + # open/d > go/south > close/d # With more depth. state = build_state(locked_door=False) options.max_depth = 20 chains = list(get_chains(state, options)) assert len(chains) == 9 + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I > go/north > unlock/c > open/c > take/c > go/south > insert > go/north + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I > go/north > unlock/c > open/c > insert-P-r-c-k-I > go/south + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I > go/north > unlock/c > open/c > insert-P-r-c-k-I > go/south + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I > go/north > unlock/c > open/c > go/south + # open/d > go/south > unlock/c > open/c > take/c-P-r-c-k-I > go/north > unlock/c > go/south + # open/d > go/south > unlock/c > open/c > insert-P-r-c-k-I > go/north + # open/d > go/south > unlock/c > open/c > go/north + # open/d > go/south > unlock/c > go/north + # open/d > go/south > close/d def test_applying_actions(): @@ -117,9 +126,9 @@ def test_going_through_door(): options.subquests = True options.create_variables = True options.rules_per_depth = [ - [data.get_rules()["take/c"], data.get_rules()["take/s"]], - data.get_rules().get_matching("go.*"), - [data.get_rules()["open/d"]], + data.get_rules().get_matching(r"take\(o, chest\).*", r"take\(o, table\).*"), + data.get_rules().get_matching(r"go\(.*\).*"), + data.get_rules().get_matching(r"open\(d\).*"), ] chains = list(get_chains(state, options)) @@ -160,8 +169,8 @@ def test_backward_chaining(): options.subquests = True options.create_variables = True options.rules_per_depth = [ - [data.get_rules()["take/c"], data.get_rules()["take/s"]], - [data.get_rules()["open/c"]], + data.get_rules().get_matching(r"take\(o, chest\).*", r"take\(o, table\).*"), + data.get_rules().get_matching(r"open\(chest\).*"), ] options.restricted_types = {"d"} @@ -174,9 +183,9 @@ def test_backward_chaining(): options.subquests = True options.create_variables = True options.rules_per_depth = [ - [data.get_rules()["put"]], - [data.get_rules()["go/north"]], - [data.get_rules()["take/c"]], + data.get_rules().get_matching(r"put\(o, table\).*"), + data.get_rules().get_matching(r"go\(north\).*"), + data.get_rules().get_matching(r"take\(o, chest\).*"), ] options.restricted_types = {"d"} @@ -186,7 +195,20 @@ def test_backward_chaining(): def test_parallel_quests(): logic = GameLogic.parse(""" - type foo { + type t { } + type P { } + type I { } + + type foo : t { + predicates { + not_a(foo); + not_b(foo); + not_c(foo); + a(foo); + b(foo); + c(foo); + } + rules { do_a :: not_a(foo) & $not_c(foo) -> a(foo); do_b :: not_b(foo) & $not_c(foo) -> b(foo); @@ -249,6 +271,11 @@ def test_parallel_quests_navigation(): } type r { + predicates { + at(P, r); + free(r, r); + } + rules { move :: at(P, r) & $free(r, r') -> at(P, r'); } @@ -258,7 +285,15 @@ def test_parallel_quests_navigation(): } } - type o { + type t { + } + + type o : t { + predicates { + at(o, r); + in(o, I); + } + rules { take :: $at(P, r) & at(o, r) -> in(o, I); } @@ -274,7 +309,11 @@ def test_parallel_quests_navigation(): type eggs : o { } - type cake { + type cake : o { + predicates { + in(o, cake); + } + rules { bake :: in(flour, I) & in(eggs, I) -> in(cake, I) & in(flour, cake) & in(eggs, cake); } @@ -292,7 +331,7 @@ def test_parallel_quests_navigation(): Proposition.parse("free(r1: r, r2: r)"), ]) - bake = [logic.rules["bake"]] + bake = [logic.rules["bake.0"]] non_bake = [r for r in logic.rules.values() if r.name != "bake"] options = ChainingOptions() diff --git a/textworld/generator/tests/test_constraints.py b/textworld/generator/tests/test_constraints.py index 45396a19..fd907680 100644 --- a/textworld/generator/tests/test_constraints.py +++ b/textworld/generator/tests/test_constraints.py @@ -33,9 +33,9 @@ def test_constraints(): 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") + chest = Variable("chest", "chest") + cabinet = Variable("cabinet", "chest") + counter = Variable("counter", "table") robe = Variable("robe", "o") # Make sure the number of basic constraints matches the number diff --git a/textworld/generator/tests/test_game.py b/textworld/generator/tests/test_game.py index 334ce6de..a6e9a6bd 100644 --- a/textworld/generator/tests/test_game.py +++ b/textworld/generator/tests/test_game.py @@ -103,7 +103,7 @@ def setUpClass(cls): M.inventory.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) @@ -152,8 +152,6 @@ def test_win_action(self): # Build the quest by providing the actions. actions = chain.actions - if len(actions) != max_depth: - print(chain) assert len(actions) == max_depth, rule.name quest = Quest(actions) tmp_world = World.from_facts(chain.initial_state.facts) @@ -180,8 +178,11 @@ def test_fail_action(self): state = self.game.world.state.copy() assert not state.is_applicable(self.quest.fail_action) - actions = list(state.all_applicable_actions(self.game._rules.values(), - self.game._types.constants_mapping)) + from textworld.logic import Placeholder, Variable + rules = self.game._game_logic.rules.values() + constants_mapping = {Placeholder(t.name): Variable(t.name) for t in self.game._game_logic.types if t.constant} + actions = list(state.all_applicable_actions(rules, constants_mapping)) + for action in actions: state = self.game.world.state.copy() state.apply(action) @@ -213,7 +214,7 @@ def setUpClass(cls): M.inventory.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) @@ -225,7 +226,7 @@ def test_directions_names(self): assert set(self.game.directions_names) == expected def test_objects_types(self): - expected_types = set(data.get_types().types) + expected_types = set(t.name for t in data.get_types()) assert set(self.game.objects_types) == expected_types def test_objects_names(self): @@ -233,7 +234,7 @@ def test_objects_names(self): assert set(self.game.objects_names) == expected_names def test_objects_names_and_types(self): - expected_names_types = {("chest", "c"), ("carrot", "f"), ("wooden door", "d")} + expected_names_types = {("chest", "chest"), ("carrot", "f"), ("wooden door", "d")} assert set(self.game.objects_names_and_types) == expected_names_types def test_verbs(self): @@ -268,7 +269,7 @@ def setUpClass(cls): R1.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) @@ -328,7 +329,7 @@ def setUpClass(cls): R1.add(carrot) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) @@ -436,10 +437,12 @@ def test_game_with_multiple_quests(self): carrot = M.new(type='f', name='carrot') lettuce = M.new(type='f', name='lettuce') + M.add_fact("edible", carrot) + M.add_fact("edible", lettuce) R1.add(carrot, lettuce) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) diff --git a/textworld/generator/tests/test_grammar.py b/textworld/generator/tests/test_grammar.py index e1861896..e2053082 100644 --- a/textworld/generator/tests/test_grammar.py +++ b/textworld/generator/tests/test_grammar.py @@ -23,7 +23,7 @@ def maybe_instantiate_variables(rule, mapping, state, max_types_counts=None): - types_counts = data.get_types().count(state) + types_counts = data.count_types(state) # Instantiate variables if needed try: @@ -77,60 +77,44 @@ def test_propositions(): def test_rules(): - # Make sure the number of basic rules matches the number - # of rules in rules.txt - basic_rules = [k for k in data.get_rules().keys() if "-" not in k] - assert len(basic_rules) == 19 - for rule in data.get_rules().values(): infos = rule.serialize() loaded_rule = Rule.deserialize(infos) assert loaded_rule == rule -def test_get_reverse_rules(verbose=False): +def test_get_reverse_rules(): + constants_mapping = {Placeholder(t.name): Variable(t.name) for t in data.get_types() if t.constant} for rule in data.get_rules().values(): - r_rule = data.get_reverse_rules(rule) - - if verbose: - print(rule, r_rule) + action = maybe_instantiate_variables(rule, constants_mapping.copy(), State([])) + r_action = data.get_reverse_action(action) if rule.name.startswith("eat"): - assert r_rule is None + assert r_action is None else: - # Check if that when applying the reverse rule we can reobtain + assert r_action is not None + + # Check if that when applying the reverse rule we can re-obtain # the previous state. - action = maybe_instantiate_variables(rule, data.get_types().constants_mapping.copy(), State([])) state = State(action.preconditions) new_state = state.copy() assert new_state.apply(action) - assert r_rule is not None - actions = list(new_state.all_applicable_actions([r_rule], data.get_types().constants_mapping)) - if len(actions) != 1: - print(actions) - print(r_rule) - print(new_state) - print(list(new_state.all_instantiations(r_rule, data.get_types().constants_mapping))) - assert len(actions) == 1 r_state = new_state.copy() - r_state.apply(actions[0]) + r_state.apply(r_action) assert state == r_state def test_serialization_deserialization(): - rule = data.get_rules()["go/east"] + constants_mapping = {Placeholder(t.name): Variable(t.name) for t in data.get_types() if t.constant} + rule = data.get_rules()["go(east).0"] mapping = { Placeholder("r'"): Variable("room1", "r"), Placeholder("r"): Variable("room2"), } - mapping.update(data.get_types().constants_mapping) + mapping.update(constants_mapping) action = rule.instantiate(mapping) infos = action.serialize() action2 = Action.deserialize(infos) assert action == action2 - - -if __name__ == "__main__": - test_get_reverse_rules() diff --git a/textworld/generator/tests/test_maker.py b/textworld/generator/tests/test_maker.py index 252b2803..c1a82fbc 100644 --- a/textworld/generator/tests/test_maker.py +++ b/textworld/generator/tests/test_maker.py @@ -38,6 +38,7 @@ def compile_game(game, folder): def test_missing_player(): M = GameMaker() M.new_room() + assert not M.validate() npt.assert_raises(MissingPlayerError, M.build) @@ -72,21 +73,22 @@ def test_adding_the_same_object_multiple_times(): M = GameMaker() room = M.new_room("room") M.set_player(room) - container1 = M.new('c', name='container') - container2 = M.new('c', name="another container") + container1 = M.new('chest', name='container') + container2 = M.new('chest', name="another container") room.add(container1, container2) obj = M.new('o') container1.add(obj) container2.add(obj) - npt.assert_raises(FailedConstraintsError, M.validate) + assert not M.validate() + npt.assert_raises(FailedConstraintsError, M.build) def test_adding_objects(): M = GameMaker() objs = [M.new('o') for _ in range(5)] # Objects to be added. - container = M.new('c') + container = M.new('chest') container.add(*objs) assert all(f.name == "in" for f in container.facts) @@ -96,7 +98,7 @@ def test_adding_objects(): assert all(f.name == "at" for f in room.facts) objs = [M.new('o') for _ in range(5)] # Objects to be added. - supporter = M.new('s') + supporter = M.new('table') supporter.add(*objs) assert all(f.name == "on" for f in supporter.facts) @@ -121,7 +123,7 @@ def test_making_a_small_game(play_the_game=False): R1.add(key) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("closed") R2.add(chest) @@ -165,7 +167,7 @@ def test_record_quest_from_commands(play_the_game=False): M.inventory.add(ball) # Add a closed chest in R2. - chest = M.new(type='c', name='chest') + chest = M.new(type='chest', name='chest') chest.add_property("open") R2.add(chest) diff --git a/textworld/generator/tests/test_text_generation.py b/textworld/generator/tests/test_text_generation.py index ef5f7fee..1f0bf9bf 100644 --- a/textworld/generator/tests/test_text_generation.py +++ b/textworld/generator/tests/test_text_generation.py @@ -24,8 +24,8 @@ def test_used_names_is_updated(verbose=False): r = Variable('r_0', 'r') k1 = Variable('k_1', 'k') k2 = Variable('k_2', 'k') - c1 = Variable('c_1', 'c') - c2 = Variable('c_2', 'c') + c1 = Variable('c_1', 'chest') + c2 = Variable('c_2', 'chest') facts = [Proposition('at', [P, r]), Proposition('at', [k1, r]), Proposition('at', [k2, r]), @@ -35,7 +35,7 @@ def test_used_names_is_updated(verbose=False): Proposition('match', [k2, c2])] world = World.from_facts(facts) world.set_player_room() # Set start room to the middle one. - world.populate_room(10, world.player_room) # Add objects to the starting room. + world.populate_room(5, world.player_room) # Add objects to the starting room. # Generate the world representation. grammar = textworld.generator.make_grammar(flags={}, rng=np.random.RandomState(42)) @@ -83,8 +83,4 @@ def test_blend_instructions(verbose=False): quest.desc = None game.change_grammar(grammar2) quest2 = quest.copy() - assert len(quest1.desc) > len(quest2.desc) - - -if __name__ == "__main__": - test_blend_instructions(verbose=True) + assert len(quest1.desc) > len(quest2.desc) \ No newline at end of file diff --git a/textworld/generator/tests/test_vtypes.py b/textworld/generator/tests/test_vtypes.py deleted file mode 100644 index 16ab8a5b..00000000 --- a/textworld/generator/tests/test_vtypes.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT license. - - -import unittest -import numpy as np -from copy import deepcopy - -from numpy.testing import assert_raises - -from textworld.generator import data -from textworld.logic import State, Proposition, Variable -from textworld.generator.vtypes import VariableTypeTree, VariableType -from textworld.generator.vtypes import parse_variable_types, get_new - -from textworld.generator import testing - - -class TestIntegration(unittest.TestCase): - - def setUp(self): - vtypes = parse_variable_types(testing.VARIABLES_TXT_CONTENT) - self.types = VariableTypeTree(vtypes) - - def test_parse(self): - assert len(self.types) == 12 - assert len(self.types.constants) == 2 - assert len(self.types.variables) == 10 - - def test_variable_types(self): - assert self.types.is_constant("P") - assert self.types.is_constant("I") - assert set(self.types.descendants('o')) == set(['f', 'k']) - assert set(self.types.descendants('c')) == set(['oven']) - assert self.types.is_descendant_of('c', 't') - assert self.types.is_descendant_of('s', 't') - - def test_parents(self): - for vtype in self.types: - for parent in self.types.get_ancestors(vtype): - assert self.types.is_descendant_of(vtype, parent) - - def test_sample(self): - rng = np.random.RandomState(1234) - vtype = self.types.sample("f", rng, include_parent=True) - assert vtype == "f" - - assert_raises(ValueError, self.types.sample, "f", rng, include_parent=False) - - for t in self.types: - for _ in range(30): - vtype = self.types.sample(t, rng, include_parent=True) - assert self.types.is_descendant_of(vtype, t) - - 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 test_serialization_deserialization(self): - data = self.types.serialize() - types2 = VariableTypeTree.deserialize(data) - assert types2.variables_types == types2.variables_types - - -def test_variable_type_parse(): - vtype = VariableType.parse("name: TYPE") - assert vtype.name == "name" - assert vtype.type == "TYPE" - assert vtype.parent is None - assert vtype.is_constant - - vtype = VariableType.parse("name: type -> parent") - assert vtype.name == "name" - assert vtype.type == "type" - assert vtype.parent == "parent" - assert not vtype.is_constant - - -def test_variable_type_serialization_deserialization(): - signature = "name: type -> parent1" - vtype = VariableType.parse(signature) - data = vtype.serialize() - vtype2 = VariableType.deserialize(data) - assert vtype == vtype2 - - -def test_get_new(): - rng = np.random.RandomState(1234) - types_counts = {t: rng.randint(2, 10) for t in data.get_types()} - orig_types_counts = deepcopy(types_counts) - for t in data.get_types(): - name = get_new(t, types_counts) - splits = name.split("_") - assert splits[0] == t - assert int(splits[1]) == orig_types_counts[t] - assert types_counts[t] == orig_types_counts[t] + 1 diff --git a/textworld/generator/tests/test_world.py b/textworld/generator/tests/test_world.py index e4b94c4d..9e48b7f1 100644 --- a/textworld/generator/tests/test_world.py +++ b/textworld/generator/tests/test_world.py @@ -55,7 +55,7 @@ def test_cannot_automatically_positioning_rooms(): def test_get_all_objects_in(): P = Variable("P") room = Variable("room", "r") - chest = Variable("chest", "c") + chest = Variable("chest", "chest") obj = Variable("obj", "o") facts = [Proposition("at", [P, room]), Proposition("at", [chest, room]), @@ -71,7 +71,7 @@ def test_get_all_objects_in(): def test_get_visible_objects_in(): P = Variable("P") room = Variable("room", "r") - chest = Variable("chest", "c") + chest = Variable("chest", "chest") obj = Variable("obj", "o") # Closed chest. diff --git a/textworld/generator/text_generation.py b/textworld/generator/text_generation.py index 2ba3e81b..72b78aa3 100644 --- a/textworld/generator/text_generation.py +++ b/textworld/generator/text_generation.py @@ -13,6 +13,13 @@ from textworld.logic import Placeholder +def _can_hold_objects(obj): + obj_type = data.get_logic().types.get(obj.type) + is_kind_of_container = obj_type.has_supertype_named("container") + is_kind_of_supporter = obj_type.has_supertype_named("supporter") + return is_kind_of_container or is_kind_of_supporter + + class CountOrderedDict(OrderedDict): """ An OrderedDict whose empty items are 0 """ @@ -98,7 +105,7 @@ def assign_description_to_object(obj, grammar, game_infos): game_infos[obj.id].desc = expand_clean_replace(desc_tag, grammar, obj, game_infos) # If we have an openable object, append an additional description - if obj.type in ["c", "d"]: + if data.get_logic().types.get(obj.type).has_supertype_named("openable"): game_infos[obj.id].desc += grammar.expand(" #openable_desc#") @@ -157,7 +164,8 @@ def assign_description_to_room(room, game, grammar): room_desc = expand_clean_replace("#dec#\n\n", grammar, room, game.infos) # Convert the objects into groupings based on adj/noun/type - objs = [o for o in room.content if data.get_types().is_descendant_of(o.type, data.get_types().CLASS_HOLDER)] + + objs = [o for o in room.content if _can_hold_objects(o)] groups = OrderedDict() groups["adj"] = OrderedDict() groups["noun"] = OrderedDict() @@ -200,8 +208,8 @@ def assign_description_to_room(room, game, grammar): for o2 in group_filt: ignore.append(o2.id) - if data.get_types().is_descendant_of(o2.type, data.get_types().CLASS_HOLDER): - for vtype in [o2.type] + data.get_types().get_ancestors(o2.type): + if _can_hold_objects(o2): + for vtype in (o2.type,) + data.get_types().get(o2.type).parents: tag = "#room_desc_({})_multi_{}#".format(vtype, "adj" if type == "noun" else "noun") if grammar.has_tag(tag): desc += expand_clean_replace(" " + tag, grammar, o2, game.infos) @@ -214,7 +222,7 @@ def assign_description_to_room(room, game, grammar): continue if obj.type not in ["P", "I", "d"]: - for vtype in [obj.type] + data.get_types().get_ancestors(obj.type): + for vtype in (obj.type,) + data.get_types().get(obj.type).parents: tag = "#room_desc_({})#".format(vtype) if grammar.has_tag(tag): room_desc += expand_clean_replace(" " + tag, grammar, obj, game.infos) @@ -303,20 +311,20 @@ def generate_instruction(action, grammar, game_infos, world, counts): """ Generate text instruction for a specific action. """ - # Get the more precise command tag. + # Get the most precise command tag. cmd_tag = "#{}#".format(action.name) if not grammar.has_tag(cmd_tag): - cmd_tag = "#{}#".format(action.name.split("-")[0]) + cmd_tag = "#{}#".format(action.name.split(".")[0]) if not grammar.has_tag(cmd_tag): - cmd_tag = "#{}#".format(action.name.split("-")[0].split("/")[0]) + cmd_tag = "#{}#".format(action.name.split("(")[0]) separator_tag = "#action_separator_{}#".format(action.name) if not grammar.has_tag(separator_tag): - separator_tag = "#action_separator_{}#".format(action.name.split("-")[0]) + separator_tag = "#action_separator_{}#".format(action.name.split(".")[0]) if not grammar.has_tag(separator_tag): - separator_tag = "#action_separator_{}#".format(action.name.split("-")[0].split("/")[0]) + separator_tag = "#action_separator_{}#".format(action.name.split("(")[0]) if not grammar.has_tag(separator_tag): separator_tag = "#action_separator#" @@ -341,9 +349,9 @@ def generate_instruction(action, grammar, game_infos, world, counts): # We can use a simple description for the room r = world.find_room_by_id(var.name) # Match on 'name' if r is None: - mapping[ph.name] = '' + mapping[ph.type] = '' else: - mapping[ph.name] = game_infos[r.id].name + mapping[ph.type] = game_infos[r.id].name elif var.type in ["P", "I"]: continue else: @@ -360,19 +368,23 @@ def generate_instruction(action, grammar, game_infos, world, counts): if t == "noun": choices.append(getattr(obj_infos, t)) elif t == "type": - choices.append(data.get_types().get_description(getattr(obj_infos, t))) + # XXX: We need human-readable type. + # data.INFORM7_HUMAN_READABLE_TYPES[getattr(obj_infos, t)] + choices.append(getattr(obj_infos, t)) else: # For adj, we pick an abstraction on the type - atype = data.get_types().get_description(grammar.rng.choice(data.get_types().get_ancestors(obj.type))) + # XXX: We need human-readable type. + atype = grammar.rng.choice(data.get_types().get(obj.type).parents) + # data.INFORM7_HUMAN_READABLE_TYPES[atype] choices.append("{} {}".format(getattr(obj_infos, t), atype)) # If we have no possibilities, use the name (ie. prioritize abstractions) if len(choices) == 0: choices.append(obj_infos.name) - mapping[ph.name] = grammar.rng.choice(choices) + mapping[ph.type] = grammar.rng.choice(choices) else: - mapping[ph.name] = obj_infos.name + mapping[ph.type] = obj_infos.name # Replace the keyword with one of the possibilities for keyword in re.findall(r'[(]\S*[)]', desc + separator): @@ -488,7 +500,7 @@ def is_seq(chain, game_infos): c_action_mapping = data.get_rules()[c.name].match(c) # Update our action name - seq.name += "_{}".format(c.name.split("/")[0]) + seq.name += "_{}".format(c.name.split("(")[0]) # We break a chain if we move rooms if c_action_mapping[room_placeholder] != seq.mapping[room_placeholder]: @@ -534,7 +546,7 @@ def expand_clean_replace(symbol, grammar, obj, game_infos): phrase = phrase.replace("(name-n)", obj_infos.noun if obj_infos.adj is not None else obj_infos.name) phrase = phrase.replace("(name-adj)", obj_infos.adj if obj_infos.adj is not None else grammar.expand("#ordinary_adj#")) if obj.type != "": - phrase = phrase.replace("(name-t)", data.get_types().get_description(obj.type)) + phrase = phrase.replace("(name-t)", obj.type) # XXX: We need human-readable type. else: assert False, "Does this even happen?" diff --git a/textworld/generator/vtypes.py b/textworld/generator/vtypes.py index 04f7ba59..64c3be9e 100644 --- a/textworld/generator/vtypes.py +++ b/textworld/generator/vtypes.py @@ -1,272 +1,275 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT license. +# # Copyright (c) Microsoft Corporation. All rights reserved. +# # Licensed under the MIT license. -from collections import deque, namedtuple -import numpy as np -import re -from typing import List +# from collections import deque, namedtuple +# import numpy as np +# import re +# from typing import List -from textworld.logic import Placeholder, Variable +# from textworld.logic import Placeholder, Variable -_WHITESPACE = re.compile(r"\s+") -_ID = re.compile(r"[\w/']+") -_PUNCT = ["::", ":", "$", "(", ")", ",", "&", "->"] +# _WHITESPACE = re.compile(r"\s+") +# _ID = re.compile(r"[\w/']+") +# _PUNCT = ["::", ":", "$", "(", ")", ",", "&", "->"] -_Token = namedtuple("_Token", ("type", "value")) +# _Token = namedtuple("_Token", ("type", "value")) -def _tokenize(expr): - """ - Helper tokenizer for logical expressions. - """ +# def _tokenize(expr): +# """ +# Helper tokenizer for logical expressions. +# """ - tokens = deque() +# tokens = deque() - i = 0 - while i < len(expr): - m = _WHITESPACE.match(expr, i) - if m: - i = m.end() - continue +# i = 0 +# while i < len(expr): +# m = _WHITESPACE.match(expr, i) +# if m: +# i = m.end() +# continue - m = _ID.match(expr, i) - if m: - tokens.append(_Token("id", m.group())) - i = m.end() - continue +# m = _ID.match(expr, i) +# if m: +# tokens.append(_Token("id", m.group())) +# i = m.end() +# continue - for punct in _PUNCT: - end = i + len(punct) - chunk = expr[i:end] - if chunk == punct: - tokens.append(_Token(chunk, chunk)) - i = end - break - else: - raise ValueError("Unexpected character `{}`.".format(expr[i])) +# for punct in _PUNCT: +# end = i + len(punct) +# chunk = expr[i:end] +# if chunk == punct: +# tokens.append(_Token(chunk, chunk)) +# i = end +# break +# else: +# raise ValueError("Unexpected character `{}`.".format(expr[i])) - return tokens +# return tokens -def _lookahead(tokens, type): - return tokens and tokens[0].type == type +# def _lookahead(tokens, type): +# return tokens and tokens[0].type == type -def _expect(tokens, type): - if type == "id": - human_type = "an identifier" - else: - human_type = "`{}`".format(type) +# def _expect(tokens, type): +# if type == "id": +# human_type = "an identifier" +# else: +# human_type = "`{}`".format(type) - if not tokens: - raise ValueError("Expected {}; found end of input.".format(human_type)) +# if not tokens: +# raise ValueError("Expected {}; found end of input.".format(human_type)) - if tokens[0].type != type: - raise ValueError("Expected {}; found `{}`.".format(human_type, tokens[0].value)) +# if tokens[0].type != type: +# raise ValueError("Expected {}; found `{}`.".format(human_type, tokens[0].value)) - return tokens.popleft() +# return tokens.popleft() class NotEnoughNounsError(NameError): pass -class VariableType: - def __init__(self, type, name, parent=None): - self.type = type - self.name = name - self.parent = parent - self.children = [] - # If the type starts with an upper case letter, it is a constant. - self.is_constant = self.type[0] == self.type.upper()[0] - - @classmethod - def parse(cls, expr: str) -> "VariableType": - """ - Parse a variable type expression. - - Parameters - ---------- - expr : - The string to parse, in the form `name: type -> parent1 & parent2` - or `name: type` for root node. - """ - tokens = _tokenize(expr) - name = _expect(tokens, "id").value - _expect(tokens, ":") - type = _expect(tokens, "id").value - - parent = None - if _lookahead(tokens, "->"): - tokens.popleft() - parent = _expect(tokens, "id").value - - return cls(type, name, parent) - - def __eq__(self, other): - return (isinstance(other, VariableType) and - self.name == other.name and - self.type == other.type and - self.parent == other.parent) - - def __str__(self): - signature = "{}: {}".format(self.name, self.type) - if self.parent is not None: - signature += " -> " + self.parent - - return signature - - def serialize(self) -> str: - return str(self) - - @classmethod - def deserialize(cls, data: str) -> "VariableType": - return cls.parse(data) - - -def parse_variable_types(content: str): - """ - Parse a list VariableType expressions. - """ - vtypes = [] - for line in content.split("\n"): - line = line.strip() - if line.startswith("#") or line == "": - continue - - vtypes.append(VariableType.parse(line)) - - return vtypes - - -class VariableTypeTree: - """ - Manages hierarchy of types defined in ./grammars/variables.txt. - Used for extending the rules. - """ - CHEST = 'c' - SUPPORTER = 's' - CLASS_HOLDER = [CHEST, SUPPORTER] - - def __init__(self, vtypes: List[VariableType]): - self.variables_types = {vtype.type: vtype for vtype in vtypes} - - # Make some convenient attributes. - self.types = [vt.type for vt in vtypes] - self.names = [vt.name for vt in vtypes] - self.constants = [t for t in self if self.is_constant(t)] - self.variables = [t for t in self if not self.is_constant(t)] - self.constants_mapping = {Placeholder(c): Variable(c) for c in self.constants} - - # Adjust variable type's parent and children references. - for vt in vtypes: - if vt.parent is not None: - vt_parent = self[vt.parent] - vt_parent.children.append(vt.type) - - @classmethod - def load(cls, path: str): - """ - Read variables from text file. - """ - with open(path) as f: - vtypes = parse_variable_types(f.read()) - return cls(vtypes) - - def __getitem__(self, vtype): - """ Get VariableType object from its type string. """ - vtype = vtype.rstrip("'") - return self.variables_types[vtype] - - def __contains__(self, vtype): - vtype = vtype.rstrip("'") - return vtype in self.variables_types - - def __iter__(self): - return iter(self.variables_types) - - def __len__(self): - return len(self.variables_types) - - def is_constant(self, vtype): - return self[vtype].is_constant - - def descendants(self, vtype): - """Given a variable type, return all possible descendants.""" - descendants = [] - for child_type in self[vtype].children: - descendants.append(child_type) - descendants += self.descendants(child_type) - - return descendants - - def get_description(self, vtype): - if vtype in self.types: - return self.names[self.types.index(vtype)] - else: - return vtype - - def get_ancestors(self, vtype): - """ List all ancestors of a type where the closest ancetors are first. """ - vtypes = [] - if self[vtype].parent is not None: - vtypes.append(self[vtype].parent) - vtypes.extend(self.get_ancestors(self[vtype].parent)) - - return vtypes - - def is_descendant_of(self, child, parents): - """ Return if child is a descendant of parent """ - if not isinstance(parents, list): - parents = [parents] - - for parent in parents: - if child == parent or child in self.descendants(parent): - return True - - return False - - def sample(self, parent_type, rng, exceptions=[], include_parent=True, probs=None): - """ Sample an object type given the parent's type. """ - types = self.descendants(parent_type) - if include_parent: - types = [parent_type] + types - types = [t for t in types if t not in exceptions] - - if probs is not None: - probs = np.array([probs[t] for t in types], dtype="float") - probs /= np.sum(probs) - - return rng.choice(types, p=probs) - - def count(self, state): - """ Counts how many objects there are of each type. """ - types_counts = {t: 0 for t in self} - for var in state.variables: - if self.is_constant(var.type): - continue - - if "_" not in var.name: - continue - - cpt = int(var.name.split("_")[-1]) - var_type = var.type - types_counts[var_type] = max(cpt + 1, types_counts[var_type]) - - return types_counts - - def serialize(self) -> List: - return [vtype.serialize() for vtype in self.variables_types.values()] +# class VariableType: +# def __init__(self, type, name, parent=None): +# self.type = type +# self.name = name +# self.parent = parent +# self.children = [] +# # If the type starts with an upper case letter, it is a constant. +# self.is_constant = self.type[0] == self.type.upper()[0] + +# @classmethod +# def parse(cls, expr: str) -> "VariableType": +# """ +# Parse a variable type expression. + +# Parameters +# ---------- +# expr : +# The string to parse, in the form `name: type -> parent1 & parent2` +# or `name: type` for root node. +# """ +# tokens = _tokenize(expr) +# name = _expect(tokens, "id").value +# _expect(tokens, ":") +# type = _expect(tokens, "id").value + +# parent = None +# if _lookahead(tokens, "->"): +# tokens.popleft() +# parent = _expect(tokens, "id").value + +# return cls(type, name, parent) + +# def __eq__(self, other): +# return (isinstance(other, VariableType) and +# self.name == other.name and +# self.type == other.type and +# self.parent == other.parent) + +# def __str__(self): +# signature = "{}: {}".format(self.name, self.type) +# if self.parent is not None: +# signature += " -> " + self.parent + +# return signature + +# def serialize(self) -> str: +# return str(self) + +# @classmethod +# def deserialize(cls, data: str) -> "VariableType": +# return cls.parse(data) + + +# def parse_variable_types(content: str): +# """ +# Parse a list VariableType expressions. +# """ +# vtypes = [] +# for line in content.split("\n"): +# line = line.strip() +# if line.startswith("#") or line == "": +# continue + +# vtypes.append(VariableType.parse(line)) + +# return vtypes + + +# class VariableTypeTree: +# """ +# Manages hierarchy of types defined in ./grammars/variables.txt. +# Used for extending the rules. +# """ +# CHEST = 'c' +# SUPPORTER = 's' +# CLASS_HOLDER = [CHEST, SUPPORTER] + +# def __init__(self, vtypes: List[VariableType]): +# self.variables_types = {vtype.type: vtype for vtype in vtypes} + +# # Make some convenient attributes. +# self.types = [vt.type for vt in vtypes] +# self.names = [vt.name for vt in vtypes] +# self.constants = [t for t in self if self.is_constant(t)] +# self.variables = [t for t in self if not self.is_constant(t)] +# self.constants_mapping = {Placeholder(c): Variable(c) for c in self.constants} + +# # Adjust variable type's parent and children references. +# for vt in vtypes: +# if vt.parent is not None: +# vt_parent = self[vt.parent] +# vt_parent.children.append(vt.type) + +# @classmethod +# def load(cls, path: str): +# """ +# Read variables from text file. +# """ +# with open(path) as f: +# vtypes = parse_variable_types(f.read()) +# return cls(vtypes) + +# def __getitem__(self, vtype): +# """ Get VariableType object from its type string. """ +# vtype = vtype.rstrip("'") +# return self.variables_types[vtype] + +# def __contains__(self, vtype): +# vtype = vtype.rstrip("'") +# return vtype in self.variables_types + +# def __iter__(self): +# return iter(self.variables_types) + +# def __len__(self): +# return len(self.variables_types) + +# def is_constant(self, vtype): +# return self[vtype].is_constant + +# def descendants(self, vtype): +# """Given a variable type, return all possible descendants.""" +# descendants = [] +# for child_type in self[vtype].children: +# descendants.append(child_type) +# descendants += self.descendants(child_type) + +# return descendants + +# def get_description(self, vtype): +# if vtype in self.types: +# return self.names[self.types.index(vtype)] +# else: +# return vtype + +# def get_ancestors(self, vtype): +# """ List all ancestors of a type where the closest ancetors are first. """ +# vtypes = [] +# if self[vtype].parent is not None: +# vtypes.append(self[vtype].parent) +# vtypes.extend(self.get_ancestors(self[vtype].parent)) + +# return vtypes + +# def is_descendant_of(self, child, parents): +# """ Return if child is a descendant of parent """ +# if not isinstance(parents, list): +# parents = [parents] + +# for parent in parents: +# if child == parent or child in self.descendants(parent): +# return True + +# return False + +# def sample(self, parent_type, rng, exceptions=[], include_parent=True, probs=None): +# """ Sample an object type given the parent's type. """ +# types = self.descendants(parent_type) +# if include_parent: +# types = [parent_type] + types +# types = [t for t in types if t not in exceptions] + +# if probs is not None: +# probs = np.array([probs[t] for t in types], dtype="float") +# probs /= np.sum(probs) + +# return rng.choice(types, p=probs) + +# def count(self, state): +# """ Counts how many objects there are of each type. """ +# types_counts = {t: 0 for t in self} +# for var in state.variables: +# if self.is_constant(var.type): +# continue + +# if "_" not in var.name: +# continue + +# cpt = int(var.name.split("_")[-1]) +# var_type = var.type +# types_counts[var_type] = max(cpt + 1, types_counts[var_type]) + +# return types_counts + +# def serialize(self) -> List: +# return [vtype.serialize() for vtype in self.variables_types.values()] - @classmethod - def deserialize(cls, data: List) -> "VariableTypeTree": - vtypes = [VariableType.deserialize(d) for d in data] - return cls(vtypes) +# @classmethod +# def deserialize(cls, data: List) -> "VariableTypeTree": +# vtypes = [VariableType.deserialize(d) for d in data] +# return cls(vtypes) def get_new(type, types_counts, max_types_counts=None): """ Get the next available id for a given type. """ + if type not in types_counts: + types_counts[type] = 0 + if max_types_counts is not None and types_counts[type] >= max_types_counts[type]: raise NotEnoughNounsError() diff --git a/textworld/generator/world.py b/textworld/generator/world.py index 4fd1ec63..0eaf94e6 100644 --- a/textworld/generator/world.py +++ b/textworld/generator/world.py @@ -23,6 +23,18 @@ class NoFreeExitError(Exception): pass +def _can_hold_objects(type_name: str) -> bool: + obj_type = data.get_logic().types.get(type_name) + is_kind_of_container = obj_type.has_supertype_named("container") + is_kind_of_supporter = obj_type.has_supertype_named("supporter") + return is_kind_of_container or is_kind_of_supporter + + +def _is_lockable(type_name: str) -> bool: + obj_type = data.get_logic().types.get(type_name) + return obj_type.has_supertype_named("lockable") + + def connect(room1: Variable, direction: str, room2: Variable, door: Optional[Variable] = None) -> List[Proposition]: """ Generate predicates that connect two rooms. @@ -252,7 +264,7 @@ def _update(self) -> None: def _process_rooms(self) -> None: for fact in self.facts: - if not data.get_types().is_descendant_of(fact.arguments[0].type, 'r'): + if not data.get_types().get(fact.arguments[0].type).has_supertype_named('r'): continue # Skip non room facts. room = self._get_room(fact.arguments[0]) @@ -314,7 +326,7 @@ def _process_rooms(self) -> None: def _process_objects(self) -> None: for fact in self.facts: - if data.get_types().is_descendant_of(fact.arguments[0].type, 'r'): + if data.get_types().get(fact.arguments[0].type).has_supertype_named('r'): continue # Skip room facts. obj = self._get_entity(fact.arguments[0]) @@ -387,7 +399,7 @@ def populate_room(self, nb_objects: int, room: Variable, object_types_probs: Optional[Dict[str, float]] = None) -> List[Proposition]: rng = g_rng.next() if rng is None else rng state = [] - types_counts = data.get_types().count(self.state) + types_counts = data.count_types(self.state) inventory = Variable("I", "I") objects_holder = [inventory, room] @@ -396,11 +408,11 @@ def populate_room(self, nb_objects: int, room: Variable, 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: + if s.name == "at" and _can_hold_objects(s.arguments[0].type) 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: + if s.name == "at" and _is_lockable(s.arguments[0].type) 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]) @@ -414,12 +426,13 @@ def populate_room(self, nb_objects: int, room: Variable, # Prioritize adding key if there are locked or closed things in the room. obj_type = "k" else: - obj_type = data.get_types().sample(parent_type='t', rng=rng, exceptions=["d", "r"], - include_parent=False, probs=object_types_probs) + obj_type = data.sample_type(parent_type='t', rng=rng, exceptions=["d", "r"], + include_parent=False, probs=object_types_probs) - if data.get_types().is_descendant_of(obj_type, "o"): - obj_name = get_new(obj_type, types_counts) - obj = Variable(obj_name, obj_type) + obj_type = data.get_logic().types.get(obj_type) + if obj_type.has_supertype_named("portable"): + obj_name = get_new(obj_type.name, types_counts) + obj = Variable(obj_name, obj_type.name) allowed_objects_holder = list(objects_holder) if obj_type == "k": @@ -448,26 +461,27 @@ def populate_room(self, nb_objects: int, room: Variable, # Place the object somewhere. obj_holder = rng.choice(allowed_objects_holder) - if data.get_types().is_descendant_of(obj_holder.type, "s"): + obj_holder_type = data.get_logic().types.get(obj_holder.type) + if obj_holder_type.has_supertype_named("supporter"): state.append(Proposition("on", [obj, obj_holder])) - elif data.get_types().is_descendant_of(obj_holder.type, "c"): + elif obj_holder_type.has_supertype_named("container"): state.append(Proposition("in", [obj, obj_holder])) - elif data.get_types().is_descendant_of(obj_holder.type, "I"): + elif obj_holder_type.has_supertype_named("I"): state.append(Proposition("in", [obj, obj_holder])) - elif data.get_types().is_descendant_of(obj_holder.type, "r"): + elif obj_holder_type.has_supertype_named("r"): state.append(Proposition("at", [obj, obj_holder])) else: raise ValueError("Unknown type for object holder: {}".format(obj_holder)) - elif data.get_types().is_descendant_of(obj_type, "s"): - supporter_name = get_new(obj_type, types_counts) - supporter = Variable(supporter_name, obj_type) + elif obj_type.has_supertype_named("supporter"): + supporter_name = get_new(obj_type.name, types_counts) + supporter = Variable(supporter_name, obj_type.name) state.append(Proposition("at", [supporter, room])) objects_holder.append(supporter) - elif data.get_types().is_descendant_of(obj_type, "c"): - container_name = get_new(obj_type, types_counts) - container = Variable(container_name, obj_type) + elif obj_type.has_supertype_named("container"): + container_name = get_new(obj_type.name, types_counts) + container = Variable(container_name, obj_type.name) state.append(Proposition("at", [container, room])) objects_holder.append(container) @@ -479,7 +493,7 @@ def populate_room(self, nb_objects: int, room: Variable, locked_or_closed_objects.append(container) else: - raise ValueError("Unknown object type: {}".format(obj_type)) + raise ValueError("Unknown object type: {}".format(obj_type.name)) object_id += 1 @@ -514,11 +528,11 @@ def populate_room_with(self, objects: WorldObject, room: WorldRoom, 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: + if s.name == "at" and _can_hold_objects(s.arguments[0].type) 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: + if s.name == "at" and _is_lockable(s.arguments[0].type) 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]) @@ -530,28 +544,29 @@ def populate_room_with(self, objects: WorldObject, room: WorldRoom, rng.shuffle(remaining_objects_id) for idx in remaining_objects_id: obj = objects[idx] - obj_type = obj.type + obj_type = data.get_logic().types.get(obj.type) - if data.get_types().is_descendant_of(obj_type, "o"): + if obj_type.has_supertype_named("portable"): allowed_objects_holder = list(objects_holder) # Place the object somewhere. obj_holder = rng.choice(allowed_objects_holder) - if data.get_types().is_descendant_of(obj_holder.type, "s"): + obj_holder_type = data.get_logic().types.get(obj_holder.type) + if obj_holder_type.has_supertype_named("supporter"): state.append(Proposition("on", [obj, obj_holder])) - elif data.get_types().is_descendant_of(obj_holder.type, "c"): + elif obj_holder_type.has_supertype_named("container"): state.append(Proposition("in", [obj, obj_holder])) - elif data.get_types().is_descendant_of(obj_holder.type, "r"): + elif obj_holder_type.has_supertype_named("r"): state.append(Proposition("at", [obj, obj_holder])) else: raise ValueError("Unknown type for object holder: {}".format(obj_holder)) - elif data.get_types().is_descendant_of(obj_type, "s"): + elif obj_type.has_supertype_named("supporter"): supporter = obj state.append(Proposition("at", [supporter, room])) objects_holder.append(supporter) - elif data.get_types().is_descendant_of(obj_type, "c"): + elif obj_type.has_supertype_named("container"): container = obj state.append(Proposition("at", [container, room])) objects_holder.append(container) diff --git a/textworld/logic/__init__.py b/textworld/logic/__init__.py index 0d16094e..3b566b33 100644 --- a/textworld/logic/__init__.py +++ b/textworld/logic/__init__.py @@ -221,6 +221,13 @@ def __init__(self, name: str, parents: Iterable[str]): def _attach(self, hier: "TypeHierarchy"): self._hier = hier + @property + def constant(self) -> bool: + """ + Whether this type is a constant. + """ + return self.name == self.name.upper() + @property def parent_types(self) -> Iterable["Type"]: """ @@ -338,7 +345,7 @@ def __len__(self): return len(self._types) def closure(self, type: Type, expand: Callable[[Type], Iterable[Type]]) -> Iterable[Type]: - """ + r""" Compute the transitive closure in a type lattice according to some type relationship (generally direct sub-/super-types). @@ -367,7 +374,7 @@ def _multi_expand(self, types: Collection[Type], expand: Callable[[Type], Iterab yield tuple(expansion) def multi_closure(self, types: Collection[Type], expand: Callable[[Type], Iterable[Type]]) -> Iterable[Collection[Type]]: - """ + r""" Compute the transitive closure of a sequence of types in a type lattice induced by some per-type relationship (generally direct sub-/super-types). @@ -395,7 +402,6 @@ def multi_closure(self, types: Collection[Type], expand: Callable[[Type], Iterab return self._bfs_unique(types, lambda ts: self._multi_expand(ts, expand)) - def _bfs_unique(self, start, expand): """ Apply breadth-first search, returning only previously unseen nodes. @@ -833,7 +839,7 @@ def substitute(self, mapping: Mapping[Placeholder, Placeholder]) -> "Predicate": params = [mapping.get(param, param) for param in self.parameters] return Predicate(self.name, params) - def instantiate(self, mapping: Mapping[Placeholder, Variable]) -> Proposition: + def instantiate(self, mapping: Optional[Mapping[Placeholder, Variable]] = None) -> Proposition: """ Instantiate this predicate with the given mapping. @@ -841,12 +847,17 @@ def instantiate(self, mapping: Mapping[Placeholder, Variable]) -> Proposition: ---------- mapping : A mapping from Placeholders to Variables. + If None, the name and type of the Placeholders will be used to create + dummy Variables. Returns ------- The instantiated Proposition with each Placeholder mapped to the corresponding Variable. """ + if mapping is None: + mapping = {param: Variable(param.name, param.type) for param in self.parameters} + args = [mapping[param] for param in self.parameters] return Proposition._instantiate(self, args) @@ -886,6 +897,16 @@ def __str__(self): def __repr__(self): return "Alias({!r}, {!r})".format(self.pattern, self.replacement) + def __eq__(self, other): + if isinstance(other, Alias): + return (self.pattern == other.pattern and + self.replacement == other.replacement) + else: + return NotImplemented + + def __hash__(self): + return hash((self.pattern, frozenset(self.replacement))) + def expand(self, predicate: Predicate) -> Collection[Predicate]: """ Expand a use of this alias into its replacement. @@ -1036,6 +1057,7 @@ def __init__(self, name: str, preconditions: Iterable[Predicate], postconditions """ self.name = name + self.parent = None self.preconditions = tuple(preconditions) self.postconditions = tuple(postconditions) @@ -1116,7 +1138,9 @@ def substitute(self, mapping: Mapping[Placeholder, Placeholder], name=None) -> " name = self.name pre_subst = [pred.substitute(mapping) for pred in self.preconditions] post_subst = [pred.substitute(mapping) for pred in self.postconditions] - return Rule(name, pre_subst, post_subst) + rule = Rule(name, pre_subst, post_subst) + rule.parent = self + return rule def instantiate(self, mapping: Mapping[Placeholder, Variable]) -> Action: """ @@ -1184,238 +1208,6 @@ def inverse(self, name=None) -> "Rule": return Rule(name, self.postconditions, self.preconditions) -class Inform7Type: - """ - Information about an Inform 7 kind. - """ - - def __init__(self, name: str, kind: str, definition: Optional[str] = None): - self.name = name - self.kind = kind - self.definition = definition - - -class Inform7Predicate: - """ - Information about an Inform 7 predicate. - """ - - def __init__(self, predicate: Predicate, source: str): - self.predicate = predicate - self.source = source - - def __str__(self): - return '{} :: "{}"'.format(self.predicate, self.source) - - def __repr__(self): - return "Inform7Predicate({!r}, {!r})".format(self.predicate, self.source) - - -class Inform7Command: - """ - Information about an Inform 7 command. - """ - - def __init__(self, rule: str, command: str, event: str): - self.rule = rule - self.command = command - self.event = event - - def __str__(self): - return '{} :: "{}" :: "{}"'.format(self.rule, self.command, self.event) - - def __repr__(self): - return "Inform7Command({!r}, {!r}, {!r})".format(self.rule, self.command, self.event) - - -class Inform7Logic: - """ - The Inform 7 bindings of a GameLogic. - """ - - def __init__(self): - self.types = {} - self.predicates = {} - self.commands = {} - self.code = "" - - def _add_type(self, i7type: Inform7Type): - if i7type.name in self.types: - raise ValueError("Duplicate Inform 7 type for {}".format(i7type.name)) - self.types[i7type.name] = i7type - - def _add_predicate(self, i7pred: Inform7Predicate): - sig = i7pred.predicate.signature - if sig in self.predicates: - raise ValueError("Duplicate Inform 7 predicate for {}".format(sig)) - self.predicates[sig] = i7pred - - def _add_command(self, i7cmd: Inform7Command): - rule_name = i7cmd.rule - if rule_name in self.commands: - raise ValueError("Duplicate Inform 7 command for {}".format(rule_name)) - self.commands[rule_name] = i7cmd - - def _add_code(self, code: str): - self.code += code + "\n" - - def _initialize(self, logic): - self._expand_predicates(logic) - self._expand_commands(logic) - - def _expand_predicates(self, logic): - for sig, pred in list(self.predicates.items()): - params = pred.predicate.parameters - types = [logic.types.get(ph.type) for ph in params] - for descendant in logic.types.multi_descendants(types): - mapping = {ph: Placeholder(ph.name, type.name) for ph, type in zip(params, descendant)} - expanded = pred.predicate.substitute(mapping) - self._add_predicate(Inform7Predicate(expanded, pred.source)) - - def _expand_commands(self, logic): - for name, command in list(self.commands.items()): - rule = logic.rules.get(name) - if not rule: - continue - - types = [logic.types.get(ph.type) for ph in rule.placeholders] - descendants = logic.types.multi_descendants(types) - for descendant in descendants: - type_names = [type.name for type in descendant] - expanded_name = name + "-" + "-".join(type_names) - self._add_command(Inform7Command(expanded_name, command.command, command.event)) - - -class GameLogic: - """ - The logic for a game (types, rules, etc.). - """ - - def __init__(self): - self.types = TypeHierarchy() - self.predicates = set() - self.aliases = {} - self.rules = {} - self.reverse_rules = {} - self.constraints = {} - self.inform7 = Inform7Logic() - - def _add_predicate(self, signature: Signature): - if signature in self.predicates: - raise ValueError("Duplicate predicate {}".format(signature)) - if signature in self.aliases: - raise ValueError("Predicate {} is also an alias".format(signature)) - self.predicates.add(signature) - - def _add_alias(self, alias: Alias): - sig = alias.pattern.signature - if sig in self.aliases: - raise ValueError("Duplicate alias {}".format(alias)) - if sig in self.predicates: - raise ValueError("Alias {} is also a predicate".format(alias)) - self.aliases[sig] = alias - - def _add_rule(self, rule: Rule): - if rule.name in self.rules: - raise ValueError("Duplicate rule {}".format(rule)) - self.rules[rule.name] = rule - - def _add_reverse_rule(self, rule_name, reverse_name): - if rule_name in self.reverse_rules: - raise ValueError("Duplicate reverse rule {}".format(rule_name)) - if reverse_name in self.reverse_rules: - raise ValueError("Duplicate reverse rule {}".format(reverse_name)) - self.reverse_rules[rule_name] = reverse_name - self.reverse_rules[reverse_name] = rule_name - - def _add_constraint(self, constraint: Rule): - if constraint.name in self.constraints: - raise ValueError("Duplicate constraint {}".format(constraint)) - self.constraints[constraint.name] = constraint - - def _parse(self, document: str, path: Optional[str] = None): - model = _PARSER.parse(document, filename=path) - _ModelConverter(self).walk(model) - - def _initialize(self): - self.aliases = {sig: self._expand_alias(alias) for sig, alias in self.aliases.items()} - - self.rules = {name: self.normalize_rule(rule) for name, rule in self.rules.items()} - self.constraints = {name: self.normalize_rule(rule) for name, rule in self.constraints.items()} - - self._expand_rules(self.rules) - self._expand_rules(self.constraints) - - self.inform7._initialize(self) - - def _expand_alias(self, alias): - return Alias(alias.pattern, self._expand_alias_recursive(alias.replacement, set())) - - def _expand_alias_recursive(self, predicates, used): - result = [] - - for pred in predicates: - sig = pred.signature - - if sig in used: - raise ValueError("Cycle of aliases involving {}".format(sig)) - - alias = self.aliases.get(pred.signature) - if alias: - expansion = alias.expand(pred) - used.add(pred.signature) - result.extend(self._expand_alias_recursive(expansion, used)) - used.remove(pred.signature) - else: - result.append(pred) - - return result - - def normalize_rule(self, rule: Rule) -> Rule: - pre = self._normalize_predicates(rule.preconditions) - post = self._normalize_predicates(rule.postconditions) - return Rule(rule.name, pre, post) - - def _normalize_predicates(self, predicates): - result = [] - for pred in predicates: - alias = self.aliases.get(pred.signature) - if alias: - result.extend(alias.expand(pred)) - else: - result.append(pred) - return result - - def _expand_rules(self, rules): - # Expand rules with variations for descendant types - for rule in list(rules.values()): - placeholders = rule.placeholders - types = [self.types.get(ph.type) for ph in placeholders] - descendants = self.types.multi_descendants(types) - for descendant in descendants: - names = [type.name for type in descendant] - new_name = rule.name + "-" + "-".join(names) - mapping = {ph: Placeholder(ph.name, type.name) for ph, type in zip(placeholders, descendant)} - new_rule = rule.substitute(mapping, new_name) - rules[new_name] = new_rule - - @classmethod - def parse(cls, document: str) -> "GameLogic": - result = cls() - result._parse(document) - result._initialize() - return result - - @classmethod - def load(cls, paths: Iterable[str]): - result = cls() - for path in paths: - with open(path, "r") as f: - result._parse(f.read(), path=path) - result._initialize() - return result - - class State: """ The current state of a world. @@ -1818,3 +1610,356 @@ def __str__(self): lines.append("})") return "\n".join(lines) + + +class Inform7Type: + """ + Information about an Inform 7 kind. + """ + + def __init__(self, name: str, kind: str, definition: Optional[str] = None): + self.name = name + self.kind = kind + self.definition = definition + + +class Inform7Predicate: + """ + Information about an Inform 7 predicate. + """ + + def __init__(self, predicate: Predicate, source: str): + self.predicate = predicate + self.source = source + + def __str__(self): + return '{} :: "{}"'.format(self.predicate, self.source) + + def __repr__(self): + return "Inform7Predicate({!r}, {!r})".format(self.predicate, self.source) + + +class Inform7Command: + """ + Information about an Inform 7 command. + """ + + def __init__(self, rule: str, command: str, event: str): + self.rule = rule + self.command = command + self.event = event + + def __str__(self): + return '{} :: "{}" :: "{}"'.format(self.rule, self.command, self.event) + + def __repr__(self): + return "Inform7Command({!r}, {!r}, {!r})".format(self.rule, self.command, self.event) + + +class Inform7Logic: + """ + The Inform 7 bindings of a GameLogic. + """ + + def __init__(self): + self.types = {} + self.predicates = {} + self.commands = {} + self.code = "" + + def _add_type(self, i7type: Inform7Type): + if i7type.name in self.types: + raise ValueError("Duplicate Inform 7 type for {}".format(i7type.name)) + self.types[i7type.name] = i7type + + def _add_predicate(self, i7pred: Inform7Predicate): + sig = i7pred.predicate.signature + if sig in self.predicates: + raise ValueError("Duplicate Inform 7 predicate for {}".format(sig)) + self.predicates[sig] = i7pred + + def _add_command(self, i7cmd: Inform7Command): + rule_name = i7cmd.rule + if rule_name in self.commands: + raise ValueError("Duplicate Inform 7 command for {}".format(rule_name)) + self.commands[rule_name] = i7cmd + + def _add_code(self, code: str): + self.code += code + "\n" + + def _expand_predicates(self, sig, new_sig): + i7_pred = self.predicates.get(sig) + if i7_pred: + predicate = Predicate(sig.name, [Placeholder(t1, t2) for t1, t2 in zip(sig.types, new_sig.types)]) + #predicate = Predicate.parse(str(new_sig)) + #for p1, p2 in zip(predicate.parameters, i7_pred.predicate.parameters): + # p1.name = p2.name + + self._add_predicate(Inform7Predicate(predicate, i7_pred.source)) + + def _expand_command(self, rule, new_rule): + command = self.commands.get(rule.name) + if command: + self._add_command(Inform7Command(new_rule.name, command.command, command.event)) + + +class GameLogic: + """ + The logic for a game (types, rules, etc.). + """ + + def __init__(self): + self.types = TypeHierarchy() + self.predicates = set() + self.aliases = defaultdict(list) + self.rules = {} + self.reverse_rules = {} + self.constraints = {} + self.inform7 = Inform7Logic() + self._document = "" + + def _add_predicate(self, signature: Signature): + if signature in self.predicates: + raise ValueError("Duplicate predicate {}".format(signature)) + if signature in self.aliases: + raise ValueError("Predicate {} is also an alias".format(signature)) + self.predicates.add(signature) + + def _add_alias(self, alias: Alias): + sig = alias.pattern.signature + if sig in self.predicates: + raise ValueError("Alias {} is also a predicate".format(alias)) + self.aliases[sig].append(alias) + + def _add_rule(self, rule: Rule): + if rule.name in self.rules: + raise ValueError("Duplicate rule {}".format(rule)) + self.rules[rule.name] = rule + + def _add_reverse_rule(self, rule_name, reverse_name): + if rule_name in self.reverse_rules: + raise ValueError("Duplicate reverse rule {}".format(rule_name)) + if reverse_name in self.reverse_rules: + raise ValueError("Duplicate reverse rule {}".format(reverse_name)) + self.reverse_rules[rule_name] = reverse_name + self.reverse_rules[reverse_name] = rule_name + + def _add_constraint(self, constraint: Rule): + if constraint.name in self.constraints: + raise ValueError("Duplicate constraint {}".format(constraint)) + self.constraints[constraint.name] = constraint + + def _parse(self, document: str, path: Optional[str] = None): + model = _PARSER.parse(document, filename=path) + _ModelConverter(self).walk(model) + + def _initialize(self): + self._expand_predicates(self.predicates) + + self._expand_aliases(self.aliases) + self.aliases = self._normalize_aliases(self.aliases) + + self._expand_rules(self.rules) + self._expand_rules(self.constraints) + self.rules = self._normalize_rules(self.rules) + self.constraints = self._normalize_constraints(self.constraints) + + def _check_constraints(self, rule: Rule) -> bool: + """Check that the pre and postconditions of the rule satisfy the constraints.""" + + state = State(predicate.instantiate() for predicate in rule.preconditions) + failed_constraints = list(state.all_applicable_actions(self.constraints.values())) + if len(failed_constraints) > 0: + return False # At least one constraint was violated. + + state = State(predicate.instantiate() for predicate in rule.postconditions) + failed_constraints = list(state.all_applicable_actions(self.constraints.values())) + if len(failed_constraints) > 0: + return False # At least one constraint was violated. + + return True + + def _normalize_aliases(self, map_of_aliases): + expanded_aliases = {} + for sig, aliases in map_of_aliases.items(): + expanded_aliases[sig] = [expanded_alias for alias in aliases for expanded_alias in self._normalize_alias(alias)] + + return expanded_aliases + + def _normalize_alias(self, alias): + for expansion in self._normalize_alias_recursive(alias.replacement, set()): + if any(p for p in expansion if p.signature not in self.predicates): + continue # Skip alias that uses undefined predicates. + + yield Alias(alias.pattern, expansion) + + def _normalize_alias_recursive(self, predicates, used): + results = [] + + for pred in predicates: + sig = pred.signature + + if sig in used: + raise ValueError("Cycle of aliases involving {}".format(sig)) + + aliases = self.aliases.get(pred.signature) + if aliases: + used.add(pred.signature) + results.append([]) + for alias in aliases: + expansion = alias.expand(pred) + results[-1].extend(self._normalize_alias_recursive(expansion, used)) + used.remove(pred.signature) + else: + results.append([(pred,)]) + + for result in itertools.product(*results): + yield list(itertools.chain(*result)) + + def _normalize_rules(self, rules): + normalized_rules = {} + for name, rule in rules.items(): + for i, normalized_rule in enumerate(self._normalize_rule(rule)): + normalized_rule.name = name + ".{}".format(i) + normalized_rules[normalized_rule.name] = normalized_rule + self.inform7._expand_command(rule, normalized_rule) + + return normalized_rules + + def _normalize_rule(self, rule: Rule) -> Rule: + pre_list = self._normalize_predicates(rule.preconditions) + post_list = self._normalize_predicates(rule.postconditions) + for pre, post in zip(itertools.product(*pre_list), itertools.product(*post_list)): + rule = Rule(rule.name, itertools.chain(*pre), itertools.chain(*post)) + if any(p for p in rule.all_predicates if p.signature not in self.predicates): + continue # Skip rule that uses undefined predicates. + + if any(t for predicate in rule.all_predicates for t in predicate.types + if (not self.types.get(t).has_supertype_named("t") and + not self.types.get(t).has_supertype_named("P") and + not self.types.get(t).has_supertype_named("I") and + not self.types.get(t).has_supertype_named("r"))): + continue # Skip rule that uses parameters which are not "things". + + if not self._check_constraints(rule): + continue # Skip rule that violates constraints. + + yield rule + + def _normalize_constraints(self, constraints): + normalized_constraints = {} + for name, constraint in constraints.items(): + for i, normalized_constraint in enumerate(self._normalize_constraint(constraint)): + normalized_constraint.name = name + ".{}".format(i) + normalized_constraints[normalized_constraint.name] = normalized_constraint + + return normalized_constraints + + def _normalize_constraint(self, constraint: Rule) -> Rule: + pre_list = self._normalize_predicates(constraint.preconditions) + post_list = self._normalize_predicates(constraint.postconditions) + for pre, post in zip(itertools.product(*pre_list), itertools.product(*post_list)): + constraint = Rule(constraint.name, itertools.chain(*pre), itertools.chain(*post)) + if any(p for p in constraint.preconditions if p.signature not in self.predicates): + continue # Skip rule that uses undefined predicates. + + if any(t for predicate in constraint.all_predicates for t in predicate.types + if (not self.types.get(t).has_supertype_named("t") and + not self.types.get(t).has_supertype_named("P") and + not self.types.get(t).has_supertype_named("I") and + not self.types.get(t).has_supertype_named("r"))): + continue # Skip rule that uses parameters which are not "things". + + yield constraint + + def _normalize_predicates(self, predicates): + results = [] + for pred in predicates: + aliases = self.aliases.get(pred.signature) + if aliases: + results.append([alias.expand(pred) for alias in aliases]) + else: + results.append([(pred,)]) + + return results + + def _expand_types(self, types: Iterable[str]) -> Iterable[Type]: + """ Returns all variations for descendant types. """ + types = [self.types.get(t) for t in types] + descendants = self.types.multi_descendants(types) + for descendant in descendants: + yield descendant + + def _expand_placeholders(self, placeholders: Iterable[Placeholder]) -> Mapping[Placeholder, Placeholder]: + types = [ph.type for ph in placeholders] + for expansion in self._expand_types(types): + mapping = {ph: Placeholder(ph.name, type.name) for ph, type in zip(placeholders, expansion)} + yield mapping + + def _expand_predicates(self, predicates): + # Expand predicates with variations for descendant types + manually_defined_predicates = set(predicates) + for signature in manually_defined_predicates: + for expansion in self._expand_types(signature.types): + new_signature = Signature(signature.name, [type_.name for type_ in expansion]) + if new_signature not in manually_defined_predicates: + predicates.add(new_signature) + self.inform7._expand_predicates(signature, new_signature) + + def _expand_rules(self, rules): + # Expand rules with variations for descendant types + manually_defined_rules = set(rules.keys()) + for rule in list(rules.values()): + for i, mapping in enumerate(self._expand_placeholders(rule.placeholders)): + new_rule_name = rule.name + for k, v in mapping.items(): + new_rule_name = new_rule_name.replace(k.type, v.type) + + if rule.name == new_rule_name: + new_rule_name = rule.name + ".{}".format(i) + + if new_rule_name in manually_defined_rules: + continue # Skip rules that are fine-grainy defined. + + new_rule = rule.substitute(mapping, new_rule_name) + rules[new_rule.name] = new_rule + self.inform7._expand_command(rule, new_rule) + + def _expand_aliases(self, aliases): + # Expand aliases with variations for descendant types + manually_defined_aliases = list(aliases.keys()) + for alias_list in list(aliases.values()): + for alias in alias_list: + parameters = set(list(alias.pattern.parameters) + [p for pred in alias.replacement for p in pred.parameters]) + for mapping in self._expand_placeholders(parameters): + new_pattern = alias.pattern.substitute(mapping) + if new_pattern.signature == alias.pattern.signature or new_pattern.signature not in manually_defined_aliases: + new_alias = Alias(new_pattern, [pred.substitute(mapping) for pred in alias.replacement]) + if new_alias not in aliases[new_pattern.signature]: + aliases[new_pattern.signature].append(new_alias) + + @classmethod + def parse(cls, document: str) -> "GameLogic": + result = cls() + result._parse(document) + result._initialize() + result._document = document + return result + + @classmethod + def load(cls, paths: Iterable[str]): + result = cls() + for path in paths: + with open(path, "r") as f: + document = f.read() + result._parse(document, path=path) + result._document += document + "\n" + result._initialize() + return result + + @classmethod + def deserialize(cls, data: str) -> "GameLogic": + return cls.parse(data) + + def serialize(self) -> str: + return self._document diff --git a/textworld/logic/logic.ebnf b/textworld/logic/logic.ebnf index ef92b2fb..a3bc8423 100644 --- a/textworld/logic/logic.ebnf +++ b/textworld/logic/logic.ebnf @@ -13,7 +13,7 @@ phName = ?"[\w']+" ; predName = ?"[\w/]+" ; -ruleName = ?"[\w/]+" ; +ruleName = ?"[\w/(), ]+[\w)]" ; variable::VariableNode = name:name [":" type:name] ; diff --git a/textworld/logic/parser.py b/textworld/logic/parser.py index 04d6af53..18c2a0fd 100644 --- a/textworld/logic/parser.py +++ b/textworld/logic/parser.py @@ -101,7 +101,7 @@ def _predName_(self): # noqa @tatsumasu() def _ruleName_(self): # noqa - self._pattern(r'[\w/]+') + self._pattern(r'[\w/(), ]+[\w)]') @tatsumasu('VariableNode') def _variable_(self): # noqa diff --git a/textworld/render/render.py b/textworld/render/render.py index 458e77c5..c419e196 100644 --- a/textworld/render/render.py +++ b/textworld/render/render.py @@ -226,7 +226,7 @@ def used_pos(): v.name = k pos = {game_infos[k].name: v for k, v in pos.items()} - + for room in world.rooms: rooms[room.id] = GraphRoom(game_infos[room.id].name, room) @@ -246,7 +246,7 @@ def used_pos(): # add all items first, in case properties are "out of order" for obj in objects: cur_item = GraphItem(obj.type, game_infos[obj.id].name) - cur_item.portable = data.get_types().is_descendant_of(cur_item.type, "o") + cur_item.portable = data.get_types().get(cur_item.type).has_supertype_named("portable") all_items[obj.id] = cur_item for obj in sorted(objects, key=lambda obj: obj.name):