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}"
HARD_MAX_STEPS = 5

The hard maximum number of steps we'll let agents take in a sequence

def frame_changed(past: numpy.ndarray, preset: numpy.ndarray, epsilon=0.01):
21def frame_changed(past: np.ndarray, preset: np.ndarray, epsilon=0.01):
22    return np.abs(past - preset).mean() > epsilon
class PassDialogueAction(gameboy_worlds.interface.action.SingleHighLevelAction):
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.

def is_valid(self, **kwargs):
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.

@staticmethod
def get_action_name() -> str:
67    @staticmethod
68    def get_action_name() -> str:
69        return "PassDialogue"

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.

class InteractAction(gameboy_worlds.interface.action.SingleHighLevelAction):
 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.

def is_valid(self, **kwargs):
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.

@staticmethod
def get_action_name() -> str:
114    @staticmethod
115    def get_action_name() -> str:
116        return "Interact"

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.

class OpenMenuAction(gameboy_worlds.interface.action.SingleHighLevelAction):
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.

def is_valid(self, **kwargs):
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.

@staticmethod
def get_action_name() -> str:
158    @staticmethod
159    def get_action_name() -> str:
160        return "OpenMenu"

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.

class BaseMovementAction(gameboy_worlds.interface.action.HighLevelAction, abc.ABC):
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.

def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]:
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.
def is_valid(self, **kwargs):
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.

class MoveStepsAction(BaseMovementAction):
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
def get_action_space(self):
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.

def space_to_parameters(self, space_action):
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.

def parameters_to_space(self, direction: str, steps: int):
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.

def is_valid(self, **kwargs):
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.

@staticmethod
def get_action_name(direction: str, steps: int) -> str:
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.