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