gameboy_worlds.interface.environment
1from abc import abstractmethod, ABC 2from typing import Optional, Type, Dict, Any, List, Tuple 3 4from gameboy_worlds.utils import ( 5 load_parameters, 6 log_error, 7 log_info, 8 log_warn, 9 get_lowest_level_subclass, 10 verify_parameters, 11 log_dict, 12 import_pygame, 13) 14 15 16from gameboy_worlds.emulation import Emulator, StateTracker, TestTrackerMixin 17from gameboy_worlds.emulation.registry import ( 18 get_state_tracker_class, 19 get_train_init_states, 20) 21from gameboy_worlds.interface.controller import Controller 22from gameboy_worlds.interface.action import HighLevelAction 23 24import numpy as np 25import gymnasium as gym 26import warnings 27from copy import deepcopy 28import uuid 29 30warnings.filterwarnings( 31 "ignore", 32 message="pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html", 33) 34# This is to ignore deprecation warnings from pygame about pkg_resources 35 36 37class Environment(gym.Env, ABC): 38 """Base class for environments interfacing with the emulator.""" 39 40 REQUIRED_EMULATOR = Emulator 41 """ The highest level emulator that the environment can interface with. """ 42 43 REQUIRED_STATE_TRACKER = StateTracker 44 """ The state tracker that tracks the minimal state information required for the environment to function. """ 45 46 @staticmethod 47 def override_emulator_kwargs(emulator_kwargs: dict) -> dict: 48 """ 49 Override default emulator keyword arguments for this environment. 50 51 Override this method in subclasses to modify the default emulator keyword arguments. 52 53 You may want to use `override_state_tracker_class` or that style to ensure compatibility of state tracker classes. 54 55 Args: 56 emulator_kwargs (dict): Incoming emulator keyword arguments. 57 Returns: 58 dict: The overridden emulator keyword arguments. 59 """ 60 return emulator_kwargs 61 62 @staticmethod 63 def override_state_tracker_class( 64 emulator_kwargs: dict, required_state_tracker_class: Type[StateTracker] 65 ): 66 """ 67 Safely overrides the state tracker class for the environment. 68 69 Use this in `override_emulator_kwargs` to ensure that the lowest level state tracker class is chosen. 70 71 Args: 72 emulator_kwargs (dict): Incoming emulator keyword arguments. 73 required_state_tracker_class (Type[StateTracker]): Usually the required state tracker class for the environment. 74 """ 75 game = emulator_kwargs["game"] 76 has_option = "state_tracker_class" in emulator_kwargs 77 incoming_state_tracker_class = emulator_kwargs.get( 78 "state_tracker_class", "default" 79 ) 80 if isinstance(incoming_state_tracker_class, str): 81 incoming_state_tracker_class = get_state_tracker_class( 82 game, incoming_state_tracker_class 83 ) 84 if issubclass(incoming_state_tracker_class, required_state_tracker_class): 85 return incoming_state_tracker_class 86 elif issubclass(required_state_tracker_class, incoming_state_tracker_class): 87 emulator_kwargs["state_tracker_class"] = required_state_tracker_class 88 else: 89 emulator_kwargs["state_tracker_class"] = ( 90 incoming_state_tracker_class # Don't know which one to pick, so just go with the incoming one. 91 ) 92 return 93 94 def __init__( 95 self, 96 emulator: Emulator, 97 controller: Controller, 98 parameters: Optional[dict] = None, 99 ): 100 """ 101 Ensures that the environment has the required attributes. 102 All subclasses must call this __init__ method AFTER setting up the required attributes. 103 104 If you are implementing a subclass, ensure that the following attributes are set: 105 - observation_space: gym space defining observation space structure 106 107 """ 108 self._parameters = load_parameters(parameters) 109 self._emulator = emulator 110 self._controller = controller 111 required_attributes = ["observation_space"] 112 for attr in required_attributes: 113 if not hasattr(self, attr): 114 log_error( 115 f"Environment requires attribute '{attr}' to be set. Implement this in the subclass __init__", 116 self._parameters, 117 ) 118 self.observation_space: gym.spaces.Space = self.observation_space 119 if not issubclass(type(self._emulator), self.REQUIRED_EMULATOR): 120 log_error( 121 f"Environment requires an Emulator of type {self.REQUIRED_EMULATOR}, but got {type(self._emulator)}", 122 self._parameters, 123 ) 124 if not isinstance(self._controller, Controller): 125 log_error( 126 f"Environment requires a Controller instance, but got {type(self._controller)}", 127 self._parameters, 128 ) 129 self.REQUIRED_STATE_TRACKER = get_lowest_level_subclass( 130 [self.REQUIRED_STATE_TRACKER, self._controller.REQUIRED_STATE_TRACKER] 131 ) 132 if not issubclass( 133 type(self._emulator.state_tracker), self.REQUIRED_STATE_TRACKER 134 ): 135 log_error( 136 f"Environment requires a StateTracker of type {self.REQUIRED_STATE_TRACKER}, but got {type(self._emulator.state_tracker)}", 137 self._parameters, 138 ) 139 self._controller.assign_emulator(self._emulator) 140 self._rng = np.random.default_rng() 141 self.action_space = self._controller.get_action_space() 142 """ The Gym action Space provided by the controller. """ 143 self.actions = self._controller.ACTIONS 144 """ A list of HighLevelAction Types provided by the controller. """ 145 self.render_mode = "human" 146 """ The render mode of the environment. Supports 'human' and 'rgb_array', but strongly assumes 'human' as can just read the emulator screen from `get_info` """ 147 self._window = None 148 """ The pygame window for rendering in 'human' mode. Initialized on first render call. """ 149 self._clock = None 150 """ The pygame clock for rendering in 'human' mode. Initialized on first render call. """ 151 self.reset() # I don't think this will cause issues, but should check that resetting here works well with gymnasium SyncVectorEnv final_obs construction. 152 153 def save_custom_state(self, state_name: str): 154 """ 155 Saves a custom state of the emulator. This is useful for saving states during training or evaluation that can be loaded later for analysis or replay. 156 157 Args: 158 state_name (str): Name of the state to save. This will be saved as a .state file in the states directory. 159 """ 160 # don't allow path like state names 161 if ( 162 "/" in state_name 163 or "\\" in state_name 164 or " " in state_name 165 or not state_name.isalnum() 166 ): 167 log_error( 168 f"State name '{state_name}' is invalid. State names must be alphanumeric and cannot contain spaces or path characters.", 169 self._parameters, 170 ) 171 state_name = state_name.replace( 172 "custom_", "" 173 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 174 state_name = f"custom_{state_name}" 175 self._emulator.save_state(state_name=state_name) 176 return state_name 177 178 def delete_custom_state(self, state_name: str): 179 """ 180 Deletes a custom state of the emulator that was previously saved with `save_custom_state`. 181 182 Args: 183 state_name (str): Name of the state to delete. This should be the name returned by `save_custom_state`. 184 """ 185 state_name = state_name.replace( 186 "custom_", "" 187 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 188 state_name = f"custom_{state_name}" 189 self._emulator.delete_state(state_name=state_name) 190 191 def load_custom_state(self, state_name: str): 192 """ 193 Loads a custom state of the emulator that was previously saved with `save_custom_state`. 194 195 Args: 196 state_name (str): Name of the state to load. This should be the name returned by `save_custom_state`. 197 """ 198 state_name = f"custom_{state_name}" 199 self._emulator.set_init_state(state_name) 200 self.reset() # reset to apply the new init state 201 202 @abstractmethod 203 def get_observation( 204 self, 205 *, 206 action: Optional[HighLevelAction] = None, 207 action_kwargs: Optional[dict] = None, 208 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 209 action_success: Optional[int] = None, 210 ) -> gym.spaces.Space: 211 """ 212 Returns the current observation from the emulator. Must match self.observation_space. 213 Args: 214 action (Optional[HighLevelAction]): The previous action taken. 215 action_kwargs (dict): The keyword arguments used for the action. 216 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 217 action_success (Optional[int]): The success code of the action. 218 219 Returns: 220 observation (gym.spaces.Space): The current observation. 221 """ 222 raise NotImplementedError 223 224 def get_info( 225 self, 226 *, 227 action: Optional[HighLevelAction] = None, 228 action_kwargs: Optional[dict] = None, 229 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 230 action_success: Optional[int] = None, 231 ) -> Dict[str, Dict[str, Any]]: 232 """ 233 Returns the full state information as defined by the emulator's state tracker. 234 235 Creates additional fields: 236 - "core"/"previous_action_details": A tuple of (action, action_kwargs, transition_states, action_success, action_return) 237 - "core"/"transition_passed_frames": An array of all frames passed during the action execution 238 - "ocr"/"transition_ocr_regions": A list of OCR regions captured during the action execution 239 240 :param action: HighLevelAction taken 241 :type action: Optional[HighLevelAction] 242 :param action_kwargs: Keyword arguments for the action 243 :type action_kwargs: Optional[dict] 244 :param transition_states: List of states observed during the action execution 245 :type transition_states: Optional[List[Dict[str, Dict[str, Any]]]] 246 :param action_success: Success code of the action 247 :type action_success: Optional[int] 248 :return: Full state information from the state tracker 249 :rtype: Dict[str, Dict[str, Any]] 250 """ 251 state_info = self._emulator.state_tracker.report() 252 if action is not None: # then transition_states should not be empty 253 # Attach the action details to the info 254 last_state = transition_states[-1] 255 if "action_return" in last_state["core"]: 256 action_return = last_state["core"]["action_return"] 257 else: 258 action_return = None 259 state_info["core"]["previous_action_details"] = ( 260 action, 261 action_kwargs, 262 transition_states, 263 action_success, 264 action_return, 265 ) 266 267 # Aggregate passed frames from transition states 268 all_passed_frames = transition_states[0]["core"]["passed_frames"] 269 for transition_state in transition_states[1:]: 270 all_passed_frames = np.concatenate( 271 [all_passed_frames, transition_state["core"]["passed_frames"]], 272 axis=0, 273 ) 274 state_info["core"][ 275 "transition_passed_frames" 276 ] = all_passed_frames # Will include the current state info last frame as as the final entry 277 278 # Aggregate OCR texts from transition states 279 all_ocr_regions = [] 280 for transition_state in transition_states: 281 if ( 282 "ocr" in transition_state 283 and "ocr_regions" in transition_state["ocr"] 284 ): 285 all_ocr_regions.append(transition_state["ocr"]["ocr_regions"]) 286 if "ocr" in state_info: 287 state_info["ocr"]["transition_ocr_regions"] = all_ocr_regions 288 else: 289 state_info["ocr"] = {"transition_ocr_regions": all_ocr_regions} 290 return state_info 291 292 def get_final_info(self) -> Dict[str, Dict[str, Any]]: 293 """ 294 Returns the final state information from the emulator when all episodes are done. 295 Will involve summaries over all episodes played. 296 Returns: 297 info (dict): The final state information from the state tracker. 298 """ 299 return self._emulator.state_tracker.report_final() 300 301 def reset( 302 self, *, seed: Optional[int] = None, options: Optional[dict] = None 303 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 304 """ 305 Resets the environment and emulator to the initial state. 306 Args: 307 seed (int, optional): Seed for random number generators. 308 options (dict, optional): Additional options for resetting the environment. 309 Returns: 310 observation (object): The initial observation of the environment. 311 312 info (dict): Additional information about the reset. 313 """ 314 super().reset(seed=seed, options=options) 315 self._emulator.reset() 316 self.seed(seed) 317 observation, info = self.get_observation(), self.get_info() 318 return observation, info 319 320 @abstractmethod 321 def determine_reward( 322 self, 323 start_state: Dict[str, Dict[str, Any]], 324 *, 325 action: Optional[HighLevelAction] = None, 326 action_kwargs: Optional[dict] = None, 327 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 328 action_success: Optional[int] = None, 329 ) -> float: 330 """ 331 Determines the reward based on the transition from start_state through transition_states. 332 Args: 333 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 334 action (HighLevelAction): The HighLevelAction action taken. 335 action_kwargs (dict): The keyword arguments used for the action. 336 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 337 action_success (bool): Whether the action was successful. 338 Returns: 339 float: The computed reward. 340 """ 341 raise NotImplementedError 342 343 def determine_truncated( 344 self, 345 start_state: Dict[str, Dict[str, Any]], 346 *, 347 action: Optional[HighLevelAction] = None, 348 action_kwargs: Optional[dict] = None, 349 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 350 action_success: Optional[int] = None, 351 ) -> bool: 352 """ 353 Determines whether the episode playthrough has exceeded some maximum step count or other truncation criteria based on the transition from start_state through transition_states. 354 This method is can be overidden to implement custom truncation logic, but it must always return: 355 `super().determine_truncated() or <custom_truncation_logic_bool>` 356 357 Args: 358 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 359 action (HighLevelAction): The HighLevelAction action taken. 360 action_kwargs (dict): The keyword arguments used for the action. 361 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 362 action_success (bool): Whether the action was successful. 363 Returns: 364 bool: Whether the episode is terminated. 365 """ 366 return self._emulator.check_if_done() 367 368 @abstractmethod 369 def determine_terminated( 370 self, 371 start_state: Dict[str, Dict[str, Any]], 372 *, 373 action: Optional[HighLevelAction] = None, 374 action_kwargs: Optional[dict] = None, 375 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 376 action_success: Optional[int] = None, 377 ) -> bool: 378 """ 379 Determines whether the episode reaches the goal / terminal state based on the transition from start_state through transition_states. 380 This method is NOT meant to be used to determine if the step count has exceeded the maximum. 381 382 Args: 383 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 384 action (HighLevelAction): The HighLevelAction action taken. 385 action_kwargs (dict): The keyword arguments used for the action. 386 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 387 action_success (bool): Whether the action was successful. 388 Returns: 389 bool: Whether the episode is terminated. 390 """ 391 pass 392 393 def before_step(self, action: Type[HighLevelAction], action_kwargs: dict): 394 """ 395 Implement any logic that needs to be executed before each step in the environment. 396 """ 397 return 398 399 def after_step( 400 self, 401 start_state: Dict[str, Dict[str, Any]], 402 action: Type[HighLevelAction], 403 action_kwargs: dict, 404 transition_states: List[Dict[str, Dict[str, Any]]], 405 action_success: int, 406 ): 407 """ 408 Implement any logic that needs to be executed after each step in the environment. 409 410 Args: 411 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 412 action (HighLevelAction): The HighLevelAction action taken. 413 action_kwargs (dict): The keyword arguments used for the action. 414 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 415 action_success (int): Whether the action was successful. 416 """ 417 return 418 419 def step( 420 self, action: gym.spaces.OneOf 421 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 422 """ 423 Executes the given Gym Space action in the environment via the controller. 424 Use step_high_level_action to execute high level actions directly. 425 426 Args: 427 action (gym.spaces.OneOf): The action to execute. Must be a valid action in the controller's action space. 428 429 Returns: 430 observation (gym.spaces.Space): The observation after executing the action. 431 reward (float): The reward obtained from executing the action. 432 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 433 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 434 info (Dict[str, Dict[str, Any]]): Full state information. 435 """ 436 high_level_action, kwargs = self._controller._space_action_to_high_level_action( 437 action 438 ) 439 return self.step_high_level_action(high_level_action, **kwargs) 440 441 def step_high_level_action( 442 self, action: Type[HighLevelAction], **kwargs 443 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 444 """ 445 Executes the given High Level action in the environment via the controller. 446 If the action is invalid according to the controller, will not perform any action and will simply return the current observation, a reward of 0, and terminated and truncated as False. The info will also include a field "invalid_action"=True to indicate that the action was invalid. 447 448 :param action: The high level action class to execute. 449 :type action: Type[HighLevelAction] 450 :param kwargs: Additional arguments required for the specific high level action. 451 :type kwargs: Dict[str, Any] 452 :return: 453 - observation (gym.spaces.Space): The observation after executing the action. 454 455 - reward (float): The reward obtained from executing the action. 456 457 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 458 459 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 460 461 - info (Dict[str, Dict[str, Any]]): Full state information. 462 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 463 """ 464 if self._emulator.check_if_done(): 465 log_error( 466 "Cannot step environment because emulator indicates done. Please reset the environment.", 467 self._parameters, 468 ) 469 start_state = self.get_info() 470 self.before_step(action, kwargs) 471 transition_states, action_success = self._controller.execute(action, **kwargs) 472 if ( 473 transition_states is None 474 ): # then the action was not a valid one according to the controller. 475 observation = self.get_observation() 476 current_state = self.get_info() 477 terminated = self.determine_terminated(start_state=start_state) 478 truncated = self.determine_truncated(start_state=start_state) 479 reward = self.determine_reward(start_state=start_state) - abs( 480 self._parameters["invalid_action_penalty"] 481 ) 482 current_state["invalid_action"] = True 483 return observation, reward, terminated, truncated, current_state 484 self.after_step(start_state, action, kwargs, transition_states, action_success) 485 truncated = self.determine_truncated( 486 start_state=start_state, 487 action=action, 488 action_kwargs=kwargs, 489 transition_states=transition_states, 490 action_success=action_success, 491 ) 492 493 observation = self.get_observation( 494 action=action, 495 action_kwargs=kwargs, 496 transition_states=transition_states, 497 action_success=action_success, 498 ) 499 current_state = self.get_info( 500 action=action, 501 action_kwargs=kwargs, 502 transition_states=transition_states, 503 action_success=action_success, 504 ) 505 terminated = self.determine_terminated( 506 start_state=start_state, 507 action=action, 508 action_kwargs=kwargs, 509 transition_states=transition_states, 510 action_success=action_success, 511 ) 512 513 reward = self.determine_reward( 514 start_state=start_state, 515 action=action, 516 action_kwargs=kwargs, 517 transition_states=transition_states, 518 action_success=action_success, 519 ) 520 return observation, reward, terminated, truncated, current_state 521 522 def step_str( 523 self, input_str: str 524 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 525 """ 526 Attempts to execute an input string representation of an action. 527 Useful for human play or VLM interaction. 528 If the action is an invalid string, will not perform any action and will simply return Nones. 529 530 :param input_str: The input string representing the action. 531 :type input_str: str 532 :return: 533 - observation (gym.spaces.Space): The observation after executing the action. 534 535 - reward (float): The reward obtained from executing the action. 536 537 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 538 539 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 540 541 - info (Dict[str, Dict[str, Any]]): Full state information. 542 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 543 """ 544 action, kwargs = self.string_to_high_level_action(input_str) 545 if ( 546 action is None 547 ): # not a valid action, will not perform an action and will simply return Nones. 548 return None, None, None, None, None 549 return self.step_high_level_action(action, **kwargs) 550 551 def string_to_high_level_action( 552 self, input_str: str 553 ) -> Tuple[Optional[Type[HighLevelAction]], Optional[dict]]: 554 """ 555 Attempts to convert an input string representation of an action into a HighLevelAction and its parameters. 556 Useful for human play or VLM interaction. 557 558 :param input_str: The input string representing the action. 559 :type input_str: str 560 :return: A tuple containing the HighLevelAction class and its execution parameters dictionary. If the input string is invalid, returns (None, None). 561 :rtype: Tuple[Type[HighLevelAction] | None, dict | None] 562 """ 563 return self._controller.string_to_high_level_action(input_str) 564 565 def _simulate(self, step_fn, *args, **kwargs): 566 """ 567 Executes step_fn(*args, **kwargs) without permanently advancing state. 568 569 Saves the emulator's current state, runs the step, then restores both the 570 emulator's runtime state and its init_state pointer, and cleans up the 571 temporary save file. 572 """ 573 original_init_state = self._emulator.init_state 574 tmp_name = uuid.uuid4().hex 575 self.save_custom_state(tmp_name) 576 try: 577 result = step_fn(*args, **kwargs) 578 finally: 579 self.load_custom_state(tmp_name) 580 self._emulator.init_state = original_init_state 581 self.delete_custom_state(tmp_name) 582 return result 583 584 def sim( 585 self, action: gym.spaces.OneOf 586 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 587 """Like `step` but reverts the emulator to its pre-step state afterward. 588 589 .. warning:: 590 Because reversion requires a full emulator reset, all state tracker counters 591 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 592 a side effect. The returned info reflects the simulated step, not post-reset state. 593 """ 594 return self._simulate(self.step, action) 595 596 def sim_str( 597 self, input_str: str 598 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 599 """Like `step_str` but reverts the emulator to its pre-step state afterward. 600 601 .. warning:: 602 Because reversion requires a full emulator reset, all state tracker counters 603 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 604 a side effect. The returned info reflects the simulated step, not post-reset state. 605 """ 606 return self._simulate(self.step_str, input_str) 607 608 def sim_high_level_action( 609 self, action: Type[HighLevelAction], **kwargs 610 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 611 """Like `step_high_level_action` but reverts the emulator to its pre-step state afterward. 612 613 .. warning:: 614 Because reversion requires a full emulator reset, all state tracker counters 615 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 616 a side effect. The returned info reflects the simulated step, not post-reset state. 617 """ 618 return self._simulate(self.step_high_level_action, action, **kwargs) 619 620 def close(self): 621 """ 622 Closes the environment and the underlying emulator. 623 """ 624 log_info("Closing environment and emulator.", self._parameters) 625 self._emulator.close() 626 627 def _screen_render(self, screen: np.ndarray): 628 """ 629 Renders the given screen using pygame in human mode. 630 Args: 631 screen (np.ndarray): The screen to render. 632 633 """ 634 pygame = import_pygame(self._parameters) 635 if self._window is None: 636 pygame.init() 637 pygame.display.init() 638 self._window = pygame.display.set_mode( 639 (self._emulator.screen_shape[0], self._emulator.screen_shape[1]) 640 ) 641 if self._clock is None: 642 self._clock = pygame.time.Clock() 643 rgb = np.stack([screen[:, :, 0], screen[:, :, 0], screen[:, :, 0]], axis=2) 644 pygame.surfarray.blit_array(self._window, rgb.swapaxes(0, 1)) 645 pygame.display.flip() 646 self._clock.tick(60) # Limit to 60 FPS 647 648 def render(self) -> Optional[np.ndarray]: 649 """ 650 Gets the current screen from the emulator and renders it. 651 652 Use this method only if you want to generally run the emulator in headless mode but still want to see the screen occasionally. 653 654 Do not call this method if the emulator is not headless, you should already have a PyBoy interactive window open in that case. 655 656 Returns: 657 If render_mode is 'rgb_array', returns the current screen as a numpy array. However this is always accessible via self.get_info()['core']['current_frame'], so this is mostly for Gym compatibility. 658 """ 659 if self._emulator.headless == False: 660 log_error( 661 "You probably don't want to call render() when the emulator is not headless.", 662 self._parameters, 663 ) 664 screen = self._emulator.get_current_frame() # shape: 144, 160, 1 665 if self.render_mode == "human": 666 self._screen_render(screen) 667 elif self.render_mode == "rgb_array": 668 return screen 669 else: 670 log_error(f"Unsupported render mode: {self.render_mode}", self._parameters) 671 672 def seed(self, seed: Optional[int] = None): 673 """ 674 Seeds the environment's random number generator and the controller's RNG. 675 676 Args: 677 seed (int, optional): The seed value. 678 """ 679 self._controller.seed(seed) 680 self._rng = np.random.default_rng(seed) 681 682 def render_obs( 683 self, 684 *, 685 action: Optional[Type[HighLevelAction]] = None, 686 action_kwargs: Optional[dict] = None, 687 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 688 action_success: Optional[int] = None, 689 action_return: Optional[Dict[str, Any]] = None, 690 ): 691 """ 692 Provide a way to render the output of `get_observation` to a human. 693 Implement if you want to use the human_step_play method. 694 695 Args: 696 action (Optional[Type[HighLevelAction]]): The previous action taken. 697 action_kwargs (dict): The keyword arguments used for the action. 698 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 699 action_success (Optional[int]): The success code of the action. 700 """ 701 raise NotImplementedError 702 703 def render_info( 704 self, 705 *, 706 action: Optional[Type[HighLevelAction]] = None, 707 action_kwargs: Optional[dict] = None, 708 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 709 action_success: Optional[int] = None, 710 action_return: Optional[Dict[str, Any]] = None, 711 ): 712 """ 713 Provide a way to render the output of `get_info` to a human. 714 Implement if you want to use the human_step_play method with show_info=True. 715 Must always be a superset of render_obs (i.e. should also show what render_obs shows). 716 717 Args: 718 action (Optional[Type[HighLevelAction]]): The previous action taken. 719 action_kwargs (dict): The keyword arguments used for the action. 720 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 721 action_success (Optional[int]): The success code of the action. 722 """ 723 raise NotImplementedError 724 725 def get_action_strings( 726 self, return_all: bool = False 727 ) -> Dict[Type[HighLevelAction], str]: 728 """ 729 Provide a way to verbalize the allowed high level actions, along with the format of the input parameters. 730 Useful for prompting a VLM to choose an action. 731 732 :param return_all: If True, returns all possible actions and parameter formats. If False, returns only the actions that are valid in the current state. 733 :type return_all: bool 734 :return: A dictionary mapping high level actions to their verbalizations and input formats. 735 :rtype: Dict[Type[HighLevelAction], str] 736 """ 737 return self._controller.get_action_strings(return_all=return_all) 738 739 def human_step_play( 740 self, max_steps: int = 50, show_obs: bool = True, show_info: bool = True 741 ) -> Tuple[List[float], bool, bool]: 742 """ 743 Opens a render window and allow the human to play through the environment as an agent would 744 745 Args: 746 max_steps (int): max steps to take 747 show_obs (bool): whether to show the observation space rendering after each step 748 show_info (bool): whether to show the info rendering after each step. Will force show_obs=False if enabled. 749 750 Returns: 751 rewards (List[float]): List of rewards obtained at each step. 752 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 753 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed emulator steps). It does not consider the max_steps parameter here. 754 """ 755 observation, info = self.reset() 756 self.render_mode = "human" 757 log_info(f"Doing human step play for {max_steps} max steps...") 758 steps = 0 759 done = False 760 terminated = False 761 truncated = False 762 rewards = [] 763 if show_info: 764 self.render_info() 765 show_obs = False 766 if show_obs: 767 self.render_obs() 768 while not done and steps < max_steps: 769 action_input = self._controller.get_action_strings() 770 action_input_str = "" 771 for action_class, action_str in action_input.items(): 772 action_input_str += f"- {action_str}\n" 773 log_info(f"Allowed Actions: \n{action_input_str}", self._parameters) 774 input_str = input("Enter Action: ").strip() 775 ( 776 possible_obs, 777 possible_reward, 778 possible_terminated, 779 possible_truncated, 780 possible_info, 781 ) = self.step_str(input_str) 782 if possible_obs is not None: 783 observation, reward, terminated, truncated, info = ( 784 possible_obs, 785 possible_reward, 786 possible_terminated, 787 possible_truncated, 788 possible_info, 789 ) 790 rewards.append(reward) 791 if reward != 0: 792 log_info(f"Reward obtained: {reward}", self._parameters) 793 ( 794 action, 795 action_kwargs, 796 transition_states, 797 action_success, 798 action_return, 799 ) = info["core"]["previous_action_details"] 800 if show_info: 801 self.render_info( 802 action=action, 803 action_kwargs=action_kwargs, 804 transition_states=transition_states, 805 action_success=action_success, 806 action_return=action_return, 807 ) 808 if show_obs: 809 self.render_obs( 810 action=action, 811 action_kwargs=action_kwargs, 812 transition_states=transition_states, 813 action_success=action_success, 814 action_return=action_return, 815 ) 816 else: 817 log_warn("That was not a valid input. did nothing", self._parameters) 818 if terminated or truncated: 819 log_info( 820 f"Episode finished! Terminated: {terminated}, Truncated: {truncated}", 821 self._parameters, 822 ) 823 break 824 steps += 1 825 if steps >= max_steps: 826 log_info( 827 f"Max steps {max_steps} reached. Ending episode.", self._parameters 828 ) 829 return rewards, terminated, truncated 830 831 832class DummyEnvironment(Environment): 833 """A dummy environment that does nothing special.""" 834 835 def __init__( 836 self, 837 emulator: Emulator, 838 controller: Controller, 839 parameters: Optional[dict] = None, 840 ): 841 """ 842 Initializes the DummyEnvironment with the given emulator and controller. 843 844 It is safe to overwrite the self.observation_space in the subclass after calling this __init__ method. 845 """ 846 screen_shape = emulator.screen_shape 847 self.observation_space = gym.spaces.Box( 848 low=0, high=255, shape=(screen_shape[1], screen_shape[0]), dtype=np.uint8 849 ) 850 """ The observation space is the raw pixel values of the emulator's screen. """ 851 super().__init__( 852 emulator=emulator, controller=controller, parameters=parameters 853 ) 854 855 def get_observation( 856 self, 857 *, 858 action=None, 859 action_kwargs=None, 860 transition_states=None, 861 action_success=None, 862 ): 863 if transition_states is None: 864 current_state = self.get_info() 865 screen = current_state["core"]["current_frame"] 866 else: 867 screen = transition_states[-1]["core"]["current_frame"] 868 return screen 869 870 def determine_reward(self, **kwargs): 871 return 0.0 872 873 def determine_terminated(self, **kwargs): 874 return False 875 876 def render_obs( 877 self, 878 action=None, 879 action_kwargs=None, 880 transition_states=None, 881 action_success=None, 882 action_return=None, 883 ): # Might cause issues if you try to render() as well 884 """ 885 Renders the screen. 886 """ 887 screen = self.get_observation() 888 self._screen_render(screen) 889 890 def render_info( 891 self, 892 action=None, 893 action_kwargs=None, 894 transition_states=None, 895 action_success=None, 896 action_return=None, 897 ): 898 info = deepcopy(self.get_info()) 899 if transition_states is not None and len(transition_states) > 0: 900 screens = transition_states[0]["core"]["passed_frames"] 901 for transition_state in transition_states[1:]: 902 screens = np.concatenate( 903 [screens, transition_state["core"]["passed_frames"]], axis=0 904 ) 905 else: 906 if "passed_frames" in info["core"]: 907 screens = info["core"]["passed_frames"] 908 else: 909 screens = None 910 if screens is None: 911 screens = [info["core"]["current_frame"]] 912 for screen in screens: 913 self._screen_render(screen) 914 info["core"].pop("current_frame") 915 info["core"].pop("passed_frames") 916 if "ocr" in info: 917 info.pop("ocr") 918 if "transition_passed_frames" in info["core"]: 919 info["core"].pop("transition_passed_frames") 920 if "previous_action_details" in info["core"]: 921 info["core"]["previous_action_details"] = ( 922 info["core"]["previous_action_details"][:2] 923 + info["core"]["previous_action_details"][3:] 924 ) # remove transition states to avoid huge logs 925 log_info("State: ", self._parameters) 926 log_dict(info, parameters=self._parameters) 927 928 929class TestEnvironmentMixin: 930 """ 931 Mixin class for testing environments. 932 Ensures the State Tracker used is a TestTrackerMixin and checks these for termination / truncation. 933 """ 934 935 REQUIRED_STATE_TRACKER = TestTrackerMixin 936 937 def determine_truncated( 938 self, 939 start_state: Dict[str, Dict[str, Any]], 940 *, 941 action: Optional[HighLevelAction] = None, 942 action_kwargs: Optional[dict] = None, 943 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 944 action_success: Optional[int] = None, 945 ) -> bool: 946 if not isinstance(self._emulator.state_tracker, TestTrackerMixin): 947 log_error( 948 "TestEnvironmentMixin requires the emulator's state tracker to be a TestTrackerMixin.", 949 self._parameters, 950 ) # we don't need to repeat this for determine_terminated since its done here. 951 if transition_states is None: 952 return self._emulator.check_if_done() 953 any_truncated = False 954 for state in transition_states: 955 if state["termination_truncation"]["truncated"]: 956 any_truncated = True 957 break 958 return any_truncated or self._emulator.check_if_done() 959 960 def determine_terminated( 961 self, 962 start_state: Dict[str, Dict[str, Any]], 963 *, 964 action: Optional[HighLevelAction] = None, 965 action_kwargs: Optional[dict] = None, 966 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 967 action_success: Optional[int] = None, 968 ) -> bool: 969 if transition_states is None: 970 return False 971 any_terminated = False 972 for state in transition_states: 973 if state["termination_truncation"]["terminated"]: 974 any_terminated = True 975 break 976 return any_terminated 977 978 979class TrainEnvironmentMixin: 980 """ 981 Mixin class for training environments. 982 Records the allowed initial states for training and random shuffles between them when resetting. 983 """ 984 985 def reset( 986 self, *, seed: Optional[int] = None, options: Optional[dict] = None 987 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 988 """ 989 990 Args: 991 seed (int, optional): Seed for random number generators. 992 options (dict, optional): Additional options for resetting the environment. 993 Returns: 994 observation (object): The initial observation of the environment. 995 996 info (dict): Additional information about the reset. 997 """ 998 game = self._emulator.game 999 init_states = get_train_init_states(game, parameters=self._parameters) 1000 choice = self._rng.choice(len(init_states)) 1001 init_state = init_states[choice] 1002 self._emulator.set_init_state(init_state) 1003 return super().reset(seed=seed, options=options)
38class Environment(gym.Env, ABC): 39 """Base class for environments interfacing with the emulator.""" 40 41 REQUIRED_EMULATOR = Emulator 42 """ The highest level emulator that the environment can interface with. """ 43 44 REQUIRED_STATE_TRACKER = StateTracker 45 """ The state tracker that tracks the minimal state information required for the environment to function. """ 46 47 @staticmethod 48 def override_emulator_kwargs(emulator_kwargs: dict) -> dict: 49 """ 50 Override default emulator keyword arguments for this environment. 51 52 Override this method in subclasses to modify the default emulator keyword arguments. 53 54 You may want to use `override_state_tracker_class` or that style to ensure compatibility of state tracker classes. 55 56 Args: 57 emulator_kwargs (dict): Incoming emulator keyword arguments. 58 Returns: 59 dict: The overridden emulator keyword arguments. 60 """ 61 return emulator_kwargs 62 63 @staticmethod 64 def override_state_tracker_class( 65 emulator_kwargs: dict, required_state_tracker_class: Type[StateTracker] 66 ): 67 """ 68 Safely overrides the state tracker class for the environment. 69 70 Use this in `override_emulator_kwargs` to ensure that the lowest level state tracker class is chosen. 71 72 Args: 73 emulator_kwargs (dict): Incoming emulator keyword arguments. 74 required_state_tracker_class (Type[StateTracker]): Usually the required state tracker class for the environment. 75 """ 76 game = emulator_kwargs["game"] 77 has_option = "state_tracker_class" in emulator_kwargs 78 incoming_state_tracker_class = emulator_kwargs.get( 79 "state_tracker_class", "default" 80 ) 81 if isinstance(incoming_state_tracker_class, str): 82 incoming_state_tracker_class = get_state_tracker_class( 83 game, incoming_state_tracker_class 84 ) 85 if issubclass(incoming_state_tracker_class, required_state_tracker_class): 86 return incoming_state_tracker_class 87 elif issubclass(required_state_tracker_class, incoming_state_tracker_class): 88 emulator_kwargs["state_tracker_class"] = required_state_tracker_class 89 else: 90 emulator_kwargs["state_tracker_class"] = ( 91 incoming_state_tracker_class # Don't know which one to pick, so just go with the incoming one. 92 ) 93 return 94 95 def __init__( 96 self, 97 emulator: Emulator, 98 controller: Controller, 99 parameters: Optional[dict] = None, 100 ): 101 """ 102 Ensures that the environment has the required attributes. 103 All subclasses must call this __init__ method AFTER setting up the required attributes. 104 105 If you are implementing a subclass, ensure that the following attributes are set: 106 - observation_space: gym space defining observation space structure 107 108 """ 109 self._parameters = load_parameters(parameters) 110 self._emulator = emulator 111 self._controller = controller 112 required_attributes = ["observation_space"] 113 for attr in required_attributes: 114 if not hasattr(self, attr): 115 log_error( 116 f"Environment requires attribute '{attr}' to be set. Implement this in the subclass __init__", 117 self._parameters, 118 ) 119 self.observation_space: gym.spaces.Space = self.observation_space 120 if not issubclass(type(self._emulator), self.REQUIRED_EMULATOR): 121 log_error( 122 f"Environment requires an Emulator of type {self.REQUIRED_EMULATOR}, but got {type(self._emulator)}", 123 self._parameters, 124 ) 125 if not isinstance(self._controller, Controller): 126 log_error( 127 f"Environment requires a Controller instance, but got {type(self._controller)}", 128 self._parameters, 129 ) 130 self.REQUIRED_STATE_TRACKER = get_lowest_level_subclass( 131 [self.REQUIRED_STATE_TRACKER, self._controller.REQUIRED_STATE_TRACKER] 132 ) 133 if not issubclass( 134 type(self._emulator.state_tracker), self.REQUIRED_STATE_TRACKER 135 ): 136 log_error( 137 f"Environment requires a StateTracker of type {self.REQUIRED_STATE_TRACKER}, but got {type(self._emulator.state_tracker)}", 138 self._parameters, 139 ) 140 self._controller.assign_emulator(self._emulator) 141 self._rng = np.random.default_rng() 142 self.action_space = self._controller.get_action_space() 143 """ The Gym action Space provided by the controller. """ 144 self.actions = self._controller.ACTIONS 145 """ A list of HighLevelAction Types provided by the controller. """ 146 self.render_mode = "human" 147 """ The render mode of the environment. Supports 'human' and 'rgb_array', but strongly assumes 'human' as can just read the emulator screen from `get_info` """ 148 self._window = None 149 """ The pygame window for rendering in 'human' mode. Initialized on first render call. """ 150 self._clock = None 151 """ The pygame clock for rendering in 'human' mode. Initialized on first render call. """ 152 self.reset() # I don't think this will cause issues, but should check that resetting here works well with gymnasium SyncVectorEnv final_obs construction. 153 154 def save_custom_state(self, state_name: str): 155 """ 156 Saves a custom state of the emulator. This is useful for saving states during training or evaluation that can be loaded later for analysis or replay. 157 158 Args: 159 state_name (str): Name of the state to save. This will be saved as a .state file in the states directory. 160 """ 161 # don't allow path like state names 162 if ( 163 "/" in state_name 164 or "\\" in state_name 165 or " " in state_name 166 or not state_name.isalnum() 167 ): 168 log_error( 169 f"State name '{state_name}' is invalid. State names must be alphanumeric and cannot contain spaces or path characters.", 170 self._parameters, 171 ) 172 state_name = state_name.replace( 173 "custom_", "" 174 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 175 state_name = f"custom_{state_name}" 176 self._emulator.save_state(state_name=state_name) 177 return state_name 178 179 def delete_custom_state(self, state_name: str): 180 """ 181 Deletes a custom state of the emulator that was previously saved with `save_custom_state`. 182 183 Args: 184 state_name (str): Name of the state to delete. This should be the name returned by `save_custom_state`. 185 """ 186 state_name = state_name.replace( 187 "custom_", "" 188 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 189 state_name = f"custom_{state_name}" 190 self._emulator.delete_state(state_name=state_name) 191 192 def load_custom_state(self, state_name: str): 193 """ 194 Loads a custom state of the emulator that was previously saved with `save_custom_state`. 195 196 Args: 197 state_name (str): Name of the state to load. This should be the name returned by `save_custom_state`. 198 """ 199 state_name = f"custom_{state_name}" 200 self._emulator.set_init_state(state_name) 201 self.reset() # reset to apply the new init state 202 203 @abstractmethod 204 def get_observation( 205 self, 206 *, 207 action: Optional[HighLevelAction] = None, 208 action_kwargs: Optional[dict] = None, 209 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 210 action_success: Optional[int] = None, 211 ) -> gym.spaces.Space: 212 """ 213 Returns the current observation from the emulator. Must match self.observation_space. 214 Args: 215 action (Optional[HighLevelAction]): The previous action taken. 216 action_kwargs (dict): The keyword arguments used for the action. 217 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 218 action_success (Optional[int]): The success code of the action. 219 220 Returns: 221 observation (gym.spaces.Space): The current observation. 222 """ 223 raise NotImplementedError 224 225 def get_info( 226 self, 227 *, 228 action: Optional[HighLevelAction] = None, 229 action_kwargs: Optional[dict] = None, 230 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 231 action_success: Optional[int] = None, 232 ) -> Dict[str, Dict[str, Any]]: 233 """ 234 Returns the full state information as defined by the emulator's state tracker. 235 236 Creates additional fields: 237 - "core"/"previous_action_details": A tuple of (action, action_kwargs, transition_states, action_success, action_return) 238 - "core"/"transition_passed_frames": An array of all frames passed during the action execution 239 - "ocr"/"transition_ocr_regions": A list of OCR regions captured during the action execution 240 241 :param action: HighLevelAction taken 242 :type action: Optional[HighLevelAction] 243 :param action_kwargs: Keyword arguments for the action 244 :type action_kwargs: Optional[dict] 245 :param transition_states: List of states observed during the action execution 246 :type transition_states: Optional[List[Dict[str, Dict[str, Any]]]] 247 :param action_success: Success code of the action 248 :type action_success: Optional[int] 249 :return: Full state information from the state tracker 250 :rtype: Dict[str, Dict[str, Any]] 251 """ 252 state_info = self._emulator.state_tracker.report() 253 if action is not None: # then transition_states should not be empty 254 # Attach the action details to the info 255 last_state = transition_states[-1] 256 if "action_return" in last_state["core"]: 257 action_return = last_state["core"]["action_return"] 258 else: 259 action_return = None 260 state_info["core"]["previous_action_details"] = ( 261 action, 262 action_kwargs, 263 transition_states, 264 action_success, 265 action_return, 266 ) 267 268 # Aggregate passed frames from transition states 269 all_passed_frames = transition_states[0]["core"]["passed_frames"] 270 for transition_state in transition_states[1:]: 271 all_passed_frames = np.concatenate( 272 [all_passed_frames, transition_state["core"]["passed_frames"]], 273 axis=0, 274 ) 275 state_info["core"][ 276 "transition_passed_frames" 277 ] = all_passed_frames # Will include the current state info last frame as as the final entry 278 279 # Aggregate OCR texts from transition states 280 all_ocr_regions = [] 281 for transition_state in transition_states: 282 if ( 283 "ocr" in transition_state 284 and "ocr_regions" in transition_state["ocr"] 285 ): 286 all_ocr_regions.append(transition_state["ocr"]["ocr_regions"]) 287 if "ocr" in state_info: 288 state_info["ocr"]["transition_ocr_regions"] = all_ocr_regions 289 else: 290 state_info["ocr"] = {"transition_ocr_regions": all_ocr_regions} 291 return state_info 292 293 def get_final_info(self) -> Dict[str, Dict[str, Any]]: 294 """ 295 Returns the final state information from the emulator when all episodes are done. 296 Will involve summaries over all episodes played. 297 Returns: 298 info (dict): The final state information from the state tracker. 299 """ 300 return self._emulator.state_tracker.report_final() 301 302 def reset( 303 self, *, seed: Optional[int] = None, options: Optional[dict] = None 304 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 305 """ 306 Resets the environment and emulator to the initial state. 307 Args: 308 seed (int, optional): Seed for random number generators. 309 options (dict, optional): Additional options for resetting the environment. 310 Returns: 311 observation (object): The initial observation of the environment. 312 313 info (dict): Additional information about the reset. 314 """ 315 super().reset(seed=seed, options=options) 316 self._emulator.reset() 317 self.seed(seed) 318 observation, info = self.get_observation(), self.get_info() 319 return observation, info 320 321 @abstractmethod 322 def determine_reward( 323 self, 324 start_state: Dict[str, Dict[str, Any]], 325 *, 326 action: Optional[HighLevelAction] = None, 327 action_kwargs: Optional[dict] = None, 328 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 329 action_success: Optional[int] = None, 330 ) -> float: 331 """ 332 Determines the reward based on the transition from start_state through transition_states. 333 Args: 334 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 335 action (HighLevelAction): The HighLevelAction action taken. 336 action_kwargs (dict): The keyword arguments used for the action. 337 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 338 action_success (bool): Whether the action was successful. 339 Returns: 340 float: The computed reward. 341 """ 342 raise NotImplementedError 343 344 def determine_truncated( 345 self, 346 start_state: Dict[str, Dict[str, Any]], 347 *, 348 action: Optional[HighLevelAction] = None, 349 action_kwargs: Optional[dict] = None, 350 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 351 action_success: Optional[int] = None, 352 ) -> bool: 353 """ 354 Determines whether the episode playthrough has exceeded some maximum step count or other truncation criteria based on the transition from start_state through transition_states. 355 This method is can be overidden to implement custom truncation logic, but it must always return: 356 `super().determine_truncated() or <custom_truncation_logic_bool>` 357 358 Args: 359 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 360 action (HighLevelAction): The HighLevelAction action taken. 361 action_kwargs (dict): The keyword arguments used for the action. 362 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 363 action_success (bool): Whether the action was successful. 364 Returns: 365 bool: Whether the episode is terminated. 366 """ 367 return self._emulator.check_if_done() 368 369 @abstractmethod 370 def determine_terminated( 371 self, 372 start_state: Dict[str, Dict[str, Any]], 373 *, 374 action: Optional[HighLevelAction] = None, 375 action_kwargs: Optional[dict] = None, 376 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 377 action_success: Optional[int] = None, 378 ) -> bool: 379 """ 380 Determines whether the episode reaches the goal / terminal state based on the transition from start_state through transition_states. 381 This method is NOT meant to be used to determine if the step count has exceeded the maximum. 382 383 Args: 384 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 385 action (HighLevelAction): The HighLevelAction action taken. 386 action_kwargs (dict): The keyword arguments used for the action. 387 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 388 action_success (bool): Whether the action was successful. 389 Returns: 390 bool: Whether the episode is terminated. 391 """ 392 pass 393 394 def before_step(self, action: Type[HighLevelAction], action_kwargs: dict): 395 """ 396 Implement any logic that needs to be executed before each step in the environment. 397 """ 398 return 399 400 def after_step( 401 self, 402 start_state: Dict[str, Dict[str, Any]], 403 action: Type[HighLevelAction], 404 action_kwargs: dict, 405 transition_states: List[Dict[str, Dict[str, Any]]], 406 action_success: int, 407 ): 408 """ 409 Implement any logic that needs to be executed after each step in the environment. 410 411 Args: 412 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 413 action (HighLevelAction): The HighLevelAction action taken. 414 action_kwargs (dict): The keyword arguments used for the action. 415 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 416 action_success (int): Whether the action was successful. 417 """ 418 return 419 420 def step( 421 self, action: gym.spaces.OneOf 422 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 423 """ 424 Executes the given Gym Space action in the environment via the controller. 425 Use step_high_level_action to execute high level actions directly. 426 427 Args: 428 action (gym.spaces.OneOf): The action to execute. Must be a valid action in the controller's action space. 429 430 Returns: 431 observation (gym.spaces.Space): The observation after executing the action. 432 reward (float): The reward obtained from executing the action. 433 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 434 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 435 info (Dict[str, Dict[str, Any]]): Full state information. 436 """ 437 high_level_action, kwargs = self._controller._space_action_to_high_level_action( 438 action 439 ) 440 return self.step_high_level_action(high_level_action, **kwargs) 441 442 def step_high_level_action( 443 self, action: Type[HighLevelAction], **kwargs 444 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 445 """ 446 Executes the given High Level action in the environment via the controller. 447 If the action is invalid according to the controller, will not perform any action and will simply return the current observation, a reward of 0, and terminated and truncated as False. The info will also include a field "invalid_action"=True to indicate that the action was invalid. 448 449 :param action: The high level action class to execute. 450 :type action: Type[HighLevelAction] 451 :param kwargs: Additional arguments required for the specific high level action. 452 :type kwargs: Dict[str, Any] 453 :return: 454 - observation (gym.spaces.Space): The observation after executing the action. 455 456 - reward (float): The reward obtained from executing the action. 457 458 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 459 460 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 461 462 - info (Dict[str, Dict[str, Any]]): Full state information. 463 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 464 """ 465 if self._emulator.check_if_done(): 466 log_error( 467 "Cannot step environment because emulator indicates done. Please reset the environment.", 468 self._parameters, 469 ) 470 start_state = self.get_info() 471 self.before_step(action, kwargs) 472 transition_states, action_success = self._controller.execute(action, **kwargs) 473 if ( 474 transition_states is None 475 ): # then the action was not a valid one according to the controller. 476 observation = self.get_observation() 477 current_state = self.get_info() 478 terminated = self.determine_terminated(start_state=start_state) 479 truncated = self.determine_truncated(start_state=start_state) 480 reward = self.determine_reward(start_state=start_state) - abs( 481 self._parameters["invalid_action_penalty"] 482 ) 483 current_state["invalid_action"] = True 484 return observation, reward, terminated, truncated, current_state 485 self.after_step(start_state, action, kwargs, transition_states, action_success) 486 truncated = self.determine_truncated( 487 start_state=start_state, 488 action=action, 489 action_kwargs=kwargs, 490 transition_states=transition_states, 491 action_success=action_success, 492 ) 493 494 observation = self.get_observation( 495 action=action, 496 action_kwargs=kwargs, 497 transition_states=transition_states, 498 action_success=action_success, 499 ) 500 current_state = self.get_info( 501 action=action, 502 action_kwargs=kwargs, 503 transition_states=transition_states, 504 action_success=action_success, 505 ) 506 terminated = self.determine_terminated( 507 start_state=start_state, 508 action=action, 509 action_kwargs=kwargs, 510 transition_states=transition_states, 511 action_success=action_success, 512 ) 513 514 reward = self.determine_reward( 515 start_state=start_state, 516 action=action, 517 action_kwargs=kwargs, 518 transition_states=transition_states, 519 action_success=action_success, 520 ) 521 return observation, reward, terminated, truncated, current_state 522 523 def step_str( 524 self, input_str: str 525 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 526 """ 527 Attempts to execute an input string representation of an action. 528 Useful for human play or VLM interaction. 529 If the action is an invalid string, will not perform any action and will simply return Nones. 530 531 :param input_str: The input string representing the action. 532 :type input_str: str 533 :return: 534 - observation (gym.spaces.Space): The observation after executing the action. 535 536 - reward (float): The reward obtained from executing the action. 537 538 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 539 540 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 541 542 - info (Dict[str, Dict[str, Any]]): Full state information. 543 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 544 """ 545 action, kwargs = self.string_to_high_level_action(input_str) 546 if ( 547 action is None 548 ): # not a valid action, will not perform an action and will simply return Nones. 549 return None, None, None, None, None 550 return self.step_high_level_action(action, **kwargs) 551 552 def string_to_high_level_action( 553 self, input_str: str 554 ) -> Tuple[Optional[Type[HighLevelAction]], Optional[dict]]: 555 """ 556 Attempts to convert an input string representation of an action into a HighLevelAction and its parameters. 557 Useful for human play or VLM interaction. 558 559 :param input_str: The input string representing the action. 560 :type input_str: str 561 :return: A tuple containing the HighLevelAction class and its execution parameters dictionary. If the input string is invalid, returns (None, None). 562 :rtype: Tuple[Type[HighLevelAction] | None, dict | None] 563 """ 564 return self._controller.string_to_high_level_action(input_str) 565 566 def _simulate(self, step_fn, *args, **kwargs): 567 """ 568 Executes step_fn(*args, **kwargs) without permanently advancing state. 569 570 Saves the emulator's current state, runs the step, then restores both the 571 emulator's runtime state and its init_state pointer, and cleans up the 572 temporary save file. 573 """ 574 original_init_state = self._emulator.init_state 575 tmp_name = uuid.uuid4().hex 576 self.save_custom_state(tmp_name) 577 try: 578 result = step_fn(*args, **kwargs) 579 finally: 580 self.load_custom_state(tmp_name) 581 self._emulator.init_state = original_init_state 582 self.delete_custom_state(tmp_name) 583 return result 584 585 def sim( 586 self, action: gym.spaces.OneOf 587 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 588 """Like `step` but reverts the emulator to its pre-step state afterward. 589 590 .. warning:: 591 Because reversion requires a full emulator reset, all state tracker counters 592 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 593 a side effect. The returned info reflects the simulated step, not post-reset state. 594 """ 595 return self._simulate(self.step, action) 596 597 def sim_str( 598 self, input_str: str 599 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 600 """Like `step_str` but reverts the emulator to its pre-step state afterward. 601 602 .. warning:: 603 Because reversion requires a full emulator reset, all state tracker counters 604 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 605 a side effect. The returned info reflects the simulated step, not post-reset state. 606 """ 607 return self._simulate(self.step_str, input_str) 608 609 def sim_high_level_action( 610 self, action: Type[HighLevelAction], **kwargs 611 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 612 """Like `step_high_level_action` but reverts the emulator to its pre-step state afterward. 613 614 .. warning:: 615 Because reversion requires a full emulator reset, all state tracker counters 616 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 617 a side effect. The returned info reflects the simulated step, not post-reset state. 618 """ 619 return self._simulate(self.step_high_level_action, action, **kwargs) 620 621 def close(self): 622 """ 623 Closes the environment and the underlying emulator. 624 """ 625 log_info("Closing environment and emulator.", self._parameters) 626 self._emulator.close() 627 628 def _screen_render(self, screen: np.ndarray): 629 """ 630 Renders the given screen using pygame in human mode. 631 Args: 632 screen (np.ndarray): The screen to render. 633 634 """ 635 pygame = import_pygame(self._parameters) 636 if self._window is None: 637 pygame.init() 638 pygame.display.init() 639 self._window = pygame.display.set_mode( 640 (self._emulator.screen_shape[0], self._emulator.screen_shape[1]) 641 ) 642 if self._clock is None: 643 self._clock = pygame.time.Clock() 644 rgb = np.stack([screen[:, :, 0], screen[:, :, 0], screen[:, :, 0]], axis=2) 645 pygame.surfarray.blit_array(self._window, rgb.swapaxes(0, 1)) 646 pygame.display.flip() 647 self._clock.tick(60) # Limit to 60 FPS 648 649 def render(self) -> Optional[np.ndarray]: 650 """ 651 Gets the current screen from the emulator and renders it. 652 653 Use this method only if you want to generally run the emulator in headless mode but still want to see the screen occasionally. 654 655 Do not call this method if the emulator is not headless, you should already have a PyBoy interactive window open in that case. 656 657 Returns: 658 If render_mode is 'rgb_array', returns the current screen as a numpy array. However this is always accessible via self.get_info()['core']['current_frame'], so this is mostly for Gym compatibility. 659 """ 660 if self._emulator.headless == False: 661 log_error( 662 "You probably don't want to call render() when the emulator is not headless.", 663 self._parameters, 664 ) 665 screen = self._emulator.get_current_frame() # shape: 144, 160, 1 666 if self.render_mode == "human": 667 self._screen_render(screen) 668 elif self.render_mode == "rgb_array": 669 return screen 670 else: 671 log_error(f"Unsupported render mode: {self.render_mode}", self._parameters) 672 673 def seed(self, seed: Optional[int] = None): 674 """ 675 Seeds the environment's random number generator and the controller's RNG. 676 677 Args: 678 seed (int, optional): The seed value. 679 """ 680 self._controller.seed(seed) 681 self._rng = np.random.default_rng(seed) 682 683 def render_obs( 684 self, 685 *, 686 action: Optional[Type[HighLevelAction]] = None, 687 action_kwargs: Optional[dict] = None, 688 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 689 action_success: Optional[int] = None, 690 action_return: Optional[Dict[str, Any]] = None, 691 ): 692 """ 693 Provide a way to render the output of `get_observation` to a human. 694 Implement if you want to use the human_step_play method. 695 696 Args: 697 action (Optional[Type[HighLevelAction]]): The previous action taken. 698 action_kwargs (dict): The keyword arguments used for the action. 699 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 700 action_success (Optional[int]): The success code of the action. 701 """ 702 raise NotImplementedError 703 704 def render_info( 705 self, 706 *, 707 action: Optional[Type[HighLevelAction]] = None, 708 action_kwargs: Optional[dict] = None, 709 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 710 action_success: Optional[int] = None, 711 action_return: Optional[Dict[str, Any]] = None, 712 ): 713 """ 714 Provide a way to render the output of `get_info` to a human. 715 Implement if you want to use the human_step_play method with show_info=True. 716 Must always be a superset of render_obs (i.e. should also show what render_obs shows). 717 718 Args: 719 action (Optional[Type[HighLevelAction]]): The previous action taken. 720 action_kwargs (dict): The keyword arguments used for the action. 721 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 722 action_success (Optional[int]): The success code of the action. 723 """ 724 raise NotImplementedError 725 726 def get_action_strings( 727 self, return_all: bool = False 728 ) -> Dict[Type[HighLevelAction], str]: 729 """ 730 Provide a way to verbalize the allowed high level actions, along with the format of the input parameters. 731 Useful for prompting a VLM to choose an action. 732 733 :param return_all: If True, returns all possible actions and parameter formats. If False, returns only the actions that are valid in the current state. 734 :type return_all: bool 735 :return: A dictionary mapping high level actions to their verbalizations and input formats. 736 :rtype: Dict[Type[HighLevelAction], str] 737 """ 738 return self._controller.get_action_strings(return_all=return_all) 739 740 def human_step_play( 741 self, max_steps: int = 50, show_obs: bool = True, show_info: bool = True 742 ) -> Tuple[List[float], bool, bool]: 743 """ 744 Opens a render window and allow the human to play through the environment as an agent would 745 746 Args: 747 max_steps (int): max steps to take 748 show_obs (bool): whether to show the observation space rendering after each step 749 show_info (bool): whether to show the info rendering after each step. Will force show_obs=False if enabled. 750 751 Returns: 752 rewards (List[float]): List of rewards obtained at each step. 753 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 754 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed emulator steps). It does not consider the max_steps parameter here. 755 """ 756 observation, info = self.reset() 757 self.render_mode = "human" 758 log_info(f"Doing human step play for {max_steps} max steps...") 759 steps = 0 760 done = False 761 terminated = False 762 truncated = False 763 rewards = [] 764 if show_info: 765 self.render_info() 766 show_obs = False 767 if show_obs: 768 self.render_obs() 769 while not done and steps < max_steps: 770 action_input = self._controller.get_action_strings() 771 action_input_str = "" 772 for action_class, action_str in action_input.items(): 773 action_input_str += f"- {action_str}\n" 774 log_info(f"Allowed Actions: \n{action_input_str}", self._parameters) 775 input_str = input("Enter Action: ").strip() 776 ( 777 possible_obs, 778 possible_reward, 779 possible_terminated, 780 possible_truncated, 781 possible_info, 782 ) = self.step_str(input_str) 783 if possible_obs is not None: 784 observation, reward, terminated, truncated, info = ( 785 possible_obs, 786 possible_reward, 787 possible_terminated, 788 possible_truncated, 789 possible_info, 790 ) 791 rewards.append(reward) 792 if reward != 0: 793 log_info(f"Reward obtained: {reward}", self._parameters) 794 ( 795 action, 796 action_kwargs, 797 transition_states, 798 action_success, 799 action_return, 800 ) = info["core"]["previous_action_details"] 801 if show_info: 802 self.render_info( 803 action=action, 804 action_kwargs=action_kwargs, 805 transition_states=transition_states, 806 action_success=action_success, 807 action_return=action_return, 808 ) 809 if show_obs: 810 self.render_obs( 811 action=action, 812 action_kwargs=action_kwargs, 813 transition_states=transition_states, 814 action_success=action_success, 815 action_return=action_return, 816 ) 817 else: 818 log_warn("That was not a valid input. did nothing", self._parameters) 819 if terminated or truncated: 820 log_info( 821 f"Episode finished! Terminated: {terminated}, Truncated: {truncated}", 822 self._parameters, 823 ) 824 break 825 steps += 1 826 if steps >= max_steps: 827 log_info( 828 f"Max steps {max_steps} reached. Ending episode.", self._parameters 829 ) 830 return rewards, terminated, truncated
Base class for environments interfacing with the emulator.
95 def __init__( 96 self, 97 emulator: Emulator, 98 controller: Controller, 99 parameters: Optional[dict] = None, 100 ): 101 """ 102 Ensures that the environment has the required attributes. 103 All subclasses must call this __init__ method AFTER setting up the required attributes. 104 105 If you are implementing a subclass, ensure that the following attributes are set: 106 - observation_space: gym space defining observation space structure 107 108 """ 109 self._parameters = load_parameters(parameters) 110 self._emulator = emulator 111 self._controller = controller 112 required_attributes = ["observation_space"] 113 for attr in required_attributes: 114 if not hasattr(self, attr): 115 log_error( 116 f"Environment requires attribute '{attr}' to be set. Implement this in the subclass __init__", 117 self._parameters, 118 ) 119 self.observation_space: gym.spaces.Space = self.observation_space 120 if not issubclass(type(self._emulator), self.REQUIRED_EMULATOR): 121 log_error( 122 f"Environment requires an Emulator of type {self.REQUIRED_EMULATOR}, but got {type(self._emulator)}", 123 self._parameters, 124 ) 125 if not isinstance(self._controller, Controller): 126 log_error( 127 f"Environment requires a Controller instance, but got {type(self._controller)}", 128 self._parameters, 129 ) 130 self.REQUIRED_STATE_TRACKER = get_lowest_level_subclass( 131 [self.REQUIRED_STATE_TRACKER, self._controller.REQUIRED_STATE_TRACKER] 132 ) 133 if not issubclass( 134 type(self._emulator.state_tracker), self.REQUIRED_STATE_TRACKER 135 ): 136 log_error( 137 f"Environment requires a StateTracker of type {self.REQUIRED_STATE_TRACKER}, but got {type(self._emulator.state_tracker)}", 138 self._parameters, 139 ) 140 self._controller.assign_emulator(self._emulator) 141 self._rng = np.random.default_rng() 142 self.action_space = self._controller.get_action_space() 143 """ The Gym action Space provided by the controller. """ 144 self.actions = self._controller.ACTIONS 145 """ A list of HighLevelAction Types provided by the controller. """ 146 self.render_mode = "human" 147 """ The render mode of the environment. Supports 'human' and 'rgb_array', but strongly assumes 'human' as can just read the emulator screen from `get_info` """ 148 self._window = None 149 """ The pygame window for rendering in 'human' mode. Initialized on first render call. """ 150 self._clock = None 151 """ The pygame clock for rendering in 'human' mode. Initialized on first render call. """ 152 self.reset() # I don't think this will cause issues, but should check that resetting here works well with gymnasium SyncVectorEnv final_obs construction.
Ensures that the environment has the required attributes. All subclasses must call this __init__ method AFTER setting up the required attributes.
If you are implementing a subclass, ensure that the following attributes are set: - observation_space: gym space defining observation space structure
The highest level emulator that the environment can interface with.
The state tracker that tracks the minimal state information required for the environment to function.
47 @staticmethod 48 def override_emulator_kwargs(emulator_kwargs: dict) -> dict: 49 """ 50 Override default emulator keyword arguments for this environment. 51 52 Override this method in subclasses to modify the default emulator keyword arguments. 53 54 You may want to use `override_state_tracker_class` or that style to ensure compatibility of state tracker classes. 55 56 Args: 57 emulator_kwargs (dict): Incoming emulator keyword arguments. 58 Returns: 59 dict: The overridden emulator keyword arguments. 60 """ 61 return emulator_kwargs
Override default emulator keyword arguments for this environment.
Override this method in subclasses to modify the default emulator keyword arguments.
You may want to use override_state_tracker_class or that style to ensure compatibility of state tracker classes.
Arguments:
- emulator_kwargs (dict): Incoming emulator keyword arguments.
Returns:
dict: The overridden emulator keyword arguments.
63 @staticmethod 64 def override_state_tracker_class( 65 emulator_kwargs: dict, required_state_tracker_class: Type[StateTracker] 66 ): 67 """ 68 Safely overrides the state tracker class for the environment. 69 70 Use this in `override_emulator_kwargs` to ensure that the lowest level state tracker class is chosen. 71 72 Args: 73 emulator_kwargs (dict): Incoming emulator keyword arguments. 74 required_state_tracker_class (Type[StateTracker]): Usually the required state tracker class for the environment. 75 """ 76 game = emulator_kwargs["game"] 77 has_option = "state_tracker_class" in emulator_kwargs 78 incoming_state_tracker_class = emulator_kwargs.get( 79 "state_tracker_class", "default" 80 ) 81 if isinstance(incoming_state_tracker_class, str): 82 incoming_state_tracker_class = get_state_tracker_class( 83 game, incoming_state_tracker_class 84 ) 85 if issubclass(incoming_state_tracker_class, required_state_tracker_class): 86 return incoming_state_tracker_class 87 elif issubclass(required_state_tracker_class, incoming_state_tracker_class): 88 emulator_kwargs["state_tracker_class"] = required_state_tracker_class 89 else: 90 emulator_kwargs["state_tracker_class"] = ( 91 incoming_state_tracker_class # Don't know which one to pick, so just go with the incoming one. 92 ) 93 return
Safely overrides the state tracker class for the environment.
Use this in override_emulator_kwargs to ensure that the lowest level state tracker class is chosen.
Arguments:
- emulator_kwargs (dict): Incoming emulator keyword arguments.
- required_state_tracker_class (Type[StateTracker]): Usually the required state tracker class for the environment.
The render mode of the environment. Supports 'human' and 'rgb_array', but strongly assumes 'human' as can just read the emulator screen from get_info
154 def save_custom_state(self, state_name: str): 155 """ 156 Saves a custom state of the emulator. This is useful for saving states during training or evaluation that can be loaded later for analysis or replay. 157 158 Args: 159 state_name (str): Name of the state to save. This will be saved as a .state file in the states directory. 160 """ 161 # don't allow path like state names 162 if ( 163 "/" in state_name 164 or "\\" in state_name 165 or " " in state_name 166 or not state_name.isalnum() 167 ): 168 log_error( 169 f"State name '{state_name}' is invalid. State names must be alphanumeric and cannot contain spaces or path characters.", 170 self._parameters, 171 ) 172 state_name = state_name.replace( 173 "custom_", "" 174 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 175 state_name = f"custom_{state_name}" 176 self._emulator.save_state(state_name=state_name) 177 return state_name
Saves a custom state of the emulator. This is useful for saving states during training or evaluation that can be loaded later for analysis or replay.
Arguments:
- state_name (str): Name of the state to save. This will be saved as a .state file in the states directory.
179 def delete_custom_state(self, state_name: str): 180 """ 181 Deletes a custom state of the emulator that was previously saved with `save_custom_state`. 182 183 Args: 184 state_name (str): Name of the state to delete. This should be the name returned by `save_custom_state`. 185 """ 186 state_name = state_name.replace( 187 "custom_", "" 188 ) # prevent users from accidentally adding the prefix and causing confusion about the actual saved state name. 189 state_name = f"custom_{state_name}" 190 self._emulator.delete_state(state_name=state_name)
Deletes a custom state of the emulator that was previously saved with save_custom_state.
Arguments:
- state_name (str): Name of the state to delete. This should be the name returned by
save_custom_state.
192 def load_custom_state(self, state_name: str): 193 """ 194 Loads a custom state of the emulator that was previously saved with `save_custom_state`. 195 196 Args: 197 state_name (str): Name of the state to load. This should be the name returned by `save_custom_state`. 198 """ 199 state_name = f"custom_{state_name}" 200 self._emulator.set_init_state(state_name) 201 self.reset() # reset to apply the new init state
Loads a custom state of the emulator that was previously saved with save_custom_state.
Arguments:
- state_name (str): Name of the state to load. This should be the name returned by
save_custom_state.
203 @abstractmethod 204 def get_observation( 205 self, 206 *, 207 action: Optional[HighLevelAction] = None, 208 action_kwargs: Optional[dict] = None, 209 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 210 action_success: Optional[int] = None, 211 ) -> gym.spaces.Space: 212 """ 213 Returns the current observation from the emulator. Must match self.observation_space. 214 Args: 215 action (Optional[HighLevelAction]): The previous action taken. 216 action_kwargs (dict): The keyword arguments used for the action. 217 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 218 action_success (Optional[int]): The success code of the action. 219 220 Returns: 221 observation (gym.spaces.Space): The current observation. 222 """ 223 raise NotImplementedError
Returns the current observation from the emulator. Must match self.observation_space.
Arguments:
- action (Optional[HighLevelAction]): The previous action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution.
- action_success (Optional[int]): The success code of the action.
Returns:
observation (gym.spaces.Space): The current observation.
225 def get_info( 226 self, 227 *, 228 action: Optional[HighLevelAction] = None, 229 action_kwargs: Optional[dict] = None, 230 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 231 action_success: Optional[int] = None, 232 ) -> Dict[str, Dict[str, Any]]: 233 """ 234 Returns the full state information as defined by the emulator's state tracker. 235 236 Creates additional fields: 237 - "core"/"previous_action_details": A tuple of (action, action_kwargs, transition_states, action_success, action_return) 238 - "core"/"transition_passed_frames": An array of all frames passed during the action execution 239 - "ocr"/"transition_ocr_regions": A list of OCR regions captured during the action execution 240 241 :param action: HighLevelAction taken 242 :type action: Optional[HighLevelAction] 243 :param action_kwargs: Keyword arguments for the action 244 :type action_kwargs: Optional[dict] 245 :param transition_states: List of states observed during the action execution 246 :type transition_states: Optional[List[Dict[str, Dict[str, Any]]]] 247 :param action_success: Success code of the action 248 :type action_success: Optional[int] 249 :return: Full state information from the state tracker 250 :rtype: Dict[str, Dict[str, Any]] 251 """ 252 state_info = self._emulator.state_tracker.report() 253 if action is not None: # then transition_states should not be empty 254 # Attach the action details to the info 255 last_state = transition_states[-1] 256 if "action_return" in last_state["core"]: 257 action_return = last_state["core"]["action_return"] 258 else: 259 action_return = None 260 state_info["core"]["previous_action_details"] = ( 261 action, 262 action_kwargs, 263 transition_states, 264 action_success, 265 action_return, 266 ) 267 268 # Aggregate passed frames from transition states 269 all_passed_frames = transition_states[0]["core"]["passed_frames"] 270 for transition_state in transition_states[1:]: 271 all_passed_frames = np.concatenate( 272 [all_passed_frames, transition_state["core"]["passed_frames"]], 273 axis=0, 274 ) 275 state_info["core"][ 276 "transition_passed_frames" 277 ] = all_passed_frames # Will include the current state info last frame as as the final entry 278 279 # Aggregate OCR texts from transition states 280 all_ocr_regions = [] 281 for transition_state in transition_states: 282 if ( 283 "ocr" in transition_state 284 and "ocr_regions" in transition_state["ocr"] 285 ): 286 all_ocr_regions.append(transition_state["ocr"]["ocr_regions"]) 287 if "ocr" in state_info: 288 state_info["ocr"]["transition_ocr_regions"] = all_ocr_regions 289 else: 290 state_info["ocr"] = {"transition_ocr_regions": all_ocr_regions} 291 return state_info
Returns the full state information as defined by the emulator's state tracker.
Creates additional fields:
- "core"/"previous_action_details": A tuple of (action, action_kwargs, transition_states, action_success, action_return)
- "core"/"transition_passed_frames": An array of all frames passed during the action execution
- "ocr"/"transition_ocr_regions": A list of OCR regions captured during the action execution
Parameters
- action: HighLevelAction taken
- action_kwargs: Keyword arguments for the action
- transition_states: List of states observed during the action execution
- action_success: Success code of the action
Returns
Full state information from the state tracker
293 def get_final_info(self) -> Dict[str, Dict[str, Any]]: 294 """ 295 Returns the final state information from the emulator when all episodes are done. 296 Will involve summaries over all episodes played. 297 Returns: 298 info (dict): The final state information from the state tracker. 299 """ 300 return self._emulator.state_tracker.report_final()
Returns the final state information from the emulator when all episodes are done. Will involve summaries over all episodes played.
Returns:
info (dict): The final state information from the state tracker.
302 def reset( 303 self, *, seed: Optional[int] = None, options: Optional[dict] = None 304 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 305 """ 306 Resets the environment and emulator to the initial state. 307 Args: 308 seed (int, optional): Seed for random number generators. 309 options (dict, optional): Additional options for resetting the environment. 310 Returns: 311 observation (object): The initial observation of the environment. 312 313 info (dict): Additional information about the reset. 314 """ 315 super().reset(seed=seed, options=options) 316 self._emulator.reset() 317 self.seed(seed) 318 observation, info = self.get_observation(), self.get_info() 319 return observation, info
Resets the environment and emulator to the initial state.
Arguments:
- seed (int, optional): Seed for random number generators.
- options (dict, optional): Additional options for resetting the environment.
Returns:
observation (object): The initial observation of the environment.
info (dict): Additional information about the reset.
321 @abstractmethod 322 def determine_reward( 323 self, 324 start_state: Dict[str, Dict[str, Any]], 325 *, 326 action: Optional[HighLevelAction] = None, 327 action_kwargs: Optional[dict] = None, 328 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 329 action_success: Optional[int] = None, 330 ) -> float: 331 """ 332 Determines the reward based on the transition from start_state through transition_states. 333 Args: 334 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 335 action (HighLevelAction): The HighLevelAction action taken. 336 action_kwargs (dict): The keyword arguments used for the action. 337 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 338 action_success (bool): Whether the action was successful. 339 Returns: 340 float: The computed reward. 341 """ 342 raise NotImplementedError
Determines the reward based on the transition from start_state through transition_states.
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (bool): Whether the action was successful.
Returns:
float: The computed reward.
344 def determine_truncated( 345 self, 346 start_state: Dict[str, Dict[str, Any]], 347 *, 348 action: Optional[HighLevelAction] = None, 349 action_kwargs: Optional[dict] = None, 350 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 351 action_success: Optional[int] = None, 352 ) -> bool: 353 """ 354 Determines whether the episode playthrough has exceeded some maximum step count or other truncation criteria based on the transition from start_state through transition_states. 355 This method is can be overidden to implement custom truncation logic, but it must always return: 356 `super().determine_truncated() or <custom_truncation_logic_bool>` 357 358 Args: 359 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 360 action (HighLevelAction): The HighLevelAction action taken. 361 action_kwargs (dict): The keyword arguments used for the action. 362 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 363 action_success (bool): Whether the action was successful. 364 Returns: 365 bool: Whether the episode is terminated. 366 """ 367 return self._emulator.check_if_done()
Determines whether the episode playthrough has exceeded some maximum step count or other truncation criteria based on the transition from start_state through transition_states.
This method is can be overidden to implement custom truncation logic, but it must always return:
super().determine_truncated() or <custom_truncation_logic_bool>
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (bool): Whether the action was successful.
Returns:
bool: Whether the episode is terminated.
369 @abstractmethod 370 def determine_terminated( 371 self, 372 start_state: Dict[str, Dict[str, Any]], 373 *, 374 action: Optional[HighLevelAction] = None, 375 action_kwargs: Optional[dict] = None, 376 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 377 action_success: Optional[int] = None, 378 ) -> bool: 379 """ 380 Determines whether the episode reaches the goal / terminal state based on the transition from start_state through transition_states. 381 This method is NOT meant to be used to determine if the step count has exceeded the maximum. 382 383 Args: 384 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 385 action (HighLevelAction): The HighLevelAction action taken. 386 action_kwargs (dict): The keyword arguments used for the action. 387 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 388 action_success (bool): Whether the action was successful. 389 Returns: 390 bool: Whether the episode is terminated. 391 """ 392 pass
Determines whether the episode reaches the goal / terminal state based on the transition from start_state through transition_states. This method is NOT meant to be used to determine if the step count has exceeded the maximum.
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (bool): Whether the action was successful.
Returns:
bool: Whether the episode is terminated.
394 def before_step(self, action: Type[HighLevelAction], action_kwargs: dict): 395 """ 396 Implement any logic that needs to be executed before each step in the environment. 397 """ 398 return
Implement any logic that needs to be executed before each step in the environment.
400 def after_step( 401 self, 402 start_state: Dict[str, Dict[str, Any]], 403 action: Type[HighLevelAction], 404 action_kwargs: dict, 405 transition_states: List[Dict[str, Dict[str, Any]]], 406 action_success: int, 407 ): 408 """ 409 Implement any logic that needs to be executed after each step in the environment. 410 411 Args: 412 start_state (Dict[str, Dict[str, Any]]): The state before the action was taken. 413 action (HighLevelAction): The HighLevelAction action taken. 414 action_kwargs (dict): The keyword arguments used for the action. 415 transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution. 416 action_success (int): Whether the action was successful. 417 """ 418 return
Implement any logic that needs to be executed after each step in the environment.
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (int): Whether the action was successful.
420 def step( 421 self, action: gym.spaces.OneOf 422 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 423 """ 424 Executes the given Gym Space action in the environment via the controller. 425 Use step_high_level_action to execute high level actions directly. 426 427 Args: 428 action (gym.spaces.OneOf): The action to execute. Must be a valid action in the controller's action space. 429 430 Returns: 431 observation (gym.spaces.Space): The observation after executing the action. 432 reward (float): The reward obtained from executing the action. 433 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 434 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 435 info (Dict[str, Dict[str, Any]]): Full state information. 436 """ 437 high_level_action, kwargs = self._controller._space_action_to_high_level_action( 438 action 439 ) 440 return self.step_high_level_action(high_level_action, **kwargs)
Executes the given Gym Space action in the environment via the controller. Use step_high_level_action to execute high level actions directly.
Arguments:
- action (gym.spaces.OneOf): The action to execute. Must be a valid action in the controller's action space.
Returns:
observation (gym.spaces.Space): The observation after executing the action. reward (float): The reward obtained from executing the action. terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). info (Dict[str, Dict[str, Any]]): Full state information.
442 def step_high_level_action( 443 self, action: Type[HighLevelAction], **kwargs 444 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 445 """ 446 Executes the given High Level action in the environment via the controller. 447 If the action is invalid according to the controller, will not perform any action and will simply return the current observation, a reward of 0, and terminated and truncated as False. The info will also include a field "invalid_action"=True to indicate that the action was invalid. 448 449 :param action: The high level action class to execute. 450 :type action: Type[HighLevelAction] 451 :param kwargs: Additional arguments required for the specific high level action. 452 :type kwargs: Dict[str, Any] 453 :return: 454 - observation (gym.spaces.Space): The observation after executing the action. 455 456 - reward (float): The reward obtained from executing the action. 457 458 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 459 460 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 461 462 - info (Dict[str, Dict[str, Any]]): Full state information. 463 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 464 """ 465 if self._emulator.check_if_done(): 466 log_error( 467 "Cannot step environment because emulator indicates done. Please reset the environment.", 468 self._parameters, 469 ) 470 start_state = self.get_info() 471 self.before_step(action, kwargs) 472 transition_states, action_success = self._controller.execute(action, **kwargs) 473 if ( 474 transition_states is None 475 ): # then the action was not a valid one according to the controller. 476 observation = self.get_observation() 477 current_state = self.get_info() 478 terminated = self.determine_terminated(start_state=start_state) 479 truncated = self.determine_truncated(start_state=start_state) 480 reward = self.determine_reward(start_state=start_state) - abs( 481 self._parameters["invalid_action_penalty"] 482 ) 483 current_state["invalid_action"] = True 484 return observation, reward, terminated, truncated, current_state 485 self.after_step(start_state, action, kwargs, transition_states, action_success) 486 truncated = self.determine_truncated( 487 start_state=start_state, 488 action=action, 489 action_kwargs=kwargs, 490 transition_states=transition_states, 491 action_success=action_success, 492 ) 493 494 observation = self.get_observation( 495 action=action, 496 action_kwargs=kwargs, 497 transition_states=transition_states, 498 action_success=action_success, 499 ) 500 current_state = self.get_info( 501 action=action, 502 action_kwargs=kwargs, 503 transition_states=transition_states, 504 action_success=action_success, 505 ) 506 terminated = self.determine_terminated( 507 start_state=start_state, 508 action=action, 509 action_kwargs=kwargs, 510 transition_states=transition_states, 511 action_success=action_success, 512 ) 513 514 reward = self.determine_reward( 515 start_state=start_state, 516 action=action, 517 action_kwargs=kwargs, 518 transition_states=transition_states, 519 action_success=action_success, 520 ) 521 return observation, reward, terminated, truncated, current_state
Executes the given High Level action in the environment via the controller. If the action is invalid according to the controller, will not perform any action and will simply return the current observation, a reward of 0, and terminated and truncated as False. The info will also include a field "invalid_action"=True to indicate that the action was invalid.
Parameters
- action: The high level action class to execute.
- kwargs: Additional arguments required for the specific high level action.
Returns
- observation (gym.spaces.Space): The observation after executing the action. - reward (float): The reward obtained from executing the action. - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). - info (Dict[str, Dict[str, Any]]): Full state information.
523 def step_str( 524 self, input_str: str 525 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 526 """ 527 Attempts to execute an input string representation of an action. 528 Useful for human play or VLM interaction. 529 If the action is an invalid string, will not perform any action and will simply return Nones. 530 531 :param input_str: The input string representing the action. 532 :type input_str: str 533 :return: 534 - observation (gym.spaces.Space): The observation after executing the action. 535 536 - reward (float): The reward obtained from executing the action. 537 538 - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 539 540 - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). 541 542 - info (Dict[str, Dict[str, Any]]): Full state information. 543 :rtype: Tuple[Space, float, bool, bool, Dict[str, Dict[str, Any]]] 544 """ 545 action, kwargs = self.string_to_high_level_action(input_str) 546 if ( 547 action is None 548 ): # not a valid action, will not perform an action and will simply return Nones. 549 return None, None, None, None, None 550 return self.step_high_level_action(action, **kwargs)
Attempts to execute an input string representation of an action. Useful for human play or VLM interaction. If the action is an invalid string, will not perform any action and will simply return Nones.
Parameters
- input_str: The input string representing the action.
Returns
- observation (gym.spaces.Space): The observation after executing the action. - reward (float): The reward obtained from executing the action. - terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). - truncated (bool): Whether the episode was truncated (exceeded the maximum allowed steps). - info (Dict[str, Dict[str, Any]]): Full state information.
552 def string_to_high_level_action( 553 self, input_str: str 554 ) -> Tuple[Optional[Type[HighLevelAction]], Optional[dict]]: 555 """ 556 Attempts to convert an input string representation of an action into a HighLevelAction and its parameters. 557 Useful for human play or VLM interaction. 558 559 :param input_str: The input string representing the action. 560 :type input_str: str 561 :return: A tuple containing the HighLevelAction class and its execution parameters dictionary. If the input string is invalid, returns (None, None). 562 :rtype: Tuple[Type[HighLevelAction] | None, dict | None] 563 """ 564 return self._controller.string_to_high_level_action(input_str)
Attempts to convert an input string representation of an action into a HighLevelAction and its parameters. Useful for human play or VLM interaction.
Parameters
- input_str: The input string representing the action.
Returns
A tuple containing the HighLevelAction class and its execution parameters dictionary. If the input string is invalid, returns (None, None).
585 def sim( 586 self, action: gym.spaces.OneOf 587 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 588 """Like `step` but reverts the emulator to its pre-step state afterward. 589 590 .. warning:: 591 Because reversion requires a full emulator reset, all state tracker counters 592 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 593 a side effect. The returned info reflects the simulated step, not post-reset state. 594 """ 595 return self._simulate(self.step, action)
Like step but reverts the emulator to its pre-step state afterward.
Because reversion requires a full emulator reset, all state tracker counters and accumulated metrics (e.g. steps taken, episode rewards) will be reset as a side effect. The returned info reflects the simulated step, not post-reset state.
597 def sim_str( 598 self, input_str: str 599 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 600 """Like `step_str` but reverts the emulator to its pre-step state afterward. 601 602 .. warning:: 603 Because reversion requires a full emulator reset, all state tracker counters 604 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 605 a side effect. The returned info reflects the simulated step, not post-reset state. 606 """ 607 return self._simulate(self.step_str, input_str)
Like step_str but reverts the emulator to its pre-step state afterward.
Because reversion requires a full emulator reset, all state tracker counters and accumulated metrics (e.g. steps taken, episode rewards) will be reset as a side effect. The returned info reflects the simulated step, not post-reset state.
609 def sim_high_level_action( 610 self, action: Type[HighLevelAction], **kwargs 611 ) -> Tuple[gym.spaces.Space, float, bool, bool, Dict[str, Dict[str, Any]]]: 612 """Like `step_high_level_action` but reverts the emulator to its pre-step state afterward. 613 614 .. warning:: 615 Because reversion requires a full emulator reset, all state tracker counters 616 and accumulated metrics (e.g. steps taken, episode rewards) will be reset as 617 a side effect. The returned info reflects the simulated step, not post-reset state. 618 """ 619 return self._simulate(self.step_high_level_action, action, **kwargs)
Like step_high_level_action but reverts the emulator to its pre-step state afterward.
Because reversion requires a full emulator reset, all state tracker counters and accumulated metrics (e.g. steps taken, episode rewards) will be reset as a side effect. The returned info reflects the simulated step, not post-reset state.
621 def close(self): 622 """ 623 Closes the environment and the underlying emulator. 624 """ 625 log_info("Closing environment and emulator.", self._parameters) 626 self._emulator.close()
Closes the environment and the underlying emulator.
649 def render(self) -> Optional[np.ndarray]: 650 """ 651 Gets the current screen from the emulator and renders it. 652 653 Use this method only if you want to generally run the emulator in headless mode but still want to see the screen occasionally. 654 655 Do not call this method if the emulator is not headless, you should already have a PyBoy interactive window open in that case. 656 657 Returns: 658 If render_mode is 'rgb_array', returns the current screen as a numpy array. However this is always accessible via self.get_info()['core']['current_frame'], so this is mostly for Gym compatibility. 659 """ 660 if self._emulator.headless == False: 661 log_error( 662 "You probably don't want to call render() when the emulator is not headless.", 663 self._parameters, 664 ) 665 screen = self._emulator.get_current_frame() # shape: 144, 160, 1 666 if self.render_mode == "human": 667 self._screen_render(screen) 668 elif self.render_mode == "rgb_array": 669 return screen 670 else: 671 log_error(f"Unsupported render mode: {self.render_mode}", self._parameters)
Gets the current screen from the emulator and renders it.
Use this method only if you want to generally run the emulator in headless mode but still want to see the screen occasionally.
Do not call this method if the emulator is not headless, you should already have a PyBoy interactive window open in that case.
Returns:
If render_mode is 'rgb_array', returns the current screen as a numpy array. However this is always accessible via self.get_info()['core']['current_frame'], so this is mostly for Gym compatibility.
673 def seed(self, seed: Optional[int] = None): 674 """ 675 Seeds the environment's random number generator and the controller's RNG. 676 677 Args: 678 seed (int, optional): The seed value. 679 """ 680 self._controller.seed(seed) 681 self._rng = np.random.default_rng(seed)
Seeds the environment's random number generator and the controller's RNG.
Arguments:
- seed (int, optional): The seed value.
683 def render_obs( 684 self, 685 *, 686 action: Optional[Type[HighLevelAction]] = None, 687 action_kwargs: Optional[dict] = None, 688 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 689 action_success: Optional[int] = None, 690 action_return: Optional[Dict[str, Any]] = None, 691 ): 692 """ 693 Provide a way to render the output of `get_observation` to a human. 694 Implement if you want to use the human_step_play method. 695 696 Args: 697 action (Optional[Type[HighLevelAction]]): The previous action taken. 698 action_kwargs (dict): The keyword arguments used for the action. 699 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 700 action_success (Optional[int]): The success code of the action. 701 """ 702 raise NotImplementedError
Provide a way to render the output of get_observation to a human.
Implement if you want to use the human_step_play method.
Arguments:
- action (Optional[Type[HighLevelAction]]): The previous action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution.
- action_success (Optional[int]): The success code of the action.
704 def render_info( 705 self, 706 *, 707 action: Optional[Type[HighLevelAction]] = None, 708 action_kwargs: Optional[dict] = None, 709 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 710 action_success: Optional[int] = None, 711 action_return: Optional[Dict[str, Any]] = None, 712 ): 713 """ 714 Provide a way to render the output of `get_info` to a human. 715 Implement if you want to use the human_step_play method with show_info=True. 716 Must always be a superset of render_obs (i.e. should also show what render_obs shows). 717 718 Args: 719 action (Optional[Type[HighLevelAction]]): The previous action taken. 720 action_kwargs (dict): The keyword arguments used for the action. 721 transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution. 722 action_success (Optional[int]): The success code of the action. 723 """ 724 raise NotImplementedError
Provide a way to render the output of get_info to a human.
Implement if you want to use the human_step_play method with show_info=True.
Must always be a superset of render_obs (i.e. should also show what render_obs shows).
Arguments:
- action (Optional[Type[HighLevelAction]]): The previous action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution.
- action_success (Optional[int]): The success code of the action.
726 def get_action_strings( 727 self, return_all: bool = False 728 ) -> Dict[Type[HighLevelAction], str]: 729 """ 730 Provide a way to verbalize the allowed high level actions, along with the format of the input parameters. 731 Useful for prompting a VLM to choose an action. 732 733 :param return_all: If True, returns all possible actions and parameter formats. If False, returns only the actions that are valid in the current state. 734 :type return_all: bool 735 :return: A dictionary mapping high level actions to their verbalizations and input formats. 736 :rtype: Dict[Type[HighLevelAction], str] 737 """ 738 return self._controller.get_action_strings(return_all=return_all)
Provide a way to verbalize the allowed high level actions, along with the format of the input parameters. Useful for prompting a VLM to choose an action.
Parameters
- return_all: If True, returns all possible actions and parameter formats. If False, returns only the actions that are valid in the current state.
Returns
A dictionary mapping high level actions to their verbalizations and input formats.
740 def human_step_play( 741 self, max_steps: int = 50, show_obs: bool = True, show_info: bool = True 742 ) -> Tuple[List[float], bool, bool]: 743 """ 744 Opens a render window and allow the human to play through the environment as an agent would 745 746 Args: 747 max_steps (int): max steps to take 748 show_obs (bool): whether to show the observation space rendering after each step 749 show_info (bool): whether to show the info rendering after each step. Will force show_obs=False if enabled. 750 751 Returns: 752 rewards (List[float]): List of rewards obtained at each step. 753 terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). 754 truncated (bool): Whether the episode was truncated (exceeded the maximum allowed emulator steps). It does not consider the max_steps parameter here. 755 """ 756 observation, info = self.reset() 757 self.render_mode = "human" 758 log_info(f"Doing human step play for {max_steps} max steps...") 759 steps = 0 760 done = False 761 terminated = False 762 truncated = False 763 rewards = [] 764 if show_info: 765 self.render_info() 766 show_obs = False 767 if show_obs: 768 self.render_obs() 769 while not done and steps < max_steps: 770 action_input = self._controller.get_action_strings() 771 action_input_str = "" 772 for action_class, action_str in action_input.items(): 773 action_input_str += f"- {action_str}\n" 774 log_info(f"Allowed Actions: \n{action_input_str}", self._parameters) 775 input_str = input("Enter Action: ").strip() 776 ( 777 possible_obs, 778 possible_reward, 779 possible_terminated, 780 possible_truncated, 781 possible_info, 782 ) = self.step_str(input_str) 783 if possible_obs is not None: 784 observation, reward, terminated, truncated, info = ( 785 possible_obs, 786 possible_reward, 787 possible_terminated, 788 possible_truncated, 789 possible_info, 790 ) 791 rewards.append(reward) 792 if reward != 0: 793 log_info(f"Reward obtained: {reward}", self._parameters) 794 ( 795 action, 796 action_kwargs, 797 transition_states, 798 action_success, 799 action_return, 800 ) = info["core"]["previous_action_details"] 801 if show_info: 802 self.render_info( 803 action=action, 804 action_kwargs=action_kwargs, 805 transition_states=transition_states, 806 action_success=action_success, 807 action_return=action_return, 808 ) 809 if show_obs: 810 self.render_obs( 811 action=action, 812 action_kwargs=action_kwargs, 813 transition_states=transition_states, 814 action_success=action_success, 815 action_return=action_return, 816 ) 817 else: 818 log_warn("That was not a valid input. did nothing", self._parameters) 819 if terminated or truncated: 820 log_info( 821 f"Episode finished! Terminated: {terminated}, Truncated: {truncated}", 822 self._parameters, 823 ) 824 break 825 steps += 1 826 if steps >= max_steps: 827 log_info( 828 f"Max steps {max_steps} reached. Ending episode.", self._parameters 829 ) 830 return rewards, terminated, truncated
Opens a render window and allow the human to play through the environment as an agent would
Arguments:
- max_steps (int): max steps to take
- show_obs (bool): whether to show the observation space rendering after each step
- show_info (bool): whether to show the info rendering after each step. Will force show_obs=False if enabled.
Returns:
rewards (List[float]): List of rewards obtained at each step. terminated (bool): Whether the episode has ended (reached the terminal state of the MDP). truncated (bool): Whether the episode was truncated (exceeded the maximum allowed emulator steps). It does not consider the max_steps parameter here.
833class DummyEnvironment(Environment): 834 """A dummy environment that does nothing special.""" 835 836 def __init__( 837 self, 838 emulator: Emulator, 839 controller: Controller, 840 parameters: Optional[dict] = None, 841 ): 842 """ 843 Initializes the DummyEnvironment with the given emulator and controller. 844 845 It is safe to overwrite the self.observation_space in the subclass after calling this __init__ method. 846 """ 847 screen_shape = emulator.screen_shape 848 self.observation_space = gym.spaces.Box( 849 low=0, high=255, shape=(screen_shape[1], screen_shape[0]), dtype=np.uint8 850 ) 851 """ The observation space is the raw pixel values of the emulator's screen. """ 852 super().__init__( 853 emulator=emulator, controller=controller, parameters=parameters 854 ) 855 856 def get_observation( 857 self, 858 *, 859 action=None, 860 action_kwargs=None, 861 transition_states=None, 862 action_success=None, 863 ): 864 if transition_states is None: 865 current_state = self.get_info() 866 screen = current_state["core"]["current_frame"] 867 else: 868 screen = transition_states[-1]["core"]["current_frame"] 869 return screen 870 871 def determine_reward(self, **kwargs): 872 return 0.0 873 874 def determine_terminated(self, **kwargs): 875 return False 876 877 def render_obs( 878 self, 879 action=None, 880 action_kwargs=None, 881 transition_states=None, 882 action_success=None, 883 action_return=None, 884 ): # Might cause issues if you try to render() as well 885 """ 886 Renders the screen. 887 """ 888 screen = self.get_observation() 889 self._screen_render(screen) 890 891 def render_info( 892 self, 893 action=None, 894 action_kwargs=None, 895 transition_states=None, 896 action_success=None, 897 action_return=None, 898 ): 899 info = deepcopy(self.get_info()) 900 if transition_states is not None and len(transition_states) > 0: 901 screens = transition_states[0]["core"]["passed_frames"] 902 for transition_state in transition_states[1:]: 903 screens = np.concatenate( 904 [screens, transition_state["core"]["passed_frames"]], axis=0 905 ) 906 else: 907 if "passed_frames" in info["core"]: 908 screens = info["core"]["passed_frames"] 909 else: 910 screens = None 911 if screens is None: 912 screens = [info["core"]["current_frame"]] 913 for screen in screens: 914 self._screen_render(screen) 915 info["core"].pop("current_frame") 916 info["core"].pop("passed_frames") 917 if "ocr" in info: 918 info.pop("ocr") 919 if "transition_passed_frames" in info["core"]: 920 info["core"].pop("transition_passed_frames") 921 if "previous_action_details" in info["core"]: 922 info["core"]["previous_action_details"] = ( 923 info["core"]["previous_action_details"][:2] 924 + info["core"]["previous_action_details"][3:] 925 ) # remove transition states to avoid huge logs 926 log_info("State: ", self._parameters) 927 log_dict(info, parameters=self._parameters)
A dummy environment that does nothing special.
836 def __init__( 837 self, 838 emulator: Emulator, 839 controller: Controller, 840 parameters: Optional[dict] = None, 841 ): 842 """ 843 Initializes the DummyEnvironment with the given emulator and controller. 844 845 It is safe to overwrite the self.observation_space in the subclass after calling this __init__ method. 846 """ 847 screen_shape = emulator.screen_shape 848 self.observation_space = gym.spaces.Box( 849 low=0, high=255, shape=(screen_shape[1], screen_shape[0]), dtype=np.uint8 850 ) 851 """ The observation space is the raw pixel values of the emulator's screen. """ 852 super().__init__( 853 emulator=emulator, controller=controller, parameters=parameters 854 )
Initializes the DummyEnvironment with the given emulator and controller.
It is safe to overwrite the self.observation_space in the subclass after calling this __init__ method.
856 def get_observation( 857 self, 858 *, 859 action=None, 860 action_kwargs=None, 861 transition_states=None, 862 action_success=None, 863 ): 864 if transition_states is None: 865 current_state = self.get_info() 866 screen = current_state["core"]["current_frame"] 867 else: 868 screen = transition_states[-1]["core"]["current_frame"] 869 return screen
Returns the current observation from the emulator. Must match self.observation_space.
Arguments:
- action (Optional[HighLevelAction]): The previous action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution.
- action_success (Optional[int]): The success code of the action.
Returns:
observation (gym.spaces.Space): The current observation.
Determines the reward based on the transition from start_state through transition_states.
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (bool): Whether the action was successful.
Returns:
float: The computed reward.
Determines whether the episode reaches the goal / terminal state based on the transition from start_state through transition_states. This method is NOT meant to be used to determine if the step count has exceeded the maximum.
Arguments:
- start_state (Dict[str, Dict[str, Any]]): The state before the action was taken.
- action (HighLevelAction): The HighLevelAction action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (List[Dict[str, Dict[str, Any]]]): A list of states observed during the action execution.
- action_success (bool): Whether the action was successful.
Returns:
bool: Whether the episode is terminated.
877 def render_obs( 878 self, 879 action=None, 880 action_kwargs=None, 881 transition_states=None, 882 action_success=None, 883 action_return=None, 884 ): # Might cause issues if you try to render() as well 885 """ 886 Renders the screen. 887 """ 888 screen = self.get_observation() 889 self._screen_render(screen)
Renders the screen.
891 def render_info( 892 self, 893 action=None, 894 action_kwargs=None, 895 transition_states=None, 896 action_success=None, 897 action_return=None, 898 ): 899 info = deepcopy(self.get_info()) 900 if transition_states is not None and len(transition_states) > 0: 901 screens = transition_states[0]["core"]["passed_frames"] 902 for transition_state in transition_states[1:]: 903 screens = np.concatenate( 904 [screens, transition_state["core"]["passed_frames"]], axis=0 905 ) 906 else: 907 if "passed_frames" in info["core"]: 908 screens = info["core"]["passed_frames"] 909 else: 910 screens = None 911 if screens is None: 912 screens = [info["core"]["current_frame"]] 913 for screen in screens: 914 self._screen_render(screen) 915 info["core"].pop("current_frame") 916 info["core"].pop("passed_frames") 917 if "ocr" in info: 918 info.pop("ocr") 919 if "transition_passed_frames" in info["core"]: 920 info["core"].pop("transition_passed_frames") 921 if "previous_action_details" in info["core"]: 922 info["core"]["previous_action_details"] = ( 923 info["core"]["previous_action_details"][:2] 924 + info["core"]["previous_action_details"][3:] 925 ) # remove transition states to avoid huge logs 926 log_info("State: ", self._parameters) 927 log_dict(info, parameters=self._parameters)
Provide a way to render the output of get_info to a human.
Implement if you want to use the human_step_play method with show_info=True.
Must always be a superset of render_obs (i.e. should also show what render_obs shows).
Arguments:
- action (Optional[Type[HighLevelAction]]): The previous action taken.
- action_kwargs (dict): The keyword arguments used for the action.
- transition_states (Optional[List[Dict[str, Dict[str, Any]]]]): The states observed during the action execution.
- action_success (Optional[int]): The success code of the action.
Inherited Members
- Environment
- REQUIRED_EMULATOR
- REQUIRED_STATE_TRACKER
- override_emulator_kwargs
- override_state_tracker_class
- action_space
- actions
- render_mode
- save_custom_state
- delete_custom_state
- load_custom_state
- get_info
- get_final_info
- reset
- determine_truncated
- before_step
- after_step
- step
- step_high_level_action
- step_str
- string_to_high_level_action
- sim
- sim_str
- sim_high_level_action
- close
- render
- seed
- get_action_strings
- human_step_play
930class TestEnvironmentMixin: 931 """ 932 Mixin class for testing environments. 933 Ensures the State Tracker used is a TestTrackerMixin and checks these for termination / truncation. 934 """ 935 936 REQUIRED_STATE_TRACKER = TestTrackerMixin 937 938 def determine_truncated( 939 self, 940 start_state: Dict[str, Dict[str, Any]], 941 *, 942 action: Optional[HighLevelAction] = None, 943 action_kwargs: Optional[dict] = None, 944 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 945 action_success: Optional[int] = None, 946 ) -> bool: 947 if not isinstance(self._emulator.state_tracker, TestTrackerMixin): 948 log_error( 949 "TestEnvironmentMixin requires the emulator's state tracker to be a TestTrackerMixin.", 950 self._parameters, 951 ) # we don't need to repeat this for determine_terminated since its done here. 952 if transition_states is None: 953 return self._emulator.check_if_done() 954 any_truncated = False 955 for state in transition_states: 956 if state["termination_truncation"]["truncated"]: 957 any_truncated = True 958 break 959 return any_truncated or self._emulator.check_if_done() 960 961 def determine_terminated( 962 self, 963 start_state: Dict[str, Dict[str, Any]], 964 *, 965 action: Optional[HighLevelAction] = None, 966 action_kwargs: Optional[dict] = None, 967 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 968 action_success: Optional[int] = None, 969 ) -> bool: 970 if transition_states is None: 971 return False 972 any_terminated = False 973 for state in transition_states: 974 if state["termination_truncation"]["terminated"]: 975 any_terminated = True 976 break 977 return any_terminated
Mixin class for testing environments. Ensures the State Tracker used is a TestTrackerMixin and checks these for termination / truncation.
938 def determine_truncated( 939 self, 940 start_state: Dict[str, Dict[str, Any]], 941 *, 942 action: Optional[HighLevelAction] = None, 943 action_kwargs: Optional[dict] = None, 944 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 945 action_success: Optional[int] = None, 946 ) -> bool: 947 if not isinstance(self._emulator.state_tracker, TestTrackerMixin): 948 log_error( 949 "TestEnvironmentMixin requires the emulator's state tracker to be a TestTrackerMixin.", 950 self._parameters, 951 ) # we don't need to repeat this for determine_terminated since its done here. 952 if transition_states is None: 953 return self._emulator.check_if_done() 954 any_truncated = False 955 for state in transition_states: 956 if state["termination_truncation"]["truncated"]: 957 any_truncated = True 958 break 959 return any_truncated or self._emulator.check_if_done()
961 def determine_terminated( 962 self, 963 start_state: Dict[str, Dict[str, Any]], 964 *, 965 action: Optional[HighLevelAction] = None, 966 action_kwargs: Optional[dict] = None, 967 transition_states: Optional[List[Dict[str, Dict[str, Any]]]] = None, 968 action_success: Optional[int] = None, 969 ) -> bool: 970 if transition_states is None: 971 return False 972 any_terminated = False 973 for state in transition_states: 974 if state["termination_truncation"]["terminated"]: 975 any_terminated = True 976 break 977 return any_terminated
980class TrainEnvironmentMixin: 981 """ 982 Mixin class for training environments. 983 Records the allowed initial states for training and random shuffles between them when resetting. 984 """ 985 986 def reset( 987 self, *, seed: Optional[int] = None, options: Optional[dict] = None 988 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 989 """ 990 991 Args: 992 seed (int, optional): Seed for random number generators. 993 options (dict, optional): Additional options for resetting the environment. 994 Returns: 995 observation (object): The initial observation of the environment. 996 997 info (dict): Additional information about the reset. 998 """ 999 game = self._emulator.game 1000 init_states = get_train_init_states(game, parameters=self._parameters) 1001 choice = self._rng.choice(len(init_states)) 1002 init_state = init_states[choice] 1003 self._emulator.set_init_state(init_state) 1004 return super().reset(seed=seed, options=options)
Mixin class for training environments. Records the allowed initial states for training and random shuffles between them when resetting.
986 def reset( 987 self, *, seed: Optional[int] = None, options: Optional[dict] = None 988 ) -> Tuple[gym.spaces.Space, Dict[str, Dict[str, Any]]]: 989 """ 990 991 Args: 992 seed (int, optional): Seed for random number generators. 993 options (dict, optional): Additional options for resetting the environment. 994 Returns: 995 observation (object): The initial observation of the environment. 996 997 info (dict): Additional information about the reset. 998 """ 999 game = self._emulator.game 1000 init_states = get_train_init_states(game, parameters=self._parameters) 1001 choice = self._rng.choice(len(init_states)) 1002 init_state = init_states[choice] 1003 self._emulator.set_init_state(init_state) 1004 return super().reset(seed=seed, options=options)
Arguments:
- seed (int, optional): Seed for random number generators.
- options (dict, optional): Additional options for resetting the environment.
Returns:
observation (object): The initial observation of the environment.
info (dict): Additional information about the reset.