From 0f234cf88821fefd77b5ae3195a07662a65e46a8 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:43:26 -0800 Subject: [PATCH 01/15] add example --- examples/nle_random_agent.py | 126 +++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/nle_random_agent.py diff --git a/examples/nle_random_agent.py b/examples/nle_random_agent.py new file mode 100644 index 00000000..587c74ff --- /dev/null +++ b/examples/nle_random_agent.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +Example: Random Agent Playing NetHack via OpenEnv + +This script demonstrates how to use the NLE environment through OpenEnv's +HTTP interface. It runs a random agent for a few episodes. + +Prerequisites: + 1. Build the Docker image: + cd src/envs/nle_env/server + docker build -t nle-env:latest . + + 2. Run this script: + python examples/nle_random_agent.py +""" + +import random +import time + +# Add src to path if running directly +import sys +from pathlib import Path + +src_path = Path(__file__).parent.parent / "src" +sys.path.insert(0, str(src_path)) + +from envs.nle_env import NLEEnv, NLEAction + + +def print_stats(observation): + """Print human-readable stats from observation.""" + if observation.blstats is None: + return + + blstats = observation.blstats + # BLstats indices from NLE documentation + print(f" HP: {blstats[10]}/{blstats[11]}") + print(f" XP Level: {blstats[18]}") + print(f" Gold: {blstats[13]}") + print(f" Dungeon Level: {blstats[12]}") + + +def main(): + print("=" * 70) + print("NLE Random Agent Example") + print("=" * 70) + + # Start environment (automatically launches Docker container) + print("\n[1/3] Starting NLE environment...") + print("(This may take a moment if container needs to start)") + + env = NLEEnv.from_docker_image( + "nle-env:latest", + # Optional: customize container + # env_vars={"NLE_MAX_STEPS": "1000"} + ) + + print("✓ Environment connected!") + + # Run a few episodes + num_episodes = 3 + max_steps_per_episode = 100 + + print(f"\n[2/3] Running {num_episodes} episodes...") + + for episode in range(num_episodes): + print(f"\n--- Episode {episode + 1}/{num_episodes} ---") + + # Reset environment + result = env.reset() + print("Environment reset") + print_stats(result.observation) + + episode_reward = 0 + steps = 0 + + # Play episode + for step in range(max_steps_per_episode): + # Random action (0-112) + action = NLEAction(action_id=random.randint(0, 112)) + + # Take step + result = env.step(action) + + episode_reward += result.reward or 0 + steps += 1 + + # Print occasional updates + if step % 20 == 0: + print(f" Step {step}: reward={episode_reward:.1f}") + + # Check if done + if result.done: + state = env.state() + print(f"\nEpisode ended after {steps} steps!") + print(f" Total reward: {episode_reward:.1f}") + print(f" End status: {state.end_status}") + print(f" Final stats:") + print_stats(result.observation) + break + else: + print(f"\nReached max steps ({max_steps_per_episode})") + print(f" Total reward: {episode_reward:.1f}") + + time.sleep(0.5) # Brief pause between episodes + + # Cleanup + print("\n[3/3] Cleaning up...") + env.close() + print("✓ Environment closed") + + print("\n" + "=" * 70) + print("Example complete!") + print("=" * 70) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\nInterrupted by user") + except Exception as e: + print(f"\n\nError: {e}") + import traceback + + traceback.print_exc() From 1f5437a2162266d42ed60e3cb74ade1107ee0ae1 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:44:08 -0800 Subject: [PATCH 02/15] Create __init__.py --- nle_env/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 nle_env/__init__.py diff --git a/nle_env/__init__.py b/nle_env/__init__.py new file mode 100644 index 00000000..eaf85e0e --- /dev/null +++ b/nle_env/__init__.py @@ -0,0 +1,12 @@ +# 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. + +"""NetHack Learning Environment - OpenEnv integration.""" + +from .client import NLEEnv +from .models import NLEAction, NLEObservation, NLEState + +__all__ = ["NLEAction", "NLEObservation", "NLEState", "NLEEnv"] From 9fb20ddd755dcfcc0685ae11a865f83266cf3995 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:44:15 -0800 Subject: [PATCH 03/15] Create client.py --- nle_env/client.py | 147 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 nle_env/client.py diff --git a/nle_env/client.py b/nle_env/client.py new file mode 100644 index 00000000..dee5c1d1 --- /dev/null +++ b/nle_env/client.py @@ -0,0 +1,147 @@ +# 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. + +""" +NetHack Learning Environment HTTP Client. + +This module provides the client for connecting to an NLE Environment server +over HTTP. +""" + +from typing import Dict + +from core.client_types import StepResult +from core.http_env_client import HTTPEnvClient + +from .models import NLEAction, NLEObservation, NLEState + + +class NLEEnv(HTTPEnvClient[NLEAction, NLEObservation]): + """ + HTTP client for the NetHack Learning Environment. + + This client connects to an NLEEnvironment HTTP server and provides + methods to interact with NetHack: reset(), step(), and state access. + + With beefy compute, we use simple JSON serialization. The server sends + all observation arrays as nested lists, which we keep as-is or convert + back to numpy arrays as needed. + + Example: + >>> # Connect to a running server + >>> client = NLEEnv(base_url="http://localhost:8000") + >>> result = client.reset() + >>> print(result.observation.blstats) # [HP, MaxHP, ...] + >>> + >>> # Take a step (move north) + >>> result = client.step(NLEAction(action_id=0)) + >>> print(result.reward) + >>> print(result.done) + + Example with Docker: + >>> # Automatically start container and connect + >>> client = NLEEnv.from_docker_image("nle-env:latest") + >>> result = client.reset() + >>> + >>> # Play NetHack! + >>> for _ in range(100): + ... action = NLEAction(action_id=random.randint(0, 112)) + ... result = client.step(action) + ... if result.done: + ... break + """ + + def _step_payload(self, action: NLEAction) -> Dict: + """ + Convert NLEAction to JSON payload for step request. + + Args: + action: NLEAction instance with action_id + + Returns: + Dictionary representation suitable for JSON encoding + """ + return { + "action_id": action.action_id, + } + + def _parse_result(self, payload: Dict) -> StepResult[NLEObservation]: + """ + Parse server response into StepResult[NLEObservation]. + + The server sends all arrays as nested lists. With beefy compute, + we just keep them as lists - no need to convert back to numpy + unless the user specifically needs it. + + Args: + payload: JSON response from server + + Returns: + StepResult with NLEObservation + """ + obs_data = payload.get("observation", {}) + + # Extract standard fields + done = obs_data.get("done", False) + reward = obs_data.get("reward") + metadata = obs_data.get("metadata", {}) + + # Build observation with all the array fields + # Keep them as lists - simple and works great with beefy compute + observation = NLEObservation( + # Core observations + glyphs=obs_data.get("glyphs"), + blstats=obs_data.get("blstats"), + message=obs_data.get("message"), + # Visual observations + chars=obs_data.get("chars"), + colors=obs_data.get("colors"), + specials=obs_data.get("specials"), + # Inventory observations + inv_glyphs=obs_data.get("inv_glyphs"), + inv_strs=obs_data.get("inv_strs"), + inv_letters=obs_data.get("inv_letters"), + inv_oclasses=obs_data.get("inv_oclasses"), + # Terminal observations + tty_chars=obs_data.get("tty_chars"), + tty_colors=obs_data.get("tty_colors"), + tty_cursor=obs_data.get("tty_cursor"), + # Extended observations + screen_descriptions=obs_data.get("screen_descriptions"), + program_state=obs_data.get("program_state"), + internal=obs_data.get("internal"), + misc=obs_data.get("misc"), + # Standard fields + done=done, + reward=reward, + metadata=metadata, + ) + + return StepResult( + observation=observation, + reward=reward, + done=done, + ) + + def _parse_state(self, payload: Dict) -> NLEState: + """ + Parse server response into NLEState object. + + Args: + payload: JSON response from /state endpoint + + Returns: + NLEState object with episode and game information + """ + return NLEState( + episode_id=payload.get("episode_id"), + step_count=payload.get("step_count", 0), + game_over=payload.get("game_over", False), + end_status=payload.get("end_status", "RUNNING"), + in_normal_game=payload.get("in_normal_game", False), + character=payload.get("character", "mon-hum-neu-mal"), + task_name=payload.get("task_name", "NetHackScore-v0"), + ) From 1f1aa3098c87f44a0848f3a63497f4a939236122 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:44:36 -0800 Subject: [PATCH 04/15] Create models.py --- nle_env/models.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 nle_env/models.py diff --git a/nle_env/models.py b/nle_env/models.py new file mode 100644 index 00000000..d1ae87c7 --- /dev/null +++ b/nle_env/models.py @@ -0,0 +1,110 @@ +# 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. + +""" +Data models for the NetHack Learning Environment (NLE). + +The NLE environment wraps the NetHack 3.6.6 game as a reinforcement learning +environment, providing rich observations and a complex action space. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from core.env_server import Action, Observation, State + + +@dataclass +class NLEAction(Action): + """ + Action for the NetHack Learning Environment. + + Uses discrete action space where action_id maps to NetHack commands + (movement, interactions, etc.). The action space has ~113 actions. + + Examples: + - action_id=0: Move North (k) + - action_id=1: Move East (l) + - action_id=37: Eat (e) + - action_id=50: Search (s) + """ + + action_id: int # Index into nethack.USEFUL_ACTIONS (0-112) + + +@dataclass +class NLEObservation(Observation): + """ + Observation from the NetHack Learning Environment. + + Contains a subset of NLE's 14+ observation types. All numpy arrays are + serialized as nested lists for JSON compatibility. + + Observation types (all optional, configured at env creation): + - glyphs: (21, 79) - Symbolic dungeon map representation + - chars: (21, 79) - ASCII character display + - colors: (21, 79) - Color codes for display + - specials: (21, 79) - Special attributes + - blstats: (26,) - Bottom-line stats (HP, XP, gold, etc.) + - message: (256,) - Game message as byte array + - inv_glyphs: (55,) - Inventory item glyphs + - inv_strs: (55, 80) - Inventory item descriptions + - inv_letters: (55,) - Inventory item letters (a-z, A-Z) + - inv_oclasses: (55,) - Inventory object classes + - tty_chars: (24, 80) - Full terminal character display + - tty_colors: (24, 80) - Full terminal colors + - tty_cursor: (2,) - Terminal cursor position [row, col] + - screen_descriptions: (21, 79, 80) - Text descriptions of dungeon + + With beefy compute, we include all observations by default. + """ + + # Core observations (always useful) + glyphs: Optional[List[List[int]]] = None + blstats: Optional[List[int]] = None + message: Optional[List[int]] = None + + # Visual observations + chars: Optional[List[List[int]]] = None + colors: Optional[List[List[int]]] = None + specials: Optional[List[List[int]]] = None + + # Inventory observations + inv_glyphs: Optional[List[int]] = None + inv_strs: Optional[List[List[int]]] = None + inv_letters: Optional[List[int]] = None + inv_oclasses: Optional[List[int]] = None + + # Terminal observations (for rendering) + tty_chars: Optional[List[List[int]]] = None + tty_colors: Optional[List[List[int]]] = None + tty_cursor: Optional[List[int]] = None + + # Extended observations + screen_descriptions: Optional[List[List[List[int]]]] = None + program_state: Optional[List[int]] = None + internal: Optional[List[int]] = None + misc: Optional[List[int]] = None + + +@dataclass +class NLEState(State): + """ + Extended state for the NLE environment. + + Includes NetHack-specific state information beyond basic episode tracking. + """ + + # NLE-specific state + game_over: bool = False + end_status: str = "RUNNING" # RUNNING, DEATH, TASK_SUCCESSFUL, ABORTED + in_normal_game: bool = False + character: str = "mon-hum-neu-mal" # role-race-gender-alignment + + # Task-specific info + task_name: str = "NetHackScore-v0" From 41c5203823b2bf3f0b2b242627945c59110a2c2a Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:44:54 -0800 Subject: [PATCH 05/15] Create README.md --- nle_env/README.md | 316 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 nle_env/README.md diff --git a/nle_env/README.md b/nle_env/README.md new file mode 100644 index 00000000..667532fe --- /dev/null +++ b/nle_env/README.md @@ -0,0 +1,316 @@ +# NetHack Learning Environment (NLE) for OpenEnv + +A reinforcement learning environment based on NetHack 3.6.6, wrapped for the OpenEnv framework. + +## Overview + +NetHack is one of the oldest and most challenging roguelike games, featuring: +- **Procedurally generated dungeons** - Every episode is unique +- **Complex action space** - 113+ discrete actions (movement, combat, magic, inventory management) +- **Rich observation space** - 14+ observation types including dungeon map, stats, inventory, messages +- **Challenging gameplay** - One of the hardest RL benchmarks available +- **Deterministic (with seeding)** - Reproducible episodes for evaluation + +This environment wraps the [NetHack Learning Environment (NLE)](https://github.com/facebookresearch/nle) project, which provides a Gym interface to NetHack. + +## Quick Start + +### Using Docker (Recommended) + +```python +from envs.nle_env import NLEEnv, NLEAction + +# Automatically start container and connect +env = NLEEnv.from_docker_image("nle-env:latest") + +# Reset to start a new game +result = env.reset() +print(f"Episode started: {result.observation.message}") + +# Take actions in the game +for step in range(100): + # Action IDs: 0-112 (movement, commands, etc.) + action = NLEAction(action_id=0) # Move north + result = env.step(action) + + print(f"Step {step}: Reward={result.reward}, Done={result.done}") + + if result.done: + print("Episode ended!") + break + +env.close() +``` + +### Building the Docker Image + +```bash +# Build from repository root (not from server directory) +cd /Users/sanyambhutani/GH/OpenEnv +docker build -f src/envs/nle_env/server/Dockerfile -t nle-env:latest . +``` + +**Note:** Building NLE from source can take 5-10 minutes as it compiles NetHack C code. + +### Running the Server Locally + +```bash +# Install NLE (requires cmake, build-essential) +pip install nle gym + +# Run the server +python -m envs.nle_env.server.app + +# Server will be available at http://localhost:8000 +``` + +## Action Space + +NLE uses a discrete action space with 113 actions: + +| Action ID Range | Category | Examples | +|----------------|----------|----------| +| 0-7 | Cardinal movement | North, South, East, West | +| 8-15 | Diagonal movement | NE, SE, SW, NW | +| 16-20 | Stair navigation | Up, Down | +| 21-112 | Commands | Eat, Search, Apply, Quaff, Read, etc. | + +Common actions: +```python +# Movement +NLEAction(action_id=0) # Move north (k) +NLEAction(action_id=1) # Move east (l) +NLEAction(action_id=2) # Move south (j) +NLEAction(action_id=3) # Move west (h) + +# Interactions +NLEAction(action_id=37) # Eat (e) +NLEAction(action_id=50) # Search (s) +NLEAction(action_id=104) # Inventory (i) +NLEAction(action_id=86) # Wait (.) +``` + +For a complete action mapping, see [NLE Actions Documentation](https://github.com/facebookresearch/nle/blob/main/nle/nethack/actions.py). + +## Observation Space + +NLE provides rich observations about the game state. With OpenEnv's beefy compute assumption, all observations are included by default: + +### Core Observations +- **glyphs** `(21, 79)`: Symbolic dungeon map representation +- **blstats** `(26,)`: Bottom-line stats (HP, MaxHP, XP, Gold, etc.) +- **message** `(256,)`: Latest game message as byte array + +### Visual Observations +- **chars** `(21, 79)`: ASCII character display +- **colors** `(21, 79)`: Color codes for display +- **specials** `(21, 79)`: Special attributes (bold, inverse, etc.) + +### Inventory Observations +- **inv_glyphs** `(55,)`: Inventory item glyphs +- **inv_strs** `(55, 80)`: Inventory item descriptions +- **inv_letters** `(55,)`: Inventory item letters (a-z, A-Z) +- **inv_oclasses** `(55,)`: Inventory object classes + +### Terminal Observations (for rendering) +- **tty_chars** `(24, 80)`: Full terminal character display +- **tty_colors** `(24, 80)`: Full terminal colors +- **tty_cursor** `(2,)`: Terminal cursor position [row, col] + +### Extended Observations +- **screen_descriptions** `(21, 79, 80)`: Text descriptions of dungeon cells +- **program_state** `(6,)`: Internal program state +- **internal** `(9,)`: Internal game state +- **misc** `(4,)`: Miscellaneous info + +All observations are serialized as nested lists (converted from numpy arrays) for JSON compatibility. + +## Reward Structure + +By default, NLE uses **score delta** as the reward: +``` +reward = current_score - previous_score +``` + +Score increases by: +- Defeating monsters +- Collecting gold +- Advancing to deeper dungeon levels +- Finding items +- Gaining experience points + +## Episode Termination + +Episodes end when: +1. **Death** - Character dies (most common) +2. **Ascension** - Player completes the game (very rare!) +3. **Aborted** - Max episode steps reached (default: 5000) +4. **Task Successful** - For task-specific environments + +Check the end status: +```python +result = env.step(action) +if result.done: + state = env.state() + print(f"End status: {state.end_status}") + # Possible values: RUNNING, DEATH, TASK_SUCCESSFUL, ABORTED +``` + +## Configuration + +Configure the environment via environment variables or Docker args: + +```bash +# Task variant (default: score) +export NLE_TASK=score + +# Character (role-race-gender-alignment) +export NLE_CHARACTER=mon-hum-neu-mal + +# Max episode steps (default: 5000) +export NLE_MAX_STEPS=10000 +``` + +### Character Options + +Format: `role-race-gender-alignment` + +**Roles:** Archaeologist (arc), Barbarian (bar), Caveman (cav), Healer (hea), Knight (kni), Monk (mon), Priest (pri), Ranger (ran), Rogue (rog), Samurai (sam), Tourist (tou), Valkyrie (val), Wizard (wiz) + +**Races:** Human (hum), Dwarf (dwa), Elf (elf), Gnome (gno), Orc (orc) + +**Genders:** Male (mal), Female (fem) + +**Alignments:** Lawful (law), Neutral (neu), Chaotic (cha) + +Example: `wiz-elf-fem-cha` = Female Elven Chaotic Wizard + +## Example: Random Agent + +```python +import random +from envs.nle_env import NLEEnv, NLEAction + +env = NLEEnv.from_docker_image("nle-env:latest") + +episodes = 10 +for episode in range(episodes): + result = env.reset() + total_reward = 0 + + while True: + # Random action + action = NLEAction(action_id=random.randint(0, 112)) + result = env.step(action) + + total_reward += result.reward or 0 + + if result.done: + state = env.state() + print(f"Episode {episode}: Reward={total_reward:.1f}, " + f"Steps={state.step_count}, Status={state.end_status}") + break + +env.close() +``` + +## Example: Rendering Game State + +```python +import numpy as np +from envs.nle_env import NLEEnv, NLEAction + +env = NLEEnv.from_docker_image("nle-env:latest") +result = env.reset() + +# Get terminal display +tty_chars = np.array(result.observation.tty_chars) +tty_colors = np.array(result.observation.tty_colors) + +# Print ASCII display +for row in tty_chars: + print(''.join(chr(c) for c in row)) + +# Get game message +message = bytes(result.observation.message) +print(f"Message: {message[:message.index(b'\\0')].decode('ascii')}") + +# Get stats +blstats = result.observation.blstats +print(f"HP: {blstats[10]}/{blstats[11]}, Gold: {blstats[13]}, " + f"XP Level: {blstats[18]}") + +env.close() +``` + +## Performance Considerations + +With **beefy compute** (64+ cores, 256GB+ RAM, 10Gbps network): +- Observation size: ~140KB per step (all observation types) +- Network overhead: Negligible (<1ms on fast network) +- Memory: ~200-500MB per container +- Throughput: 100+ parallel environments easily + +**Optimizations are NOT needed** - just run it simple with JSON serialization! + +## Task Variants (Future) + +Current implementation: **NetHackScore** (maximize game score) + +Planned task variants: +- **NetHackStaircase** - Reach the stairs down +- **NetHackOracle** - Find the Oracle +- **NetHackGold** - Collect gold +- **NetHackEat** - Maximize hunger satisfaction +- **NetHackScout** - Maximize exploration + +## Troubleshooting + +### Build Issues + +If Docker build fails with cmake errors: +```bash +# Ensure cmake is recent enough (3.15+) +cmake --version +``` + +### Container Won't Start + +Check logs: +```bash +docker logs +``` + +Common issues: +- NLE compilation failed → Check cmake, build-essential installed +- Import errors → Check PYTHONPATH set correctly +- Port already in use → Use different port mapping + +### Slow Performance + +If you experience slowness even with beefy compute: +1. Check network latency: `ping ` +2. Monitor CPU: NLE is CPU-intensive for dungeon generation +3. Check Docker resources: Ensure containers have sufficient CPU allocation + +## References + +- [NLE GitHub](https://github.com/facebookresearch/nle) +- [NLE Paper (NeurIPS 2020)](https://arxiv.org/abs/2006.13760) +- [NetHack Wiki](https://nethackwiki.com) +- [NetHack Official Site](https://nethack.org) + +## Citation + +If you use NLE in your research, please cite: + +```bibtex +@inproceedings{kuettler2020nethack, + title={The NetHack Learning Environment}, + author={K{\"u}ttler, Heinrich and Nardelli, Nantas and Miller, Alexander H and + Raileanu, Roberta and Selvatici, Marco and Grefenstette, Edward and + Rockt{\"a}schel, Tim}, + booktitle={Proceedings of NeurIPS}, + year={2020} +} +``` From 73b5687d1379c7064338048c6df26a4d7857d5eb Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:45:25 -0800 Subject: [PATCH 06/15] Create __init__.py --- nle_env/server/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 nle_env/server/__init__.py diff --git a/nle_env/server/__init__.py b/nle_env/server/__init__.py new file mode 100644 index 00000000..f9087cf5 --- /dev/null +++ b/nle_env/server/__init__.py @@ -0,0 +1,7 @@ +# 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. + +"""Server module for NLE environment.""" From ea73ebecdde8d196222dfbb0f71db1fd744de13c Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:45:29 -0800 Subject: [PATCH 07/15] Create app.py --- nle_env/server/app.py | 54 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 nle_env/server/app.py diff --git a/nle_env/server/app.py b/nle_env/server/app.py new file mode 100644 index 00000000..19fc69f4 --- /dev/null +++ b/nle_env/server/app.py @@ -0,0 +1,54 @@ +# 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. + +""" +FastAPI application for the NetHack Learning Environment. + +This module creates an HTTP server that exposes the NLE environment +over HTTP endpoints, making it compatible with HTTPEnvClient. + +Usage: + # Development (with auto-reload): + uvicorn envs.nle_env.server.app:app --reload --host 0.0.0.0 --port 8000 + + # Production: + uvicorn envs.nle_env.server.app:app --host 0.0.0.0 --port 8000 --workers 1 + + # Or run directly: + python -m envs.nle_env.server.app + +Note: + NLE is single-threaded (uses C extension with global state), so workers=1 +""" + +import os + +from core.env_server.http_server import create_app + +from ..models import NLEAction, NLEObservation +from .nle_environment import NLEEnvironment + +# Read configuration from environment variables +TASK_NAME = os.getenv("NLE_TASK", "score") +CHARACTER = os.getenv("NLE_CHARACTER", "mon-hum-neu-mal") +MAX_STEPS = int(os.getenv("NLE_MAX_STEPS", "5000")) + +# Create the environment instance +env = NLEEnvironment( + task_name=TASK_NAME, + character=CHARACTER, + max_episode_steps=MAX_STEPS, +) + +# Create the app with web interface and README integration +app = create_app(env, NLEAction, NLEObservation, env_name="nle_env") + + +if __name__ == "__main__": + import uvicorn + + # NLE must run single-threaded (workers=1) due to C extension + uvicorn.run(app, host="0.0.0.0", port=8000, workers=1) From 92e482e7a5da267b877db5b05aa584a9867733ff Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:45:32 -0800 Subject: [PATCH 08/15] Create Dockerfile --- nle_env/server/Dockerfile | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 nle_env/server/Dockerfile diff --git a/nle_env/server/Dockerfile b/nle_env/server/Dockerfile new file mode 100644 index 00000000..8d965a00 --- /dev/null +++ b/nle_env/server/Dockerfile @@ -0,0 +1,60 @@ +# 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. + +# NLE requires build dependencies, so we start from a base image with build tools +# Using Python 3.11 for kw_only dataclass support (required by OpenEnv core) +FROM python:3.11-slim + +# Install system dependencies needed for NLE +RUN apt-get update && apt-get install -y \ + build-essential \ + cmake \ + git \ + libbz2-dev \ + flex \ + bison \ + libncurses5-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Install Python dependencies +# NLE requires cmake to be available during installation +RUN pip install --no-cache-dir \ + fastapi \ + uvicorn \ + requests \ + pydantic \ + numpy + +# Install NLE (this will compile NetHack from source) +# Using the stable version from PyPI +RUN pip install --no-cache-dir nle gym + +# Copy OpenEnv core +COPY src/core/ /app/src/core/ + +# Copy NLE environment implementation +COPY src/envs/nle_env/ /app/src/envs/nle_env/ + +# Copy README for web interface documentation +COPY src/envs/nle_env/README.md /app/README.md + +# Set PYTHONPATH so imports work +ENV PYTHONPATH=/app/src + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run the FastAPI server +# Note: workers=1 because NLE uses C extension with global state +CMD ["uvicorn", "envs.nle_env.server.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"] From 60fac832963989260ad5cc9ecea9300ed9343ca2 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:45:36 -0800 Subject: [PATCH 09/15] Create nle_environment.py --- nle_env/server/nle_environment.py | 241 ++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 nle_env/server/nle_environment.py diff --git a/nle_env/server/nle_environment.py b/nle_env/server/nle_environment.py new file mode 100644 index 00000000..9839b0de --- /dev/null +++ b/nle_env/server/nle_environment.py @@ -0,0 +1,241 @@ +# 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. + +""" +NetHack Learning Environment Implementation. + +This module wraps the NLE (NetHack Learning Environment) as an OpenEnv +environment, providing HTTP-based access to NetHack for RL training. +""" + +import time +from typing import Optional + +from core.env_server.interfaces import Environment, Transform + +from ..models import NLEAction, NLEObservation, NLEState + +# Import NLE - will be installed in Docker +try: + from nle.env import NLE +except ImportError: + NLE = None # type: ignore + + +class NLEEnvironment(Environment): + """ + OpenEnv wrapper for the NetHack Learning Environment. + + This environment wraps NLE's gym interface and provides OpenEnv-compatible + reset(), step(), and state access. + + With beefy compute, we use simple JSON serialization and include all + observation types by default. No optimization needed - compute handles it. + + Example: + >>> env = NLEEnvironment() + >>> obs = env.reset() + >>> print(obs.reward) # 0.0 + >>> + >>> obs = env.step(NLEAction(action_id=0)) # Move north + >>> print(obs.reward) # Score delta + >>> print(env.state.step_count) # 1 + """ + + def __init__( + self, + task_name: str = "score", + character: str = "mon-hum-neu-mal", + max_episode_steps: int = 5000, + observation_keys: tuple = ( + "glyphs", + "chars", + "colors", + "specials", + "blstats", + "message", + "inv_glyphs", + "inv_strs", + "inv_letters", + "inv_oclasses", + "tty_chars", + "tty_colors", + "tty_cursor", + ), + transform: Optional[Transform] = None, + ): + """ + Initialize the NLE environment. + + Args: + task_name: Task variant (score, staircase, oracle, gold, etc.) + character: Character definition (role-race-gender-alignment) + max_episode_steps: Maximum steps before episode is aborted + observation_keys: Which observations to include + transform: Optional observation transform + """ + super().__init__(transform=transform) + + if NLE is None: + raise ImportError( + "NLE is not installed. Install with: pip install nle\n" + "For Docker builds, this will be installed automatically." + ) + + self._task_name = task_name + self._character = character + self._observation_keys = observation_keys + + # Create NLE gym environment + # With beefy compute: no ttyrec saving, all observations enabled + self.nle_env = NLE( + character=character, + observation_keys=observation_keys, + max_episode_steps=max_episode_steps, + save_ttyrec_every=0, # Disable by default (can enable via env var) + wizard=False, # Can enable via env var for debugging + spawn_monsters=True, + ) + + # Episode tracking + self._episode_id: Optional[str] = None + self._step_count = 0 + self._last_reward = 0.0 + self._last_done = False + self._end_status = "RUNNING" + self._in_normal_game = False + + def reset(self) -> NLEObservation: + """ + Reset the environment and return initial observation. + + Returns: + NLEObservation with initial game state + """ + # Reset NLE gym env + # Note: Gym 0.26+ returns (obs, info) tuple from reset() + reset_result = self.nle_env.reset() + + # Handle both old gym API (returns obs dict) and new API (returns tuple) + if isinstance(reset_result, tuple): + gym_obs, _ = reset_result # Unpack (observation, info) + else: + gym_obs = reset_result # Old API + + # Initialize episode tracking + self._episode_id = f"nle_{int(time.time() * 1000000)}" + self._step_count = 0 + self._last_reward = 0.0 + self._last_done = False + self._end_status = "RUNNING" + self._in_normal_game = self.nle_env.nethack.in_normal_game() + + # Convert gym observation to OpenEnv observation + obs = self._convert_observation(gym_obs, reward=0.0, done=False) + + return self._apply_transform(obs) + + def step(self, action: NLEAction) -> NLEObservation: # type: ignore[override] + """ + Execute action in NetHack and return observation. + + Args: + action: NLEAction with action_id (0-112) + + Returns: + NLEObservation with game state after action + """ + # Execute action in NLE + # Note: Gym 0.26+ returns (obs, reward, terminated, truncated, info) + # Older gym returns (obs, reward, done, info) + step_result = self.nle_env.step(action.action_id) + + # Handle both old and new gym APIs + if len(step_result) == 5: + # New gym API (0.26+): (obs, reward, terminated, truncated, info) + gym_obs, reward, terminated, truncated, info = step_result + done = terminated or truncated + elif len(step_result) == 4: + # Old gym API: (obs, reward, done, info) + gym_obs, reward, done, info = step_result + else: + raise ValueError(f"Unexpected step result length: {len(step_result)}") + + # Update tracking + self._step_count += 1 + self._last_reward = float(reward) + self._last_done = bool(done) + self._end_status = str(info.get("end_status", "RUNNING")) + self._in_normal_game = self.nle_env.nethack.in_normal_game() + + # Convert observation + obs = self._convert_observation(gym_obs, reward=reward, done=done) + + # Add metadata from NLE + obs.metadata.update( + { + "end_status": self._end_status, + "is_ascended": info.get("is_ascended", False), + } + ) + + return self._apply_transform(obs) + + @property + def state(self) -> NLEState: + """ + Get current environment state. + + Returns: + NLEState with episode and game information + """ + return NLEState( + episode_id=self._episode_id, + step_count=self._step_count, + game_over=self._last_done, + end_status=self._end_status, + in_normal_game=self._in_normal_game, + character=self._character, + task_name=self._task_name, + ) + + def _convert_observation( + self, gym_obs: dict, reward: float, done: bool + ) -> NLEObservation: + """ + Convert NLE gym observation to NLEObservation. + + With beefy compute, we just convert numpy arrays to lists. + No compression, no optimization - simplicity first. + + Args: + gym_obs: Dictionary from NLE gym env + reward: Reward for this step + done: Whether episode is done + + Returns: + NLEObservation with serialized arrays + """ + obs_dict = { + "reward": float(reward), + "done": bool(done), + "metadata": {}, + } + + # Convert each observation type from numpy array to nested list + # This is simple and works perfectly with JSON + beefy compute + for key in self._observation_keys: + if key in gym_obs: + array = gym_obs[key] + # Convert numpy array to nested list for JSON serialization + obs_dict[key] = array.tolist() + + return NLEObservation(**obs_dict) + + def close(self): + """Clean up NLE environment.""" + if hasattr(self, "nle_env"): + self.nle_env.close() From 0867881341d98af45a2a098bbf8f71b09ccbd132 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:45:56 -0800 Subject: [PATCH 10/15] Create test_integration.py --- nle_env/test_integration.py | 111 ++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 nle_env/test_integration.py diff --git a/nle_env/test_integration.py b/nle_env/test_integration.py new file mode 100644 index 00000000..3d631103 --- /dev/null +++ b/nle_env/test_integration.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Basic integration test for NLE environment. + +This script tests that the NLE environment can be imported and basic +structure is correct. Does NOT require NLE to be installed (that happens +in Docker). +""" + +import sys +from pathlib import Path + +# Add src to path +src_path = Path(__file__).parent.parent.parent.parent / "src" +sys.path.insert(0, str(src_path)) + +print("=" * 70) +print("NLE Environment Integration Test") +print("=" * 70) + +# Test 1: Import models +print("\n[1/5] Testing model imports...") +try: + from envs.nle_env import NLEAction, NLEObservation, NLEState + + print("✓ Models imported successfully") + print(f" - NLEAction: {NLEAction}") + print(f" - NLEObservation: {NLEObservation}") + print(f" - NLEState: {NLEState}") +except Exception as e: + print(f"✗ Failed to import models: {e}") + sys.exit(1) + +# Test 2: Import client +print("\n[2/5] Testing client import...") +try: + from envs.nle_env import NLEEnv + + print("✓ Client imported successfully") + print(f" - NLEEnv: {NLEEnv}") +except Exception as e: + print(f"✗ Failed to import client: {e}") + sys.exit(1) + +# Test 3: Create action instances +print("\n[3/5] Testing action creation...") +try: + action1 = NLEAction(action_id=0) # Move north + action2 = NLEAction(action_id=37) # Eat + action3 = NLEAction(action_id=50) # Search + + print("✓ Actions created successfully") + print(f" - Move north: {action1}") + print(f" - Eat: {action2}") + print(f" - Search: {action3}") +except Exception as e: + print(f"✗ Failed to create actions: {e}") + sys.exit(1) + +# Test 4: Create observation instances +print("\n[4/5] Testing observation creation...") +try: + obs = NLEObservation( + glyphs=[[0] * 79 for _ in range(21)], + blstats=[0] * 26, + message=[0] * 256, + done=False, + reward=0.0, + ) + + print("✓ Observation created successfully") + print(f" - done: {obs.done}") + print(f" - reward: {obs.reward}") + print(f" - glyphs shape: {len(obs.glyphs)}x{len(obs.glyphs[0])}") + print(f" - blstats length: {len(obs.blstats)}") +except Exception as e: + print(f"✗ Failed to create observation: {e}") + sys.exit(1) + +# Test 5: Create state instances +print("\n[5/5] Testing state creation...") +try: + state = NLEState( + episode_id="test_123", + step_count=42, + game_over=False, + end_status="RUNNING", + in_normal_game=True, + character="mon-hum-neu-mal", + task_name="NetHackScore-v0", + ) + + print("✓ State created successfully") + print(f" - episode_id: {state.episode_id}") + print(f" - step_count: {state.step_count}") + print(f" - end_status: {state.end_status}") + print(f" - character: {state.character}") +except Exception as e: + print(f"✗ Failed to create state: {e}") + sys.exit(1) + +print("\n" + "=" * 70) +print("✓ All basic integration tests passed!") +print("=" * 70) +print("\nNext steps:") +print(" 1. Build Docker image (from repo root):") +print(" cd /Users/sanyambhutani/GH/OpenEnv") +print(" docker build -f src/envs/nle_env/server/Dockerfile -t nle-env:latest .") +print(" 2. Run server: docker run -p 8000:8000 nle-env:latest") +print(" 3. Test client: python examples/test_nle_client.py") +print() From 8e89feca20aee80a3dc4bb2bf92c97988e586651 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:46:04 -0800 Subject: [PATCH 11/15] Create validate_structure.py --- nle_env/validate_structure.py | 198 ++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 nle_env/validate_structure.py diff --git a/nle_env/validate_structure.py b/nle_env/validate_structure.py new file mode 100644 index 00000000..1d6b6465 --- /dev/null +++ b/nle_env/validate_structure.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Code structure validation for NLE environment. + +This script validates that all files exist and have the correct structure +without requiring dependencies to be installed. +""" + +import ast +from pathlib import Path +import sys + +print("=" * 70) +print("NLE Environment Code Structure Validation") +print("=" * 70) + +base_path = Path(__file__).parent + +# Test 1: Check all files exist +print("\n[1/6] Checking file structure...") +required_files = [ + "__init__.py", + "models.py", + "client.py", + "README.md", + "server/__init__.py", + "server/app.py", + "server/nle_environment.py", + "server/Dockerfile", +] + +missing_files = [] +for file_path in required_files: + full_path = base_path / file_path + if not full_path.exists(): + missing_files.append(file_path) + print(f" ✗ Missing: {file_path}") + else: + print(f" ✓ Found: {file_path}") + +if missing_files: + print(f"\n✗ Missing {len(missing_files)} files") + sys.exit(1) + +# Test 2: Validate models.py +print("\n[2/6] Validating models.py...") +models_path = base_path / "models.py" +with open(models_path) as f: + try: + tree = ast.parse(f.read()) + classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] + + required_classes = ["NLEAction", "NLEObservation", "NLEState"] + for cls in required_classes: + if cls in classes: + print(f" ✓ Found class: {cls}") + else: + print(f" ✗ Missing class: {cls}") + sys.exit(1) + except SyntaxError as e: + print(f" ✗ Syntax error in models.py: {e}") + sys.exit(1) + +# Test 3: Validate client.py +print("\n[3/6] Validating client.py...") +client_path = base_path / "client.py" +with open(client_path) as f: + try: + tree = ast.parse(f.read()) + classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] + + if "NLEEnv" in classes: + print(f" ✓ Found class: NLEEnv") + else: + print(f" ✗ Missing class: NLEEnv") + sys.exit(1) + + # Check for required methods + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == "NLEEnv": + methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)] + required_methods = ["_step_payload", "_parse_result", "_parse_state"] + for method in required_methods: + if method in methods: + print(f" ✓ Found method: {method}") + else: + print(f" ✗ Missing method: {method}") + sys.exit(1) + except SyntaxError as e: + print(f" ✗ Syntax error in client.py: {e}") + sys.exit(1) + +# Test 4: Validate server/nle_environment.py +print("\n[4/6] Validating server/nle_environment.py...") +env_path = base_path / "server" / "nle_environment.py" +with open(env_path) as f: + try: + tree = ast.parse(f.read()) + classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] + + if "NLEEnvironment" in classes: + print(f" ✓ Found class: NLEEnvironment") + else: + print(f" ✗ Missing class: NLEEnvironment") + sys.exit(1) + + # Check for required methods + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == "NLEEnvironment": + methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)] + required_methods = ["reset", "step", "state"] + for method in required_methods: + if method in methods: + print(f" ✓ Found method: {method}") + else: + print(f" ✗ Missing method: {method}") + sys.exit(1) + except SyntaxError as e: + print(f" ✗ Syntax error in server/nle_environment.py: {e}") + sys.exit(1) + +# Test 5: Validate server/app.py +print("\n[5/6] Validating server/app.py...") +app_path = base_path / "server" / "app.py" +with open(app_path) as f: + content = f.read() + try: + tree = ast.parse(content) + + # Check for create_app call + has_create_app = "create_app" in content + if has_create_app: + print(f" ✓ Found create_app call") + else: + print(f" ✗ Missing create_app call") + sys.exit(1) + + # Check for app variable + has_app_var = any( + isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "app" + for target in node.targets + ) + for node in ast.walk(tree) + ) + if has_app_var: + print(f" ✓ Found app variable") + else: + print(f" ✗ Missing app variable") + sys.exit(1) + except SyntaxError as e: + print(f" ✗ Syntax error in server/app.py: {e}") + sys.exit(1) + +# Test 6: Validate Dockerfile +print("\n[6/6] Validating server/Dockerfile...") +dockerfile_path = base_path / "server" / "Dockerfile" +with open(dockerfile_path) as f: + content = f.read() + + required_elements = [ + ("FROM", "Base image"), + ("RUN", "Install commands"), + ("COPY", "Copy files"), + ("CMD", "Startup command"), + ("EXPOSE", "Port exposure"), + ] + + for element, description in required_elements: + if element in content: + print(f" ✓ Found {description} ({element})") + else: + print(f" ✗ Missing {description} ({element})") + sys.exit(1) + + # Check for NLE installation + if "nle" in content.lower(): + print(f" ✓ Found NLE installation") + else: + print(f" ✗ Missing NLE installation") + sys.exit(1) + +# Summary +print("\n" + "=" * 70) +print("✓ All code structure validations passed!") +print("=" * 70) +print("\nStructure is correct. Integration complete!") +print("\nNext steps:") +print(" 1. Build Docker image (from repo root):") +print(" cd /Users/sanyambhutani/GH/OpenEnv") +print(" docker build -f src/envs/nle_env/server/Dockerfile -t nle-env:latest .") +print() +print(" 2. Run server:") +print(" docker run -p 8000:8000 nle-env:latest") +print() +print(" 3. Test with client (requires OpenEnv dependencies):") +print(" python examples/test_nle.py") +print() From 7e519c7a02813ba0da6a023e0fb7740980441ed1 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:46:10 -0800 Subject: [PATCH 12/15] Delete test_integration.py --- nle_env/test_integration.py | 111 ------------------------------------ 1 file changed, 111 deletions(-) delete mode 100644 nle_env/test_integration.py diff --git a/nle_env/test_integration.py b/nle_env/test_integration.py deleted file mode 100644 index 3d631103..00000000 --- a/nle_env/test_integration.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -Basic integration test for NLE environment. - -This script tests that the NLE environment can be imported and basic -structure is correct. Does NOT require NLE to be installed (that happens -in Docker). -""" - -import sys -from pathlib import Path - -# Add src to path -src_path = Path(__file__).parent.parent.parent.parent / "src" -sys.path.insert(0, str(src_path)) - -print("=" * 70) -print("NLE Environment Integration Test") -print("=" * 70) - -# Test 1: Import models -print("\n[1/5] Testing model imports...") -try: - from envs.nle_env import NLEAction, NLEObservation, NLEState - - print("✓ Models imported successfully") - print(f" - NLEAction: {NLEAction}") - print(f" - NLEObservation: {NLEObservation}") - print(f" - NLEState: {NLEState}") -except Exception as e: - print(f"✗ Failed to import models: {e}") - sys.exit(1) - -# Test 2: Import client -print("\n[2/5] Testing client import...") -try: - from envs.nle_env import NLEEnv - - print("✓ Client imported successfully") - print(f" - NLEEnv: {NLEEnv}") -except Exception as e: - print(f"✗ Failed to import client: {e}") - sys.exit(1) - -# Test 3: Create action instances -print("\n[3/5] Testing action creation...") -try: - action1 = NLEAction(action_id=0) # Move north - action2 = NLEAction(action_id=37) # Eat - action3 = NLEAction(action_id=50) # Search - - print("✓ Actions created successfully") - print(f" - Move north: {action1}") - print(f" - Eat: {action2}") - print(f" - Search: {action3}") -except Exception as e: - print(f"✗ Failed to create actions: {e}") - sys.exit(1) - -# Test 4: Create observation instances -print("\n[4/5] Testing observation creation...") -try: - obs = NLEObservation( - glyphs=[[0] * 79 for _ in range(21)], - blstats=[0] * 26, - message=[0] * 256, - done=False, - reward=0.0, - ) - - print("✓ Observation created successfully") - print(f" - done: {obs.done}") - print(f" - reward: {obs.reward}") - print(f" - glyphs shape: {len(obs.glyphs)}x{len(obs.glyphs[0])}") - print(f" - blstats length: {len(obs.blstats)}") -except Exception as e: - print(f"✗ Failed to create observation: {e}") - sys.exit(1) - -# Test 5: Create state instances -print("\n[5/5] Testing state creation...") -try: - state = NLEState( - episode_id="test_123", - step_count=42, - game_over=False, - end_status="RUNNING", - in_normal_game=True, - character="mon-hum-neu-mal", - task_name="NetHackScore-v0", - ) - - print("✓ State created successfully") - print(f" - episode_id: {state.episode_id}") - print(f" - step_count: {state.step_count}") - print(f" - end_status: {state.end_status}") - print(f" - character: {state.character}") -except Exception as e: - print(f"✗ Failed to create state: {e}") - sys.exit(1) - -print("\n" + "=" * 70) -print("✓ All basic integration tests passed!") -print("=" * 70) -print("\nNext steps:") -print(" 1. Build Docker image (from repo root):") -print(" cd /Users/sanyambhutani/GH/OpenEnv") -print(" docker build -f src/envs/nle_env/server/Dockerfile -t nle-env:latest .") -print(" 2. Run server: docker run -p 8000:8000 nle-env:latest") -print(" 3. Test client: python examples/test_nle_client.py") -print() From eb7a0eb3e7b4c3b1c0994afdbbaa217ec889b5af Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:46:13 -0800 Subject: [PATCH 13/15] Delete validate_structure.py --- nle_env/validate_structure.py | 198 ---------------------------------- 1 file changed, 198 deletions(-) delete mode 100644 nle_env/validate_structure.py diff --git a/nle_env/validate_structure.py b/nle_env/validate_structure.py deleted file mode 100644 index 1d6b6465..00000000 --- a/nle_env/validate_structure.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 -""" -Code structure validation for NLE environment. - -This script validates that all files exist and have the correct structure -without requiring dependencies to be installed. -""" - -import ast -from pathlib import Path -import sys - -print("=" * 70) -print("NLE Environment Code Structure Validation") -print("=" * 70) - -base_path = Path(__file__).parent - -# Test 1: Check all files exist -print("\n[1/6] Checking file structure...") -required_files = [ - "__init__.py", - "models.py", - "client.py", - "README.md", - "server/__init__.py", - "server/app.py", - "server/nle_environment.py", - "server/Dockerfile", -] - -missing_files = [] -for file_path in required_files: - full_path = base_path / file_path - if not full_path.exists(): - missing_files.append(file_path) - print(f" ✗ Missing: {file_path}") - else: - print(f" ✓ Found: {file_path}") - -if missing_files: - print(f"\n✗ Missing {len(missing_files)} files") - sys.exit(1) - -# Test 2: Validate models.py -print("\n[2/6] Validating models.py...") -models_path = base_path / "models.py" -with open(models_path) as f: - try: - tree = ast.parse(f.read()) - classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] - - required_classes = ["NLEAction", "NLEObservation", "NLEState"] - for cls in required_classes: - if cls in classes: - print(f" ✓ Found class: {cls}") - else: - print(f" ✗ Missing class: {cls}") - sys.exit(1) - except SyntaxError as e: - print(f" ✗ Syntax error in models.py: {e}") - sys.exit(1) - -# Test 3: Validate client.py -print("\n[3/6] Validating client.py...") -client_path = base_path / "client.py" -with open(client_path) as f: - try: - tree = ast.parse(f.read()) - classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] - - if "NLEEnv" in classes: - print(f" ✓ Found class: NLEEnv") - else: - print(f" ✗ Missing class: NLEEnv") - sys.exit(1) - - # Check for required methods - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == "NLEEnv": - methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)] - required_methods = ["_step_payload", "_parse_result", "_parse_state"] - for method in required_methods: - if method in methods: - print(f" ✓ Found method: {method}") - else: - print(f" ✗ Missing method: {method}") - sys.exit(1) - except SyntaxError as e: - print(f" ✗ Syntax error in client.py: {e}") - sys.exit(1) - -# Test 4: Validate server/nle_environment.py -print("\n[4/6] Validating server/nle_environment.py...") -env_path = base_path / "server" / "nle_environment.py" -with open(env_path) as f: - try: - tree = ast.parse(f.read()) - classes = [node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef)] - - if "NLEEnvironment" in classes: - print(f" ✓ Found class: NLEEnvironment") - else: - print(f" ✗ Missing class: NLEEnvironment") - sys.exit(1) - - # Check for required methods - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == "NLEEnvironment": - methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)] - required_methods = ["reset", "step", "state"] - for method in required_methods: - if method in methods: - print(f" ✓ Found method: {method}") - else: - print(f" ✗ Missing method: {method}") - sys.exit(1) - except SyntaxError as e: - print(f" ✗ Syntax error in server/nle_environment.py: {e}") - sys.exit(1) - -# Test 5: Validate server/app.py -print("\n[5/6] Validating server/app.py...") -app_path = base_path / "server" / "app.py" -with open(app_path) as f: - content = f.read() - try: - tree = ast.parse(content) - - # Check for create_app call - has_create_app = "create_app" in content - if has_create_app: - print(f" ✓ Found create_app call") - else: - print(f" ✗ Missing create_app call") - sys.exit(1) - - # Check for app variable - has_app_var = any( - isinstance(node, ast.Assign) and any( - isinstance(target, ast.Name) and target.id == "app" - for target in node.targets - ) - for node in ast.walk(tree) - ) - if has_app_var: - print(f" ✓ Found app variable") - else: - print(f" ✗ Missing app variable") - sys.exit(1) - except SyntaxError as e: - print(f" ✗ Syntax error in server/app.py: {e}") - sys.exit(1) - -# Test 6: Validate Dockerfile -print("\n[6/6] Validating server/Dockerfile...") -dockerfile_path = base_path / "server" / "Dockerfile" -with open(dockerfile_path) as f: - content = f.read() - - required_elements = [ - ("FROM", "Base image"), - ("RUN", "Install commands"), - ("COPY", "Copy files"), - ("CMD", "Startup command"), - ("EXPOSE", "Port exposure"), - ] - - for element, description in required_elements: - if element in content: - print(f" ✓ Found {description} ({element})") - else: - print(f" ✗ Missing {description} ({element})") - sys.exit(1) - - # Check for NLE installation - if "nle" in content.lower(): - print(f" ✓ Found NLE installation") - else: - print(f" ✗ Missing NLE installation") - sys.exit(1) - -# Summary -print("\n" + "=" * 70) -print("✓ All code structure validations passed!") -print("=" * 70) -print("\nStructure is correct. Integration complete!") -print("\nNext steps:") -print(" 1. Build Docker image (from repo root):") -print(" cd /Users/sanyambhutani/GH/OpenEnv") -print(" docker build -f src/envs/nle_env/server/Dockerfile -t nle-env:latest .") -print() -print(" 2. Run server:") -print(" docker run -p 8000:8000 nle-env:latest") -print() -print(" 3. Test with client (requires OpenEnv dependencies):") -print(" python examples/test_nle.py") -print() From 6dc12cc35fb96d272eec28b296c1ad55b606c025 Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Thu, 6 Nov 2025 19:47:15 -0800 Subject: [PATCH 14/15] mv fikes --- {nle_env => src/envs/nle_env}/README.md | 0 {nle_env => src/envs/nle_env}/__init__.py | 0 {nle_env => src/envs/nle_env}/client.py | 0 {nle_env => src/envs/nle_env}/models.py | 0 {nle_env => src/envs/nle_env}/server/Dockerfile | 0 {nle_env => src/envs/nle_env}/server/__init__.py | 0 {nle_env => src/envs/nle_env}/server/app.py | 0 {nle_env => src/envs/nle_env}/server/nle_environment.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {nle_env => src/envs/nle_env}/README.md (100%) rename {nle_env => src/envs/nle_env}/__init__.py (100%) rename {nle_env => src/envs/nle_env}/client.py (100%) rename {nle_env => src/envs/nle_env}/models.py (100%) rename {nle_env => src/envs/nle_env}/server/Dockerfile (100%) rename {nle_env => src/envs/nle_env}/server/__init__.py (100%) rename {nle_env => src/envs/nle_env}/server/app.py (100%) rename {nle_env => src/envs/nle_env}/server/nle_environment.py (100%) diff --git a/nle_env/README.md b/src/envs/nle_env/README.md similarity index 100% rename from nle_env/README.md rename to src/envs/nle_env/README.md diff --git a/nle_env/__init__.py b/src/envs/nle_env/__init__.py similarity index 100% rename from nle_env/__init__.py rename to src/envs/nle_env/__init__.py diff --git a/nle_env/client.py b/src/envs/nle_env/client.py similarity index 100% rename from nle_env/client.py rename to src/envs/nle_env/client.py diff --git a/nle_env/models.py b/src/envs/nle_env/models.py similarity index 100% rename from nle_env/models.py rename to src/envs/nle_env/models.py diff --git a/nle_env/server/Dockerfile b/src/envs/nle_env/server/Dockerfile similarity index 100% rename from nle_env/server/Dockerfile rename to src/envs/nle_env/server/Dockerfile diff --git a/nle_env/server/__init__.py b/src/envs/nle_env/server/__init__.py similarity index 100% rename from nle_env/server/__init__.py rename to src/envs/nle_env/server/__init__.py diff --git a/nle_env/server/app.py b/src/envs/nle_env/server/app.py similarity index 100% rename from nle_env/server/app.py rename to src/envs/nle_env/server/app.py diff --git a/nle_env/server/nle_environment.py b/src/envs/nle_env/server/nle_environment.py similarity index 100% rename from nle_env/server/nle_environment.py rename to src/envs/nle_env/server/nle_environment.py From 0614916ff08a54c1c7b0fd8ea575af4fc71ae9dd Mon Sep 17 00:00:00 2001 From: Sanyam Bhutani Date: Fri, 7 Nov 2025 13:17:10 -0800 Subject: [PATCH 15/15] Update src/envs/nle_env/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/envs/nle_env/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/envs/nle_env/models.py b/src/envs/nle_env/models.py index d1ae87c7..c362ae28 100644 --- a/src/envs/nle_env/models.py +++ b/src/envs/nle_env/models.py @@ -13,7 +13,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any, Dict, List, Optional from core.env_server import Action, Observation, State