Skip to content

Commit 4a8d333

Browse files
committed
Adding support for subquests in the Inform7 code.
1 parent c77a105 commit 4a8d333

24 files changed

+349
-178
lines changed

scripts/tw-make

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def parse_args():
4646
help="Nb. of objects in the world.")
4747
custom_parser.add_argument("--quest-length", type=int, default=5, metavar="LENGTH",
4848
help="Minimum nb. of actions the quest requires to be completed.")
49+
custom_parser.add_argument("--quest-breadth", type=int, default=3, metavar="BREADTH",
50+
help="Control how non-linear a quest can be.")
4951

5052
challenge_parser = subparsers.add_parser("challenge", parents=[general_parser],
5153
help='Generate a game for one of the challenges.')
@@ -72,7 +74,7 @@ if __name__ == "__main__":
7274
}
7375

7476
if args.subcommand == "custom":
75-
game_file, game = textworld.make(args.world_size, args.nb_objects, args.quest_length, grammar_flags,
77+
game_file, game = textworld.make(args.world_size, args.nb_objects, args.quest_length, args.quest_breadth, grammar_flags,
7678
seed=args.seed, games_dir=args.output)
7779

7880
elif args.subcommand == "challenge":
@@ -87,7 +89,7 @@ if __name__ == "__main__":
8789

8890
print("Game generated: {}".format(game_file))
8991
if args.verbose:
90-
print(game.quests[0].desc)
92+
print(game.objective)
9193

9294
if args.view:
9395
textworld.render.visualize(game, interactive=True)

scripts/tw-stats

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ if __name__ == "__main__":
3838
continue
3939

4040
if len(game.quests) > 0:
41-
objectives[game_filename] = game.quests[0].desc
41+
objectives[game_filename] = game.objective
4242

4343
names |= set(info.name for info in game.infos.values() if info.name is not None)
4444
game_logger.collect(game)

scripts_dev/benchmark_framework.py

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,14 @@
1010
from textworld.generator import World
1111

1212

13-
def generate_never_ending_game_old(args):
14-
g_rng.set_seed(args.seed)
15-
msg = "--max-steps {} --nb-objects {} --nb-rooms {} --seed {}"
16-
print(msg.format(args.max_steps, args.nb_objects, args.nb_rooms, g_rng.seed))
17-
print("Generating game...")
18-
19-
map_ = textworld.generator.make_map(n_rooms=args.nb_rooms)
20-
world = World.from_map(map_)
21-
world.set_player_room()
22-
world.populate(nb_objects=args.nb_objects)
23-
grammar = textworld.generator.make_grammar(flags={"theme": "house"})
24-
25-
quests = [] # No quest
26-
game = textworld.generator.make_game_with(world, quests, grammar)
27-
28-
game_name = "neverending"
29-
game_file = textworld.generator.compile_game(game, game_name, force_recompile=True,
30-
games_folder=args.output)
31-
return game_file
32-
33-
3413
def generate_never_ending_game(args):
3514
g_rng.set_seed(args.seed)
36-
msg = "--max-steps {} --nb-objects {} --nb-rooms {} --quest-length {} --seed {}"
37-
print(msg.format(args.max_steps, args.nb_objects, args.nb_rooms, args.quest_length, g_rng.seed))
15+
msg = "--max-steps {} --nb-objects {} --nb-rooms {} --quest-length {} --quest-breadth {} --seed {}"
16+
print(msg.format(args.max_steps, args.nb_objects, args.nb_rooms, args.quest_length, args.quest_breadth, g_rng.seed))
3817
print("Generating game...")
3918

4019
grammar_flags = {}
41-
game = textworld.generator.make_game(args.nb_rooms, args.nb_objects, args.quest_length, grammar_flags)
20+
game = textworld.generator.make_game(args.nb_rooms, args.nb_objects, args.quest_length, args.quest_breadth, grammar_flags)
4221
if args.no_quest:
4322
game.quests = []
4423

@@ -52,9 +31,11 @@ def benchmark(game_file, args):
5231
print("Using {}".format(env.__class__.__name__))
5332

5433
if args.mode == "random":
55-
agent = textworld.agents.RandomTextAgent()
34+
agent = textworld.agents.NaiveAgent()
5635
elif args.mode == "random-cmd":
5736
agent = textworld.agents.RandomCommandAgent()
37+
elif args.mode == "walkthrough":
38+
agent = textworld.agents.WalkthroughAgent()
5839

5940
agent.reset(env)
6041

@@ -96,13 +77,15 @@ def parse_args():
9677
help="Nb. of rooms in the world. Default: %(default)s")
9778
parser.add_argument("--nb-objects", type=int, default=50,
9879
help="Nb. of objects in the world. Default: %(default)s")
99-
parser.add_argument("--quest-length", type=int, default=10,
80+
parser.add_argument("--quest-length", type=int, default=5,
10081
help="Minimum nb. of actions the quest requires to be completed. Default: %(default)s")
82+
parser.add_argument("--quest-breadth", type=int, default=3,
83+
help="Control how non-linear a quest can be. Default: %(default)s")
10184
parser.add_argument("--max-steps", type=int, default=1000,
10285
help="Stop the game after that many steps. Default: %(default)s")
10386
parser.add_argument("--output", default="./gen_games/",
10487
help="Output folder to save generated game files.")
105-
parser.add_argument("--mode", default="random-cmd", choices=["random", "random-cmd"])
88+
parser.add_argument("--mode", default="random-cmd", choices=["random", "random-cmd", "walkthrough"])
10689
parser.add_argument("--no-quest", action="store_true")
10790
parser.add_argument("--compute_intermediate_reward", action="store_true")
10891
parser.add_argument("--activate_state_tracking", action="store_true")

tests/test_make_game.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ def test_making_game_with_names_to_exclude():
1111
g_rng.set_seed(42)
1212

1313
with make_temp_directory(prefix="test_render_wrapper") as tmpdir:
14-
game_file1, game1 = textworld.make(2, 20, 3, {"names_to_exclude": []},
14+
game_file1, game1 = textworld.make(2, 20, 3, 3, {"names_to_exclude": []},
1515
seed=123, games_dir=tmpdir)
1616

1717
game1_objects_names = [info.name for info in game1.infos.values() if info.name is not None]
18-
game_file2, game2 = textworld.make(2, 20, 3, {"names_to_exclude": game1_objects_names},
18+
game_file2, game2 = textworld.make(2, 20, 3, 3, {"names_to_exclude": game1_objects_names},
1919
seed=123, games_dir=tmpdir)
2020
game2_objects_names = [info.name for info in game2.infos.values() if info.name is not None]
2121
assert len(set(game1_objects_names) & set(game2_objects_names)) == 0
@@ -24,8 +24,8 @@ def test_making_game_with_names_to_exclude():
2424
def test_making_game_is_reproducible_with_seed():
2525
grammar_flags = {}
2626
with make_temp_directory(prefix="test_render_wrapper") as tmpdir:
27-
game_file1, game1 = textworld.make(2, 20, 3, grammar_flags, seed=123, games_dir=tmpdir)
28-
game_file2, game2 = textworld.make(2, 20, 3, grammar_flags, seed=123, games_dir=tmpdir)
27+
game_file1, game1 = textworld.make(2, 20, 3, 3, grammar_flags, seed=123, games_dir=tmpdir)
28+
game_file2, game2 = textworld.make(2, 20, 3, 3, grammar_flags, seed=123, games_dir=tmpdir)
2929
assert game_file1 == game_file2
3030
assert game1 == game2
3131
# Make sure they are not the same Python objects.

tests/test_play_generated_games.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ def test_play_generated_games():
1616
# Sample game specs.
1717
world_size = rng.randint(1, 10)
1818
nb_objects = rng.randint(0, 20)
19-
quest_length = rng.randint(1, 10)
19+
quest_length = rng.randint(2, 5)
20+
quest_breadth = rng.randint(3, 7)
2021
game_seed = rng.randint(0, 65365)
2122
grammar_flags = {} # Default grammar.
2223

2324
with make_temp_directory(prefix="test_play_generated_games") as tmpdir:
24-
game_file, game = textworld.make(world_size, nb_objects, quest_length, grammar_flags,
25+
game_file, game = textworld.make(world_size, nb_objects, quest_length, quest_breadth, grammar_flags,
2526
seed=game_seed, games_dir=tmpdir)
2627

2728
# Solve the game using WalkthroughAgent.

tests/test_textworld.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_game_walkthrough_agent(self):
5858
agent = textworld.agents.WalkthroughAgent()
5959
env = textworld.start(self.game_file)
6060
env.activate_state_tracking()
61-
commands = self.game.quests[0].commands
61+
commands = self.game.main_quest.commands
6262
agent.reset(env)
6363
game_state = env.reset()
6464

tests/test_tw_play.py renamed to tests/test_tw-play.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
from textworld.utils import make_temp_directory
88

99

10-
def test_making_a_custom_game():
11-
with make_temp_directory(prefix="test_tw-play") as tmpdir:
12-
game_file, _ = textworld.make(5, 10, 5, {}, seed=1234, games_dir=tmpdir)
10+
def test_playing_a_game():
11+
with make_temp_directory(prefix="test_tw-play") as tmpdir:
12+
game_file, _ = textworld.make(5, 10, 5, 4, {}, seed=1234, games_dir=tmpdir)
1313

1414
command = ["tw-play", "--max-steps", "100", "--mode", "random", game_file]
1515
assert check_call(command) == 0
@@ -18,4 +18,4 @@ def test_making_a_custom_game():
1818
assert check_call(command) == 0
1919

2020
command = ["tw-play", "--max-steps", "100", "--mode", "walkthrough", game_file]
21-
assert check_call(command) == 0
21+
assert check_call(command) == 0

textworld/agents/walkthrough.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def reset(self, env):
2626
raise NameError(msg)
2727

2828
# Load command from the generated game.
29-
self._commands = iter(env.game.quests[0].commands)
29+
self._commands = iter(env.game.main_quest.commands)
3030

3131
def act(self, game_state, reward, done):
3232
try:

textworld/envs/glulx/git_glulx_ml.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ def __init__(self, *args, **kwargs):
129129
super().__init__(*args, **kwargs)
130130
self._has_won = False
131131
self._has_lost = False
132+
self.has_timeout = False
132133
self._state_tracking = False
133134
self._compute_intermediate_reward = False
135+
self._max_score = 0
134136

135-
def init(self, output: str, game=None,
137+
def init(self, output: str, game: Game,
136138
state_tracking: bool = False, compute_intermediate_reward: bool = False):
137139
"""
138140
Initialize the game state and set tracking parameters.
@@ -149,10 +151,8 @@ def init(self, output: str, game=None,
149151
self._game_progression = GameProgression(game, track_quests=compute_intermediate_reward)
150152
self._state_tracking = state_tracking
151153
self._compute_intermediate_reward = compute_intermediate_reward and len(game.quests) > 0
152-
153-
self._objective = ""
154-
if len(game.quests) > 0:
155-
self._objective = game.quests[0].desc
154+
self._objective = game.objective
155+
self._max_score = sum(quest.reward for quest in game.quests)
156156

157157
def view(self) -> "GlulxGameState":
158158
"""
@@ -177,6 +177,7 @@ def view(self) -> "GlulxGameState":
177177
game_state._nb_moves = self.nb_moves
178178
game_state._has_won = self.has_won
179179
game_state._has_lost = self.has_lost
180+
game_state.has_timeout = self.has_timeout
180181

181182
if self._state_tracking:
182183
game_state._admissible_commands = self.admissible_commands
@@ -199,6 +200,7 @@ def update(self, command: str, output: str) -> "GlulxGameState":
199200
game_state = super().update(command, output)
200201
game_state.previous_state = self.view()
201202
game_state._objective = self.objective
203+
game_state._max_score = self.max_score
202204
game_state._game_progression = self._game_progression
203205
game_state._state_tracking = self._state_tracking
204206
game_state._compute_intermediate_reward = self._compute_intermediate_reward
@@ -317,16 +319,21 @@ def intermediate_reward(self):
317319

318320
@property
319321
def score(self):
320-
if self.has_won:
321-
return 1
322-
elif self.has_lost:
323-
return -1
322+
if not hasattr(self, "_score"):
323+
output = self._raw
324+
if not self.game_ended:
325+
output = self._env._send("score")
326+
327+
match = re.search("scored (?P<score>[0-9]+) out of a possible (?P<max_score>[0-9]+),", output)
328+
self._score = 0
329+
if match:
330+
self._score = int(match.groupdict()["score"])
324331

325-
return 0
332+
return self._score
326333

327334
@property
328335
def max_score(self):
329-
return 1
336+
return self._max_score
330337

331338
@property
332339
def has_won(self):
@@ -336,6 +343,11 @@ def has_won(self):
336343
def has_lost(self):
337344
return self._has_lost or '*** You lost! ***' in self.feedback
338345

346+
@property
347+
def game_ended(self) -> bool:
348+
""" Whether the game is finished or not. """
349+
return self.has_won | self.has_lost | self.has_timeout
350+
339351
@property
340352
def game_infos(self) -> Mapping:
341353
""" Additional information about the game. """
@@ -439,8 +451,8 @@ def step(self, command: str) -> Tuple[GlulxGameState, float, bool]:
439451
raise GameNotRunningError()
440452

441453
self.game_state = self.game_state.update(command, output)
442-
done = self.game_state.game_ended or not self.game_running
443-
return self.game_state, self.game_state.score, done
454+
self.game_state.has_timeout = not self.game_running
455+
return self.game_state, self.game_state.score, self.game_state.game_ended
444456

445457
def _send(self, command: str) -> Union[str, None]:
446458
if not self.game_running:

textworld/envs/glulx/tests/test_git_glulx_ml.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ def build_test_game():
4747
chest.add_property("open")
4848
R2.add(chest)
4949

50-
M.set_quest_from_commands(commands)
50+
quest1 = M.new_quest_using_commands(commands)
51+
quest1.reward = 2
52+
quest2 = M.new_quest_using_commands(commands + ["close chest"])
53+
quest2.set_winning_conditions([M.new_fact("in", carrot, chest),
54+
M.new_fact("closed", chest)])
55+
M._quests = [quest1, quest2]
5156
game = M.build()
5257
return game
5358

@@ -128,7 +133,10 @@ def test_inventory(self):
128133
game_state, _, _ = self.env.step("take carrot")
129134
game_state, _, _ = self.env.step("go east")
130135
game_state, _, _ = self.env.step("insert carrot into chest")
131-
assert game_state.inventory == ""
136+
assert "carrying nothing" in game_state.inventory
137+
138+
game_state, _, _ = self.env.step("close chest")
139+
assert game_state.inventory == "" # Game has ended
132140

133141
def test_objective(self):
134142
assert self.game_state.objective.strip() in self.game_state.feedback
@@ -145,16 +153,19 @@ def test_description(self):
145153

146154
# End the game.
147155
game_state, _, _ = self.env.step("insert carrot into chest")
156+
game_state, _, _ = self.env.step("close chest")
148157
assert game_state.description == ""
149158

150159
def test_score(self):
151160
assert self.game_state.score == 0
152-
assert self.game_state.max_score == 1
161+
assert self.game_state.max_score == 3
153162
game_state, _, _ = self.env.step("go east")
154163
assert game_state.score == 0
155164
game_state, _, _ = self.env.step("insert carrot into chest")
156-
assert game_state.score == 1
157-
assert game_state.max_score == 1
165+
assert game_state.score == 2
166+
assert game_state.max_score == 3
167+
game_state, _, _ = self.env.step("close chest")
168+
assert game_state.score == 3
158169

159170
def test_game_ended_when_no_quest(self):
160171
M = GameMaker()
@@ -184,6 +195,8 @@ def test_has_won(self):
184195
game_state, _, _ = self.env.step("go east")
185196
assert not game_state.has_won
186197
game_state, _, done = self.env.step("insert carrot into chest")
198+
assert not game_state.has_won
199+
game_state, _, done = self.env.step("close chest")
187200
assert game_state.has_won
188201

189202
def test_has_lost(self):
@@ -210,31 +223,33 @@ def test_intermediate_reward(self):
210223
game_state, _, _ = self.env.step("close wooden door")
211224
assert game_state.intermediate_reward == 0
212225
game_state, _, done = self.env.step("insert carrot into chest")
226+
game_state, _, done = self.env.step("close chest")
213227
assert done
214228
assert game_state.has_won
215229
assert game_state.intermediate_reward == 1
216230

217231
def test_policy_commands(self):
218-
assert self.game_state.policy_commands == self.game.quests[0].commands
232+
assert self.game_state.policy_commands == self.game.main_quest.commands
219233

220234
game_state, _, _ = self.env.step("drop carrot")
221-
expected = ["take carrot"] + self.game.quests[0].commands
235+
expected = ["take carrot"] + self.game.main_quest.commands
222236
assert game_state.policy_commands == expected, game_state.policy_commands
223237

224238
game_state, _, _ = self.env.step("take carrot")
225-
expected = self.game.quests[0].commands
239+
expected = self.game.main_quest.commands
226240
assert game_state.policy_commands == expected
227241

228242
game_state, _, _ = self.env.step("go east")
229-
expected = self.game.quests[0].commands[1:]
243+
expected = self.game.main_quest.commands[1:]
230244
assert game_state.policy_commands == expected
231245

232246
game_state, _, _ = self.env.step("insert carrot into chest")
247+
game_state, _, _ = self.env.step("close chest")
233248
assert game_state.policy_commands == [], game_state.policy_commands
234249

235250
# Test parallel subquests.
236251
game_state = self.env.reset()
237-
commands = self.game.quests[0].commands
252+
commands = self.game.main_quest.commands
238253
assert game_state.policy_commands == commands
239254
game_state, _, _ = self.env.step("close wooden door")
240255
assert game_state.policy_commands == ["open wooden door"] + commands
@@ -248,15 +263,15 @@ def test_policy_commands(self):
248263

249264
# Irreversible action.
250265
game_state = self.env.reset()
251-
assert game_state.policy_commands == self.game.quests[0].commands
266+
assert game_state.policy_commands == self.game.main_quest.commands
252267
game_state, _, done = self.env.step("eat carrot")
253268
assert done
254269
assert game_state.has_lost
255270
assert len(game_state.policy_commands) == 0
256271

257272
def test_admissible_commands(self):
258273
game_state = self.env.reset()
259-
for command in self.game.quests[0].commands:
274+
for command in self.game.main_quest.commands:
260275
assert command in game_state.admissible_commands
261276
game_state, _, done = self.env.step(command)
262277

0 commit comments

Comments
 (0)