Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/envs/connect4_env/.gitignore
Original file line number Diff line number Diff line change
@@ -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/

55 changes: 55 additions & 0 deletions src/envs/connect4_env/README.md
Original file line number Diff line number Diff line change
@@ -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`.
6 changes: 3 additions & 3 deletions src/envs/connect4_env/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 4 additions & 8 deletions src/envs/connect4_env/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
"""
Expand Down Expand Up @@ -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),
Expand All @@ -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),
)
9 changes: 5 additions & 4 deletions src/envs/connect4_env/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +27,7 @@ class Connect4Action(Action):
Attributes:
column: The column index (0 to 6) where the piece will be placed.
"""

column: int


Expand All @@ -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)
Expand All @@ -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
5 changes: 5 additions & 0 deletions src/envs/connect4_env/openenv.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name: connect4_env
version: "0.1.0"
description: "Connect4 environment for OpenEnv"
action: Connect4Action
observation: Connect4Observation
37 changes: 37 additions & 0 deletions src/envs/connect4_env/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
39 changes: 21 additions & 18 deletions src/envs/connect4_env/server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
# 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"]
23 changes: 19 additions & 4 deletions src/envs/connect4_env/server/app.py
Original file line number Diff line number Diff line change
@@ -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)
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)
21 changes: 10 additions & 11 deletions src/envs/connect4_env/server/connect4_environment.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()

Expand All @@ -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)
Expand All @@ -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
Expand Down