SadyaMeta commited on
Commit
c755ba9
·
verified ·
1 Parent(s): 16a97d9

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Multi-stage build using openenv-base
8
+ # This Dockerfile is flexible and works for both:
9
+ # - In-repo environments (with local src/core)
10
+ # - Standalone environments (with openenv-core from pip)
11
+ # The build script (openenv build) handles context detection and sets appropriate build args.
12
+
13
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
14
+ FROM ${BASE_IMAGE} AS builder
15
+
16
+ WORKDIR /app
17
+
18
+ # Ensure git is available (required for installing dependencies from VCS)
19
+ RUN apt-get update && \
20
+ apt-get install -y --no-install-recommends git && \
21
+ rm -rf /var/lib/apt/lists/*
22
+
23
+ # Build argument to control whether we're building standalone or in-repo
24
+ ARG BUILD_MODE=in-repo
25
+ ARG ENV_NAME=my_game
26
+
27
+ # Copy environment code (always at root of build context)
28
+ COPY . /app/env
29
+
30
+ # For in-repo builds, openenv-core is already in the pyproject.toml dependencies
31
+ # For standalone builds, openenv-core will be installed from pip via pyproject.toml
32
+ WORKDIR /app/env
33
+
34
+ # Ensure uv is available (for local builds where base image lacks it)
35
+ RUN if ! command -v uv >/dev/null 2>&1; then \
36
+ curl -LsSf https://astral.sh/uv/install.sh | sh && \
37
+ mv /root/.local/bin/uv /usr/local/bin/uv && \
38
+ mv /root/.local/bin/uvx /usr/local/bin/uvx; \
39
+ fi
40
+
41
+ # Install dependencies using uv sync
42
+ # If uv.lock exists, use it; otherwise resolve on the fly
43
+ RUN --mount=type=cache,target=/root/.cache/uv \
44
+ if [ -f uv.lock ]; then \
45
+ uv sync --frozen --no-install-project --no-editable; \
46
+ else \
47
+ uv sync --no-install-project --no-editable; \
48
+ fi
49
+
50
+ RUN --mount=type=cache,target=/root/.cache/uv \
51
+ if [ -f uv.lock ]; then \
52
+ uv sync --frozen --no-editable; \
53
+ else \
54
+ uv sync --no-editable; \
55
+ fi
56
+
57
+ # Final runtime stage
58
+ FROM ${BASE_IMAGE}
59
+
60
+ WORKDIR /app
61
+
62
+ # Copy the virtual environment from builder
63
+ COPY --from=builder /app/env/.venv /app/.venv
64
+
65
+ # Copy the environment code
66
+ COPY --from=builder /app/env /app/env
67
+
68
+ # Set PATH to use the virtual environment
69
+ ENV PATH="/app/.venv/bin:$PATH"
70
+
71
+ # Set PYTHONPATH so imports work correctly
72
+ ENV PYTHONPATH="/app/env:$PYTHONPATH"
73
+
74
+ # Health check
75
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
76
+ CMD curl -f http://localhost:8000/health || exit 1
77
+
78
+ # Run the FastAPI server
79
+ # The module path is constructed to work with the /app/env structure
80
+ ENV ENABLE_WEB_INTERFACE=true
81
+ CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
README.md CHANGED
@@ -1,10 +1,199 @@
1
  ---
2
- title: Chess
3
- emoji: 👁
4
- colorFrom: yellow
5
- colorTo: pink
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: My Game Environment Server
3
+ emoji: 🥉
4
+ colorFrom: indigo
5
+ colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
  ---
13
 
14
+ # My Game Environment
15
+
16
+ A simple test environment that echoes back messages. Perfect for testing the env APIs as well as demonstrating environment usage patterns.
17
+
18
+ ## Quick Start
19
+
20
+ The simplest way to use the My Game environment is through the `MyGameEnv` class:
21
+
22
+ ```python
23
+ from my_game import MyGameAction, MyGameEnv
24
+
25
+ try:
26
+ # Create environment from Docker image
27
+ my_gameenv = MyGameEnv.from_docker_image("my_game-env:latest")
28
+
29
+ # Reset
30
+ result = my_gameenv.reset()
31
+ print(f"Reset: {result.observation.echoed_message}")
32
+
33
+ # Send multiple messages
34
+ messages = ["Hello, World!", "Testing echo", "Final message"]
35
+
36
+ for msg in messages:
37
+ result = my_gameenv.step(MyGameAction(message=msg))
38
+ print(f"Sent: '{msg}'")
39
+ print(f" → Echoed: '{result.observation.echoed_message}'")
40
+ print(f" → Length: {result.observation.message_length}")
41
+ print(f" → Reward: {result.reward}")
42
+
43
+ finally:
44
+ # Always clean up
45
+ my_gameenv.close()
46
+ ```
47
+
48
+ That's it! The `MyGameEnv.from_docker_image()` method handles:
49
+ - Starting the Docker container
50
+ - Waiting for the server to be ready
51
+ - Connecting to the environment
52
+ - Container cleanup when you call `close()`
53
+
54
+ ## Building the Docker Image
55
+
56
+ Before using the environment, you need to build the Docker image:
57
+
58
+ ```bash
59
+ # From project root
60
+ docker build -t my_game-env:latest -f server/Dockerfile .
61
+ ```
62
+
63
+ ## Deploying to Hugging Face Spaces
64
+
65
+ You can easily deploy your OpenEnv environment to Hugging Face Spaces using the `openenv push` command:
66
+
67
+ ```bash
68
+ # From the environment directory (where openenv.yaml is located)
69
+ openenv push
70
+
71
+ # Or specify options
72
+ openenv push --namespace my-org --private
73
+ ```
74
+
75
+ The `openenv push` command will:
76
+ 1. Validate that the directory is an OpenEnv environment (checks for `openenv.yaml`)
77
+ 2. Prepare a custom build for Hugging Face Docker space (enables web interface)
78
+ 3. Upload to Hugging Face (ensuring you're logged in)
79
+
80
+ ### Prerequisites
81
+
82
+ - Authenticate with Hugging Face: The command will prompt for login if not already authenticated
83
+
84
+ ### Options
85
+
86
+ - `--directory`, `-d`: Directory containing the OpenEnv environment (defaults to current directory)
87
+ - `--repo-id`, `-r`: Repository ID in format 'username/repo-name' (defaults to 'username/env-name' from openenv.yaml)
88
+ - `--base-image`, `-b`: Base Docker image to use (overrides Dockerfile FROM)
89
+ - `--private`: Deploy the space as private (default: public)
90
+
91
+ ### Examples
92
+
93
+ ```bash
94
+ # Push to your personal namespace (defaults to username/env-name from openenv.yaml)
95
+ openenv push
96
+
97
+ # Push to a specific repository
98
+ openenv push --repo-id my-org/my-env
99
+
100
+ # Push with a custom base image
101
+ openenv push --base-image ghcr.io/meta-pytorch/openenv-base:latest
102
+
103
+ # Push as a private space
104
+ openenv push --private
105
+
106
+ # Combine options
107
+ openenv push --repo-id my-org/my-env --base-image custom-base:latest --private
108
+ ```
109
+
110
+ After deployment, your space will be available at:
111
+ `https://huggingface.co/spaces/<repo-id>`
112
+
113
+ The deployed space includes:
114
+ - **Web Interface** at `/web` - Interactive UI for exploring the environment
115
+ - **API Documentation** at `/docs` - Full OpenAPI/Swagger interface
116
+ - **Health Check** at `/health` - Container health monitoring
117
+
118
+ ## Environment Details
119
+
120
+ ### Action
121
+ **MyGameAction**: Contains a single field
122
+ - `message` (str) - The message to echo back
123
+
124
+ ### Observation
125
+ **MyGameObservation**: Contains the echo response and metadata
126
+ - `echoed_message` (str) - The message echoed back
127
+ - `message_length` (int) - Length of the message
128
+ - `reward` (float) - Reward based on message length (length × 0.1)
129
+ - `done` (bool) - Always False for echo environment
130
+ - `metadata` (dict) - Additional info like step count
131
+
132
+ ### Reward
133
+ The reward is calculated as: `message_length × 0.1`
134
+ - "Hi" → reward: 0.2
135
+ - "Hello, World!" → reward: 1.3
136
+ - Empty message → reward: 0.0
137
+
138
+ ## Advanced Usage
139
+
140
+ ### Connecting to an Existing Server
141
+
142
+ If you already have a My Game environment server running, you can connect directly:
143
+
144
+ ```python
145
+ from my_game import MyGameEnv
146
+
147
+ # Connect to existing server
148
+ my_gameenv = MyGameEnv(base_url="<ENV_HTTP_URL_HERE>")
149
+
150
+ # Use as normal
151
+ result = my_gameenv.reset()
152
+ result = my_gameenv.step(MyGameAction(message="Hello!"))
153
+ ```
154
+
155
+ Note: When connecting to an existing server, `my_gameenv.close()` will NOT stop the server.
156
+
157
+ ## Development & Testing
158
+
159
+ ### Direct Environment Testing
160
+
161
+ Test the environment logic directly without starting the HTTP server:
162
+
163
+ ```bash
164
+ # From the server directory
165
+ python3 server/my_game_environment.py
166
+ ```
167
+
168
+ This verifies that:
169
+ - Environment resets correctly
170
+ - Step executes actions properly
171
+ - State tracking works
172
+ - Rewards are calculated correctly
173
+
174
+ ### Running Locally
175
+
176
+ Run the server locally for development:
177
+
178
+ ```bash
179
+ uvicorn server.app:app --reload
180
+ ```
181
+
182
+ ## Project Structure
183
+
184
+ ```
185
+ my_game/
186
+ ├── .dockerignore # Docker build exclusions
187
+ ├── __init__.py # Module exports
188
+ ├── README.md # This file
189
+ ├── openenv.yaml # OpenEnv manifest
190
+ ├── pyproject.toml # Project metadata and dependencies
191
+ ├── uv.lock # Locked dependencies (generated)
192
+ ├── client.py # MyGameEnv client implementation
193
+ ├── models.py # Action and Observation models
194
+ └── server/
195
+ ├── __init__.py # Server module exports
196
+ ├── my_game_environment.py # Core environment logic
197
+ ├── app.py # FastAPI application
198
+ └── Dockerfile # Container image definition
199
+ ```
__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Chess Environment - RL agent (White) vs random bot (Black)."""
8
+
9
+ from .client import ChessEnv
10
+ from .models import ChessAction, ChessObservation
11
+
12
+ __all__ = ["ChessAction", "ChessObservation", "ChessEnv"]
client.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Chess Environment HTTP Client.
9
+
10
+ This module provides the client for connecting to a Chess Environment server
11
+ over HTTP.
12
+ """
13
+
14
+ from typing import Any, Dict, Optional
15
+
16
+ from openenv_core.client_types import StepResult
17
+ from openenv_core.env_server.types import State
18
+ from openenv_core.http_env_client import HTTPEnvClient
19
+
20
+ from .models import ChessAction, ChessObservation
21
+
22
+
23
+ class ChessEnv(HTTPEnvClient[ChessAction, ChessObservation]):
24
+ """
25
+ HTTP client for the Chess Environment.
26
+
27
+ This client connects to a ChessEnvironment HTTP server and provides
28
+ methods to interact with it: reset(), step(), and state access.
29
+
30
+ Example:
31
+ >>> client = ChessEnv(base_url="http://localhost:8000")
32
+ >>> result = client.reset()
33
+ >>> print(result.observation.board_fen)
34
+ >>>
35
+ >>> result = client.step(ChessAction(move="e2e4"))
36
+ >>> print(result.observation.game_status)
37
+ >>> print(result.reward)
38
+ """
39
+
40
+ def _step_payload(self, action: ChessAction) -> Dict:
41
+ return {
42
+ "move": action.move,
43
+ }
44
+
45
+ def _parse_result(self, payload: Dict) -> StepResult[ChessObservation]:
46
+ obs_data = payload.get("observation", {})
47
+ observation = ChessObservation(
48
+ board_fen=obs_data.get("board_fen", ""),
49
+ legal_moves=obs_data.get("legal_moves", []),
50
+ white_move=obs_data.get("white_move", ""),
51
+ black_move=obs_data.get("black_move"),
52
+ material_balance=obs_data.get("material_balance", 0.0),
53
+ game_status=obs_data.get("game_status", "ongoing"),
54
+ captured_pieces=obs_data.get("captured_pieces", []),
55
+ done=payload.get("done", False),
56
+ reward=payload.get("reward"),
57
+ metadata=obs_data.get("metadata", {}),
58
+ )
59
+
60
+ return StepResult(
61
+ observation=observation,
62
+ reward=payload.get("reward"),
63
+ done=payload.get("done", False),
64
+ )
65
+
66
+ def _parse_state(self, payload: Dict) -> State:
67
+ return State(
68
+ episode_id=payload.get("episode_id"),
69
+ step_count=payload.get("step_count", 0),
70
+ )
models.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 the Chess Environment.
9
+
10
+ The chess environment lets an RL agent play White against a random bot (Black).
11
+ """
12
+
13
+ from dataclasses import dataclass, field
14
+ from typing import List, Optional
15
+
16
+ from openenv_core.env_server.types import Action, Observation
17
+
18
+
19
+ @dataclass(kw_only=True)
20
+ class ChessAction(Action):
21
+ """Action for the Chess environment — a UCI move string (e.g. 'e2e4')."""
22
+
23
+ move: str
24
+
25
+
26
+ @dataclass(kw_only=True)
27
+ class ChessObservation(Observation):
28
+ """Observation from the Chess environment."""
29
+
30
+ board_fen: str
31
+ legal_moves: List[str] = field(default_factory=list)
32
+ white_move: str = ""
33
+ black_move: Optional[str] = None
34
+ material_balance: float = 0.0
35
+ game_status: str = "ongoing"
36
+ captured_pieces: List[str] = field(default_factory=list)
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: chess
3
+ type: space
4
+ runtime: fastapi
5
+ app: server.app:app
6
+ port: 8000
openenv_my_game.egg-info/PKG-INFO ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-my_game
3
+ Version: 0.1.0
4
+ Summary: My Game environment for OpenEnv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core>=0.1.0
7
+ Requires-Dist: fastapi>=0.115.0
8
+ Requires-Dist: pydantic>=2.0.0
9
+ Requires-Dist: uvicorn>=0.24.0
10
+ Requires-Dist: requests>=2.31.0
11
+ Requires-Dist: chess>=1.10.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
openenv_my_game.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ pyproject.toml
3
+ ./__init__.py
4
+ ./client.py
5
+ ./models.py
6
+ openenv_my_game.egg-info/PKG-INFO
7
+ openenv_my_game.egg-info/SOURCES.txt
8
+ openenv_my_game.egg-info/dependency_links.txt
9
+ openenv_my_game.egg-info/entry_points.txt
10
+ openenv_my_game.egg-info/requires.txt
11
+ openenv_my_game.egg-info/top_level.txt
12
+ server/__init__.py
13
+ server/app.py
14
+ server/my_game_environment.py
openenv_my_game.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv_my_game.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = my_game.server.app:main
openenv_my_game.egg-info/requires.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core>=0.1.0
2
+ fastapi>=0.115.0
3
+ pydantic>=2.0.0
4
+ uvicorn>=0.24.0
5
+ requests>=2.31.0
6
+ chess>=1.10.0
7
+
8
+ [dev]
9
+ pytest>=8.0.0
10
+ pytest-cov>=4.0.0
openenv_my_game.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ my_game
pyproject.toml ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-my_game"
13
+ version = "0.1.0"
14
+ description = "My Game environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv dependencies (required for server functionality)
18
+ # "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git@main#subdirectory=src/core",
19
+ "openenv-core>=0.1.0",
20
+ "fastapi>=0.115.0",
21
+ "pydantic>=2.0.0",
22
+ "uvicorn>=0.24.0",
23
+ "requests>=2.31.0",
24
+ # Environment-specific dependencies
25
+ "chess>=1.10.0",
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 - enables running via: uv run --project . server
36
+ # or: python -m my_game.server.app
37
+ server = "my_game.server.app:main"
38
+
39
+ [tool.setuptools]
40
+ include-package-data = true
41
+ packages = ["my_game", "my_game.server"]
42
+ package-dir = { "my_game" = ".", "my_game.server" = "server" }
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """Chess environment server components."""
8
+
9
+ from .my_game_environment import ChessEnvironment
10
+
11
+ __all__ = ["ChessEnvironment"]
server/app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Chess Environment.
9
+
10
+ This module creates an HTTP server that exposes the ChessEnvironment
11
+ over HTTP endpoints, making it compatible with HTTPEnvClient.
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
+ python -m server.app
22
+ """
23
+
24
+ try:
25
+ from openenv_core.env_server.http_server import create_app
26
+ except Exception as e: # pragma: no cover
27
+ raise ImportError("openenv_core is required for the web interface. Install dependencies with '\n uv sync\n'") from e
28
+
29
+ from .my_game_environment import ChessEnvironment
30
+ from models import ChessAction, ChessObservation
31
+
32
+ # Create the environment instance
33
+ env = ChessEnvironment()
34
+
35
+ # Create the app with web interface and README integration
36
+ app = create_app(
37
+ env,
38
+ ChessAction,
39
+ ChessObservation,
40
+ env_name="chess",
41
+ )
42
+
43
+
44
+ def main(host: str = "0.0.0.0", port: int = 8000):
45
+ """
46
+ Entry point for direct execution via uv run or python -m.
47
+
48
+ This function enables running the server without Docker:
49
+ uv run --project . server
50
+ uv run --project . server --port 8001
51
+ python -m my_game.server.app
52
+
53
+ Args:
54
+ host: Host address to bind to (default: "0.0.0.0")
55
+ port: Port number to listen on (default: 8000)
56
+
57
+ For production deployments, consider using uvicorn directly with
58
+ multiple workers:
59
+ uvicorn my_game.server.app:app --workers 4
60
+ """
61
+ import uvicorn
62
+
63
+ uvicorn.run(app, host=host, port=port)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ import argparse
68
+
69
+ parser = argparse.ArgumentParser()
70
+ parser.add_argument("--port", type=int, default=8000)
71
+ args = parser.parse_args()
72
+ main(port=args.port)
server/my_game_environment.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Chess Environment Implementation.
9
+
10
+ An RL agent plays White against a random bot (Black).
11
+ Reward is shaped: per-move material delta + terminal win/loss/draw bonus.
12
+ """
13
+
14
+ import random
15
+ from typing import List
16
+ from uuid import uuid4
17
+
18
+ import chess
19
+
20
+ from openenv_core.env_server.interfaces import Environment
21
+ from openenv_core.env_server.types import State
22
+
23
+ from models import ChessAction, ChessObservation
24
+
25
+ # Piece values: P=1, N=3, B=3, R=5, Q=9
26
+ PIECE_VALUES = {
27
+ chess.PAWN: 1,
28
+ chess.KNIGHT: 3,
29
+ chess.BISHOP: 3,
30
+ chess.ROOK: 5,
31
+ chess.QUEEN: 9,
32
+ }
33
+
34
+
35
+ class ChessEnvironment(Environment):
36
+ """
37
+ Chess environment where an RL agent (White) plays against a random bot (Black).
38
+
39
+ Reward shaping:
40
+ - material_delta: change in (White material - Black material) each step
41
+ - terminal bonus: +1 for win, -1 for loss, 0 for draw
42
+ """
43
+
44
+ def __init__(self):
45
+ self._board = chess.Board()
46
+ self._state = State(episode_id=str(uuid4()), step_count=0)
47
+ self._captured_pieces: List[str] = []
48
+
49
+ def reset(self) -> ChessObservation:
50
+ self._board = chess.Board()
51
+ self._state = State(episode_id=str(uuid4()), step_count=0)
52
+ self._captured_pieces = []
53
+
54
+ return ChessObservation(
55
+ board_fen=self._board.fen(),
56
+ legal_moves=[m.uci() for m in self._board.legal_moves],
57
+ white_move="",
58
+ black_move=None,
59
+ material_balance=0.0,
60
+ game_status="ongoing",
61
+ captured_pieces=[],
62
+ done=False,
63
+ reward=0.0,
64
+ )
65
+
66
+ def step(self, action: ChessAction) -> ChessObservation: # type: ignore[override]
67
+ # --- validate ---
68
+ try:
69
+ move = chess.Move.from_uci(action.move)
70
+ except (chess.InvalidMoveError, ValueError) as exc:
71
+ raise ValueError(f"Invalid UCI string: {action.move!r}") from exc
72
+
73
+ if move not in self._board.legal_moves:
74
+ raise ValueError(
75
+ f"Illegal move: {action.move!r}. "
76
+ f"Legal moves: {[m.uci() for m in self._board.legal_moves]}"
77
+ )
78
+
79
+ self._state.step_count += 1
80
+ balance_before = self._material_balance()
81
+
82
+ # --- White's move ---
83
+ self._track_capture(move)
84
+ self._board.push(move)
85
+ white_uci = action.move
86
+
87
+ # check if game ended after White's move
88
+ status = self._get_game_status()
89
+ if self._is_terminal_status(status):
90
+ balance_after = self._material_balance()
91
+ material_delta = balance_after - balance_before
92
+ reward = material_delta + self._terminal_reward(status)
93
+ return ChessObservation(
94
+ board_fen=self._board.fen(),
95
+ legal_moves=[],
96
+ white_move=white_uci,
97
+ black_move=None,
98
+ material_balance=balance_after,
99
+ game_status=status,
100
+ captured_pieces=list(self._captured_pieces),
101
+ done=True,
102
+ reward=reward,
103
+ )
104
+
105
+ # --- Black's move (random) ---
106
+ black_moves = list(self._board.legal_moves)
107
+ black_move = random.choice(black_moves)
108
+ self._track_capture(black_move)
109
+ self._board.push(black_move)
110
+ black_uci = black_move.uci()
111
+
112
+ # --- post-move evaluation ---
113
+ status = self._get_game_status()
114
+ balance_after = self._material_balance()
115
+ material_delta = balance_after - balance_before
116
+ terminal = self._is_terminal_status(status)
117
+ reward = material_delta + (self._terminal_reward(status) if terminal else 0.0)
118
+
119
+ return ChessObservation(
120
+ board_fen=self._board.fen(),
121
+ legal_moves=[m.uci() for m in self._board.legal_moves] if not terminal else [],
122
+ white_move=white_uci,
123
+ black_move=black_uci,
124
+ material_balance=balance_after,
125
+ game_status=status,
126
+ captured_pieces=list(self._captured_pieces),
127
+ done=terminal,
128
+ reward=reward,
129
+ )
130
+
131
+ # ------------------------------------------------------------------
132
+ # Helpers
133
+ # ------------------------------------------------------------------
134
+
135
+ def _track_capture(self, move: chess.Move) -> None:
136
+ """Record a captured piece symbol (handles en passant)."""
137
+ board = self._board
138
+ if board.is_en_passant(move):
139
+ self._captured_pieces.append(chess.piece_symbol(chess.PAWN))
140
+ elif board.piece_at(move.to_square) is not None:
141
+ self._captured_pieces.append(board.piece_at(move.to_square).symbol())
142
+
143
+ def _material_balance(self) -> float:
144
+ """Return White material minus Black material."""
145
+ white = 0.0
146
+ black = 0.0
147
+ for sq in chess.SQUARES:
148
+ piece = self._board.piece_at(sq)
149
+ if piece is None:
150
+ continue
151
+ val = PIECE_VALUES.get(piece.piece_type, 0)
152
+ if piece.color == chess.WHITE:
153
+ white += val
154
+ else:
155
+ black += val
156
+ return white - black
157
+
158
+ def _get_game_status(self) -> str:
159
+ b = self._board
160
+ if b.is_checkmate():
161
+ return "checkmate"
162
+ if b.is_stalemate():
163
+ return "stalemate"
164
+ if b.is_insufficient_material():
165
+ return "draw_insufficient"
166
+ if b.is_fifty_moves():
167
+ return "draw_fifty"
168
+ if b.is_repetition():
169
+ return "draw_repetition"
170
+ if b.is_check():
171
+ return "check"
172
+ return "ongoing"
173
+
174
+ @staticmethod
175
+ def _is_terminal_status(status: str) -> bool:
176
+ return status in ("checkmate", "stalemate", "draw_insufficient",
177
+ "draw_fifty", "draw_repetition")
178
+
179
+ def _terminal_reward(self, status: str) -> float:
180
+ if status == "checkmate":
181
+ # whoever is to move is in checkmate → they lost
182
+ if self._board.turn == chess.BLACK:
183
+ return 1.0 # White delivered checkmate
184
+ else:
185
+ return -1.0 # Black delivered checkmate
186
+ # all other terminal states are draws
187
+ return 0.0
188
+
189
+ @property
190
+ def state(self) -> State:
191
+ return self._state
uv.lock ADDED
The diff for this file is too large to render. See raw diff