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