Simon Sassi commited on
Commit
c85904c
·
1 Parent(s): 893a53d

chore: add back spacy dependency for inventory/location parsing and valid action generation

Browse files
Files changed (3) hide show
  1. games/__init__.py +6 -0
  2. games/zork_env.py +219 -0
  3. requirements.txt +1 -0
games/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .zork_env import TextAdventureEnv, GameState, list_available_games, discover_games
2
+
3
+ # Alias for backwards compatibility
4
+ ZorkEnvironment = TextAdventureEnv
5
+
6
+ __all__ = ["TextAdventureEnv", "ZorkEnvironment", "GameState", "list_available_games", "discover_games"]
games/zork_env.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Text Adventure Game Environment
3
+
4
+ Provides a clean interface to text adventure games via Jericho.
5
+ Supports Zork and many other classic Z-machine games.
6
+ """
7
+
8
+ from jericho import FrotzEnv
9
+ from dataclasses import dataclass
10
+ from typing import Optional
11
+ from pathlib import Path
12
+ import os
13
+
14
+
15
+ @dataclass
16
+ class GameState:
17
+ """Represents the current state of the game."""
18
+ observation: str
19
+ score: int
20
+ max_score: int
21
+ moves: int
22
+ done: bool
23
+ reward: int # Points gained from last action
24
+ inventory: list[str]
25
+ location: str
26
+
27
+
28
+ def get_default_games_dir() -> Path:
29
+ """Get the default directory containing game files."""
30
+ project_root = Path(__file__).parent.parent
31
+ return project_root / "z-machine-games-master" / "jericho-game-suite"
32
+
33
+
34
+ def discover_games(games_dir: Optional[Path] = None) -> dict[str, Path]:
35
+ """
36
+ Discover all available Z-machine games in the games directory.
37
+
38
+ Args:
39
+ games_dir: Directory to search for games (default: jericho-game-suite)
40
+
41
+ Returns:
42
+ Dictionary mapping game name (without extension) to full path
43
+ """
44
+ if games_dir is None:
45
+ games_dir = get_default_games_dir()
46
+
47
+ games_dir = Path(games_dir)
48
+ if not games_dir.exists():
49
+ return {}
50
+
51
+ games = {}
52
+ # Find all Z-machine game files (.z3, .z4, .z5, .z8)
53
+ for ext in ["*.z3", "*.z4", "*.z5", "*.z8"]:
54
+ for game_path in games_dir.glob(ext):
55
+ # Use stem (filename without extension) as game name
56
+ game_name = game_path.stem.lower()
57
+ games[game_name] = game_path
58
+
59
+ return dict(sorted(games.items()))
60
+
61
+
62
+ def list_available_games(games_dir: Optional[Path] = None) -> list[str]:
63
+ """Return a sorted list of available game names."""
64
+ return list(discover_games(games_dir).keys())
65
+
66
+
67
+ class TextAdventureEnv:
68
+ """Wrapper around Jericho's FrotzEnv for text adventure games."""
69
+
70
+ def __init__(self, game: str = "zork1", games_dir: Optional[str] = None):
71
+ """
72
+ Initialize the text adventure environment.
73
+
74
+ Args:
75
+ game: Game name (e.g., 'zork1', 'advent', 'enchanter')
76
+ Can also be a full path to a .z* file
77
+ games_dir: Directory containing game files (optional)
78
+ """
79
+ # Check if game is a full path
80
+ if os.path.isfile(game):
81
+ game_path = Path(game)
82
+ self.game = game_path.stem
83
+ else:
84
+ # Look up game by name
85
+ games_path = Path(games_dir) if games_dir else None
86
+ available_games = discover_games(games_path)
87
+
88
+ if game.lower() not in available_games:
89
+ available = list(available_games.keys())[:20]
90
+ raise ValueError(
91
+ f"Unknown game: {game}. "
92
+ f"Available: {', '.join(available)}... "
93
+ f"({len(available_games)} total)"
94
+ )
95
+
96
+ game_path = available_games[game.lower()]
97
+ self.game = game.lower()
98
+
99
+ self.env = FrotzEnv(str(game_path))
100
+ self.game_path = game_path
101
+ self._last_score = 0
102
+ self._history: list[tuple[str, str]] = [] # (action, observation) pairs
103
+
104
+ def reset(self) -> GameState:
105
+ """Reset the game to the beginning."""
106
+ observation, info = self.env.reset()
107
+ self._last_score = 0
108
+ self._history = []
109
+ return self._make_game_state(observation, info, done=False, reward=0)
110
+
111
+ def step(self, action: str) -> GameState:
112
+ """
113
+ Take an action in the game.
114
+
115
+ Args:
116
+ action: The text command to execute (e.g., "go north", "take lamp")
117
+
118
+ Returns:
119
+ GameState with the result of the action
120
+ """
121
+ observation, reward, done, info = self.env.step(action)
122
+
123
+ # Track reward as score change
124
+ current_score = info.get('score', 0)
125
+ reward = current_score - self._last_score
126
+ self._last_score = current_score
127
+
128
+ # Record history
129
+ self._history.append((action, observation))
130
+
131
+ return self._make_game_state(observation, info, done, reward)
132
+
133
+ def _make_game_state(self, observation: str, info: dict, done: bool, reward: int) -> GameState:
134
+ """Create a GameState from the environment info."""
135
+ # Try to get inventory and location (may fail without spacy)
136
+ try:
137
+ inventory = [str(obj) for obj in self.env.get_inventory()]
138
+ except Exception:
139
+ inventory = []
140
+
141
+ try:
142
+ location = str(self.env.get_player_location())
143
+ except Exception:
144
+ location = "Unknown"
145
+
146
+ return GameState(
147
+ observation=observation,
148
+ score=info.get('score', 0),
149
+ max_score=self.env.get_max_score(),
150
+ moves=info.get('moves', 0),
151
+ done=done,
152
+ reward=reward,
153
+ inventory=inventory,
154
+ location=location,
155
+ )
156
+
157
+ def get_history(self) -> list[tuple[str, str]]:
158
+ """Get the history of (action, observation) pairs."""
159
+ return self._history.copy()
160
+
161
+ def get_valid_actions(self) -> list[str]:
162
+ """
163
+ Get a list of valid actions for the current state.
164
+ Note: This requires spacy to be properly installed.
165
+ """
166
+ try:
167
+ return self.env.get_valid_actions()
168
+ except Exception:
169
+ # Return common actions if spacy isn't available
170
+ return [
171
+ "north", "south", "east", "west",
172
+ "up", "down", "look", "inventory",
173
+ "take all", "open mailbox", "read"
174
+ ]
175
+
176
+ def save_state(self):
177
+ """Save the current game state."""
178
+ return self.env.get_state()
179
+
180
+ def load_state(self, state):
181
+ """Load a previously saved game state."""
182
+ self.env.set_state(state)
183
+
184
+ def get_walkthrough(self) -> list[str]:
185
+ """Get the walkthrough for the game (for debugging/comparison only)."""
186
+ return self.env.get_walkthrough()
187
+
188
+
189
+ # Alias for backwards compatibility
190
+ ZorkEnvironment = TextAdventureEnv
191
+
192
+
193
+ # Example usage
194
+ if __name__ == "__main__":
195
+ import sys
196
+
197
+ # List available games
198
+ games = list_available_games()
199
+ print(f"Available games ({len(games)} total):")
200
+ print(f" {', '.join(games[:15])}...")
201
+ print()
202
+
203
+ # Use command line arg or default to zork1
204
+ game = sys.argv[1] if len(sys.argv) > 1 else "zork1"
205
+
206
+ env = TextAdventureEnv(game)
207
+ state = env.reset()
208
+
209
+ print(f"=== {env.game.upper()} ===")
210
+ print(f"Max Score: {state.max_score}")
211
+ print(f"\n{state.observation}")
212
+ print(f"\nValid actions: {env.get_valid_actions()[:10]}...")
213
+
214
+ # Try a few actions
215
+ for action in ["look", "inventory"]:
216
+ print(f"\n> {action}")
217
+ state = env.step(action)
218
+ print(state.observation)
219
+ print(f"Score: {state.score}, Reward: {state.reward}")
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
  graphiti-core[kuzu]>=0.28.0
2
  kuzu>=0.11.3
 
 
1
  graphiti-core[kuzu]>=0.28.0
2
  kuzu>=0.11.3
3
+ en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl