diff --git a/src/envs/connect4_env/.gitignore b/src/envs/connect4_env/.gitignore new file mode 100644 index 00000000..218b74eb --- /dev/null +++ b/src/envs/connect4_env/.gitignore @@ -0,0 +1,48 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +env/ +ENV/ +.venv/ + +# IDEs and editors +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store + +# Environment outputs +outputs/ +logs/ +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ + diff --git a/src/envs/connect4_env/README.md b/src/envs/connect4_env/README.md index e69de29b..0534b4c7 100644 --- a/src/envs/connect4_env/README.md +++ b/src/envs/connect4_env/README.md @@ -0,0 +1,55 @@ +--- +title: Connect4 Environment Server +emoji: 🔴 +colorFrom: red +colorTo: pink +sdk: docker +pinned: false +app_port: 8000 +base_path: /web +tags: + - openenv + - games +--- + +# Connect4 Environment + +This repository packages the classic Connect4 board game as a standalone OpenEnv +environment. It exposes a FastAPI server compatible with the OpenEnv CLI and +provides a Python client for interacting with the environment programmatically. + +## Quick Start + +```bash +# Install dependencies (editable mode for local development) +uv pip install -e . + +# Launch the server locally +uv run server +``` + +Once running, visit `http://localhost:8000/docs` to explore the OpenAPI schema. + +## Python Usage + +```python +from connect4_env import Connect4Env, Connect4Action + +env = Connect4Env.from_docker_image("connect4-env:latest") +result = env.reset() +print(result.observation.board) + +result = env.step(Connect4Action(column=3)) +print(result.reward, result.done) + +env.close() +``` + +## Deploy + +- **Validate:** `openenv validate` +- **Build Docker image:** `openenv build` +- **Push to Hugging Face / Docker Hub:** `openenv push` + +Customize the Docker build or deployment metadata through environment variables +as needed. The default server listens on port `8000`. \ No newline at end of file diff --git a/src/envs/connect4_env/__init__.py b/src/envs/connect4_env/__init__.py index 03d92d39..4c9cd9f5 100644 --- a/src/envs/connect4_env/__init__.py +++ b/src/envs/connect4_env/__init__.py @@ -10,14 +10,14 @@ This module provides OpenEnv integration for the classic Connect4 board game. Example: - >>> from envs.Connect4_env import Connect4Env, Connect4Action + >>> from connect4_env import Connect4Env, Connect4Action >>> >>> # Connect to a running server or start via Docker - >>> env = Connect4Env.from_docker_image("Connect4-env:latest") + >>> env = Connect4Env.from_docker_image("connect4-env:latest") >>> >>> # Reset and interact >>> result = env.reset() - >>> result = env.step(Connect4Action(column=2)) + >>> result = env.step(Connect4Action(column=2)) >>> print(result.reward, result.done) >>> >>> # Cleanup diff --git a/src/envs/connect4_env/client.py b/src/envs/connect4_env/client.py index 56aee843..abb8994b 100644 --- a/src/envs/connect4_env/client.py +++ b/src/envs/connect4_env/client.py @@ -13,16 +13,12 @@ from __future__ import annotations -from typing import Any, Dict, TYPE_CHECKING +from typing import Any, Dict -from core.client_types import StepResult -from core.http_env_client import HTTPEnvClient +from openenv_core.http_env_client import HTTPEnvClient, StepResult from .models import Connect4Action, Connect4Observation, Connect4State -if TYPE_CHECKING: - from core.containers.runtime import ContainerProvider - class Connect4Env(HTTPEnvClient[Connect4Action, Connect4Observation]): """ @@ -68,7 +64,7 @@ def _parse_result(self, payload: Dict[str, Any]) -> StepResult[Connect4Observati obs_data = payload.get("observation", {}) observation = Connect4Observation( - board=obs_data.get("board", [[0]*7 for _ in range(6)]), + board=obs_data.get("board", [[0] * 7 for _ in range(6)]), legal_actions=obs_data.get("legal_actions", []), done=payload.get("done", False), reward=payload.get("reward", 0.0), @@ -93,7 +89,7 @@ def _parse_state(self, payload: Dict[str, Any]) -> Connect4State: """ return Connect4State( episode_id=payload.get("episode_id", ""), - board=payload.get("board", [[0]*7 for _ in range(6)]), + board=payload.get("board", [[0] * 7 for _ in range(6)]), next_player=payload.get("next_player", 1), step_count=payload.get("step_count", 0), ) diff --git a/src/envs/connect4_env/models.py b/src/envs/connect4_env/models.py index d10bb5ef..9a2d0f35 100644 --- a/src/envs/connect4_env/models.py +++ b/src/envs/connect4_env/models.py @@ -16,7 +16,7 @@ import numpy as np from typing import List -from core.env_server import Action, Observation, State +from openenv_core.env_server.types import Action, Observation, State @dataclass @@ -27,6 +27,7 @@ class Connect4Action(Action): Attributes: column: The column index (0 to 6) where the piece will be placed. """ + column: int @@ -42,13 +43,12 @@ class Connect4Observation(Observation): done: Whether the game is over. reward: Reward for the last action. """ - + board: List[List[int]] legal_actions: List[int] done: bool = False reward: float = 0.0 metadata: dict = field(default_factory=dict) - @dataclass(kw_only=True) @@ -62,7 +62,8 @@ class Connect4State(State): next_player: Whose turn it is (1 or -1). step_count: Number of steps taken in the game. """ + episode_id: str - board: List[List[int]] = field(default_factory=lambda: np.zeros((6,7), dtype=int).tolist()) + board: List[List[int]] = field(default_factory=lambda: np.zeros((6, 7), dtype=int).tolist()) next_player: int = 1 step_count: int = 0 diff --git a/src/envs/connect4_env/openenv.yaml b/src/envs/connect4_env/openenv.yaml new file mode 100644 index 00000000..f8320fda --- /dev/null +++ b/src/envs/connect4_env/openenv.yaml @@ -0,0 +1,5 @@ +name: connect4_env +version: "0.1.0" +description: "Connect4 environment for OpenEnv" +action: Connect4Action +observation: Connect4Observation diff --git a/src/envs/connect4_env/pyproject.toml b/src/envs/connect4_env/pyproject.toml new file mode 100644 index 00000000..beac697b --- /dev/null +++ b/src/envs/connect4_env/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "openenv-connect4" +version = "0.1.0" +description = "Connect4 environment for OpenEnv" +requires-python = ">=3.10" +dependencies = [ + "openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git@main#subdirectory=src/core", + "fastapi>=0.115.0", + "pydantic>=2.0.0", + "uvicorn>=0.24.0", + "requests>=2.25.0", + # Add your environment-specific dependencies here + "gymnasium>=0.29.0", + "ale-py>=0.8.0", + "numpy>=1.24.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.0.0", + "ipykernel>=6.29.5", +] + +[project.scripts] +server = "connect4_env.server.app:main" + +[tool.setuptools] +packages = ["connect4_env", "connect4_env.server"] +package-dir = { "connect4_env" = ".", "connect4_env.server" = "server" } + +[tool.setuptools.package-data] +connect4_env = ["**/*.yaml", "**/*.yml"] \ No newline at end of file diff --git a/src/envs/connect4_env/server/Dockerfile b/src/envs/connect4_env/server/Dockerfile index 04d40ff2..c8f28e70 100644 --- a/src/envs/connect4_env/server/Dockerfile +++ b/src/envs/connect4_env/server/Dockerfile @@ -1,18 +1,21 @@ -ARG BASE_IMAGE=openenv-base:latest -FROM ${BASE_IMAGE} - -# Install any additional dependencies -RUN pip install --no-cache-dir \ - gymnasium>=0.29.0 \ - ale-py>=0.8.0 \ - numpy>=1.24.0 -# Copy environment code -COPY src/core/ /app/src/core/ -COPY src/envs/connect4_env/ /app/src/envs/connect4_env/ - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8000/health || exit 1 - -# Run server -CMD ["uvicorn", "envs.connect4_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +# syntax=docker/dockerfile:1.6 + +FROM python:3.11-slim AS runtime + +WORKDIR /app/env + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +COPY . . + +RUN pip install --upgrade pip && \ + pip install --no-cache-dir -e . + +EXPOSE 8000 + +ENV PYTHONUNBUFFERED=1 \ + ENABLE_WEB_INTERFACE=true + +CMD ["python", "-m", "uvicorn", "connect4_env.server.app:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/src/envs/connect4_env/server/app.py b/src/envs/connect4_env/server/app.py index a214e42b..c9e32641 100644 --- a/src/envs/connect4_env/server/app.py +++ b/src/envs/connect4_env/server/app.py @@ -1,12 +1,27 @@ -from core.env_server import create_fastapi_app +from openenv_core.env_server.http_server import create_app + from ..models import Connect4Action, Connect4Observation from .connect4_environment import Connect4Environment env = Connect4Environment() -app = create_fastapi_app(env, Connect4Action, Connect4Observation) +app = create_app( + env, + Connect4Action, + Connect4Observation, + env_name="connect4_env", +) -if __name__ == "__main__": +def main(port: int = 8000): import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=port) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--port", type=int, default=8000) + args = parser.parse_args() + main(args.port) diff --git a/src/envs/connect4_env/server/connect4_environment.py b/src/envs/connect4_env/server/connect4_environment.py index 1ef6414b..f83c7913 100644 --- a/src/envs/connect4_env/server/connect4_environment.py +++ b/src/envs/connect4_env/server/connect4_environment.py @@ -1,9 +1,11 @@ import uuid + import numpy as np -from core.env_server import Environment +from openenv_core.env_server.interfaces import Environment from ..models import Connect4Action, Connect4Observation, Connect4State + class Connect4Environment(Environment): ROWS = 6 COLUMNS = 7 @@ -19,10 +21,7 @@ def reset(self): self.invalid_move_played = False self._state = Connect4State( - board=self.board.copy().tolist(), - next_player=self.next_player, - episode_id=str(uuid.uuid4()), - step_count=0 + board=self.board.copy().tolist(), next_player=self.next_player, episode_id=str(uuid.uuid4()), step_count=0 ) return self._make_observation() @@ -47,12 +46,12 @@ def step(self, action: Connect4Action): reward, done = self._check_win_or_draw(row, col) self.next_player *= -1 - + self._state = Connect4State( board=self.board.copy().tolist(), next_player=self.next_player, episode_id=self._state.episode_id, - step_count=self._state.step_count + 1 + step_count=self._state.step_count + 1, ) return self._make_observation(reward, done) @@ -64,18 +63,18 @@ def _make_observation(self, reward=0.0, done=False): legal_actions=legal_actions, reward=reward, done=done, - metadata={"next_player": self.next_player} + metadata={"next_player": self.next_player}, ) def _check_win_or_draw(self, row, col): # Implement 4-in-a-row check (like your Gymnasium code) player = self.board[row, col] - directions = [(1,0),(0,1),(1,1),(1,-1)] + directions = [(1, 0), (0, 1), (1, 1), (1, -1)] for dr, dc in directions: count = 0 for step in range(-3, 4): - r, c = row + step*dr, col + step*dc - if 0 <= r < self.ROWS and 0 <= c < self.COLUMNS and self.board[r,c] == player: + r, c = row + step * dr, col + step * dc + if 0 <= r < self.ROWS and 0 <= c < self.COLUMNS and self.board[r, c] == player: count += 1 if count >= 4: return 1.0, True