Jericho¶
The Jericho adapter wraps Jericho (Microsoft) to provide access to 50+ classic interactive fiction text adventure games for LLM agent evaluation. Games include Zork, Hitchhiker's Guide to the Galaxy, Detective, Planetfall, and many more.
Installation¶
Jericho bundles game ROM files, so no additional data download is needed.
Quick Start¶
from llenvs.adapters import JerichoAdapter
from llenvs.core import Action
adapter = JerichoAdapter()
env = adapter.get_environment("jericho:zork1", pure_step=True)
state, info = env.reset(options={"task_index": 0})
print(state.observation.prompt)
# "ZORK I: The Great Underground Empire..."
result = env.step(state, Action(text="open mailbox"))
print(result.next_state.observation.messages[-1]["content"])
Available Games¶
Jericho bundles 50+ classic games. Notable examples:
| Game | Description |
|---|---|
zork1 |
The Great Underground Empire |
zork2 |
The Wizard of Frobozz |
zork3 |
The Dungeon Master |
detective |
A murder mystery |
hhgg |
Hitchhiker's Guide to the Galaxy |
planetfall |
Sci-fi adventure |
trinity |
Nuclear age adventure |
wishbringer |
Fantasy adventure |
enchanter |
Magic-themed adventure |
lurking |
Lurking Horror |
List all available games:
adapter = JerichoAdapter()
games = adapter.list_environments()
# ["jericho:acorncourt", "jericho:anchor", "jericho:detective", ...]
Action Format¶
Actions are free-form text commands understood by the Z-Machine:
| Action | Description |
|---|---|
go north/south/east/west/up/down |
Navigate |
open {obj} / close {obj} |
Open or close something |
take {obj} / drop {obj} |
Pick up or drop an object |
put {obj} in/on {obj} |
Place an object |
examine {obj} |
Look at something |
turn on {obj} / turn off {obj} |
Operate a device |
look |
Look around |
inventory |
Check held items |
Valid Actions¶
Jericho can compute valid actions at each step. By default, these are not shown in the observation and are not computed at all (wrapper fidelity — the original games don't show valid actions to players):
# Not generated or shown in observation (default, wrapper fidelity)
env = adapter.get_environment("jericho:zork1", include_valid_actions=False)
state, _ = env.reset(options={"task_index": 0})
print(state.hidden.valid_actions) # ()
# Generated and included in observation (research/debug mode)
env = adapter.get_environment("jericho:zork1", include_valid_actions=True)
state, _ = env.reset(options={"task_index": 0})
print(state.hidden.valid_actions) # ('open mailbox', 'go north', ...)
Adapter Configuration¶
Game Selection¶
Select games by name, file path, or get all bundled games:
# Single game from name
env = adapter.get_environment("jericho:zork1")
# Multiple games by name
env = adapter.get_environment(games=["zork1", "detective", "hhgg"])
# All bundled games
env = adapter.get_environment("jericho")
# Direct ROM file path
env = adapter.get_environment(game_files=["/path/to/custom_game.z5"])
Parameters¶
env = adapter.get_environment(
name="jericho:zork1",
max_steps=100, # default: 100
include_valid_actions=False, # default: False (wrapper fidelity)
pure_step=True, # default: False; enable for MC rollouts
invalid_action_text="[invalid action]", # default placeholder in history
invalid_action_observation=None, # custom reminder prefix
advance_on_invalid="wait", # default: keep Jericho moves aligned
prompts={ # override prompt templates
"valid_actions_prefix": "Available commands:",
},
)
When an answer_extractor is configured and extraction fails, Jericho no longer
forwards the raw model text to the emulator. By default it executes wait,
prepends an invalid-format reminder to the observation, and stores
[invalid action] in history display. This keeps wrapper turns and Jericho's
internal move counter aligned while avoiding refusal or hallucinated text
polluting the conversation history.
Rewards¶
Jericho provides score-based rewards with two modes depending on step type:
| Reward | Type | Value |
|---|---|---|
game_score |
STEP (intermediate) |
Score delta since last step |
game_score |
OUTCOME (terminal) |
Normalized score: score / max_score |
result = env.step(state, Action(text="take leaflet"))
signal = result.rewards.by_name("game_score")
print(signal.reward) # e.g., 5 (score delta)
print(signal.reward_type) # RewardType.STEP
On terminal steps (game over or truncation):
signal = result.rewards.by_name("game_score")
print(signal.reward) # e.g., 0.5 (175/350 normalized)
print(signal.reward_type) # RewardType.OUTCOME
Add extra reward signals:
env = JerichoEnvironment(
game_files=game_files,
game_names=game_names,
extra_rewards=(my_custom_reward,),
)
Hidden State¶
@dataclass(frozen=True)
class JerichoHidden:
task_index: int # index into game_files
game_name: str # e.g., "zork1"
game_file: str # path to ROM file
episode_step: int # current step count
last_action: str | None # last action taken
score: int # current cumulative score
max_score: int # maximum achievable score
moves: int # move counter from Jericho
valid_actions: tuple[str, ...] # generated valid actions, or () when disabled
prev_score: int = 0 # score at previous step (for delta)
frotz_state: Any = None # Z-Machine snapshot (pure_step only)
Native Fault Handling¶
Jericho's upstream Frotz integration can report a halted emulator or trigger a native fault on rare (state, action) pairs. The adapter surfaces detected halts as JerichoEmulatorHaltedError instead of silently continuing with a poisoned interpreter instance:
from llenvs.adapters.jericho import JerichoEmulatorHaltedError
try:
result = env.step(state, Action(text="some risky command"))
except JerichoEmulatorHaltedError:
# Discard the point/trajectory and continue at the caller level.
...
When this happens, the adapter closes and discards the underlying FrotzEnv immediately.
Using with TrajectoryRunner¶
from llenvs.adapters import JerichoAdapter
from llenvs.inference.backends import OpenAIBackend
from llenvs.evaluation import TrajectoryRunner
from llenvs.inference import SamplingParams
adapter = JerichoAdapter()
env = adapter.get_environment(
"jericho:zork1",
max_steps=100,
)
backend = OpenAIBackend(model="gpt-4o")
runner = TrajectoryRunner(
environment=env,
backend=backend,
sampling_params=SamplingParams(temperature=0.0),
)
result = runner.run_trajectory(task_index=0)
print(f"Score: {result.trajectory.final_state.hidden.score}")
print(f"Max: {result.trajectory.final_state.hidden.max_score}")
State Snapshots (pure_step)¶
With pure_step=True, the adapter uses Jericho's native get_state()/set_state() to save and restore Z-Machine state. This enables stepping from any previously visited state (branching), which is required for Monte Carlo rollout-based ground truth estimation.
env = adapter.get_environment("jericho:zork1", pure_step=True)
state_0, _ = env.reset(options={"task_index": 0})
# Branch A
result_a = env.step(state_0, Action(text="go north"))
# Branch B from the same state
result_b = env.step(state_0, Action(text="open mailbox"))
State snapshots are compact (native Z-Machine tuples, a few KB) and much faster than pickle-based approaches.
Limitations¶
- Seed support is available but behavior may vary by game
- Some games may have quirks in their Z-Machine implementations