# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT license.
import os
import warnings
import jericho
import textworld
from textworld.core import GameState
from textworld.core import GameNotRunningError
[docs]class JerichoEnv(textworld.Environment):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._seed = -1
self._jericho = None
self.gamefile = None
self._reset = False
[docs] def load(self, z_file: str) -> None:
self.gamefile = os.path.abspath(z_file)
_, ext = os.path.splitext(os.path.basename(self.gamefile))
# Check if game is supported by Jericho.
if not ext.startswith(".z"):
raise ValueError("Only .z[1-8] files are supported!")
if not os.path.isfile(self.gamefile):
raise FileNotFoundError(self.gamefile)
if self._jericho is None:
# Start the game using Jericho.
self._jericho = jericho.FrotzEnv(self.gamefile, self._seed)
else:
self._jericho.load(self.gamefile)
def __del__(self) -> None:
self.close()
@property
def game_running(self) -> bool:
""" Determines if the game is still running. """
return self._jericho is not None
[docs] def seed(self, seed=None):
self._seed = seed
if self._jericho:
self._jericho.seed(self._seed)
return self._seed
def _gather_infos(self):
""" Adds additional information to the internal state. """
self.state.feedback = self.state.raw
if not self._jericho.is_fully_supported:
return # No more information can be gathered.
for attr in self.infos.basics:
self.state[attr] = getattr(self._jericho, "get_" + attr, lambda: self.state.get(attr))()
for attr in self.infos.extras:
self.state["extra.{}".format(attr)] = getattr(self._jericho, "get_" + attr, lambda: None)()
# Deal with information that has different method name in Jericho.
self.state["won"] = self._jericho.victory()
self.state["lost"] = self._jericho.game_over()
self.state["score"] = self._jericho.get_score()
self.state["moves"] = self._jericho.get_moves()
self.state["location"] = self._jericho.get_player_location()
if self.infos.description:
bkp = self._jericho.get_state()
self.state["description"], _, _, _ = self._jericho.step("look")
self._jericho.set_state(bkp)
if self.infos.inventory:
bkp = self._jericho.get_state()
self.state["inventory"], _, _, _ = self._jericho.step("inventory")
self._jericho.set_state(bkp)
if self.infos.admissible_commands:
self.state["_valid_commands"] = self._jericho.get_valid_actions()
self.state["admissible_commands"] = sorted(set(self.state["_valid_commands"]))
[docs] def reset(self):
if not self.game_running:
raise GameNotRunningError("Call env.load(gamefile) before env.reset().")
self.state = GameState()
self.state.raw, _ = self._jericho.reset()
self._gather_infos()
self._reset = True
return self.state
def _send(self, command: str) -> str:
""" Send a command directly to the interpreter.
This method will not affect the internal state variable.
"""
feedback, _, _, _ = self._jericho.step(command)
return feedback
[docs] def step(self, command):
if not self.game_running or not self._reset:
raise GameNotRunningError()
self.state = GameState()
self.state.last_command = command.strip()
res = self._jericho.step(self.state.last_command)
# As of Jericho >= 2.1.0, the reward is returned instead of the score.
self.state.raw, _, self.state.done, _ = res
self._gather_infos()
return self.state, self.state.score, self.state.done
[docs] def close(self):
if self.game_running:
self._jericho.close()
self._jericho = None
self._reset = False
[docs] def copy(self) -> "JerichoEnv":
""" Return a copy of this environment at the same state. """
env = JerichoEnv(self.infos)
env._seed = self._seed
if self.gamefile:
env.load(self.gamefile)
if self._jericho:
env._jericho = self._jericho.copy()
env._reset = True
# Copy core Environment's attributes.
env.state = self.state.copy()
env.infos = self.infos.copy()
return env
# By default disable the warning about unsupported games.
warnings.simplefilter("ignore", jericho.UnsupportedGameWarning)