| """ |
| Building Environments |
| ===================== |
| |
| **Part 3 of 5** in the OpenEnv Getting Started Series |
| |
| This notebook covers how to create your own OpenEnv environment, package it |
| with Docker, and share it on Hugging Face Hub. |
| |
| .. note:: |
| **Time**: ~20 minutes | **Difficulty**: Intermediate | **GPU Required**: No |
| |
| What You'll Learn |
| ----------------- |
| |
| - **Environment Structure**: The standard OpenEnv project layout |
| - **Defining Models**: Type-safe Action and Observation classes |
| - **Implementing Logic**: The reset() and step() methods |
| - **Docker Packaging**: Containerizing your environment |
| - **Sharing**: Deploying to Hugging Face Hub |
| """ |
|
|
| |
| |
| |
| |
| |
|
|
| import subprocess |
| import sys |
| from pathlib import Path |
|
|
| |
| try: |
| import google.colab |
|
|
| IN_COLAB = True |
| except ImportError: |
| IN_COLAB = False |
|
|
| if IN_COLAB: |
| print("=" * 70) |
| print(" GOOGLE COLAB DETECTED - Installing OpenEnv...") |
| print("=" * 70) |
|
|
| subprocess.run( |
| [sys.executable, "-m", "pip", "install", "-q", "openenv-core"], |
| capture_output=True, |
| ) |
| print(" OpenEnv installed!") |
| print("=" * 70) |
| else: |
| print("=" * 70) |
| print(" RUNNING LOCALLY") |
| print("=" * 70) |
|
|
| |
| src_path = Path.cwd().parent.parent.parent / "src" |
| if src_path.exists(): |
| sys.path.insert(0, str(src_path)) |
| envs_path = Path.cwd().parent.parent.parent / "envs" |
| if envs_path.exists(): |
| sys.path.insert(0, str(envs_path.parent)) |
|
|
| print("=" * 70) |
|
|
| print() |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| print("=" * 70) |
| print(" PREREQUISITES") |
| print("=" * 70) |
|
|
| |
| import platform |
|
|
| python_version = platform.python_version() |
| print(f"\nβ Python version: {python_version}") |
|
|
| |
| try: |
| result = subprocess.run( |
| ["docker", "--version"], capture_output=True, text=True, timeout=5 |
| ) |
| if result.returncode == 0: |
| print(f"β Docker: {result.stdout.strip()}") |
| else: |
| print("β Docker: Not found (required for deployment)") |
| except Exception: |
| print("β Docker: Not found (required for deployment)") |
|
|
| |
| try: |
| result = subprocess.run( |
| ["openenv", "--help"], capture_output=True, text=True, timeout=5 |
| ) |
| if result.returncode == 0: |
| print("β OpenEnv CLI: Available") |
| else: |
| print("β OpenEnv CLI: Not found") |
| except Exception: |
| print("β OpenEnv CLI: Not found (install with: pip install openenv-core)") |
|
|
| print() |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| print("=" * 70) |
| print(" ENVIRONMENT STRUCTURE") |
| print("=" * 70) |
| print() |
|
|
| |
| envs_base = Path.cwd().parent.parent.parent / "envs" |
|
|
| |
| openspiel_path = envs_base / "openspiel_env" |
|
|
| if openspiel_path.exists(): |
| print("Exploring REAL environment structure from envs/openspiel_env/:") |
| print() |
|
|
| def show_tree(path: Path, prefix: str = "", max_depth: int = 2, current_depth: int = 0): |
| """Display directory tree.""" |
| if current_depth > max_depth: |
| return |
|
|
| |
| try: |
| items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name)) |
| except PermissionError: |
| return |
|
|
| |
| items = [i for i in items if not i.name.startswith('.') and i.name != '__pycache__'] |
|
|
| for i, item in enumerate(items): |
| is_last = i == len(items) - 1 |
| connector = "βββ " if is_last else "βββ " |
| print(f"{prefix}{connector}{item.name}{'/' if item.is_dir() else ''}") |
|
|
| if item.is_dir() and current_depth < max_depth: |
| extension = " " if is_last else "β " |
| show_tree(item, prefix + extension, max_depth, current_depth + 1) |
|
|
| show_tree(openspiel_path, " ") |
| print() |
|
|
| |
| print("Key files detected:") |
| key_files = [ |
| ("__init__.py", "Package exports"), |
| ("models.py", "Action & Observation definitions"), |
| ("client.py", "Client for connecting to env"), |
| ("openenv.yaml", "Environment manifest"), |
| ("README.md", "Documentation"), |
| ("server/app.py", "FastAPI server"), |
| ("server/Dockerfile", "Container definition"), |
| ] |
|
|
| for filename, description in key_files: |
| filepath = openspiel_path / filename |
| exists = "β" if filepath.exists() else "β" |
| print(f" {exists} {filename:<25} - {description}") |
|
|
| else: |
| print("Standard OpenEnv environment layout:") |
| print( |
| """ |
| my_game/ |
| βββ __init__.py # Package exports |
| βββ models.py # Action & Observation definitions |
| βββ client.py # Client for connecting to env |
| βββ openenv.yaml # Environment manifest |
| βββ README.md # Documentation |
| βββ server/ |
| βββ __init__.py |
| βββ my_game_environment.py # Core environment logic |
| βββ app.py # FastAPI server |
| βββ Dockerfile # Container definition |
| βββ requirements.txt # Python dependencies |
| """ |
| ) |
|
|
| print() |
| print("Create a new environment with: openenv init my_game") |
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| try: |
| from openenv.core.client_types import StepResult |
|
|
| CORE_IMPORTS_OK = True |
| print("β OpenEnv core imports successful") |
| except ImportError as e: |
| CORE_IMPORTS_OK = False |
| print(f"β Could not import OpenEnv core: {e}") |
|
|
| |
| |
|
|
| from dataclasses import dataclass, field |
| from typing import List, Optional, Dict, Any |
|
|
|
|
| @dataclass |
| class GuessAction: |
| """ |
| Action for the Number Guessing game. |
| |
| The player guesses a number between min_value and max_value. |
| """ |
|
|
| guess: int |
|
|
|
|
| @dataclass |
| class GuessObservation: |
| """ |
| Observation returned after each guess. |
| |
| Contains feedback about the guess and game state. |
| """ |
|
|
| hint: str |
| guesses_remaining: int |
| min_value: int |
| max_value: int |
| done: bool = False |
| reward: float = 0.0 |
|
|
|
|
| @dataclass |
| class GuessState: |
| """ |
| Episode state metadata. |
| """ |
|
|
| episode_id: str |
| step_count: int |
| target_number: int |
| max_guesses: int |
|
|
|
|
| |
| action = GuessAction(guess=50) |
| observation = GuessObservation( |
| hint="too_low", guesses_remaining=5, min_value=1, max_value=100 |
| ) |
| state = GuessState( |
| episode_id="ep_001", step_count=1, target_number=73, max_guesses=7 |
| ) |
|
|
| print("\nExample instances:") |
| print(f" Action: {action}") |
| print(f" Observation: {observation}") |
| print(f" State: {state}") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import random |
| import uuid |
| from abc import ABC, abstractmethod |
|
|
|
|
| class NumberGuessingEnvironment: |
| """ |
| A simple number guessing game environment. |
| |
| The environment picks a random number, and the agent tries to guess it. |
| Feedback is given after each guess ("too_low", "too_high", "correct"). |
| """ |
|
|
| def __init__(self, min_value: int = 1, max_value: int = 100, max_guesses: int = 7): |
| """ |
| Initialize the environment. |
| |
| Args: |
| min_value: Minimum possible target value |
| max_value: Maximum possible target value |
| max_guesses: Maximum guesses allowed per episode |
| """ |
| self.min_value = min_value |
| self.max_value = max_value |
| self.max_guesses = max_guesses |
|
|
| |
| self._target: Optional[int] = None |
| self._guesses_remaining: int = 0 |
| self._step_count: int = 0 |
| self._episode_id: Optional[str] = None |
|
|
| def reset(self, seed: Optional[int] = None) -> GuessObservation: |
| """ |
| Start a new episode. |
| |
| Args: |
| seed: Optional random seed for reproducibility |
| |
| Returns: |
| Initial observation for the new episode |
| """ |
| if seed is not None: |
| random.seed(seed) |
|
|
| |
| self._target = random.randint(self.min_value, self.max_value) |
| self._guesses_remaining = self.max_guesses |
| self._step_count = 0 |
| self._episode_id = str(uuid.uuid4())[:8] |
|
|
| return GuessObservation( |
| hint="game_started", |
| guesses_remaining=self._guesses_remaining, |
| min_value=self.min_value, |
| max_value=self.max_value, |
| done=False, |
| reward=0.0, |
| ) |
|
|
| def step(self, action: GuessAction) -> GuessObservation: |
| """ |
| Process a guess and return the result. |
| |
| Args: |
| action: The player's guess |
| |
| Returns: |
| Observation with hint and game state |
| """ |
| self._step_count += 1 |
| self._guesses_remaining -= 1 |
|
|
| guess = action.guess |
|
|
| |
| if guess == self._target: |
| hint = "correct" |
| reward = 1.0 |
| done = True |
| elif guess < self._target: |
| hint = "too_low" |
| reward = 0.0 |
| done = self._guesses_remaining <= 0 |
| else: |
| hint = "too_high" |
| reward = 0.0 |
| done = self._guesses_remaining <= 0 |
|
|
| |
| if done and hint != "correct": |
| reward = -0.5 |
|
|
| return GuessObservation( |
| hint=hint, |
| guesses_remaining=self._guesses_remaining, |
| min_value=self.min_value, |
| max_value=self.max_value, |
| done=done, |
| reward=reward, |
| ) |
|
|
| @property |
| def state(self) -> GuessState: |
| """Get current episode state.""" |
| return GuessState( |
| episode_id=self._episode_id or "", |
| step_count=self._step_count, |
| target_number=self._target or 0, |
| max_guesses=self.max_guesses, |
| ) |
|
|
|
|
| print("=" * 70) |
| print(" ENVIRONMENT IMPLEMENTATION") |
| print("=" * 70) |
|
|
| |
| import inspect |
|
|
| print("\nNumberGuessingEnvironment class defined above with these methods:") |
| print() |
|
|
| for name, method in inspect.getmembers(NumberGuessingEnvironment, predicate=inspect.isfunction): |
| if not name.startswith('_'): |
| sig = inspect.signature(method) |
| print(f" β’ {name}{sig}") |
| if method.__doc__: |
| first_line = method.__doc__.strip().split('\n')[0] |
| print(f" {first_line}") |
|
|
| |
| for name, prop in inspect.getmembers(NumberGuessingEnvironment, lambda x: isinstance(x, property)): |
| print(f" β’ {name} (property)") |
| if prop.fget and prop.fget.__doc__: |
| first_line = prop.fget.__doc__.strip().split('\n')[0] |
| print(f" {first_line}") |
|
|
| |
| |
| |
| |
| |
|
|
| print("=" * 70) |
| print(" LOCAL TESTING") |
| print("=" * 70) |
|
|
| |
| env = NumberGuessingEnvironment(min_value=1, max_value=100, max_guesses=7) |
|
|
| |
| obs = env.reset(seed=42) |
| print(f"\nNew episode started!") |
| print(f" Hint: {obs.hint}") |
| print(f" Guesses remaining: {obs.guesses_remaining}") |
| print(f" Range: {obs.min_value} - {obs.max_value}") |
| print(f" (Secret target: {env.state.target_number})") |
|
|
| |
| low, high = obs.min_value, obs.max_value |
| step = 0 |
|
|
| print(f"\nPlaying with binary search strategy:") |
| print("-" * 50) |
|
|
| while not obs.done: |
| |
| guess = (low + high) // 2 |
| action = GuessAction(guess=guess) |
| obs = env.step(action) |
| step += 1 |
|
|
| print(f" Step {step}: Guessed {guess} -> {obs.hint}", end="") |
| if obs.done: |
| print(f" (Reward: {obs.reward})") |
| else: |
| print(f" (Remaining: {obs.guesses_remaining})") |
|
|
| |
| if obs.hint == "too_low": |
| low = guess + 1 |
| elif obs.hint == "too_high": |
| high = guess - 1 |
|
|
| print(f"\nEpisode complete!") |
| print(f" Total steps: {env.state.step_count}") |
| print(f" Result: {'Won!' if obs.reward > 0 else 'Lost!'}") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|