BenTouss commited on
Commit
7fa26a1
ยท
verified ยท
1 Parent(s): f49862c

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # =============================================================================
8
+ # OpenSpiel Environment Dockerfile
9
+ # =============================================================================
10
+ #
11
+ # Uses a pre-built OpenSpiel base image to avoid long build times (~30-60 min).
12
+ # The base image contains compiled OpenSpiel (C++ and Python bindings).
13
+ #
14
+ # DEFAULT (recommended for HuggingFace Spaces):
15
+ # Uses pre-built image from GHCR - no C++ compilation needed
16
+ #
17
+ # BUILD YOUR OWN BASE IMAGE (if you need custom OpenSpiel configuration):
18
+ # 1. Build the base image first (takes ~30-60 min):
19
+ # docker build -t openspiel-base:latest -f server/Dockerfile.openspiel-base .
20
+ # 2. Then build with your local base image:
21
+ # docker build --build-arg OPENSPIEL_BASE_IMAGE=openspiel-base:latest -t openspiel-env .
22
+ #
23
+ # =============================================================================
24
+
25
+ # Default: use pre-built image from GHCR (skips C++ compilation)
26
+ ARG OPENSPIEL_BASE_IMAGE=ghcr.io/meta-pytorch/openenv-openspiel-base:sha-e622c7e
27
+ FROM ${OPENSPIEL_BASE_IMAGE}
28
+
29
+ WORKDIR /app
30
+
31
+ # Install git (needed for pip install from git repos in pyproject.toml)
32
+ RUN apt-get update && apt-get install -y --no-install-recommends git \
33
+ && rm -rf /var/lib/apt/lists/*
34
+
35
+ # Copy environment code (context is the environment directory)
36
+ COPY . /app/env
37
+
38
+ # Install Python dependencies from pyproject.toml
39
+ WORKDIR /app/env
40
+ RUN pip3 install --no-cache-dir .
41
+
42
+ WORKDIR /app
43
+
44
+ # Copy README for web interface documentation
45
+ COPY README.md /app/README.md
46
+
47
+ # Python path configuration
48
+ # - /repo and /repo/build/python: OpenSpiel paths from base image
49
+ # - /app/env: Environment code
50
+ ENV PYTHONPATH=/repo:/repo/build/python:/app/env
51
+
52
+ # OpenSpiel-specific environment variables (can be overridden at runtime)
53
+ ENV OPENSPIEL_GAME=catch
54
+ ENV OPENSPIEL_AGENT_PLAYER=0
55
+ ENV OPENSPIEL_OPPONENT_POLICY=random
56
+
57
+ # Max simultaneous WebSocket sessions. Read by server/app.py (this fork only โ€”
58
+ # upstream ignores it and caps the server at 1). Keep >= the trainer's
59
+ # generation_batch_size, since GRPO opens one session per rollout.
60
+ ENV MAX_CONCURRENT_ENVS=64
61
+
62
+ # Uvicorn configuration. Keep WORKERS=1: the session registry that enforces
63
+ # MAX_CONCURRENT_ENVS lives in-process, so multiple workers would each track
64
+ # their own sessions independently.
65
+ ENV WORKERS=1
66
+
67
+ # Health check
68
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=120s --retries=3 \
69
+ CMD curl -f http://localhost:8000/health || exit 1
70
+
71
+ EXPOSE 8000
72
+
73
+ # Run the FastAPI server
74
+ ENV ENABLE_WEB_INTERFACE=true
75
+ CMD uvicorn server.app:app --host 0.0.0.0 --port 8000 --timeout-keep-alive 120 --workers ${WORKERS}
README.md CHANGED
@@ -1,10 +1,404 @@
1
  ---
2
- title: Openspiel Env
3
- emoji: ๐Ÿ‘
4
- colorFrom: purple
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: OpenSpiel Environment Server
3
+ emoji: ๐ŸŽฎ
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # OpenSpiel Environment
15
+
16
+ Integration of OpenSpiel games with the OpenEnv framework. [OpenSpiel](https://github.com/google-deepmind/open_spiel) is DeepMind's collection of 70+ game environments for RL research.
17
+
18
+ ## Fork notes
19
+
20
+ This is a fork of the upstream [`openenv/openspiel_env`](https://huggingface.co/spaces/openenv/openspiel_env)
21
+ Space (pinned at commit `87d5923`), maintained in the
22
+ [`openenv-tests`](https://github.com/) repo under `openenvs/openspiel_env/`. It differs
23
+ from upstream in exactly one way: **the server honours `MAX_CONCURRENT_ENVS`.**
24
+
25
+ Upstream calls `create_app()` without `max_concurrent_envs`, so the server falls back to
26
+ a single session and rejects every additional connection โ€” with a close code of `1000`,
27
+ which looks like a clean shutdown rather than an error. GRPO opens one session per
28
+ rollout, so training against the upstream image fails at `environment.reset()`. This fork
29
+ reads `MAX_CONCURRENT_ENVS` in `server/app.py` and sets
30
+ `SUPPORTS_CONCURRENT_SESSIONS = True` on `OpenSpielEnvironment`, which is safe because
31
+ every session already gets its own environment instance from the factory.
32
+
33
+ Everything else is upstream code, kept verbatim so the fork stays easy to rebase.
34
+
35
+ ## Supported Games
36
+
37
+ This environment supports 6 games across different categories:
38
+
39
+ ### Single-Player Games (No Opponent)
40
+ 1. **Catch** - Move horizontally to catch a falling ball
41
+ 2. **Cliff Walking** - Navigate grid without falling off cliff (Sutton & Barto benchmark)
42
+ 3. **2048** - Classic tile-merging puzzle game
43
+ 4. **Blackjack** - Simplified blackjack (HIT/STAND only)
44
+
45
+ ### Multi-Player Games (with Bot Opponent)
46
+ 5. **Tic-Tac-Toe** - Classic 3x3 game
47
+ 6. **Kuhn Poker** - 2-player simplified poker (game theory benchmark)
48
+
49
+ ## Quick Start
50
+
51
+ The simplest way to use the OpenSpiel environment is through the `OpenSpielEnv` class:
52
+
53
+ ```python
54
+ from openspiel_env import OpenSpielEnv, OpenSpielAction
55
+
56
+ try:
57
+ # Create environment from Docker image
58
+ env = OpenSpielEnv.from_docker_image("openspiel-env:latest")
59
+
60
+ # Reset to start a new episode
61
+ result = env.reset()
62
+ print(f"Initial state: {result.observation.info_state}")
63
+ print(f"Legal actions: {result.observation.legal_actions}")
64
+
65
+ # Play until done
66
+ while not result.done:
67
+ action_id = result.observation.legal_actions[0]
68
+ result = env.step(OpenSpielAction(action_id=action_id))
69
+ print(f"Reward: {result.reward}, Done: {result.done}")
70
+
71
+ finally:
72
+ # Always clean up
73
+ env.close()
74
+ ```
75
+
76
+ That's it! The `OpenSpielEnv.from_docker_image()` method handles:
77
+ - Starting the Docker container
78
+ - Waiting for the server to be ready
79
+ - Connecting to the environment
80
+ - Container cleanup when you call `close()`
81
+
82
+ ## Building the Docker Image
83
+
84
+ OpenSpiel requires compilation from C++ source. The Docker build uses a **pre-built base image** by default to avoid long build times.
85
+
86
+ ### Default Build (Recommended)
87
+
88
+ From the **environment directory** (`envs/openspiel_env/`):
89
+
90
+ ```bash
91
+ # Uses pre-built base image from GHCR (fast, ~1-2 min)
92
+ docker build -t openspiel-env:latest -f server/Dockerfile .
93
+ ```
94
+
95
+ This uses the pre-built `ghcr.io/meta-pytorch/openenv-openspiel-base` image which already contains compiled OpenSpiel.
96
+
97
+ ### Building Your Own Base Image (Optional)
98
+
99
+ If you need to customize OpenSpiel or can't access the pre-built image:
100
+
101
+ ```bash
102
+ # Step 1: Build the base image (compiles OpenSpiel, ~30-60 min)
103
+ docker build -t openspiel-base:latest -f server/Dockerfile.openspiel-base .
104
+
105
+ # Step 2: Build the environment using your local base image
106
+ docker build -t openspiel-env:latest \
107
+ --build-arg OPENSPIEL_BASE_IMAGE=openspiel-base:latest \
108
+ -f server/Dockerfile .
109
+ ```
110
+
111
+ ## Deploying to Hugging Face Spaces
112
+
113
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
114
+
115
+ ```bash
116
+ # From the environment directory (envs/openspiel_env/)
117
+ openenv push
118
+
119
+ # Or specify options
120
+ openenv push --namespace my-org --private
121
+ ```
122
+
123
+ The `openenv push` command will:
124
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
125
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
126
+ 3. Upload to Hugging Face (ensuring you're logged in)
127
+
128
+ ### Prerequisites
129
+
130
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
131
+
132
+ ### Options
133
+
134
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
135
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
136
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
137
+ - `--private`: Deploy the space as private (default: public)
138
+
139
+ ### Examples
140
+
141
+ ```bash
142
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
143
+ openenv push
144
+
145
+ # Push to a specific repository
146
+ openenv push --repo-id my-org/openspiel-env
147
+
148
+ # Push as a private space
149
+ openenv push --private
150
+
151
+ # Combine options
152
+ openenv push --repo-id my-org/openspiel-env --private
153
+ ```
154
+
155
+ After deployment, your space will be available at:
156
+ `https://huggingface.co/spaces/<repo-id>`
157
+
158
+ The deployed space includes:
159
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
160
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
161
+ - **Health Check** at `/health` - Container health monitoring
162
+
163
+ > **Note**: The default Dockerfile uses a pre-built base image with OpenSpiel already compiled, so deployment is fast and works with standard CPU hardware. If you build your own base image, compilation requires more resources and time.
164
+
165
+ ## Running Specific Games
166
+
167
+ ```bash
168
+ # Catch (default)
169
+ docker run -p 8000:8000 openspiel-env:latest
170
+
171
+ # Tic-Tac-Toe with random opponent
172
+ docker run -p 8000:8000 -e OPENSPIEL_GAME=tic_tac_toe openspiel-env:latest
173
+
174
+ # Kuhn Poker
175
+ docker run -p 8000:8000 -e OPENSPIEL_GAME=kuhn_poker openspiel-env:latest
176
+
177
+ # 2048
178
+ docker run -p 8000:8000 -e OPENSPIEL_GAME=2048 openspiel-env:latest
179
+
180
+ # Blackjack
181
+ docker run -p 8000:8000 -e OPENSPIEL_GAME=blackjack openspiel-env:latest
182
+
183
+ # Cliff Walking
184
+ docker run -p 8000:8000 -e OPENSPIEL_GAME=cliff_walking openspiel-env:latest
185
+ ```
186
+
187
+ ## Environment Details
188
+
189
+ ### Action
190
+ **OpenSpielAction**: Contains the action to take
191
+ - `action_id` (int) - Action ID to execute
192
+ - `game_name` (str) - Game name (default: "catch")
193
+ - `game_params` (Dict) - Optional game parameters
194
+
195
+ ### Observation
196
+ **OpenSpielObservation**: Contains the game state
197
+ - `info_state` (List[float]) - Agent's information state vector
198
+ - `legal_actions` (List[int]) - Legal action IDs
199
+ - `game_phase` (str) - "initial", "playing", or "terminal"
200
+ - `current_player_id` (int) - Current player (-1 for simultaneous)
201
+ - `opponent_last_action` (Optional[int]) - Last opponent action
202
+ - `done` (bool) - Whether the episode has ended
203
+ - `reward` (Optional[float]) - Reward for the last action
204
+
205
+ ### State
206
+ **OpenSpielState**: Server-side state snapshot
207
+ - `episode_id` (str) - Unique identifier for the current episode
208
+ - `step_count` (int) - Number of steps taken
209
+ - `game_name` (str) - Game name
210
+ - `agent_player` (int) - Agent's player ID
211
+ - `opponent_policy` (str) - Opponent policy name
212
+ - `num_players` (int) - Total players
213
+
214
+ ## Configuration
215
+
216
+ ### Environment Variables
217
+
218
+ - `OPENSPIEL_GAME`: Game name (default: "catch")
219
+ - `OPENSPIEL_AGENT_PLAYER`: Player ID for agent (default: 0)
220
+ - `OPENSPIEL_OPPONENT_POLICY`: Opponent policy for multi-player games
221
+ - `random`: Uniform random (default)
222
+ - `first`: Always picks first legal action
223
+ - `last`: Always picks last legal action
224
+ - `MAX_CONCURRENT_ENVS`: Max simultaneous WebSocket sessions (default: 64). **Fork-only** โ€”
225
+ upstream ignores this and caps the server at 1. Set it at least as high as the trainer's
226
+ `generation_batch_size`; exceeding it closes the extra sessions with code `1000`.
227
+
228
+ Defaults for a deployed Space live in `variables:` in `openenv.yaml` and are re-applied on
229
+ every `openenv push`. Override per-push with `-e KEY=VALUE`.
230
+
231
+ ### Example: Tic-Tac-Toe with Fixed Opponent
232
+
233
+ ```bash
234
+ docker run -p 8000:8000 \
235
+ -e OPENSPIEL_GAME=tic_tac_toe \
236
+ -e OPENSPIEL_OPPONENT_POLICY=first \
237
+ openspiel-env:latest
238
+ ```
239
+
240
+ ## Advanced Usage
241
+
242
+ ### Connecting to an Existing Server
243
+
244
+ If you already have an OpenSpiel environment server running:
245
+
246
+ ```python
247
+ from openspiel_env import OpenSpielEnv, OpenSpielAction
248
+
249
+ # Connect to existing server
250
+ env = OpenSpielEnv(base_url="http://localhost:8000")
251
+
252
+ # Use as normal
253
+ result = env.reset()
254
+ result = env.step(OpenSpielAction(action_id=result.observation.legal_actions[0]))
255
+
256
+ # Close connection (does NOT stop the server)
257
+ env.close()
258
+ ```
259
+
260
+ ### Connecting to HuggingFace Space
261
+
262
+ ```python
263
+ from openspiel_env import OpenSpielEnv, OpenSpielAction
264
+
265
+ # Connect to remote Space
266
+ env = OpenSpielEnv(base_url="https://your-username-openspiel.hf.space")
267
+
268
+ result = env.reset()
269
+ print(f"Game: {result.observation.game_phase}")
270
+ print(f"Legal actions: {result.observation.legal_actions}")
271
+
272
+ result = env.step(OpenSpielAction(action_id=result.observation.legal_actions[0]))
273
+ env.close()
274
+ ```
275
+
276
+ ## Game-Specific Information
277
+
278
+ ### 1. Catch
279
+ - **Type**: Single-player
280
+ - **Action Space**: 3 actions (left, stay, right)
281
+ - **Observation**: 5x5 grid flattened (25 dimensions)
282
+ - **Reward**: +1 for catching ball, 0 otherwise
283
+ - **Episode Length**: ~10 steps
284
+
285
+ ### 2. Tic-Tac-Toe
286
+ - **Type**: 2-player turn-based, perfect information
287
+ - **Players**: Agent (X) vs Random Bot (O)
288
+ - **Action Space**: 9 positions
289
+ - **Observation**: 27 dimensions (3x3 board + game state)
290
+ - **Reward**: +1 win, -1 loss, 0 draw/mid-game
291
+
292
+ ### 3. Kuhn Poker
293
+ - **Type**: 2-player turn-based, imperfect information
294
+ - **Players**: Agent vs Random Bot
295
+ - **Action Space**: 2 actions (pass/fold, bet/call)
296
+ - **Observation**: 6 dimensions (card + betting history)
297
+ - **Reward**: Pot winnings (typically -1, 0, +1, +2)
298
+ - **Notes**: THE benchmark for imperfect-information RL
299
+
300
+ ### 4. Cliff Walking
301
+ - **Type**: Single-player grid world
302
+ - **Action Space**: 4 actions (up, down, left, right)
303
+ - **Observation**: Position encoding
304
+ - **Reward**: -1 per step, -100 for falling off cliff
305
+ - **Notes**: Classic RL benchmark from Sutton & Barto
306
+
307
+ ### 5. 2048
308
+ - **Type**: Single-player puzzle
309
+ - **Action Space**: 4 actions (up, down, left, right)
310
+ - **Observation**: 4x4 grid with tile values
311
+ - **Reward**: Points from merging tiles
312
+ - **Notes**: Stochastic tile spawning
313
+
314
+ ### 6. Blackjack
315
+ - **Type**: Single-player vs dealer
316
+ - **Action Space**: 2 actions (HIT, STAND)
317
+ - **Observation**: Player hand + dealer's visible card
318
+ - **Reward**: +1 win, -1 loss, 0 draw
319
+ - **Notes**: Simplified version, no double/split
320
+
321
+ ## Development & Testing
322
+
323
+ ### Direct Environment Testing
324
+
325
+ Test the environment logic directly without starting the HTTP server (requires OpenSpiel installed locally):
326
+
327
+ ```python
328
+ from openspiel_env.server.openspiel_environment import OpenSpielEnvironment
329
+ from openspiel_env.models import OpenSpielAction
330
+
331
+ # Create environment directly
332
+ env = OpenSpielEnvironment(game_name="catch")
333
+
334
+ # Test reset
335
+ obs = env.reset()
336
+ print(f"Info state: {obs.info_state}")
337
+
338
+ # Test step
339
+ obs = env.step(OpenSpielAction(action_id=0))
340
+ print(f"Done: {obs.done}, Reward: {obs.reward}")
341
+ ```
342
+
343
+ ### Running Locally
344
+
345
+ Run the server locally for development (requires OpenSpiel installed):
346
+
347
+ ```bash
348
+ # From the environment directory
349
+ cd envs/openspiel_env
350
+
351
+ # Install dependencies
352
+ uv venv && source .venv/bin/activate
353
+ uv pip install -e .
354
+
355
+ # Start the server
356
+ python -m uvicorn server.app:app --reload
357
+ ```
358
+
359
+ Or using the CLI entry point:
360
+
361
+ ```bash
362
+ uv run --project . server --port 8000
363
+ ```
364
+
365
+ ### Automated Testing (All 6 Games)
366
+
367
+ ```bash
368
+ ./test_docker_all_games.sh
369
+ ```
370
+
371
+ This script will build and test all 6 supported games in Docker.
372
+
373
+ ## Project Structure
374
+
375
+ ```
376
+ openspiel_env/
377
+ โ”œโ”€โ”€ __init__.py # Module exports
378
+ โ”œโ”€โ”€ README.md # This file
379
+ โ”œโ”€โ”€ openenv.yaml # OpenEnv manifest
380
+ โ”œโ”€โ”€ pyproject.toml # Project metadata and dependencies
381
+ โ”œโ”€โ”€ client.py # OpenSpielEnv client implementation
382
+ โ”œโ”€โ”€ models.py # Action, Observation, and State models
383
+ โ”œโ”€โ”€ test_docker_all_games.sh # Automated test script
384
+ โ””โ”€โ”€ server/
385
+ โ”œโ”€โ”€ __init__.py # Server module exports
386
+ โ”œโ”€โ”€ openspiel_environment.py # Core OpenSpielEnvironment implementation
387
+ โ”œโ”€โ”€ opponent_policies.py # Opponent policies (random, fixed)
388
+ โ”œโ”€โ”€ app.py # FastAPI application
389
+ โ”œโ”€โ”€ Dockerfile # Environment container (uses pre-built base)
390
+ โ””โ”€โ”€ Dockerfile.openspiel-base # Base image with compiled OpenSpiel
391
+ ```
392
+
393
+ ## Limitations
394
+
395
+ - **Simultaneous-move games**: Only agent_player=0 supported
396
+ - **Multi-agent training**: Single agent only (no self-play yet)
397
+ - **Opponent policies**: Random and fixed only (no MCTS yet)
398
+ - **Build time**: Building your own base image takes ~30-60 min (compiles OpenSpiel C++). Using the pre-built image is fast (~1-2 min) and works with standard hardware.
399
+
400
+ ## References
401
+
402
+ - [OpenSpiel Paper (2019)](https://arxiv.org/abs/1908.09453)
403
+ - [OpenSpiel GitHub](https://github.com/google-deepmind/open_spiel)
404
+ - [OpenSpiel Documentation](https://openspiel.readthedocs.io/)
__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ OpenSpiel Environment Integration.
9
+
10
+ This module provides integration between OpenSpiel games and the OpenEnv framework.
11
+ OpenSpiel (https://github.com/google-deepmind/open_spiel) is DeepMind's collection
12
+ of environments and algorithms for research in RL in games.
13
+
14
+ Supported games:
15
+ - Catch (1P)
16
+ - Tic-Tac-Toe (2P)
17
+ - Kuhn Poker (2P, imperfect info)
18
+ - Cliff Walking (1P)
19
+ - 2048 (1P)
20
+ - Blackjack (1P)
21
+ """
22
+
23
+ from .client import OpenSpielEnv
24
+ from .models import OpenSpielAction, OpenSpielObservation, OpenSpielState
25
+
26
+ __all__ = ["OpenSpielEnv", "OpenSpielAction", "OpenSpielObservation", "OpenSpielState"]
client.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ OpenSpielEnv Client.
9
+
10
+ This module provides the client for connecting to an OpenSpiel Environment server
11
+ via WebSocket for persistent sessions.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any, Dict, Optional, TYPE_CHECKING
17
+
18
+ from openenv.core.client_types import StepResult
19
+ from openenv.core.env_client import EnvClient
20
+
21
+ from .models import OpenSpielAction, OpenSpielObservation, OpenSpielState
22
+
23
+ if TYPE_CHECKING:
24
+ from openenv.core.containers.runtime import ContainerProvider
25
+
26
+
27
+ class OpenSpielEnv(EnvClient[OpenSpielAction, OpenSpielObservation, OpenSpielState]):
28
+ """
29
+ Client for OpenSpiel Environment.
30
+
31
+ This client maintains a persistent WebSocket connection to the environment
32
+ server, enabling efficient multi-step interactions with lower latency.
33
+
34
+ Example:
35
+ >>> # Connect to a running server
36
+ >>> with OpenSpielEnv(base_url="http://localhost:8000") as client:
37
+ ... result = client.reset()
38
+ ... print(result.observation.info_state)
39
+ ...
40
+ ... result = client.step(OpenSpielAction(action_id=1, game_name="catch"))
41
+ ... print(result.observation.reward)
42
+
43
+ Example with Docker:
44
+ >>> # Automatically start container and connect
45
+ >>> client = OpenSpielEnv.from_docker_image("openspiel-env:latest")
46
+ >>> try:
47
+ ... result = client.reset()
48
+ ... result = client.step(OpenSpielAction(action_id=0))
49
+ ... finally:
50
+ ... client.close()
51
+ """
52
+
53
+ def _step_payload(self, action: OpenSpielAction) -> Dict[str, Any]:
54
+ """
55
+ Convert OpenSpielAction to JSON payload for step request.
56
+
57
+ Args:
58
+ action: OpenSpielAction instance.
59
+
60
+ Returns:
61
+ Dictionary representation suitable for JSON encoding.
62
+ """
63
+ return {
64
+ "action_id": action.action_id,
65
+ "game_name": action.game_name,
66
+ "game_params": action.game_params,
67
+ }
68
+
69
+ def _parse_result(
70
+ self, payload: Dict[str, Any]
71
+ ) -> StepResult[OpenSpielObservation]:
72
+ """
73
+ Parse server response into StepResult[OpenSpielObservation].
74
+
75
+ Args:
76
+ payload: JSON response from server.
77
+
78
+ Returns:
79
+ StepResult with OpenSpielObservation.
80
+ """
81
+ obs_data = payload.get("observation", {})
82
+
83
+ observation = OpenSpielObservation(
84
+ info_state=obs_data.get("info_state", []),
85
+ legal_actions=obs_data.get("legal_actions", []),
86
+ game_phase=obs_data.get("game_phase", "playing"),
87
+ current_player_id=obs_data.get("current_player_id", 0),
88
+ opponent_last_action=obs_data.get("opponent_last_action"),
89
+ done=payload.get("done", False),
90
+ reward=payload.get("reward"),
91
+ metadata=obs_data.get("metadata", {}),
92
+ )
93
+
94
+ return StepResult(
95
+ observation=observation,
96
+ reward=payload.get("reward"),
97
+ done=payload.get("done", False),
98
+ )
99
+
100
+ def _parse_state(self, payload: Dict[str, Any]) -> OpenSpielState:
101
+ """
102
+ Parse server response into OpenSpielState object.
103
+
104
+ Args:
105
+ payload: JSON response from /state endpoint.
106
+
107
+ Returns:
108
+ OpenSpielState object with environment state information.
109
+ """
110
+ return OpenSpielState(
111
+ episode_id=payload.get("episode_id"),
112
+ step_count=payload.get("step_count", 0),
113
+ game_name=payload.get("game_name", "unknown"),
114
+ agent_player=payload.get("agent_player", 0),
115
+ opponent_policy=payload.get("opponent_policy", "random"),
116
+ game_params=payload.get("game_params", {}),
117
+ num_players=payload.get("num_players", 1),
118
+ )
models.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for OpenSpiel Environment.
9
+
10
+ This module defines the Action, Observation, and State types for OpenSpiel games.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from openenv.core.env_server import Action, Observation, State
18
+ from pydantic import Field
19
+
20
+
21
+ class OpenSpielAction(Action):
22
+ """
23
+ Action for OpenSpiel environments.
24
+
25
+ Attributes:
26
+ action_id: The integer action ID to take (from legal_actions).
27
+ game_name: Name of the OpenSpiel game (e.g., "catch", "tic_tac_toe").
28
+ game_params: Optional game-specific parameters (e.g., {"rows": 8, "columns": 6}).
29
+ """
30
+
31
+ action_id: int
32
+ game_name: str = "catch"
33
+ game_params: Dict[str, Any] = Field(default_factory=dict)
34
+
35
+
36
+ class OpenSpielObservation(Observation):
37
+ """
38
+ Observation from OpenSpiel environment.
39
+
40
+ This represents what the agent sees after taking an action.
41
+ For single-player games, this is straightforward.
42
+ For multi-player games, this is from the perspective of the agent player.
43
+
44
+ Attributes:
45
+ info_state: Information state tensor (list of floats) for the agent.
46
+ This contains all information available to the agent.
47
+ legal_actions: List of legal action IDs the agent can take.
48
+ game_phase: String describing the current phase (e.g., "playing", "terminal").
49
+ current_player_id: ID of the current player (-1 for simultaneous, player ID otherwise).
50
+ opponent_last_action: Last action taken by opponent (if available, None otherwise).
51
+ """
52
+
53
+ info_state: List[float]
54
+ legal_actions: List[int]
55
+ game_phase: str = "playing"
56
+ current_player_id: int = 0
57
+ opponent_last_action: Optional[int] = None
58
+
59
+
60
+ class OpenSpielState(State):
61
+ """
62
+ State for OpenSpiel environment.
63
+
64
+ Attributes:
65
+ game_name: Name of the OpenSpiel game.
66
+ agent_player: Which player ID the agent controls (0 by default).
67
+ opponent_policy: Name of the opponent policy ("random", "fixed", etc.).
68
+ game_params: Game-specific parameters.
69
+ num_players: Total number of players in the game.
70
+ """
71
+
72
+ game_name: str = "catch"
73
+ agent_player: int = 0
74
+ opponent_policy: str = "random"
75
+ game_params: Dict[str, Any] = Field(default_factory=dict)
76
+ num_players: int = 1
openenv.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: openspiel_env
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
7
+
8
+ # Public Space variables, applied by `openenv push` on every deploy.
9
+ # Override at push time with -e KEY=VALUE (e.g. `openenv push -e OPENSPIEL_GAME=2048`).
10
+ # Secrets never belong here โ€” pass those as `openenv push --secret KEY=VALUE`.
11
+ variables:
12
+ OPENSPIEL_GAME: catch
13
+ OPENSPIEL_AGENT_PLAYER: "0"
14
+ OPENSPIEL_OPPONENT_POLICY: random
15
+ # Must be >= the trainer's generation_batch_size; GRPO opens one session per rollout.
16
+ MAX_CONCURRENT_ENVS: "64"
17
+ ENABLE_WEB_INTERFACE: "true"
pyproject.toml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-openspiel-env"
13
+ version = "0.1.0"
14
+ description = "OpenSpiel Environment for OpenEnv - integration with DeepMind's game research framework"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv dependencies (required for server functionality)
18
+ "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.3",
19
+ "fastapi>=0.115.0",
20
+ "pydantic>=2.0.0",
21
+ "uvicorn>=0.24.0",
22
+ "requests>=2.31.0",
23
+ # Note: OpenSpiel (pyspiel) is built from source in the Docker image
24
+ # and is not available as a pip package. The Docker build compiles it
25
+ # from https://github.com/google-deepmind/open_spiel
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0.0",
31
+ "pytest-cov>=4.0.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ # Server entry point
36
+ server = "openspiel_env.server.app:main"
37
+
38
+ [tool.setuptools]
39
+ include-package-data = true
40
+ packages = ["openspiel_env", "openspiel_env.server"]
41
+ package-dir = { "openspiel_env" = ".", "openspiel_env.server" = "server" }
server/Dockerfile.openspiel-base ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ # Pre-built OpenSpiel base image
8
+ # This image contains OpenSpiel compiled and ready to use
9
+ # Built from: docker build -t openspiel-base:latest -f envs/openspiel_env/server/Dockerfile.openspiel-base .
10
+ # In GitHub Actions, this is overridden to use the GHCR base image
11
+ ARG BASE_IMAGE=openenv-base:latest
12
+ FROM ${BASE_IMAGE}
13
+
14
+ # Avoid interactive prompts during build
15
+ ENV DEBIAN_FRONTEND=noninteractive
16
+ ENV TZ=UTC
17
+
18
+ # Install build dependencies (curl already installed by openenv-base)
19
+ RUN apt-get update && apt-get install -y --no-install-recommends \
20
+ build-essential \
21
+ clang \
22
+ cmake \
23
+ git \
24
+ sudo \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # Set up OpenSpiel build directory
28
+ RUN mkdir /repo
29
+ WORKDIR /repo
30
+
31
+ # Clone OpenSpiel
32
+ RUN git clone https://github.com/google-deepmind/open_spiel.git .
33
+
34
+ # Run OpenSpiel's installation script (downloads C++ dependencies)
35
+ RUN ./install.sh
36
+
37
+ # Install Python dependencies
38
+ # First upgrade pip and setuptools, then install other packages
39
+ RUN pip3 install --no-cache-dir --upgrade pip setuptools wheel
40
+ RUN pip3 install --no-cache-dir --upgrade pbr testresources importlib_metadata
41
+ RUN pip3 install --no-cache-dir --upgrade -r requirements.txt cmake
42
+
43
+ # Build OpenSpiel with Python 3.11
44
+ # Use the exact same Python executable as the base image
45
+ # Disable gin_rummy to speed up build (complex game, not needed for basic usage)
46
+ RUN mkdir -p build
47
+ WORKDIR /repo/build
48
+ RUN cmake -DPython3_EXECUTABLE=/usr/local/bin/python3 \
49
+ -DCMAKE_CXX_COMPILER=$(which clang++) \
50
+ -DOPEN_SPIEL_BUILD_WITH_GIN_RUMMY=OFF \
51
+ ../open_spiel
52
+ RUN make -j$(nproc) pyspiel
53
+
54
+ # Install OpenSpiel Python requirements
55
+ WORKDIR /repo
56
+ RUN pip3 install --no-cache-dir --upgrade -r requirements.txt
57
+
58
+ # Set Python path for OpenSpiel
59
+ ENV PYTHONPATH=/repo:/repo/build/python:${PYTHONPATH}
60
+
61
+ # Test OpenSpiel import to verify ABI compatibility
62
+ RUN python3 -c "import pyspiel; print('OpenSpiel import successful')" || echo "OpenSpiel import failed"
63
+
64
+ # Clean up build dependencies to reduce image size
65
+ RUN apt-get remove -y build-essential clang cmake git sudo || true && \
66
+ apt-get autoremove -y && \
67
+ apt-get clean && \
68
+ rm -rf /var/lib/apt/lists/*
69
+
70
+ # Set working directory back to /app (standard for openenv-base)
71
+ WORKDIR /app
server/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Server-side implementation for OpenSpiel environments."""
server/app.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the OpenSpiel Environment.
9
+
10
+ This module creates an HTTP server that exposes OpenSpiel games
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Usage:
14
+ # Development (with auto-reload):
15
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
16
+
17
+ # Production:
18
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
19
+
20
+ # Or run directly:
21
+ uv run --project . server
22
+
23
+ Environment variables:
24
+ OPENSPIEL_GAME: Game name to serve (default: "catch")
25
+ OPENSPIEL_AGENT_PLAYER: Agent player ID (default: 0)
26
+ OPENSPIEL_OPPONENT_POLICY: Opponent policy (default: "random")
27
+ MAX_CONCURRENT_ENVS: Max simultaneous WebSocket sessions (default: 64)
28
+
29
+ Local fork note (differs from upstream openenv/openspiel_env):
30
+ Upstream calls create_app() without max_concurrent_envs, so the server
31
+ defaults to 1 session and silently rejects the rest. GRPO opens one session
32
+ per rollout, so this fork reads MAX_CONCURRENT_ENVS and passes it through.
33
+ See openenvs/README.md for the rationale.
34
+ """
35
+
36
+ import os
37
+
38
+ # Support both in-repo and standalone imports
39
+ try:
40
+ # In-repo imports (when running from OpenEnv repository)
41
+ from openenv.core.env_server.http_server import create_app
42
+
43
+ from ..models import OpenSpielAction, OpenSpielObservation
44
+ from .openspiel_environment import OpenSpielEnvironment
45
+ except ImportError:
46
+ from models import OpenSpielAction, OpenSpielObservation
47
+
48
+ # Standalone imports (when environment is standalone with openenv from pip)
49
+ from openenv.core.env_server.http_server import create_app
50
+ from server.openspiel_environment import OpenSpielEnvironment
51
+
52
+ # Get game configuration from environment variables
53
+ game_name = os.getenv("OPENSPIEL_GAME", "catch")
54
+ agent_player = int(os.getenv("OPENSPIEL_AGENT_PLAYER", "0"))
55
+ opponent_policy = os.getenv("OPENSPIEL_OPPONENT_POLICY", "random")
56
+ max_concurrent_envs = int(os.getenv("MAX_CONCURRENT_ENVS", "64"))
57
+
58
+
59
+ # Factory function to create OpenSpielEnvironment instances
60
+ def create_openspiel_environment():
61
+ """Factory function that creates OpenSpielEnvironment with config."""
62
+ return OpenSpielEnvironment(
63
+ game_name=game_name,
64
+ agent_player=agent_player,
65
+ opponent_policy=opponent_policy,
66
+ )
67
+
68
+
69
+ # Create the FastAPI app with web interface and README integration
70
+ # Pass the factory function instead of an instance for WebSocket session support
71
+ app = create_app(
72
+ create_openspiel_environment,
73
+ OpenSpielAction,
74
+ OpenSpielObservation,
75
+ env_name="openspiel_env",
76
+ max_concurrent_envs=max_concurrent_envs,
77
+ )
78
+
79
+
80
+ def main(host: str = "0.0.0.0", port: int = 8000):
81
+ """
82
+ Entry point for direct execution via uv run or python -m.
83
+
84
+ This function enables running the server without Docker:
85
+ uv run --project . server
86
+ uv run --project . server --port 8001
87
+ python -m openspiel_env.server.app
88
+
89
+ Args:
90
+ host: Host address to bind to (default: "0.0.0.0")
91
+ port: Port number to listen on (default: 8000)
92
+ """
93
+ import uvicorn
94
+
95
+ uvicorn.run(app, host=host, port=port)
96
+
97
+
98
+ if __name__ == "__main__":
99
+ main()
server/openspiel_environment.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ OpenSpiel Environment Server Implementation.
9
+
10
+ This module wraps OpenSpiel's rl_environment.Environment and exposes it
11
+ via the OpenEnv Environment interface.
12
+ """
13
+
14
+ import uuid
15
+ from typing import Any, Dict
16
+
17
+ # Support both in-repo and standalone imports
18
+ try:
19
+ # In-repo imports (when running from OpenEnv repository)
20
+ from openenv.core.env_server.interfaces import Environment
21
+
22
+ from ..models import OpenSpielAction, OpenSpielObservation, OpenSpielState
23
+ from .opponent_policies import get_opponent_policy, OpponentPolicy
24
+ except ImportError:
25
+ from models import OpenSpielAction, OpenSpielObservation, OpenSpielState
26
+
27
+ # Standalone imports (when environment is standalone with openenv from pip)
28
+ from openenv.core.env_server.interfaces import Environment
29
+ from server.opponent_policies import get_opponent_policy, OpponentPolicy
30
+
31
+ # Import OpenSpiel
32
+ try:
33
+ import pyspiel
34
+ from open_spiel.python import rl_environment
35
+ except ImportError as e:
36
+ raise ImportError(
37
+ "OpenSpiel is not installed. "
38
+ "Please install it following instructions at: "
39
+ "https://github.com/google-deepmind/open_spiel"
40
+ ) from e
41
+
42
+
43
+ class OpenSpielEnvironment(Environment):
44
+ """
45
+ OpenSpiel Environment wrapper for OpenEnv.
46
+
47
+ This environment wraps OpenSpiel games and provides a single-agent interface.
48
+ For multi-player games, the agent controls one player while opponent(s) use
49
+ a fixed policy (e.g., random).
50
+
51
+ Supported games:
52
+ - Single-player: catch, cliff_walking, 2048, blackjack
53
+ - Multi-player: tic_tac_toe, kuhn_poker
54
+
55
+ Args:
56
+ game_name: Name of the OpenSpiel game (e.g., "catch", "tic_tac_toe").
57
+ agent_player: Which player ID the agent controls (default 0).
58
+ opponent_policy: Policy for opponent players ("random", "first", etc.).
59
+ game_params: Optional game-specific parameters.
60
+
61
+ Example:
62
+ >>> env = OpenSpielEnvironment("catch")
63
+ >>> obs = env.reset()
64
+ >>> print(obs.info_state) # Agent's observation
65
+ >>> obs = env.step(OpenSpielAction(action_id=1))
66
+ >>> print(obs.reward)
67
+ """
68
+
69
+ # Each WebSocket session gets its own OpenSpielEnvironment via the factory in
70
+ # server/app.py, and all mutable game state lives on the instance, so sessions
71
+ # are already isolated from each other. Upstream leaves this at the framework
72
+ # default of False, which caps the server at one session; declaring it lets
73
+ # create_app() accept max_concurrent_envs > 1.
74
+ SUPPORTS_CONCURRENT_SESSIONS = True
75
+
76
+ def __init__(
77
+ self,
78
+ game_name: str = "catch",
79
+ agent_player: int = 0,
80
+ opponent_policy: str = "random",
81
+ game_params: Dict[str, Any] | None = None,
82
+ ):
83
+ """Initialize OpenSpiel environment."""
84
+ super().__init__()
85
+
86
+ self.game_name = game_name
87
+ self.agent_player = agent_player
88
+ self.game_params = game_params or {}
89
+
90
+ # Create OpenSpiel environment
91
+ try:
92
+ self._ospiel_env = rl_environment.Environment(game_name, **self.game_params)
93
+ except Exception as e:
94
+ raise ValueError(
95
+ f"Failed to create OpenSpiel game '{game_name}': {e}"
96
+ ) from e
97
+
98
+ self.num_players = self._ospiel_env.num_players
99
+ self.is_turn_based = self._ospiel_env.is_turn_based
100
+
101
+ # Validate agent_player
102
+ if agent_player >= self.num_players:
103
+ raise ValueError(
104
+ f"agent_player={agent_player} >= num_players={self.num_players}"
105
+ )
106
+
107
+ # Set up opponent policy for multi-player games
108
+ self.opponent_policy_fn: OpponentPolicy | None = None
109
+ if self.num_players > 1:
110
+ self.opponent_policy_fn = get_opponent_policy(opponent_policy)
111
+
112
+ # Initialize state
113
+ self._state = OpenSpielState(
114
+ game_name=game_name,
115
+ agent_player=agent_player,
116
+ opponent_policy=opponent_policy,
117
+ game_params=self.game_params,
118
+ num_players=self.num_players,
119
+ )
120
+
121
+ # Track last opponent action for learning
122
+ self._last_opponent_action: int | None = None
123
+
124
+ def reset(self) -> OpenSpielObservation:
125
+ """
126
+ Reset the environment and return initial observation.
127
+
128
+ For multi-player games, this will autoplay opponent turns until
129
+ it's the agent's turn (or terminal state).
130
+
131
+ Returns:
132
+ Initial observation for the agent.
133
+ """
134
+ # Reset OpenSpiel environment
135
+ time_step = self._ospiel_env.reset()
136
+
137
+ # Reset state tracking
138
+ self._state.episode_id = str(uuid.uuid4())
139
+ self._state.step_count = 0
140
+ self._last_opponent_action = None
141
+
142
+ # Autoplay opponent turns until agent's turn
143
+ time_step = self._auto_play_opponents(time_step)
144
+
145
+ # Convert to OpenEnv observation
146
+ return self._make_observation(time_step)
147
+
148
+ def step(self, action: OpenSpielAction) -> OpenSpielObservation: # type: ignore[override]
149
+ """
150
+ Execute agent's action and return resulting observation.
151
+
152
+ For multi-player games, this will:
153
+ 1. Apply the agent's action
154
+ 2. Autoplay opponent turns until it's the agent's turn again
155
+ 3. Return the observation from the agent's perspective
156
+
157
+ Args:
158
+ action: OpenSpielAction containing the action_id to execute.
159
+
160
+ Returns:
161
+ Observation after action execution (and opponent turns if multi-player).
162
+
163
+ Raises:
164
+ ValueError: If action is not an OpenSpielAction.
165
+ """
166
+ if not isinstance(action, OpenSpielAction):
167
+ raise ValueError(f"Expected OpenSpielAction, got {type(action)}")
168
+
169
+ # Apply agent's action
170
+ if self.is_turn_based:
171
+ # Turn-based: single action
172
+ time_step = self._ospiel_env.step([action.action_id])
173
+ else:
174
+ # Simultaneous-move: need actions for all players
175
+ # For now, only support agent as player 0 in simultaneous games
176
+ if self.agent_player != 0:
177
+ raise NotImplementedError(
178
+ "Simultaneous-move games only support agent_player=0"
179
+ )
180
+ # Get opponent actions
181
+ opponent_actions = []
182
+ for player_id in range(self.num_players):
183
+ if player_id == self.agent_player:
184
+ opponent_actions.append(action.action_id)
185
+ else:
186
+ legal_actions = time_step.observations["legal_actions"][player_id]
187
+ opp_action = self.opponent_policy_fn.select_action(
188
+ legal_actions, time_step.observations
189
+ )
190
+ opponent_actions.append(opp_action)
191
+ time_step = self._ospiel_env.step(opponent_actions)
192
+
193
+ self._state.step_count += 1
194
+
195
+ # Autoplay opponent turns (for turn-based games)
196
+ if self.is_turn_based:
197
+ time_step = self._auto_play_opponents(time_step)
198
+
199
+ # Convert to OpenEnv observation
200
+ return self._make_observation(time_step)
201
+
202
+ @property
203
+ def state(self) -> OpenSpielState:
204
+ """Get current environment state."""
205
+ return self._state
206
+
207
+ def _auto_play_opponents(self, time_step) -> Any:
208
+ """
209
+ Autoplay opponent turns until it's the agent's turn or game is terminal.
210
+
211
+ Args:
212
+ time_step: Current TimeStep from OpenSpiel environment.
213
+
214
+ Returns:
215
+ Updated TimeStep after opponent moves.
216
+ """
217
+ # Single-player games: nothing to do
218
+ if self.num_players == 1:
219
+ return time_step
220
+
221
+ # Multi-player games: play opponent turns
222
+ while (
223
+ not time_step.last()
224
+ and time_step.observations["current_player"] != self.agent_player
225
+ ):
226
+ current_player = time_step.observations["current_player"]
227
+ legal_actions = time_step.observations["legal_actions"][current_player]
228
+
229
+ # Select opponent action
230
+ opp_action = self.opponent_policy_fn.select_action(
231
+ legal_actions, time_step.observations
232
+ )
233
+ self._last_opponent_action = opp_action
234
+
235
+ # Apply opponent action
236
+ time_step = self._ospiel_env.step([opp_action])
237
+ self._state.step_count += 1
238
+
239
+ return time_step
240
+
241
+ def _make_observation(self, time_step) -> OpenSpielObservation:
242
+ """
243
+ Convert OpenSpiel TimeStep to OpenEnv Observation.
244
+
245
+ Args:
246
+ time_step: OpenSpiel TimeStep object.
247
+
248
+ Returns:
249
+ OpenSpielObservation for the agent.
250
+ """
251
+ # Extract agent's information
252
+ info_state = time_step.observations["info_state"][self.agent_player]
253
+ legal_actions = time_step.observations["legal_actions"][self.agent_player]
254
+ current_player_id = time_step.observations["current_player"]
255
+
256
+ # Determine game phase
257
+ if time_step.last():
258
+ game_phase = "terminal"
259
+ elif time_step.first():
260
+ game_phase = "initial"
261
+ else:
262
+ game_phase = "playing"
263
+
264
+ # Get reward for agent
265
+ reward = None
266
+ if time_step.rewards is not None:
267
+ reward = float(time_step.rewards[self.agent_player])
268
+
269
+ # Create observation
270
+ obs = OpenSpielObservation(
271
+ info_state=info_state.tolist()
272
+ if hasattr(info_state, "tolist")
273
+ else list(info_state),
274
+ legal_actions=legal_actions,
275
+ game_phase=game_phase,
276
+ current_player_id=current_player_id,
277
+ opponent_last_action=self._last_opponent_action,
278
+ done=time_step.last(),
279
+ reward=reward,
280
+ )
281
+
282
+ return obs
server/opponent_policies.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Opponent policies for multi-player OpenSpiel games.
9
+
10
+ These policies are used to control non-agent players in multi-player games,
11
+ allowing single-agent RL training against fixed or adaptive opponents.
12
+ """
13
+
14
+ import random
15
+ from typing import Any, Protocol
16
+
17
+
18
+ class OpponentPolicy(Protocol):
19
+ """Protocol for opponent policies."""
20
+
21
+ def select_action(
22
+ self, legal_actions: list[int], observations: dict[str, Any]
23
+ ) -> int:
24
+ """
25
+ Select an action for the opponent.
26
+
27
+ Args:
28
+ legal_actions: List of legal action IDs.
29
+ observations: Current observations from the environment.
30
+
31
+ Returns:
32
+ Selected action ID.
33
+ """
34
+ ...
35
+
36
+
37
+ class RandomOpponent:
38
+ """Random opponent that selects uniformly from legal actions."""
39
+
40
+ def select_action(
41
+ self, legal_actions: list[int], observations: dict[str, Any]
42
+ ) -> int:
43
+ """Select a random legal action."""
44
+ if not legal_actions:
45
+ raise ValueError("No legal actions available")
46
+ return random.choice(legal_actions)
47
+
48
+
49
+ class FixedActionOpponent:
50
+ """Opponent that always selects the same action (e.g., first legal action)."""
51
+
52
+ def __init__(self, action_selector: str = "first"):
53
+ """
54
+ Initialize fixed action opponent.
55
+
56
+ Args:
57
+ action_selector: Which action to select ("first", "last", "middle").
58
+ """
59
+ self.action_selector = action_selector
60
+
61
+ def select_action(
62
+ self, legal_actions: list[int], observations: dict[str, Any]
63
+ ) -> int:
64
+ """Select a fixed legal action based on selector."""
65
+ if not legal_actions:
66
+ raise ValueError("No legal actions available")
67
+
68
+ if self.action_selector == "first":
69
+ return legal_actions[0]
70
+ elif self.action_selector == "last":
71
+ return legal_actions[-1]
72
+ elif self.action_selector == "middle":
73
+ return legal_actions[len(legal_actions) // 2]
74
+ else:
75
+ return legal_actions[0]
76
+
77
+
78
+ def get_opponent_policy(policy_name: str) -> OpponentPolicy:
79
+ """
80
+ Get an opponent policy by name.
81
+
82
+ Args:
83
+ policy_name: Name of the policy ("random", "first", "last", "middle").
84
+
85
+ Returns:
86
+ OpponentPolicy instance.
87
+
88
+ Raises:
89
+ ValueError: If policy_name is not recognized.
90
+ """
91
+ if policy_name == "random":
92
+ return RandomOpponent()
93
+ elif policy_name in ("first", "last", "middle"):
94
+ return FixedActionOpponent(action_selector=policy_name)
95
+ else:
96
+ raise ValueError(f"Unknown opponent policy: {policy_name}")
test_docker_all_games.sh ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ # All rights reserved.
4
+ #
5
+ # This source code is licensed under the BSD-style license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ # Automated test script for all OpenSpiel games in Docker
9
+ # Usage: ./test_docker_all_games.sh
10
+
11
+ set -e
12
+
13
+ # Colors for output
14
+ GREEN='\033[0;32m'
15
+ RED='\033[0;31m'
16
+ YELLOW='\033[1;33m'
17
+ BLUE='\033[0;34m'
18
+ NC='\033[0m' # No Color
19
+
20
+ # Configuration
21
+ IMAGE_NAME="openspiel-env:latest"
22
+ CONTAINER_NAME="openspiel-test"
23
+ PORT=8000
24
+ HEALTH_CHECK_URL="http://localhost:${PORT}/health"
25
+ MAX_WAIT=30
26
+
27
+ # Games to test
28
+ GAMES=("catch" "tic_tac_toe" "kuhn_poker" "cliff_walking" "2048" "blackjack")
29
+
30
+ # Results tracking
31
+ declare -a RESULTS
32
+ PASSED=0
33
+ FAILED=0
34
+
35
+ echo -e "${BLUE}========================================${NC}"
36
+ echo -e "${BLUE}OpenSpiel Docker Integration Test${NC}"
37
+ echo -e "${BLUE}========================================${NC}"
38
+ echo ""
39
+
40
+ # Function to cleanup containers
41
+ cleanup() {
42
+ echo -e "${YELLOW}Cleaning up containers...${NC}"
43
+ docker stop ${CONTAINER_NAME} 2>/dev/null || true
44
+ docker rm ${CONTAINER_NAME} 2>/dev/null || true
45
+ }
46
+
47
+ # Function to wait for server health
48
+ wait_for_health() {
49
+ local game=$1
50
+ echo -e " โณ Waiting for server to be ready..."
51
+
52
+ for i in $(seq 1 $MAX_WAIT); do
53
+ if curl -s -f ${HEALTH_CHECK_URL} > /dev/null 2>&1; then
54
+ echo -e " ${GREEN}โœ“${NC} Server ready (${i}s)"
55
+ return 0
56
+ fi
57
+ sleep 1
58
+ done
59
+
60
+ echo -e " ${RED}โœ—${NC} Server health check failed after ${MAX_WAIT}s"
61
+ return 1
62
+ }
63
+
64
+ # Function to test a game
65
+ test_game() {
66
+ local game=$1
67
+ echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}"
68
+ echo -e "${BLUE}Testing: ${game}${NC}"
69
+ echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}"
70
+
71
+ # Stop any existing container
72
+ cleanup
73
+
74
+ # Start container with game
75
+ echo -e " ๐Ÿณ Starting Docker container..."
76
+ docker run -d \
77
+ --name ${CONTAINER_NAME} \
78
+ -p ${PORT}:8000 \
79
+ -e OPENSPIEL_GAME=${game} \
80
+ ${IMAGE_NAME} > /dev/null
81
+
82
+ # Wait for server to be ready
83
+ if ! wait_for_health ${game}; then
84
+ echo -e " ${RED}โœ— FAILED${NC} - Server did not start"
85
+ RESULTS+=("${game}:FAILED:Server did not start")
86
+ FAILED=$((FAILED + 1))
87
+ cleanup
88
+ return 1
89
+ fi
90
+
91
+ # Run Python client test
92
+ echo -e " ๐ŸŽฎ Running Python client test..."
93
+ if NO_PROXY=localhost,127.0.0.1 HTTP_PROXY= HTTPS_PROXY= \
94
+ PYTHONPATH=$PWD/src:$PYTHONPATH \
95
+ python3 examples/openspiel_simple.py > /tmp/test_${game}.log 2>&1; then
96
+
97
+ # Check if episode completed successfully
98
+ if grep -q "Episode finished!" /tmp/test_${game}.log; then
99
+ echo -e " ${GREEN}โœ“ PASSED${NC} - Episode completed successfully"
100
+ RESULTS+=("${game}:PASSED")
101
+ PASSED=$((PASSED + 1))
102
+ else
103
+ echo -e " ${RED}โœ— FAILED${NC} - Episode did not complete"
104
+ RESULTS+=("${game}:FAILED:Episode incomplete")
105
+ FAILED=$((FAILED + 1))
106
+ fi
107
+ else
108
+ echo -e " ${RED}โœ— FAILED${NC} - Python client error"
109
+ RESULTS+=("${game}:FAILED:Client error")
110
+ FAILED=$((FAILED + 1))
111
+ fi
112
+
113
+ # Cleanup
114
+ cleanup
115
+ }
116
+
117
+ # Run tests for all games
118
+ for game in "${GAMES[@]}"; do
119
+ test_game ${game}
120
+ done
121
+
122
+ # Print summary
123
+ echo -e "\n${BLUE}========================================${NC}"
124
+ echo -e "${BLUE}Test Summary${NC}"
125
+ echo -e "${BLUE}========================================${NC}"
126
+ echo ""
127
+
128
+ for result in "${RESULTS[@]}"; do
129
+ IFS=':' read -r game status message <<< "$result"
130
+ if [ "$status" == "PASSED" ]; then
131
+ echo -e " ${GREEN}โœ“${NC} ${game}"
132
+ else
133
+ echo -e " ${RED}โœ—${NC} ${game} - ${message}"
134
+ fi
135
+ done
136
+
137
+ echo ""
138
+ echo -e "Total: ${PASSED} passed, ${FAILED} failed out of ${#GAMES[@]} games"
139
+ echo ""
140
+
141
+ # Exit with appropriate code
142
+ if [ $FAILED -eq 0 ]; then
143
+ echo -e "${GREEN}========================================${NC}"
144
+ echo -e "${GREEN}All tests PASSED! ๐ŸŽ‰${NC}"
145
+ echo -e "${GREEN}========================================${NC}"
146
+ exit 0
147
+ else
148
+ echo -e "${RED}========================================${NC}"
149
+ echo -e "${RED}Some tests FAILED${NC}"
150
+ echo -e "${RED}========================================${NC}"
151
+ exit 1
152
+ fi