Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3cda7d
Add Minesweeper environment core implementation
acharyaanusha Dec 5, 2025
9730c27
Add Docker support and example for Minesweeper environment
acharyaanusha Dec 5, 2025
e147894
Add tests and CI/CD integration for Minesweeper environment
acharyaanusha Dec 5, 2025
1469cc8
Update minesweeper environment to align with OpenEnv standards
acharyaanusha Jan 16, 2026
98bf1d4
Refactor Minesweeper environment tests and Dockerfile CMD for improve…
acharyaanusha Jan 21, 2026
2b38813
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Jan 31, 2026
100833d
Merge branch 'main' into feature/minesweeper_env
burtenshaw Feb 5, 2026
ab02918
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 5, 2026
d54657e
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 5, 2026
fe25387
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 11, 2026
0601bab
Fix minesweeper tests: close WebSocket clients to prevent session cap…
acharyaanusha Feb 11, 2026
f94968f
Convert minesweeper tests to async to work with async EnvClient
acharyaanusha Feb 23, 2026
ae12c52
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 23, 2026
1dde181
Fix IndexError: auto-reset environment in __init__ so board is playab…
acharyaanusha Feb 23, 2026
67eda74
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 24, 2026
a9a0d6f
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Feb 28, 2026
2a06cd1
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Mar 8, 2026
58e1b68
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Mar 28, 2026
f90158e
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Apr 8, 2026
7ed5ac3
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Apr 16, 2026
faf4e51
Merge branch 'main' into feature/minesweeper_env
acharyaanusha May 13, 2026
fa03d71
Address PR review: remove no-op try/except shims and tidy Tier 1 issues
acharyaanusha May 13, 2026
84a565d
Move minesweeper env to envs/, align with project conventions
acharyaanusha May 13, 2026
41836c9
Fix win-condition bug and address PR review follow-ups
acharyaanusha May 14, 2026
0b9b3d5
Merge branch 'main' into feature/minesweeper_env
acharyaanusha May 25, 2026
09e31e2
Merge branch 'main' into feature/minesweeper_env
acharyaanusha Jun 3, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ jobs:
context: envs/git_env
- name: connect4_env
dockerfile: envs/connect4_env/server/Dockerfile
context: envs/connect4_env
- name: minesweeper-env
dockerfile: envs/minesweeper_env/server/Dockerfile
context: envs/minesweeper_env
- name: chess-env
dockerfile: envs/chess_env/server/Dockerfile
context: envs/chess_env
Expand Down
143 changes: 143 additions & 0 deletions envs/minesweeper_env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Minesweeper Environment

A Minesweeper game environment for reinforcement learning agents. The environment consists of a grid with hidden mines where the agent must reveal all non-mine cells without triggering any mines.

## Overview

The agent can perform two types of actions:
- Reveal cells to uncover numbers indicating adjacent mines
- Place or remove flags on suspected mine locations

The game ends when all non-mine cells are revealed (win) or a mine is revealed (loss).

## Quick Start

```python
from envs.minesweeper_env import MinesweeperAction, MinesweeperEnv

# Create environment from Docker image
minesweeper_env = MinesweeperEnv.from_docker_image("minesweeper-env:latest")

try:
# Reset the environment
result = minesweeper_env.reset()
print(f"Board size: {result.observation.board_height}x{result.observation.board_width}")
print(f"Number of mines: {result.observation.num_mines}")

# Reveal a cell
result = minesweeper_env.step(MinesweeperAction(row=2, col=2, action_type="reveal"))
print(f"Cells revealed: {result.observation.cells_revealed}")
print(f"Reward: {result.observation.reward}")

# Place a flag
result = minesweeper_env.step(MinesweeperAction(row=1, col=1, action_type="flag"))
print(f"Flags placed: {result.observation.flags_placed}")

finally:
minesweeper_env.close()
```

## Building the Docker Image

Build the Docker image from the project root:

```bash
docker build -t minesweeper-env:latest -f envs/minesweeper_env/server/Dockerfile envs/minesweeper_env
```

Or use the build script:

```bash
cd envs/minesweeper_env/server
./build_docker.sh latest
```

## Environment Details

### Action

**MinesweeperAction**: Specifies the cell and action type
- `row` (int) - Row index (0-indexed)
- `col` (int) - Column index (0-indexed)
- `action_type` (str) - Either "reveal" or "flag"

### Observation

**MinesweeperObservation**: Current board state and game information
- `board` (list[list]) - 2D grid showing the current state of each cell:
- `-1`: Unrevealed cell
- `0-8`: Number of adjacent mines (revealed cell)
- `'F'`: Flagged cell
- `'*'`: Mine (only shown when game is lost)
- `num_mines` (int) - Total number of mines on the board
- `flags_placed` (int) - Number of flags currently placed
- `cells_revealed` (int) - Number of cells that have been revealed
- `game_status` (GameStatus) - Current game status (ONGOING, WON, or LOST)
- `done` (bool) - Whether the game has ended
- `reward` (float) - Reward from the last action
- `metadata` (dict) - Additional information

### Rewards

- Revealing a safe cell: +1.0
- Placing a flag on a mine: +0.5
- Revealing a mine (game over): -10.0
- Revealing an already revealed cell: -0.05
- Invalid action: -0.1

### Game Status

- `GameStatus.ONGOING`: Game is still in progress
- `GameStatus.WON`: All non-mine cells have been revealed
- `GameStatus.LOST`: A mine was revealed

## Configuration

The default configuration is:
- Board height: 5
- Board width: 5
- Number of mines: 5

These can be configured when initializing the environment server.

## Connecting to an Existing Server

If you have a server already running:

```python
from envs.minesweeper_env import MinesweeperEnv

# Connect to existing server
minesweeper_env = MinesweeperEnv(base_url="http://localhost:8000")

# Use as normal
result = minesweeper_env.reset()
```

Note: When connecting to an existing server, `close()` will not stop the server.

## Running Tests

Run the test suite:

```bash
python tests/envs/test_minesweeper_env.py
```

## Project Structure

```
minesweeper_env/
├── __init__.py # Module exports
├── README.md # This file
├── client.py # MinesweeperEnv client implementation
├── models.py # Action, Observation, and State models
├── openenv.yaml # Environment configuration
├── pyproject.toml # Package dependencies
└── server/
├── __init__.py # Server module exports
├── minesweeper_environment.py # Core game logic
├── app.py # FastAPI application
├── Dockerfile # Container image definition
└── build_docker.sh # Build script
```
17 changes: 17 additions & 0 deletions envs/minesweeper_env/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""Minesweeper Environment - a grid-based puzzle game for OpenEnv."""

from .client import MinesweeperEnv
from .models import GameStatus, MinesweeperAction, MinesweeperObservation

__all__ = [
"GameStatus",
"MinesweeperAction",
"MinesweeperEnv",
"MinesweeperObservation",
]
115 changes: 115 additions & 0 deletions envs/minesweeper_env/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

"""
Minesweeper Environment Client.

This module provides the client for connecting to a Minesweeper Environment server
via WebSocket for persistent sessions.
"""

from typing import Dict

from openenv.core.client_types import StepResult
from openenv.core.env_client import EnvClient
from openenv.core.env_server.types import State

try:
# In-repo imports (when running from OpenEnv repository)
from .models import MinesweeperAction, MinesweeperObservation
except ImportError:
# Standalone imports (when this module is imported as a top-level package)
from models import MinesweeperAction, MinesweeperObservation


class MinesweeperEnv(EnvClient[MinesweeperAction, MinesweeperObservation, State]):
"""
Client for the Minesweeper Environment.

This client maintains a persistent WebSocket connection to the environment
server, enabling efficient multi-step interactions with lower latency.
Each client instance has its own dedicated environment session on the server.

Example:
>>> # Connect to a running server
>>> with MinesweeperEnv(base_url="http://localhost:8000") as client:
... result = client.reset()
... print(result.observation.board)
... print(result.observation.game_status)
...
... # Reveal a cell
... result = client.step(MinesweeperAction(row=0, col=0, action_type="reveal"))
... print(result.observation.board)
... print(result.reward)

Example with Docker:
>>> # Automatically start container and connect
>>> client = MinesweeperEnv.from_docker_image("minesweeper-env:latest")
>>> try:
... result = client.reset()
... result = client.step(MinesweeperAction(row=2, col=3, action_type="reveal"))
... finally:
... client.close()
"""

def _step_payload(self, action: MinesweeperAction) -> Dict:
"""
Convert MinesweeperAction to JSON payload for step request.

Args:
action: MinesweeperAction instance

Returns:
Dictionary representation suitable for JSON encoding
"""
return {
"row": action.row,
"col": action.col,
"action_type": action.action_type,
}

def _parse_result(self, payload: Dict) -> StepResult[MinesweeperObservation]:
"""
Parse server response into StepResult[MinesweeperObservation].

Args:
payload: JSON response from server

Returns:
StepResult with MinesweeperObservation
"""
obs_data = payload.get("observation", {})
observation = MinesweeperObservation(
board=obs_data.get("board", []),
num_mines=obs_data.get("num_mines", 0),
flags_placed=obs_data.get("flags_placed", 0),
cells_revealed=obs_data.get("cells_revealed", 0),
game_status=obs_data.get("game_status", "ongoing"),
done=payload.get("done", False),
reward=payload.get("reward"),
metadata=obs_data.get("metadata", {}),
)

return StepResult(
observation=observation,
reward=payload.get("reward"),
done=payload.get("done", False),
)

def _parse_state(self, payload: Dict) -> State:
"""
Parse server response into State object.

Args:
payload: JSON response from /state endpoint

Returns:
State object with episode_id and step_count
"""
return State(
episode_id=payload.get("episode_id"),
step_count=payload.get("step_count", 0),
)
Loading