Skip to content

Commit 43a86ee

Browse files
authored
Various OpenSpiel wrapper improvements and multiple game UIs. (#348)
* Update OpenSpiel to v1.6.0 * Add example HTML playthrough generator. * Refactor open_spiel.py wrapper. - Moves Open Spiel game and state objects from global variables to env instance variables. - Stores complete state and action history in env.info. - Cleaner logic for registering envs. - Open Spiel proxies are loaded by default if they exist and are accessed without the {game_name}_proxy suffix. - Adds logging. - Removes remaining references to GameMaster agent, particularly in default HTML renderer. * Add proxies and UIs for tic-tac-toe, Go, poker, and chess (UI only). Credit to chris-prichard for Go renderer (#347). * Fix Open Spiel env test.
1 parent d2c401b commit 43a86ee

File tree

16 files changed

+2303
-261
lines changed

16 files changed

+2303
-261
lines changed

kaggle_environments/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
try:
3333
env = import_module(f".envs.{name}.{name}", __name__)
3434
if name == "open_spiel":
35-
for env_name, env_dict in env.registered_open_spiel_envs.items():
35+
for env_name, env_dict in env.ENV_REGISTRY.items():
3636
register(env_name, {
3737
"agents": env_dict.get("agents"),
3838
"html_renderer": env_dict.get("html_renderer"),
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
function renderer(options) {
2+
const { environment, step, parent, interactive, isInteractive } = options;
3+
4+
// Chess-specific constants
5+
const DEFAULT_NUM_ROWS = 8;
6+
const DEFAULT_NUM_COLS = 8;
7+
const PIECE_SVG_URLS = {
8+
'p': 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg', // Black Pawn
9+
'r': 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg', // Black Rook
10+
'n': 'https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg', // Black Knight
11+
'b': 'https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg', // Black Bishop
12+
'q': 'https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg', // Black Queen
13+
'k': 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg', // Black King
14+
'P': 'https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg', // White Pawn
15+
'R': 'https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg', // White Rook
16+
'N': 'https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg', // White Knight
17+
'B': 'https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg', // White Bishop
18+
'Q': 'https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg', // White Queen
19+
'K': 'https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg' // White King
20+
};
21+
const LIGHT_SQUARE_COLOR = '#f0d9b5';
22+
const DARK_SQUARE_COLOR = '#b58863';
23+
24+
// Renderer state variables
25+
let currentBoardElement = null;
26+
let currentStatusTextElement = null;
27+
let currentWinnerTextElement = null;
28+
let currentMessageBoxElement = typeof document !== 'undefined' ? document.getElementById('messageBox') : null;
29+
let currentRendererContainer = null;
30+
let currentTitleElement = null;
31+
32+
function _showMessage(message, type = 'info', duration = 3000) {
33+
if (typeof document === 'undefined' || !document.body) return;
34+
if (!currentMessageBoxElement) {
35+
currentMessageBoxElement = document.createElement('div');
36+
currentMessageBoxElement.id = 'messageBox';
37+
// Identical styling to the Connect Four renderer's message box
38+
Object.assign(currentMessageBoxElement.style, {
39+
position: 'fixed',
40+
top: '10px',
41+
left: '50%',
42+
transform: 'translateX(-50%)',
43+
padding: '0.75rem 1rem',
44+
borderRadius: '0.375rem',
45+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
46+
zIndex: '1000',
47+
opacity: '0',
48+
transition: 'opacity 0.3s ease-in-out, background-color 0.3s',
49+
fontSize: '0.875rem',
50+
fontFamily: "'Inter', sans-serif",
51+
color: 'white'
52+
});
53+
document.body.appendChild(currentMessageBoxElement);
54+
}
55+
currentMessageBoxElement.textContent = message;
56+
currentMessageBoxElement.style.backgroundColor = type === 'error' ? '#ef4444' : '#10b981';
57+
currentMessageBoxElement.style.opacity = '1';
58+
setTimeout(() => { if (currentMessageBoxElement) currentMessageBoxElement.style.opacity = '0'; }, duration);
59+
}
60+
61+
function _ensureRendererElements(parentElementToClear, rows, cols) {
62+
if (!parentElementToClear) return false;
63+
parentElementToClear.innerHTML = '';
64+
65+
currentRendererContainer = document.createElement('div');
66+
Object.assign(currentRendererContainer.style, {
67+
display: 'flex',
68+
flexDirection: 'column',
69+
alignItems: 'center',
70+
padding: '20px',
71+
boxSizing: 'border-box',
72+
width: '100%',
73+
height: '100%',
74+
fontFamily: "'Inter', sans-serif"
75+
});
76+
77+
currentTitleElement = document.createElement('h1');
78+
currentTitleElement.textContent = 'Chess';
79+
// Identical styling to the Connect Four renderer's title
80+
Object.assign(currentTitleElement.style, {
81+
fontSize: '1.875rem',
82+
fontWeight: 'bold',
83+
marginBottom: '1rem',
84+
textAlign: 'center',
85+
color: '#2563eb'
86+
});
87+
currentRendererContainer.appendChild(currentTitleElement);
88+
89+
currentBoardElement = document.createElement('div');
90+
Object.assign(currentBoardElement.style, {
91+
display: 'grid',
92+
gridTemplateColumns: `repeat(${cols}, 50px)`,
93+
gridTemplateRows: `repeat(${rows}, 50px)`,
94+
width: `${cols * 50}px`,
95+
height: `${rows * 50}px`,
96+
border: '2px solid #333'
97+
});
98+
99+
for (let r = 0; r < rows; r++) {
100+
for (let c = 0; c < cols; c++) {
101+
const square = document.createElement('div');
102+
square.id = `cell-${r}-${c}`;
103+
Object.assign(square.style, {
104+
width: '50px',
105+
height: '50px',
106+
backgroundColor: (r + c) % 2 === 0 ? LIGHT_SQUARE_COLOR : DARK_SQUARE_COLOR,
107+
display: 'flex',
108+
alignItems: 'center',
109+
justifyContent: 'center',
110+
});
111+
currentBoardElement.appendChild(square);
112+
}
113+
}
114+
currentRendererContainer.appendChild(currentBoardElement);
115+
116+
const statusContainer = document.createElement('div');
117+
// Identical styling to the Connect Four renderer's status container
118+
Object.assign(statusContainer.style, {
119+
padding: '10px 15px',
120+
backgroundColor: 'white',
121+
borderRadius: '8px',
122+
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06)',
123+
textAlign: 'center',
124+
width: 'auto',
125+
minWidth: '200px',
126+
maxWidth: '90vw',
127+
marginTop: '20px'
128+
});
129+
currentRendererContainer.appendChild(statusContainer);
130+
131+
currentStatusTextElement = document.createElement('p');
132+
Object.assign(currentStatusTextElement.style, {
133+
fontSize: '1.1rem',
134+
fontWeight: '600',
135+
margin: '0 0 5px 0'
136+
});
137+
statusContainer.appendChild(currentStatusTextElement);
138+
139+
currentWinnerTextElement = document.createElement('p');
140+
Object.assign(currentWinnerTextElement.style, {
141+
fontSize: '1.25rem',
142+
fontWeight: '700',
143+
margin: '5px 0 0 0'
144+
});
145+
statusContainer.appendChild(currentWinnerTextElement);
146+
147+
parentElementToClear.appendChild(currentRendererContainer);
148+
149+
if (typeof document !== 'undefined' && !document.body.hasAttribute('data-renderer-initialized')) {
150+
document.body.setAttribute('data-renderer-initialized', 'true');
151+
}
152+
return true;
153+
}
154+
155+
function _parseFen(fen) {
156+
if (!fen || typeof fen !== 'string') return null;
157+
158+
const [piecePlacement, activeColor, castling, enPassant, halfmoveClock, fullmoveNumber] = fen.split(' ');
159+
const board = [];
160+
const rows = piecePlacement.split('/');
161+
162+
for (const row of rows) {
163+
const boardRow = [];
164+
for (const char of row) {
165+
if (isNaN(parseInt(char))) {
166+
boardRow.push(char);
167+
} else {
168+
for (let i = 0; i < parseInt(char); i++) {
169+
boardRow.push(null);
170+
}
171+
}
172+
}
173+
board.push(boardRow);
174+
}
175+
176+
return {
177+
board,
178+
activeColor,
179+
castling,
180+
enPassant,
181+
halfmoveClock,
182+
fullmoveNumber
183+
};
184+
}
185+
186+
187+
function _renderBoardDisplay(gameStateToDisplay, displayRows, displayCols) {
188+
if (!currentBoardElement || !currentStatusTextElement || !currentWinnerTextElement) return;
189+
190+
// Clear board
191+
for (let r = 0; r < displayRows; r++) {
192+
for (let c = 0; c < displayCols; c++) {
193+
const squareElement = currentBoardElement.querySelector(`#cell-${r}-${c}`);
194+
if (squareElement) {
195+
squareElement.innerHTML = '';
196+
}
197+
}
198+
}
199+
200+
if (!gameStateToDisplay || !gameStateToDisplay.board) {
201+
currentStatusTextElement.textContent = "Waiting for game data...";
202+
currentWinnerTextElement.textContent = "";
203+
return;
204+
}
205+
206+
207+
const { board, activeColor, is_terminal, winner } = gameStateToDisplay;
208+
209+
for (let r_data = 0; r_data < displayRows; r_data++) {
210+
for (let c_data = 0; c_data < displayCols; c_data++) {
211+
const piece = board[r_data][c_data];
212+
const squareElement = currentBoardElement.querySelector(`#cell-${r_data}-${c_data}`);
213+
if (squareElement && piece) {
214+
const pieceImg = document.createElement('img');
215+
pieceImg.src = PIECE_SVG_URLS[piece];
216+
pieceImg.style.width = '45px';
217+
pieceImg.style.height = '45px';
218+
squareElement.appendChild(pieceImg);
219+
}
220+
}
221+
}
222+
223+
currentStatusTextElement.innerHTML = '';
224+
currentWinnerTextElement.innerHTML = '';
225+
if (is_terminal) {
226+
currentStatusTextElement.textContent = "Game Over!";
227+
if (winner) {
228+
if (String(winner).toLowerCase() === 'draw') {
229+
currentWinnerTextElement.textContent = "It's a Draw!";
230+
} else {
231+
const winnerColor = String(winner).toLowerCase() === 'white' ? 'White' : 'Black';
232+
currentWinnerTextElement.innerHTML = `Winner: <span style="font-weight: bold;">${winnerColor}</span>`;
233+
}
234+
} else {
235+
currentWinnerTextElement.textContent = "Game ended.";
236+
}
237+
} else {
238+
const playerColor = String(activeColor).toLowerCase() === 'w' ? 'White' : 'Black';
239+
currentStatusTextElement.innerHTML = `Current Player: <span style="font-weight: bold;">${playerColor}</span>`;
240+
}
241+
}
242+
243+
// --- Main execution logic ---
244+
if (!_ensureRendererElements(parent, DEFAULT_NUM_ROWS, DEFAULT_NUM_COLS)) {
245+
if (parent && typeof parent.innerHTML !== 'undefined') {
246+
parent.innerHTML = "<p style='color:red; font-family: sans-serif;'>Critical Error: Renderer element setup failed.</p>";
247+
}
248+
return;
249+
}
250+
251+
if (!environment || !environment.steps || !environment.steps[step]) {
252+
_renderBoardDisplay(null, DEFAULT_NUM_ROWS, DEFAULT_NUM_COLS);
253+
if (currentStatusTextElement) currentStatusTextElement.textContent = "Initializing environment...";
254+
return;
255+
}
256+
257+
const currentStepAgents = environment.steps[step];
258+
if (!currentStepAgents || !Array.isArray(currentStepAgents) || currentStepAgents.length === 0) {
259+
_renderBoardDisplay(null, DEFAULT_NUM_ROWS, DEFAULT_NUM_COLS);
260+
if (currentStatusTextElement) currentStatusTextElement.textContent = "Waiting for agent data...";
261+
return;
262+
}
263+
264+
// In chess, observation is the same for both agents. We can take it from the first.
265+
const agent = currentStepAgents[0];
266+
267+
if (!agent || typeof agent.observation === 'undefined') {
268+
_renderBoardDisplay(null, DEFAULT_NUM_ROWS, DEFAULT_NUM_COLS);
269+
if (currentStatusTextElement) currentStatusTextElement.textContent = "Waiting for observation data...";
270+
return;
271+
}
272+
const observationForRenderer = agent.observation;
273+
274+
let gameSpecificState = null;
275+
276+
if (observationForRenderer && typeof observationForRenderer.observation_string === 'string' && observationForRenderer.observation_string.trim() !== '') {
277+
try {
278+
const fen = observationForRenderer.observation_string;
279+
const parsedFen = _parseFen(fen);
280+
if (parsedFen) {
281+
// Assuming `is_terminal` and `winner` are provided in the top-level observation
282+
gameSpecificState = {
283+
...parsedFen,
284+
is_terminal: observationForRenderer.is_terminal,
285+
winner: observationForRenderer.winner
286+
};
287+
}
288+
} catch (e) {
289+
_showMessage("Error: Corrupted game state (obs_string).", 'error');
290+
}
291+
}
292+
293+
_renderBoardDisplay(gameSpecificState, DEFAULT_NUM_ROWS, DEFAULT_NUM_COLS);
294+
}

kaggle_environments/envs/open_spiel/games/go/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)