Rate this Page

Source code for torchrl.envs.libs.craftground

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""CraftGround (Minecraft) environment wrappers.

.. note:: **Minecraft ownership and licensing.** TorchRL does not ship, bundle
    or redistribute Minecraft. On first use, CraftGround's Gradle project
    downloads the Minecraft client and its assets from Mojang's servers onto
    the local machine and runs the game in offline mode. Offline mode removes
    the need for an account login during headless training; it does not remove
    the requirement to own the game: users are expected to own a valid
    Minecraft: Java Edition license, and any use of Minecraft is subject to the
    Minecraft End User License Agreement (https://www.minecraft.net/eula),
    including its restrictions on commercial exploitation. Never redistribute
    the downloaded game files (for instance inside public Docker images or CI
    caches). CraftGround is a separate, optional Python dependency that TorchRL
    does not vendor or redistribute. Its upstream repository currently ships a
    GPL-3.0 license file while its package metadata reports MIT; consult the
    upstream licensing information before distribution. See
    ``knowledge_base/MINECRAFT.md`` for details.
"""
from __future__ import annotations

import importlib.util
from typing import Literal, TYPE_CHECKING

import numpy as np
import torch
from torchrl.data.tensor_specs import Bounded, Categorical, Composite, Unbounded
from torchrl.envs.libs.gym import (
    _gym_to_torchrl_spec_transform,
    GymWrapper,
    set_gym_backend,
)
from torchrl.envs.utils import _classproperty

if TYPE_CHECKING:
    from craftground.environment.environment import CraftGroundEnvironment
    from craftground.initial_environment_config import InitialEnvironmentConfig

_has_craftground = importlib.util.find_spec("craftground") is not None

# Boolean entries of CraftGround's v2 (MineRL-human-like) action space. The
# upstream gymnasium Dict space uses dotted keys ("hotbar.1") which cannot be
# used as TensorDict keys; we expose underscored variants and translate back in
# read_action.
_V2_BOOL_KEYS = (
    "attack",
    "back",
    "forward",
    "jump",
    "left",
    "right",
    "sneak",
    "sprint",
    "use",
    "drop",
    "inventory",
) + tuple(f"hotbar_{i}" for i in range(1, 10))


[docs] class CraftGroundWrapper(GymWrapper): """CraftGround (Minecraft) environment wrapper. GitHub: https://github.com/yhs0602/CraftGround Documentation: https://yhs0602.github.io/CraftGround/ Paper: Yun et al., "CraftGround: A Flexible Reinforcement Learning Environment Based on the Latest Minecraft" (2025). CraftGround runs a lightweight, headless-capable Minecraft client instrumented through a Fabric mod, and exposes it as a gymnasium environment. Observations are ego-centric RGB frames; actions follow either the MineDojo-style multi-discrete layout (v1) or a MineRL-human-like dictionary layout (v2). The wrapped environment is a *sandbox*: the underlying ``step`` always returns a zero reward and never terminates. Rewards and termination conditions are meant to be composed on top with TorchRL transforms (see the example below), or by sending Minecraft commands through ``env.add_command(...)`` and reading the resulting state. .. note:: **Minecraft ownership and licensing.** TorchRL does not distribute Minecraft. On the first :meth:`reset`, CraftGround's Gradle project downloads the Minecraft client from Mojang's servers onto the local machine and runs it in offline mode. Users are expected to own a valid Minecraft: Java Edition license; offline mode bypasses authentication, not ownership. Usage of Minecraft is governed by the Minecraft EULA (https://www.minecraft.net/eula), including its restrictions on commercial exploitation. Never redistribute the downloaded game files (e.g. in public Docker images or CI caches). CraftGround is a separate, optional dependency that TorchRL does not vendor or redistribute. Its upstream repository currently ships a GPL-3.0 license file while its package metadata reports MIT; consult the upstream licensing information before distribution. See ``knowledge_base/MINECRAFT.md`` in the TorchRL repository for details. .. note:: The environment is spawned lazily: constructing the wrapper only binds an IPC channel. The Minecraft client (a Java subprocess built and launched through Gradle) starts on the first :meth:`reset`, which can take several minutes on the very first run while Gradle downloads Minecraft and compiles the mod. Requires a JDK (OpenJDK 21) and, on headless machines, a virtual display (e.g. ``Xvfb``); see ``knowledge_base/MINECRAFT.md``. .. note:: Only the ``RAW`` (default) and ``PNG`` screen encoding modes are supported. ``RAW`` frames have shape ``(H, W, 3)``, ``PNG`` frames ``(3, W, H)``, both ``torch.uint8``. With a binocular configuration (``eye_distance > 0``) a second ``pixels_2`` entry is added. Args: env (craftground.environment.environment.CraftGroundEnvironment): the CraftGround environment instance to wrap. Keyword Args: categorical_action_encoding (bool, optional): if ``True``, categorical specs will be converted to the TorchRL equivalent (:class:`torchrl.data.Categorical`), otherwise a one-hot encoding will be used (:class:`torchrl.data.OneHot`). Defaults to ``False``. Only used with the v1 (MineDojo-style) action space. device (torch.device, optional): if provided, the device on which the data is to be cast. Defaults to ``torch.device("cpu")``. batch_size (torch.Size, optional): only ``torch.Size([])`` is supported, as CraftGround environments are not vectorized. allow_done_after_reset (bool, optional): if ``True``, it is tolerated for envs to be ``done`` just after :meth:`reset` is called. Defaults to ``False``. Attributes: available_envs: an empty list; CraftGround environments are described by a ``craftground.InitialEnvironmentConfig`` rather than selected from a registry of task ids. Examples: >>> import craftground # doctest: +SKIP >>> from torchrl.envs import TransformedEnv, StepCounter >>> from torchrl.envs.libs.craftground import CraftGroundWrapper >>> base = craftground.make(port=8023) # doctest: +SKIP >>> env = CraftGroundWrapper(base) # doctest: +SKIP >>> # the sandbox emits no reward: compose one with a transform, >>> # e.g. a step-count budget via StepCounter >>> env = TransformedEnv(env, StepCounter(max_steps=100)) # doctest: +SKIP >>> td = env.rollout(3) # doctest: +SKIP >>> assert td["next", "pixels"].dtype is torch.uint8 # doctest: +SKIP """ git_url = "https://github.com/yhs0602/CraftGround" libname = "craftground" _lib = None @_classproperty def lib(cls): if cls._lib is not None: return cls._lib try: import craftground except ImportError as err: raise ImportError( "craftground not found. Install it with `pip install craftground` " "(requires OpenJDK 21 at runtime; see " "https://yhs0602.github.io/CraftGround/ and " "knowledge_base/MINECRAFT.md for setup and licensing notes)." ) from err cls._lib = craftground return craftground @_classproperty def available_envs(cls): # CraftGround has no registry of named tasks: environments are # parameterized by an InitialEnvironmentConfig instead. return [] def _check_kwargs(self, kwargs: dict): super()._check_kwargs(kwargs) env = kwargs["env"] if not hasattr(env, "initial_env"): raise TypeError( "env must be a craftground CraftGroundEnvironment instance " "(missing the `initial_env` attribute). Use CraftGroundEnv to " "build one from a configuration." ) def _build_env( self, env, from_pixels: bool = False, pixels_only: bool = False, ): if from_pixels: raise ValueError( "CraftGround environments are natively pixel-based: observations " "already contain a `pixels` entry, and `from_pixels` is not " "supported." ) return super()._build_env(env, from_pixels=from_pixels, pixels_only=pixels_only) @staticmethod def _uses_v2_actions(env) -> bool: version = getattr(env, "action_space_version", None) return getattr(version, "name", None) == "V2_MINERL_HUMAN" def _make_specs(self, env, batch_size=None) -> None: config = env.initial_env height = config.imageSizeY width = config.imageSizeX mode = getattr( config.screen_encoding_mode, "name", str(config.screen_encoding_mode) ) if mode == "RAW": pixels_shape = (height, width, 3) elif mode == "PNG": pixels_shape = (3, width, height) else: raise NotImplementedError( f"screen_encoding_mode {mode!r} is not supported by " "CraftGroundWrapper. Use ScreenEncodingMode.RAW (default) or " "ScreenEncodingMode.PNG." ) pixels_spec = Bounded( low=0, high=255, shape=pixels_shape, dtype=torch.uint8, device=self.device, ) observation_spec = Composite(pixels=pixels_spec, shape=self.batch_size) self._binocular = config.eye_distance > 0 if self._binocular: observation_spec["pixels_2"] = pixels_spec.clone() self._v2_actions = self._uses_v2_actions(env) if self._v2_actions: action_spec = self._make_v2_action_spec() else: action_spec = _gym_to_torchrl_spec_transform( env.action_space, device=self.device, categorical_action_encoding=self._categorical_action_encoding, ) self.done_spec = self._make_done_spec() self.action_spec = action_spec self.reward_spec = Unbounded(shape=(1,), device=self.device) self.observation_spec = observation_spec _make_specs = set_gym_backend("gymnasium")(_make_specs) def _make_v2_action_spec(self) -> Composite: spec = { key: Categorical(2, shape=(), dtype=torch.bool, device=self.device) for key in _V2_BOOL_KEYS } spec["camera"] = Bounded( low=-180.0, high=180.0, shape=(2,), dtype=torch.float32, device=self.device, ) return Composite(spec, shape=self.batch_size) def read_action(self, action): if self._v2_actions: # Translate the composite action back to CraftGround's dict format # (dotted hotbar keys, python bools, float32 camera). out = {} for key, value in action.items(): if key == "camera": if isinstance(value, torch.Tensor): value = value.detach().cpu() out["camera"] = np.asarray(value, dtype=np.float32) else: out[key.replace("hotbar_", "hotbar.")] = bool(value) return out return super().read_action(action) def _process_obs(self, observations): out = {"pixels": self._to_image(observations["pov"])} if self._binocular: out["pixels_2"] = self._to_image(observations["pov_2"]) return out @staticmethod def _to_image(image): if isinstance(image, torch.Tensor): return image # CraftGround returns flipped, read-only numpy views; make them # contiguous and writable before tensor conversion. return np.ascontiguousarray(image) def _output_transform(self, step_outputs_tuple): # CraftGround's step returns (obs, reward, terminated, truncated, info) # where info is the observation dict itself and reward/terminated are # sandbox placeholders. The raw protobuf entry of the observation dict # is dropped here. observations, reward, terminated, truncated, _ = step_outputs_tuple observations = self._process_obs(observations) terminated = bool(terminated) truncated = bool(truncated) return ( observations, reward, terminated, truncated, terminated | truncated, None, ) def _reset_output_transform(self, reset_outputs_tuple): observations, _ = reset_outputs_tuple return self._process_obs(observations), None
[docs] class CraftGroundEnv(CraftGroundWrapper): """CraftGround (Minecraft) environment built from a configuration. See :class:`CraftGroundWrapper` for behavior details and licensing notes. The constructor builds the environment through ``craftground.make(...)``. .. note:: **Minecraft ownership and licensing.** TorchRL does not distribute Minecraft. On the first :meth:`reset`, CraftGround's Gradle project downloads the Minecraft client from Mojang's servers onto the local machine and runs it in offline mode. Users are expected to own a valid Minecraft: Java Edition license; offline mode bypasses authentication, not ownership. Usage of Minecraft is governed by the Minecraft EULA (https://www.minecraft.net/eula), including its restrictions on commercial exploitation. Never redistribute the downloaded game files (e.g. in public Docker images or CI caches). CraftGround is a separate, optional dependency that TorchRL does not vendor or redistribute. Its upstream repository currently ships a GPL-3.0 license file while its package metadata reports MIT; consult the upstream licensing information before distribution. See ``knowledge_base/MINECRAFT.md`` in the TorchRL repository for details. Keyword Args: initial_env_config (craftground.InitialEnvironmentConfig, optional): the world/observation configuration (image size, game mode, world type, seed, initial commands, ...). Defaults to a fresh ``InitialEnvironmentConfig()``. mc_version (str, optional): the Minecraft version to run. Only ``"1.21"`` is currently functional upstream. Defaults to ``"1.21"``. port (int, optional): the IPC port used to communicate with the Minecraft process. A free port is picked automatically if the given one is busy. Defaults to ``8000``. action_space_version (craftground.ActionSpaceVersion, optional): the action layout, either ``V1_MINEDOJO`` (multi-discrete) or ``V2_MINERL_HUMAN`` (dict of booleans plus a continuous camera). Defaults to ``V1_MINEDOJO``. env_path (str, optional): path to a custom CraftGround Gradle project. Defaults to the project shipped with the installed ``craftground-runtime-mc121`` package. use_shared_memory (bool, optional): if ``True``, uses the shared-memory IPC backend instead of TCP sockets. Defaults to ``False``. verbose (bool, optional): enables CraftGround's verbose logging. Defaults to ``False``. craftground_kwargs (dict, optional): additional keyword arguments forwarded verbatim to ``craftground.make``. **kwargs: additional keyword arguments passed to :class:`CraftGroundWrapper` (e.g. ``device``). Examples: >>> from craftground.initial_environment_config import ( # doctest: +SKIP ... InitialEnvironmentConfig, WorldType) >>> from torchrl.envs.libs.craftground import CraftGroundEnv >>> env = CraftGroundEnv( # doctest: +SKIP ... initial_env_config=InitialEnvironmentConfig( ... image_width=114, image_height=64, ... world_type=WorldType.SUPERFLAT, ... ), ... port=8023, ... ) >>> td = env.reset() # doctest: +SKIP >>> td["pixels"].shape # doctest: +SKIP torch.Size([64, 114, 3]) """ def __init__( self, *, initial_env_config: InitialEnvironmentConfig | None = None, mc_version: Literal["1.21"] = "1.21", port: int = 8000, action_space_version=None, env_path: str | None = None, use_shared_memory: bool = False, verbose: bool = False, craftground_kwargs: dict | None = None, **kwargs, ): craftground = self.lib if action_space_version is None: action_space_version = craftground.ActionSpaceVersion.V1_MINEDOJO craftground_kwargs = ( dict(craftground_kwargs) if craftground_kwargs is not None else {} ) env: CraftGroundEnvironment = craftground.make( initial_env_config=initial_env_config, mc_version=mc_version, port=port, action_space_version=action_space_version, env_path=env_path, use_shared_memory=use_shared_memory, verbose=verbose, **craftground_kwargs, ) super().__init__(env=env, **kwargs)