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.
- kaggle_environments/__init__.py +2 -2
- kaggle_environments/envs/open_spiel/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/chess/chess.js +294 -0
- kaggle_environments/envs/open_spiel/games/connect_four/__init__.py +0 -0
- kaggle_environments/envs/open_spiel/games/connect_four/connect_four.js +296 -0
- kaggle_environments/envs/open_spiel/games/connect_four/connect_four_proxy.py +86 -0
- kaggle_environments/envs/open_spiel/games/connect_four/connect_four_proxy_test.py +57 -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/observation.py +133 -0
- kaggle_environments/envs/open_spiel/open_spiel.py +325 -224
- kaggle_environments/envs/open_spiel/proxy.py +139 -0
- kaggle_environments/envs/open_spiel/proxy_test.py +64 -0
- kaggle_environments/envs/open_spiel/test_open_spiel.py +23 -8
- {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/METADATA +2 -2
- {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/RECORD +30 -9
- {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/WHEEL +0 -0
- {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/entry_points.txt +0 -0
- {kaggle_environments-1.17.2.dist-info → kaggle_environments-1.17.5.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
"actTimeout": DEFAULT_ACT_TIMEOUT,
|
|
27
|
-
"runTimeout": DEFAULT_RUN_TIMEOUT,
|
|
70
|
+
OBSERVATION_SPEC_TEMPLATE = {
|
|
71
|
+
"properties": {
|
|
28
72
|
"openSpielGameString": {
|
|
29
|
-
"description": "
|
|
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": "
|
|
35
|
-
"type": "string"
|
|
36
|
-
"default": "PLACEHOLDER_GAME_SHORT_NAME"
|
|
77
|
+
"description": "Short name of the OpenSpiel game.",
|
|
78
|
+
"type": "string"
|
|
37
79
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
|
|
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[
|
|
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
|
-
#
|
|
137
|
-
# TODO(jhtschultz): test this behavior.
|
|
173
|
+
# TODO(jhtschultz): Test reset behavior.
|
|
138
174
|
is_initial_step = len(env.steps) == 1
|
|
139
|
-
if
|
|
140
|
-
|
|
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
|
-
# ---
|
|
143
|
-
|
|
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 <=
|
|
148
|
-
if kaggle_state[
|
|
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[
|
|
152
|
-
|
|
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
|
-
|
|
192
|
+
os_state.apply_action(action_submitted)
|
|
156
193
|
action_applied = action_submitted
|
|
157
|
-
|
|
158
|
-
|
|
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[
|
|
161
|
-
elif
|
|
200
|
+
kaggle_state[acting_agent].status = "INVALID"
|
|
201
|
+
elif acting_agent == pyspiel.PlayerId.SIMULTANEOUS:
|
|
162
202
|
raise NotImplementedError
|
|
163
|
-
elif
|
|
203
|
+
elif acting_agent == pyspiel.PlayerId.TERMINAL:
|
|
164
204
|
pass
|
|
165
|
-
elif
|
|
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: {
|
|
169
|
-
|
|
170
|
-
# ---
|
|
171
|
-
while
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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 =
|
|
192
|
-
elif
|
|
225
|
+
reward = os_state.returns()[player_id]
|
|
226
|
+
elif os_state.current_player() == player_id:
|
|
193
227
|
status = "ACTIVE"
|
|
194
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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":
|
|
216
|
-
"legal_actions": legal_actions,
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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 = '';
|
|
280
|
+
parent.innerHTML = ''; // Clear previous rendering
|
|
246
281
|
|
|
247
|
-
// Get the current step's data
|
|
248
282
|
const currentStepData = environment.steps[step];
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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";
|
|
264
|
-
pre.style.margin = "10px";
|
|
309
|
+
pre.style.fontFamily = "monospace";
|
|
310
|
+
pre.style.margin = "10px";
|
|
265
311
|
pre.style.border = "1px solid #ccc";
|
|
266
|
-
pre.style.padding = "
|
|
267
|
-
pre.style.backgroundColor = "#
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
359
|
+
AGENT_REGISTRY = {
|
|
292
360
|
"random": random_agent,
|
|
293
361
|
}
|
|
294
362
|
|
|
295
363
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
) -> dict[str, Any]:
|
|
299
|
-
|
|
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
|
-
|
|
303
|
-
games_list = pyspiel.registered_names()
|
|
304
|
-
for short_name in games_list:
|
|
423
|
+
for game_string in games_list:
|
|
305
424
|
try:
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
314
|
-
env_name
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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)
|