Skip to content

Commit 2fb6b36

Browse files
authored
Merge pull request #43 from MarcCote/add_subquests
Adding support for subquests in the Inform7 code.
2 parents c77a105 + 1370fac commit 2fb6b36

24 files changed

+402
-202
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: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def _detect_i7_events_debug_tags(text: str) -> Tuple[List[str], str]:
8989
"""
9090
matches = []
9191
open_tags = []
92-
for match in re.findall("\[[^]]+\]\n?", text):
92+
for match in re.findall(r"\[[^]]+\]\n?", text):
9393
text = text.replace(match, "") # Remove i7 debug tags.
9494
tag_name = match.strip()[1:-1] # Strip starting '[' and trailing ']'.
9595

@@ -127,12 +127,12 @@ def __init__(self, *args, **kwargs):
127127
:param kwargs: The kwargs
128128
"""
129129
super().__init__(*args, **kwargs)
130-
self._has_won = False
131-
self._has_lost = False
130+
self.has_timeout = False
132131
self._state_tracking = False
133132
self._compute_intermediate_reward = False
133+
self._max_score = 0
134134

135-
def init(self, output: str, game=None,
135+
def init(self, output: str, game: Game,
136136
state_tracking: bool = False, compute_intermediate_reward: bool = False):
137137
"""
138138
Initialize the game state and set tracking parameters.
@@ -149,10 +149,9 @@ def init(self, output: str, game=None,
149149
self._game_progression = GameProgression(game, track_quests=compute_intermediate_reward)
150150
self._state_tracking = state_tracking
151151
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
152+
self._objective = game.objective
153+
self._score = 0
154+
self._max_score = self._game_progression.max_score
156155

157156
def view(self) -> "GlulxGameState":
158157
"""
@@ -177,6 +176,7 @@ def view(self) -> "GlulxGameState":
177176
game_state._nb_moves = self.nb_moves
178177
game_state._has_won = self.has_won
179178
game_state._has_lost = self.has_lost
179+
game_state.has_timeout = self.has_timeout
180180

181181
if self._state_tracking:
182182
game_state._admissible_commands = self.admissible_commands
@@ -199,6 +199,7 @@ def update(self, command: str, output: str) -> "GlulxGameState":
199199
game_state = super().update(command, output)
200200
game_state.previous_state = self.view()
201201
game_state._objective = self.objective
202+
game_state._max_score = self.max_score
202203
game_state._game_progression = self._game_progression
203204
game_state._state_tracking = self._state_tracking
204205
game_state._compute_intermediate_reward = self._compute_intermediate_reward
@@ -215,12 +216,6 @@ def update(self, command: str, output: str) -> "GlulxGameState":
215216
# An action that affects the state of the game.
216217
game_state._game_progression.update(game_state._action)
217218

218-
if game_state._compute_intermediate_reward:
219-
if game_state._game_progression.winning_policy is None:
220-
game_state._has_lost = True
221-
elif len(game_state._game_progression.winning_policy) == 0:
222-
game_state._has_won = True
223-
224219
return game_state
225220

226221
@property
@@ -317,24 +312,54 @@ def intermediate_reward(self):
317312

318313
@property
319314
def score(self):
320-
if self.has_won:
321-
return 1
322-
elif self.has_lost:
323-
return -1
324-
325-
return 0
315+
if not hasattr(self, "_score"):
316+
if self._state_tracking:
317+
self._score = self._game_progression.score
318+
else:
319+
320+
# Check if there was any Inform7 events.
321+
if self._feedback == self._raw:
322+
self._score = self.previous_state.score
323+
else:
324+
output = self._raw
325+
if not self.game_ended:
326+
output = self._env._send("score")
327+
328+
match = re.search("scored (?P<score>[0-9]+) out of a possible (?P<max_score>[0-9]+),", output)
329+
self._score = 0
330+
if match:
331+
self._score = int(match.groupdict()["score"])
332+
333+
return self._score
326334

327335
@property
328336
def max_score(self):
329-
return 1
337+
return self._max_score
330338

331339
@property
332340
def has_won(self):
333-
return self._has_won or '*** The End ***' in self.feedback
341+
if not hasattr(self, "_has_won"):
342+
if self._compute_intermediate_reward:
343+
self._has_won = self._game_progression.completed
344+
else:
345+
self._has_won = '*** The End ***' in self.feedback
346+
347+
return self._has_won
334348

335349
@property
336350
def has_lost(self):
337-
return self._has_lost or '*** You lost! ***' in self.feedback
351+
if not hasattr(self, "_has_lost"):
352+
if self._compute_intermediate_reward:
353+
self._has_lost = self._game_progression.failed
354+
else:
355+
self._has_lost = '*** You lost! ***' in self.feedback
356+
357+
return self._has_lost
358+
359+
@property
360+
def game_ended(self) -> bool:
361+
""" Whether the game is finished or not. """
362+
return self.has_won | self.has_lost | self.has_timeout
338363

339364
@property
340365
def game_infos(self) -> Mapping:
@@ -439,8 +464,8 @@ def step(self, command: str) -> Tuple[GlulxGameState, float, bool]:
439464
raise GameNotRunningError()
440465

441466
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
467+
self.game_state.has_timeout = not self.game_running
468+
return self.game_state, self.game_state.score, self.game_state.game_ended
444469

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

0 commit comments

Comments
 (0)