gameboy_worlds.interface.runes_of_virtue.actions
1from gameboy_worlds.utils import log_error, log_warn 2from gameboy_worlds.interface.action import HighLevelAction, SingleHighLevelAction 3from gameboy_worlds.emulation.runes_of_virtue.parsers import ( 4 AgentState, 5 RunesOfVirtueStateParser, 6) 7from gameboy_worlds.emulation.runes_of_virtue.trackers import CoreRunesOfVirtueTracker 8from gameboy_worlds.emulation import LowLevelActions 9from abc import ABC, abstractmethod 10from typing import List, Tuple, Dict 11import numpy as np 12 13from gymnasium.spaces import Discrete 14 15 16HARD_MAX_STEPS = 5 17""" The hard maximum number of steps we'll let agents take in a sequence """ 18 19 20def frame_changed(past: np.ndarray, preset: np.ndarray, epsilon=0.01): 21 return np.abs(past - preset).mean() > epsilon 22 23 24class PassDialogueAction(SingleHighLevelAction): 25 """ 26 Skips dialogue by pressing the B button. 27 28 Is Valid When: 29 - In Dialogue State 30 31 Action Success Interpretation: 32 - -1: Frame did not change 33 - 0: Frame changed and no longer in dialogue state 34 - 1: Frame changed but still in dialogue state 35 """ 36 37 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 38 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 39 40 def is_valid(self, **kwargs): 41 """ 42 Just checks if the agent is in dialogue state. 43 """ 44 return ( 45 self._state_tracker.get_episode_metric( 46 ("runes_of_virtue_core", "agent_state") 47 ) 48 == AgentState.IN_DIALOGUE 49 ) 50 51 def _execute(self): 52 previous_frame = self._emulator.get_current_frame() 53 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 54 report = self._state_tracker.report() 55 if not frame_changed(previous_frame, frames[-1]): 56 action_success = -1 57 else: 58 action_success = ( 59 0 60 if self._emulator.state_parser.get_agent_state(frames[-1]) 61 != AgentState.IN_DIALOGUE 62 else 1 63 ) 64 return [report], action_success 65 66 @staticmethod 67 def get_action_name() -> str: 68 return "PassDialogue" 69 70 71class InteractAction(SingleHighLevelAction): 72 """ 73 Presses the A button to interact with an object in front of the agent. 74 75 Is Valid When: 76 - In Free Roam State 77 78 Action Success Interpretation: 79 - -1: Frame did not change or agent still in free roam state 80 - 1: Agent not in free roam state 81 """ 82 83 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 84 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 85 86 def is_valid(self, **kwargs): 87 """ 88 Just checks if the agent is in free roam state. 89 """ 90 return ( 91 self._state_tracker.get_episode_metric( 92 ("runes_of_virtue_core", "agent_state") 93 ) 94 == AgentState.FREE_ROAM 95 ) 96 97 def _execute(self): 98 previous_frame = self._emulator.get_current_frame() 99 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 100 report = self._state_tracker.report() 101 action_success = -1 102 for frame in frames: 103 if ( 104 self._emulator.state_parser.get_agent_state(frame) 105 != AgentState.FREE_ROAM 106 ): 107 action_success = 1 108 break 109 if frame_changed(previous_frame, frame): 110 action_success = 0 111 return [report], action_success 112 113 @staticmethod 114 def get_action_name() -> str: 115 return "Interact" 116 117 118class OpenMenuAction(SingleHighLevelAction): 119 """ 120 Opens the inventory/status menu by pressing the START button. 121 122 Is Valid When: 123 - In Free Roam State 124 125 Action Success Interpretation: 126 - -1: Frame did not change 127 - 0: Menu opened successfully (agent now in IN_MENU state) 128 - 1: Frame changed but agent not in menu state 129 """ 130 131 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 132 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 133 134 def is_valid(self, **kwargs): 135 """ 136 Checks if the agent is in free roam state. 137 """ 138 return ( 139 self._state_tracker.get_episode_metric( 140 ("runes_of_virtue_core", "agent_state") 141 ) 142 == AgentState.FREE_ROAM 143 ) 144 145 def _execute(self): 146 previous_frame = self._emulator.get_current_frame() 147 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 148 report = self._state_tracker.report() 149 if not frame_changed(previous_frame, frames[-1]): 150 action_success = -1 151 elif self._emulator.state_parser.is_in_menu(frames[-1]): 152 action_success = 0 153 else: 154 action_success = 1 155 return [report], action_success 156 157 @staticmethod 158 def get_action_name() -> str: 159 return "OpenMenu" 160 161 162class BaseMovementAction(HighLevelAction, ABC): 163 """ 164 Base class for movement actions in the Runes of Virtue environment. 165 Has utility methods for moving in directions. 166 167 Is Valid When: 168 - In Free Roam State 169 170 Action Success Interpretation: 171 - -1: Frame did not change, even on the first step 172 - 0: Finished all steps 173 - 1: Took some steps, but not all, and then frame stopped changing. This usually means we ran into an obstacle. 174 - 2: Took some steps, but agent state changed from free roam. This often means we entered a dialogue or menu. 175 176 Action Returns: 177 - `n_steps_taken` (`int`): Number of steps actually taken 178 179 Known Limitations: 180 - Uses single-frame change detection. RoV maps do not have the consistent tile-grid texture needed for Pokemon-style quadrant-uniformity analysis, so obstacle detection may be less precise than Pokemon's `BaseMovementAction`. 181 """ 182 183 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 184 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 185 186 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 187 """ 188 Move in a given direction for a number of steps. 189 190 :param direction: One of "up", "down", "left", "right" 191 :type direction: str 192 :param steps: Number of steps to move in that direction 193 :type steps: int 194 :return: A tuple containing: 195 196 - A list of state tracker reports after each low level action executed. 197 198 - An integer action success status. 199 :rtype: Tuple[List[Dict], int] 200 """ 201 action_dict = { 202 "right": LowLevelActions.PRESS_ARROW_RIGHT, 203 "down": LowLevelActions.PRESS_ARROW_DOWN, 204 "up": LowLevelActions.PRESS_ARROW_UP, 205 "left": LowLevelActions.PRESS_ARROW_LEFT, 206 } 207 if direction not in action_dict: 208 log_error(f"Got invalid direction to move {direction}", self._parameters) 209 action = action_dict[direction] 210 transition_state_dicts = [] 211 previous_frame = self._emulator.get_current_frame() 212 n_step = 0 213 n_successful_steps = 0 214 agent_state = AgentState.FREE_ROAM 215 while n_step < steps and agent_state == AgentState.FREE_ROAM: 216 frames, done = self._emulator.step(action) 217 transition_state_dicts.append(self._state_tracker.report()) 218 current_frame = self._emulator.get_current_frame() 219 if done: 220 break 221 if not frame_changed(previous_frame, current_frame): 222 # obstacle: frame did not change 223 break 224 n_successful_steps += 1 225 agent_state = self._emulator.state_parser.get_agent_state(current_frame) 226 if agent_state != AgentState.FREE_ROAM: 227 break 228 n_step += 1 229 previous_frame = current_frame 230 if agent_state != AgentState.FREE_ROAM: 231 action_success = 2 232 elif n_successful_steps == 0: 233 action_success = -1 234 elif n_successful_steps == steps: 235 action_success = 0 236 else: 237 action_success = 1 238 if transition_state_dicts: 239 transition_state_dicts[-1]["core"]["action_return"] = { 240 "n_steps_taken": n_successful_steps, 241 } 242 return transition_state_dicts, action_success 243 244 def is_valid(self, **kwargs): 245 """ 246 Just checks if the agent is in free roam state. 247 """ 248 return ( 249 self._state_tracker.get_episode_metric( 250 ("runes_of_virtue_core", "agent_state") 251 ) 252 == AgentState.FREE_ROAM 253 ) 254 255 256class MoveStepsAction(BaseMovementAction): 257 """ 258 Moves the agent in a specified cardinal direction for a specified number of steps. 259 260 Is Valid When: 261 - In Free Roam State 262 263 Action Success Interpretation: 264 - -1: Frame did not change, even on the first step 265 - 0: Finished all steps 266 - 1: Took some steps, but not all, and then frame stopped changing. This usually means we ran into an obstacle. 267 - 2: Took some steps, but agent state changed from free roam. This often means we entered a dialogue or menu. 268 269 Action Returns: 270 - `n_steps_taken` (`int`): Number of steps actually taken 271 """ 272 273 def get_action_space(self): 274 """ 275 Returns a Discrete space encoding (direction, steps). 276 4 directions × HARD_MAX_STEPS step counts. 277 """ 278 return Discrete(4 * HARD_MAX_STEPS) 279 280 def space_to_parameters(self, space_action): 281 if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS: 282 return None 283 if space_action < HARD_MAX_STEPS: 284 direction = "up" 285 steps = space_action 286 elif space_action < 2 * HARD_MAX_STEPS: 287 direction = "down" 288 steps = space_action - HARD_MAX_STEPS 289 elif space_action < 3 * HARD_MAX_STEPS: 290 direction = "left" 291 steps = space_action - 2 * HARD_MAX_STEPS 292 else: 293 direction = "right" 294 steps = space_action - 3 * HARD_MAX_STEPS 295 return {"direction": direction, "steps": steps + 1} 296 297 def parameters_to_space(self, direction: str, steps: int): 298 if steps <= 0 or steps > HARD_MAX_STEPS: 299 return None 300 if direction == "up": 301 return steps - 1 302 elif direction == "down": 303 return HARD_MAX_STEPS + steps - 1 304 elif direction == "left": 305 return 2 * HARD_MAX_STEPS + steps - 1 306 elif direction == "right": 307 return 3 * HARD_MAX_STEPS + steps - 1 308 else: 309 return None 310 311 def _execute(self, direction, steps): 312 transition_states, status = self.move(direction=direction, steps=steps) 313 return transition_states, status 314 315 def is_valid(self, **kwargs): 316 direction = kwargs.get("direction") 317 steps = kwargs.get("steps") 318 if direction is not None and direction not in ["up", "down", "left", "right"]: 319 return False 320 if steps is not None: 321 if not isinstance(steps, int): 322 return False 323 if steps <= 0 or steps > HARD_MAX_STEPS: 324 return False 325 return super().is_valid(**kwargs) 326 327 @staticmethod 328 def get_action_name(direction: str, steps: int) -> str: 329 return f"Move {direction} {steps}" 330 331 332class MenuAction(HighLevelAction): 333 """ 334 Allows simple navigation and option selection of menus. 335 336 Is Valid When: 337 - In Menu State 338 339 Action Success Interpretation: 340 - -1: Frame did not change. 341 - 0: Frame changed. 342 """ 343 344 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 345 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 346 347 _MENU_ACTION_MAP = { 348 "up": LowLevelActions.PRESS_ARROW_UP, 349 "down": LowLevelActions.PRESS_ARROW_DOWN, 350 "confirm": LowLevelActions.PRESS_BUTTON_A, 351 "left": LowLevelActions.PRESS_ARROW_LEFT, 352 "right": LowLevelActions.PRESS_ARROW_RIGHT, 353 "back": LowLevelActions.PRESS_BUTTON_B, 354 } 355 356 _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys()) 357 358 def is_valid(self, **kwargs): 359 """ 360 Checks if the menu action is valid in the current state. 361 362 Args: 363 menu_action (str, optional): The menu action to check. 364 Returns: 365 bool: True if the action is valid, False otherwise. 366 """ 367 menu_action = kwargs.get("menu_action", None) 368 if menu_action is not None: 369 if menu_action not in self._MENU_ACTION_KEYS: 370 return False 371 state = self._state_tracker.get_episode_metric( 372 ("runes_of_virtue_core", "agent_state") 373 ) 374 return state == AgentState.IN_MENU 375 376 def get_action_space(self): 377 """ 378 Returns a Discrete space representing menu actions. 379 """ 380 return Discrete(len(self._MENU_ACTION_MAP)) 381 382 def parameters_to_space(self, menu_action): 383 if menu_action not in self._MENU_ACTION_KEYS: 384 return None 385 return self._MENU_ACTION_KEYS.index(menu_action) 386 387 def space_to_parameters(self, space_action): 388 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 389 return None 390 return {"menu_action": self._MENU_ACTION_KEYS[space_action]} 391 392 def _execute(self, menu_action): 393 action = self._MENU_ACTION_MAP[menu_action] 394 current_frame = self._emulator.get_current_frame() 395 frames, done = self._emulator.step(action) 396 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 397 return [self._state_tracker.report()], action_success 398 399 @staticmethod 400 def get_action_name(menu_action: str) -> str: 401 return f"Menu {menu_action}"
The hard maximum number of steps we'll let agents take in a sequence
25class PassDialogueAction(SingleHighLevelAction): 26 """ 27 Skips dialogue by pressing the B button. 28 29 Is Valid When: 30 - In Dialogue State 31 32 Action Success Interpretation: 33 - -1: Frame did not change 34 - 0: Frame changed and no longer in dialogue state 35 - 1: Frame changed but still in dialogue state 36 """ 37 38 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 39 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 40 41 def is_valid(self, **kwargs): 42 """ 43 Just checks if the agent is in dialogue state. 44 """ 45 return ( 46 self._state_tracker.get_episode_metric( 47 ("runes_of_virtue_core", "agent_state") 48 ) 49 == AgentState.IN_DIALOGUE 50 ) 51 52 def _execute(self): 53 previous_frame = self._emulator.get_current_frame() 54 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_B) 55 report = self._state_tracker.report() 56 if not frame_changed(previous_frame, frames[-1]): 57 action_success = -1 58 else: 59 action_success = ( 60 0 61 if self._emulator.state_parser.get_agent_state(frames[-1]) 62 != AgentState.IN_DIALOGUE 63 else 1 64 ) 65 return [report], action_success 66 67 @staticmethod 68 def get_action_name() -> str: 69 return "PassDialogue"
Skips dialogue by pressing the B button.
Is Valid When:
- In Dialogue State
Action Success Interpretation:
- -1: Frame did not change
- 0: Frame changed and no longer in dialogue state
- 1: Frame changed but still in dialogue 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.
41 def is_valid(self, **kwargs): 42 """ 43 Just checks if the agent is in dialogue state. 44 """ 45 return ( 46 self._state_tracker.get_episode_metric( 47 ("runes_of_virtue_core", "agent_state") 48 ) 49 == AgentState.IN_DIALOGUE 50 )
Just checks if the agent is in dialogue state.
72class InteractAction(SingleHighLevelAction): 73 """ 74 Presses the A button to interact with an object in front of the agent. 75 76 Is Valid When: 77 - In Free Roam State 78 79 Action Success Interpretation: 80 - -1: Frame did not change or agent still in free roam state 81 - 1: Agent not in free roam state 82 """ 83 84 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 85 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 86 87 def is_valid(self, **kwargs): 88 """ 89 Just checks if the agent is in free roam state. 90 """ 91 return ( 92 self._state_tracker.get_episode_metric( 93 ("runes_of_virtue_core", "agent_state") 94 ) 95 == AgentState.FREE_ROAM 96 ) 97 98 def _execute(self): 99 previous_frame = self._emulator.get_current_frame() 100 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A) 101 report = self._state_tracker.report() 102 action_success = -1 103 for frame in frames: 104 if ( 105 self._emulator.state_parser.get_agent_state(frame) 106 != AgentState.FREE_ROAM 107 ): 108 action_success = 1 109 break 110 if frame_changed(previous_frame, frame): 111 action_success = 0 112 return [report], action_success 113 114 @staticmethod 115 def get_action_name() -> str: 116 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.
87 def is_valid(self, **kwargs): 88 """ 89 Just checks if the agent is in free roam state. 90 """ 91 return ( 92 self._state_tracker.get_episode_metric( 93 ("runes_of_virtue_core", "agent_state") 94 ) 95 == AgentState.FREE_ROAM 96 )
Just checks if the agent is in free roam state.
119class OpenMenuAction(SingleHighLevelAction): 120 """ 121 Opens the inventory/status menu by pressing the START button. 122 123 Is Valid When: 124 - In Free Roam State 125 126 Action Success Interpretation: 127 - -1: Frame did not change 128 - 0: Menu opened successfully (agent now in IN_MENU state) 129 - 1: Frame changed but agent not in menu state 130 """ 131 132 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 133 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 134 135 def is_valid(self, **kwargs): 136 """ 137 Checks if the agent is in free roam state. 138 """ 139 return ( 140 self._state_tracker.get_episode_metric( 141 ("runes_of_virtue_core", "agent_state") 142 ) 143 == AgentState.FREE_ROAM 144 ) 145 146 def _execute(self): 147 previous_frame = self._emulator.get_current_frame() 148 frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_START) 149 report = self._state_tracker.report() 150 if not frame_changed(previous_frame, frames[-1]): 151 action_success = -1 152 elif self._emulator.state_parser.is_in_menu(frames[-1]): 153 action_success = 0 154 else: 155 action_success = 1 156 return [report], action_success 157 158 @staticmethod 159 def get_action_name() -> str: 160 return "OpenMenu"
Opens the inventory/status menu by pressing the START button.
Is Valid When:
- In Free Roam State
Action Success Interpretation:
- -1: Frame did not change
- 0: Menu opened successfully (agent now in IN_MENU state)
- 1: Frame changed but agent not in menu 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.
135 def is_valid(self, **kwargs): 136 """ 137 Checks if the agent is in free roam state. 138 """ 139 return ( 140 self._state_tracker.get_episode_metric( 141 ("runes_of_virtue_core", "agent_state") 142 ) 143 == AgentState.FREE_ROAM 144 )
Checks if the agent is in free roam state.
163class BaseMovementAction(HighLevelAction, ABC): 164 """ 165 Base class for movement actions in the Runes of Virtue environment. 166 Has utility methods for moving in directions. 167 168 Is Valid When: 169 - In Free Roam State 170 171 Action Success Interpretation: 172 - -1: Frame did not change, even on the first step 173 - 0: Finished all steps 174 - 1: Took some steps, but not all, and then frame stopped changing. This usually means we ran into an obstacle. 175 - 2: Took some steps, but agent state changed from free roam. This often means we entered a dialogue or menu. 176 177 Action Returns: 178 - `n_steps_taken` (`int`): Number of steps actually taken 179 180 Known Limitations: 181 - Uses single-frame change detection. RoV maps do not have the consistent tile-grid texture needed for Pokemon-style quadrant-uniformity analysis, so obstacle detection may be less precise than Pokemon's `BaseMovementAction`. 182 """ 183 184 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 185 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 186 187 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 188 """ 189 Move in a given direction for a number of steps. 190 191 :param direction: One of "up", "down", "left", "right" 192 :type direction: str 193 :param steps: Number of steps to move in that direction 194 :type steps: int 195 :return: A tuple containing: 196 197 - A list of state tracker reports after each low level action executed. 198 199 - An integer action success status. 200 :rtype: Tuple[List[Dict], int] 201 """ 202 action_dict = { 203 "right": LowLevelActions.PRESS_ARROW_RIGHT, 204 "down": LowLevelActions.PRESS_ARROW_DOWN, 205 "up": LowLevelActions.PRESS_ARROW_UP, 206 "left": LowLevelActions.PRESS_ARROW_LEFT, 207 } 208 if direction not in action_dict: 209 log_error(f"Got invalid direction to move {direction}", self._parameters) 210 action = action_dict[direction] 211 transition_state_dicts = [] 212 previous_frame = self._emulator.get_current_frame() 213 n_step = 0 214 n_successful_steps = 0 215 agent_state = AgentState.FREE_ROAM 216 while n_step < steps and agent_state == AgentState.FREE_ROAM: 217 frames, done = self._emulator.step(action) 218 transition_state_dicts.append(self._state_tracker.report()) 219 current_frame = self._emulator.get_current_frame() 220 if done: 221 break 222 if not frame_changed(previous_frame, current_frame): 223 # obstacle: frame did not change 224 break 225 n_successful_steps += 1 226 agent_state = self._emulator.state_parser.get_agent_state(current_frame) 227 if agent_state != AgentState.FREE_ROAM: 228 break 229 n_step += 1 230 previous_frame = current_frame 231 if agent_state != AgentState.FREE_ROAM: 232 action_success = 2 233 elif n_successful_steps == 0: 234 action_success = -1 235 elif n_successful_steps == steps: 236 action_success = 0 237 else: 238 action_success = 1 239 if transition_state_dicts: 240 transition_state_dicts[-1]["core"]["action_return"] = { 241 "n_steps_taken": n_successful_steps, 242 } 243 return transition_state_dicts, action_success 244 245 def is_valid(self, **kwargs): 246 """ 247 Just checks if the agent is in free roam state. 248 """ 249 return ( 250 self._state_tracker.get_episode_metric( 251 ("runes_of_virtue_core", "agent_state") 252 ) 253 == AgentState.FREE_ROAM 254 )
Base class for movement actions in the Runes of Virtue environment. Has utility methods for moving in directions.
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. 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 dialogue or menu.
Action Returns:
n_steps_taken(int): Number of steps actually taken
Known Limitations:
- Uses single-frame change detection. RoV maps do not have the consistent tile-grid texture needed for Pokemon-style quadrant-uniformity analysis, so obstacle detection may be less precise than Pokemon's
BaseMovementAction.
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.
187 def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]: 188 """ 189 Move in a given direction for a number of steps. 190 191 :param direction: One of "up", "down", "left", "right" 192 :type direction: str 193 :param steps: Number of steps to move in that direction 194 :type steps: int 195 :return: A tuple containing: 196 197 - A list of state tracker reports after each low level action executed. 198 199 - An integer action success status. 200 :rtype: Tuple[List[Dict], int] 201 """ 202 action_dict = { 203 "right": LowLevelActions.PRESS_ARROW_RIGHT, 204 "down": LowLevelActions.PRESS_ARROW_DOWN, 205 "up": LowLevelActions.PRESS_ARROW_UP, 206 "left": LowLevelActions.PRESS_ARROW_LEFT, 207 } 208 if direction not in action_dict: 209 log_error(f"Got invalid direction to move {direction}", self._parameters) 210 action = action_dict[direction] 211 transition_state_dicts = [] 212 previous_frame = self._emulator.get_current_frame() 213 n_step = 0 214 n_successful_steps = 0 215 agent_state = AgentState.FREE_ROAM 216 while n_step < steps and agent_state == AgentState.FREE_ROAM: 217 frames, done = self._emulator.step(action) 218 transition_state_dicts.append(self._state_tracker.report()) 219 current_frame = self._emulator.get_current_frame() 220 if done: 221 break 222 if not frame_changed(previous_frame, current_frame): 223 # obstacle: frame did not change 224 break 225 n_successful_steps += 1 226 agent_state = self._emulator.state_parser.get_agent_state(current_frame) 227 if agent_state != AgentState.FREE_ROAM: 228 break 229 n_step += 1 230 previous_frame = current_frame 231 if agent_state != AgentState.FREE_ROAM: 232 action_success = 2 233 elif n_successful_steps == 0: 234 action_success = -1 235 elif n_successful_steps == steps: 236 action_success = 0 237 else: 238 action_success = 1 239 if transition_state_dicts: 240 transition_state_dicts[-1]["core"]["action_return"] = { 241 "n_steps_taken": n_successful_steps, 242 } 243 return transition_state_dicts, action_success
Move in a given direction for a number of steps.
Parameters
- direction: One of "up", "down", "left", "right"
- steps: Number of steps to move in that direction
Returns
A tuple containing:
- A list of state tracker reports after each low level action executed. - An integer action success status.
245 def is_valid(self, **kwargs): 246 """ 247 Just checks if the agent is in free roam state. 248 """ 249 return ( 250 self._state_tracker.get_episode_metric( 251 ("runes_of_virtue_core", "agent_state") 252 ) 253 == AgentState.FREE_ROAM 254 )
Just checks if the agent is in free roam state.
257class MoveStepsAction(BaseMovementAction): 258 """ 259 Moves the agent in a specified cardinal direction for a specified number of steps. 260 261 Is Valid When: 262 - In Free Roam State 263 264 Action Success Interpretation: 265 - -1: Frame did not change, even on the first step 266 - 0: Finished all steps 267 - 1: Took some steps, but not all, and then frame stopped changing. This usually means we ran into an obstacle. 268 - 2: Took some steps, but agent state changed from free roam. This often means we entered a dialogue or menu. 269 270 Action Returns: 271 - `n_steps_taken` (`int`): Number of steps actually taken 272 """ 273 274 def get_action_space(self): 275 """ 276 Returns a Discrete space encoding (direction, steps). 277 4 directions × HARD_MAX_STEPS step counts. 278 """ 279 return Discrete(4 * HARD_MAX_STEPS) 280 281 def space_to_parameters(self, space_action): 282 if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS: 283 return None 284 if space_action < HARD_MAX_STEPS: 285 direction = "up" 286 steps = space_action 287 elif space_action < 2 * HARD_MAX_STEPS: 288 direction = "down" 289 steps = space_action - HARD_MAX_STEPS 290 elif space_action < 3 * HARD_MAX_STEPS: 291 direction = "left" 292 steps = space_action - 2 * HARD_MAX_STEPS 293 else: 294 direction = "right" 295 steps = space_action - 3 * HARD_MAX_STEPS 296 return {"direction": direction, "steps": steps + 1} 297 298 def parameters_to_space(self, direction: str, steps: int): 299 if steps <= 0 or steps > HARD_MAX_STEPS: 300 return None 301 if direction == "up": 302 return steps - 1 303 elif direction == "down": 304 return HARD_MAX_STEPS + steps - 1 305 elif direction == "left": 306 return 2 * HARD_MAX_STEPS + steps - 1 307 elif direction == "right": 308 return 3 * HARD_MAX_STEPS + steps - 1 309 else: 310 return None 311 312 def _execute(self, direction, steps): 313 transition_states, status = self.move(direction=direction, steps=steps) 314 return transition_states, status 315 316 def is_valid(self, **kwargs): 317 direction = kwargs.get("direction") 318 steps = kwargs.get("steps") 319 if direction is not None and direction not in ["up", "down", "left", "right"]: 320 return False 321 if steps is not None: 322 if not isinstance(steps, int): 323 return False 324 if steps <= 0 or steps > HARD_MAX_STEPS: 325 return False 326 return super().is_valid(**kwargs) 327 328 @staticmethod 329 def get_action_name(direction: str, steps: int) -> str: 330 return f"Move {direction} {steps}"
Moves the agent in a specified cardinal direction for a specified number of steps.
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. 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 dialogue or menu.
Action Returns:
n_steps_taken(int): Number of steps actually taken
274 def get_action_space(self): 275 """ 276 Returns a Discrete space encoding (direction, steps). 277 4 directions × HARD_MAX_STEPS step counts. 278 """ 279 return Discrete(4 * HARD_MAX_STEPS)
Returns a Discrete space encoding (direction, steps). 4 directions × HARD_MAX_STEPS step counts.
281 def space_to_parameters(self, space_action): 282 if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS: 283 return None 284 if space_action < HARD_MAX_STEPS: 285 direction = "up" 286 steps = space_action 287 elif space_action < 2 * HARD_MAX_STEPS: 288 direction = "down" 289 steps = space_action - HARD_MAX_STEPS 290 elif space_action < 3 * HARD_MAX_STEPS: 291 direction = "left" 292 steps = space_action - 2 * HARD_MAX_STEPS 293 else: 294 direction = "right" 295 steps = space_action - 3 * HARD_MAX_STEPS 296 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.
298 def parameters_to_space(self, direction: str, steps: int): 299 if steps <= 0 or steps > HARD_MAX_STEPS: 300 return None 301 if direction == "up": 302 return steps - 1 303 elif direction == "down": 304 return HARD_MAX_STEPS + steps - 1 305 elif direction == "left": 306 return 2 * HARD_MAX_STEPS + steps - 1 307 elif direction == "right": 308 return 3 * HARD_MAX_STEPS + steps - 1 309 else: 310 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.
316 def is_valid(self, **kwargs): 317 direction = kwargs.get("direction") 318 steps = kwargs.get("steps") 319 if direction is not None and direction not in ["up", "down", "left", "right"]: 320 return False 321 if steps is not None: 322 if not isinstance(steps, int): 323 return False 324 if steps <= 0 or steps > HARD_MAX_STEPS: 325 return False 326 return super().is_valid(**kwargs)
Just checks if the agent is in free roam state.
328 @staticmethod 329 def get_action_name(direction: str, steps: int) -> str: 330 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.
333class MenuAction(HighLevelAction): 334 """ 335 Allows simple navigation and option selection of menus. 336 337 Is Valid When: 338 - In Menu State 339 340 Action Success Interpretation: 341 - -1: Frame did not change. 342 - 0: Frame changed. 343 """ 344 345 REQUIRED_STATE_PARSER = RunesOfVirtueStateParser 346 REQUIRED_STATE_TRACKER = CoreRunesOfVirtueTracker 347 348 _MENU_ACTION_MAP = { 349 "up": LowLevelActions.PRESS_ARROW_UP, 350 "down": LowLevelActions.PRESS_ARROW_DOWN, 351 "confirm": LowLevelActions.PRESS_BUTTON_A, 352 "left": LowLevelActions.PRESS_ARROW_LEFT, 353 "right": LowLevelActions.PRESS_ARROW_RIGHT, 354 "back": LowLevelActions.PRESS_BUTTON_B, 355 } 356 357 _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys()) 358 359 def is_valid(self, **kwargs): 360 """ 361 Checks if the menu action is valid in the current state. 362 363 Args: 364 menu_action (str, optional): The menu action to check. 365 Returns: 366 bool: True if the action is valid, False otherwise. 367 """ 368 menu_action = kwargs.get("menu_action", None) 369 if menu_action is not None: 370 if menu_action not in self._MENU_ACTION_KEYS: 371 return False 372 state = self._state_tracker.get_episode_metric( 373 ("runes_of_virtue_core", "agent_state") 374 ) 375 return state == AgentState.IN_MENU 376 377 def get_action_space(self): 378 """ 379 Returns a Discrete space representing menu actions. 380 """ 381 return Discrete(len(self._MENU_ACTION_MAP)) 382 383 def parameters_to_space(self, menu_action): 384 if menu_action not in self._MENU_ACTION_KEYS: 385 return None 386 return self._MENU_ACTION_KEYS.index(menu_action) 387 388 def space_to_parameters(self, space_action): 389 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 390 return None 391 return {"menu_action": self._MENU_ACTION_KEYS[space_action]} 392 393 def _execute(self, menu_action): 394 action = self._MENU_ACTION_MAP[menu_action] 395 current_frame = self._emulator.get_current_frame() 396 frames, done = self._emulator.step(action) 397 action_success = 0 if frame_changed(current_frame, frames[-1]) else -1 398 return [self._state_tracker.report()], action_success 399 400 @staticmethod 401 def get_action_name(menu_action: str) -> str: 402 return f"Menu {menu_action}"
Allows simple navigation and option selection of 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.
359 def is_valid(self, **kwargs): 360 """ 361 Checks if the menu action is valid in the current state. 362 363 Args: 364 menu_action (str, optional): The menu action to check. 365 Returns: 366 bool: True if the action is valid, False otherwise. 367 """ 368 menu_action = kwargs.get("menu_action", None) 369 if menu_action is not None: 370 if menu_action not in self._MENU_ACTION_KEYS: 371 return False 372 state = self._state_tracker.get_episode_metric( 373 ("runes_of_virtue_core", "agent_state") 374 ) 375 return state == AgentState.IN_MENU
Checks if the menu action is valid in the current state.
Arguments:
- menu_action (str, optional): The menu action to check.
Returns:
bool: True if the action is valid, False otherwise.
377 def get_action_space(self): 378 """ 379 Returns a Discrete space representing menu actions. 380 """ 381 return Discrete(len(self._MENU_ACTION_MAP))
Returns a Discrete space representing menu actions.
383 def parameters_to_space(self, menu_action): 384 if menu_action not in self._MENU_ACTION_KEYS: 385 return None 386 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.
388 def space_to_parameters(self, space_action): 389 if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP): 390 return None 391 return {"menu_action": self._MENU_ACTION_KEYS[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.
400 @staticmethod 401 def get_action_name(menu_action: str) -> str: 402 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.