gameboy_worlds.emulation.pokemon.parsers

Pokemon specific game state parser implementations for both PokemonRed and PokemonCrystal. While this code base started from: https://github.com/PWhiddy/PokemonRedExperiments/ (v2) and was initially read from memory states https://github.com/thatguy11325/pokemonred_puffer/blob/main/pokemonred_puffer/global_map.py, this is no longer the case as we have moved to visual based state parsing. This decision was primarily made to facilitate easier extension to other games and rom hacks in the future, as well as to avoid reliance on specific memory addresses which may vary between different versions of the game.

However, the code base supports reading from memory addresses to extract game state information, which can be useful for incorporating domain knowledge into reward structures or other aspects of the environment. See the MemoryBasedPokemonRedStateParser class for examples of how to read game state information from memory addresses.

WARNING: The screen capture mechanisms of the parsers rely on a SPECIFIC FRAME being used in the game. This is not a concern with Gen I games, but Gen II games have options for frames. All states and captures in this repo assume a particular choice of frame, and often it is NOT the default Frame 1. Ensure that your agents DO NOT change the frame settings in the game, or the state parsing will fail.

CORE DESIGN PRINCIPLE: Never branch the parser subclasses for a given variant. The inheritance tree for a parser after the game variant parser should always be a tree with only one child per layer. This is to ensure that we don't double effort, any capability added to a parser will always be valid for that game variant. If this principle is followed, any state tracker can always use the STRONGEST (lowest level) parser for a given variant without concern for missing functionality.

  1"""
  2Pokemon specific game state parser implementations for both PokemonRed and PokemonCrystal.
  3While this code base started from: https://github.com/PWhiddy/PokemonRedExperiments/ (v2) and was initially read from memory states https://github.com/thatguy11325/pokemonred_puffer/blob/main/pokemonred_puffer/global_map.py, this is no longer the case as we have moved to visual based state parsing.
  4This decision was primarily made to facilitate easier extension to other games and rom hacks in the future, as well as to avoid reliance on specific memory addresses which may vary between different versions of the game.
  5
  6However, the code base supports reading from memory addresses to extract game state information, which can be useful for incorporating domain knowledge into reward structures or other aspects of the environment. See the MemoryBasedPokemonRedStateParser class for examples of how to read game state information from memory addresses.
  7
  8WARNING: The screen capture mechanisms of the parsers rely on a SPECIFIC FRAME being used in the game. This is not a concern with Gen I games, but Gen II games have options for frames. All states and captures in this repo assume a particular choice of frame, and often it is NOT the default Frame 1.
  9Ensure that your agents DO NOT change the frame settings in the game, or the state parsing will fail.
 10
 11CORE DESIGN PRINCIPLE: Never branch the parser subclasses for a given variant. The inheritance tree for a parser after the game variant parser should always be a tree with only one child per layer.
 12This is to ensure that we don't double effort, any capability added to a parser will always be valid for that game variant.
 13If this principle is followed, any state tracker can always use the STRONGEST (lowest level) parser for a given variant without concern for missing functionality.
 14"""
 15
 16from gameboy_worlds.emulation.parser import NamedScreenRegion
 17from gameboy_worlds.utils import (
 18    log_warn,
 19    log_info,
 20    log_error,
 21    load_parameters,
 22    verify_parameters,
 23)
 24from gameboy_worlds.emulation.parser import StateParser, _get_proper_regions
 25
 26from typing import Set, List, Type, Dict, Optional, Tuple
 27import os
 28from abc import ABC, abstractmethod
 29from enum import Enum
 30
 31from pyboy import PyBoy
 32
 33import json
 34import numpy as np
 35from bidict import bidict
 36
 37
 38class AgentState(Enum):
 39    """
 40    0. FREE_ROAM: The agent is freely roaming the game world.
 41    1. IN_DIALOGUE: The agent is currently in a dialogue state. (including reading signs, talking to NPCs, etc.)
 42    2. IN_MENU: The agent is currently in a menu state. (including PC, Name Entry, Pokedex, etc.)
 43    3. IN_BATTLE: The agent is currently in a battle state.
 44    """
 45
 46    FREE_ROAM = 0
 47    IN_DIALOGUE = 1
 48    IN_MENU = 2
 49    IN_BATTLE = 3
 50
 51
 52class PokemonStateParser(StateParser, ABC):
 53    """
 54    Base class for Pokemon game state parsers. Uses visual screen regions to parse game state.
 55    Defines common named screen regions and methods for determining game states such as being in battle, menu, or dialogue.
 56
 57    Can be used to determine the exact AgentState
 58    """
 59
 60    COMMON_REGIONS = [
 61        ("dialogue_bottom_right", 153, 135, 10, 10),
 62        ("menu_top_right", 152, 1, 6, 6),
 63        ("pc_top_left", 0, 0, 6, 6),
 64        ("battle_enemy_hp_text", 15, 17, 10, 5),
 65        ("battle_player_hp_text", 80, 73, 10, 5),
 66        ("battle_base_menu_top_left", 65, 96, 5, 5),
 67        ("battle_fight_options_top_right", 80, 64, 5, 5),
 68        ("battle_fight_options_cursor_on_top", 40, 103, 3, 3),
 69        ("dialogue_choice_bottom_right", 153, 87, 6, 6),
 70        ("name_entity_top_left", 0, 32, 6, 6),
 71        ("player_card_middle", 56, 70, 6, 6),
 72        ("map_bottom_right", 140, 130, 10, 10),
 73    ]
 74    """ List of common named screen regions for Pokemon games. 
 75    - dialogue_bottom_right: Bottom right of dialogue box when interacting with NPCs, signs, etc. Speak to an NPC to capture this.
 76    
 77    - menu_top_right: Top right of the screen when the player start menu is open. Open the start menu to capture this.
 78    
 79    - pc_top_left: Top left of the screen when the PC is open. Open the PC to capture this.
 80    
 81    - battle_enemy_hp_text: Region showing the text 'HP' for the enemy Pokémon in battle. Engage in a battle to capture this.
 82    
 83    - battle_player_hp_text: Region showing the text 'HP' for the player's Pokémon in battle. Engage in a battle to capture this.
 84    
 85    - battle_base_menu_top_left: Top left of the battle base menu. Engage in a battle to capture this.
 86
 87    - battle_fight_options_top_right: Top right of the fight options menu in battle. Engage in a battle and open the fight options to capture this.
 88
 89    - battle_fight_options_cursor_on_top: Region showing the cursor on the top attack option in the fight options menu. Engage in a battle, open the fight options and move the cursor to the top option to capture this.
 90    
 91    - dialogue_choice_bottom_right: Bottom right of the choice dialogue box when answering choice questions (e.g. Yes/No prompts). Trigger a choice dialogue to capture this (e.g. confirmation of starter choice)
 92    
 93    - name_entity_top_left: Top left of the screen when naming a character or Pokémon. Catch a pokemon and give it a nickname to capture this.
 94    
 95    - player_card_middle: Middle of the player card screen. Go to this from start menu -> player name
 96    
 97    - map_bottom_right: Bottom right of the map screen when the town map is open.  Open the town map to capture this.
 98    """
 99
100    COMMON_MULTI_TARGET_REGIONS = [
101        ("screen", 0, 0, 150, 140),
102        ("dialogue_box_middle", 10, 105, 120, 30),
103        ("dialogue_box_full", 5, 100, 150, 40),
104        ("screen_bottom_half", 5, 70, 150, 70),
105        ("screen_quadrant_1", 85, 0, 60, 60),
106        ("screen_quadrant_2", 0, 0, 60, 60),
107        ("screen_quadrant_3", 0, 70, 60, 70),
108        ("screen_quadrant_4", 85, 70, 60, 70),
109        ("screen_middle", 65, 55, 20, 20),
110    ]
111    """ List of common multi-target named screen regions for Pokemon games.
112    
113    - screen: Most of the screen except for the very edges. Useful for general state parsing.
114    - dialogue_box_middle: Middle of the dialogue box, but not on that spot where the blinking arrow cursor appears. Useful for catching particular dialogues.
115    - dialogue_box_full: Full dialogue box area, is useful to capture for OCR purposes.
116    - screen_bottom_half: Bottom half of the screen, useful for OCR of dialogue and other text.
117    - screen_quadrant_1: Top right quadrant of the screen.
118    - screen_quadrant_2: Top left quadrant of the screen.
119    - screen_quadrant_3: Bottom left quadrant of the screen.
120    - screen_quadrant_4: Bottom right quadrant of the screen.
121    - screen_middle: Middle of the screen.
122    """
123
124    COMMON_MULTI_TARGETS = {
125        "dialogue_box_middle": [
126            "got_away_safely",
127            "cannot_escape",
128            "cannot_run_from_trainer",
129            "no_pp_for_move",
130        ],
131        "menu_box_strip": ["cursor_on_options", "cursor_on_pokedex"],
132    }
133    """ Common multi-targets for the common multi-target named screen regions. 
134    - dialogue_box_middle:
135        - got_away_safely: Run successfully from a wild battle.
136        - cannot_escape: Fail to run from a wild Pokemon
137        - cannot_run_from_trainer: Try to run from a trainer battle and get an error message
138        - no_pp_for_move: Try to use a move with no PP remaining.
139    - menu_box_strip:
140        - cursor_on_options: Cursor is on the options in the start menu. This is vital to prevent agents from changing the text frame option. 
141        - cursor_on_pokedex: Cursor is on the Pokedex in the start menu.
142
143    """
144
145    def __init__(
146        self,
147        variant: str,
148        pyboy: PyBoy,
149        parameters: dict,
150        additional_named_screen_region_details: List[
151            Tuple[str, int, int, int, int]
152        ] = [],
153        additional_multi_target_named_screen_region_details: List[
154            Tuple[str, int, int, int, int]
155        ] = [],
156        override_multi_targets: Dict[str, List[str]] = {},
157    ):
158        """
159        Initializes the PokemonStateParser.
160        Args:
161            variant (str): The variant of the Pokemon game.
162            pyboy (PyBoy): The PyBoy emulator instance.
163            parameters (dict): Configuration parameters for the emulator.
164            additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
165            additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
166            override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
167        """
168        verify_parameters(parameters)
169        regions = _get_proper_regions(
170            override_regions=additional_named_screen_region_details,
171            base_regions=self.COMMON_REGIONS,
172        )
173        self.variant = variant
174        if f"{variant}_rom_data_path" not in parameters:
175            log_error(
176                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/pokemon_red_vars.yaml for an example",
177                parameters,
178            )
179        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
180        """ Path to the ROM data directory for the specific Pokemon variant."""
181        captures_dir = self.rom_data_path + "/captures/"
182        named_screen_regions = []
183        for region_name, x, y, w, h in regions:
184            region = NamedScreenRegion(
185                region_name,
186                x,
187                y,
188                w,
189                h,
190                parameters=parameters,
191                target_path=os.path.join(captures_dir, region_name),
192            )
193            named_screen_regions.append(region)
194        multi_target_regions = _get_proper_regions(
195            override_regions=additional_multi_target_named_screen_region_details,
196            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
197        )
198        multi_target_region_names = [region[0] for region in multi_target_regions]
199        multi_targets = self.COMMON_MULTI_TARGETS.copy()
200        for key in override_multi_targets:
201            if key in multi_targets:
202                multi_targets[key].extend(override_multi_targets[key])
203            else:
204                multi_targets[key] = override_multi_targets[key]
205        multi_target_provided_region_names = list(multi_targets.keys())
206        if not set(multi_target_provided_region_names).issubset(
207            set(multi_target_region_names)
208        ):
209            log_error(
210                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {multi_target_provided_region_names}, Defined: {multi_target_region_names}",
211                parameters,
212            )
213        for region_name, x, y, w, h in multi_target_regions:
214            region_target_paths = {}
215            subdir = captures_dir + f"/{region_name}/"
216            for target_name in multi_targets.get(region_name, []):
217                region_target_paths[target_name] = os.path.join(subdir, target_name)
218            region = NamedScreenRegion(
219                region_name,
220                x,
221                y,
222                w,
223                h,
224                parameters=parameters,
225                multi_target_paths=region_target_paths,
226            )
227            named_screen_regions.append(region)
228        super().__init__(pyboy, parameters, named_screen_regions)
229
230    @abstractmethod
231    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
232        """
233        Determines if the Pokedex is currently open.
234        Args:
235            current_screen (np.ndarray): The current screen frame from the emulator.
236
237        Returns:
238            bool: True if the Pokedex is open, False otherwise.
239        """
240        raise NotImplementedError
241
242    @staticmethod
243    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
244        """
245        Determines if the Pokemon menu is currently open.
246        Args:
247            current_screen (np.ndarray): The current screen frame from the emulator.
248        Returns:
249            bool: True if the Pokemon menu is open, False otherwise.
250        """
251        raise NotImplementedError
252
253    def is_hovering_over_options_in_menu(self, current_screen: np.ndarray) -> bool:
254        """
255        Determines if the cursor is currently hovering over options in the menu. Typically we force the agent off this state.
256
257        # TODO: This method currently only has one multi_target screen checked, cursor_on_options, which is screen captured AFTER the player gets the pokedex
258        The problem is the menu layout is slightly different before the pokedex is acquired, making the check useless before that point.
259        To fix this, we need to capture another target for the same multi_target region (e.g. cursor_on_options_no_pokedex) and check for both here.
260        But I am lazy, and so will hope this is not needed.
261
262        Args:
263            current_screen (np.ndarray): The current screen frame from the emulator.
264
265        Returns:
266            bool: True if hovering over options, False otherwise.
267        """
268        return self.named_region_matches_multi_target(
269            current_screen, "menu_box_strip", "cursor_on_options"
270        )
271
272    def is_in_battle(self, current_screen: np.ndarray) -> bool:
273        """
274        Determines if the player is currently in a battle by checking for battle HP text regions.
275
276        Args:
277            current_screen (np.ndarray): The current screen frame from the emulator.
278
279        Returns:
280            bool: True if in battle, False otherwise.
281        """
282        if self.is_in_fight_bag(current_screen):
283            return False  # Then, is in menu
284        enemy_hp_match = self.named_region_matches_target(
285            current_screen, "battle_enemy_hp_text"
286        )
287        player_hp_match = self.named_region_matches_target(
288            current_screen, "battle_player_hp_text"
289        )
290        return enemy_hp_match or player_hp_match
291
292    def is_in_base_battle_menu(self, current_screen: np.ndarray) -> bool:
293        """
294        Determines if the player is currently in the base battle menu by checking for the battle base menu top left region.
295
296        Args:
297            current_screen (np.ndarray): The current screen frame from the emulator.
298        Returns:
299            bool: True if in the base battle menu, False otherwise.
300        """
301        return self.named_region_matches_target(
302            current_screen, "battle_base_menu_top_left"
303        )
304
305    def is_in_run_screen(self, current_screen: np.ndarray) -> bool:
306        """
307        Determines if the player is currently in the run screen by checking for the battle base menu top left region.
308        Args:
309            current_screen (np.ndarray): The current screen frame from the emulator.
310        Returns:
311            bool: True if in the run screen, False otherwise.
312        """
313        got_away_safely = self.named_region_matches_multi_target(
314            current_screen, "dialogue_box_middle", "got_away_safely"
315        )
316        cannot_escape = self.named_region_matches_multi_target(
317            current_screen, "dialogue_box_middle", "cannot_escape"
318        )
319        cannot_run_from_trainer = self.named_region_matches_multi_target(
320            current_screen, "dialogue_box_middle", "cannot_run_from_trainer"
321        )
322        return got_away_safely or cannot_escape or cannot_run_from_trainer
323
324    def is_in_fight_options_menu(self, current_screen: np.ndarray) -> bool:
325        """
326        Determines if the player is currently in the fight options menu by checking for the battle fight options top right region.
327
328        Args:
329            current_screen (np.ndarray): The current screen frame from the emulator.
330        Returns:
331            bool: True if in the fight options menu, False otherwise.
332        """
333        return self.named_region_matches_target(
334            current_screen, "battle_fight_options_top_right"
335        )
336
337    def is_on_top_attack_option(self, current_screen: np.ndarray) -> bool:
338        """
339        Determines if the cursor is currently on the top attack option in the fight options menu.
340
341        Args:
342            current_screen (np.ndarray): The current screen frame from the emulator.
343        Returns:
344            bool: True if the cursor is on the top attack option, False otherwise.
345        """
346        return self.named_region_matches_target(
347            current_screen, "battle_fight_options_cursor_on_top"
348        )
349
350    def tried_no_pp_move(self, current_screen: np.ndarray) -> bool:
351        """
352        Determines if the player tried to use a move with no PP by checking for the no_pp_for_move target in the dialogue box middle region.
353
354        Args:
355            current_screen (np.ndarray): The current screen frame from the emulator.
356        Returns:
357            bool: True if the player tried to use a move with no PP, False otherwise.
358        """
359        return self.named_region_matches_multi_target(
360            current_screen, "dialogue_box_middle", "no_pp_for_move"
361        )
362
363    @abstractmethod
364    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
365        raise NotImplementedError
366
367    def is_on_top_menu_option(self, current_screen: np.ndarray) -> bool:
368        """
369        Determines if the cursor is currently on the top option in the start menu.
370
371        Args:
372            current_screen (np.ndarray): The current screen frame from the emulator.
373        Returns:
374            bool: True if the cursor is on the top menu option, False otherwise.
375        """
376        return self.named_region_matches_multi_target(
377            current_screen, "menu_box_strip", "cursor_on_pokedex"
378        )
379
380    def is_in_menu(
381        self, current_screen: np.ndarray, trust_previous: bool = False
382    ) -> bool:
383        """
384        Determines if any form of menu (or choice dialogue) is currently open by checking a variety of screen regions.
385
386        Args:
387            current_screen (np.ndarray): The current screen frame from the emulator.
388            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
389
390        Returns:
391            bool: True if the menu is open, False otherwise.
392        """
393        any_match_regions = [
394            "menu_top_right",
395            "dialogue_choice_bottom_right",
396            "pc_top_left",
397            "name_entity_top_left",
398            "player_card_middle",
399            "map_bottom_right",
400            "pokemon_list_hp_text",  # This one is defined in each subclass as the position varies slightly between games
401        ]
402        if not trust_previous:
403            if self.is_in_battle(current_screen):
404                return False
405        if self.is_in_fight_bag(current_screen):
406            return True
407        if self.is_in_pokedex(current_screen):
408            return True
409        if self.is_in_pokemon_menu(current_screen):
410            return True
411        for region_name in any_match_regions:
412            if self.named_region_matches_target(current_screen, region_name):
413                return True
414        return False
415
416    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
417        """
418        Determines if a dialogue box is currently open by checking the dialogue bottom right region.
419        Args:
420            current_screen (np.ndarray): The current screen frame from the emulator.
421        Returns:
422            bool: True if a dialogue box is open, False otherwise.
423        """
424        return self.named_region_matches_target(current_screen, "dialogue_bottom_right")
425
426    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
427        box = self.capture_named_region(
428            current_frame=current_screen, name="dialogue_box_full"
429        )
430        perc_lt_255 = np.mean(box < 255)
431        if perc_lt_255 < 0.082:  # Empirical threshold
432            return True
433        return False
434
435    def is_in_dialogue(
436        self, current_screen: np.ndarray, trust_previous: bool = False
437    ) -> bool:
438        """
439        Determines if the player is currently in a dialogue state or reading text from a sign, interacting with an object etc.
440        Essentially anything that causes text to appear at the bottom of the screen that isn't a battle, pc or menu.
441
442        Args:
443            current_screen (np.ndarray): The current screen frame from the emulator.
444            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
445
446        Returns:
447            bool: True if in dialogue, False otherwise.
448        """
449        if trust_previous:
450            return self.named_region_matches_target(
451                current_screen, "dialogue_bottom_right"
452            )
453        if self.is_in_battle(current_screen):
454            return False
455        elif self.is_in_menu(current_screen):
456            return False
457        else:
458            return self.dialogue_box_open(current_screen)
459
460    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
461        """
462        Determines the current agent state based on the screen.
463
464        Uses trust_previous to optimize checks.
465
466        Args:
467            current_screen (np.ndarray): The current screen frame from the emulator.
468
469        Returns:
470            AgentState: The current agent state.
471        """
472        if self.is_in_battle(current_screen):
473            return AgentState.IN_BATTLE
474        elif self.is_in_menu(current_screen, trust_previous=True):
475            return AgentState.IN_MENU
476        elif self.is_in_dialogue(current_screen, trust_previous=True):
477            return AgentState.IN_DIALOGUE
478        else:
479            return AgentState.FREE_ROAM
480
481
482class BasePokemonRedStateParser(PokemonStateParser, ABC):
483    """
484    Game state parser for all PokemonRed-based games.
485    """
486
487    REGIONS = [
488        ("pokedex_top_left", 7, 6, 12, 6),
489        ("pokedex_info_mid_left", 6, 71, 6, 6),
490        ("pokemon_list_hp_text", 32, 9, 10, 5),
491        ("pokemon_stats_line", 66, 55, 5, 5),
492        ("battle_bag_options_bottom_left", 32, 96, 5, 5),
493    ]
494    """ Additional named screen regions specific to Pokemon Red games.
495    - pokedex_top_left: Top left of the screen when the Pokedex is open. Open the Pokedex to capture this.
496    - pokedex_info_mid_left: Middle left of the screen when viewing a Pokémon's info in the Pokedex. Open a Pokémon's info in the Pokedex to capture this.
497    - pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
498    - pokemon_stats_line: A line in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
499    - battle_bag_options_bottom_left: The bottom left part of the menu when you open the items option from the battle menu. 
500    """
501
502    MULTI_TARGET_REGIONS = [
503        ("menu_box_strip", 89, 13, 5, 100),
504    ]
505    """ Additional multi-target named screen regions specific to Pokemon Red games. 
506    - menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states. 
507    """
508
509    def __init__(
510        self,
511        pyboy: PyBoy,
512        variant: str,
513        parameters: dict,
514        override_regions: List[Tuple[str, int, int, int, int]] = [],
515        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
516        override_multi_targets: Dict[str, List[str]] = {},
517    ):
518        self.REGIONS = _get_proper_regions(
519            override_regions=override_regions, base_regions=self.REGIONS
520        )
521        self.MULTI_TARGET_REGIONS = _get_proper_regions(
522            override_regions=override_multi_target_regions,
523            base_regions=self.MULTI_TARGET_REGIONS,
524        )
525        super().__init__(
526            variant=variant,
527            pyboy=pyboy,
528            parameters=parameters,
529            additional_named_screen_region_details=self.REGIONS,
530            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
531            override_multi_targets=override_multi_targets,
532        )
533
534    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
535        return self.named_region_matches_target(
536            current_screen, "pokedex_top_left"
537        ) or self.named_region_matches_target(current_screen, "pokedex_info_mid_left")
538
539    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
540        return self.named_region_matches_target(
541            current_screen, "pokemon_list_hp_text"
542        ) or self.named_region_matches_target(current_screen, "pokemon_stats_line")
543
544    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
545        return self.named_region_matches_target(
546            current_screen, "battle_bag_options_bottom_left"
547        )
548
549    def __repr__(self):
550        return f"<PokemonRedParser(variant={self.variant})>"
551
552
553class BasePokemonCrystalStateParser(PokemonStateParser, ABC):
554    """
555    Game state parser for all PokemonCrystal-based games.
556
557    TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it.
558    Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.
559    """
560
561    REGIONS = [
562        ("pokemon_list_hp_text", 87, 16, 10, 5),
563        ("pokedex_seen_text", 3, 88, 5, 5),
564        ("pokedex_info_height_text", 69, 57, 5, 5),
565        ("pokegear_top_left", 0, 0, 6, 6),
566        ("pokemon_stats_lvl_text", 113, 0, 5, 5),
567        ("bag_text", 18, 0, 6, 6),
568    ]
569    """ Additional named screen regions specific to Pokemon Crystal games.
570    - pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
571    - pokedex_seen_text: Region showing the 'SEEN' text in the Pokedex. Open the Pokedex to capture this.
572    - pokedex_info_height_text: Region showing the 'HEIGHT' text in the Pokedex info screen. Open a Pokémon's info in the Pokedex to capture this.
573    - pokegear_top_left: Top left of the screen when the Pokegear is open. Open the Pokegear to capture this.
574    - pokemon_stats_lvl_text: Region showing the 'LV' text in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
575    - bag_text: Top left of the screen when the Bag is open. Open the Bag to capture this.
576    """
577
578    MULTI_TARGET_REGIONS = [
579        ("menu_box_strip", 89, 13, 5, 120),
580    ]
581    """ Additional multi-target named screen regions specific to Pokemon Crystal games. 
582    - menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states. 
583    """
584
585    def __init__(
586        self,
587        pyboy: PyBoy,
588        variant: str,
589        parameters: dict,
590        override_regions: List[Tuple[str, int, int, int, int]] = [],
591        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
592        override_multi_targets: Dict[str, List[str]] = {},
593    ):
594        self.REGIONS = _get_proper_regions(
595            override_regions=override_regions, base_regions=self.REGIONS
596        )
597        self.MULTI_TARGET_REGIONS = _get_proper_regions(
598            override_regions=override_multi_target_regions,
599            base_regions=self.MULTI_TARGET_REGIONS,
600        )
601        super().__init__(
602            variant=variant,
603            pyboy=pyboy,
604            parameters=parameters,
605            additional_named_screen_region_details=self.REGIONS,
606            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
607            override_multi_targets=override_multi_targets,
608        )
609
610    def is_in_bag(self, current_screen: np.ndarray) -> bool:
611        """
612        Determines if the Bag is currently open.
613        """
614        return self.named_region_matches_target(current_screen, "bag_text")
615
616    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
617        """
618        For PokemonCrystal, this is just the same as in bag.
619        """
620        return self.is_in_bag(current_screen)
621
622    def is_in_pokegear(self, current_screen: np.ndarray) -> bool:
623        """
624        Determines if the Pokegear is currently open.
625        """
626        return self.named_region_matches_target(current_screen, "pokegear_top_left")
627
628    def is_in_pokedex(self, current_screen):
629        return self.named_region_matches_target(
630            current_screen, "pokedex_seen_text"
631        ) or self.named_region_matches_target(
632            current_screen, "pokedex_info_height_text"
633        )
634
635    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
636        return self.named_region_matches_target(
637            current_screen, "pokemon_stats_lvl_text"
638        ) or self.named_region_matches_target(current_screen, "pokemon_list_hp_text")
639
640    def is_in_menu(
641        self, current_screen: np.ndarray, trust_previous: bool = False
642    ) -> bool:
643        # This technically mistakenly also flags when someone calls you on the pokegear, but that's probably fine for now.
644        # Could change by adding special region for pokegear_call_top_left and overriding is_in_menu and is_in_dialogue.
645        result = super().is_in_menu(current_screen, trust_previous=trust_previous)
646        if result:
647            return True
648        if self.is_in_bag(current_screen):
649            return True
650        if self.is_in_pokegear(current_screen):
651            return True
652        # Finally, when transitioning to PC screens, maps etc, the screen goes white. Catch that here.
653        # print(f"Checking for white screen... Pixel stats: {np.min(current_screen)}, {np.max(current_screen)}, {np.mean(current_screen)}") # I get 248, 248, 248.0
654        # The following doesn't catch all white screens (e.g town maps), but does catch some important ones like PC screens.
655        if np.mean(current_screen) > 245 and np.min(current_screen) > 245:
656            return True
657        elif np.mean(current_screen) > 210:  # screen coming down from full white
658            return True
659        else:
660            return False
661
662    def __repr__(self):
663        return f"<PokemonCrystalParser(variant={self.variant})>"
664
665
666class PokemonRedStateParser(BasePokemonRedStateParser):
667    def __init__(self, pyboy, parameters):
668        override_multi_targets = {
669            "dialogue_box_middle": [
670                "picked_charmander",
671                "picked_bulbasaur",
672                "picked_squirtle",
673                "talk_bill_complete",
674                "pick_up_pokeball_starting",  # is tied to player character name being You
675                "trainers_tips_sign",
676                "cinnabar_gym_aid_complete",
677                "talk_cinnabar_monk",
678                "defeated_brock",
679                "defeated_lass",
680                "caught_pidgey",
681                "caught_pikachu",
682                "used_potion_on_charmander",
683            ],
684            "screen_bottom_half": [
685                "viridian_pokemon_center_entrance",
686                "mt_moon_entrance",
687                "bought_potion_at_pewter_pokemart",
688            ],
689            "screen_middle": [
690                "outside_viridian_center_from_left",
691                "outside_viridian_center_from_right",
692            ],
693        }
694        super().__init__(
695            pyboy,
696            variant="pokemon_red",
697            parameters=parameters,
698            override_multi_targets=override_multi_targets,
699        )
700
701
702class PokemonBrownStateParser(BasePokemonRedStateParser):
703    def __init__(self, pyboy, parameters):
704        super().__init__(pyboy, variant="pokemon_brown", parameters=parameters)
705
706
707class PokemonStarBeastsStateParser(BasePokemonRedStateParser):
708    def __init__(self, pyboy, parameters):
709        override_regions = [
710            ("pokemon_list_hp_text", 33, 10, 4, 4),
711            ("battle_enemy_hp_text", 6, 15, 5, 5),
712            ("battle_player_hp_text", 88, 72, 5, 5),
713        ]
714        super().__init__(
715            pyboy,
716            variant="pokemon_starbeasts",
717            parameters=parameters,
718            override_regions=override_regions,
719        )
720
721
722class PokemonStarBeastsCometStateParser(BasePokemonRedStateParser):
723    def __init__(self, pyboy, parameters):
724        #override_regions = [
725            #("pokemon_list_hp_text", 33, 10, 4, 4),
726            #("battle_enemy_hp_text", 6, 15, 5, 5),
727            #("battle_player_hp_text", 88, 72, 5, 5),
728        #]
729        super().__init__(
730            pyboy,
731            variant="pokemon_starbeasts_comet",
732            parameters=parameters,
733            #override_regions=override_regions,
734        )
735
736
737class PokemonCrystalStateParser(BasePokemonCrystalStateParser):
738    def __init__(self, pyboy, parameters):
739        super().__init__(pyboy, variant="pokemon_crystal", parameters=parameters)
740
741
742class PokemonPrismStateParser(BasePokemonCrystalStateParser):
743    def __init__(self, pyboy, parameters):
744        override_regions = [("player_card_middle", 25, 58, 5, 5)]
745        super().__init__(
746            pyboy,
747            variant="pokemon_prism",
748            parameters=parameters,
749            override_regions=override_regions,
750        )
751
752
753class PokemonFoolsGoldStateParser(BasePokemonCrystalStateParser):
754    def __init__(self, pyboy, parameters):
755        override_regions = [("pokedex_info_height_text", 66, 65, 5, 5)]
756        super().__init__(
757            pyboy,
758            variant="pokemon_fools_gold",
759            parameters=parameters,
760            override_regions=override_regions,
761        )
762
763
764"""
765The below code shows how to add domain information into the game state parser and read from memory addresses to get descriptive state information. 
766
767This is not actually used in any of the current environments, but is left here to show that if you want to bake in more domain knowledge and create explicit reward schedules etc., you can read the information required to do so in this class. 
768"""
769
770
771class MemoryBasedPokemonRedStateParser(PokemonRedStateParser):
772    """
773    Game state parser for Pokemon Red. Uses memory addresses to parse game state.
774    Can be used to reproduce https://github.com/PWhiddy/PokemonRedExperiments/ (v2) and facilitates reward engineering based on memory states.
775    """
776
777    _PAD = 20
778    _GLOBAL_MAP_SHAPE = (444 + _PAD * 2, 436 + _PAD * 2)
779    _MAP_ROW_OFFSET = _PAD
780    _MAP_COL_OFFSET = _PAD
781
782    def __init__(self, pyboy, parameters):
783        """
784        Initializes the Pokemon Red game state parser.
785
786        Args:
787            pyboy: An instance of the PyBoy emulator.
788            parameters: A dictionary of parameters for configuration.
789        """
790        super().__init__(pyboy, parameters=parameters)
791        events_location = parameters["pokemon_red_rom_data_path"] + "/events.json"
792        with open(events_location) as f:
793            event_slots = json.load(f)
794        event_slots = event_slots
795        event_names = {v: k for k, v in event_slots.items() if not v[0].isdigit()}
796        beat_opponent_events = bidict()
797
798        def _pop(d, keys):
799            for key in keys:
800                if key in d:
801                    d.pop(key, None)
802
803        pop_queue = []
804        for name, slot in event_names.items():
805            if name.startswith("Beat "):
806                beat_opponent_events[name.replace("Beat ", "")] = slot
807                pop_queue.append(name)
808        _pop(event_names, pop_queue)
809        self.defeated_opponent_events = beat_opponent_events
810        """Events related to beating specific opponents. E.g. Beat Brock"""
811        tms_obtained_events = bidict()
812        pop_queue = []
813        for name, slot in event_names.items():
814            if name.startswith("Got Tm"):
815                tms_obtained_events[name.replace("Got ", "").strip()] = slot
816                pop_queue.append(name)
817        _pop(event_names, pop_queue)
818        self.tms_obtained_events = tms_obtained_events
819        """Events related to obtaining specific TMs. E.g. Got Tm01"""
820        hm_obtained_events = bidict()
821        pop_queue = []
822        for name, slot in event_names.items():
823            if name.startswith("Got Hm"):
824                hm_obtained_events[name.replace("Got ", "").strip()] = slot
825                pop_queue.append(name)
826        _pop(event_names, pop_queue)
827        self.hm_obtained_events = hm_obtained_events
828        """Events related to obtaining specific HMs. E.g. Got Hm01"""
829        passed_badge_check_events = bidict()
830        pop_queue = []
831        for name, slot in event_names.items():
832            if name.startswith("Passed ") and "badge" in name:
833                passed_badge_check_events[
834                    name.replace("Passed ", "").replace(" Check", "").strip()
835                ] = slot
836                pop_queue.append(name)
837        _pop(event_names, pop_queue)
838        self.passed_badge_check_events = passed_badge_check_events
839        """Events related to passing badge checks. E.g. Passed Boulder badge check. These will only be relevant to enter Victory Road."""
840        self.key_items_obtained_events = bidict()
841        """Events related to obtaining key items. E.g. Got Bicycle"""
842        pop_queue = []
843        for name, slot in event_names.items():
844            if name.startswith("Got "):
845                self.key_items_obtained_events[name.replace("Got ", "").strip()] = slot
846                pop_queue.append(name)
847        _pop(event_names, pop_queue)
848        self.map_events = {
849            "Cinnabar Gym": bidict(),
850            "Victory Road": bidict(),
851            "Silph Co": bidict(),
852            "Seafoam Islands": bidict(),
853        }
854        """Events related to specific map events like unlocking gates or moving boulders."""
855        for name, slot in event_names.items():
856            if name.startswith("Cinnabar Gym Gate") and name.endswith("Unlocked"):
857                self.map_events["Cinnabar Gym"][name] = slot
858                pop_queue.append(name)
859            elif name.startswith("Victory Road") and "Boulder On" in name:
860                self.map_events["Victory Road"][name] = slot
861                pop_queue.append(name)
862            elif name.startswith("Silph Co") and "Unlocked" in name:
863                self.map_events["Silph Co"][name] = slot
864                pop_queue.append(name)
865            elif name.startswith("Seafoam"):
866                self.map_events["Seafoam Islands"][name] = slot
867                pop_queue.append(name)
868        _pop(event_names, pop_queue)
869        self.cutscene_events = bidict()
870        """ Flags for cutscene based events (I think, lol). """
871
872        cutscenes = [
873            "Event 001",
874            "Daisy Walking",
875            "Pokemon Tower Rival On Left",
876            "Seel Fan Boast",
877            "Pikachu Fan Boast",
878            "Lab Handing Over Fossil Mon",
879            "Route22 Rival Wants Battle",
880        ]  # my best guess, need to verify, Silph Co Receptionist At Desk? Autowalks?
881        pop_queue = []
882        for name, slot in event_names.items():
883            if name in cutscenes:
884                self.cutscene_events[name] = slot
885                pop_queue.append(name)
886        _pop(event_names, pop_queue)
887        self.special_events = bidict(event_names)
888        """ All other events not categorized elsewhere."""
889
890        MAP_PATH = parameters["pokemon_red_rom_data_path"] + "/map_data.json"
891        with open(MAP_PATH) as map_data:
892            MAP_DATA = json.load(map_data)["regions"]
893        self._MAP_DATA = {int(e["id"]): e for e in MAP_DATA}
894
895    def get_map_name(self, map_n: int) -> Optional[str]:
896        """
897        Gets the name of the map given its identifier.
898        Args:
899            map_n (int): Map identifier.
900        Returns:
901            Optional[str]: Name of the map if found, None otherwise.
902        """
903        try:
904            return self._MAP_DATA[map_n]["name"]
905        except KeyError:
906            return None
907
908    def local_to_global(self, r: int, c: int, map_n: int) -> Tuple[int, int]:
909        """
910        Converts local map coordinates to global map coordinates.
911        Args:
912            r (int): Local row coordinate.
913            c (int): Local column coordinate.
914            map_n (int): Map identifier.
915        Returns:
916            (int, int): Global (row, column) coordinates.
917        """
918        try:
919            (
920                map_x,
921                map_y,
922            ) = self._MAP_DATA[
923                map_n
924            ]["coordinates"]
925            gy = r + map_y + self._MAP_ROW_OFFSET
926            gx = c + map_x + self._MAP_COL_OFFSET
927            if (
928                0 <= gy < self._GLOBAL_MAP_SHAPE[0]
929                and 0 <= gx < self._GLOBAL_MAP_SHAPE[1]
930            ):
931                return gy, gx
932            print(
933                f"coord out of bounds! global: ({gx}, {gy}) game: ({r}, {c}, {map_n})"
934            )
935            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2
936        except KeyError:
937            print(f"Map id {map_n} not found in map_data.json.")
938            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2
939
940    def get_opponents_defeated(self) -> Set[str]:
941        """
942        Returns a set of all defeated opponents. This function isn't actually used in any current environments, but is left here to show how to read game state information.
943        Similar functions can be created to read obtained TMs, HMs, key items, passed badge checks, etc.
944
945        Returns:
946            Set[str]: A set of names of defeated opponents.
947        """
948        return self.get_raised_flags(self.defeated_opponent_events)
949
950    def get_facing_direction(self) -> Tuple[int, int]:
951        """
952        Gets the direction the player is facing.
953        Returns:
954            (int, int): Tuple representing the direction vector (dy, dx).
955        """
956        direction = self.read_m(0xD52A)
957        if direction == 1:
958            return (0, 1)  # Right
959        elif direction == 2:
960            return (0, -1)  # Left
961        elif direction == 4:
962            return (1, 0)  # Down
963        else:
964            return (-1, 0)  # Up
965
966    def get_local_coords(self) -> Tuple[int, int, int]:
967        """
968        Gets the local game coordinates (x, y, map number).
969        Returns:
970            (int, int, int): Tuple containing (x, y, map number).
971        """
972        return (self.read_m(0xD362), self.read_m(0xD361), self.read_m(0xD35E))
973
974    def get_global_coords(self):
975        """
976        Gets the global coordinates of the player.
977        Returns:
978            (int, int): Tuple containing (global y, global x) coordinates.
979        """
980        x_pos, y_pos, map_n = self.get_local_coords()
981        return self.local_to_global(y_pos, x_pos, map_n)
982
983    def get_badges(self) -> np.array:
984        """
985        Gets the player's badges as a binary array.
986        Returns:
987            np.array: Array of 8 binary values representing whether the player has obtained each of the badges.
988        """
989        # or  self.bit_count(self.read_m(0xD356))
990        return np.array(
991            [int(bit) for bit in f"{self.read_m(0xD356):08b}"], dtype=np.int8
992        )
class AgentState(enum.Enum):
39class AgentState(Enum):
40    """
41    0. FREE_ROAM: The agent is freely roaming the game world.
42    1. IN_DIALOGUE: The agent is currently in a dialogue state. (including reading signs, talking to NPCs, etc.)
43    2. IN_MENU: The agent is currently in a menu state. (including PC, Name Entry, Pokedex, etc.)
44    3. IN_BATTLE: The agent is currently in a battle state.
45    """
46
47    FREE_ROAM = 0
48    IN_DIALOGUE = 1
49    IN_MENU = 2
50    IN_BATTLE = 3
  1. FREE_ROAM: The agent is freely roaming the game world.
  2. IN_DIALOGUE: The agent is currently in a dialogue state. (including reading signs, talking to NPCs, etc.)
  3. IN_MENU: The agent is currently in a menu state. (including PC, Name Entry, Pokedex, etc.)
  4. IN_BATTLE: The agent is currently in a battle state.
FREE_ROAM = <AgentState.FREE_ROAM: 0>
IN_DIALOGUE = <AgentState.IN_DIALOGUE: 1>
IN_MENU = <AgentState.IN_MENU: 2>
IN_BATTLE = <AgentState.IN_BATTLE: 3>
class PokemonStateParser(gameboy_worlds.emulation.parser.StateParser, abc.ABC):
 53class PokemonStateParser(StateParser, ABC):
 54    """
 55    Base class for Pokemon game state parsers. Uses visual screen regions to parse game state.
 56    Defines common named screen regions and methods for determining game states such as being in battle, menu, or dialogue.
 57
 58    Can be used to determine the exact AgentState
 59    """
 60
 61    COMMON_REGIONS = [
 62        ("dialogue_bottom_right", 153, 135, 10, 10),
 63        ("menu_top_right", 152, 1, 6, 6),
 64        ("pc_top_left", 0, 0, 6, 6),
 65        ("battle_enemy_hp_text", 15, 17, 10, 5),
 66        ("battle_player_hp_text", 80, 73, 10, 5),
 67        ("battle_base_menu_top_left", 65, 96, 5, 5),
 68        ("battle_fight_options_top_right", 80, 64, 5, 5),
 69        ("battle_fight_options_cursor_on_top", 40, 103, 3, 3),
 70        ("dialogue_choice_bottom_right", 153, 87, 6, 6),
 71        ("name_entity_top_left", 0, 32, 6, 6),
 72        ("player_card_middle", 56, 70, 6, 6),
 73        ("map_bottom_right", 140, 130, 10, 10),
 74    ]
 75    """ List of common named screen regions for Pokemon games. 
 76    - dialogue_bottom_right: Bottom right of dialogue box when interacting with NPCs, signs, etc. Speak to an NPC to capture this.
 77    
 78    - menu_top_right: Top right of the screen when the player start menu is open. Open the start menu to capture this.
 79    
 80    - pc_top_left: Top left of the screen when the PC is open. Open the PC to capture this.
 81    
 82    - battle_enemy_hp_text: Region showing the text 'HP' for the enemy Pokémon in battle. Engage in a battle to capture this.
 83    
 84    - battle_player_hp_text: Region showing the text 'HP' for the player's Pokémon in battle. Engage in a battle to capture this.
 85    
 86    - battle_base_menu_top_left: Top left of the battle base menu. Engage in a battle to capture this.
 87
 88    - battle_fight_options_top_right: Top right of the fight options menu in battle. Engage in a battle and open the fight options to capture this.
 89
 90    - battle_fight_options_cursor_on_top: Region showing the cursor on the top attack option in the fight options menu. Engage in a battle, open the fight options and move the cursor to the top option to capture this.
 91    
 92    - dialogue_choice_bottom_right: Bottom right of the choice dialogue box when answering choice questions (e.g. Yes/No prompts). Trigger a choice dialogue to capture this (e.g. confirmation of starter choice)
 93    
 94    - name_entity_top_left: Top left of the screen when naming a character or Pokémon. Catch a pokemon and give it a nickname to capture this.
 95    
 96    - player_card_middle: Middle of the player card screen. Go to this from start menu -> player name
 97    
 98    - map_bottom_right: Bottom right of the map screen when the town map is open.  Open the town map to capture this.
 99    """
100
101    COMMON_MULTI_TARGET_REGIONS = [
102        ("screen", 0, 0, 150, 140),
103        ("dialogue_box_middle", 10, 105, 120, 30),
104        ("dialogue_box_full", 5, 100, 150, 40),
105        ("screen_bottom_half", 5, 70, 150, 70),
106        ("screen_quadrant_1", 85, 0, 60, 60),
107        ("screen_quadrant_2", 0, 0, 60, 60),
108        ("screen_quadrant_3", 0, 70, 60, 70),
109        ("screen_quadrant_4", 85, 70, 60, 70),
110        ("screen_middle", 65, 55, 20, 20),
111    ]
112    """ List of common multi-target named screen regions for Pokemon games.
113    
114    - screen: Most of the screen except for the very edges. Useful for general state parsing.
115    - dialogue_box_middle: Middle of the dialogue box, but not on that spot where the blinking arrow cursor appears. Useful for catching particular dialogues.
116    - dialogue_box_full: Full dialogue box area, is useful to capture for OCR purposes.
117    - screen_bottom_half: Bottom half of the screen, useful for OCR of dialogue and other text.
118    - screen_quadrant_1: Top right quadrant of the screen.
119    - screen_quadrant_2: Top left quadrant of the screen.
120    - screen_quadrant_3: Bottom left quadrant of the screen.
121    - screen_quadrant_4: Bottom right quadrant of the screen.
122    - screen_middle: Middle of the screen.
123    """
124
125    COMMON_MULTI_TARGETS = {
126        "dialogue_box_middle": [
127            "got_away_safely",
128            "cannot_escape",
129            "cannot_run_from_trainer",
130            "no_pp_for_move",
131        ],
132        "menu_box_strip": ["cursor_on_options", "cursor_on_pokedex"],
133    }
134    """ Common multi-targets for the common multi-target named screen regions. 
135    - dialogue_box_middle:
136        - got_away_safely: Run successfully from a wild battle.
137        - cannot_escape: Fail to run from a wild Pokemon
138        - cannot_run_from_trainer: Try to run from a trainer battle and get an error message
139        - no_pp_for_move: Try to use a move with no PP remaining.
140    - menu_box_strip:
141        - cursor_on_options: Cursor is on the options in the start menu. This is vital to prevent agents from changing the text frame option. 
142        - cursor_on_pokedex: Cursor is on the Pokedex in the start menu.
143
144    """
145
146    def __init__(
147        self,
148        variant: str,
149        pyboy: PyBoy,
150        parameters: dict,
151        additional_named_screen_region_details: List[
152            Tuple[str, int, int, int, int]
153        ] = [],
154        additional_multi_target_named_screen_region_details: List[
155            Tuple[str, int, int, int, int]
156        ] = [],
157        override_multi_targets: Dict[str, List[str]] = {},
158    ):
159        """
160        Initializes the PokemonStateParser.
161        Args:
162            variant (str): The variant of the Pokemon game.
163            pyboy (PyBoy): The PyBoy emulator instance.
164            parameters (dict): Configuration parameters for the emulator.
165            additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
166            additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
167            override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
168        """
169        verify_parameters(parameters)
170        regions = _get_proper_regions(
171            override_regions=additional_named_screen_region_details,
172            base_regions=self.COMMON_REGIONS,
173        )
174        self.variant = variant
175        if f"{variant}_rom_data_path" not in parameters:
176            log_error(
177                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/pokemon_red_vars.yaml for an example",
178                parameters,
179            )
180        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
181        """ Path to the ROM data directory for the specific Pokemon variant."""
182        captures_dir = self.rom_data_path + "/captures/"
183        named_screen_regions = []
184        for region_name, x, y, w, h in regions:
185            region = NamedScreenRegion(
186                region_name,
187                x,
188                y,
189                w,
190                h,
191                parameters=parameters,
192                target_path=os.path.join(captures_dir, region_name),
193            )
194            named_screen_regions.append(region)
195        multi_target_regions = _get_proper_regions(
196            override_regions=additional_multi_target_named_screen_region_details,
197            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
198        )
199        multi_target_region_names = [region[0] for region in multi_target_regions]
200        multi_targets = self.COMMON_MULTI_TARGETS.copy()
201        for key in override_multi_targets:
202            if key in multi_targets:
203                multi_targets[key].extend(override_multi_targets[key])
204            else:
205                multi_targets[key] = override_multi_targets[key]
206        multi_target_provided_region_names = list(multi_targets.keys())
207        if not set(multi_target_provided_region_names).issubset(
208            set(multi_target_region_names)
209        ):
210            log_error(
211                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {multi_target_provided_region_names}, Defined: {multi_target_region_names}",
212                parameters,
213            )
214        for region_name, x, y, w, h in multi_target_regions:
215            region_target_paths = {}
216            subdir = captures_dir + f"/{region_name}/"
217            for target_name in multi_targets.get(region_name, []):
218                region_target_paths[target_name] = os.path.join(subdir, target_name)
219            region = NamedScreenRegion(
220                region_name,
221                x,
222                y,
223                w,
224                h,
225                parameters=parameters,
226                multi_target_paths=region_target_paths,
227            )
228            named_screen_regions.append(region)
229        super().__init__(pyboy, parameters, named_screen_regions)
230
231    @abstractmethod
232    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
233        """
234        Determines if the Pokedex is currently open.
235        Args:
236            current_screen (np.ndarray): The current screen frame from the emulator.
237
238        Returns:
239            bool: True if the Pokedex is open, False otherwise.
240        """
241        raise NotImplementedError
242
243    @staticmethod
244    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
245        """
246        Determines if the Pokemon menu is currently open.
247        Args:
248            current_screen (np.ndarray): The current screen frame from the emulator.
249        Returns:
250            bool: True if the Pokemon menu is open, False otherwise.
251        """
252        raise NotImplementedError
253
254    def is_hovering_over_options_in_menu(self, current_screen: np.ndarray) -> bool:
255        """
256        Determines if the cursor is currently hovering over options in the menu. Typically we force the agent off this state.
257
258        # TODO: This method currently only has one multi_target screen checked, cursor_on_options, which is screen captured AFTER the player gets the pokedex
259        The problem is the menu layout is slightly different before the pokedex is acquired, making the check useless before that point.
260        To fix this, we need to capture another target for the same multi_target region (e.g. cursor_on_options_no_pokedex) and check for both here.
261        But I am lazy, and so will hope this is not needed.
262
263        Args:
264            current_screen (np.ndarray): The current screen frame from the emulator.
265
266        Returns:
267            bool: True if hovering over options, False otherwise.
268        """
269        return self.named_region_matches_multi_target(
270            current_screen, "menu_box_strip", "cursor_on_options"
271        )
272
273    def is_in_battle(self, current_screen: np.ndarray) -> bool:
274        """
275        Determines if the player is currently in a battle by checking for battle HP text regions.
276
277        Args:
278            current_screen (np.ndarray): The current screen frame from the emulator.
279
280        Returns:
281            bool: True if in battle, False otherwise.
282        """
283        if self.is_in_fight_bag(current_screen):
284            return False  # Then, is in menu
285        enemy_hp_match = self.named_region_matches_target(
286            current_screen, "battle_enemy_hp_text"
287        )
288        player_hp_match = self.named_region_matches_target(
289            current_screen, "battle_player_hp_text"
290        )
291        return enemy_hp_match or player_hp_match
292
293    def is_in_base_battle_menu(self, current_screen: np.ndarray) -> bool:
294        """
295        Determines if the player is currently in the base battle menu by checking for the battle base menu top left region.
296
297        Args:
298            current_screen (np.ndarray): The current screen frame from the emulator.
299        Returns:
300            bool: True if in the base battle menu, False otherwise.
301        """
302        return self.named_region_matches_target(
303            current_screen, "battle_base_menu_top_left"
304        )
305
306    def is_in_run_screen(self, current_screen: np.ndarray) -> bool:
307        """
308        Determines if the player is currently in the run screen by checking for the battle base menu top left region.
309        Args:
310            current_screen (np.ndarray): The current screen frame from the emulator.
311        Returns:
312            bool: True if in the run screen, False otherwise.
313        """
314        got_away_safely = self.named_region_matches_multi_target(
315            current_screen, "dialogue_box_middle", "got_away_safely"
316        )
317        cannot_escape = self.named_region_matches_multi_target(
318            current_screen, "dialogue_box_middle", "cannot_escape"
319        )
320        cannot_run_from_trainer = self.named_region_matches_multi_target(
321            current_screen, "dialogue_box_middle", "cannot_run_from_trainer"
322        )
323        return got_away_safely or cannot_escape or cannot_run_from_trainer
324
325    def is_in_fight_options_menu(self, current_screen: np.ndarray) -> bool:
326        """
327        Determines if the player is currently in the fight options menu by checking for the battle fight options top right region.
328
329        Args:
330            current_screen (np.ndarray): The current screen frame from the emulator.
331        Returns:
332            bool: True if in the fight options menu, False otherwise.
333        """
334        return self.named_region_matches_target(
335            current_screen, "battle_fight_options_top_right"
336        )
337
338    def is_on_top_attack_option(self, current_screen: np.ndarray) -> bool:
339        """
340        Determines if the cursor is currently on the top attack option in the fight options menu.
341
342        Args:
343            current_screen (np.ndarray): The current screen frame from the emulator.
344        Returns:
345            bool: True if the cursor is on the top attack option, False otherwise.
346        """
347        return self.named_region_matches_target(
348            current_screen, "battle_fight_options_cursor_on_top"
349        )
350
351    def tried_no_pp_move(self, current_screen: np.ndarray) -> bool:
352        """
353        Determines if the player tried to use a move with no PP by checking for the no_pp_for_move target in the dialogue box middle region.
354
355        Args:
356            current_screen (np.ndarray): The current screen frame from the emulator.
357        Returns:
358            bool: True if the player tried to use a move with no PP, False otherwise.
359        """
360        return self.named_region_matches_multi_target(
361            current_screen, "dialogue_box_middle", "no_pp_for_move"
362        )
363
364    @abstractmethod
365    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
366        raise NotImplementedError
367
368    def is_on_top_menu_option(self, current_screen: np.ndarray) -> bool:
369        """
370        Determines if the cursor is currently on the top option in the start menu.
371
372        Args:
373            current_screen (np.ndarray): The current screen frame from the emulator.
374        Returns:
375            bool: True if the cursor is on the top menu option, False otherwise.
376        """
377        return self.named_region_matches_multi_target(
378            current_screen, "menu_box_strip", "cursor_on_pokedex"
379        )
380
381    def is_in_menu(
382        self, current_screen: np.ndarray, trust_previous: bool = False
383    ) -> bool:
384        """
385        Determines if any form of menu (or choice dialogue) is currently open by checking a variety of screen regions.
386
387        Args:
388            current_screen (np.ndarray): The current screen frame from the emulator.
389            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
390
391        Returns:
392            bool: True if the menu is open, False otherwise.
393        """
394        any_match_regions = [
395            "menu_top_right",
396            "dialogue_choice_bottom_right",
397            "pc_top_left",
398            "name_entity_top_left",
399            "player_card_middle",
400            "map_bottom_right",
401            "pokemon_list_hp_text",  # This one is defined in each subclass as the position varies slightly between games
402        ]
403        if not trust_previous:
404            if self.is_in_battle(current_screen):
405                return False
406        if self.is_in_fight_bag(current_screen):
407            return True
408        if self.is_in_pokedex(current_screen):
409            return True
410        if self.is_in_pokemon_menu(current_screen):
411            return True
412        for region_name in any_match_regions:
413            if self.named_region_matches_target(current_screen, region_name):
414                return True
415        return False
416
417    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
418        """
419        Determines if a dialogue box is currently open by checking the dialogue bottom right region.
420        Args:
421            current_screen (np.ndarray): The current screen frame from the emulator.
422        Returns:
423            bool: True if a dialogue box is open, False otherwise.
424        """
425        return self.named_region_matches_target(current_screen, "dialogue_bottom_right")
426
427    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
428        box = self.capture_named_region(
429            current_frame=current_screen, name="dialogue_box_full"
430        )
431        perc_lt_255 = np.mean(box < 255)
432        if perc_lt_255 < 0.082:  # Empirical threshold
433            return True
434        return False
435
436    def is_in_dialogue(
437        self, current_screen: np.ndarray, trust_previous: bool = False
438    ) -> bool:
439        """
440        Determines if the player is currently in a dialogue state or reading text from a sign, interacting with an object etc.
441        Essentially anything that causes text to appear at the bottom of the screen that isn't a battle, pc or menu.
442
443        Args:
444            current_screen (np.ndarray): The current screen frame from the emulator.
445            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
446
447        Returns:
448            bool: True if in dialogue, False otherwise.
449        """
450        if trust_previous:
451            return self.named_region_matches_target(
452                current_screen, "dialogue_bottom_right"
453            )
454        if self.is_in_battle(current_screen):
455            return False
456        elif self.is_in_menu(current_screen):
457            return False
458        else:
459            return self.dialogue_box_open(current_screen)
460
461    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
462        """
463        Determines the current agent state based on the screen.
464
465        Uses trust_previous to optimize checks.
466
467        Args:
468            current_screen (np.ndarray): The current screen frame from the emulator.
469
470        Returns:
471            AgentState: The current agent state.
472        """
473        if self.is_in_battle(current_screen):
474            return AgentState.IN_BATTLE
475        elif self.is_in_menu(current_screen, trust_previous=True):
476            return AgentState.IN_MENU
477        elif self.is_in_dialogue(current_screen, trust_previous=True):
478            return AgentState.IN_DIALOGUE
479        else:
480            return AgentState.FREE_ROAM

Base class for Pokemon game state parsers. Uses visual screen regions to parse game state. Defines common named screen regions and methods for determining game states such as being in battle, menu, or dialogue.

Can be used to determine the exact AgentState

PokemonStateParser( variant: str, pyboy: pyboy.pyboy.PyBoy, parameters: dict, additional_named_screen_region_details: List[Tuple[str, int, int, int, int]] = [], additional_multi_target_named_screen_region_details: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
146    def __init__(
147        self,
148        variant: str,
149        pyboy: PyBoy,
150        parameters: dict,
151        additional_named_screen_region_details: List[
152            Tuple[str, int, int, int, int]
153        ] = [],
154        additional_multi_target_named_screen_region_details: List[
155            Tuple[str, int, int, int, int]
156        ] = [],
157        override_multi_targets: Dict[str, List[str]] = {},
158    ):
159        """
160        Initializes the PokemonStateParser.
161        Args:
162            variant (str): The variant of the Pokemon game.
163            pyboy (PyBoy): The PyBoy emulator instance.
164            parameters (dict): Configuration parameters for the emulator.
165            additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
166            additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
167            override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
168        """
169        verify_parameters(parameters)
170        regions = _get_proper_regions(
171            override_regions=additional_named_screen_region_details,
172            base_regions=self.COMMON_REGIONS,
173        )
174        self.variant = variant
175        if f"{variant}_rom_data_path" not in parameters:
176            log_error(
177                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/pokemon_red_vars.yaml for an example",
178                parameters,
179            )
180        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
181        """ Path to the ROM data directory for the specific Pokemon variant."""
182        captures_dir = self.rom_data_path + "/captures/"
183        named_screen_regions = []
184        for region_name, x, y, w, h in regions:
185            region = NamedScreenRegion(
186                region_name,
187                x,
188                y,
189                w,
190                h,
191                parameters=parameters,
192                target_path=os.path.join(captures_dir, region_name),
193            )
194            named_screen_regions.append(region)
195        multi_target_regions = _get_proper_regions(
196            override_regions=additional_multi_target_named_screen_region_details,
197            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
198        )
199        multi_target_region_names = [region[0] for region in multi_target_regions]
200        multi_targets = self.COMMON_MULTI_TARGETS.copy()
201        for key in override_multi_targets:
202            if key in multi_targets:
203                multi_targets[key].extend(override_multi_targets[key])
204            else:
205                multi_targets[key] = override_multi_targets[key]
206        multi_target_provided_region_names = list(multi_targets.keys())
207        if not set(multi_target_provided_region_names).issubset(
208            set(multi_target_region_names)
209        ):
210            log_error(
211                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {multi_target_provided_region_names}, Defined: {multi_target_region_names}",
212                parameters,
213            )
214        for region_name, x, y, w, h in multi_target_regions:
215            region_target_paths = {}
216            subdir = captures_dir + f"/{region_name}/"
217            for target_name in multi_targets.get(region_name, []):
218                region_target_paths[target_name] = os.path.join(subdir, target_name)
219            region = NamedScreenRegion(
220                region_name,
221                x,
222                y,
223                w,
224                h,
225                parameters=parameters,
226                multi_target_paths=region_target_paths,
227            )
228            named_screen_regions.append(region)
229        super().__init__(pyboy, parameters, named_screen_regions)

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
COMMON_REGIONS = [('dialogue_bottom_right', 153, 135, 10, 10), ('menu_top_right', 152, 1, 6, 6), ('pc_top_left', 0, 0, 6, 6), ('battle_enemy_hp_text', 15, 17, 10, 5), ('battle_player_hp_text', 80, 73, 10, 5), ('battle_base_menu_top_left', 65, 96, 5, 5), ('battle_fight_options_top_right', 80, 64, 5, 5), ('battle_fight_options_cursor_on_top', 40, 103, 3, 3), ('dialogue_choice_bottom_right', 153, 87, 6, 6), ('name_entity_top_left', 0, 32, 6, 6), ('player_card_middle', 56, 70, 6, 6), ('map_bottom_right', 140, 130, 10, 10)]

List of common named screen regions for Pokemon games.

  • dialogue_bottom_right: Bottom right of dialogue box when interacting with NPCs, signs, etc. Speak to an NPC to capture this.
  • menu_top_right: Top right of the screen when the player start menu is open. Open the start menu to capture this.

  • pc_top_left: Top left of the screen when the PC is open. Open the PC to capture this.

  • battle_enemy_hp_text: Region showing the text 'HP' for the enemy Pokémon in battle. Engage in a battle to capture this.

  • battle_player_hp_text: Region showing the text 'HP' for the player's Pokémon in battle. Engage in a battle to capture this.

  • battle_base_menu_top_left: Top left of the battle base menu. Engage in a battle to capture this.

  • battle_fight_options_top_right: Top right of the fight options menu in battle. Engage in a battle and open the fight options to capture this.

  • battle_fight_options_cursor_on_top: Region showing the cursor on the top attack option in the fight options menu. Engage in a battle, open the fight options and move the cursor to the top option to capture this.

  • dialogue_choice_bottom_right: Bottom right of the choice dialogue box when answering choice questions (e.g. Yes/No prompts). Trigger a choice dialogue to capture this (e.g. confirmation of starter choice)

  • name_entity_top_left: Top left of the screen when naming a character or Pokémon. Catch a pokemon and give it a nickname to capture this.

  • player_card_middle: Middle of the player card screen. Go to this from start menu -> player name

  • map_bottom_right: Bottom right of the map screen when the town map is open. Open the town map to capture this.

COMMON_MULTI_TARGET_REGIONS = [('screen', 0, 0, 150, 140), ('dialogue_box_middle', 10, 105, 120, 30), ('dialogue_box_full', 5, 100, 150, 40), ('screen_bottom_half', 5, 70, 150, 70), ('screen_quadrant_1', 85, 0, 60, 60), ('screen_quadrant_2', 0, 0, 60, 60), ('screen_quadrant_3', 0, 70, 60, 70), ('screen_quadrant_4', 85, 70, 60, 70), ('screen_middle', 65, 55, 20, 20)]

List of common multi-target named screen regions for Pokemon games.

  • screen: Most of the screen except for the very edges. Useful for general state parsing.
  • dialogue_box_middle: Middle of the dialogue box, but not on that spot where the blinking arrow cursor appears. Useful for catching particular dialogues.
  • dialogue_box_full: Full dialogue box area, is useful to capture for OCR purposes.
  • screen_bottom_half: Bottom half of the screen, useful for OCR of dialogue and other text.
  • screen_quadrant_1: Top right quadrant of the screen.
  • screen_quadrant_2: Top left quadrant of the screen.
  • screen_quadrant_3: Bottom left quadrant of the screen.
  • screen_quadrant_4: Bottom right quadrant of the screen.
  • screen_middle: Middle of the screen.
COMMON_MULTI_TARGETS = {'dialogue_box_middle': ['got_away_safely', 'cannot_escape', 'cannot_run_from_trainer', 'no_pp_for_move'], 'menu_box_strip': ['cursor_on_options', 'cursor_on_pokedex']}

Common multi-targets for the common multi-target named screen regions.

  • dialogue_box_middle:
    • got_away_safely: Run successfully from a wild battle.
    • cannot_escape: Fail to run from a wild Pokemon
    • cannot_run_from_trainer: Try to run from a trainer battle and get an error message
    • no_pp_for_move: Try to use a move with no PP remaining.
  • menu_box_strip:
    • cursor_on_options: Cursor is on the options in the start menu. This is vital to prevent agents from changing the text frame option.
    • cursor_on_pokedex: Cursor is on the Pokedex in the start menu.
variant
rom_data_path

Path to the ROM data directory for the specific Pokemon variant.

@abstractmethod
def is_in_pokedex(self, current_screen: numpy.ndarray) -> bool:
231    @abstractmethod
232    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
233        """
234        Determines if the Pokedex is currently open.
235        Args:
236            current_screen (np.ndarray): The current screen frame from the emulator.
237
238        Returns:
239            bool: True if the Pokedex is open, False otherwise.
240        """
241        raise NotImplementedError

Determines if the Pokedex is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokedex is open, False otherwise.

@staticmethod
def is_in_pokemon_menu(self, current_screen: numpy.ndarray) -> bool:
243    @staticmethod
244    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
245        """
246        Determines if the Pokemon menu is currently open.
247        Args:
248            current_screen (np.ndarray): The current screen frame from the emulator.
249        Returns:
250            bool: True if the Pokemon menu is open, False otherwise.
251        """
252        raise NotImplementedError

Determines if the Pokemon menu is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokemon menu is open, False otherwise.

def is_hovering_over_options_in_menu(self, current_screen: numpy.ndarray) -> bool:
254    def is_hovering_over_options_in_menu(self, current_screen: np.ndarray) -> bool:
255        """
256        Determines if the cursor is currently hovering over options in the menu. Typically we force the agent off this state.
257
258        # TODO: This method currently only has one multi_target screen checked, cursor_on_options, which is screen captured AFTER the player gets the pokedex
259        The problem is the menu layout is slightly different before the pokedex is acquired, making the check useless before that point.
260        To fix this, we need to capture another target for the same multi_target region (e.g. cursor_on_options_no_pokedex) and check for both here.
261        But I am lazy, and so will hope this is not needed.
262
263        Args:
264            current_screen (np.ndarray): The current screen frame from the emulator.
265
266        Returns:
267            bool: True if hovering over options, False otherwise.
268        """
269        return self.named_region_matches_multi_target(
270            current_screen, "menu_box_strip", "cursor_on_options"
271        )

Determines if the cursor is currently hovering over options in the menu. Typically we force the agent off this state.

TODO: This method currently only has one multi_target screen checked, cursor_on_options, which is screen captured AFTER the player gets the pokedex

The problem is the menu layout is slightly different before the pokedex is acquired, making the check useless before that point. To fix this, we need to capture another target for the same multi_target region (e.g. cursor_on_options_no_pokedex) and check for both here. But I am lazy, and so will hope this is not needed.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if hovering over options, False otherwise.

def is_in_battle(self, current_screen: numpy.ndarray) -> bool:
273    def is_in_battle(self, current_screen: np.ndarray) -> bool:
274        """
275        Determines if the player is currently in a battle by checking for battle HP text regions.
276
277        Args:
278            current_screen (np.ndarray): The current screen frame from the emulator.
279
280        Returns:
281            bool: True if in battle, False otherwise.
282        """
283        if self.is_in_fight_bag(current_screen):
284            return False  # Then, is in menu
285        enemy_hp_match = self.named_region_matches_target(
286            current_screen, "battle_enemy_hp_text"
287        )
288        player_hp_match = self.named_region_matches_target(
289            current_screen, "battle_player_hp_text"
290        )
291        return enemy_hp_match or player_hp_match

Determines if the player is currently in a battle by checking for battle HP text regions.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if in battle, False otherwise.

def is_in_base_battle_menu(self, current_screen: numpy.ndarray) -> bool:
293    def is_in_base_battle_menu(self, current_screen: np.ndarray) -> bool:
294        """
295        Determines if the player is currently in the base battle menu by checking for the battle base menu top left region.
296
297        Args:
298            current_screen (np.ndarray): The current screen frame from the emulator.
299        Returns:
300            bool: True if in the base battle menu, False otherwise.
301        """
302        return self.named_region_matches_target(
303            current_screen, "battle_base_menu_top_left"
304        )

Determines if the player is currently in the base battle menu by checking for the battle base menu top left region.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if in the base battle menu, False otherwise.

def is_in_run_screen(self, current_screen: numpy.ndarray) -> bool:
306    def is_in_run_screen(self, current_screen: np.ndarray) -> bool:
307        """
308        Determines if the player is currently in the run screen by checking for the battle base menu top left region.
309        Args:
310            current_screen (np.ndarray): The current screen frame from the emulator.
311        Returns:
312            bool: True if in the run screen, False otherwise.
313        """
314        got_away_safely = self.named_region_matches_multi_target(
315            current_screen, "dialogue_box_middle", "got_away_safely"
316        )
317        cannot_escape = self.named_region_matches_multi_target(
318            current_screen, "dialogue_box_middle", "cannot_escape"
319        )
320        cannot_run_from_trainer = self.named_region_matches_multi_target(
321            current_screen, "dialogue_box_middle", "cannot_run_from_trainer"
322        )
323        return got_away_safely or cannot_escape or cannot_run_from_trainer

Determines if the player is currently in the run screen by checking for the battle base menu top left region.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if in the run screen, False otherwise.

def is_in_fight_options_menu(self, current_screen: numpy.ndarray) -> bool:
325    def is_in_fight_options_menu(self, current_screen: np.ndarray) -> bool:
326        """
327        Determines if the player is currently in the fight options menu by checking for the battle fight options top right region.
328
329        Args:
330            current_screen (np.ndarray): The current screen frame from the emulator.
331        Returns:
332            bool: True if in the fight options menu, False otherwise.
333        """
334        return self.named_region_matches_target(
335            current_screen, "battle_fight_options_top_right"
336        )

Determines if the player is currently in the fight options menu by checking for the battle fight options top right region.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if in the fight options menu, False otherwise.

def is_on_top_attack_option(self, current_screen: numpy.ndarray) -> bool:
338    def is_on_top_attack_option(self, current_screen: np.ndarray) -> bool:
339        """
340        Determines if the cursor is currently on the top attack option in the fight options menu.
341
342        Args:
343            current_screen (np.ndarray): The current screen frame from the emulator.
344        Returns:
345            bool: True if the cursor is on the top attack option, False otherwise.
346        """
347        return self.named_region_matches_target(
348            current_screen, "battle_fight_options_cursor_on_top"
349        )

Determines if the cursor is currently on the top attack option in the fight options menu.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the cursor is on the top attack option, False otherwise.

def tried_no_pp_move(self, current_screen: numpy.ndarray) -> bool:
351    def tried_no_pp_move(self, current_screen: np.ndarray) -> bool:
352        """
353        Determines if the player tried to use a move with no PP by checking for the no_pp_for_move target in the dialogue box middle region.
354
355        Args:
356            current_screen (np.ndarray): The current screen frame from the emulator.
357        Returns:
358            bool: True if the player tried to use a move with no PP, False otherwise.
359        """
360        return self.named_region_matches_multi_target(
361            current_screen, "dialogue_box_middle", "no_pp_for_move"
362        )

Determines if the player tried to use a move with no PP by checking for the no_pp_for_move target in the dialogue box middle region.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the player tried to use a move with no PP, False otherwise.

@abstractmethod
def is_in_fight_bag(self, current_screen: numpy.ndarray) -> bool:
364    @abstractmethod
365    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
366        raise NotImplementedError
def is_on_top_menu_option(self, current_screen: numpy.ndarray) -> bool:
368    def is_on_top_menu_option(self, current_screen: np.ndarray) -> bool:
369        """
370        Determines if the cursor is currently on the top option in the start menu.
371
372        Args:
373            current_screen (np.ndarray): The current screen frame from the emulator.
374        Returns:
375            bool: True if the cursor is on the top menu option, False otherwise.
376        """
377        return self.named_region_matches_multi_target(
378            current_screen, "menu_box_strip", "cursor_on_pokedex"
379        )

Determines if the cursor is currently on the top option in the start menu.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the cursor is on the top menu option, False otherwise.

def is_in_menu( self, current_screen: numpy.ndarray, trust_previous: bool = False) -> bool:
381    def is_in_menu(
382        self, current_screen: np.ndarray, trust_previous: bool = False
383    ) -> bool:
384        """
385        Determines if any form of menu (or choice dialogue) is currently open by checking a variety of screen regions.
386
387        Args:
388            current_screen (np.ndarray): The current screen frame from the emulator.
389            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
390
391        Returns:
392            bool: True if the menu is open, False otherwise.
393        """
394        any_match_regions = [
395            "menu_top_right",
396            "dialogue_choice_bottom_right",
397            "pc_top_left",
398            "name_entity_top_left",
399            "player_card_middle",
400            "map_bottom_right",
401            "pokemon_list_hp_text",  # This one is defined in each subclass as the position varies slightly between games
402        ]
403        if not trust_previous:
404            if self.is_in_battle(current_screen):
405                return False
406        if self.is_in_fight_bag(current_screen):
407            return True
408        if self.is_in_pokedex(current_screen):
409            return True
410        if self.is_in_pokemon_menu(current_screen):
411            return True
412        for region_name in any_match_regions:
413            if self.named_region_matches_target(current_screen, region_name):
414                return True
415        return False

Determines if any form of menu (or choice dialogue) is currently open by checking a variety of screen regions.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
  • trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
Returns:

bool: True if the menu is open, False otherwise.

def dialogue_box_open(self, current_screen: numpy.ndarray) -> bool:
417    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
418        """
419        Determines if a dialogue box is currently open by checking the dialogue bottom right region.
420        Args:
421            current_screen (np.ndarray): The current screen frame from the emulator.
422        Returns:
423            bool: True if a dialogue box is open, False otherwise.
424        """
425        return self.named_region_matches_target(current_screen, "dialogue_bottom_right")

Determines if a dialogue box is currently open by checking the dialogue bottom right region.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if a dialogue box is open, False otherwise.

def dialogue_box_empty(self, current_screen: numpy.ndarray) -> bool:
427    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
428        box = self.capture_named_region(
429            current_frame=current_screen, name="dialogue_box_full"
430        )
431        perc_lt_255 = np.mean(box < 255)
432        if perc_lt_255 < 0.082:  # Empirical threshold
433            return True
434        return False
def is_in_dialogue( self, current_screen: numpy.ndarray, trust_previous: bool = False) -> bool:
436    def is_in_dialogue(
437        self, current_screen: np.ndarray, trust_previous: bool = False
438    ) -> bool:
439        """
440        Determines if the player is currently in a dialogue state or reading text from a sign, interacting with an object etc.
441        Essentially anything that causes text to appear at the bottom of the screen that isn't a battle, pc or menu.
442
443        Args:
444            current_screen (np.ndarray): The current screen frame from the emulator.
445            trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
446
447        Returns:
448            bool: True if in dialogue, False otherwise.
449        """
450        if trust_previous:
451            return self.named_region_matches_target(
452                current_screen, "dialogue_bottom_right"
453            )
454        if self.is_in_battle(current_screen):
455            return False
456        elif self.is_in_menu(current_screen):
457            return False
458        else:
459            return self.dialogue_box_open(current_screen)

Determines if the player is currently in a dialogue state or reading text from a sign, interacting with an object etc. Essentially anything that causes text to appear at the bottom of the screen that isn't a battle, pc or menu.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
  • trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
Returns:

bool: True if in dialogue, False otherwise.

def get_agent_state( self, current_screen: numpy.ndarray) -> AgentState:
461    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
462        """
463        Determines the current agent state based on the screen.
464
465        Uses trust_previous to optimize checks.
466
467        Args:
468            current_screen (np.ndarray): The current screen frame from the emulator.
469
470        Returns:
471            AgentState: The current agent state.
472        """
473        if self.is_in_battle(current_screen):
474            return AgentState.IN_BATTLE
475        elif self.is_in_menu(current_screen, trust_previous=True):
476            return AgentState.IN_MENU
477        elif self.is_in_dialogue(current_screen, trust_previous=True):
478            return AgentState.IN_DIALOGUE
479        else:
480            return AgentState.FREE_ROAM

Determines the current agent state based on the screen.

Uses trust_previous to optimize checks.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

AgentState: The current agent state.

class BasePokemonRedStateParser(PokemonStateParser, abc.ABC):
483class BasePokemonRedStateParser(PokemonStateParser, ABC):
484    """
485    Game state parser for all PokemonRed-based games.
486    """
487
488    REGIONS = [
489        ("pokedex_top_left", 7, 6, 12, 6),
490        ("pokedex_info_mid_left", 6, 71, 6, 6),
491        ("pokemon_list_hp_text", 32, 9, 10, 5),
492        ("pokemon_stats_line", 66, 55, 5, 5),
493        ("battle_bag_options_bottom_left", 32, 96, 5, 5),
494    ]
495    """ Additional named screen regions specific to Pokemon Red games.
496    - pokedex_top_left: Top left of the screen when the Pokedex is open. Open the Pokedex to capture this.
497    - pokedex_info_mid_left: Middle left of the screen when viewing a Pokémon's info in the Pokedex. Open a Pokémon's info in the Pokedex to capture this.
498    - pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
499    - pokemon_stats_line: A line in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
500    - battle_bag_options_bottom_left: The bottom left part of the menu when you open the items option from the battle menu. 
501    """
502
503    MULTI_TARGET_REGIONS = [
504        ("menu_box_strip", 89, 13, 5, 100),
505    ]
506    """ Additional multi-target named screen regions specific to Pokemon Red games. 
507    - menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states. 
508    """
509
510    def __init__(
511        self,
512        pyboy: PyBoy,
513        variant: str,
514        parameters: dict,
515        override_regions: List[Tuple[str, int, int, int, int]] = [],
516        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
517        override_multi_targets: Dict[str, List[str]] = {},
518    ):
519        self.REGIONS = _get_proper_regions(
520            override_regions=override_regions, base_regions=self.REGIONS
521        )
522        self.MULTI_TARGET_REGIONS = _get_proper_regions(
523            override_regions=override_multi_target_regions,
524            base_regions=self.MULTI_TARGET_REGIONS,
525        )
526        super().__init__(
527            variant=variant,
528            pyboy=pyboy,
529            parameters=parameters,
530            additional_named_screen_region_details=self.REGIONS,
531            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
532            override_multi_targets=override_multi_targets,
533        )
534
535    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
536        return self.named_region_matches_target(
537            current_screen, "pokedex_top_left"
538        ) or self.named_region_matches_target(current_screen, "pokedex_info_mid_left")
539
540    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
541        return self.named_region_matches_target(
542            current_screen, "pokemon_list_hp_text"
543        ) or self.named_region_matches_target(current_screen, "pokemon_stats_line")
544
545    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
546        return self.named_region_matches_target(
547            current_screen, "battle_bag_options_bottom_left"
548        )
549
550    def __repr__(self):
551        return f"<PokemonRedParser(variant={self.variant})>"

Game state parser for all PokemonRed-based games.

BasePokemonRedStateParser( pyboy: pyboy.pyboy.PyBoy, variant: str, parameters: dict, override_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
510    def __init__(
511        self,
512        pyboy: PyBoy,
513        variant: str,
514        parameters: dict,
515        override_regions: List[Tuple[str, int, int, int, int]] = [],
516        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
517        override_multi_targets: Dict[str, List[str]] = {},
518    ):
519        self.REGIONS = _get_proper_regions(
520            override_regions=override_regions, base_regions=self.REGIONS
521        )
522        self.MULTI_TARGET_REGIONS = _get_proper_regions(
523            override_regions=override_multi_target_regions,
524            base_regions=self.MULTI_TARGET_REGIONS,
525        )
526        super().__init__(
527            variant=variant,
528            pyboy=pyboy,
529            parameters=parameters,
530            additional_named_screen_region_details=self.REGIONS,
531            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
532            override_multi_targets=override_multi_targets,
533        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
REGIONS = [('pokedex_top_left', 7, 6, 12, 6), ('pokedex_info_mid_left', 6, 71, 6, 6), ('pokemon_list_hp_text', 32, 9, 10, 5), ('pokemon_stats_line', 66, 55, 5, 5), ('battle_bag_options_bottom_left', 32, 96, 5, 5)]

Additional named screen regions specific to Pokemon Red games.

  • pokedex_top_left: Top left of the screen when the Pokedex is open. Open the Pokedex to capture this.
  • pokedex_info_mid_left: Middle left of the screen when viewing a Pokémon's info in the Pokedex. Open a Pokémon's info in the Pokedex to capture this.
  • pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
  • pokemon_stats_line: A line in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
  • battle_bag_options_bottom_left: The bottom left part of the menu when you open the items option from the battle menu.
MULTI_TARGET_REGIONS = [('menu_box_strip', 89, 13, 5, 100)]

Additional multi-target named screen regions specific to Pokemon Red games.

  • menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states.
def is_in_pokedex(self, current_screen: numpy.ndarray) -> bool:
535    def is_in_pokedex(self, current_screen: np.ndarray) -> bool:
536        return self.named_region_matches_target(
537            current_screen, "pokedex_top_left"
538        ) or self.named_region_matches_target(current_screen, "pokedex_info_mid_left")

Determines if the Pokedex is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokedex is open, False otherwise.

def is_in_pokemon_menu(self, current_screen: numpy.ndarray) -> bool:
540    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
541        return self.named_region_matches_target(
542            current_screen, "pokemon_list_hp_text"
543        ) or self.named_region_matches_target(current_screen, "pokemon_stats_line")

Determines if the Pokemon menu is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokemon menu is open, False otherwise.

def is_in_fight_bag(self, current_screen: numpy.ndarray) -> bool:
545    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
546        return self.named_region_matches_target(
547            current_screen, "battle_bag_options_bottom_left"
548        )
class BasePokemonCrystalStateParser(PokemonStateParser, abc.ABC):
554class BasePokemonCrystalStateParser(PokemonStateParser, ABC):
555    """
556    Game state parser for all PokemonCrystal-based games.
557
558    TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it.
559    Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.
560    """
561
562    REGIONS = [
563        ("pokemon_list_hp_text", 87, 16, 10, 5),
564        ("pokedex_seen_text", 3, 88, 5, 5),
565        ("pokedex_info_height_text", 69, 57, 5, 5),
566        ("pokegear_top_left", 0, 0, 6, 6),
567        ("pokemon_stats_lvl_text", 113, 0, 5, 5),
568        ("bag_text", 18, 0, 6, 6),
569    ]
570    """ Additional named screen regions specific to Pokemon Crystal games.
571    - pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
572    - pokedex_seen_text: Region showing the 'SEEN' text in the Pokedex. Open the Pokedex to capture this.
573    - pokedex_info_height_text: Region showing the 'HEIGHT' text in the Pokedex info screen. Open a Pokémon's info in the Pokedex to capture this.
574    - pokegear_top_left: Top left of the screen when the Pokegear is open. Open the Pokegear to capture this.
575    - pokemon_stats_lvl_text: Region showing the 'LV' text in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
576    - bag_text: Top left of the screen when the Bag is open. Open the Bag to capture this.
577    """
578
579    MULTI_TARGET_REGIONS = [
580        ("menu_box_strip", 89, 13, 5, 120),
581    ]
582    """ Additional multi-target named screen regions specific to Pokemon Crystal games. 
583    - menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states. 
584    """
585
586    def __init__(
587        self,
588        pyboy: PyBoy,
589        variant: str,
590        parameters: dict,
591        override_regions: List[Tuple[str, int, int, int, int]] = [],
592        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
593        override_multi_targets: Dict[str, List[str]] = {},
594    ):
595        self.REGIONS = _get_proper_regions(
596            override_regions=override_regions, base_regions=self.REGIONS
597        )
598        self.MULTI_TARGET_REGIONS = _get_proper_regions(
599            override_regions=override_multi_target_regions,
600            base_regions=self.MULTI_TARGET_REGIONS,
601        )
602        super().__init__(
603            variant=variant,
604            pyboy=pyboy,
605            parameters=parameters,
606            additional_named_screen_region_details=self.REGIONS,
607            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
608            override_multi_targets=override_multi_targets,
609        )
610
611    def is_in_bag(self, current_screen: np.ndarray) -> bool:
612        """
613        Determines if the Bag is currently open.
614        """
615        return self.named_region_matches_target(current_screen, "bag_text")
616
617    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
618        """
619        For PokemonCrystal, this is just the same as in bag.
620        """
621        return self.is_in_bag(current_screen)
622
623    def is_in_pokegear(self, current_screen: np.ndarray) -> bool:
624        """
625        Determines if the Pokegear is currently open.
626        """
627        return self.named_region_matches_target(current_screen, "pokegear_top_left")
628
629    def is_in_pokedex(self, current_screen):
630        return self.named_region_matches_target(
631            current_screen, "pokedex_seen_text"
632        ) or self.named_region_matches_target(
633            current_screen, "pokedex_info_height_text"
634        )
635
636    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
637        return self.named_region_matches_target(
638            current_screen, "pokemon_stats_lvl_text"
639        ) or self.named_region_matches_target(current_screen, "pokemon_list_hp_text")
640
641    def is_in_menu(
642        self, current_screen: np.ndarray, trust_previous: bool = False
643    ) -> bool:
644        # This technically mistakenly also flags when someone calls you on the pokegear, but that's probably fine for now.
645        # Could change by adding special region for pokegear_call_top_left and overriding is_in_menu and is_in_dialogue.
646        result = super().is_in_menu(current_screen, trust_previous=trust_previous)
647        if result:
648            return True
649        if self.is_in_bag(current_screen):
650            return True
651        if self.is_in_pokegear(current_screen):
652            return True
653        # Finally, when transitioning to PC screens, maps etc, the screen goes white. Catch that here.
654        # print(f"Checking for white screen... Pixel stats: {np.min(current_screen)}, {np.max(current_screen)}, {np.mean(current_screen)}") # I get 248, 248, 248.0
655        # The following doesn't catch all white screens (e.g town maps), but does catch some important ones like PC screens.
656        if np.mean(current_screen) > 245 and np.min(current_screen) > 245:
657            return True
658        elif np.mean(current_screen) > 210:  # screen coming down from full white
659            return True
660        else:
661            return False
662
663    def __repr__(self):
664        return f"<PokemonCrystalParser(variant={self.variant})>"

Game state parser for all PokemonCrystal-based games.

TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it. Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.

BasePokemonCrystalStateParser( pyboy: pyboy.pyboy.PyBoy, variant: str, parameters: dict, override_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
586    def __init__(
587        self,
588        pyboy: PyBoy,
589        variant: str,
590        parameters: dict,
591        override_regions: List[Tuple[str, int, int, int, int]] = [],
592        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
593        override_multi_targets: Dict[str, List[str]] = {},
594    ):
595        self.REGIONS = _get_proper_regions(
596            override_regions=override_regions, base_regions=self.REGIONS
597        )
598        self.MULTI_TARGET_REGIONS = _get_proper_regions(
599            override_regions=override_multi_target_regions,
600            base_regions=self.MULTI_TARGET_REGIONS,
601        )
602        super().__init__(
603            variant=variant,
604            pyboy=pyboy,
605            parameters=parameters,
606            additional_named_screen_region_details=self.REGIONS,
607            additional_multi_target_named_screen_region_details=self.MULTI_TARGET_REGIONS,
608            override_multi_targets=override_multi_targets,
609        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
REGIONS = [('pokemon_list_hp_text', 87, 16, 10, 5), ('pokedex_seen_text', 3, 88, 5, 5), ('pokedex_info_height_text', 69, 57, 5, 5), ('pokegear_top_left', 0, 0, 6, 6), ('pokemon_stats_lvl_text', 113, 0, 5, 5), ('bag_text', 18, 0, 6, 6)]

Additional named screen regions specific to Pokemon Crystal games.

  • pokemon_list_hp_text: Region showing the text 'HP' for the player's Pokémon in the Pokémon list. Open the Pokémon list from the start menu to capture this.
  • pokedex_seen_text: Region showing the 'SEEN' text in the Pokedex. Open the Pokedex to capture this.
  • pokedex_info_height_text: Region showing the 'HEIGHT' text in the Pokedex info screen. Open a Pokémon's info in the Pokedex to capture this.
  • pokegear_top_left: Top left of the screen when the Pokegear is open. Open the Pokegear to capture this.
  • pokemon_stats_lvl_text: Region showing the 'LV' text in the Pokémon stats screen. Open a Pokémon's stats from the start menu -> pokemon menu to capture this.
  • bag_text: Top left of the screen when the Bag is open. Open the Bag to capture this.
MULTI_TARGET_REGIONS = [('menu_box_strip', 89, 13, 5, 120)]

Additional multi-target named screen regions specific to Pokemon Crystal games.

  • menu_box_strip: Strip of the menu box when the start menu is open. Open the start menu to capture this. The margins are adjusted to avoiding capturing the player name, as this may change across sav files and states.
def is_in_bag(self, current_screen: numpy.ndarray) -> bool:
611    def is_in_bag(self, current_screen: np.ndarray) -> bool:
612        """
613        Determines if the Bag is currently open.
614        """
615        return self.named_region_matches_target(current_screen, "bag_text")

Determines if the Bag is currently open.

def is_in_fight_bag(self, current_screen: numpy.ndarray) -> bool:
617    def is_in_fight_bag(self, current_screen: np.ndarray) -> bool:
618        """
619        For PokemonCrystal, this is just the same as in bag.
620        """
621        return self.is_in_bag(current_screen)

For PokemonCrystal, this is just the same as in bag.

def is_in_pokegear(self, current_screen: numpy.ndarray) -> bool:
623    def is_in_pokegear(self, current_screen: np.ndarray) -> bool:
624        """
625        Determines if the Pokegear is currently open.
626        """
627        return self.named_region_matches_target(current_screen, "pokegear_top_left")

Determines if the Pokegear is currently open.

def is_in_pokedex(self, current_screen):
629    def is_in_pokedex(self, current_screen):
630        return self.named_region_matches_target(
631            current_screen, "pokedex_seen_text"
632        ) or self.named_region_matches_target(
633            current_screen, "pokedex_info_height_text"
634        )

Determines if the Pokedex is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokedex is open, False otherwise.

def is_in_pokemon_menu(self, current_screen: numpy.ndarray) -> bool:
636    def is_in_pokemon_menu(self, current_screen: np.ndarray) -> bool:
637        return self.named_region_matches_target(
638            current_screen, "pokemon_stats_lvl_text"
639        ) or self.named_region_matches_target(current_screen, "pokemon_list_hp_text")

Determines if the Pokemon menu is currently open.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
Returns:

bool: True if the Pokemon menu is open, False otherwise.

def is_in_menu( self, current_screen: numpy.ndarray, trust_previous: bool = False) -> bool:
641    def is_in_menu(
642        self, current_screen: np.ndarray, trust_previous: bool = False
643    ) -> bool:
644        # This technically mistakenly also flags when someone calls you on the pokegear, but that's probably fine for now.
645        # Could change by adding special region for pokegear_call_top_left and overriding is_in_menu and is_in_dialogue.
646        result = super().is_in_menu(current_screen, trust_previous=trust_previous)
647        if result:
648            return True
649        if self.is_in_bag(current_screen):
650            return True
651        if self.is_in_pokegear(current_screen):
652            return True
653        # Finally, when transitioning to PC screens, maps etc, the screen goes white. Catch that here.
654        # print(f"Checking for white screen... Pixel stats: {np.min(current_screen)}, {np.max(current_screen)}, {np.mean(current_screen)}") # I get 248, 248, 248.0
655        # The following doesn't catch all white screens (e.g town maps), but does catch some important ones like PC screens.
656        if np.mean(current_screen) > 245 and np.min(current_screen) > 245:
657            return True
658        elif np.mean(current_screen) > 210:  # screen coming down from full white
659            return True
660        else:
661            return False

Determines if any form of menu (or choice dialogue) is currently open by checking a variety of screen regions.

Arguments:
  • current_screen (np.ndarray): The current screen frame from the emulator.
  • trust_previous (bool): If True, trusts that checks for other states like is_in_battle have been done and can be skipped.
Returns:

bool: True if the menu is open, False otherwise.

class PokemonRedStateParser(BasePokemonRedStateParser):
667class PokemonRedStateParser(BasePokemonRedStateParser):
668    def __init__(self, pyboy, parameters):
669        override_multi_targets = {
670            "dialogue_box_middle": [
671                "picked_charmander",
672                "picked_bulbasaur",
673                "picked_squirtle",
674                "talk_bill_complete",
675                "pick_up_pokeball_starting",  # is tied to player character name being You
676                "trainers_tips_sign",
677                "cinnabar_gym_aid_complete",
678                "talk_cinnabar_monk",
679                "defeated_brock",
680                "defeated_lass",
681                "caught_pidgey",
682                "caught_pikachu",
683                "used_potion_on_charmander",
684            ],
685            "screen_bottom_half": [
686                "viridian_pokemon_center_entrance",
687                "mt_moon_entrance",
688                "bought_potion_at_pewter_pokemart",
689            ],
690            "screen_middle": [
691                "outside_viridian_center_from_left",
692                "outside_viridian_center_from_right",
693            ],
694        }
695        super().__init__(
696            pyboy,
697            variant="pokemon_red",
698            parameters=parameters,
699            override_multi_targets=override_multi_targets,
700        )

Game state parser for all PokemonRed-based games.

PokemonRedStateParser(pyboy, parameters)
668    def __init__(self, pyboy, parameters):
669        override_multi_targets = {
670            "dialogue_box_middle": [
671                "picked_charmander",
672                "picked_bulbasaur",
673                "picked_squirtle",
674                "talk_bill_complete",
675                "pick_up_pokeball_starting",  # is tied to player character name being You
676                "trainers_tips_sign",
677                "cinnabar_gym_aid_complete",
678                "talk_cinnabar_monk",
679                "defeated_brock",
680                "defeated_lass",
681                "caught_pidgey",
682                "caught_pikachu",
683                "used_potion_on_charmander",
684            ],
685            "screen_bottom_half": [
686                "viridian_pokemon_center_entrance",
687                "mt_moon_entrance",
688                "bought_potion_at_pewter_pokemart",
689            ],
690            "screen_middle": [
691                "outside_viridian_center_from_left",
692                "outside_viridian_center_from_right",
693            ],
694        }
695        super().__init__(
696            pyboy,
697            variant="pokemon_red",
698            parameters=parameters,
699            override_multi_targets=override_multi_targets,
700        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonBrownStateParser(BasePokemonRedStateParser):
703class PokemonBrownStateParser(BasePokemonRedStateParser):
704    def __init__(self, pyboy, parameters):
705        super().__init__(pyboy, variant="pokemon_brown", parameters=parameters)

Game state parser for all PokemonRed-based games.

PokemonBrownStateParser(pyboy, parameters)
704    def __init__(self, pyboy, parameters):
705        super().__init__(pyboy, variant="pokemon_brown", parameters=parameters)

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonStarBeastsStateParser(BasePokemonRedStateParser):
708class PokemonStarBeastsStateParser(BasePokemonRedStateParser):
709    def __init__(self, pyboy, parameters):
710        override_regions = [
711            ("pokemon_list_hp_text", 33, 10, 4, 4),
712            ("battle_enemy_hp_text", 6, 15, 5, 5),
713            ("battle_player_hp_text", 88, 72, 5, 5),
714        ]
715        super().__init__(
716            pyboy,
717            variant="pokemon_starbeasts",
718            parameters=parameters,
719            override_regions=override_regions,
720        )

Game state parser for all PokemonRed-based games.

PokemonStarBeastsStateParser(pyboy, parameters)
709    def __init__(self, pyboy, parameters):
710        override_regions = [
711            ("pokemon_list_hp_text", 33, 10, 4, 4),
712            ("battle_enemy_hp_text", 6, 15, 5, 5),
713            ("battle_player_hp_text", 88, 72, 5, 5),
714        ]
715        super().__init__(
716            pyboy,
717            variant="pokemon_starbeasts",
718            parameters=parameters,
719            override_regions=override_regions,
720        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonStarBeastsCometStateParser(BasePokemonRedStateParser):
723class PokemonStarBeastsCometStateParser(BasePokemonRedStateParser):
724    def __init__(self, pyboy, parameters):
725        #override_regions = [
726            #("pokemon_list_hp_text", 33, 10, 4, 4),
727            #("battle_enemy_hp_text", 6, 15, 5, 5),
728            #("battle_player_hp_text", 88, 72, 5, 5),
729        #]
730        super().__init__(
731            pyboy,
732            variant="pokemon_starbeasts_comet",
733            parameters=parameters,
734            #override_regions=override_regions,
735        )

Game state parser for all PokemonRed-based games.

PokemonStarBeastsCometStateParser(pyboy, parameters)
724    def __init__(self, pyboy, parameters):
725        #override_regions = [
726            #("pokemon_list_hp_text", 33, 10, 4, 4),
727            #("battle_enemy_hp_text", 6, 15, 5, 5),
728            #("battle_player_hp_text", 88, 72, 5, 5),
729        #]
730        super().__init__(
731            pyboy,
732            variant="pokemon_starbeasts_comet",
733            parameters=parameters,
734            #override_regions=override_regions,
735        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonCrystalStateParser(BasePokemonCrystalStateParser):
738class PokemonCrystalStateParser(BasePokemonCrystalStateParser):
739    def __init__(self, pyboy, parameters):
740        super().__init__(pyboy, variant="pokemon_crystal", parameters=parameters)

Game state parser for all PokemonCrystal-based games.

TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it. Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.

PokemonCrystalStateParser(pyboy, parameters)
739    def __init__(self, pyboy, parameters):
740        super().__init__(pyboy, variant="pokemon_crystal", parameters=parameters)

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonPrismStateParser(BasePokemonCrystalStateParser):
743class PokemonPrismStateParser(BasePokemonCrystalStateParser):
744    def __init__(self, pyboy, parameters):
745        override_regions = [("player_card_middle", 25, 58, 5, 5)]
746        super().__init__(
747            pyboy,
748            variant="pokemon_prism",
749            parameters=parameters,
750            override_regions=override_regions,
751        )

Game state parser for all PokemonCrystal-based games.

TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it. Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.

PokemonPrismStateParser(pyboy, parameters)
744    def __init__(self, pyboy, parameters):
745        override_regions = [("player_card_middle", 25, 58, 5, 5)]
746        super().__init__(
747            pyboy,
748            variant="pokemon_prism",
749            parameters=parameters,
750            override_regions=override_regions,
751        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class PokemonFoolsGoldStateParser(BasePokemonCrystalStateParser):
754class PokemonFoolsGoldStateParser(BasePokemonCrystalStateParser):
755    def __init__(self, pyboy, parameters):
756        override_regions = [("pokedex_info_height_text", 66, 65, 5, 5)]
757        super().__init__(
758            pyboy,
759            variant="pokemon_fools_gold",
760            parameters=parameters,
761            override_regions=override_regions,
762        )

Game state parser for all PokemonCrystal-based games.

TODO: The map screenshot for crystal assumes a Jhoto map. Must do a similar process for Kanto. To add Kanto we should add another named screen region called map_bottom_right_kanto with same boundary as player_card_middle and then recapture it. Without this fix, the is_in_menu check may fail when in Kanto as the map_bottom_right region will not match.

PokemonFoolsGoldStateParser(pyboy, parameters)
755    def __init__(self, pyboy, parameters):
756        override_regions = [("pokedex_info_height_text", 66, 65, 5, 5)]
757        super().__init__(
758            pyboy,
759            variant="pokemon_fools_gold",
760            parameters=parameters,
761            override_regions=override_regions,
762        )

Initializes the PokemonStateParser.

Arguments:
  • variant (str): The variant of the Pokemon game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional named screen regions to include.
  • additional_multi_target_named_screen_region_details (List[Tuple[str, int, int, int, int]]): Parameters associated with additional multi-target named screen regions to include.
  • override_multi_targets (Dict[str, List[str]]): Dictionary mapping region names to lists of target names for multi-target regions.
class MemoryBasedPokemonRedStateParser(PokemonRedStateParser):
772class MemoryBasedPokemonRedStateParser(PokemonRedStateParser):
773    """
774    Game state parser for Pokemon Red. Uses memory addresses to parse game state.
775    Can be used to reproduce https://github.com/PWhiddy/PokemonRedExperiments/ (v2) and facilitates reward engineering based on memory states.
776    """
777
778    _PAD = 20
779    _GLOBAL_MAP_SHAPE = (444 + _PAD * 2, 436 + _PAD * 2)
780    _MAP_ROW_OFFSET = _PAD
781    _MAP_COL_OFFSET = _PAD
782
783    def __init__(self, pyboy, parameters):
784        """
785        Initializes the Pokemon Red game state parser.
786
787        Args:
788            pyboy: An instance of the PyBoy emulator.
789            parameters: A dictionary of parameters for configuration.
790        """
791        super().__init__(pyboy, parameters=parameters)
792        events_location = parameters["pokemon_red_rom_data_path"] + "/events.json"
793        with open(events_location) as f:
794            event_slots = json.load(f)
795        event_slots = event_slots
796        event_names = {v: k for k, v in event_slots.items() if not v[0].isdigit()}
797        beat_opponent_events = bidict()
798
799        def _pop(d, keys):
800            for key in keys:
801                if key in d:
802                    d.pop(key, None)
803
804        pop_queue = []
805        for name, slot in event_names.items():
806            if name.startswith("Beat "):
807                beat_opponent_events[name.replace("Beat ", "")] = slot
808                pop_queue.append(name)
809        _pop(event_names, pop_queue)
810        self.defeated_opponent_events = beat_opponent_events
811        """Events related to beating specific opponents. E.g. Beat Brock"""
812        tms_obtained_events = bidict()
813        pop_queue = []
814        for name, slot in event_names.items():
815            if name.startswith("Got Tm"):
816                tms_obtained_events[name.replace("Got ", "").strip()] = slot
817                pop_queue.append(name)
818        _pop(event_names, pop_queue)
819        self.tms_obtained_events = tms_obtained_events
820        """Events related to obtaining specific TMs. E.g. Got Tm01"""
821        hm_obtained_events = bidict()
822        pop_queue = []
823        for name, slot in event_names.items():
824            if name.startswith("Got Hm"):
825                hm_obtained_events[name.replace("Got ", "").strip()] = slot
826                pop_queue.append(name)
827        _pop(event_names, pop_queue)
828        self.hm_obtained_events = hm_obtained_events
829        """Events related to obtaining specific HMs. E.g. Got Hm01"""
830        passed_badge_check_events = bidict()
831        pop_queue = []
832        for name, slot in event_names.items():
833            if name.startswith("Passed ") and "badge" in name:
834                passed_badge_check_events[
835                    name.replace("Passed ", "").replace(" Check", "").strip()
836                ] = slot
837                pop_queue.append(name)
838        _pop(event_names, pop_queue)
839        self.passed_badge_check_events = passed_badge_check_events
840        """Events related to passing badge checks. E.g. Passed Boulder badge check. These will only be relevant to enter Victory Road."""
841        self.key_items_obtained_events = bidict()
842        """Events related to obtaining key items. E.g. Got Bicycle"""
843        pop_queue = []
844        for name, slot in event_names.items():
845            if name.startswith("Got "):
846                self.key_items_obtained_events[name.replace("Got ", "").strip()] = slot
847                pop_queue.append(name)
848        _pop(event_names, pop_queue)
849        self.map_events = {
850            "Cinnabar Gym": bidict(),
851            "Victory Road": bidict(),
852            "Silph Co": bidict(),
853            "Seafoam Islands": bidict(),
854        }
855        """Events related to specific map events like unlocking gates or moving boulders."""
856        for name, slot in event_names.items():
857            if name.startswith("Cinnabar Gym Gate") and name.endswith("Unlocked"):
858                self.map_events["Cinnabar Gym"][name] = slot
859                pop_queue.append(name)
860            elif name.startswith("Victory Road") and "Boulder On" in name:
861                self.map_events["Victory Road"][name] = slot
862                pop_queue.append(name)
863            elif name.startswith("Silph Co") and "Unlocked" in name:
864                self.map_events["Silph Co"][name] = slot
865                pop_queue.append(name)
866            elif name.startswith("Seafoam"):
867                self.map_events["Seafoam Islands"][name] = slot
868                pop_queue.append(name)
869        _pop(event_names, pop_queue)
870        self.cutscene_events = bidict()
871        """ Flags for cutscene based events (I think, lol). """
872
873        cutscenes = [
874            "Event 001",
875            "Daisy Walking",
876            "Pokemon Tower Rival On Left",
877            "Seel Fan Boast",
878            "Pikachu Fan Boast",
879            "Lab Handing Over Fossil Mon",
880            "Route22 Rival Wants Battle",
881        ]  # my best guess, need to verify, Silph Co Receptionist At Desk? Autowalks?
882        pop_queue = []
883        for name, slot in event_names.items():
884            if name in cutscenes:
885                self.cutscene_events[name] = slot
886                pop_queue.append(name)
887        _pop(event_names, pop_queue)
888        self.special_events = bidict(event_names)
889        """ All other events not categorized elsewhere."""
890
891        MAP_PATH = parameters["pokemon_red_rom_data_path"] + "/map_data.json"
892        with open(MAP_PATH) as map_data:
893            MAP_DATA = json.load(map_data)["regions"]
894        self._MAP_DATA = {int(e["id"]): e for e in MAP_DATA}
895
896    def get_map_name(self, map_n: int) -> Optional[str]:
897        """
898        Gets the name of the map given its identifier.
899        Args:
900            map_n (int): Map identifier.
901        Returns:
902            Optional[str]: Name of the map if found, None otherwise.
903        """
904        try:
905            return self._MAP_DATA[map_n]["name"]
906        except KeyError:
907            return None
908
909    def local_to_global(self, r: int, c: int, map_n: int) -> Tuple[int, int]:
910        """
911        Converts local map coordinates to global map coordinates.
912        Args:
913            r (int): Local row coordinate.
914            c (int): Local column coordinate.
915            map_n (int): Map identifier.
916        Returns:
917            (int, int): Global (row, column) coordinates.
918        """
919        try:
920            (
921                map_x,
922                map_y,
923            ) = self._MAP_DATA[
924                map_n
925            ]["coordinates"]
926            gy = r + map_y + self._MAP_ROW_OFFSET
927            gx = c + map_x + self._MAP_COL_OFFSET
928            if (
929                0 <= gy < self._GLOBAL_MAP_SHAPE[0]
930                and 0 <= gx < self._GLOBAL_MAP_SHAPE[1]
931            ):
932                return gy, gx
933            print(
934                f"coord out of bounds! global: ({gx}, {gy}) game: ({r}, {c}, {map_n})"
935            )
936            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2
937        except KeyError:
938            print(f"Map id {map_n} not found in map_data.json.")
939            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2
940
941    def get_opponents_defeated(self) -> Set[str]:
942        """
943        Returns a set of all defeated opponents. This function isn't actually used in any current environments, but is left here to show how to read game state information.
944        Similar functions can be created to read obtained TMs, HMs, key items, passed badge checks, etc.
945
946        Returns:
947            Set[str]: A set of names of defeated opponents.
948        """
949        return self.get_raised_flags(self.defeated_opponent_events)
950
951    def get_facing_direction(self) -> Tuple[int, int]:
952        """
953        Gets the direction the player is facing.
954        Returns:
955            (int, int): Tuple representing the direction vector (dy, dx).
956        """
957        direction = self.read_m(0xD52A)
958        if direction == 1:
959            return (0, 1)  # Right
960        elif direction == 2:
961            return (0, -1)  # Left
962        elif direction == 4:
963            return (1, 0)  # Down
964        else:
965            return (-1, 0)  # Up
966
967    def get_local_coords(self) -> Tuple[int, int, int]:
968        """
969        Gets the local game coordinates (x, y, map number).
970        Returns:
971            (int, int, int): Tuple containing (x, y, map number).
972        """
973        return (self.read_m(0xD362), self.read_m(0xD361), self.read_m(0xD35E))
974
975    def get_global_coords(self):
976        """
977        Gets the global coordinates of the player.
978        Returns:
979            (int, int): Tuple containing (global y, global x) coordinates.
980        """
981        x_pos, y_pos, map_n = self.get_local_coords()
982        return self.local_to_global(y_pos, x_pos, map_n)
983
984    def get_badges(self) -> np.array:
985        """
986        Gets the player's badges as a binary array.
987        Returns:
988            np.array: Array of 8 binary values representing whether the player has obtained each of the badges.
989        """
990        # or  self.bit_count(self.read_m(0xD356))
991        return np.array(
992            [int(bit) for bit in f"{self.read_m(0xD356):08b}"], dtype=np.int8
993        )

Game state parser for Pokemon Red. Uses memory addresses to parse game state. Can be used to reproduce https://github.com/PWhiddy/PokemonRedExperiments/ (v2) and facilitates reward engineering based on memory states.

MemoryBasedPokemonRedStateParser(pyboy, parameters)
783    def __init__(self, pyboy, parameters):
784        """
785        Initializes the Pokemon Red game state parser.
786
787        Args:
788            pyboy: An instance of the PyBoy emulator.
789            parameters: A dictionary of parameters for configuration.
790        """
791        super().__init__(pyboy, parameters=parameters)
792        events_location = parameters["pokemon_red_rom_data_path"] + "/events.json"
793        with open(events_location) as f:
794            event_slots = json.load(f)
795        event_slots = event_slots
796        event_names = {v: k for k, v in event_slots.items() if not v[0].isdigit()}
797        beat_opponent_events = bidict()
798
799        def _pop(d, keys):
800            for key in keys:
801                if key in d:
802                    d.pop(key, None)
803
804        pop_queue = []
805        for name, slot in event_names.items():
806            if name.startswith("Beat "):
807                beat_opponent_events[name.replace("Beat ", "")] = slot
808                pop_queue.append(name)
809        _pop(event_names, pop_queue)
810        self.defeated_opponent_events = beat_opponent_events
811        """Events related to beating specific opponents. E.g. Beat Brock"""
812        tms_obtained_events = bidict()
813        pop_queue = []
814        for name, slot in event_names.items():
815            if name.startswith("Got Tm"):
816                tms_obtained_events[name.replace("Got ", "").strip()] = slot
817                pop_queue.append(name)
818        _pop(event_names, pop_queue)
819        self.tms_obtained_events = tms_obtained_events
820        """Events related to obtaining specific TMs. E.g. Got Tm01"""
821        hm_obtained_events = bidict()
822        pop_queue = []
823        for name, slot in event_names.items():
824            if name.startswith("Got Hm"):
825                hm_obtained_events[name.replace("Got ", "").strip()] = slot
826                pop_queue.append(name)
827        _pop(event_names, pop_queue)
828        self.hm_obtained_events = hm_obtained_events
829        """Events related to obtaining specific HMs. E.g. Got Hm01"""
830        passed_badge_check_events = bidict()
831        pop_queue = []
832        for name, slot in event_names.items():
833            if name.startswith("Passed ") and "badge" in name:
834                passed_badge_check_events[
835                    name.replace("Passed ", "").replace(" Check", "").strip()
836                ] = slot
837                pop_queue.append(name)
838        _pop(event_names, pop_queue)
839        self.passed_badge_check_events = passed_badge_check_events
840        """Events related to passing badge checks. E.g. Passed Boulder badge check. These will only be relevant to enter Victory Road."""
841        self.key_items_obtained_events = bidict()
842        """Events related to obtaining key items. E.g. Got Bicycle"""
843        pop_queue = []
844        for name, slot in event_names.items():
845            if name.startswith("Got "):
846                self.key_items_obtained_events[name.replace("Got ", "").strip()] = slot
847                pop_queue.append(name)
848        _pop(event_names, pop_queue)
849        self.map_events = {
850            "Cinnabar Gym": bidict(),
851            "Victory Road": bidict(),
852            "Silph Co": bidict(),
853            "Seafoam Islands": bidict(),
854        }
855        """Events related to specific map events like unlocking gates or moving boulders."""
856        for name, slot in event_names.items():
857            if name.startswith("Cinnabar Gym Gate") and name.endswith("Unlocked"):
858                self.map_events["Cinnabar Gym"][name] = slot
859                pop_queue.append(name)
860            elif name.startswith("Victory Road") and "Boulder On" in name:
861                self.map_events["Victory Road"][name] = slot
862                pop_queue.append(name)
863            elif name.startswith("Silph Co") and "Unlocked" in name:
864                self.map_events["Silph Co"][name] = slot
865                pop_queue.append(name)
866            elif name.startswith("Seafoam"):
867                self.map_events["Seafoam Islands"][name] = slot
868                pop_queue.append(name)
869        _pop(event_names, pop_queue)
870        self.cutscene_events = bidict()
871        """ Flags for cutscene based events (I think, lol). """
872
873        cutscenes = [
874            "Event 001",
875            "Daisy Walking",
876            "Pokemon Tower Rival On Left",
877            "Seel Fan Boast",
878            "Pikachu Fan Boast",
879            "Lab Handing Over Fossil Mon",
880            "Route22 Rival Wants Battle",
881        ]  # my best guess, need to verify, Silph Co Receptionist At Desk? Autowalks?
882        pop_queue = []
883        for name, slot in event_names.items():
884            if name in cutscenes:
885                self.cutscene_events[name] = slot
886                pop_queue.append(name)
887        _pop(event_names, pop_queue)
888        self.special_events = bidict(event_names)
889        """ All other events not categorized elsewhere."""
890
891        MAP_PATH = parameters["pokemon_red_rom_data_path"] + "/map_data.json"
892        with open(MAP_PATH) as map_data:
893            MAP_DATA = json.load(map_data)["regions"]
894        self._MAP_DATA = {int(e["id"]): e for e in MAP_DATA}

Initializes the Pokemon Red game state parser.

Arguments:
  • pyboy: An instance of the PyBoy emulator.
  • parameters: A dictionary of parameters for configuration.
defeated_opponent_events

Events related to beating specific opponents. E.g. Beat Brock

tms_obtained_events

Events related to obtaining specific TMs. E.g. Got Tm01

hm_obtained_events

Events related to obtaining specific HMs. E.g. Got Hm01

passed_badge_check_events

Events related to passing badge checks. E.g. Passed Boulder badge check. These will only be relevant to enter Victory Road.

key_items_obtained_events

Events related to obtaining key items. E.g. Got Bicycle

map_events

Events related to specific map events like unlocking gates or moving boulders.

cutscene_events

Flags for cutscene based events (I think, lol).

special_events

All other events not categorized elsewhere.

def get_map_name(self, map_n: int) -> Optional[str]:
896    def get_map_name(self, map_n: int) -> Optional[str]:
897        """
898        Gets the name of the map given its identifier.
899        Args:
900            map_n (int): Map identifier.
901        Returns:
902            Optional[str]: Name of the map if found, None otherwise.
903        """
904        try:
905            return self._MAP_DATA[map_n]["name"]
906        except KeyError:
907            return None

Gets the name of the map given its identifier.

Arguments:
  • map_n (int): Map identifier.
Returns:

Optional[str]: Name of the map if found, None otherwise.

def local_to_global(self, r: int, c: int, map_n: int) -> Tuple[int, int]:
909    def local_to_global(self, r: int, c: int, map_n: int) -> Tuple[int, int]:
910        """
911        Converts local map coordinates to global map coordinates.
912        Args:
913            r (int): Local row coordinate.
914            c (int): Local column coordinate.
915            map_n (int): Map identifier.
916        Returns:
917            (int, int): Global (row, column) coordinates.
918        """
919        try:
920            (
921                map_x,
922                map_y,
923            ) = self._MAP_DATA[
924                map_n
925            ]["coordinates"]
926            gy = r + map_y + self._MAP_ROW_OFFSET
927            gx = c + map_x + self._MAP_COL_OFFSET
928            if (
929                0 <= gy < self._GLOBAL_MAP_SHAPE[0]
930                and 0 <= gx < self._GLOBAL_MAP_SHAPE[1]
931            ):
932                return gy, gx
933            print(
934                f"coord out of bounds! global: ({gx}, {gy}) game: ({r}, {c}, {map_n})"
935            )
936            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2
937        except KeyError:
938            print(f"Map id {map_n} not found in map_data.json.")
939            return self._GLOBAL_MAP_SHAPE[0] // 2, self._GLOBAL_MAP_SHAPE[1] // 2

Converts local map coordinates to global map coordinates.

Arguments:
  • r (int): Local row coordinate.
  • c (int): Local column coordinate.
  • map_n (int): Map identifier.
Returns:

(int, int): Global (row, column) coordinates.

def get_opponents_defeated(self) -> Set[str]:
941    def get_opponents_defeated(self) -> Set[str]:
942        """
943        Returns a set of all defeated opponents. This function isn't actually used in any current environments, but is left here to show how to read game state information.
944        Similar functions can be created to read obtained TMs, HMs, key items, passed badge checks, etc.
945
946        Returns:
947            Set[str]: A set of names of defeated opponents.
948        """
949        return self.get_raised_flags(self.defeated_opponent_events)

Returns a set of all defeated opponents. This function isn't actually used in any current environments, but is left here to show how to read game state information. Similar functions can be created to read obtained TMs, HMs, key items, passed badge checks, etc.

Returns:

Set[str]: A set of names of defeated opponents.

def get_facing_direction(self) -> Tuple[int, int]:
951    def get_facing_direction(self) -> Tuple[int, int]:
952        """
953        Gets the direction the player is facing.
954        Returns:
955            (int, int): Tuple representing the direction vector (dy, dx).
956        """
957        direction = self.read_m(0xD52A)
958        if direction == 1:
959            return (0, 1)  # Right
960        elif direction == 2:
961            return (0, -1)  # Left
962        elif direction == 4:
963            return (1, 0)  # Down
964        else:
965            return (-1, 0)  # Up

Gets the direction the player is facing.

Returns:

(int, int): Tuple representing the direction vector (dy, dx).

def get_local_coords(self) -> Tuple[int, int, int]:
967    def get_local_coords(self) -> Tuple[int, int, int]:
968        """
969        Gets the local game coordinates (x, y, map number).
970        Returns:
971            (int, int, int): Tuple containing (x, y, map number).
972        """
973        return (self.read_m(0xD362), self.read_m(0xD361), self.read_m(0xD35E))

Gets the local game coordinates (x, y, map number).

Returns:

(int, int, int): Tuple containing (x, y, map number).

def get_global_coords(self):
975    def get_global_coords(self):
976        """
977        Gets the global coordinates of the player.
978        Returns:
979            (int, int): Tuple containing (global y, global x) coordinates.
980        """
981        x_pos, y_pos, map_n = self.get_local_coords()
982        return self.local_to_global(y_pos, x_pos, map_n)

Gets the global coordinates of the player.

Returns:

(int, int): Tuple containing (global y, global x) coordinates.

def get_badges(self) -> <built-in function array>:
984    def get_badges(self) -> np.array:
985        """
986        Gets the player's badges as a binary array.
987        Returns:
988            np.array: Array of 8 binary values representing whether the player has obtained each of the badges.
989        """
990        # or  self.bit_count(self.read_m(0xD356))
991        return np.array(
992            [int(bit) for bit in f"{self.read_m(0xD356):08b}"], dtype=np.int8
993        )

Gets the player's badges as a binary array.

Returns:

np.array: Array of 8 binary values representing whether the player has obtained each of the badges.