kaggle-environments 1.17.2__py2.py3-none-any.whl → 1.17.5__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kaggle-environments might be problematic. Click here for more details.

Files changed (30) hide show
  1. kaggle_environments/__init__.py +2 -2
  2. kaggle_environments/envs/open_spiel/__init__.py +0 -0
  3. kaggle_environments/envs/open_spiel/games/__init__.py +0 -0
  4. kaggle_environments/envs/open_spiel/games/chess/chess.js +294 -0
  5. kaggle_environments/envs/open_spiel/games/connect_four/__init__.py +0 -0
  6. kaggle_environments/envs/open_spiel/games/connect_four/connect_four.js +296 -0
  7. kaggle_environments/envs/open_spiel/games/connect_four/connect_four_proxy.py +86 -0
  8. kaggle_environments/envs/open_spiel/games/connect_four/connect_four_proxy_test.py +57 -0
  9. kaggle_environments/envs/open_spiel/games/go/__init__.py +0 -0
  10. kaggle_environments/envs/open_spiel/games/go/go.js +481 -0
  11. kaggle_environments/envs/open_spiel/games/go/go_proxy.py +105 -0
  12. kaggle_environments/envs/open_spiel/games/tic_tac_toe/__init__.py +0 -0
  13. kaggle_environments/envs/open_spiel/games/tic_tac_toe/tic_tac_toe.js +345 -0
  14. kaggle_environments/envs/open_spiel/games/tic_tac_toe/tic_tac_toe_proxy.py +101 -0
  15. kaggle_environments/envs/open_spiel/games/universal_poker/__init__.py +0 -0
  16. kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker.js +431 -0
  17. kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker_proxy.py +159 -0
  18. kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker_proxy_test.py +49 -0
  19. kaggle_environments/envs/open_spiel/html_playthrough_generator.py +30 -0
  20. kaggle_environments/envs/open_spiel/observation.py +133 -0
  21. kaggle_environments/envs/open_spiel/open_spiel.py +325 -224
  22. kaggle_environments/envs/open_spiel/proxy.py +139 -0
  23. kaggle_environments/envs/open_spiel/proxy_test.py +64 -0
  24. kaggle_environments/envs/open_spiel/test_open_spiel.py +23 -8
  25. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/METADATA +2 -2
  26. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/RECORD +30 -9
  27. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/WHEEL +0 -0
  28. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/entry_points.txt +0 -0
  29. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/licenses/LICENSE +0 -0
  30. {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/top_level.txt +0 -0
@@ -1,87 +1,129 @@
1
1
  """Kaggle environment wrapper for OpenSpiel games."""
2
2
 
3
3
  import copy
4
+ import importlib
5
+ import logging
6
+ import os
7
+ import pathlib
4
8
  import random
5
- from typing import Any
9
+ import sys
10
+ from typing import Any, Callable
6
11
 
7
12
  from kaggle_environments import core
8
13
  from kaggle_environments import utils
9
14
  import numpy as np
10
15
  import pyspiel
11
16
 
17
+ _log = logging.getLogger(__name__)
18
+ _log.setLevel(logging.INFO)
19
+ _handler = logging.StreamHandler(sys.stdout)
20
+ _formatter = logging.Formatter('[%(name)s] %(levelname)s: %(message)s')
21
+ _handler.setFormatter(_formatter)
22
+ _log.addHandler(_handler)
23
+
24
+ # --- Import proxy games ---
25
+ _log.debug("Auto-importing OpenSpiel game proxies...")
26
+ GAMES_DIR = pathlib.Path(__file__).parent / "games"
27
+ for proxy_file in GAMES_DIR.glob("**/*_proxy.py"):
28
+ try:
29
+ relative_path = proxy_file.relative_to(GAMES_DIR.parent)
30
+ module_path = str(relative_path.with_suffix("")).replace(os.path.sep, ".")
31
+ importlib.import_module("." + module_path, package=__package__)
32
+ _log.debug(f" - Imported: {module_path}")
33
+ except Exception as e: # pylint: disable=broad-exception-caught
34
+ _log.debug(f" - FAILED to import proxy from {proxy_file.name}: {e}")
35
+
12
36
 
37
+ # --- Constants ---
13
38
  DEFAULT_ACT_TIMEOUT = 5
14
39
  DEFAULT_RUN_TIMEOUT = 1200
15
- DEFAULT_EPISODE_STEP_BUFFER = 100 # To account for timeouts, retrys, etc...
40
+ # Buffer in addition to max game length to account for timeouts, retrys, etc.
41
+ DEFAULT_STEP_BUFFER = 100
42
+ # TODO(jhtschultz): Add individual game descriptions.
43
+ DEFAULT_DESCRIPTION = """
44
+ Kaggle environment wrapper for OpenSpiel games.
45
+ For game implementation details see:
46
+ https://github.com/google-deepmind/open_spiel/tree/master/open_spiel/games
47
+ """.strip()
16
48
 
17
- BASE_SPEC_TEMPLATE = {
18
- "name": "PLACEHOLDER_NAME",
19
- "title": "PLACEHOLDER_TITLE",
20
- "description": "PLACEHOLDER_DESCRIPTION",
21
- "version": "0.1.0",
22
- "agents": ["PLACEHOLDER_NUM_AGENTS"],
49
+ CONFIGURATION_SPEC_TEMPLATE = {
50
+ "episodeSteps": -1,
51
+ "actTimeout": DEFAULT_ACT_TIMEOUT,
52
+ "runTimeout": DEFAULT_RUN_TIMEOUT,
53
+ "openSpielGameString": {
54
+ "description": "The full game string including parameters.",
55
+ "type": "string",
56
+ "default": "PLACEHOLDER_GAME_STRING"
57
+ },
58
+ "openSpielGameName": {
59
+ "description": "The short_name of the OpenSpiel game to load.",
60
+ "type": "string",
61
+ "default": "PLACEHOLDER_GAME_SHORT_NAME"
62
+ },
63
+ "openSpielGameParameters": {
64
+ "description": "Game parameters for Open Spiel game.",
65
+ "type": "object",
66
+ "default": {}
67
+ },
68
+ }
23
69
 
24
- "configuration": {
25
- "episodeSteps": -1,
26
- "actTimeout": DEFAULT_ACT_TIMEOUT,
27
- "runTimeout": DEFAULT_RUN_TIMEOUT,
70
+ OBSERVATION_SPEC_TEMPLATE = {
71
+ "properties": {
28
72
  "openSpielGameString": {
29
- "description": "The full game string including parameters.",
30
- "type": "string",
31
- "default": "PLACEHOLDER_GAME_STRING"
73
+ "description": "Full game string including parameters.",
74
+ "type": "string"
32
75
  },
33
76
  "openSpielGameName": {
34
- "description": "The short_name of the OpenSpiel game to load.",
35
- "type": "string",
36
- "default": "PLACEHOLDER_GAME_SHORT_NAME"
77
+ "description": "Short name of the OpenSpiel game.",
78
+ "type": "string"
37
79
  },
38
- },
39
- "observation": {
40
- "properties": {
41
- "openSpielGameString": {
42
- "description": "Full game string including parameters.",
43
- "type": "string"
44
- },
45
- "openSpielGameName": {
46
- "description": "Short name of the OpenSpiel game.",
47
- "type": "string"
48
- },
49
- "observation_string": {
50
- "description": "String representation of state.",
51
- "type": "string"
52
- },
53
- # TODO(jhtschultz): add legal action strings
54
- "legal_actions": {
55
- "description": "List of OpenSpiel legal actions.",
56
- "type": "array",
57
- "items": {
58
- "type": "integer"
59
- }
60
- },
61
- "chance_outcome_probs": {
62
- "description": "List of probabilities for chance outcomes.",
63
- "type": "array",
64
- "items": {
65
- "type": "float"
66
- }
67
- },
68
- "current_player": {
69
- "description": "ID of player whose turn it is.",
70
- "type": "integer"
71
- },
72
- "is_terminal": {
73
- "description": "Boolean indicating game end.",
74
- "type": "boolean"
75
- },
76
- "player_id": {
77
- "description": "ID of the agent receiving this observation.",
80
+ "observation_string": {
81
+ "description": "String representation of state.",
82
+ "type": "string"
83
+ },
84
+ # TODO(jhtschultz): Use camel case for consistency with spec, or snake
85
+ # case for consistency with pyspiel?
86
+ "legal_actions": {
87
+ "description": "List of OpenSpiel legal action integers.",
88
+ "type": "array",
89
+ "items": {
78
90
  "type": "integer"
79
- },
80
- "remainingOverageTime": 60,
81
- "step": 0
91
+ }
82
92
  },
83
- "default": {}
93
+ "legal_action_strings": {
94
+ "description": "List of OpenSpiel legal actions strings.",
95
+ "type": "array",
96
+ "items": {
97
+ "type": "string"
98
+ }
99
+ },
100
+ "current_player": {
101
+ "description": "ID of player whose turn it is.",
102
+ "type": "integer"
103
+ },
104
+ "player_id": {
105
+ "description": "ID of the agent receiving this observation.",
106
+ "type": "integer"
107
+ },
108
+ "is_terminal": {
109
+ "description": "Boolean indicating game end.",
110
+ "type": "boolean"
111
+ },
112
+ "remainingOverageTime": 60,
113
+ "step": 0
84
114
  },
115
+ "default": {}
116
+ }
117
+
118
+
119
+ ENV_SPEC_TEMPLATE = {
120
+ "name": "PLACEHOLDER_NAME",
121
+ "title": "PLACEHOLDER_TITLE",
122
+ "description": DEFAULT_DESCRIPTION,
123
+ "version": "0.1.0",
124
+ "agents": ["PLACEHOLDER_NUM_AGENTS"],
125
+ "configuration": CONFIGURATION_SPEC_TEMPLATE,
126
+ "observation": OBSERVATION_SPEC_TEMPLATE,
85
127
  "action": {
86
128
  "type": ["integer"],
87
129
  "minimum": -1,
@@ -94,130 +136,120 @@ BASE_SPEC_TEMPLATE = {
94
136
  }
95
137
 
96
138
 
97
- _OS_GLOBAL_GAME = None
98
- _OS_GLOBAL_STATE = None
99
-
100
-
101
- def _get_open_spiel_game(env_config: utils.Struct) -> pyspiel.Game:
102
- global _OS_GLOBAL_GAME
103
- game_string = env_config.get("openSpielGameString")
104
- if game_string == str(_OS_GLOBAL_GAME):
105
- return _OS_GLOBAL_GAME
106
- if _OS_GLOBAL_GAME is not None:
107
- print(
108
- f"WARNING: Overwriting game. Old: {_OS_GLOBAL_GAME}. New {game_string}"
109
- )
110
- _OS_GLOBAL_GAME = pyspiel.load_game(game_string)
111
- return _OS_GLOBAL_GAME
112
-
139
+ # --- Core step logic ---
113
140
 
114
141
  def interpreter(
115
142
  state: list[utils.Struct],
116
143
  env: core.Environment,
117
144
  ) -> list[utils.Struct]:
118
145
  """Updates environment using player responses and returns new observations."""
119
- global _OS_GLOBAL_GAME, _OS_GLOBAL_STATE
120
- kaggle_state = state
146
+ kaggle_state = state # Not to be confused with OpenSpiel state.
121
147
  del state
122
148
 
149
+ # TODO(jhtschultz): Test reset behavior. Currently containers are restarted
150
+ # after each episode.
123
151
  if env.done:
124
152
  return kaggle_state
125
153
 
126
- # --- Get Game Info ---
127
- game = _get_open_spiel_game(env.configuration)
128
- num_players = game.num_players()
154
+ # --- Get and maybe initialize game and state on the env object ---
155
+ if not hasattr(env, 'os_game'):
156
+ game_string = env.configuration.get("openSpielGameString")
157
+ env.os_game = pyspiel.load_game(game_string)
158
+ if not hasattr(env, 'os_state'):
159
+ env.os_state = env.os_game.new_initial_state()
160
+ if "state_history" not in env.info:
161
+ env.info['state_history'] = [str(env.os_state)]
162
+ env.info['action_history'] = []
163
+
164
+ os_game = env.os_game
165
+ os_state = env.os_state
166
+ num_players = os_game.num_players()
129
167
  statuses = [
130
- kaggle_state[os_current_player].status
131
- for os_current_player in range(num_players)
168
+ kaggle_state[player_id].status for player_id in range(num_players)
132
169
  ]
133
170
  if not any(status == "ACTIVE" for status in statuses):
134
171
  raise ValueError("Environment not done and no active agents.")
135
172
 
136
- # --- Initialization / Reset ---
137
- # TODO(jhtschultz): test this behavior.
173
+ # TODO(jhtschultz): Test reset behavior.
138
174
  is_initial_step = len(env.steps) == 1
139
- if _OS_GLOBAL_STATE is None or (not is_initial_step and env.done):
140
- _OS_GLOBAL_STATE = game.new_initial_state()
175
+ if is_initial_step and os_state.is_terminal():
176
+ env.os_state = os_game.new_initial_state()
177
+ os_state = env.os_state
141
178
 
142
- # --- Maybe apply agent action ---
143
- os_current_player = _OS_GLOBAL_STATE.current_player()
179
+ # --- Apply agent action ---
180
+ acting_agent = os_state.current_player()
181
+ action_submitted = None
144
182
  action_applied = None
145
183
  if is_initial_step:
146
184
  pass
147
- elif 0 <= os_current_player < num_players:
148
- if kaggle_state[os_current_player].status != "ACTIVE":
185
+ elif 0 <= acting_agent < num_players:
186
+ if kaggle_state[acting_agent].status != "ACTIVE":
149
187
  pass
150
188
  else:
151
- action_submitted = kaggle_state[os_current_player].action
152
- legal = _OS_GLOBAL_STATE.legal_actions()
153
- if action_submitted in legal:
189
+ action_submitted = kaggle_state[acting_agent].action
190
+ if action_submitted in os_state.legal_actions():
154
191
  try:
155
- _OS_GLOBAL_STATE.apply_action(action_submitted)
192
+ os_state.apply_action(action_submitted)
156
193
  action_applied = action_submitted
157
- except Exception: # pylint: disable=broad-exception-caught
158
- kaggle_state[os_current_player].status = "ERROR"
194
+ env.info['action_history'].append(str(action_applied))
195
+ env.info['state_history'].append(str(os_state))
196
+ except Exception as e: # pylint: disable=broad-exception-caught
197
+ _log.debug(e)
198
+ kaggle_state[acting_agent].status = "ERROR"
159
199
  else:
160
- kaggle_state[os_current_player].status = "INVALID"
161
- elif os_current_player == pyspiel.PlayerId.SIMULTANEOUS:
200
+ kaggle_state[acting_agent].status = "INVALID"
201
+ elif acting_agent == pyspiel.PlayerId.SIMULTANEOUS:
162
202
  raise NotImplementedError
163
- elif os_current_player == pyspiel.PlayerId.TERMINAL:
203
+ elif acting_agent == pyspiel.PlayerId.TERMINAL:
164
204
  pass
165
- elif os_current_player == pyspiel.PlayerId.CHANCE:
205
+ elif acting_agent == pyspiel.PlayerId.CHANCE:
166
206
  raise ValueError("Interpreter should not be called at chance nodes.")
167
207
  else:
168
- raise ValueError(f"Unknown OpenSpiel player ID: {os_current_player}")
169
-
170
- # --- Update state info ---
171
- while _OS_GLOBAL_STATE.is_chance_node():
172
- chance_outcomes = _OS_GLOBAL_STATE.chance_outcomes
173
- outcomes = _OS_GLOBAL_STATE.chance_outcomes()
174
- legal_actions, chance_outcome_probs = zip(*outcomes)
175
- action = np.random.choice(legal_actions, p=chance_outcome_probs)
176
- _OS_GLOBAL_STATE.apply_action(action)
177
- is_terminal = _OS_GLOBAL_STATE.is_terminal()
178
- agent_returns = _OS_GLOBAL_STATE.returns() + [None]
179
- next_agent = _OS_GLOBAL_STATE.current_player()
180
-
181
- for i, agent_state in enumerate(kaggle_state):
182
- input_status = agent_state.status
183
- status = ""
208
+ raise ValueError(f"Unknown OpenSpiel player ID: {acting_agent}")
209
+
210
+ # --- Step chance nodes ---
211
+ while os_state.is_chance_node():
212
+ outcomes, probs = zip(*os_state.chance_outcomes())
213
+ chance_action = np.random.choice(outcomes, p=probs)
214
+ os_state.apply_action(chance_action)
215
+ env.info['action_history'].append(str(chance_action))
216
+ env.info['state_history'].append(str(os_state))
217
+
218
+ # --- Update agent states ---
219
+ for player_id, agent_state in enumerate(kaggle_state):
184
220
  reward = None
185
-
186
- if input_status in ["TIMEOUT", "ERROR", "INVALID"]:
187
- status = input_status
188
- reward = None
189
- elif is_terminal:
221
+ if agent_state.status in ["TIMEOUT", "ERROR", "INVALID"]:
222
+ status = agent_state.status
223
+ elif os_state.is_terminal():
190
224
  status = "DONE"
191
- reward = agent_returns[i]
192
- elif next_agent == i:
225
+ reward = os_state.returns()[player_id]
226
+ elif os_state.current_player() == player_id:
193
227
  status = "ACTIVE"
194
- reward = agent_returns[i]
228
+ if not os_state.legal_actions(player_id):
229
+ raise ValueError(
230
+ f"Active agent {i} has no legal actions in state {os_state}."
231
+ )
195
232
  else:
196
233
  status = "INACTIVE"
197
- reward = agent_returns[i]
198
234
 
199
235
  info_dict = {}
200
- # Store the applied action in info for potential debugging/analysis
201
- if os_current_player == i and action_applied is not None:
236
+ if acting_agent == player_id:
237
+ info_dict["action_submitted"] = action_submitted
202
238
  info_dict["action_applied"] = action_applied
203
239
 
204
- game_type = _OS_GLOBAL_GAME.get_type()
205
- obs_str = str(_OS_GLOBAL_STATE)
206
- legal_actions = _OS_GLOBAL_STATE.legal_actions(i)
207
-
208
- if status == "ACTIVE" and not legal_actions:
209
- raise ValueError(
210
- f"Active agent {i} has no legal actions in state {_OS_GLOBAL_STATE}."
211
- )
212
-
213
- # Apply updates
214
240
  obs_update_dict = {
215
- "observation_string": obs_str,
216
- "legal_actions": legal_actions,
217
- "current_player": next_agent,
218
- "is_terminal": is_terminal,
219
- "player_id": i,
241
+ "observation_string": os_state.observation_string(player_id),
242
+ "legal_actions": os_state.legal_actions(player_id),
243
+ "legal_action_strings": [
244
+ os_state.action_to_string(action) for action
245
+ in os_state.legal_actions(player_id)
246
+ ],
247
+ "current_player": os_state.current_player(),
248
+ "is_terminal": os_state.is_terminal(),
249
+ "player_id": player_id,
220
250
  }
251
+
252
+ # Apply updates
221
253
  for k, v in obs_update_dict.items():
222
254
  setattr(agent_state.observation, k, v)
223
255
  agent_state.reward = reward
@@ -227,54 +259,90 @@ def interpreter(
227
259
  return kaggle_state
228
260
 
229
261
 
262
+ # --- Rendering ---
263
+
230
264
  def renderer(state: list[utils.Struct], env: core.Environment) -> str:
231
- """Kaggle renderer function."""
232
- try:
233
- obs_str = state[-1].observation["observation_string"]
234
- return obs_str if obs_str else "<Empty observation string>"
235
- except Exception as e: # pylint: disable=broad-exception-caught
236
- print(f"Error rendering {env.name} at state: {state}.")
237
- raise e
265
+ """Kaggle environment text renderer."""
266
+ if hasattr(env, 'os_state'):
267
+ return str(env.os_state)
268
+ else:
269
+ return "Game state uninitialized."
238
270
 
239
271
 
240
- def html_renderer():
241
- """Provides the simplest possible HTML/JS renderer for OpenSpiel text observations."""
272
+ # TODO(jhtschultz): Use custom player.html that replays from env.info instead
273
+ # of player steps. The full game state is stored in env.info, player steps only
274
+ # contain player observations.
275
+ def _default_html_renderer() -> str:
276
+ """Provides the JavaScript string for the default HTML renderer."""
242
277
  return """
243
278
  function renderer(context) {
244
279
  const { parent, environment, step } = context;
245
- parent.innerHTML = ''; // Clear previous rendering
280
+ parent.innerHTML = ''; // Clear previous rendering
246
281
 
247
- // Get the current step's data
248
282
  const currentStepData = environment.steps[step];
249
- const numAgents = currentStepData.length
250
- const gameMasterIndex = numAgents - 1
283
+ if (!currentStepData) {
284
+ parent.textContent = "Waiting for step data...";
285
+ return;
286
+ }
287
+ const agentObsIndex = 0
251
288
  let obsString = "Observation not available for this step.";
289
+ let title = `Step: ${step}`;
252
290
 
253
- // Try to get the raw observation string from the game master agent.
254
- if (currentStepData && currentStepData[gameMasterIndex] && currentStepData[gameMasterIndex].observation && currentStepData[gameMasterIndex].observation.observation_string !== undefined) {
255
- obsString = currentStepData[gameMasterIndex].observation.observation_string;
256
- } else if (step === 0 && environment.steps[0] && environment.steps[0][gameMasterIndex] && environment.steps[0][gameMasterIndex].observation && environment.steps[0][gameMasterIndex].observation.observation_string !== undefined) {
257
- // Fallback for initial state if current step data is missing
258
- obsString = environment.steps[0][gameMasterIndex].observation.observation_string;
291
+ if (environment.configuration && environment.configuration.openSpielGameName) {
292
+ title = `${environment.configuration.openSpielGameName} - Step: ${step}`;
293
+ }
294
+
295
+ // Try to get obs_string from game_master of current step
296
+ if (currentStepData[agentObsIndex] &&
297
+ currentStepData[agentObsIndex].observation &&
298
+ typeof currentStepData[agentObsIndex].observation.observation_string === 'string') {
299
+ obsString = currentStepData[agentObsIndex].observation.observation_string;
300
+ }
301
+ // Fallback to initial step if current is unavailable (e.g. very first render call)
302
+ else if (step === 0 && environment.steps[0] && environment.steps[0][agentObsIndex] &&
303
+ environment.steps[0][agentObsIndex].observation &&
304
+ typeof environment.steps[0][agentObsIndex].observation.observation_string === 'string') {
305
+ obsString = environment.steps[0][agentObsIndex].observation.observation_string;
259
306
  }
260
307
 
261
- // Create a <pre> element to preserve formatting
262
308
  const pre = document.createElement("pre");
263
- pre.style.fontFamily = "monospace"; // Ensure monospace font
264
- pre.style.margin = "10px"; // Add some padding
309
+ pre.style.fontFamily = "monospace";
310
+ pre.style.margin = "10px";
265
311
  pre.style.border = "1px solid #ccc";
266
- pre.style.padding = "5px";
267
- pre.style.backgroundColor = "#f0f0f0";
268
-
269
- // Set the text content (safer than innerHTML for plain text)
270
- pre.textContent = `Step: ${step}\\n\\n${obsString}`; // Add step number for context
312
+ pre.style.padding = "10px";
313
+ pre.style.backgroundColor = "#f9f9f9";
314
+ pre.style.whiteSpace = "pre-wrap";
315
+ pre.style.wordBreak = "break-all";
271
316
 
317
+ pre.textContent = `${title}\\n\\n${obsString}`;
272
318
  parent.appendChild(pre);
273
319
  }
274
320
  """
275
321
 
322
+ def _get_html_renderer_content(
323
+ open_spiel_short_name: str,
324
+ base_path_for_custom_renderers: pathlib.Path,
325
+ default_renderer_func: Callable[[], str]
326
+ ) -> str:
327
+ """Tries to load a custom JS renderer for the game, falls back to default."""
328
+ custom_renderer_js_path = pathlib.Path(
329
+ base_path_for_custom_renderers,
330
+ open_spiel_short_name,
331
+ f"{open_spiel_short_name}.js",
332
+ )
333
+ if custom_renderer_js_path.is_file():
334
+ try:
335
+ with open(custom_renderer_js_path, "r", encoding="utf-8") as f:
336
+ content = f.read()
337
+ _log.debug(f"Using custom HTML renderer for {open_spiel_short_name} from {custom_renderer_js_path}")
338
+ return content
339
+ except Exception as e: # pylint: disable=broad-exception-caught
340
+ _log.debug(e)
341
+ return default_renderer_func()
342
+
276
343
 
277
344
  # --- Agents ---
345
+
278
346
  def random_agent(
279
347
  observation: dict[str, Any],
280
348
  configuration: dict[str, Any],
@@ -288,67 +356,100 @@ def random_agent(
288
356
  return int(action)
289
357
 
290
358
 
291
- agents = {
359
+ AGENT_REGISTRY = {
292
360
  "random": random_agent,
293
361
  }
294
362
 
295
363
 
296
- def _register_open_spiel_envs(
297
- games_list: list[str] | None = None,
298
- ) -> dict[str, Any]:
299
- successfully_loaded_games = []
364
+ # --- Build and register environments ---
365
+
366
+ def _build_env(game_string: str) -> dict[str, Any]:
367
+ game = pyspiel.load_game(game_string)
368
+ short_name = game.get_type().short_name
369
+
370
+ proxy_path = GAMES_DIR / short_name / f"{short_name}_proxy.py"
371
+ if proxy_path.is_file():
372
+ game = pyspiel.load_game(short_name + "_proxy", game.get_parameters())
373
+
374
+ game_type = game.get_type()
375
+ if not game_type.provides_observation_string:
376
+ raise ValueError(f"No observation string for game: {game_string}")
377
+
378
+ env_spec = copy.deepcopy(ENV_SPEC_TEMPLATE)
379
+ env_spec["name"] = f"open_spiel_{short_name}"
380
+ env_spec["title"] = f"Open Spiel: {short_name}"
381
+ env_spec["agents"] = [game.num_players()]
382
+
383
+ env_config = copy.deepcopy(CONFIGURATION_SPEC_TEMPLATE)
384
+ env_spec["configuration"] = env_config
385
+ env_config["episodeSteps"] = game.max_history_length() + DEFAULT_STEP_BUFFER
386
+ env_config["openSpielGameString"]["default"] = str(game)
387
+ env_config["openSpielGameName"]["default"] = short_name
388
+
389
+ env_obs = copy.deepcopy(OBSERVATION_SPEC_TEMPLATE)
390
+ env_spec["observation"] = env_obs
391
+ env_obs["properties"]["openSpielGameString"]["default"] = str(game)
392
+ env_obs["properties"]["openSpielGameName"]["default"] = short_name
393
+
394
+ # Building html_renderer_callable is a bit convoluted but other approaches
395
+ # fail for a variety of reasons. Returning a simple lambda function
396
+ # doesn't work because of late-binding -- the last env registered will
397
+ # overwrite all previous renderers.
398
+ js_string_content = _get_html_renderer_content(
399
+ open_spiel_short_name=short_name,
400
+ base_path_for_custom_renderers=GAMES_DIR,
401
+ default_renderer_func=_default_html_renderer,
402
+ )
403
+
404
+ def create_html_renderer_closure(captured_content):
405
+ def html_renderer_callable_no_args():
406
+ return captured_content
407
+ return html_renderer_callable_no_args
408
+
409
+ html_renderer_callable = create_html_renderer_closure(js_string_content)
410
+
411
+ return {
412
+ "specification": env_spec,
413
+ "interpreter": interpreter,
414
+ "renderer": renderer,
415
+ "html_renderer": html_renderer_callable,
416
+ "agents": AGENT_REGISTRY,
417
+ }
418
+
419
+
420
+ def _register_game_envs(games_list: list[str]) -> dict[str, Any]:
300
421
  skipped_games = []
301
422
  registered_envs = {}
302
- if games_list is None:
303
- games_list = pyspiel.registered_names()
304
- for short_name in games_list:
423
+ for game_string in games_list:
305
424
  try:
306
- game = pyspiel.load_game(short_name)
307
- game_type = game.get_type()
308
- if not any([
309
- game_type.provides_information_state_string,
310
- game_type.provides_observation_string,
311
- ]):
425
+ env_config = _build_env(game_string)
426
+ if env_config is None:
312
427
  continue
313
- game_spec = copy.deepcopy(BASE_SPEC_TEMPLATE)
314
- env_name = f"open_spiel_{short_name.replace('-', '_').replace('.', '_')}"
315
- game_spec["name"] = env_name
316
- game_spec["title"] = f"Open Spiel: {short_name}"
317
- game_spec["description"] = """
318
- Kaggle environment wrapper for OpenSpiel games.
319
- For game implementation details see:
320
- https://github.com/google-deepmind/open_spiel/tree/master/open_spiel/games
321
- """.strip()
322
- game_spec["agents"] = [game.num_players()]
323
- game_spec["configuration"]["episodeSteps"] = (
324
- game.max_history_length() + DEFAULT_EPISODE_STEP_BUFFER
325
- )
326
- game_spec["configuration"]["openSpielGameString"]["default"] = str(game)
327
- game_spec["configuration"]["openSpielGameName"]["default"] = short_name
328
- game_spec["observation"]["properties"]["openSpielGameString"][
329
- "default"] = str(game)
330
- game_spec["observation"]["properties"]["openSpielGameName"][
331
- "default"] = short_name
332
-
333
- registered_envs[env_name] = {
334
- "specification": game_spec,
335
- "interpreter": interpreter,
336
- "renderer": renderer,
337
- "html_renderer": html_renderer,
338
- "agents": agents,
339
- }
340
- successfully_loaded_games.append(short_name)
341
-
342
- except Exception: # pylint: disable=broad-exception-caught
343
- skipped_games.append(short_name)
344
- continue
345
-
346
- print(f"""
347
- Successfully loaded OpenSpiel environments: {len(successfully_loaded_games)}.
348
- OpenSpiel games skipped: {len(skipped_games)}.
349
- """.strip())
428
+ env_name = env_config["specification"]["name"]
429
+ if env_name in registered_envs:
430
+ raise ValueError(f"Attempting to overwrite existing env: {env_name}")
431
+ registered_envs[env_name] = env_config
432
+ except Exception as e: # pylint: disable=broad-exception-caught
433
+ _log.debug(e)
434
+ skipped_games.append(game_string)
435
+
436
+ _log.info(f"Successfully loaded OpenSpiel environments: {len(registered_envs)}.")
437
+ for env_name in registered_envs:
438
+ _log.info(f" {env_name}")
439
+ _log.info(f"OpenSpiel games skipped: {len(skipped_games)}.")
440
+ for game_string in skipped_games:
441
+ _log.info(f" {game_string}")
350
442
 
351
443
  return registered_envs
352
444
 
353
445
 
354
- registered_open_spiel_envs = _register_open_spiel_envs()
446
+ GAMES_LIST = [
447
+ "chess",
448
+ "connect_four",
449
+ "gin_rummy",
450
+ "go(board_size=9)",
451
+ "tic_tac_toe",
452
+ "universal_poker(betting=nolimit,bettingAbstraction=fullgame,blind=1 2,firstPlayer=2 1 1 1,numBoardCards=0 3 1 1,numHoleCards=2,numPlayers=2,numRanks=13,numRounds=4,numSuits=4,stack=400 400)",
453
+ ]
454
+
455
+ ENV_REGISTRY = _register_game_envs(GAMES_LIST)