Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ python -m spacy download en_core_web_sm
- [Utilities](https://jericho-py.readthedocs.io/en/latest/util.html)
- [Defines](https://jericho-py.readthedocs.io/en/latest/defines.html)

## Breaking changes in Jericho 4.0

Prior to version 4.0, creating an environment without specifying a seed would silently
use the game's walkthrough seed (when known), making episodes deterministic. As described
in the [Jericho paper](http://arxiv.org/abs/1909.05398), a fixed random seed is a *handicap*
that should be chosen and disclosed explicitly. Starting with version 4.0:

- `FrotzEnv(rom)` (i.e. without a seed) now uses a time-dependent seed, i.e. episodes are stochastic.
- `FrotzEnv.reset()` accepts a `use_walkthrough_seed` argument to seed the emulator with the
game's walkthrough seed, which is needed to reproduce the walkthrough.
- `FrotzEnv.walkthrough_seed` returns the game's walkthrough seed, if it is known, otherwise `None`.
- A `ImplicitRandomSeedWarning` is issued when resetting a game that has a walkthrough seed while
neither an explicit seed nor `use_walkthrough_seed` was provided.

```python
from jericho import FrotzEnv

env = FrotzEnv("zork1.z5") # Stochastic (time-dependent seed).
env = FrotzEnv("zork1.z5", seed=-1) # Stochastic, explicitly (no warning).
env = FrotzEnv("zork1.z5", seed=42) # Deterministic with seed 42.

env.reset() # Uses the seed above.
env.reset(use_walkthrough_seed=True) # Deterministic, reproduces env.get_walkthrough().
print(env.walkthrough_seed) # 12
```

## Agents

- [Reading Comprehension Deep Q-Network (RCDQN)](https://github.com/XiaoxiaoGuo/rcdqn)
Expand Down
6 changes: 5 additions & 1 deletion docs/source/tutorial_quick.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Jericho implements a reinforcement learning interface in which the agent provide

from jericho import *
# Create the environment, optionally specifying a random seed
# (by default, the emulator is seeded with the current time).
env = FrotzEnv("z-machine-games-master/jericho-game-suite/zork1.z5")
initial_observation, info = env.reset()
done = False
Expand Down Expand Up @@ -127,12 +128,15 @@ One of the most common difficulties with parser-based text games is identifying
Walkthroughs
------------

Jericho provides walkthroughs for supported games using :meth:`jericho.FrotzEnv.get_walkthrough`. To use the walkthrough, it is necessary to reset the environment with the desired seed:
Jericho provides walkthroughs for supported games using :meth:`jericho.FrotzEnv.get_walkthrough`. To reproduce a walkthrough, it is necessary to reset the environment with the game's walkthrough seed, which is available via :attr:`jericho.FrotzEnv.walkthrough_seed`:

.. code-block:: python

>>> from jericho import *
>>> env = FrotzEnv("z-machine-games-master/jericho-game-suite/zork1.z5")
>>> walkthrough = env.get_walkthrough()
>>> env.reset(use_walkthrough_seed=True) # Equivalent to env.seed(env.walkthrough_seed); env.reset()
>>> for act in walkthrough:
>>> env.step(act)

.. note:: Since Jericho 4.0, an environment created without an explicit seed is stochastic, i.e. the emulator's random number generator is seeded with the current time. Seeding the emulator (e.g. with the walkthrough seed) is a *handicap*, as defined in the `Jericho paper <https://arxiv.org/abs/1909.05398>`_, and should be disclosed when reporting results.
80 changes: 69 additions & 11 deletions jericho/jericho.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,17 +368,26 @@ class TruncatedInputActionWarning(UserWarning):
pass


class ImplicitRandomSeedWarning(UserWarning):
pass


class FrotzEnv():
"""
The Frotz Environment is a fast interface to Z-Machine games.

:param story_file: Path to a Z-machine rom file (.z3/.z5/.z6/.z8).
:param seed: Seed the random number generator used by the emulator.
Default: use walkthrough's seed if it exists,
otherwise use value of -1 which changes with time.
Default: -1, i.e. the emulator's random number generator is
seeded with the current time, making episodes stochastic.
:type story_file: path
:type seed: int

.. note:: Since Jericho 4.0, the seed needed to reproduce a game's walkthrough
is no longer used by default. To reproduce a walkthrough, either call
:meth:`jericho.FrotzEnv.reset` with `use_walkthrough_seed=True` or
provide :attr:`jericho.FrotzEnv.walkthrough_seed` as the `seed` argument.

"""
def __init__(self, story_file, seed=None):
self._cache = {}
Expand All @@ -397,8 +406,8 @@ def load(self, story_file, seed=None):

:param story_file: Path to a Z-machine rom file (.z3/.z5/.z6/.z8).
:param seed: Seed the random number generator used by the emulator.
Default: use walkthrough's seed if it exists,
otherwise use value of -1 which changes with time.
Default: -1, i.e. the emulator's random number generator is
seeded with the current time, making episodes stochastic.
:type story_file: path
:type seed: int
'''
Expand Down Expand Up @@ -434,30 +443,79 @@ def seed(self, seed=None):
Changes seed used for the emulator's random number generator.

:param seed: Seed the random number generator used by the emulator.
Default: use walkthrough's seed if it exists,
otherwise use value of -1 which changes with time.
Default: -1, i.e. the emulator's random number generator is
seeded with the current time, making episodes stochastic.
:returns: The value of the seed.

.. note:: :meth:`jericho.FrotzEnv.reset()` must be called before the seed takes effect.

.. note:: Since Jericho 4.0, calling this method without a seed no longer
silently uses the game's walkthrough seed. Use
:attr:`jericho.FrotzEnv.walkthrough_seed` or
:meth:`jericho.FrotzEnv.reset` with `use_walkthrough_seed=True`
to reproduce a walkthrough.

'''
self._seed_is_explicit = seed is not None
self._seed = seed if seed is not None else -1
return self._seed

@property
def walkthrough_seed(self):
'''
Seed needed to reproduce this game's walkthrough, if it is known.

:returns: The walkthrough's seed, or `None` if the game has no known walkthrough seed.

:Example:

>>> import jericho
>>> env = jericho.FrotzEnv('zork1.z5')
>>> env.walkthrough_seed
12
>>> env.reset(use_walkthrough_seed=True) # Same as env.seed(env.walkthrough_seed); env.reset()

'''
seed = seed or self.bindings.get('seed', -1)
self._seed = seed
return seed
return self.bindings.get('seed')

def reset(self):
def reset(self, use_walkthrough_seed=False):
'''
Resets the game.

:param use_walkthrough_seed: Seed the emulator to reproduce the walkthrough.
Default: `False`, i.e. use the seed set with
:meth:`jericho.FrotzEnv.seed` (a time-dependent
seed, unless one was explicitly provided).
:type use_walkthrough_seed: bool
:returns: A tuple containing the initial observation,\
and a dictionary of info.
:rtype: string, dictionary

.. note:: Using `use_walkthrough_seed=True` makes the game deterministic.
As described in the Jericho paper, this is a *handicap* that
should be disclosed when reporting results.

'''
seed = self._seed
if use_walkthrough_seed:
if self.walkthrough_seed is None:
msg = ("No walkthrough seed is known for game '{}',"
" using a time-dependent seed instead.").format(self.story_file.decode())
warnings.warn(msg, UnsupportedGameWarning)
else:
seed = self.walkthrough_seed

elif not self._seed_is_explicit and self.walkthrough_seed is not None:
msg = ("Since Jericho 4.0, the walkthrough seed ({}) of game '{}' is no longer used"
" by default, i.e. this episode is stochastic (time-dependent seed)."
" Call reset(use_walkthrough_seed=True) to reproduce the walkthrough,"
" or provide an explicit seed (e.g. FrotzEnv(rom, seed=-1)) to silence"
" this warning.").format(self.walkthrough_seed, self.story_file.decode())
warnings.warn(msg, ImplicitRandomSeedWarning)

self.close()
rom, _, _ = self._cache[self.story_file.decode()]
obs_ini = self.frotz_lib.setup(self.story_file, self._seed, rom, len(rom)).decode('cp1252')
obs_ini = self.frotz_lib.setup(self.story_file, seed, rom, len(rom)).decode('cp1252')
score = self.frotz_lib.get_score()
return obs_ini, {'moves':self.get_moves(), 'score':score}

Expand Down
2 changes: 1 addition & 1 deletion jericho/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '3.3.1'
__version__ = '4.0.0'
4 changes: 2 additions & 2 deletions tests/test_jericho.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ def _get_mem():
def test_copy():
rom = pjoin(DATA_PATH, "905.z5")
env = jericho.FrotzEnv(rom)
env.reset()
env.reset(use_walkthrough_seed=True)

walkthrough = env.get_walkthrough()
expected = [env.step(act) for act in walkthrough]

env.reset()
env.reset(use_walkthrough_seed=True)
for i, act in enumerate(walkthrough):
obs, rew, done, info = env.step(act)

Expand Down
76 changes: 76 additions & 0 deletions tests/test_seed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import os
import warnings
from os.path import join as pjoin

import pytest

import jericho


DATA_PATH = os.path.abspath(pjoin(__file__, '..', "data"))


def test_default_seed_is_time_dependent():
# By default, the walkthrough seed should *not* be used silently.
rom = pjoin(DATA_PATH, "905.z5")
env = jericho.FrotzEnv(rom)
assert env._seed == -1
assert env.seed() == -1


def test_explicit_seed():
rom = pjoin(DATA_PATH, "905.z5")
env = jericho.FrotzEnv(rom, seed=42)
assert env._seed == 42

# Zero is a valid seed.
assert env.seed(0) == 0
assert env._seed == 0


def test_walkthrough_seed_property():
env = jericho.FrotzEnv(pjoin(DATA_PATH, "905.z5"))
assert env.walkthrough_seed == env.bindings['seed']

# Games without bindings have no walkthrough seed.
env = jericho.FrotzEnv(pjoin(DATA_PATH, "tw-game.z8"))
assert env.walkthrough_seed is None


def test_warning_when_using_implicit_random_seed():
rom = pjoin(DATA_PATH, "905.z5")
env = jericho.FrotzEnv(rom)

with pytest.warns(jericho.ImplicitRandomSeedWarning):
env.reset()

# No warning when the choice is explicit.
with warnings.catch_warnings():
warnings.simplefilter("error")
env.reset(use_walkthrough_seed=True)
jericho.FrotzEnv(rom, seed=-1).reset()
jericho.FrotzEnv(rom, seed=env.walkthrough_seed).reset()

# No warning for games without a walkthrough seed.
with warnings.catch_warnings():
warnings.simplefilter("error")
jericho.FrotzEnv(pjoin(DATA_PATH, "tw-game.z8")).reset()


def test_reset_with_walkthrough_seed_but_no_bindings():
env = jericho.FrotzEnv(pjoin(DATA_PATH, "tw-game.z8"))
with pytest.warns(jericho.UnsupportedGameWarning):
env.reset(use_walkthrough_seed=True)


def test_walkthrough_is_reproducible_with_walkthrough_seed():
rom = pjoin(DATA_PATH, "905.z5")
env = jericho.FrotzEnv(rom)
walkthrough = env.get_walkthrough()

env.reset(use_walkthrough_seed=True)
for act in walkthrough:
obs, rew, done, info = env.step(act)

assert done
assert info["score"] == env.get_max_score()
2 changes: 1 addition & 1 deletion tools/find_walkthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def parse_args():

history = []
env = jericho.FrotzEnv(args.filename)
obs, info = env.reset()
obs, info = env.reset(use_walkthrough_seed=True)

history.append(env.get_state())

Expand Down
2 changes: 1 addition & 1 deletion tools/test_games.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def parse_args():
print(colored("SKIP\tMissing walkthrough", 'yellow'))
continue

env.reset()
env.reset(use_walkthrough_seed=True)

#walkthrough = bindings['walkthrough'].split('/')
for cmd in env.get_walkthrough():
Expand Down
Loading