Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions textworld/envs/glulx/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert unneeded change.

4 changes: 4 additions & 0 deletions textworld/envs/glulx/git_glulx_ml.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,10 @@ def compute_intermediate_reward(self) -> None:
def __del__(self) -> None:
self.close()

@property
def gamefile(self) -> str:
return self._gamefile

@property
def game_running(self) -> bool:
""" Determines if the game is still running. """
Expand Down
1 change: 1 addition & 0 deletions textworld/envs/wrappers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

from textworld.envs.wrappers.viewer import HtmlViewer
from textworld.envs.wrappers.recorder import Recorder
from textworld.envs.wrappers.glulx_logger import GlulxLogger
268 changes: 268 additions & 0 deletions textworld/envs/wrappers/glulx_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT license.
import json
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a blank space after the license.


from typing import Tuple, List, Optional, Iterable, Union, Sized, Any, Mapping

from textworld.core import GameState, Wrapper
from textworld.envs.glulx.git_glulx_ml import GitGlulxMLEnvironment, GlulxGameState


class GameLog:
def __init__(self):
"""
GameLog object. Allows your to load and save previous game logs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your -> you
Also, put this docstring at the class level instead of the init method.

"""
self._logs = [[]]
self._current_game = self._logs[-1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No needed, see my comments on current_game property.

self._filename = ''

def __getitem__(self, idx: int) -> list:
"""
Gets a particular game log at index idx.
Args:
idx: index to retrieve
Returns:
"""
assert idx <= len(self._logs)
return self._logs[idx]

def __len__(self) -> int:
return len(self._logs)

@property
def current_game(self) -> list:
"""
Gets current game we're logging.
Returns: list of logs from current game.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a newline after Returns:, then the next line is indented (ref).

"""
return self._current_game
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of having a private variable _current_game simple make this property return return self._logs[-1]


@property
def logs(self) -> list:
"""
Get all logs from all games.
Returns: All logs from all games.
"""
return self._logs

def new_game(self):
"""
Start logs for a new game.
Returns: log object for current game.
"""
if len(self._current_game) > 0:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need this check?

self._logs.append([])
self._current_game = self._logs[-1]
return self._current_game

def set(self, key: Any, value: Any) -> None:
"""
Sets value for latest game
Args:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a blank line between the description and the Args:

key: Key to set
value: Value to set
"""
current = self._current_game[-1]
current[key] = value

def append_optional(self, value: Any) -> None:
"""
Appends optional information to current game
Args:
value: Value to append
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention value needs to be JSON serializable.

"""
current = self._current_game[-1]
if 'optional' not in current:
current['optional'] = []
current['optional'].append(value)

def add_log(self, log: Mapping):
"""
Adds a new log to our logs
Args:
log: Mapping of a log
"""
self._current_game.append(log)

def save(self, filename):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add type hinting, e.g. save(self, filename: str) -> None.

"""
Save current logs to specified file name
Args:
filename: File path to save to (should have JSON extension)
"""
self._filename = filename
try:
with open(filename, 'w') as outfile:
json.dump(self._logs, outfile)
except TypeError as e:
raise TypeError('Log not serializable')

def load(self, filename):
"""
Loads a JSON object as logs
Args:
filename: file path to load.
"""
self._filename = filename
with open(filename) as f:
self._logs= json.load(f)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8: missing space in front of =.



class GlulxLogger(Wrapper):
def __init__(self, env: GitGlulxMLEnvironment) -> None:
"""
Wrap around a TextWorld GitGlulxML environment to provide logging capabilities.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap -> wrapper?

Args:
env: The GitGlulxML environment to wrap.
"""
super().__init__(env)
self.activate_state_tracking()

self.serialized_game = env.game.serialize()
self._gamefile = env.gamefile

self._logs = GameLog()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's called this variable self._game_log


def step(self, command: str) -> Tuple[GlulxGameState, float, bool]:
"""
Take a step in the environment.
Args:
command: input string for taking an action
Returns:
GlulxGameState, score and done.
"""
new_log = {}
new_log['optional'] = []
new_log['command'] = command

game_state, score, done = super().step(command)
new_log['feedback'] = game_state.feedback
new_log['score'] = score
new_log['done'] = done
new_log['description'] = game_state.description
new_log['inventory'] = game_state.inventory
new_log['state'] = game_state.state.serialize()
self._logs.add_log(new_log)

return game_state, score, done

def reset(self) -> GameState:
"""
Reset the environment.
Adds a new game into the logs.
Returns:
GameState
"""
new_log = {}
self._logs.new_game()

game_state = super().reset()
new_log['optional'] = []
new_log['done'] = False
new_log['description'] = game_state.description
new_log['inventory'] = game_state.inventory
new_log['state'] = game_state.state.serialize()
self._logs.add_log(new_log)

return game_state

def add_commands(self, commands: List[str], scores: Optional[Union[Iterable[float], Sized]]=None) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the Union? Isn't Iterable[float] enough?

"""
Add custom commands to the logger. Optionally add scores for each command.
Args:
commands: A list of commands.
scores: scores for each command. Must be same size as commands if provided.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove blank line

"""
if scores is not None:
self._logs.set('command_scores', scores)

self._logs.set('commands', commands)

def add(self, info: Any) -> None:
"""
Add any additional information you want to log.
Args:
info: Additional information to log for the current game state.
"""
self._logs.append_optional(info)

@property
def current(self) -> Mapping:
"""
Returns:
Current game state logs.
"""
return self._logs.current_game[-1]

@property
def logs(self) -> List[Mapping]:
"""
Returns: List of all logs from this game.
"""
return self._logs.current_game

@property
def all_logs(self) -> GameLog:
"""
Returns: GameLog object containing all logs.
"""
return self._logs

@property
def gamefile(self) -> str:
"""
Returns:
Game file currently loaded
"""
return self._gamefile

def __getitem__(self, index: int) -> Mapping:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we need this anymore.

"""
Get a certain log at a given index.
Args:
index: index of log to get.
Returns:
log at index
"""
assert index <= len(self._logs)

return self._logs.current_game[index]

def __str__(self) -> str:
return str(self._logs.current_game)

def serialize(self) -> List[Mapping]:
"""
Get serialized mappings of logs.
Returns:
List of serialized mappings.
"""
return self._logs.logs

def save(self, filename) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need save/load/serialize anymore. Users can call save directly on the gamelog object, i.e. env.game_log.save()

"""
Saves all logs given a filename
Returns: None
"""
self._logs.save(filename)

def load(self, filename) -> None:
"""
Loads logs from a file
Args:
filename:
string representing file location
Returns: None
"""
self._logs.load(filename)


53 changes: 53 additions & 0 deletions textworld/envs/wrappers/tests/test_glulx_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT license.

import textworld
import numpy as np

from textworld.envs.wrappers import GlulxLogger
from textworld.utils import make_temp_directory
from textworld.generator import compile_game
from textworld import g_rng


def test_glulx_logger():
num_nodes = 3
num_items = 10
g_rng.set_seed(1234)
grammar_flags = {"theme": "house", "include_adj": True}
game = textworld.generator.make_game(world_size=num_nodes, nb_objects=num_items, quest_length=3, grammar_flags=grammar_flags)

game_name = "test_glulx_logger"
with make_temp_directory(prefix=game_name) as tmpdir:
game_file = compile_game(game, game_name, games_folder=tmpdir)

env = textworld.start(game_file)
env = GlulxLogger(env)
env.activate_state_tracking()
game_state = env.reset()
game_state = env.reset()
assert len(env.all_logs.logs) == 2

# test reset
assert 'state' in env.current

# test step
options = game_state.admissible_commands
game_state, score, done = env.step(options[0])
assert len(env.logs) > 1
assert 'command' in env.current
assert 'state' in env.current
assert 'feedback' in env.current

# test add_commands
option_scores = np.array([0.1] * len(options))
env.add_commands(options, option_scores)
assert len(env.current['commands']) == len(env.current['command_scores'])

# test add
additional_info = {'scores': option_scores}
env.add(additional_info)
assert len(env.current['optional']) > 0