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.
- kaggle_environments/__init__.py +2 -2
- kaggle_environments/envs/open_spiel/games/chess/chess.js +294 -0
- kaggle_environments/envs/open_spiel/games/go/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/go/go.js +481 -0
- kaggle_environments/envs/open_spiel/games/go/go_proxy.py +105 -0
- kaggle_environments/envs/open_spiel/games/tic_tac_toe/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/tic_tac_toe/tic_tac_toe.js +345 -0
- kaggle_environments/envs/open_spiel/games/tic_tac_toe/tic_tac_toe_proxy.py +101 -0
- kaggle_environments/envs/open_spiel/games/universal_poker/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker.js +431 -0
- kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker_proxy.py +159 -0
- kaggle_environments/envs/open_spiel/games/universal_poker/universal_poker_proxy_test.py +49 -0
- kaggle_environments/envs/open_spiel/html_playthrough_generator.py +30 -0
- kaggle_environments/envs/open_spiel/open_spiel.py +287 -248
- kaggle_environments/envs/open_spiel/test_open_spiel.py +22 -7
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.5.dist-info}/METADATA +2 -2
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.5.dist-info}/RECORD +21 -9
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.5.dist-info}/WHEEL +0 -0
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.5.dist-info}/entry_points.txt +0 -0
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.5.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
"actTimeout": DEFAULT_ACT_TIMEOUT,
|
|
29
|
-
"runTimeout": DEFAULT_RUN_TIMEOUT,
|
|
70
|
+
OBSERVATION_SPEC_TEMPLATE = {
|
|
71
|
+
"properties": {
|
|
30
72
|
"openSpielGameString": {
|
|
31
|
-
"description": "
|
|
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": "
|
|
37
|
-
"type": "string"
|
|
38
|
-
"default": "PLACEHOLDER_GAME_SHORT_NAME"
|
|
77
|
+
"description": "Short name of the OpenSpiel game.",
|
|
78
|
+
"type": "string"
|
|
39
79
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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[
|
|
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
|
-
#
|
|
139
|
-
# TODO(jhtschultz): test this behavior.
|
|
173
|
+
# TODO(jhtschultz): Test reset behavior.
|
|
140
174
|
is_initial_step = len(env.steps) == 1
|
|
141
|
-
if
|
|
142
|
-
|
|
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
|
-
# ---
|
|
145
|
-
|
|
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 <=
|
|
150
|
-
if kaggle_state[
|
|
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[
|
|
154
|
-
|
|
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
|
-
|
|
192
|
+
os_state.apply_action(action_submitted)
|
|
158
193
|
action_applied = action_submitted
|
|
159
|
-
|
|
160
|
-
|
|
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[
|
|
163
|
-
elif
|
|
200
|
+
kaggle_state[acting_agent].status = "INVALID"
|
|
201
|
+
elif acting_agent == pyspiel.PlayerId.SIMULTANEOUS:
|
|
164
202
|
raise NotImplementedError
|
|
165
|
-
elif
|
|
203
|
+
elif acting_agent == pyspiel.PlayerId.TERMINAL:
|
|
166
204
|
pass
|
|
167
|
-
elif
|
|
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: {
|
|
171
|
-
|
|
172
|
-
# ---
|
|
173
|
-
while
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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 =
|
|
194
|
-
elif
|
|
225
|
+
reward = os_state.returns()[player_id]
|
|
226
|
+
elif os_state.current_player() == player_id:
|
|
195
227
|
status = "ACTIVE"
|
|
196
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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":
|
|
218
|
-
"legal_actions": legal_actions,
|
|
219
|
-
"
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 = '';
|
|
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
|
|
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[
|
|
266
|
-
currentStepData[
|
|
267
|
-
typeof currentStepData[
|
|
268
|
-
obsString = currentStepData[
|
|
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][
|
|
272
|
-
environment.steps[0][
|
|
273
|
-
typeof environment.steps[0][
|
|
274
|
-
obsString = environment.steps[0][
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
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
|
-
|
|
359
|
+
AGENT_REGISTRY = {
|
|
335
360
|
"random": random_agent,
|
|
336
361
|
}
|
|
337
362
|
|
|
338
363
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
) -> dict[str, Any]:
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
359
|
-
env_name
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
406
|
-
|
|
433
|
+
_log.debug(e)
|
|
434
|
+
skipped_games.append(game_string)
|
|
407
435
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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)
|