from collections import defaultdict, namedtuple import math from pathlib import Path from typing import Any, Dict, List, Tuple import pygame import torch from torch import Tensor from agent import Agent from data import Dataset, Episode from game.keymap import ActionNames, Keymap from envs import WorldModelEnv NamedEnv = namedtuple("NamedEnv", "name env") OneStepData = namedtuple("OneStepData", "obs act rew end trunc") class PlayEnv: def __init__( self, agent: Agent, envs: List[NamedEnv], action_names: ActionNames, keymap: Keymap, recording_mode: bool, store_denoising_trajectory: bool, store_original_obs: bool, ) -> None: self.agent = agent self.envs = envs self.action_names = action_names self.keymap = keymap self.recording_mode = recording_mode self.store_denoising_trajectory = store_denoising_trajectory self.store_original_obs = store_original_obs self.is_human_player = False self.env_id = 0 self.env_name, self.env = self.envs[0] self.obs, self.t, self.return_, self.hx_cx, self.ckpt_id, self.buffer, self.rec_dataset = (None,) * 7 def print_controls(self) -> None: print("\nControls (play mode):\n") print("m : controller (policy/human)") print("↑ : imagination horizon (+1)") print("↓ : imagination horizon (-1)") print(f"→ : next environment ({' → '.join([env_name for (env_name, _) in self.envs])})") print(f"← : prev environment ({' ← '.join([env_name for (env_name, _) in self.envs])})") print("\nEnvironment actions:\n") for keys, idx in self.keymap.items(): key_names = [pygame.key.name(key) for key in keys] key_names = ["⎵" if key_name == "space" else key_name for key_name in key_names] print(f"{' + '.join(key_names)} : {self.action_names[idx]}") def next_mode(self) -> bool: self.switch_controller() return True def next_axis_1(self) -> bool: self.update_wm_horizon(+1) return True def prev_axis_1(self) -> bool: self.update_wm_horizon(-1) return True def next_axis_2(self) -> bool: self.switch_env(self.env_id + 1) return True def prev_axis_2(self) -> bool: self.switch_env(self.env_id - 1) return True def is_wm_env(self) -> bool: return isinstance(self.env, WorldModelEnv) def print_env(self) -> None: print(f"> Environment: {self.env_name}") def print_control(self) -> None: print(f"> Control: {'human' if self.is_human_player else 'policy'}") def switch_env(self, env_id: int) -> None: self.env_id = env_id % len(self.envs) self.env_name, self.env = self.envs[self.env_id] self.print_env() def switch_controller(self) -> None: self.is_human_player = not self.is_human_player self.print_control() def update_wm_horizon(self, incr: int) -> None: if self.is_wm_env(): self.env.horizon = max(1, self.env.horizon + incr) def reset_recording(self) -> None: self.buffer = defaultdict(list) self.buffer["info"] = defaultdict(list) dir = Path("dataset") / f"rec_{self.env_name}_{'H' if self.is_human_player else 'π'}" self.rec_dataset = Dataset(dir) self.rec_dataset.load_from_default_path() def reset(self) -> Tuple[Tensor, None]: self.obs, _ = self.env.reset() self.t, self.return_, self.hx_cx = 0, 0, None if self.recording_mode: self.reset_recording() return self.obs, None @torch.no_grad() def step(self, act: int) -> Tuple[Tensor, Tensor, Tensor, Tensor, Dict[str, Any]]: if self.is_human_player: act = torch.tensor([act], device=self.agent.device) else: logits_act, value, self.hx_cx = self.agent.actor_critic.predict_act_value(self.obs, self.hx_cx) dst = torch.distributions.categorical.Categorical(logits=logits_act) act = dst.sample() entropy = dst.entropy() / math.log(2) entropy = None if self.is_human_player else f"{entropy.item():.2f}" value = None if self.is_human_player else f"{value.item():.2f}" next_obs, rew, end, trunc, env_info = self.env.step(act) data = OneStepData(self.obs, act, rew, end, trunc) self.return_ += rew.item() control = "human" if self.is_human_player else "policy" header = [ [ f"Env : {self.env_name}", f"Control : {control}", f"Timestep: {self.t + 1}", f"Horizon : {self.env.horizon}" if self.is_wm_env() else "", ], [ f"Trunc : {bool(trunc)}", f"Done : {bool(end)}", f"Reward: {rew.item():.2f}", f"Return: {self.return_:.2f}", ], [ f"Action : {self.action_names[act[0]]}", f"Entropy: {entropy}", f"Value : {value}", ], ] info = {"header": header} if end or trunc: d = "Dead" if end else ("Horizon" if self.is_wm_env() else "Timed out") print(f"[{d}] return = {self.return_} - length = {self.t + 1}.") if self.recording_mode: for k, v in data._asdict().items(): self.buffer[k].append(v) if self.store_denoising_trajectory and "denoising_trajectory" in env_info: self.buffer["info"]["denoising_trajectory"].append(env_info["denoising_trajectory"]) if self.store_original_obs and "original_obs" in env_info: original_obs = (torch.tensor(env_info["original_obs"][0]).permute(2, 0, 1).unsqueeze(0).contiguous()) self.buffer["info"]["original_obs"].append(original_obs) if end or trunc: ep_dict = {k: torch.cat(v, dim=0) for k, v in self.buffer.items() if k != "info"} ep_info = {k: torch.cat(v, dim=0) for k, v in self.buffer["info"].items()} ep = Episode(**ep_dict, info=ep_info).to("cpu") self.rec_dataset.add_episode(ep) self.rec_dataset.save_to_default_path() self.obs = next_obs self.t += 1 return next_obs, rew, end, trunc, info