gameboy_worlds.interface.deja_vu.actions
1from abc import ABC 2from gameboy_worlds.utils import log_error, log_warn 3from typing import List, Optional, Tuple, Dict 4import numpy as np 5from gymnasium.spaces import Box, Discrete 6from gameboy_worlds.emulation import LowLevelActions 7from gameboy_worlds.emulation.deja_vu.parsers import AgentState, DejaVuStateParser 8from gameboy_worlds.emulation.deja_vu.trackers import CoreDejaVuTracker 9from gameboy_worlds.interface.action import HighLevelAction, SingleHighLevelAction 10 11HARD_MAX_STEPS = 20 12MENU_NAV_MAX_STEPS = 8 13 14 15def frame_changed(past: np.ndarray, present: np.ndarray, epsilon: float = 0.01) -> bool: 16 """Return True if the mean pixel difference between two frames is greater than epsilon.""" 17 return np.abs(past - present).mean() > epsilon 18 19 20class TickUntilStable(SingleHighLevelAction): 21 """Tick the emulator until the screen becomes stable (no change).""" 22 23 REQUIRED_STATE_PARSER = DejaVuStateParser 24 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 25 26 def is_valid(self, **kwargs): 27 return True 28 29 def _execute(self, max_ticks: int = 30): 30 prev = self._emulator.get_current_frame() 31 for _ in range(max_ticks): 32 self._emulator.step(None) 33 curr = self._emulator.get_current_frame() 34 if not frame_changed(prev, curr): 35 return [self._state_tracker.report()], 0 36 prev = curr 37 return [self._state_tracker.report()], -1 38 39 @staticmethod 40 def get_action_name() -> str: 41 return "TickUntilStable" 42 43 44class MoveCursor(SingleHighLevelAction): 45 """Move the menu cursor in a given direction (up, down, left, right).""" 46 47 REQUIRED_STATE_PARSER = DejaVuStateParser 48 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 49 _ACTION_MAP = { 50 "up": LowLevelActions.PRESS_ARROW_UP, 51 "down": LowLevelActions.PRESS_ARROW_DOWN, 52 "left": LowLevelActions.PRESS_ARROW_LEFT, 53 "right": LowLevelActions.PRESS_ARROW_RIGHT, 54 } 55 56 def is_valid(self, direction=None, **kwargs): 57 return direction in self._ACTION_MAP 58 59 def _execute(self, direction): 60 action = self._ACTION_MAP[direction] 61 prev = self._emulator.get_current_frame() 62 self._emulator.step(action) 63 curr = self._emulator.get_current_frame() 64 return [self._state_tracker.report()], 0 if frame_changed(prev, curr) else -1 65 66 @staticmethod 67 def get_action_name(**kwargs) -> str: 68 return f"MoveCursor {kwargs.get('direction', '')}" 69 70 71class MoveGrid(HighLevelAction): 72 """Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical.""" 73 74 REQUIRED_STATE_PARSER = DejaVuStateParser 75 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 76 77 def get_action_space(self): 78 return Box( 79 low=-HARD_MAX_STEPS // 2, 80 high=HARD_MAX_STEPS // 2, 81 shape=(2,), 82 dtype=np.int8, 83 ) 84 85 def is_valid(self, x_steps=None, y_steps=None, **kwargs): 86 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 87 return state == AgentState.FREE_ROAM and (x_steps or y_steps) 88 89 def _execute(self, x_steps, y_steps): 90 reports = [] 91 for axis, steps, dir_pos, dir_neg in [ 92 ("x", x_steps, "right", "left"), 93 ("y", y_steps, "down", "up"), 94 ]: 95 if steps: 96 direction = dir_pos if steps > 0 else dir_neg 97 for _ in range(abs(steps)): 98 prev = self._emulator.get_current_frame() 99 self._emulator.step(MoveStepsAction._ACTION_MAP[direction]) 100 curr = self._emulator.get_current_frame() 101 reports.append(self._state_tracker.report()) 102 if not frame_changed(prev, curr): 103 return reports, -1 104 return reports, 0 105 106 @staticmethod 107 def get_action_name(x_steps: int, y_steps: int) -> str: 108 return f"MoveGrid ({x_steps}, {y_steps})" 109 110 111class OpenButtonMenu(SingleHighLevelAction): 112 """Open the bottom button menu (usually START button).""" 113 114 REQUIRED_STATE_PARSER = DejaVuStateParser 115 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 116 117 def is_valid(self, **kwargs): 118 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 119 return state == AgentState.FREE_ROAM 120 121 def _execute(self): 122 self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 123 return [self._state_tracker.report()], 0 124 125 @staticmethod 126 def get_action_name() -> str: 127 return "OpenButtonMenu" 128 129 130class CloseButtonMenu(SingleHighLevelAction): 131 """Close the bottom button menu (usually B button).""" 132 133 REQUIRED_STATE_PARSER = DejaVuStateParser 134 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 135 136 def is_valid(self, **kwargs): 137 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 138 return state == AgentState.IN_MENU 139 140 def _execute(self): 141 self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 142 return [self._state_tracker.report()], 0 143 144 @staticmethod 145 def get_action_name() -> str: 146 return "CloseButtonMenu" 147 148 149class SelectMenuOption(SingleHighLevelAction): 150 """Select the current menu option (A button).""" 151 152 REQUIRED_STATE_PARSER = DejaVuStateParser 153 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 154 155 def is_valid(self, **kwargs): 156 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 157 return state == AgentState.IN_MENU 158 159 def _execute(self): 160 self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 161 return [self._state_tracker.report()], 0 162 163 @staticmethod 164 def get_action_name() -> str: 165 return "SelectMenuOption" 166 167 168class AdvanceDialogue(SingleHighLevelAction): 169 """Advance dialogue (B button).""" 170 171 REQUIRED_STATE_PARSER = DejaVuStateParser 172 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 173 174 def is_valid(self, **kwargs): 175 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 176 return state == AgentState.IN_DIALOGUE 177 178 def _execute(self): 179 self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 180 return [self._state_tracker.report()], 0 181 182 @staticmethod 183 def get_action_name() -> str: 184 return "AdvanceDialogue" 185 186 187# Helper for MoveGrid 188class MoveStepsAction: 189 _ACTION_MAP = { 190 "up": LowLevelActions.PRESS_ARROW_UP, 191 "down": LowLevelActions.PRESS_ARROW_DOWN, 192 "left": LowLevelActions.PRESS_ARROW_LEFT, 193 "right": LowLevelActions.PRESS_ARROW_RIGHT, 194 } 195 196 197class InteractAction(SingleHighLevelAction): 198 """ 199 Presses the A button to interact with an object in front of the agent. 200 201 Is Valid When: 202 - In Free Roam State 203 204 Action Success Interpretation: 205 - -1: Frame did not change or agent still in free roam state 206 - 1: Agent not in free roam state 207 """ 208 209 REQUIRED_STATE_PARSER = DejaVuStateParser 210 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 211 212 def is_valid(self, **kwargs): 213 return ( 214 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 215 == AgentState.FREE_ROAM 216 ) 217 218 def _execute(self): 219 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 220 action_success = 0 221 prev_frames = [] 222 for frame in frames: 223 if ( 224 self._emulator.state_parser.get_agent_state(frame) 225 != AgentState.FREE_ROAM 226 ): 227 action_success = 1 228 break 229 for past_frame in prev_frames: 230 if not frame_changed(past_frame, frame): 231 action_success = -1 232 break 233 if action_success != 0: 234 break 235 prev_frames.append(frame) 236 if action_success == 0: 237 action_success = -1 238 return [self._state_tracker.report()], action_success 239 240 @staticmethod 241 def get_action_name() -> str: 242 return "Interact" 243 244 245class BaseMovementAction(HighLevelAction, ABC): 246 """ 247 Base class for movement actions in the Deja Vu environment. 248 249 Is Valid When: 250 - In Free Roam State 251 252 Action Success Interpretation: 253 - -1: Frame did not change, even on the first step 254 - 0: Finished all steps 255 - 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle. 256 - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle. 257 258 Action Returns: 259 - n_steps_taken (int): Number of steps actually taken 260 - rotated (bool or None): True if the player has not moved, but has rotated. 261 """ 262 263 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 264 REQUIRED_STATE_PARSER = DejaVuStateParser 265 266 def _is_uniform_quadrant(self, frame, quadrant_name) -> bool: 267 mapper = { 268 "screen_quadrant_1": "tr", 269 "screen_quadrant_2": "tl", 270 "screen_quadrant_3": "bl", 271 "screen_quadrant_4": "br", 272 } 273 quadrant_cells = self._emulator.state_parser.capture_grid_cells( 274 frame, quadrant=mapper[quadrant_name] 275 ) 276 keys = list(quadrant_cells.keys()) 277 x_min = 1e9 278 y_min = 1e9 279 x_max = -1e9 280 y_max = -1e9 281 for key in keys: 282 x_coord, y_coord = key 283 if x_coord * y_coord == 0: 284 quadrant_cells.pop(key) 285 else: 286 x_min = min(x_min, x_coord) 287 y_min = min(y_min, y_coord) 288 x_max = max(x_max, x_coord) 289 y_max = max(y_max, y_coord) 290 x_min = int(x_min) 291 y_min = int(y_min) 292 x_max = int(x_max) 293 y_max = int(y_max) 294 keys = list(quadrant_cells.keys()) 295 vertical_uniform = True 296 horizontal_uniform = True 297 for y in range(y_min + 1, y_max): 298 first_cell = None 299 for x in range(x_min + 1, x_max): 300 cell = quadrant_cells[(x, y)] 301 if first_cell is None: 302 first_cell = cell 303 else: 304 if first_cell.shape == cell.shape: 305 if frame_changed(first_cell, cell): 306 horizontal_uniform = False 307 break 308 if not horizontal_uniform: 309 break 310 if horizontal_uniform: 311 return True 312 for x in range(x_min + 1, x_max): 313 first_cell = None 314 for y in range(y_min + 1, y_max): 315 cell = quadrant_cells[(x, y)] 316 if first_cell is None: 317 first_cell = cell 318 else: 319 if first_cell.shape == cell.shape: 320 if frame_changed(first_cell, cell): 321 vertical_uniform = False 322 break 323 if not vertical_uniform: 324 break 325 if vertical_uniform: 326 return True 327 return False 328 329 def judge_movement( 330 self, previous_frame: np.ndarray, current_frame: np.ndarray 331 ) -> Tuple[bool, Optional[bool]]: 332 if not frame_changed(previous_frame, current_frame): 333 return False, False 334 flag = False 335 for quadrant in [ 336 "screen_quadrant_1", 337 "screen_quadrant_2", 338 "screen_quadrant_3", 339 "screen_quadrant_4", 340 ]: 341 prev_quad = self._emulator.state_parser.capture_named_region( 342 previous_frame, quadrant 343 ) 344 curr_quad = self._emulator.state_parser.capture_named_region( 345 current_frame, quadrant 346 ) 347 prev_uniform = ( 348 prev_quad.max() == prev_quad.min() 349 or self._is_uniform_quadrant(previous_frame, quadrant) 350 ) 351 curr_uniform = ( 352 curr_quad.max() == curr_quad.min() 353 or self._is_uniform_quadrant(current_frame, quadrant) 354 ) 355 if ( 356 not frame_changed(prev_quad, curr_quad) 357 and not prev_uniform 358 and not curr_uniform 359 ): 360 flag = True 361 break 362 if flag: 363 prev_player_cell = self._emulator.state_parser.capture_grid_cells( 364 previous_frame 365 )[(0, 0)] 366 curr_player_cell = self._emulator.state_parser.capture_grid_cells( 367 current_frame 368 )[(0, 0)] 369 if frame_changed(prev_player_cell, curr_player_cell): 370 return False, True 371 return False, False 372 return True, None 373 374 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 375 action_dict = { 376 "right": LowLevelActions.PRESS_ARROW_RIGHT, 377 "down": LowLevelActions.PRESS_ARROW_DOWN, 378 "up": LowLevelActions.PRESS_ARROW_UP, 379 "left": LowLevelActions.PRESS_ARROW_LEFT, 380 } 381 if direction not in action_dict.keys(): 382 log_error(f"Got invalid direction to move {direction}", self._parameters) 383 action = action_dict[direction] 384 action_success = -1 385 transition_state_dicts = [] 386 transition_frames = [] 387 previous_frame = self._emulator.get_current_frame() 388 n_step = 0 389 n_successful_steps = 0 390 has_rotated = None 391 agent_state = AgentState.FREE_ROAM 392 while n_step < steps and agent_state == AgentState.FREE_ROAM: 393 frames, done = self._emulator.step(action) 394 transition_state_dicts.append(self._state_tracker.report()) 395 transition_frames.extend(frames) 396 current_frame = self._emulator.get_current_frame() 397 if done: 398 break 399 player_moved, player_rotated = self.judge_movement( 400 previous_frame, current_frame 401 ) 402 if player_rotated is True: 403 has_rotated = True 404 if not player_moved and not player_rotated: 405 break 406 if player_moved: 407 n_successful_steps += 1 408 agent_state = self._emulator.state_parser.get_agent_state( 409 self._emulator.get_current_frame() 410 ) 411 if agent_state != AgentState.FREE_ROAM: 412 break 413 n_step += 1 414 previous_frame = current_frame 415 if agent_state != AgentState.FREE_ROAM: 416 action_success = 2 417 else: 418 if n_step <= 0: 419 action_success = -1 420 elif n_step == steps: 421 action_success = 0 422 else: 423 action_success = 1 424 transition_state_dicts[-1]["core"]["action_return"] = { 425 "n_steps_taken": n_successful_steps, 426 "rotated": has_rotated, 427 } 428 return transition_state_dicts, action_success 429 430 def is_valid(self, **kwargs): 431 return ( 432 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 433 == AgentState.FREE_ROAM 434 ) 435 436 437class MoveStepsAction(BaseMovementAction): 438 """ 439 Moves the agent in a specified cardinal direction for a specified number of steps. 440 """ 441 442 def get_action_space(self): 443 return Discrete(4 * HARD_MAX_STEPS) 444 445 def space_to_parameters(self, space_action): 446 direction = None 447 steps = None 448 if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS: 449 return None 450 if space_action < HARD_MAX_STEPS: 451 direction = "up" 452 steps = space_action 453 elif space_action < 2 * HARD_MAX_STEPS: 454 direction = "down" 455 steps = space_action - HARD_MAX_STEPS 456 elif space_action < 3 * HARD_MAX_STEPS: 457 direction = "left" 458 steps = space_action - 2 * HARD_MAX_STEPS 459 else: 460 direction = "right" 461 steps = space_action - 3 * HARD_MAX_STEPS 462 return {"direction": direction, "steps": steps + 1} 463 464 def parameters_to_space(self, direction: str, steps: int): 465 if steps is None or steps <= 0 or steps > HARD_MAX_STEPS: 466 return None 467 if direction == "up": 468 return steps - 1 469 if direction == "down": 470 return HARD_MAX_STEPS + steps - 1 471 if direction == "left": 472 return 2 * HARD_MAX_STEPS + steps - 1 473 if direction == "right": 474 return 3 * HARD_MAX_STEPS + steps - 1 475 return None 476 477 def _execute(self, direction, steps): 478 transition_states, status = self.move(direction=direction, steps=steps) 479 return transition_states, status 480 481 def is_valid(self, **kwargs): 482 direction = kwargs.get("direction") 483 steps = kwargs.get("steps") 484 if direction is not None and direction not in ["up", "down", "left", "right"]: 485 return False 486 if steps is not None: 487 if not isinstance(steps, int): 488 return False 489 if steps <= 0: 490 return False 491 return super().is_valid(**kwargs) 492 493 @staticmethod 494 def get_action_name(direction: str, steps: int) -> str: 495 return f"Move {direction} {steps}" 496 497 498class MoveGridAction(BaseMovementAction): 499 """ 500 Moves the agent on both axes. Will always try to move right/left first and then up/down. 501 """ 502 503 def get_action_space(self): 504 return Box( 505 low=-HARD_MAX_STEPS // 2, 506 high=HARD_MAX_STEPS // 2, 507 shape=(2,), 508 dtype=np.int8, 509 ) 510 511 def space_to_parameters(self, space_action): 512 right_action = space_action[0] 513 up_action = space_action[1] 514 return {"x_steps": right_action, "y_steps": up_action} 515 516 def parameters_to_space(self, x_steps, y_steps): 517 move_vec = np.zeros(2) 518 move_vec[0] = x_steps 519 move_vec[1] = y_steps 520 return move_vec 521 522 def _execute(self, x_steps, y_steps): 523 x_direction = "right" if x_steps >= 0 else "left" 524 y_direction = "up" if y_steps >= 0 else "down" 525 if x_steps != 0: 526 transition_states, status = self.move( 527 direction=x_direction, steps=abs(x_steps) 528 ) 529 if status != 0: 530 return transition_states, status 531 else: 532 transition_states = [] 533 if y_steps != 0: 534 more_transition_states, status = self.move( 535 direction=y_direction, steps=abs(y_steps) 536 ) 537 transition_states.extend(more_transition_states) 538 try: 539 status is not None 540 except NameError: 541 log_warn( 542 "Weird case where both x_steps and y_steps are 0 in MoveGridAction or something.", 543 self._parameters, 544 ) 545 transition_states = [self._state_tracker.report()] 546 status = -1 547 return transition_states, status 548 549 def is_valid(self, x_steps: int = None, y_steps: int = None): 550 if x_steps is not None and y_steps is not None: 551 if not isinstance(x_steps, int) or not isinstance(y_steps, int): 552 return False 553 if x_steps == 0 and y_steps == 0: 554 return False 555 return super().is_valid() 556 557 @staticmethod 558 def get_action_name(x_steps: int, y_steps: int) -> str: 559 return f"MoveGrid ({x_steps}, {y_steps})" 560 561 562class MenuAction(HighLevelAction): 563 """ 564 Allows navigation and option selection within Deja Vu menus. 565 566 Is Valid When: 567 - In Menu State 568 569 Action Success Interpretation: 570 - -1: Frame did not change. 571 - 0: Frame changed. 572 """ 573 574 REQUIRED_STATE_PARSER = DejaVuStateParser 575 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 576 577 _MENU_ACTION_MAP = { 578 "up": LowLevelActions.PRESS_ARROW_UP, 579 "down": LowLevelActions.PRESS_ARROW_DOWN, 580 "confirm": LowLevelActions.PRESS_BUTTON_A, 581 "left": LowLevelActions.PRESS_ARROW_LEFT, 582 "right": LowLevelActions.PRESS_ARROW_RIGHT, 583 "back": LowLevelActions.PRESS_BUTTON_B, 584 } 585 586 _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys()) 587 588 def is_valid(self, **kwargs): 589 menu_action = kwargs.get("menu_action", None) 590 if menu_action is not None and menu_action not in self._MENU_ACTION_KEYS: 591 return False 592 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 593 return state == AgentState.IN_MENU 594 595 def get_action_space(self): 596 return Discrete(len(self._MENU_ACTION_MAP)) 597 598 def parameters_to_space(self, menu_action): 599 if menu_action not in self._MENU_ACTION_KEYS: 600 return None 601 return self._MENU_ACTION_KEYS.index(menu_action) 602 603 def space_to_parameters(self, space_action): 604 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 605 return None 606 menu_action = self._MENU_ACTION_KEYS[space_action] 607 return {"menu_action": menu_action} 608 609 def _execute(self, menu_action): 610 action = self._MENU_ACTION_MAP[menu_action] 611 current_frame = self._emulator.get_current_frame() 612 frames, done = self._emulator.step(action) 613 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 614 return [self._state_tracker.report()], action_success 615 616 @staticmethod 617 def get_action_name(menu_action: str) -> str: 618 return f"Menu {menu_action}" 619 620 621class OpenMenuAction(HighLevelAction): 622 """ 623 Opens the investigation menu from free roam and optionally navigates to a section. 624 625 Is Valid When: 626 - In Free Roam State 627 628 Action Success Interpretation: 629 - -1: Could not open the menu or reach the requested section. 630 - 0: Menu opened (and section selected if specified). 631 """ 632 633 OPTIONS = ["open", "case_notes", "evidence", "location"] 634 635 REQUIRED_STATE_PARSER = DejaVuStateParser 636 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 637 638 def get_action_space(self): 639 return Discrete(len(self.OPTIONS)) 640 641 def space_to_parameters(self, space_action): 642 if space_action < 0 or space_action >= len(self.OPTIONS): 643 return None 644 return {"option": self.OPTIONS[space_action]} 645 646 def parameters_to_space(self, option: Optional[str] = None): 647 if option is None: 648 option = "open" 649 if option not in self.OPTIONS: 650 return None 651 return self.OPTIONS.index(option) 652 653 def is_valid(self, option: Optional[str] = None): 654 if option is not None and option not in self.OPTIONS: 655 return False 656 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 657 return state == AgentState.FREE_ROAM 658 659 def _is_target_menu(self, option: str, frame: np.ndarray) -> bool: 660 parser = self._emulator.state_parser 661 if option == "case_notes": 662 return parser.is_in_case_notes(frame) 663 if option == "evidence": 664 return parser.is_in_evidence_menu(frame) 665 if option == "location": 666 return parser.is_location_menu_open(frame) 667 return False 668 669 def _execute(self, option: Optional[str] = None): 670 if option is None: 671 option = "open" 672 self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 673 state_reports = [self._state_tracker.report()] 674 current_frame = self._emulator.get_current_frame() 675 if not self._emulator.state_parser.is_in_menu(current_frame): 676 return state_reports, -1 677 if option == "open": 678 return state_reports, 0 679 if self._is_target_menu(option, current_frame): 680 return state_reports, 0 681 for action in [ 682 LowLevelActions.PRESS_ARROW_RIGHT, 683 LowLevelActions.PRESS_ARROW_LEFT, 684 ]: 685 for _ in range(MENU_NAV_MAX_STEPS): 686 self._emulator.step(action) 687 state_reports.append(self._state_tracker.report()) 688 current_frame = self._emulator.get_current_frame() 689 if self._is_target_menu(option, current_frame): 690 return state_reports, 0 691 return state_reports, -1 692 693 @staticmethod 694 def get_action_name(option: str) -> str: 695 return f"OpenMenu {option}" 696 697 698class PuzzleAction(HighLevelAction): 699 """ 700 Allows navigation and option selection within Deja Vu puzzle/deduction states. 701 702 Is Valid When: 703 - In Puzzle State 704 705 Action Success Interpretation: 706 - -1: Frame did not change. 707 - 0: Frame changed. 708 """ 709 710 REQUIRED_STATE_PARSER = DejaVuStateParser 711 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 712 713 _PUZZLE_ACTION_MAP = { 714 "up": LowLevelActions.PRESS_ARROW_UP, 715 "down": LowLevelActions.PRESS_ARROW_DOWN, 716 "confirm": LowLevelActions.PRESS_BUTTON_A, 717 "left": LowLevelActions.PRESS_ARROW_LEFT, 718 "right": LowLevelActions.PRESS_ARROW_RIGHT, 719 "back": LowLevelActions.PRESS_BUTTON_B, 720 } 721 722 _PUZZLE_ACTION_KEYS = list(_PUZZLE_ACTION_MAP.keys()) 723 724 def is_valid(self, **kwargs): 725 puzzle_action = kwargs.get("puzzle_action", None) 726 if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS: 727 return False 728 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 729 return state == AgentState.IN_PUZZLE 730 731 def get_action_space(self): 732 return Discrete(len(self._PUZZLE_ACTION_MAP)) 733 734 def parameters_to_space(self, puzzle_action): 735 if puzzle_action not in self._PUZZLE_ACTION_KEYS: 736 return None 737 return self._PUZZLE_ACTION_KEYS.index(puzzle_action) 738 739 def space_to_parameters(self, space_action): 740 if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP): 741 return None 742 puzzle_action = self._PUZZLE_ACTION_KEYS[space_action] 743 return {"puzzle_action": puzzle_action} 744 745 def _execute(self, puzzle_action): 746 action = self._PUZZLE_ACTION_MAP[puzzle_action] 747 current_frame = self._emulator.get_current_frame() 748 frames, done = self._emulator.step(action) 749 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 750 return [self._state_tracker.report()], action_success 751 752 @staticmethod 753 def get_action_name(puzzle_action: str) -> str: 754 return f"Puzzle {puzzle_action}"
16def frame_changed(past: np.ndarray, present: np.ndarray, epsilon: float = 0.01) -> bool: 17 """Return True if the mean pixel difference between two frames is greater than epsilon.""" 18 return np.abs(past - present).mean() > epsilon
Return True if the mean pixel difference between two frames is greater than epsilon.
21class TickUntilStable(SingleHighLevelAction): 22 """Tick the emulator until the screen becomes stable (no change).""" 23 24 REQUIRED_STATE_PARSER = DejaVuStateParser 25 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 26 27 def is_valid(self, **kwargs): 28 return True 29 30 def _execute(self, max_ticks: int = 30): 31 prev = self._emulator.get_current_frame() 32 for _ in range(max_ticks): 33 self._emulator.step(None) 34 curr = self._emulator.get_current_frame() 35 if not frame_changed(prev, curr): 36 return [self._state_tracker.report()], 0 37 prev = curr 38 return [self._state_tracker.report()], -1 39 40 @staticmethod 41 def get_action_name() -> str: 42 return "TickUntilStable"
Tick the emulator until the screen becomes stable (no change).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
45class MoveCursor(SingleHighLevelAction): 46 """Move the menu cursor in a given direction (up, down, left, right).""" 47 48 REQUIRED_STATE_PARSER = DejaVuStateParser 49 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 50 _ACTION_MAP = { 51 "up": LowLevelActions.PRESS_ARROW_UP, 52 "down": LowLevelActions.PRESS_ARROW_DOWN, 53 "left": LowLevelActions.PRESS_ARROW_LEFT, 54 "right": LowLevelActions.PRESS_ARROW_RIGHT, 55 } 56 57 def is_valid(self, direction=None, **kwargs): 58 return direction in self._ACTION_MAP 59 60 def _execute(self, direction): 61 action = self._ACTION_MAP[direction] 62 prev = self._emulator.get_current_frame() 63 self._emulator.step(action) 64 curr = self._emulator.get_current_frame() 65 return [self._state_tracker.report()], 0 if frame_changed(prev, curr) else -1 66 67 @staticmethod 68 def get_action_name(**kwargs) -> str: 69 return f"MoveCursor {kwargs.get('direction', '')}"
Move the menu cursor in a given direction (up, down, left, right).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
67 @staticmethod 68 def get_action_name(**kwargs) -> str: 69 return f"MoveCursor {kwargs.get('direction', '')}"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.
72class MoveGrid(HighLevelAction): 73 """Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical.""" 74 75 REQUIRED_STATE_PARSER = DejaVuStateParser 76 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 77 78 def get_action_space(self): 79 return Box( 80 low=-HARD_MAX_STEPS // 2, 81 high=HARD_MAX_STEPS // 2, 82 shape=(2,), 83 dtype=np.int8, 84 ) 85 86 def is_valid(self, x_steps=None, y_steps=None, **kwargs): 87 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 88 return state == AgentState.FREE_ROAM and (x_steps or y_steps) 89 90 def _execute(self, x_steps, y_steps): 91 reports = [] 92 for axis, steps, dir_pos, dir_neg in [ 93 ("x", x_steps, "right", "left"), 94 ("y", y_steps, "down", "up"), 95 ]: 96 if steps: 97 direction = dir_pos if steps > 0 else dir_neg 98 for _ in range(abs(steps)): 99 prev = self._emulator.get_current_frame() 100 self._emulator.step(MoveStepsAction._ACTION_MAP[direction]) 101 curr = self._emulator.get_current_frame() 102 reports.append(self._state_tracker.report()) 103 if not frame_changed(prev, curr): 104 return reports, -1 105 return reports, 0 106 107 @staticmethod 108 def get_action_name(x_steps: int, y_steps: int) -> str: 109 return f"MoveGrid ({x_steps}, {y_steps})"
Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical.
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
78 def get_action_space(self): 79 return Box( 80 low=-HARD_MAX_STEPS // 2, 81 high=HARD_MAX_STEPS // 2, 82 shape=(2,), 83 dtype=np.int8, 84 )
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
86 def is_valid(self, x_steps=None, y_steps=None, **kwargs): 87 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 88 return state == AgentState.FREE_ROAM and (x_steps or y_steps)
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
107 @staticmethod 108 def get_action_name(x_steps: int, y_steps: int) -> str: 109 return f"MoveGrid ({x_steps}, {y_steps})"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.
112class OpenButtonMenu(SingleHighLevelAction): 113 """Open the bottom button menu (usually START button).""" 114 115 REQUIRED_STATE_PARSER = DejaVuStateParser 116 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 117 118 def is_valid(self, **kwargs): 119 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 120 return state == AgentState.FREE_ROAM 121 122 def _execute(self): 123 self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 124 return [self._state_tracker.report()], 0 125 126 @staticmethod 127 def get_action_name() -> str: 128 return "OpenButtonMenu"
Open the bottom button menu (usually START button).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
118 def is_valid(self, **kwargs): 119 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 120 return state == AgentState.FREE_ROAM
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
131class CloseButtonMenu(SingleHighLevelAction): 132 """Close the bottom button menu (usually B button).""" 133 134 REQUIRED_STATE_PARSER = DejaVuStateParser 135 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 136 137 def is_valid(self, **kwargs): 138 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 139 return state == AgentState.IN_MENU 140 141 def _execute(self): 142 self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 143 return [self._state_tracker.report()], 0 144 145 @staticmethod 146 def get_action_name() -> str: 147 return "CloseButtonMenu"
Close the bottom button menu (usually B button).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
137 def is_valid(self, **kwargs): 138 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 139 return state == AgentState.IN_MENU
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
150class SelectMenuOption(SingleHighLevelAction): 151 """Select the current menu option (A button).""" 152 153 REQUIRED_STATE_PARSER = DejaVuStateParser 154 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 155 156 def is_valid(self, **kwargs): 157 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 158 return state == AgentState.IN_MENU 159 160 def _execute(self): 161 self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 162 return [self._state_tracker.report()], 0 163 164 @staticmethod 165 def get_action_name() -> str: 166 return "SelectMenuOption"
Select the current menu option (A button).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
156 def is_valid(self, **kwargs): 157 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 158 return state == AgentState.IN_MENU
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
169class AdvanceDialogue(SingleHighLevelAction): 170 """Advance dialogue (B button).""" 171 172 REQUIRED_STATE_PARSER = DejaVuStateParser 173 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 174 175 def is_valid(self, **kwargs): 176 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 177 return state == AgentState.IN_DIALOGUE 178 179 def _execute(self): 180 self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 181 return [self._state_tracker.report()], 0 182 183 @staticmethod 184 def get_action_name() -> str: 185 return "AdvanceDialogue"
Advance dialogue (B button).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
175 def is_valid(self, **kwargs): 176 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 177 return state == AgentState.IN_DIALOGUE
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
189class MoveStepsAction: 190 _ACTION_MAP = { 191 "up": LowLevelActions.PRESS_ARROW_UP, 192 "down": LowLevelActions.PRESS_ARROW_DOWN, 193 "left": LowLevelActions.PRESS_ARROW_LEFT, 194 "right": LowLevelActions.PRESS_ARROW_RIGHT, 195 }
Moves the agent in a specified cardinal direction for a specified number of steps.
482 def is_valid(self, **kwargs): 483 direction = kwargs.get("direction") 484 steps = kwargs.get("steps") 485 if direction is not None and direction not in ["up", "down", "left", "right"]: 486 return False 487 if steps is not None: 488 if not isinstance(steps, int): 489 return False 490 if steps <= 0: 491 return False 492 return super().is_valid(**kwargs)
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
446 def space_to_parameters(self, space_action): 447 direction = None 448 steps = None 449 if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS: 450 return None 451 if space_action < HARD_MAX_STEPS: 452 direction = "up" 453 steps = space_action 454 elif space_action < 2 * HARD_MAX_STEPS: 455 direction = "down" 456 steps = space_action - HARD_MAX_STEPS 457 elif space_action < 3 * HARD_MAX_STEPS: 458 direction = "left" 459 steps = space_action - 2 * HARD_MAX_STEPS 460 else: 461 direction = "right" 462 steps = space_action - 3 * HARD_MAX_STEPS 463 return {"direction": direction, "steps": steps + 1}
Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.
Parameters
- space_action: The action in the high level action's parameter space.
Returns
The high level action's parameters corresponding to the space action.
465 def parameters_to_space(self, direction: str, steps: int): 466 if steps is None or steps <= 0 or steps > HARD_MAX_STEPS: 467 return None 468 if direction == "up": 469 return steps - 1 470 if direction == "down": 471 return HARD_MAX_STEPS + steps - 1 472 if direction == "left": 473 return 2 * HARD_MAX_STEPS + steps - 1 474 if direction == "right": 475 return 3 * HARD_MAX_STEPS + steps - 1 476 return None
Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.
Parameters
- kwargs: The high level action's parameters.
Returns
The action in the high level action's parameter space.
494 @staticmethod 495 def get_action_name(direction: str, steps: int) -> str: 496 return f"Move {direction} {steps}"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.
198class InteractAction(SingleHighLevelAction): 199 """ 200 Presses the A button to interact with an object in front of the agent. 201 202 Is Valid When: 203 - In Free Roam State 204 205 Action Success Interpretation: 206 - -1: Frame did not change or agent still in free roam state 207 - 1: Agent not in free roam state 208 """ 209 210 REQUIRED_STATE_PARSER = DejaVuStateParser 211 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 212 213 def is_valid(self, **kwargs): 214 return ( 215 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 216 == AgentState.FREE_ROAM 217 ) 218 219 def _execute(self): 220 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 221 action_success = 0 222 prev_frames = [] 223 for frame in frames: 224 if ( 225 self._emulator.state_parser.get_agent_state(frame) 226 != AgentState.FREE_ROAM 227 ): 228 action_success = 1 229 break 230 for past_frame in prev_frames: 231 if not frame_changed(past_frame, frame): 232 action_success = -1 233 break 234 if action_success != 0: 235 break 236 prev_frames.append(frame) 237 if action_success == 0: 238 action_success = -1 239 return [self._state_tracker.report()], action_success 240 241 @staticmethod 242 def get_action_name() -> str: 243 return "Interact"
Presses the A button to interact with an object in front of the agent.
Is Valid When:
- In Free Roam State
Action Success Interpretation:
- -1: Frame did not change or agent still in free roam state
- 1: Agent not in free roam state
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
213 def is_valid(self, **kwargs): 214 return ( 215 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 216 == AgentState.FREE_ROAM 217 )
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
246class BaseMovementAction(HighLevelAction, ABC): 247 """ 248 Base class for movement actions in the Deja Vu environment. 249 250 Is Valid When: 251 - In Free Roam State 252 253 Action Success Interpretation: 254 - -1: Frame did not change, even on the first step 255 - 0: Finished all steps 256 - 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle. 257 - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle. 258 259 Action Returns: 260 - n_steps_taken (int): Number of steps actually taken 261 - rotated (bool or None): True if the player has not moved, but has rotated. 262 """ 263 264 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 265 REQUIRED_STATE_PARSER = DejaVuStateParser 266 267 def _is_uniform_quadrant(self, frame, quadrant_name) -> bool: 268 mapper = { 269 "screen_quadrant_1": "tr", 270 "screen_quadrant_2": "tl", 271 "screen_quadrant_3": "bl", 272 "screen_quadrant_4": "br", 273 } 274 quadrant_cells = self._emulator.state_parser.capture_grid_cells( 275 frame, quadrant=mapper[quadrant_name] 276 ) 277 keys = list(quadrant_cells.keys()) 278 x_min = 1e9 279 y_min = 1e9 280 x_max = -1e9 281 y_max = -1e9 282 for key in keys: 283 x_coord, y_coord = key 284 if x_coord * y_coord == 0: 285 quadrant_cells.pop(key) 286 else: 287 x_min = min(x_min, x_coord) 288 y_min = min(y_min, y_coord) 289 x_max = max(x_max, x_coord) 290 y_max = max(y_max, y_coord) 291 x_min = int(x_min) 292 y_min = int(y_min) 293 x_max = int(x_max) 294 y_max = int(y_max) 295 keys = list(quadrant_cells.keys()) 296 vertical_uniform = True 297 horizontal_uniform = True 298 for y in range(y_min + 1, y_max): 299 first_cell = None 300 for x in range(x_min + 1, x_max): 301 cell = quadrant_cells[(x, y)] 302 if first_cell is None: 303 first_cell = cell 304 else: 305 if first_cell.shape == cell.shape: 306 if frame_changed(first_cell, cell): 307 horizontal_uniform = False 308 break 309 if not horizontal_uniform: 310 break 311 if horizontal_uniform: 312 return True 313 for x in range(x_min + 1, x_max): 314 first_cell = None 315 for y in range(y_min + 1, y_max): 316 cell = quadrant_cells[(x, y)] 317 if first_cell is None: 318 first_cell = cell 319 else: 320 if first_cell.shape == cell.shape: 321 if frame_changed(first_cell, cell): 322 vertical_uniform = False 323 break 324 if not vertical_uniform: 325 break 326 if vertical_uniform: 327 return True 328 return False 329 330 def judge_movement( 331 self, previous_frame: np.ndarray, current_frame: np.ndarray 332 ) -> Tuple[bool, Optional[bool]]: 333 if not frame_changed(previous_frame, current_frame): 334 return False, False 335 flag = False 336 for quadrant in [ 337 "screen_quadrant_1", 338 "screen_quadrant_2", 339 "screen_quadrant_3", 340 "screen_quadrant_4", 341 ]: 342 prev_quad = self._emulator.state_parser.capture_named_region( 343 previous_frame, quadrant 344 ) 345 curr_quad = self._emulator.state_parser.capture_named_region( 346 current_frame, quadrant 347 ) 348 prev_uniform = ( 349 prev_quad.max() == prev_quad.min() 350 or self._is_uniform_quadrant(previous_frame, quadrant) 351 ) 352 curr_uniform = ( 353 curr_quad.max() == curr_quad.min() 354 or self._is_uniform_quadrant(current_frame, quadrant) 355 ) 356 if ( 357 not frame_changed(prev_quad, curr_quad) 358 and not prev_uniform 359 and not curr_uniform 360 ): 361 flag = True 362 break 363 if flag: 364 prev_player_cell = self._emulator.state_parser.capture_grid_cells( 365 previous_frame 366 )[(0, 0)] 367 curr_player_cell = self._emulator.state_parser.capture_grid_cells( 368 current_frame 369 )[(0, 0)] 370 if frame_changed(prev_player_cell, curr_player_cell): 371 return False, True 372 return False, False 373 return True, None 374 375 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 376 action_dict = { 377 "right": LowLevelActions.PRESS_ARROW_RIGHT, 378 "down": LowLevelActions.PRESS_ARROW_DOWN, 379 "up": LowLevelActions.PRESS_ARROW_UP, 380 "left": LowLevelActions.PRESS_ARROW_LEFT, 381 } 382 if direction not in action_dict.keys(): 383 log_error(f"Got invalid direction to move {direction}", self._parameters) 384 action = action_dict[direction] 385 action_success = -1 386 transition_state_dicts = [] 387 transition_frames = [] 388 previous_frame = self._emulator.get_current_frame() 389 n_step = 0 390 n_successful_steps = 0 391 has_rotated = None 392 agent_state = AgentState.FREE_ROAM 393 while n_step < steps and agent_state == AgentState.FREE_ROAM: 394 frames, done = self._emulator.step(action) 395 transition_state_dicts.append(self._state_tracker.report()) 396 transition_frames.extend(frames) 397 current_frame = self._emulator.get_current_frame() 398 if done: 399 break 400 player_moved, player_rotated = self.judge_movement( 401 previous_frame, current_frame 402 ) 403 if player_rotated is True: 404 has_rotated = True 405 if not player_moved and not player_rotated: 406 break 407 if player_moved: 408 n_successful_steps += 1 409 agent_state = self._emulator.state_parser.get_agent_state( 410 self._emulator.get_current_frame() 411 ) 412 if agent_state != AgentState.FREE_ROAM: 413 break 414 n_step += 1 415 previous_frame = current_frame 416 if agent_state != AgentState.FREE_ROAM: 417 action_success = 2 418 else: 419 if n_step <= 0: 420 action_success = -1 421 elif n_step == steps: 422 action_success = 0 423 else: 424 action_success = 1 425 transition_state_dicts[-1]["core"]["action_return"] = { 426 "n_steps_taken": n_successful_steps, 427 "rotated": has_rotated, 428 } 429 return transition_state_dicts, action_success 430 431 def is_valid(self, **kwargs): 432 return ( 433 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 434 == AgentState.FREE_ROAM 435 )
Base class for movement actions in the Deja Vu environment.
Is Valid When:
- In Free Roam State
Action Success Interpretation:
- -1: Frame did not change, even on the first step
- 0: Finished all steps
- 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle.
- 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
Action Returns:
- n_steps_taken (int): Number of steps actually taken
- rotated (bool or None): True if the player has not moved, but has rotated.
The state tracker that tracks the minimal state information required for the action to function.
The state parser that parses the minimal state information required for the action to function.
330 def judge_movement( 331 self, previous_frame: np.ndarray, current_frame: np.ndarray 332 ) -> Tuple[bool, Optional[bool]]: 333 if not frame_changed(previous_frame, current_frame): 334 return False, False 335 flag = False 336 for quadrant in [ 337 "screen_quadrant_1", 338 "screen_quadrant_2", 339 "screen_quadrant_3", 340 "screen_quadrant_4", 341 ]: 342 prev_quad = self._emulator.state_parser.capture_named_region( 343 previous_frame, quadrant 344 ) 345 curr_quad = self._emulator.state_parser.capture_named_region( 346 current_frame, quadrant 347 ) 348 prev_uniform = ( 349 prev_quad.max() == prev_quad.min() 350 or self._is_uniform_quadrant(previous_frame, quadrant) 351 ) 352 curr_uniform = ( 353 curr_quad.max() == curr_quad.min() 354 or self._is_uniform_quadrant(current_frame, quadrant) 355 ) 356 if ( 357 not frame_changed(prev_quad, curr_quad) 358 and not prev_uniform 359 and not curr_uniform 360 ): 361 flag = True 362 break 363 if flag: 364 prev_player_cell = self._emulator.state_parser.capture_grid_cells( 365 previous_frame 366 )[(0, 0)] 367 curr_player_cell = self._emulator.state_parser.capture_grid_cells( 368 current_frame 369 )[(0, 0)] 370 if frame_changed(prev_player_cell, curr_player_cell): 371 return False, True 372 return False, False 373 return True, None
375 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 376 action_dict = { 377 "right": LowLevelActions.PRESS_ARROW_RIGHT, 378 "down": LowLevelActions.PRESS_ARROW_DOWN, 379 "up": LowLevelActions.PRESS_ARROW_UP, 380 "left": LowLevelActions.PRESS_ARROW_LEFT, 381 } 382 if direction not in action_dict.keys(): 383 log_error(f"Got invalid direction to move {direction}", self._parameters) 384 action = action_dict[direction] 385 action_success = -1 386 transition_state_dicts = [] 387 transition_frames = [] 388 previous_frame = self._emulator.get_current_frame() 389 n_step = 0 390 n_successful_steps = 0 391 has_rotated = None 392 agent_state = AgentState.FREE_ROAM 393 while n_step < steps and agent_state == AgentState.FREE_ROAM: 394 frames, done = self._emulator.step(action) 395 transition_state_dicts.append(self._state_tracker.report()) 396 transition_frames.extend(frames) 397 current_frame = self._emulator.get_current_frame() 398 if done: 399 break 400 player_moved, player_rotated = self.judge_movement( 401 previous_frame, current_frame 402 ) 403 if player_rotated is True: 404 has_rotated = True 405 if not player_moved and not player_rotated: 406 break 407 if player_moved: 408 n_successful_steps += 1 409 agent_state = self._emulator.state_parser.get_agent_state( 410 self._emulator.get_current_frame() 411 ) 412 if agent_state != AgentState.FREE_ROAM: 413 break 414 n_step += 1 415 previous_frame = current_frame 416 if agent_state != AgentState.FREE_ROAM: 417 action_success = 2 418 else: 419 if n_step <= 0: 420 action_success = -1 421 elif n_step == steps: 422 action_success = 0 423 else: 424 action_success = 1 425 transition_state_dicts[-1]["core"]["action_return"] = { 426 "n_steps_taken": n_successful_steps, 427 "rotated": has_rotated, 428 } 429 return transition_state_dicts, action_success
431 def is_valid(self, **kwargs): 432 return ( 433 self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 434 == AgentState.FREE_ROAM 435 )
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
499class MoveGridAction(BaseMovementAction): 500 """ 501 Moves the agent on both axes. Will always try to move right/left first and then up/down. 502 """ 503 504 def get_action_space(self): 505 return Box( 506 low=-HARD_MAX_STEPS // 2, 507 high=HARD_MAX_STEPS // 2, 508 shape=(2,), 509 dtype=np.int8, 510 ) 511 512 def space_to_parameters(self, space_action): 513 right_action = space_action[0] 514 up_action = space_action[1] 515 return {"x_steps": right_action, "y_steps": up_action} 516 517 def parameters_to_space(self, x_steps, y_steps): 518 move_vec = np.zeros(2) 519 move_vec[0] = x_steps 520 move_vec[1] = y_steps 521 return move_vec 522 523 def _execute(self, x_steps, y_steps): 524 x_direction = "right" if x_steps >= 0 else "left" 525 y_direction = "up" if y_steps >= 0 else "down" 526 if x_steps != 0: 527 transition_states, status = self.move( 528 direction=x_direction, steps=abs(x_steps) 529 ) 530 if status != 0: 531 return transition_states, status 532 else: 533 transition_states = [] 534 if y_steps != 0: 535 more_transition_states, status = self.move( 536 direction=y_direction, steps=abs(y_steps) 537 ) 538 transition_states.extend(more_transition_states) 539 try: 540 status is not None 541 except NameError: 542 log_warn( 543 "Weird case where both x_steps and y_steps are 0 in MoveGridAction or something.", 544 self._parameters, 545 ) 546 transition_states = [self._state_tracker.report()] 547 status = -1 548 return transition_states, status 549 550 def is_valid(self, x_steps: int = None, y_steps: int = None): 551 if x_steps is not None and y_steps is not None: 552 if not isinstance(x_steps, int) or not isinstance(y_steps, int): 553 return False 554 if x_steps == 0 and y_steps == 0: 555 return False 556 return super().is_valid() 557 558 @staticmethod 559 def get_action_name(x_steps: int, y_steps: int) -> str: 560 return f"MoveGrid ({x_steps}, {y_steps})"
Moves the agent on both axes. Will always try to move right/left first and then up/down.
504 def get_action_space(self): 505 return Box( 506 low=-HARD_MAX_STEPS // 2, 507 high=HARD_MAX_STEPS // 2, 508 shape=(2,), 509 dtype=np.int8, 510 )
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
512 def space_to_parameters(self, space_action): 513 right_action = space_action[0] 514 up_action = space_action[1] 515 return {"x_steps": right_action, "y_steps": up_action}
Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.
Parameters
- space_action: The action in the high level action's parameter space.
Returns
The high level action's parameters corresponding to the space action.
517 def parameters_to_space(self, x_steps, y_steps): 518 move_vec = np.zeros(2) 519 move_vec[0] = x_steps 520 move_vec[1] = y_steps 521 return move_vec
Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.
Parameters
- kwargs: The high level action's parameters.
Returns
The action in the high level action's parameter space.
550 def is_valid(self, x_steps: int = None, y_steps: int = None): 551 if x_steps is not None and y_steps is not None: 552 if not isinstance(x_steps, int) or not isinstance(y_steps, int): 553 return False 554 if x_steps == 0 and y_steps == 0: 555 return False 556 return super().is_valid()
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
558 @staticmethod 559 def get_action_name(x_steps: int, y_steps: int) -> str: 560 return f"MoveGrid ({x_steps}, {y_steps})"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.
563class MenuAction(HighLevelAction): 564 """ 565 Allows navigation and option selection within Deja Vu menus. 566 567 Is Valid When: 568 - In Menu State 569 570 Action Success Interpretation: 571 - -1: Frame did not change. 572 - 0: Frame changed. 573 """ 574 575 REQUIRED_STATE_PARSER = DejaVuStateParser 576 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 577 578 _MENU_ACTION_MAP = { 579 "up": LowLevelActions.PRESS_ARROW_UP, 580 "down": LowLevelActions.PRESS_ARROW_DOWN, 581 "confirm": LowLevelActions.PRESS_BUTTON_A, 582 "left": LowLevelActions.PRESS_ARROW_LEFT, 583 "right": LowLevelActions.PRESS_ARROW_RIGHT, 584 "back": LowLevelActions.PRESS_BUTTON_B, 585 } 586 587 _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys()) 588 589 def is_valid(self, **kwargs): 590 menu_action = kwargs.get("menu_action", None) 591 if menu_action is not None and menu_action not in self._MENU_ACTION_KEYS: 592 return False 593 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 594 return state == AgentState.IN_MENU 595 596 def get_action_space(self): 597 return Discrete(len(self._MENU_ACTION_MAP)) 598 599 def parameters_to_space(self, menu_action): 600 if menu_action not in self._MENU_ACTION_KEYS: 601 return None 602 return self._MENU_ACTION_KEYS.index(menu_action) 603 604 def space_to_parameters(self, space_action): 605 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 606 return None 607 menu_action = self._MENU_ACTION_KEYS[space_action] 608 return {"menu_action": menu_action} 609 610 def _execute(self, menu_action): 611 action = self._MENU_ACTION_MAP[menu_action] 612 current_frame = self._emulator.get_current_frame() 613 frames, done = self._emulator.step(action) 614 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 615 return [self._state_tracker.report()], action_success 616 617 @staticmethod 618 def get_action_name(menu_action: str) -> str: 619 return f"Menu {menu_action}"
Allows navigation and option selection within Deja Vu menus.
Is Valid When:
- In Menu State
Action Success Interpretation:
- -1: Frame did not change.
- 0: Frame changed.
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
589 def is_valid(self, **kwargs): 590 menu_action = kwargs.get("menu_action", None) 591 if menu_action is not None and menu_action not in self._MENU_ACTION_KEYS: 592 return False 593 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 594 return state == AgentState.IN_MENU
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
599 def parameters_to_space(self, menu_action): 600 if menu_action not in self._MENU_ACTION_KEYS: 601 return None 602 return self._MENU_ACTION_KEYS.index(menu_action)
Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.
Parameters
- kwargs: The high level action's parameters.
Returns
The action in the high level action's parameter space.
604 def space_to_parameters(self, space_action): 605 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 606 return None 607 menu_action = self._MENU_ACTION_KEYS[space_action] 608 return {"menu_action": menu_action}
Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.
Parameters
- space_action: The action in the high level action's parameter space.
Returns
The high level action's parameters corresponding to the space action.
617 @staticmethod 618 def get_action_name(menu_action: str) -> str: 619 return f"Menu {menu_action}"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.
622class OpenMenuAction(HighLevelAction): 623 """ 624 Opens the investigation menu from free roam and optionally navigates to a section. 625 626 Is Valid When: 627 - In Free Roam State 628 629 Action Success Interpretation: 630 - -1: Could not open the menu or reach the requested section. 631 - 0: Menu opened (and section selected if specified). 632 """ 633 634 OPTIONS = ["open", "case_notes", "evidence", "location"] 635 636 REQUIRED_STATE_PARSER = DejaVuStateParser 637 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 638 639 def get_action_space(self): 640 return Discrete(len(self.OPTIONS)) 641 642 def space_to_parameters(self, space_action): 643 if space_action < 0 or space_action >= len(self.OPTIONS): 644 return None 645 return {"option": self.OPTIONS[space_action]} 646 647 def parameters_to_space(self, option: Optional[str] = None): 648 if option is None: 649 option = "open" 650 if option not in self.OPTIONS: 651 return None 652 return self.OPTIONS.index(option) 653 654 def is_valid(self, option: Optional[str] = None): 655 if option is not None and option not in self.OPTIONS: 656 return False 657 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 658 return state == AgentState.FREE_ROAM 659 660 def _is_target_menu(self, option: str, frame: np.ndarray) -> bool: 661 parser = self._emulator.state_parser 662 if option == "case_notes": 663 return parser.is_in_case_notes(frame) 664 if option == "evidence": 665 return parser.is_in_evidence_menu(frame) 666 if option == "location": 667 return parser.is_location_menu_open(frame) 668 return False 669 670 def _execute(self, option: Optional[str] = None): 671 if option is None: 672 option = "open" 673 self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 674 state_reports = [self._state_tracker.report()] 675 current_frame = self._emulator.get_current_frame() 676 if not self._emulator.state_parser.is_in_menu(current_frame): 677 return state_reports, -1 678 if option == "open": 679 return state_reports, 0 680 if self._is_target_menu(option, current_frame): 681 return state_reports, 0 682 for action in [ 683 LowLevelActions.PRESS_ARROW_RIGHT, 684 LowLevelActions.PRESS_ARROW_LEFT, 685 ]: 686 for _ in range(MENU_NAV_MAX_STEPS): 687 self._emulator.step(action) 688 state_reports.append(self._state_tracker.report()) 689 current_frame = self._emulator.get_current_frame() 690 if self._is_target_menu(option, current_frame): 691 return state_reports, 0 692 return state_reports, -1 693 694 @staticmethod 695 def get_action_name(option: str) -> str: 696 return f"OpenMenu {option}"
Opens the investigation menu from free roam and optionally navigates to a section.
Is Valid When:
- In Free Roam State
Action Success Interpretation:
- -1: Could not open the menu or reach the requested section.
- 0: Menu opened (and section selected if specified).
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
642 def space_to_parameters(self, space_action): 643 if space_action < 0 or space_action >= len(self.OPTIONS): 644 return None 645 return {"option": self.OPTIONS[space_action]}
Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.
Parameters
- space_action: The action in the high level action's parameter space.
Returns
The high level action's parameters corresponding to the space action.
647 def parameters_to_space(self, option: Optional[str] = None): 648 if option is None: 649 option = "open" 650 if option not in self.OPTIONS: 651 return None 652 return self.OPTIONS.index(option)
Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.
Parameters
- kwargs: The high level action's parameters.
Returns
The action in the high level action's parameter space.
654 def is_valid(self, option: Optional[str] = None): 655 if option is not None and option not in self.OPTIONS: 656 return False 657 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 658 return state == AgentState.FREE_ROAM
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
699class PuzzleAction(HighLevelAction): 700 """ 701 Allows navigation and option selection within Deja Vu puzzle/deduction states. 702 703 Is Valid When: 704 - In Puzzle State 705 706 Action Success Interpretation: 707 - -1: Frame did not change. 708 - 0: Frame changed. 709 """ 710 711 REQUIRED_STATE_PARSER = DejaVuStateParser 712 REQUIRED_STATE_TRACKER = CoreDejaVuTracker 713 714 _PUZZLE_ACTION_MAP = { 715 "up": LowLevelActions.PRESS_ARROW_UP, 716 "down": LowLevelActions.PRESS_ARROW_DOWN, 717 "confirm": LowLevelActions.PRESS_BUTTON_A, 718 "left": LowLevelActions.PRESS_ARROW_LEFT, 719 "right": LowLevelActions.PRESS_ARROW_RIGHT, 720 "back": LowLevelActions.PRESS_BUTTON_B, 721 } 722 723 _PUZZLE_ACTION_KEYS = list(_PUZZLE_ACTION_MAP.keys()) 724 725 def is_valid(self, **kwargs): 726 puzzle_action = kwargs.get("puzzle_action", None) 727 if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS: 728 return False 729 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 730 return state == AgentState.IN_PUZZLE 731 732 def get_action_space(self): 733 return Discrete(len(self._PUZZLE_ACTION_MAP)) 734 735 def parameters_to_space(self, puzzle_action): 736 if puzzle_action not in self._PUZZLE_ACTION_KEYS: 737 return None 738 return self._PUZZLE_ACTION_KEYS.index(puzzle_action) 739 740 def space_to_parameters(self, space_action): 741 if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP): 742 return None 743 puzzle_action = self._PUZZLE_ACTION_KEYS[space_action] 744 return {"puzzle_action": puzzle_action} 745 746 def _execute(self, puzzle_action): 747 action = self._PUZZLE_ACTION_MAP[puzzle_action] 748 current_frame = self._emulator.get_current_frame() 749 frames, done = self._emulator.step(action) 750 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 751 return [self._state_tracker.report()], action_success 752 753 @staticmethod 754 def get_action_name(puzzle_action: str) -> str: 755 return f"Puzzle {puzzle_action}"
Allows navigation and option selection within Deja Vu puzzle/deduction states.
Is Valid When:
- In Puzzle State
Action Success Interpretation:
- -1: Frame did not change.
- 0: Frame changed.
The state parser that parses the minimal state information required for the action to function.
The state tracker that tracks the minimal state information required for the action to function.
725 def is_valid(self, **kwargs): 726 puzzle_action = kwargs.get("puzzle_action", None) 727 if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS: 728 return False 729 state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state")) 730 return state == AgentState.IN_PUZZLE
Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.
Arguments:
- **kwargs: Additional arguments required for the specific high level action.
Returns:
bool: Whether the action is valid in the current state.
Returns the Gym defined Space that characterizes the high level action's parameter space.
You can use this API to get a Space for sampling high level actions of this type.
Returns:
Space: The Gym space that characterizes the high level action's parameter space.
735 def parameters_to_space(self, puzzle_action): 736 if puzzle_action not in self._PUZZLE_ACTION_KEYS: 737 return None 738 return self._PUZZLE_ACTION_KEYS.index(puzzle_action)
Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.
Parameters
- kwargs: The high level action's parameters.
Returns
The action in the high level action's parameter space.
740 def space_to_parameters(self, space_action): 741 if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP): 742 return None 743 puzzle_action = self._PUZZLE_ACTION_KEYS[space_action] 744 return {"puzzle_action": puzzle_action}
Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.
Parameters
- space_action: The action in the high level action's parameter space.
Returns
The high level action's parameters corresponding to the space action.
753 @staticmethod 754 def get_action_name(puzzle_action: str) -> str: 755 return f"Puzzle {puzzle_action}"
Returns a human readable name for the high level action with the given parameters.
Parameters
- kwargs: The high level action's parameters.
Returns
A human readable name for the high level action.