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.
- kaggle_environments/__init__.py +2 -2
- kaggle_environments/core.py +1 -1
- kaggle_environments/envs/open_spiel/games/chess/chess.js +294 -0
- kaggle_environments/envs/open_spiel/games/connect_four/connect_four.js +2 -14
- 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 +319 -260
- kaggle_environments/envs/open_spiel/test_open_spiel.py +22 -7
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.6.dist-info}/METADATA +2 -2
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.6.dist-info}/RECORD +23 -11
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.6.dist-info}/WHEEL +0 -0
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.6.dist-info}/entry_points.txt +0 -0
- {kaggle_environments-1.17.3.dist-info → kaggle_environments-1.17.6.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
"actTimeout": DEFAULT_ACT_TIMEOUT,
|
|
29
|
-
"runTimeout": DEFAULT_RUN_TIMEOUT,
|
|
76
|
+
OBSERVATION_SPEC_TEMPLATE = {
|
|
77
|
+
"properties": {
|
|
30
78
|
"openSpielGameString": {
|
|
31
|
-
"description": "
|
|
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": "
|
|
37
|
-
"type": "string"
|
|
38
|
-
"default": "PLACEHOLDER_GAME_SHORT_NAME"
|
|
83
|
+
"description": "Short name of the OpenSpiel game.",
|
|
84
|
+
"type": "string"
|
|
39
85
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
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
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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[
|
|
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 ==
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
142
|
-
|
|
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
|
-
# ---
|
|
145
|
-
|
|
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 <=
|
|
150
|
-
if kaggle_state[
|
|
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[
|
|
154
|
-
|
|
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
|
-
|
|
207
|
+
os_state.apply_action(action_submitted)
|
|
158
208
|
action_applied = action_submitted
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
elif
|
|
220
|
+
env.info["moveDurations"].append(None)
|
|
221
|
+
elif acting_agent == pyspiel.PlayerId.SIMULTANEOUS:
|
|
164
222
|
raise NotImplementedError
|
|
165
|
-
elif
|
|
223
|
+
elif acting_agent == pyspiel.PlayerId.TERMINAL:
|
|
166
224
|
pass
|
|
167
|
-
elif
|
|
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: {
|
|
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 = ""
|
|
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
|
-
|
|
189
|
-
|
|
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 =
|
|
194
|
-
elif
|
|
245
|
+
reward = os_state.returns()[player_id]
|
|
246
|
+
elif os_state.current_player() == player_id:
|
|
195
247
|
status = "ACTIVE"
|
|
196
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
info_dict["
|
|
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
|
-
"
|
|
218
|
-
"
|
|
219
|
-
"
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 = '';
|
|
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
|
|
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[
|
|
266
|
-
currentStepData[
|
|
267
|
-
typeof currentStepData[
|
|
268
|
-
obsString = currentStepData[
|
|
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][
|
|
272
|
-
environment.steps[0][
|
|
273
|
-
typeof environment.steps[0][
|
|
274
|
-
obsString = environment.steps[0][
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
316
|
-
|
|
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("
|
|
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
|
-
|
|
379
|
+
AGENT_REGISTRY = {
|
|
335
380
|
"random": random_agent,
|
|
336
381
|
}
|
|
337
382
|
|
|
338
383
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
) -> dict[str, Any]:
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
453
|
+
_log.debug(e)
|
|
454
|
+
skipped_games.append(game_string)
|
|
407
455
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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)
|