Source code for pettingzoo.utils.wrappers.black_death

"""Wrapper that keeps departed Parallel agents visible until episode end."""

from __future__ import annotations

from typing import Any

import gymnasium.spaces
import numpy as np
from gymnasium.spaces import Box
from typing_extensions import override

from pettingzoo.utils.env import ActionType, AgentID, ParallelEnv
from pettingzoo.utils.wrappers.base_parallel import BaseParallelWrapper


def _box_observation_space(space: gymnasium.spaces.Space[Any], agent: AgentID) -> Box:
    """Return a Box observation space or raise a clear compatibility error."""
    if not isinstance(space, Box):
        raise TypeError(
            "BlackDeathParallelV4 only supports Box observation spaces, "
            f"but agent {agent!r} has {space}."
        )
    return space


[docs] class BlackDeathParallelV4(BaseParallelWrapper[AgentID, Any, ActionType]): """Keep the reset-time agent set visible until the whole episode ends. When an agent leaves the wrapped environment early, subsequent steps expose a zero-valued observation with the same shape and dtype as that agent's Box observation space, zero reward, and an empty info dictionary. Actions for departed agents are ignored and only actions for currently active wrapped agents are forwarded. To keep the public agent set stable, an early departure is reported as terminated=False and truncated=False until the underlying episode ends. The wrapper remembers the original cause of each departure and reports those per-agent termination and truncation flags on the final step instead of collapsing both causes into one done flag. Agents that appear after reset are not supported because they would change the fixed agent set exposed by this wrapper. :param env: The Parallel environment to wrap. """ def __init__(self, env: ParallelEnv[AgentID, Any, ActionType]): super().__init__(env) self._episode_agents: list[AgentID] = [] self._zero_observations: dict[AgentID, np.ndarray] = {} self._stored_terminations: dict[AgentID, bool] = {} self._stored_truncations: dict[AgentID, bool] = {} for agent in getattr(env, "possible_agents", []): _box_observation_space(self.env.observation_space(agent), agent) @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> tuple[dict[AgentID, Any], dict[AgentID, dict[str, Any]]]: observations, infos = self.env.reset(seed=seed, options=options) self._episode_agents = list(self.env.agents) self.agents = self._episode_agents.copy() self._stored_terminations = dict.fromkeys(self._episode_agents, False) self._stored_truncations = dict.fromkeys(self._episode_agents, False) self._zero_observations = {} for agent in self._episode_agents: space = _box_observation_space(self.env.observation_space(agent), agent) self._zero_observations[agent] = np.zeros(space.shape, dtype=space.dtype) return ( { agent: observations.get(agent, self._zero_observations[agent].copy()) for agent in self._episode_agents }, {agent: infos.get(agent, {}) for agent in self._episode_agents}, ) @override def step( self, actions: dict[AgentID, ActionType] ) -> tuple[ dict[AgentID, Any], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict[str, Any]], ]: active_agents = list(self.env.agents) unexpected = [ agent for agent in active_agents if agent not in self._episode_agents ] if unexpected: raise RuntimeError( "BlackDeathParallelV4 does not support agents appearing after reset: " f"{unexpected}." ) active_actions = {agent: actions[agent] for agent in active_agents} observations, rewards, terminations, truncations, infos = self.env.step( active_actions ) unexpected = [ agent for agent in self.env.agents if agent not in self._episode_agents ] if unexpected: raise RuntimeError( "BlackDeathParallelV4 does not support agents appearing after reset: " f"{unexpected}." ) for agent in self._episode_agents: if terminations.get(agent, False): self._stored_terminations[agent] = True if truncations.get(agent, False): self._stored_truncations[agent] = True episode_done = not self.env.agents if episode_done: total_terminations = self._stored_terminations.copy() total_truncations = self._stored_truncations.copy() self.agents = [] else: total_terminations = dict.fromkeys(self._episode_agents, False) total_truncations = dict.fromkeys(self._episode_agents, False) self.agents = self._episode_agents.copy() total_observations = { agent: observations.get(agent, self._zero_observations[agent].copy()) for agent in self._episode_agents } total_rewards = { agent: rewards.get(agent, 0.0) for agent in self._episode_agents } total_infos = {agent: infos.get(agent, {}) for agent in self._episode_agents} return ( total_observations, total_rewards, total_terminations, total_truncations, total_infos, ) @override def __str__(self) -> str: return f"BlackDeathParallelV4<{self.env!s}>"