gameboy_worlds.interface.pokemon.actions

  1from gameboy_worlds.utils import log_error, log_warn
  2from gameboy_worlds.interface.action import HighLevelAction, SingleHighLevelAction
  3from gameboy_worlds.emulation.pokemon.parsers import (
  4    AgentState,
  5    PokemonStateParser,
  6    BasePokemonRedStateParser,
  7)
  8from gameboy_worlds.emulation.pokemon.trackers import CorePokemonTracker
  9from gameboy_worlds.emulation import LowLevelActions
 10from abc import ABC, abstractmethod
 11from typing import List, Tuple, Dict
 12from gameboy_worlds.utils import show_frames
 13import numpy as np
 14
 15from gymnasium.spaces import Box, Discrete, Text, OneOf
 16import matplotlib.pyplot as plt
 17from PIL import Image
 18
 19HARD_MAX_STEPS = 5
 20""" The hard maximum number of steps we'll let agents take in a sequence """
 21
 22
 23def frame_changed(past: np.ndarray, preset: np.ndarray, epsilon=0.01):
 24    return np.abs(past - preset).mean() > epsilon
 25
 26
 27def _plot(past: np.ndarray, current: np.ndarray):
 28    # plot both side by side for debug
 29    fig, axs = plt.subplots(1, 2)
 30    axs[0].imshow(past)
 31    axs[1].imshow(current)
 32    plt.show()
 33
 34
 35class PassDialogueAction(SingleHighLevelAction):
 36    """
 37    Skips dialogue by pressing the B button.
 38
 39    Is Valid When:
 40    - In Dialogue State
 41
 42    Action Success Interpretation:
 43    - -1: Frame did not change
 44    - 0: Frame changed and no longer in dialogue state
 45    - 1: Frame changed but still in dialogue state
 46    """
 47
 48    REQUIRED_STATE_PARSER = PokemonStateParser
 49    REQUIRED_STATE_TRACKER = CorePokemonTracker
 50
 51    def is_valid(self, **kwargs):
 52        """
 53        Just checks if the agent is in dialogue state.
 54        """
 55        return (
 56            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
 57            == AgentState.IN_DIALOGUE
 58        )
 59
 60    def _execute(self):
 61        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
 62        report = self._state_tracker.report()
 63        if not report["core"]["frame_changed"]:
 64            action_success = -1
 65        else:
 66            action_success = (
 67                0
 68                if self._emulator.state_parser.get_agent_state(frames[-1])
 69                != AgentState.IN_DIALOGUE
 70                else 1
 71            )
 72        return [report], action_success
 73
 74    @staticmethod
 75    def get_action_name() -> str:
 76        return "PassDialogue"
 77
 78
 79class InteractAction(SingleHighLevelAction):
 80    """
 81    Presses the A button to interact with an object in front of the agent.
 82
 83    Is Valid When:
 84    - In Free Roam State
 85
 86    Action Success Interpretation:
 87    - -1: Frame did not change or agent still in free roam state
 88    - 1: Agent not in free roam state
 89    """
 90
 91    REQUIRED_STATE_PARSER = PokemonStateParser
 92    REQUIRED_STATE_TRACKER = CorePokemonTracker
 93
 94    def is_valid(self, **kwargs):
 95        """
 96        Just checks if the agent is in free roam state.
 97        """
 98        return (
 99            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
100            == AgentState.FREE_ROAM
101        )
102
103    def _execute(self):
104        current_frame = self._emulator.get_current_frame()
105        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
106        action_success = 0
107        # Check if the frames have changed. Be strict and require all to not permit jittering screens.
108        prev_frames = []
109        for frame in frames:
110            if (
111                self._emulator.state_parser.get_agent_state(frame)
112                != AgentState.FREE_ROAM
113            ):  # something happened lol
114                action_success = 1
115                break
116            for past_frame in prev_frames:
117                if not frame_changed(past_frame, frame):
118                    action_success = -1
119                    break
120            if action_success != 0:
121                break
122            prev_frames.append(frame)
123        if action_success == 0:
124            action_success = (
125                -1
126            )  # I guess? For some reason the previous thing doesn't catch same frames
127        return [
128            self._state_tracker.report()
129        ], action_success  # 0 means something maybe happened. 1 means def happened.
130
131    @staticmethod
132    def get_action_name() -> str:
133        return "Interact"
134
135
136class BaseMovementAction(HighLevelAction, ABC):
137    """
138    Base class for movement actions in the Pokemon environment.
139    Has utility methods for moving in directions.
140
141    Is Valid When:
142    - In Free Roam State
143
144    Action Success Interpretation:
145    - -1: Frame did not change, even on the first step
146    - 0: Finished all steps
147    - 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.
148    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
149
150    Action Returns:
151    - `n_steps_taken` (`int`): Number of steps actually taken
152    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
153
154    Known Limitations:
155    - Struggles to handle oscillating frames when the player is next to an obstacle. So if you are surrounded by water or bouncing flowers on all quadrants (e.g. the pier in Cinnabar Island), the system will think that the agent has moved forward, even though it has not.
156    """
157
158    REQUIRED_STATE_TRACKER = CorePokemonTracker
159    REQUIRED_STATE_PARSER = PokemonStateParser
160
161    def _is_uniform_quadrant(self, frame, quadrant_name) -> bool:
162        mapper = {
163            "screen_quadrant_1": "tr",
164            "screen_quadrant_2": "tl",
165            "screen_quadrant_3": "bl",
166            "screen_quadrant_4": "br",
167        }
168        quadrant_cells = self._emulator.state_parser.capture_grid_cells(
169            frame, quadrant=mapper[quadrant_name]
170        )
171        keys = list(quadrant_cells.keys())
172        x_min = 1e9
173        y_min = 1e9
174        x_max = -1e9
175        y_max = -1e9
176        for key in keys:
177            x_coord, y_coord = key
178            if x_coord * y_coord == 0:  # pop it to avoid checking player cell
179                quadrant_cells.pop(key)
180            else:
181                x_min = min(x_min, x_coord)
182                y_min = min(y_min, y_coord)
183                x_max = max(x_max, x_coord)
184                y_max = max(y_max, y_coord)
185        x_min = int(x_min)
186        y_min = int(y_min)
187        x_max = int(x_max)
188        y_max = int(y_max)
189        keys = list(quadrant_cells.keys())
190        vertical_uniform = True
191        horizontal_uniform = True
192        # check horizontal lines
193        for y in range(y_min + 1, y_max):  # avoid edges
194            first_cell = None
195            for x in range(x_min + 1, x_max):
196                cell = quadrant_cells[(x, y)]
197                if first_cell is None:
198                    first_cell = cell
199                else:
200                    if first_cell.shape == cell.shape:
201                        if frame_changed(first_cell, cell):
202                            horizontal_uniform = False
203                            break
204            if not horizontal_uniform:
205                break
206        if horizontal_uniform:
207            return True
208        # check vertical lines
209        for x in range(x_min + 1, x_max):
210            first_cell = None
211            for y in range(y_min + 1, y_max):
212                cell = quadrant_cells[(x, y)]
213                if first_cell is None:
214                    first_cell = cell
215                else:
216                    if first_cell.shape == cell.shape:
217                        if frame_changed(first_cell, cell):
218                            vertical_uniform = False
219                            break
220            if not vertical_uniform:
221                break
222        if vertical_uniform:
223            return True
224        return False
225
226    def judge_movement(
227        self, previous_frame: np.ndarray, current_frame: np.ndarray
228    ) -> Tuple[bool, bool]:
229        """
230        Judges whether movement has occurred between two frames.
231
232        Args:
233            previous_frame (np.ndarray): The previous frame.
234            current_frame (np.ndarray): The current frame.
235        Returns:
236            Tuple[bool, bool]: A tuple containing:
237            - bool: True if movement has occurred, False otherwise.
238            - bool: True if the player has not moved, but has rotated.
239        """
240        # if the full screen hasn't changed at all, player has neither moved nor rotated
241        if not frame_changed(previous_frame, current_frame):
242            return False, False
243        # split the screen into quadrants and check which quadrants have changed. If any of them stayed the same, the player has not moved, but may have rotated.
244        # One caveat is if the quadrant is uniform tiles (i.e. all have the same grid texture in them). In this case, we can't say for sure that the quadrant hasn't changed, since it may just be that the uniform texture is the same. So we check for that too.
245        flag = False
246        for quadrant in [
247            "screen_quadrant_1",
248            "screen_quadrant_2",
249            "screen_quadrant_3",
250            "screen_quadrant_4",
251        ]:
252            prev_quad = self._emulator.state_parser.capture_named_region(
253                previous_frame, quadrant
254            )
255            curr_quad = self._emulator.state_parser.capture_named_region(
256                current_frame, quadrant
257            )
258            prev_uniform = (
259                prev_quad.max() == prev_quad.min()  # screen is all black or all white
260                or self._is_uniform_quadrant(
261                    previous_frame, quadrant
262                )  # screen quadrant is uniform tiles
263            )
264            curr_uniform = (
265                curr_quad.max() == curr_quad.min()
266                or self._is_uniform_quadrant(current_frame, quadrant)
267            )
268            if (
269                not frame_changed(prev_quad, curr_quad)
270                and not prev_uniform
271                and not curr_uniform
272            ):  # then screen isn't just black, but also hasn't changed.
273                flag = True
274                break
275        if flag:  # then some frame stayed the same, so no movement, but maybe rotation.
276            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
277                previous_frame
278            )[(0, 0)]
279            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
280                current_frame
281            )[(0, 0)]
282            if frame_changed(prev_player_cell, curr_player_cell):
283                return False, True
284            else:
285                return False, False
286        else:
287            return True, None
288
289    def move(self, direction: str, steps: int) -> Tuple[np.ndarray, int]:
290        """
291        Move in a given direction for a number of steps.
292
293        :param direction: One of "up", "down", "left", "right"
294        :type direction: str
295        :param steps: Number of steps to move in that direction
296        :type steps: int
297        :return:  A tuple containing:
298
299                - A list of state tracker reports after each low level action executed. Length is equal to the number of low level actions executed.
300
301                - An integer action success status
302        :rtype: Tuple[ndarray[_AnyShape, dtype[Any]], int]
303        """
304        action_dict = {
305            "right": LowLevelActions.PRESS_ARROW_RIGHT,
306            "down": LowLevelActions.PRESS_ARROW_DOWN,
307            "up": LowLevelActions.PRESS_ARROW_UP,
308            "left": LowLevelActions.PRESS_ARROW_LEFT,
309        }
310        if direction not in action_dict.keys():
311            log_error(f"Got invalid direction to move {direction}", self._parameters)
312        action = action_dict[direction]
313        # keep trying the action.
314        # exit status 0 -> finished steps
315        # 1 -> took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter)
316        # 2 -> took some steps, but agent state changed from free roam
317        # -1 -> frame didn't change, even on the first step
318        action_success = -1
319        transition_state_dicts = []
320        transition_frames = []
321        previous_frame = (
322            self._emulator.get_current_frame()
323        )  # Do NOT get the state tracker frame, as it may have a grid on it.
324        n_step = 0
325        n_successful_steps = 0
326        has_rotated = None
327        agent_state = AgentState.FREE_ROAM
328        while n_step < steps and agent_state == AgentState.FREE_ROAM:
329            frames, done = self._emulator.step(action)
330            transition_state_dicts.append(self._state_tracker.report())
331            transition_frames.extend(frames)
332            current_frame = (
333                self._emulator.get_current_frame()
334            )  # Do NOT use the emulator frame, as it may have a grid on it.
335            if done:
336                break
337            # check if frames changed. If not, break out.
338            # We check all frames in sequence to try and catch oscillations. But nothing will catch 1 step into wall in areas like this
339            player_moved, player_rotated = self.judge_movement(
340                previous_frame, current_frame
341            )
342            if player_rotated == True:
343                has_rotated = True
344            if not player_moved and not player_rotated:
345                break
346            if player_moved:
347                n_successful_steps += 1  # don't count rotation as a step
348            agent_state = self._emulator.state_parser.get_agent_state(
349                self._emulator.get_current_frame()
350            )
351            if agent_state != AgentState.FREE_ROAM:
352                break
353            n_step += 1
354            previous_frame = current_frame
355        if agent_state != AgentState.FREE_ROAM:
356            action_success = 2
357        else:
358            if n_step <= 0:
359                action_success = -1
360            elif n_step == steps:
361                action_success = 0
362            else:
363                action_success = 1
364        transition_state_dicts[-1]["core"]["action_return"] = {
365            "n_steps_taken": n_successful_steps,
366            "rotated": has_rotated,
367        }
368        return transition_state_dicts, action_success
369
370    def is_valid(self, **kwargs):
371        """
372        Just checks if the agent is in free roam state.
373        """
374        return (
375            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
376            == AgentState.FREE_ROAM
377        )
378
379
380class MoveStepsAction(BaseMovementAction):
381    """
382    Moves the agent in a specified cardinal direction for a specified number of steps.
383
384    Is Valid When:
385    - In Free Roam State
386    Action Success Interpretation:
387    - -1: Frame did not change, even on the first step
388    - 0: Finished all steps
389    - 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.
390    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
391
392    Action Returns:
393    - `n_steps_taken` (`int`): Number of steps actually taken
394    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
395    """
396
397    def get_action_space(self):
398        """
399        Returns a Box space representing movement in 2D.
400        The first dimension represents vertical movement (positive is up, negative is down).
401        The second dimension represents horizontal movement (positive is right, negative is left).
402
403        Returns:
404            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
405
406        """
407        return Discrete(4 * HARD_MAX_STEPS)
408
409    def space_to_parameters(self, space_action):
410        direction = None
411        steps = None
412        if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS:
413            # log_warn(f"Invalid space action {space_action}", self._parameters)
414            return None
415        if space_action < HARD_MAX_STEPS:
416            direction = "up"
417            steps = space_action
418        elif space_action < 2 * HARD_MAX_STEPS:
419            direction = "down"
420            steps = space_action - HARD_MAX_STEPS
421        elif space_action < 3 * HARD_MAX_STEPS:
422            direction = "left"
423            steps = space_action - 2 * HARD_MAX_STEPS
424        else:
425            direction = "right"
426            steps = space_action - 3 * HARD_MAX_STEPS
427        return {"direction": direction, "steps": steps + 1}
428
429    def parameters_to_space(self, direction: str, steps: int):
430        if steps <= 0 or steps > HARD_MAX_STEPS:
431            return None
432        if direction == "up":
433            return steps - 1
434        elif direction == "down":
435            return HARD_MAX_STEPS + steps - 1
436        elif direction == "left":
437            return 2 * HARD_MAX_STEPS + steps - 1
438        elif direction == "right":
439            return 3 * HARD_MAX_STEPS + steps - 1
440        else:
441            # log_warn(f"Unrecognized direction {direction}", self._parameters)
442            return None
443
444    def _execute(self, direction, steps):
445        transition_states, status = self.move(direction=direction, steps=steps)
446        return transition_states, status
447
448    def is_valid(self, **kwargs):
449        direction = kwargs.get("direction")
450        step = kwargs.get("step")
451        if direction is not None and direction not in ["up", "down", "left", "right"]:
452            return False
453        if step is not None:
454            if not isinstance(step, str):
455                return False
456            if step <= 0:
457                return False
458        return super().is_valid(**kwargs)
459
460    @staticmethod
461    def get_action_name(direction: str, steps: int) -> str:
462        return f"Move {direction} {steps}"
463
464
465class MoveGridAction(BaseMovementAction):
466    """
467    Moves the agent on both axes. Will always try to move right first and then up.
468
469    Is Valid When:
470    - In Free Roam State
471    Action Success Interpretation:
472    - -1: Frame did not change, even on the first step
473    - 0: Finished all steps
474    - 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.
475    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
476
477    Action Returns:
478    - `n_steps_taken` (`int`): Number of steps actually taken
479    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
480    """
481
482    def get_action_space(self):
483        """
484        Returns a Box space representing movement in 2D.
485        The first dimension represents vertical movement (positive is up, negative is down).
486        The second dimension represents horizontal movement (positive is right, negative is left).
487
488        Returns:
489            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
490
491        """
492        return Box(
493            low=-HARD_MAX_STEPS // 2,
494            high=HARD_MAX_STEPS // 2,
495            shape=(2,),
496            dtype=np.int8,
497        )
498
499    def space_to_parameters(self, space_action):
500        right_action = space_action[0]
501        up_action = space_action[1]
502        return {"x_steps": right_action, "y_steps": up_action}
503
504    def parameters_to_space(self, x_steps, y_steps):
505        move_vec = np.zeros(2)  # x, y
506        move_vec[0] = x_steps
507        move_vec[1] = y_steps
508        return move_vec
509
510    def _execute(self, x_steps, y_steps):
511        x_direction = "right" if x_steps >= 0 else "left"
512        y_direction = "up" if y_steps >= 0 else "down"
513        if x_steps != 0:
514            transition_states, status = self.move(
515                direction=x_direction, steps=abs(x_steps)
516            )
517            if status != 0:
518                return transition_states, status
519        else:
520            transition_states = []
521        if y_steps != 0:
522            more_transition_states, status = self.move(
523                direction=y_direction, steps=abs(y_steps)
524            )
525            transition_states.extend(more_transition_states)
526        try:
527            status is not None
528        except NameError:
529            log_warn(
530                f"Weird case where both x_steps and y_steps are 0 in MoveGridAction or something. {x_steps}, {y_steps}",
531                self._parameters,
532            )
533            transition_states = [self._state_tracker.report()]
534            status = -1
535        return transition_states, status
536
537    def is_valid(self, x_steps: int = None, y_steps: int = None):
538        if x_steps is not None and y_steps is not None:
539            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
540                return False
541            if x_steps == 0 and y_steps == 0:
542                return False
543        return super().is_valid()
544
545    @staticmethod
546    def get_action_name(x_steps: int, y_steps: int) -> str:
547        return f"MoveGrid ({x_steps}, {y_steps})"
548
549
550class MenuAction(HighLevelAction):
551    """
552    Allows simple navigation and option selection of menus.
553
554    Is Valid When:
555    - In Menu State
556
557    Action Success Interpretation:
558    - -1: Frame did not change.
559    - 0: Frame changed.
560    """
561
562    REQUIRED_STATE_PARSER = PokemonStateParser
563    REQUIRED_STATE_TRACKER = CorePokemonTracker
564
565    _MENU_ACTION_MAP = {
566        "up": LowLevelActions.PRESS_ARROW_UP,
567        "down": LowLevelActions.PRESS_ARROW_DOWN,
568        "confirm": LowLevelActions.PRESS_BUTTON_A,
569        "left": LowLevelActions.PRESS_ARROW_LEFT,
570        "right": LowLevelActions.PRESS_ARROW_RIGHT,
571        "back": LowLevelActions.PRESS_BUTTON_B,
572        # "open": LowLevelActions.PRESS_BUTTON_START,  # In general, we won't be using this action, but prefer OpenMenuAction instead.
573    }
574
575    _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys())
576
577    def is_valid(self, **kwargs):
578        """
579        Checks if the menu action is valid in the current state.
580
581        Args:
582            menu_action (str, optional): The menu action to check.
583        Returns:
584            bool: True if the action is valid, False otherwise.
585        """
586        menu_action = kwargs.get("menu_action", None)
587        if menu_action is not None:
588            if menu_action not in self._MENU_ACTION_KEYS:
589                return False
590        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
591        return state in [
592            AgentState.IN_MENU,
593            AgentState.IN_BATTLE,
594        ]  # works in battle screens and menu state
595
596    def get_action_space(self):
597        """
598        Returns a Discrete space representing menu actions.
599        Returns:
600            Discrete: A Discrete space with size equal to the number of menu actions.
601        """
602        return Discrete(len(self._MENU_ACTION_MAP))
603
604    def parameters_to_space(self, menu_action):
605        if menu_action not in self._MENU_ACTION_KEYS:
606            return None
607        return self._MENU_ACTION_KEYS.index(menu_action)
608
609    def space_to_parameters(self, space_action):
610        menu_action = None
611        if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP):
612            return None
613        menu_action = self._MENU_ACTION_KEYS[space_action]
614        return {"menu_action": menu_action}
615
616    def _execute(self, menu_action):
617        action = self._MENU_ACTION_MAP[menu_action]
618        current_frame = self._emulator.get_current_frame()
619        frames, done = self._emulator.step(action)
620        action_success = 0 if frame_changed(current_frame, frames[-1]) else -1
621        return [self._state_tracker.report()], action_success
622
623    @staticmethod
624    def get_action_name(menu_action: str) -> str:
625        return f"Menu {menu_action}"
626
627
628class OpenMenuAction(HighLevelAction):
629    """
630    Opens the main menu from free roam and navigates to specified option.
631    Is Valid When:
632    - In Free Roam State
633
634    Action Success Interpretation:
635    - -1: Navigation Failure. Screen did not end up in expected end state. This is a bug and should not happen.
636    - 0: Navigation Success. Screen ended up in expected end state.
637
638    """
639
640    options = ["pokedex", "pokemon", "bag", "trainer"]
641
642    def get_action_space(self):
643        return Discrete(len(self.options))
644
645    def space_to_parameters(self, space_action):
646        if space_action < 0 or space_action >= len(self.options):
647            return None
648        return {"option": self.options[space_action]}
649
650    def parameters_to_space(self, option: str):
651        if option not in self.options:
652            return None
653        return self.options.index(option)
654
655    def is_valid(self, option: str = None):
656        if option is not None and option not in self.options:
657            return False
658        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
659        return state == AgentState.FREE_ROAM
660
661    def is_red_variant(self):
662        """
663        Returns true iff the current game is a red/blue variant.
664        """
665        return isinstance(self._emulator.state_parser, BasePokemonRedStateParser)
666
667    def _execute(self, option: str):
668        n_steps_down = 0
669        if option == "pokedex":
670            pass
671        elif option == "pokemon":
672            n_steps_down = 1
673        elif option == "bag":
674            n_steps_down = 2
675        elif option == "trainer":
676            if self.is_red_variant():
677                n_steps_down = 3
678            else:
679                n_steps_down = 4
680        else:
681            log_error(f"Invalid option {option}", self._parameters)
682        # first open menu
683        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
684        ret_states = [self._state_tracker.report()]
685        # go to the top
686        flag = False
687        for _ in range(6):
688            self._emulator.step(LowLevelActions.PRESS_ARROW_UP)
689            if self._emulator.state_parser.is_on_top_menu_option(
690                self._emulator.get_current_frame()
691            ):
692                flag = True
693                break
694        if not flag:  # could not get to top. Some error
695            return ret_states, -1
696        # go down n_steps_down
697        for _ in range(n_steps_down):
698            self._emulator.step(LowLevelActions.PRESS_ARROW_DOWN)
699            ret_states.append(self._state_tracker.report())
700        # confirm
701        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
702        ret_states.append(self._state_tracker.report())
703        return ret_states, 0
704
705    @staticmethod
706    def get_action_name(option: str) -> str:
707        return f"OpenMenu {option}"
708
709
710class BattleMenuAction(HighLevelAction):
711    """
712    Allows navigation of the battle menu.
713
714    Is Valid When:
715    - In Battle State
716
717    Action Success Interpretation:
718    - -1: Navigation Failure. Screen did not end up in expected end state.
719    - 0: Navigation Success. Screen ended up in expected end state. For run, got away safely.
720    - 1: Run Attempt Failed (cannot escape wild pokemon)
721    - 2: Run Attempt Failed (cannot escape trainer battle)
722    """
723
724    _OPTIONS = ["fight", "bag", "pokemon", "run", "progress"]
725
726    def is_valid(self, option: str = None):
727        if option is not None and option not in self._OPTIONS:
728            return False
729        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
730        # TODO: Must check we aren't in a 'learn new move' screen
731        return state == AgentState.IN_BATTLE
732
733    def get_action_space(self):
734        return Discrete(len(self._OPTIONS))
735
736    def get_all_valid_parameters(self):
737        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
738        if state != AgentState.IN_BATTLE:
739            return []
740        return [{"option": option} for option in self._OPTIONS]
741
742    def parameters_to_space(self, option: str):
743        return self._OPTIONS.index(option)
744
745    def space_to_parameters(self, space_action):
746        if space_action < 0 or space_action >= len(self._OPTIONS):
747            return None
748        return {"option": self._OPTIONS[space_action]}
749
750    def go_to_battle_menu(self):
751        # assumes we are in battle menu or will get there with some B's
752        state_reports = []
753        for i in range(3):
754            self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
755            state_reports.append(self._state_tracker.report())
756        return state_reports
757
758    def button_sequence(self, low_level_actions: List[LowLevelActions]):
759        state_reports = self.go_to_battle_menu()
760        for action in low_level_actions:
761            self._emulator.step(action)
762        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)  # confirm option
763        return state_reports + [self._state_tracker.report()]
764
765    def go_to_fight_menu(self):
766        return self.button_sequence(
767            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_LEFT]
768        )
769
770    def go_to_bag_menu(self):
771        return self.button_sequence(
772            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_LEFT]
773        )
774
775    def go_to_pokemon_menu(self):
776        return self.button_sequence(
777            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_RIGHT]
778        )
779
780    def go_to_run(self):
781        return self.button_sequence(
782            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_RIGHT]
783        )
784
785    def _execute(self, option):
786        success = -1
787        if option == "fight":
788            state_reports = self.go_to_fight_menu()
789            success = (
790                0
791                if self._emulator.state_parser.is_in_fight_options_menu(
792                    self._emulator.get_current_frame()
793                )
794                else -1
795            )
796        elif option == "bag":
797            state_reports = self.go_to_bag_menu()
798            success = (
799                0
800                if self._emulator.state_parser.is_in_fight_bag(
801                    self._emulator.get_current_frame()
802                )
803                else -1
804            )
805        elif option == "pokemon":
806            state_reports = self.go_to_pokemon_menu()
807            success = (
808                0
809                if self._emulator.state_parser.is_in_pokemon_menu(
810                    self._emulator.get_current_frame()
811                )
812                else -1
813            )
814        elif option == "run":
815            state_reports = self.go_to_run()
816            current_frame = self._emulator.get_current_frame()
817            got_away_safely = (
818                self._emulator.state_parser.named_region_matches_multi_target(
819                    current_frame, "dialogue_box_middle", "got_away_safely"
820                )
821            )
822            cannot_escape = (
823                self._emulator.state_parser.named_region_matches_multi_target(
824                    current_frame, "dialogue_box_middle", "cannot_escape"
825                )
826            )
827            cannot_run_from_trainer = (
828                self._emulator.state_parser.named_region_matches_multi_target(
829                    current_frame, "dialogue_box_middle", "cannot_run_from_trainer"
830                )
831            )
832            if got_away_safely:
833                success = 0
834                self._emulator.step(
835                    LowLevelActions.PRESS_BUTTON_B
836                )  # to clear the dialogue
837            elif cannot_escape:
838                success = 1
839                self._emulator.step(
840                    LowLevelActions.PRESS_BUTTON_B
841                )  # to clear the dialogue
842            elif cannot_run_from_trainer:
843                success = 2
844                self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
845                state_reports.append(self._state_tracker.report())
846                self._emulator.step(
847                    LowLevelActions.PRESS_BUTTON_B
848                )  # Twice, to clear the dialogue
849            else:
850                pass  # Should never happen, but might.
851            state_reports.append(self._state_tracker.report())
852            return state_reports, success
853        elif option == "progress":
854            current_frame = self._emulator.get_current_frame()
855            state_reports = self.go_to_battle_menu()
856            new_frame = self._emulator.get_current_frame()
857            if frame_changed(current_frame, new_frame):
858                success = 0  # valid frame change, screen changed
859            else:
860                success = -1  # uneccesary progress press
861        else:
862            pass  # Will never happen.
863        return state_reports, success
864
865    @staticmethod
866    def get_action_name(option: str) -> str:
867        return f"BattleMenu {option}"
868
869
870class PickAttackAction(HighLevelAction):
871    """
872    Selects an attack option in the battle fight menu.
873
874    Is Valid When:
875    - In Battle State AND In Fight Menu
876
877    Action Success Interpretation:
878    - -1: Navigation Failure. Either could not get to the top of the attack menu (this should not happen) or the option index was too high (more likely the cause of failure).
879    - 0: Used attack successfully.
880    - 1: Tried to use a move with no PP remaining.
881    """
882
883    def get_action_space(self):
884        return Discrete(4)
885
886    def is_valid(self, option: int = None):
887        option = option - 1
888        if option is not None:
889            if option < 0 or option >= 4:
890                return False
891        return self._emulator.state_parser.is_in_fight_options_menu(
892            self._emulator.get_current_frame()
893        )
894
895    def parameters_to_space(self, option: int):
896        option = option - 1
897        return option
898
899    def space_to_parameters(self, space_action):
900        return {"option": space_action + 1}
901
902    def _execute(self, option: int):
903        # assume we are in the attack menu already
904        # first go to the top:
905        option = option - 1
906        flag = False
907        for _ in range(4):
908            self._emulator.step(LowLevelActions.PRESS_ARROW_UP)
909            if self._emulator.state_parser.is_on_top_attack_option(
910                self._emulator.get_current_frame()
911            ):
912                flag = True
913                break
914        if not flag:  # could not get to top. Some error
915            return [self._state_tracker.report()], -1
916        # then go down option times
917        for time in range(option):
918            self._emulator.step(LowLevelActions.PRESS_ARROW_DOWN)
919            if self._emulator.state_parser.is_on_top_attack_option(
920                self._emulator.get_current_frame()
921            ):
922                # went back to top, means that option was invalid
923                return [self._state_tracker.report()], -1
924        state_reports = []
925        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)  # confirm option
926        state_reports.append(self._state_tracker.report())
927        if self._emulator.state_parser.tried_no_pp_move(
928            self._emulator.get_current_frame()
929        ):
930            self._emulator.step(
931                LowLevelActions.PRESS_BUTTON_B
932            )  # to clear the no PP dialogue
933            state_reports.append(self._state_tracker.report())
934            return state_reports, 1  # tried to use a move with no PP
935        else:
936            self._emulator.step(
937                LowLevelActions.PRESS_BUTTON_B
938            )  # to get through any attack animation dialogue
939            state_reports.append(self._state_tracker.report())
940        return state_reports, 0
941
942    @staticmethod
943    def get_action_name(option: int) -> str:
944        return f"PickAttack {option}"
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):
24def frame_changed(past: np.ndarray, preset: np.ndarray, epsilon=0.01):
25    return np.abs(past - preset).mean() > epsilon
class PassDialogueAction(gameboy_worlds.interface.action.SingleHighLevelAction):
36class PassDialogueAction(SingleHighLevelAction):
37    """
38    Skips dialogue by pressing the B button.
39
40    Is Valid When:
41    - In Dialogue State
42
43    Action Success Interpretation:
44    - -1: Frame did not change
45    - 0: Frame changed and no longer in dialogue state
46    - 1: Frame changed but still in dialogue state
47    """
48
49    REQUIRED_STATE_PARSER = PokemonStateParser
50    REQUIRED_STATE_TRACKER = CorePokemonTracker
51
52    def is_valid(self, **kwargs):
53        """
54        Just checks if the agent is in dialogue state.
55        """
56        return (
57            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
58            == AgentState.IN_DIALOGUE
59        )
60
61    def _execute(self):
62        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
63        report = self._state_tracker.report()
64        if not report["core"]["frame_changed"]:
65            action_success = -1
66        else:
67            action_success = (
68                0
69                if self._emulator.state_parser.get_agent_state(frames[-1])
70                != AgentState.IN_DIALOGUE
71                else 1
72            )
73        return [report], action_success
74
75    @staticmethod
76    def get_action_name() -> str:
77        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):
52    def is_valid(self, **kwargs):
53        """
54        Just checks if the agent is in dialogue state.
55        """
56        return (
57            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
58            == AgentState.IN_DIALOGUE
59        )

Just checks if the agent is in dialogue state.

@staticmethod
def get_action_name() -> str:
75    @staticmethod
76    def get_action_name() -> str:
77        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):
 80class InteractAction(SingleHighLevelAction):
 81    """
 82    Presses the A button to interact with an object in front of the agent.
 83
 84    Is Valid When:
 85    - In Free Roam State
 86
 87    Action Success Interpretation:
 88    - -1: Frame did not change or agent still in free roam state
 89    - 1: Agent not in free roam state
 90    """
 91
 92    REQUIRED_STATE_PARSER = PokemonStateParser
 93    REQUIRED_STATE_TRACKER = CorePokemonTracker
 94
 95    def is_valid(self, **kwargs):
 96        """
 97        Just checks if the agent is in free roam state.
 98        """
 99        return (
100            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
101            == AgentState.FREE_ROAM
102        )
103
104    def _execute(self):
105        current_frame = self._emulator.get_current_frame()
106        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
107        action_success = 0
108        # Check if the frames have changed. Be strict and require all to not permit jittering screens.
109        prev_frames = []
110        for frame in frames:
111            if (
112                self._emulator.state_parser.get_agent_state(frame)
113                != AgentState.FREE_ROAM
114            ):  # something happened lol
115                action_success = 1
116                break
117            for past_frame in prev_frames:
118                if not frame_changed(past_frame, frame):
119                    action_success = -1
120                    break
121            if action_success != 0:
122                break
123            prev_frames.append(frame)
124        if action_success == 0:
125            action_success = (
126                -1
127            )  # I guess? For some reason the previous thing doesn't catch same frames
128        return [
129            self._state_tracker.report()
130        ], action_success  # 0 means something maybe happened. 1 means def happened.
131
132    @staticmethod
133    def get_action_name() -> str:
134        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):
 95    def is_valid(self, **kwargs):
 96        """
 97        Just checks if the agent is in free roam state.
 98        """
 99        return (
100            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
101            == AgentState.FREE_ROAM
102        )

Just checks if the agent is in free roam state.

@staticmethod
def get_action_name() -> str:
132    @staticmethod
133    def get_action_name() -> str:
134        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 BaseMovementAction(gameboy_worlds.interface.action.HighLevelAction, abc.ABC):
137class BaseMovementAction(HighLevelAction, ABC):
138    """
139    Base class for movement actions in the Pokemon environment.
140    Has utility methods for moving in directions.
141
142    Is Valid When:
143    - In Free Roam State
144
145    Action Success Interpretation:
146    - -1: Frame did not change, even on the first step
147    - 0: Finished all steps
148    - 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.
149    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
150
151    Action Returns:
152    - `n_steps_taken` (`int`): Number of steps actually taken
153    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
154
155    Known Limitations:
156    - Struggles to handle oscillating frames when the player is next to an obstacle. So if you are surrounded by water or bouncing flowers on all quadrants (e.g. the pier in Cinnabar Island), the system will think that the agent has moved forward, even though it has not.
157    """
158
159    REQUIRED_STATE_TRACKER = CorePokemonTracker
160    REQUIRED_STATE_PARSER = PokemonStateParser
161
162    def _is_uniform_quadrant(self, frame, quadrant_name) -> bool:
163        mapper = {
164            "screen_quadrant_1": "tr",
165            "screen_quadrant_2": "tl",
166            "screen_quadrant_3": "bl",
167            "screen_quadrant_4": "br",
168        }
169        quadrant_cells = self._emulator.state_parser.capture_grid_cells(
170            frame, quadrant=mapper[quadrant_name]
171        )
172        keys = list(quadrant_cells.keys())
173        x_min = 1e9
174        y_min = 1e9
175        x_max = -1e9
176        y_max = -1e9
177        for key in keys:
178            x_coord, y_coord = key
179            if x_coord * y_coord == 0:  # pop it to avoid checking player cell
180                quadrant_cells.pop(key)
181            else:
182                x_min = min(x_min, x_coord)
183                y_min = min(y_min, y_coord)
184                x_max = max(x_max, x_coord)
185                y_max = max(y_max, y_coord)
186        x_min = int(x_min)
187        y_min = int(y_min)
188        x_max = int(x_max)
189        y_max = int(y_max)
190        keys = list(quadrant_cells.keys())
191        vertical_uniform = True
192        horizontal_uniform = True
193        # check horizontal lines
194        for y in range(y_min + 1, y_max):  # avoid edges
195            first_cell = None
196            for x in range(x_min + 1, x_max):
197                cell = quadrant_cells[(x, y)]
198                if first_cell is None:
199                    first_cell = cell
200                else:
201                    if first_cell.shape == cell.shape:
202                        if frame_changed(first_cell, cell):
203                            horizontal_uniform = False
204                            break
205            if not horizontal_uniform:
206                break
207        if horizontal_uniform:
208            return True
209        # check vertical lines
210        for x in range(x_min + 1, x_max):
211            first_cell = None
212            for y in range(y_min + 1, y_max):
213                cell = quadrant_cells[(x, y)]
214                if first_cell is None:
215                    first_cell = cell
216                else:
217                    if first_cell.shape == cell.shape:
218                        if frame_changed(first_cell, cell):
219                            vertical_uniform = False
220                            break
221            if not vertical_uniform:
222                break
223        if vertical_uniform:
224            return True
225        return False
226
227    def judge_movement(
228        self, previous_frame: np.ndarray, current_frame: np.ndarray
229    ) -> Tuple[bool, bool]:
230        """
231        Judges whether movement has occurred between two frames.
232
233        Args:
234            previous_frame (np.ndarray): The previous frame.
235            current_frame (np.ndarray): The current frame.
236        Returns:
237            Tuple[bool, bool]: A tuple containing:
238            - bool: True if movement has occurred, False otherwise.
239            - bool: True if the player has not moved, but has rotated.
240        """
241        # if the full screen hasn't changed at all, player has neither moved nor rotated
242        if not frame_changed(previous_frame, current_frame):
243            return False, False
244        # split the screen into quadrants and check which quadrants have changed. If any of them stayed the same, the player has not moved, but may have rotated.
245        # One caveat is if the quadrant is uniform tiles (i.e. all have the same grid texture in them). In this case, we can't say for sure that the quadrant hasn't changed, since it may just be that the uniform texture is the same. So we check for that too.
246        flag = False
247        for quadrant in [
248            "screen_quadrant_1",
249            "screen_quadrant_2",
250            "screen_quadrant_3",
251            "screen_quadrant_4",
252        ]:
253            prev_quad = self._emulator.state_parser.capture_named_region(
254                previous_frame, quadrant
255            )
256            curr_quad = self._emulator.state_parser.capture_named_region(
257                current_frame, quadrant
258            )
259            prev_uniform = (
260                prev_quad.max() == prev_quad.min()  # screen is all black or all white
261                or self._is_uniform_quadrant(
262                    previous_frame, quadrant
263                )  # screen quadrant is uniform tiles
264            )
265            curr_uniform = (
266                curr_quad.max() == curr_quad.min()
267                or self._is_uniform_quadrant(current_frame, quadrant)
268            )
269            if (
270                not frame_changed(prev_quad, curr_quad)
271                and not prev_uniform
272                and not curr_uniform
273            ):  # then screen isn't just black, but also hasn't changed.
274                flag = True
275                break
276        if flag:  # then some frame stayed the same, so no movement, but maybe rotation.
277            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
278                previous_frame
279            )[(0, 0)]
280            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
281                current_frame
282            )[(0, 0)]
283            if frame_changed(prev_player_cell, curr_player_cell):
284                return False, True
285            else:
286                return False, False
287        else:
288            return True, None
289
290    def move(self, direction: str, steps: int) -> Tuple[np.ndarray, int]:
291        """
292        Move in a given direction for a number of steps.
293
294        :param direction: One of "up", "down", "left", "right"
295        :type direction: str
296        :param steps: Number of steps to move in that direction
297        :type steps: int
298        :return:  A tuple containing:
299
300                - A list of state tracker reports after each low level action executed. Length is equal to the number of low level actions executed.
301
302                - An integer action success status
303        :rtype: Tuple[ndarray[_AnyShape, dtype[Any]], int]
304        """
305        action_dict = {
306            "right": LowLevelActions.PRESS_ARROW_RIGHT,
307            "down": LowLevelActions.PRESS_ARROW_DOWN,
308            "up": LowLevelActions.PRESS_ARROW_UP,
309            "left": LowLevelActions.PRESS_ARROW_LEFT,
310        }
311        if direction not in action_dict.keys():
312            log_error(f"Got invalid direction to move {direction}", self._parameters)
313        action = action_dict[direction]
314        # keep trying the action.
315        # exit status 0 -> finished steps
316        # 1 -> took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter)
317        # 2 -> took some steps, but agent state changed from free roam
318        # -1 -> frame didn't change, even on the first step
319        action_success = -1
320        transition_state_dicts = []
321        transition_frames = []
322        previous_frame = (
323            self._emulator.get_current_frame()
324        )  # Do NOT get the state tracker frame, as it may have a grid on it.
325        n_step = 0
326        n_successful_steps = 0
327        has_rotated = None
328        agent_state = AgentState.FREE_ROAM
329        while n_step < steps and agent_state == AgentState.FREE_ROAM:
330            frames, done = self._emulator.step(action)
331            transition_state_dicts.append(self._state_tracker.report())
332            transition_frames.extend(frames)
333            current_frame = (
334                self._emulator.get_current_frame()
335            )  # Do NOT use the emulator frame, as it may have a grid on it.
336            if done:
337                break
338            # check if frames changed. If not, break out.
339            # We check all frames in sequence to try and catch oscillations. But nothing will catch 1 step into wall in areas like this
340            player_moved, player_rotated = self.judge_movement(
341                previous_frame, current_frame
342            )
343            if player_rotated == True:
344                has_rotated = True
345            if not player_moved and not player_rotated:
346                break
347            if player_moved:
348                n_successful_steps += 1  # don't count rotation as a step
349            agent_state = self._emulator.state_parser.get_agent_state(
350                self._emulator.get_current_frame()
351            )
352            if agent_state != AgentState.FREE_ROAM:
353                break
354            n_step += 1
355            previous_frame = current_frame
356        if agent_state != AgentState.FREE_ROAM:
357            action_success = 2
358        else:
359            if n_step <= 0:
360                action_success = -1
361            elif n_step == steps:
362                action_success = 0
363            else:
364                action_success = 1
365        transition_state_dicts[-1]["core"]["action_return"] = {
366            "n_steps_taken": n_successful_steps,
367            "rotated": has_rotated,
368        }
369        return transition_state_dicts, action_success
370
371    def is_valid(self, **kwargs):
372        """
373        Just checks if the agent is in free roam state.
374        """
375        return (
376            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
377            == AgentState.FREE_ROAM
378        )

Base class for movement actions in the Pokemon 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 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. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.

Known Limitations:

  • Struggles to handle oscillating frames when the player is next to an obstacle. So if you are surrounded by water or bouncing flowers on all quadrants (e.g. the pier in Cinnabar Island), the system will think that the agent has moved forward, even though it has not.

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 judge_movement( self, previous_frame: numpy.ndarray, current_frame: numpy.ndarray) -> Tuple[bool, bool]:
227    def judge_movement(
228        self, previous_frame: np.ndarray, current_frame: np.ndarray
229    ) -> Tuple[bool, bool]:
230        """
231        Judges whether movement has occurred between two frames.
232
233        Args:
234            previous_frame (np.ndarray): The previous frame.
235            current_frame (np.ndarray): The current frame.
236        Returns:
237            Tuple[bool, bool]: A tuple containing:
238            - bool: True if movement has occurred, False otherwise.
239            - bool: True if the player has not moved, but has rotated.
240        """
241        # if the full screen hasn't changed at all, player has neither moved nor rotated
242        if not frame_changed(previous_frame, current_frame):
243            return False, False
244        # split the screen into quadrants and check which quadrants have changed. If any of them stayed the same, the player has not moved, but may have rotated.
245        # One caveat is if the quadrant is uniform tiles (i.e. all have the same grid texture in them). In this case, we can't say for sure that the quadrant hasn't changed, since it may just be that the uniform texture is the same. So we check for that too.
246        flag = False
247        for quadrant in [
248            "screen_quadrant_1",
249            "screen_quadrant_2",
250            "screen_quadrant_3",
251            "screen_quadrant_4",
252        ]:
253            prev_quad = self._emulator.state_parser.capture_named_region(
254                previous_frame, quadrant
255            )
256            curr_quad = self._emulator.state_parser.capture_named_region(
257                current_frame, quadrant
258            )
259            prev_uniform = (
260                prev_quad.max() == prev_quad.min()  # screen is all black or all white
261                or self._is_uniform_quadrant(
262                    previous_frame, quadrant
263                )  # screen quadrant is uniform tiles
264            )
265            curr_uniform = (
266                curr_quad.max() == curr_quad.min()
267                or self._is_uniform_quadrant(current_frame, quadrant)
268            )
269            if (
270                not frame_changed(prev_quad, curr_quad)
271                and not prev_uniform
272                and not curr_uniform
273            ):  # then screen isn't just black, but also hasn't changed.
274                flag = True
275                break
276        if flag:  # then some frame stayed the same, so no movement, but maybe rotation.
277            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
278                previous_frame
279            )[(0, 0)]
280            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
281                current_frame
282            )[(0, 0)]
283            if frame_changed(prev_player_cell, curr_player_cell):
284                return False, True
285            else:
286                return False, False
287        else:
288            return True, None

Judges whether movement has occurred between two frames.

Arguments:
  • previous_frame (np.ndarray): The previous frame.
  • current_frame (np.ndarray): The current frame.
Returns:

Tuple[bool, bool]: A tuple containing:

  • bool: True if movement has occurred, False otherwise.
  • bool: True if the player has not moved, but has rotated.
def move(self, direction: str, steps: int) -> Tuple[numpy.ndarray, int]:
290    def move(self, direction: str, steps: int) -> Tuple[np.ndarray, int]:
291        """
292        Move in a given direction for a number of steps.
293
294        :param direction: One of "up", "down", "left", "right"
295        :type direction: str
296        :param steps: Number of steps to move in that direction
297        :type steps: int
298        :return:  A tuple containing:
299
300                - A list of state tracker reports after each low level action executed. Length is equal to the number of low level actions executed.
301
302                - An integer action success status
303        :rtype: Tuple[ndarray[_AnyShape, dtype[Any]], int]
304        """
305        action_dict = {
306            "right": LowLevelActions.PRESS_ARROW_RIGHT,
307            "down": LowLevelActions.PRESS_ARROW_DOWN,
308            "up": LowLevelActions.PRESS_ARROW_UP,
309            "left": LowLevelActions.PRESS_ARROW_LEFT,
310        }
311        if direction not in action_dict.keys():
312            log_error(f"Got invalid direction to move {direction}", self._parameters)
313        action = action_dict[direction]
314        # keep trying the action.
315        # exit status 0 -> finished steps
316        # 1 -> took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter)
317        # 2 -> took some steps, but agent state changed from free roam
318        # -1 -> frame didn't change, even on the first step
319        action_success = -1
320        transition_state_dicts = []
321        transition_frames = []
322        previous_frame = (
323            self._emulator.get_current_frame()
324        )  # Do NOT get the state tracker frame, as it may have a grid on it.
325        n_step = 0
326        n_successful_steps = 0
327        has_rotated = None
328        agent_state = AgentState.FREE_ROAM
329        while n_step < steps and agent_state == AgentState.FREE_ROAM:
330            frames, done = self._emulator.step(action)
331            transition_state_dicts.append(self._state_tracker.report())
332            transition_frames.extend(frames)
333            current_frame = (
334                self._emulator.get_current_frame()
335            )  # Do NOT use the emulator frame, as it may have a grid on it.
336            if done:
337                break
338            # check if frames changed. If not, break out.
339            # We check all frames in sequence to try and catch oscillations. But nothing will catch 1 step into wall in areas like this
340            player_moved, player_rotated = self.judge_movement(
341                previous_frame, current_frame
342            )
343            if player_rotated == True:
344                has_rotated = True
345            if not player_moved and not player_rotated:
346                break
347            if player_moved:
348                n_successful_steps += 1  # don't count rotation as a step
349            agent_state = self._emulator.state_parser.get_agent_state(
350                self._emulator.get_current_frame()
351            )
352            if agent_state != AgentState.FREE_ROAM:
353                break
354            n_step += 1
355            previous_frame = current_frame
356        if agent_state != AgentState.FREE_ROAM:
357            action_success = 2
358        else:
359            if n_step <= 0:
360                action_success = -1
361            elif n_step == steps:
362                action_success = 0
363            else:
364                action_success = 1
365        transition_state_dicts[-1]["core"]["action_return"] = {
366            "n_steps_taken": n_successful_steps,
367            "rotated": has_rotated,
368        }
369        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. Length is equal to the number of low level actions executed.

    - An integer action success status
def is_valid(self, **kwargs):
371    def is_valid(self, **kwargs):
372        """
373        Just checks if the agent is in free roam state.
374        """
375        return (
376            self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
377            == AgentState.FREE_ROAM
378        )

Just checks if the agent is in free roam state.

class MoveStepsAction(BaseMovementAction):
381class MoveStepsAction(BaseMovementAction):
382    """
383    Moves the agent in a specified cardinal direction for a specified number of steps.
384
385    Is Valid When:
386    - In Free Roam State
387    Action Success Interpretation:
388    - -1: Frame did not change, even on the first step
389    - 0: Finished all steps
390    - 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.
391    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
392
393    Action Returns:
394    - `n_steps_taken` (`int`): Number of steps actually taken
395    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
396    """
397
398    def get_action_space(self):
399        """
400        Returns a Box space representing movement in 2D.
401        The first dimension represents vertical movement (positive is up, negative is down).
402        The second dimension represents horizontal movement (positive is right, negative is left).
403
404        Returns:
405            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
406
407        """
408        return Discrete(4 * HARD_MAX_STEPS)
409
410    def space_to_parameters(self, space_action):
411        direction = None
412        steps = None
413        if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS:
414            # log_warn(f"Invalid space action {space_action}", self._parameters)
415            return None
416        if space_action < HARD_MAX_STEPS:
417            direction = "up"
418            steps = space_action
419        elif space_action < 2 * HARD_MAX_STEPS:
420            direction = "down"
421            steps = space_action - HARD_MAX_STEPS
422        elif space_action < 3 * HARD_MAX_STEPS:
423            direction = "left"
424            steps = space_action - 2 * HARD_MAX_STEPS
425        else:
426            direction = "right"
427            steps = space_action - 3 * HARD_MAX_STEPS
428        return {"direction": direction, "steps": steps + 1}
429
430    def parameters_to_space(self, direction: str, steps: int):
431        if steps <= 0 or steps > HARD_MAX_STEPS:
432            return None
433        if direction == "up":
434            return steps - 1
435        elif direction == "down":
436            return HARD_MAX_STEPS + steps - 1
437        elif direction == "left":
438            return 2 * HARD_MAX_STEPS + steps - 1
439        elif direction == "right":
440            return 3 * HARD_MAX_STEPS + steps - 1
441        else:
442            # log_warn(f"Unrecognized direction {direction}", self._parameters)
443            return None
444
445    def _execute(self, direction, steps):
446        transition_states, status = self.move(direction=direction, steps=steps)
447        return transition_states, status
448
449    def is_valid(self, **kwargs):
450        direction = kwargs.get("direction")
451        step = kwargs.get("step")
452        if direction is not None and direction not in ["up", "down", "left", "right"]:
453            return False
454        if step is not None:
455            if not isinstance(step, str):
456                return False
457            if step <= 0:
458                return False
459        return super().is_valid(**kwargs)
460
461    @staticmethod
462    def get_action_name(direction: str, steps: int) -> str:
463        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 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. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
def get_action_space(self):
398    def get_action_space(self):
399        """
400        Returns a Box space representing movement in 2D.
401        The first dimension represents vertical movement (positive is up, negative is down).
402        The second dimension represents horizontal movement (positive is right, negative is left).
403
404        Returns:
405            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
406
407        """
408        return Discrete(4 * HARD_MAX_STEPS)

Returns a Box space representing movement in 2D. The first dimension represents vertical movement (positive is up, negative is down). The second dimension represents horizontal movement (positive is right, negative is left).

Returns:

Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.

def space_to_parameters(self, space_action):
410    def space_to_parameters(self, space_action):
411        direction = None
412        steps = None
413        if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS:
414            # log_warn(f"Invalid space action {space_action}", self._parameters)
415            return None
416        if space_action < HARD_MAX_STEPS:
417            direction = "up"
418            steps = space_action
419        elif space_action < 2 * HARD_MAX_STEPS:
420            direction = "down"
421            steps = space_action - HARD_MAX_STEPS
422        elif space_action < 3 * HARD_MAX_STEPS:
423            direction = "left"
424            steps = space_action - 2 * HARD_MAX_STEPS
425        else:
426            direction = "right"
427            steps = space_action - 3 * HARD_MAX_STEPS
428        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):
430    def parameters_to_space(self, direction: str, steps: int):
431        if steps <= 0 or steps > HARD_MAX_STEPS:
432            return None
433        if direction == "up":
434            return steps - 1
435        elif direction == "down":
436            return HARD_MAX_STEPS + steps - 1
437        elif direction == "left":
438            return 2 * HARD_MAX_STEPS + steps - 1
439        elif direction == "right":
440            return 3 * HARD_MAX_STEPS + steps - 1
441        else:
442            # log_warn(f"Unrecognized direction {direction}", self._parameters)
443            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):
449    def is_valid(self, **kwargs):
450        direction = kwargs.get("direction")
451        step = kwargs.get("step")
452        if direction is not None and direction not in ["up", "down", "left", "right"]:
453            return False
454        if step is not None:
455            if not isinstance(step, str):
456                return False
457            if step <= 0:
458                return False
459        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:
461    @staticmethod
462    def get_action_name(direction: str, steps: int) -> str:
463        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.

class MoveGridAction(BaseMovementAction):
466class MoveGridAction(BaseMovementAction):
467    """
468    Moves the agent on both axes. Will always try to move right first and then up.
469
470    Is Valid When:
471    - In Free Roam State
472    Action Success Interpretation:
473    - -1: Frame did not change, even on the first step
474    - 0: Finished all steps
475    - 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.
476    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
477
478    Action Returns:
479    - `n_steps_taken` (`int`): Number of steps actually taken
480    - `rotated` (`bool` or `None`): True if the player has not moved, but has rotated. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
481    """
482
483    def get_action_space(self):
484        """
485        Returns a Box space representing movement in 2D.
486        The first dimension represents vertical movement (positive is up, negative is down).
487        The second dimension represents horizontal movement (positive is right, negative is left).
488
489        Returns:
490            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
491
492        """
493        return Box(
494            low=-HARD_MAX_STEPS // 2,
495            high=HARD_MAX_STEPS // 2,
496            shape=(2,),
497            dtype=np.int8,
498        )
499
500    def space_to_parameters(self, space_action):
501        right_action = space_action[0]
502        up_action = space_action[1]
503        return {"x_steps": right_action, "y_steps": up_action}
504
505    def parameters_to_space(self, x_steps, y_steps):
506        move_vec = np.zeros(2)  # x, y
507        move_vec[0] = x_steps
508        move_vec[1] = y_steps
509        return move_vec
510
511    def _execute(self, x_steps, y_steps):
512        x_direction = "right" if x_steps >= 0 else "left"
513        y_direction = "up" if y_steps >= 0 else "down"
514        if x_steps != 0:
515            transition_states, status = self.move(
516                direction=x_direction, steps=abs(x_steps)
517            )
518            if status != 0:
519                return transition_states, status
520        else:
521            transition_states = []
522        if y_steps != 0:
523            more_transition_states, status = self.move(
524                direction=y_direction, steps=abs(y_steps)
525            )
526            transition_states.extend(more_transition_states)
527        try:
528            status is not None
529        except NameError:
530            log_warn(
531                f"Weird case where both x_steps and y_steps are 0 in MoveGridAction or something. {x_steps}, {y_steps}",
532                self._parameters,
533            )
534            transition_states = [self._state_tracker.report()]
535            status = -1
536        return transition_states, status
537
538    def is_valid(self, x_steps: int = None, y_steps: int = None):
539        if x_steps is not None and y_steps is not None:
540            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
541                return False
542            if x_steps == 0 and y_steps == 0:
543                return False
544        return super().is_valid()
545
546    @staticmethod
547    def get_action_name(x_steps: int, y_steps: int) -> str:
548        return f"MoveGrid ({x_steps}, {y_steps})"

Moves the agent on both axes. Will always try to move right first and then up.

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. If the player has moved, this will be None. If it is False, it means the player tried to walk straight into an obstacle.
def get_action_space(self):
483    def get_action_space(self):
484        """
485        Returns a Box space representing movement in 2D.
486        The first dimension represents vertical movement (positive is up, negative is down).
487        The second dimension represents horizontal movement (positive is right, negative is left).
488
489        Returns:
490            Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.
491
492        """
493        return Box(
494            low=-HARD_MAX_STEPS // 2,
495            high=HARD_MAX_STEPS // 2,
496            shape=(2,),
497            dtype=np.int8,
498        )

Returns a Box space representing movement in 2D. The first dimension represents vertical movement (positive is up, negative is down). The second dimension represents horizontal movement (positive is right, negative is left).

Returns:

Box: A Box space with shape (2,) and values ranging from -HARD_MAX_STEPS//2 to HARD_MAX_STEPS//2.

def space_to_parameters(self, space_action):
500    def space_to_parameters(self, space_action):
501        right_action = space_action[0]
502        up_action = space_action[1]
503        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.

def parameters_to_space(self, x_steps, y_steps):
505    def parameters_to_space(self, x_steps, y_steps):
506        move_vec = np.zeros(2)  # x, y
507        move_vec[0] = x_steps
508        move_vec[1] = y_steps
509        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.

def is_valid(self, x_steps: int = None, y_steps: int = None):
538    def is_valid(self, x_steps: int = None, y_steps: int = None):
539        if x_steps is not None and y_steps is not None:
540            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
541                return False
542            if x_steps == 0 and y_steps == 0:
543                return False
544        return super().is_valid()

Just checks if the agent is in free roam state.

@staticmethod
def get_action_name(x_steps: int, y_steps: int) -> str:
546    @staticmethod
547    def get_action_name(x_steps: int, y_steps: int) -> str:
548        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.

class OpenMenuAction(gameboy_worlds.interface.action.HighLevelAction):
629class OpenMenuAction(HighLevelAction):
630    """
631    Opens the main menu from free roam and navigates to specified option.
632    Is Valid When:
633    - In Free Roam State
634
635    Action Success Interpretation:
636    - -1: Navigation Failure. Screen did not end up in expected end state. This is a bug and should not happen.
637    - 0: Navigation Success. Screen ended up in expected end state.
638
639    """
640
641    options = ["pokedex", "pokemon", "bag", "trainer"]
642
643    def get_action_space(self):
644        return Discrete(len(self.options))
645
646    def space_to_parameters(self, space_action):
647        if space_action < 0 or space_action >= len(self.options):
648            return None
649        return {"option": self.options[space_action]}
650
651    def parameters_to_space(self, option: str):
652        if option not in self.options:
653            return None
654        return self.options.index(option)
655
656    def is_valid(self, option: str = None):
657        if option is not None and option not in self.options:
658            return False
659        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
660        return state == AgentState.FREE_ROAM
661
662    def is_red_variant(self):
663        """
664        Returns true iff the current game is a red/blue variant.
665        """
666        return isinstance(self._emulator.state_parser, BasePokemonRedStateParser)
667
668    def _execute(self, option: str):
669        n_steps_down = 0
670        if option == "pokedex":
671            pass
672        elif option == "pokemon":
673            n_steps_down = 1
674        elif option == "bag":
675            n_steps_down = 2
676        elif option == "trainer":
677            if self.is_red_variant():
678                n_steps_down = 3
679            else:
680                n_steps_down = 4
681        else:
682            log_error(f"Invalid option {option}", self._parameters)
683        # first open menu
684        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
685        ret_states = [self._state_tracker.report()]
686        # go to the top
687        flag = False
688        for _ in range(6):
689            self._emulator.step(LowLevelActions.PRESS_ARROW_UP)
690            if self._emulator.state_parser.is_on_top_menu_option(
691                self._emulator.get_current_frame()
692            ):
693                flag = True
694                break
695        if not flag:  # could not get to top. Some error
696            return ret_states, -1
697        # go down n_steps_down
698        for _ in range(n_steps_down):
699            self._emulator.step(LowLevelActions.PRESS_ARROW_DOWN)
700            ret_states.append(self._state_tracker.report())
701        # confirm
702        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
703        ret_states.append(self._state_tracker.report())
704        return ret_states, 0
705
706    @staticmethod
707    def get_action_name(option: str) -> str:
708        return f"OpenMenu {option}"

Opens the main menu from free roam and navigates to specified option. Is Valid When:

  • In Free Roam State

Action Success Interpretation:

  • -1: Navigation Failure. Screen did not end up in expected end state. This is a bug and should not happen.
  • 0: Navigation Success. Screen ended up in expected end state.
options = ['pokedex', 'pokemon', 'bag', 'trainer']
def get_action_space(self):
643    def get_action_space(self):
644        return Discrete(len(self.options))

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.

def space_to_parameters(self, space_action):
646    def space_to_parameters(self, space_action):
647        if space_action < 0 or space_action >= len(self.options):
648            return None
649        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.

def parameters_to_space(self, option: str):
651    def parameters_to_space(self, option: str):
652        if option not in self.options:
653            return None
654        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.

def is_valid(self, option: str = None):
656    def is_valid(self, option: str = None):
657        if option is not None and option not in self.options:
658            return False
659        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
660        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.

def is_red_variant(self):
662    def is_red_variant(self):
663        """
664        Returns true iff the current game is a red/blue variant.
665        """
666        return isinstance(self._emulator.state_parser, BasePokemonRedStateParser)

Returns true iff the current game is a red/blue variant.

@staticmethod
def get_action_name(option: str) -> str:
706    @staticmethod
707    def get_action_name(option: str) -> str:
708        return f"OpenMenu {option}"

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 BattleMenuAction(gameboy_worlds.interface.action.HighLevelAction):
711class BattleMenuAction(HighLevelAction):
712    """
713    Allows navigation of the battle menu.
714
715    Is Valid When:
716    - In Battle State
717
718    Action Success Interpretation:
719    - -1: Navigation Failure. Screen did not end up in expected end state.
720    - 0: Navigation Success. Screen ended up in expected end state. For run, got away safely.
721    - 1: Run Attempt Failed (cannot escape wild pokemon)
722    - 2: Run Attempt Failed (cannot escape trainer battle)
723    """
724
725    _OPTIONS = ["fight", "bag", "pokemon", "run", "progress"]
726
727    def is_valid(self, option: str = None):
728        if option is not None and option not in self._OPTIONS:
729            return False
730        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
731        # TODO: Must check we aren't in a 'learn new move' screen
732        return state == AgentState.IN_BATTLE
733
734    def get_action_space(self):
735        return Discrete(len(self._OPTIONS))
736
737    def get_all_valid_parameters(self):
738        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
739        if state != AgentState.IN_BATTLE:
740            return []
741        return [{"option": option} for option in self._OPTIONS]
742
743    def parameters_to_space(self, option: str):
744        return self._OPTIONS.index(option)
745
746    def space_to_parameters(self, space_action):
747        if space_action < 0 or space_action >= len(self._OPTIONS):
748            return None
749        return {"option": self._OPTIONS[space_action]}
750
751    def go_to_battle_menu(self):
752        # assumes we are in battle menu or will get there with some B's
753        state_reports = []
754        for i in range(3):
755            self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
756            state_reports.append(self._state_tracker.report())
757        return state_reports
758
759    def button_sequence(self, low_level_actions: List[LowLevelActions]):
760        state_reports = self.go_to_battle_menu()
761        for action in low_level_actions:
762            self._emulator.step(action)
763        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)  # confirm option
764        return state_reports + [self._state_tracker.report()]
765
766    def go_to_fight_menu(self):
767        return self.button_sequence(
768            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_LEFT]
769        )
770
771    def go_to_bag_menu(self):
772        return self.button_sequence(
773            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_LEFT]
774        )
775
776    def go_to_pokemon_menu(self):
777        return self.button_sequence(
778            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_RIGHT]
779        )
780
781    def go_to_run(self):
782        return self.button_sequence(
783            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_RIGHT]
784        )
785
786    def _execute(self, option):
787        success = -1
788        if option == "fight":
789            state_reports = self.go_to_fight_menu()
790            success = (
791                0
792                if self._emulator.state_parser.is_in_fight_options_menu(
793                    self._emulator.get_current_frame()
794                )
795                else -1
796            )
797        elif option == "bag":
798            state_reports = self.go_to_bag_menu()
799            success = (
800                0
801                if self._emulator.state_parser.is_in_fight_bag(
802                    self._emulator.get_current_frame()
803                )
804                else -1
805            )
806        elif option == "pokemon":
807            state_reports = self.go_to_pokemon_menu()
808            success = (
809                0
810                if self._emulator.state_parser.is_in_pokemon_menu(
811                    self._emulator.get_current_frame()
812                )
813                else -1
814            )
815        elif option == "run":
816            state_reports = self.go_to_run()
817            current_frame = self._emulator.get_current_frame()
818            got_away_safely = (
819                self._emulator.state_parser.named_region_matches_multi_target(
820                    current_frame, "dialogue_box_middle", "got_away_safely"
821                )
822            )
823            cannot_escape = (
824                self._emulator.state_parser.named_region_matches_multi_target(
825                    current_frame, "dialogue_box_middle", "cannot_escape"
826                )
827            )
828            cannot_run_from_trainer = (
829                self._emulator.state_parser.named_region_matches_multi_target(
830                    current_frame, "dialogue_box_middle", "cannot_run_from_trainer"
831                )
832            )
833            if got_away_safely:
834                success = 0
835                self._emulator.step(
836                    LowLevelActions.PRESS_BUTTON_B
837                )  # to clear the dialogue
838            elif cannot_escape:
839                success = 1
840                self._emulator.step(
841                    LowLevelActions.PRESS_BUTTON_B
842                )  # to clear the dialogue
843            elif cannot_run_from_trainer:
844                success = 2
845                self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
846                state_reports.append(self._state_tracker.report())
847                self._emulator.step(
848                    LowLevelActions.PRESS_BUTTON_B
849                )  # Twice, to clear the dialogue
850            else:
851                pass  # Should never happen, but might.
852            state_reports.append(self._state_tracker.report())
853            return state_reports, success
854        elif option == "progress":
855            current_frame = self._emulator.get_current_frame()
856            state_reports = self.go_to_battle_menu()
857            new_frame = self._emulator.get_current_frame()
858            if frame_changed(current_frame, new_frame):
859                success = 0  # valid frame change, screen changed
860            else:
861                success = -1  # uneccesary progress press
862        else:
863            pass  # Will never happen.
864        return state_reports, success
865
866    @staticmethod
867    def get_action_name(option: str) -> str:
868        return f"BattleMenu {option}"

Allows navigation of the battle menu.

Is Valid When:

  • In Battle State

Action Success Interpretation:

  • -1: Navigation Failure. Screen did not end up in expected end state.
  • 0: Navigation Success. Screen ended up in expected end state. For run, got away safely.
  • 1: Run Attempt Failed (cannot escape wild pokemon)
  • 2: Run Attempt Failed (cannot escape trainer battle)
def is_valid(self, option: str = None):
727    def is_valid(self, option: str = None):
728        if option is not None and option not in self._OPTIONS:
729            return False
730        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
731        # TODO: Must check we aren't in a 'learn new move' screen
732        return state == AgentState.IN_BATTLE

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.

def get_action_space(self):
734    def get_action_space(self):
735        return Discrete(len(self._OPTIONS))

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.

def get_all_valid_parameters(self):
737    def get_all_valid_parameters(self):
738        state = self._state_tracker.get_episode_metric(("pokemon_core", "agent_state"))
739        if state != AgentState.IN_BATTLE:
740            return []
741        return [{"option": option} for option in self._OPTIONS]

Returns a list of all valid parameterizations for the high level action in the current state.

May not well defined for all high level actions, because some high level actions may have infinite parameterizations. (e.g. move to any (x, y) position.)

Use this to enumerate all valid ways to perform the action, and provide a way to sample over all valid parameterizations.

Returns:

List[Dict[str, Any]]: A list of valid parameterizations for the high level action.

def parameters_to_space(self, option: str):
743    def parameters_to_space(self, option: str):
744        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.

def space_to_parameters(self, space_action):
746    def space_to_parameters(self, space_action):
747        if space_action < 0 or space_action >= len(self._OPTIONS):
748            return None
749        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.

def go_to_battle_menu(self):
751    def go_to_battle_menu(self):
752        # assumes we are in battle menu or will get there with some B's
753        state_reports = []
754        for i in range(3):
755            self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
756            state_reports.append(self._state_tracker.report())
757        return state_reports
def button_sequence( self, low_level_actions: List[gameboy_worlds.emulation.emulator.LowLevelActions]):
759    def button_sequence(self, low_level_actions: List[LowLevelActions]):
760        state_reports = self.go_to_battle_menu()
761        for action in low_level_actions:
762            self._emulator.step(action)
763        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)  # confirm option
764        return state_reports + [self._state_tracker.report()]
def go_to_fight_menu(self):
766    def go_to_fight_menu(self):
767        return self.button_sequence(
768            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_LEFT]
769        )
def go_to_bag_menu(self):
771    def go_to_bag_menu(self):
772        return self.button_sequence(
773            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_LEFT]
774        )
def go_to_pokemon_menu(self):
776    def go_to_pokemon_menu(self):
777        return self.button_sequence(
778            [LowLevelActions.PRESS_ARROW_UP, LowLevelActions.PRESS_ARROW_RIGHT]
779        )
def go_to_run(self):
781    def go_to_run(self):
782        return self.button_sequence(
783            [LowLevelActions.PRESS_ARROW_DOWN, LowLevelActions.PRESS_ARROW_RIGHT]
784        )
@staticmethod
def get_action_name(option: str) -> str:
866    @staticmethod
867    def get_action_name(option: str) -> str:
868        return f"BattleMenu {option}"

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 PickAttackAction(gameboy_worlds.interface.action.HighLevelAction):
871class PickAttackAction(HighLevelAction):
872    """
873    Selects an attack option in the battle fight menu.
874
875    Is Valid When:
876    - In Battle State AND In Fight Menu
877
878    Action Success Interpretation:
879    - -1: Navigation Failure. Either could not get to the top of the attack menu (this should not happen) or the option index was too high (more likely the cause of failure).
880    - 0: Used attack successfully.
881    - 1: Tried to use a move with no PP remaining.
882    """
883
884    def get_action_space(self):
885        return Discrete(4)
886
887    def is_valid(self, option: int = None):
888        option = option - 1
889        if option is not None:
890            if option < 0 or option >= 4:
891                return False
892        return self._emulator.state_parser.is_in_fight_options_menu(
893            self._emulator.get_current_frame()
894        )
895
896    def parameters_to_space(self, option: int):
897        option = option - 1
898        return option
899
900    def space_to_parameters(self, space_action):
901        return {"option": space_action + 1}
902
903    def _execute(self, option: int):
904        # assume we are in the attack menu already
905        # first go to the top:
906        option = option - 1
907        flag = False
908        for _ in range(4):
909            self._emulator.step(LowLevelActions.PRESS_ARROW_UP)
910            if self._emulator.state_parser.is_on_top_attack_option(
911                self._emulator.get_current_frame()
912            ):
913                flag = True
914                break
915        if not flag:  # could not get to top. Some error
916            return [self._state_tracker.report()], -1
917        # then go down option times
918        for time in range(option):
919            self._emulator.step(LowLevelActions.PRESS_ARROW_DOWN)
920            if self._emulator.state_parser.is_on_top_attack_option(
921                self._emulator.get_current_frame()
922            ):
923                # went back to top, means that option was invalid
924                return [self._state_tracker.report()], -1
925        state_reports = []
926        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)  # confirm option
927        state_reports.append(self._state_tracker.report())
928        if self._emulator.state_parser.tried_no_pp_move(
929            self._emulator.get_current_frame()
930        ):
931            self._emulator.step(
932                LowLevelActions.PRESS_BUTTON_B
933            )  # to clear the no PP dialogue
934            state_reports.append(self._state_tracker.report())
935            return state_reports, 1  # tried to use a move with no PP
936        else:
937            self._emulator.step(
938                LowLevelActions.PRESS_BUTTON_B
939            )  # to get through any attack animation dialogue
940            state_reports.append(self._state_tracker.report())
941        return state_reports, 0
942
943    @staticmethod
944    def get_action_name(option: int) -> str:
945        return f"PickAttack {option}"

Selects an attack option in the battle fight menu.

Is Valid When:

  • In Battle State AND In Fight Menu

Action Success Interpretation:

  • -1: Navigation Failure. Either could not get to the top of the attack menu (this should not happen) or the option index was too high (more likely the cause of failure).
  • 0: Used attack successfully.
  • 1: Tried to use a move with no PP remaining.
def get_action_space(self):
884    def get_action_space(self):
885        return Discrete(4)

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.

def is_valid(self, option: int = None):
887    def is_valid(self, option: int = None):
888        option = option - 1
889        if option is not None:
890            if option < 0 or option >= 4:
891                return False
892        return self._emulator.state_parser.is_in_fight_options_menu(
893            self._emulator.get_current_frame()
894        )

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.

def parameters_to_space(self, option: int):
896    def parameters_to_space(self, option: int):
897        option = option - 1
898        return 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.

def space_to_parameters(self, space_action):
900    def space_to_parameters(self, space_action):
901        return {"option": space_action + 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.

@staticmethod
def get_action_name(option: int) -> str:
943    @staticmethod
944    def get_action_name(option: int) -> str:
945        return f"PickAttack {option}"

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.