gameboy_worlds.emulation.hamtaro.parsers
1import os 2from enum import Enum 3 4import numpy as np 5 6from gameboy_worlds.utils import verify_parameters, log_error, log_warn 7from gameboy_worlds.emulation.parser import NamedScreenRegion, StateParser 8 9 10class _BaseHamtaroParser(StateParser): 11 """ 12 Minimal parser scaffold for Hamtaro variants. 13 14 This only configures rom_data_path so the base StateParser can run. 15 """ 16 17 VARIANT = "" 18 19 def __init__(self, pyboy, parameters): 20 verify_parameters(parameters) 21 variant = self.VARIANT 22 if f"{variant}_rom_data_path" not in parameters: 23 log_error( 24 f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to config files.", 25 parameters, 26 ) 27 self.rom_data_path = parameters[f"{variant}_rom_data_path"] 28 super().__init__(pyboy, parameters) 29 30 def __repr__(self) -> str: 31 return f"{self.__class__.__name__}(variant={self.VARIANT})" 32 33 34class AgentState(Enum): 35 FREE_ROAM = 0 36 IN_DIALOGUE = 1 37 IN_MENU = 2 38 39 40class HamtaroStateParser(_BaseHamtaroParser): 41 """ 42 Hamtaro parser with optional reference-region matching. 43 44 If captured reference regions are available under rom_data/captures, they are 45 used for menu and dialogue detection. Otherwise the parser falls back to 46 broad visual heuristics so dev_play remains usable before bootstrapping. 47 """ 48 49 REGIONS = [ 50 ("dialogue_top_border", 8, 96, 144, 4), 51 ("dialogue_bottom_border", 8, 140, 144, 4), 52 ("menu_top_band", 45, 6, 25, 94), 53 ("menu_bottom_band", 10, 100, 135, 10), 54 ] 55 56 DIALOGUE_TOP_Y = 96 57 DIALOGUE_BOTTOM_Y = 140 58 59 def __init__(self, pyboy, parameters): 60 named_regions = [] 61 super().__init__(pyboy, parameters) 62 captures_dir = os.path.join(self.rom_data_path, "captures") 63 for region_name, start_x, start_y, width, height in self.REGIONS: 64 target_path = os.path.join(captures_dir, region_name) 65 if not os.path.exists(f"{target_path}.npy"): 66 target_path = None 67 region = NamedScreenRegion( 68 region_name, 69 start_x, 70 start_y, 71 width, 72 height, 73 parameters=parameters, 74 target_path=target_path, 75 ) 76 expected_shape = (height, width, 1) 77 if region.target is not None and region.target.shape != expected_shape: 78 log_warn( 79 f"Ignoring stale capture for {region_name}: expected shape {expected_shape}, found {region.target.shape}. Re-capture this region in dev_play.", 80 parameters, 81 ) 82 region.target = None 83 region.target_path = None 84 named_regions.append(region) 85 self.named_screen_regions.update( 86 {region.name: region for region in named_regions} 87 ) 88 89 def _has_target(self, region_name: str) -> bool: 90 region = self.named_screen_regions.get(region_name) 91 return region is not None and region.target is not None 92 93 def _matches_all_available(self, current_screen: np.ndarray, region_names) -> bool: 94 available_regions = [name for name in region_names if self._has_target(name)] 95 if len(available_regions) == 0: 96 return False 97 try: 98 return all( 99 self.named_region_matches_target(current_screen, region_name) 100 for region_name in available_regions 101 ) 102 except (RuntimeError, ValueError): 103 return False 104 105 def _row_dark_fraction(self, frame: np.ndarray, row_idx: int) -> float: 106 row = frame[row_idx, :, 0] 107 return float(np.mean(row < 40)) 108 109 def _region_dark_fraction( 110 self, frame: np.ndarray, x0: int, y0: int, x1: int, y1: int 111 ) -> float: 112 region = frame[y0:y1, x0:x1, 0] 113 return float(np.mean(region < 90)) 114 115 def _region_bright_fraction( 116 self, frame: np.ndarray, x0: int, y0: int, x1: int, y1: int 117 ) -> float: 118 region = frame[y0:y1, x0:x1, 0] 119 return float(np.mean(region > 180)) 120 121 def is_in_dialogue(self, current_screen: np.ndarray) -> bool: 122 if self._matches_all_available( 123 current_screen, ["dialogue_top_border", "dialogue_bottom_border"] 124 ): 125 return True 126 top_border = self._row_dark_fraction(current_screen, self.DIALOGUE_TOP_Y) 127 bottom_border = self._row_dark_fraction( 128 current_screen, self.DIALOGUE_BOTTOM_Y 129 ) 130 text_density = self._region_dark_fraction(current_screen, 12, 108, 148, 132) 131 background_brightness = self._region_bright_fraction( 132 current_screen, 12, 108, 148, 132 133 ) 134 return ( 135 top_border > 0.45 136 and bottom_border > 0.45 137 and text_density > 0.06 138 and background_brightness > 0.30 139 ) 140 141 def is_in_menu(self, current_screen: np.ndarray) -> bool: 142 if self._matches_all_available( 143 current_screen, ["menu_top_band", "menu_bottom_band"] 144 ): 145 return True 146 if self.is_in_dialogue(current_screen): 147 return False 148 top_band_dark = self._region_dark_fraction(current_screen, 0, 0, 160, 14) 149 bottom_band_dark = self._region_dark_fraction(current_screen, 0, 104, 160, 144) 150 center_bright = self._region_bright_fraction(current_screen, 20, 20, 140, 124) 151 return top_band_dark > 0.35 and bottom_band_dark > 0.35 and center_bright > 0.2 152 153 def get_agent_state(self, current_screen: np.ndarray) -> AgentState: 154 if self.is_in_menu(current_screen): 155 return AgentState.IN_MENU 156 if self.is_in_dialogue(current_screen): 157 return AgentState.IN_DIALOGUE 158 return AgentState.FREE_ROAM 159 160 161class HamtaroHamHamHeartbreakParser(HamtaroStateParser): 162 VARIANT = "hamtaro_ham_ham_heartbreak" 163 164 165class HamtaroHamHamsUniteParser(HamtaroStateParser): 166 VARIANT = "hamtaro_ham_hams_unite"
41class HamtaroStateParser(_BaseHamtaroParser): 42 """ 43 Hamtaro parser with optional reference-region matching. 44 45 If captured reference regions are available under rom_data/captures, they are 46 used for menu and dialogue detection. Otherwise the parser falls back to 47 broad visual heuristics so dev_play remains usable before bootstrapping. 48 """ 49 50 REGIONS = [ 51 ("dialogue_top_border", 8, 96, 144, 4), 52 ("dialogue_bottom_border", 8, 140, 144, 4), 53 ("menu_top_band", 45, 6, 25, 94), 54 ("menu_bottom_band", 10, 100, 135, 10), 55 ] 56 57 DIALOGUE_TOP_Y = 96 58 DIALOGUE_BOTTOM_Y = 140 59 60 def __init__(self, pyboy, parameters): 61 named_regions = [] 62 super().__init__(pyboy, parameters) 63 captures_dir = os.path.join(self.rom_data_path, "captures") 64 for region_name, start_x, start_y, width, height in self.REGIONS: 65 target_path = os.path.join(captures_dir, region_name) 66 if not os.path.exists(f"{target_path}.npy"): 67 target_path = None 68 region = NamedScreenRegion( 69 region_name, 70 start_x, 71 start_y, 72 width, 73 height, 74 parameters=parameters, 75 target_path=target_path, 76 ) 77 expected_shape = (height, width, 1) 78 if region.target is not None and region.target.shape != expected_shape: 79 log_warn( 80 f"Ignoring stale capture for {region_name}: expected shape {expected_shape}, found {region.target.shape}. Re-capture this region in dev_play.", 81 parameters, 82 ) 83 region.target = None 84 region.target_path = None 85 named_regions.append(region) 86 self.named_screen_regions.update( 87 {region.name: region for region in named_regions} 88 ) 89 90 def _has_target(self, region_name: str) -> bool: 91 region = self.named_screen_regions.get(region_name) 92 return region is not None and region.target is not None 93 94 def _matches_all_available(self, current_screen: np.ndarray, region_names) -> bool: 95 available_regions = [name for name in region_names if self._has_target(name)] 96 if len(available_regions) == 0: 97 return False 98 try: 99 return all( 100 self.named_region_matches_target(current_screen, region_name) 101 for region_name in available_regions 102 ) 103 except (RuntimeError, ValueError): 104 return False 105 106 def _row_dark_fraction(self, frame: np.ndarray, row_idx: int) -> float: 107 row = frame[row_idx, :, 0] 108 return float(np.mean(row < 40)) 109 110 def _region_dark_fraction( 111 self, frame: np.ndarray, x0: int, y0: int, x1: int, y1: int 112 ) -> float: 113 region = frame[y0:y1, x0:x1, 0] 114 return float(np.mean(region < 90)) 115 116 def _region_bright_fraction( 117 self, frame: np.ndarray, x0: int, y0: int, x1: int, y1: int 118 ) -> float: 119 region = frame[y0:y1, x0:x1, 0] 120 return float(np.mean(region > 180)) 121 122 def is_in_dialogue(self, current_screen: np.ndarray) -> bool: 123 if self._matches_all_available( 124 current_screen, ["dialogue_top_border", "dialogue_bottom_border"] 125 ): 126 return True 127 top_border = self._row_dark_fraction(current_screen, self.DIALOGUE_TOP_Y) 128 bottom_border = self._row_dark_fraction( 129 current_screen, self.DIALOGUE_BOTTOM_Y 130 ) 131 text_density = self._region_dark_fraction(current_screen, 12, 108, 148, 132) 132 background_brightness = self._region_bright_fraction( 133 current_screen, 12, 108, 148, 132 134 ) 135 return ( 136 top_border > 0.45 137 and bottom_border > 0.45 138 and text_density > 0.06 139 and background_brightness > 0.30 140 ) 141 142 def is_in_menu(self, current_screen: np.ndarray) -> bool: 143 if self._matches_all_available( 144 current_screen, ["menu_top_band", "menu_bottom_band"] 145 ): 146 return True 147 if self.is_in_dialogue(current_screen): 148 return False 149 top_band_dark = self._region_dark_fraction(current_screen, 0, 0, 160, 14) 150 bottom_band_dark = self._region_dark_fraction(current_screen, 0, 104, 160, 144) 151 center_bright = self._region_bright_fraction(current_screen, 20, 20, 140, 124) 152 return top_band_dark > 0.35 and bottom_band_dark > 0.35 and center_bright > 0.2 153 154 def get_agent_state(self, current_screen: np.ndarray) -> AgentState: 155 if self.is_in_menu(current_screen): 156 return AgentState.IN_MENU 157 if self.is_in_dialogue(current_screen): 158 return AgentState.IN_DIALOGUE 159 return AgentState.FREE_ROAM
Hamtaro parser with optional reference-region matching.
If captured reference regions are available under rom_data/captures, they are used for menu and dialogue detection. Otherwise the parser falls back to broad visual heuristics so dev_play remains usable before bootstrapping.
60 def __init__(self, pyboy, parameters): 61 named_regions = [] 62 super().__init__(pyboy, parameters) 63 captures_dir = os.path.join(self.rom_data_path, "captures") 64 for region_name, start_x, start_y, width, height in self.REGIONS: 65 target_path = os.path.join(captures_dir, region_name) 66 if not os.path.exists(f"{target_path}.npy"): 67 target_path = None 68 region = NamedScreenRegion( 69 region_name, 70 start_x, 71 start_y, 72 width, 73 height, 74 parameters=parameters, 75 target_path=target_path, 76 ) 77 expected_shape = (height, width, 1) 78 if region.target is not None and region.target.shape != expected_shape: 79 log_warn( 80 f"Ignoring stale capture for {region_name}: expected shape {expected_shape}, found {region.target.shape}. Re-capture this region in dev_play.", 81 parameters, 82 ) 83 region.target = None 84 region.target_path = None 85 named_regions.append(region) 86 self.named_screen_regions.update( 87 {region.name: region for region in named_regions} 88 )
Initializes the StateParser. Child implementations should call super().__init__() after running their code. All children must create a self.rom_data_path variable
Arguments:
- pyboy: An instance of the PyBoy emulator.
- parameters: A dictionary of parameters for configuration.
- named_screen_regions (Optional[list[NamedScreenRegion]]): A list of NamedScreenRegion objects for easy access to specific screen regions.
122 def is_in_dialogue(self, current_screen: np.ndarray) -> bool: 123 if self._matches_all_available( 124 current_screen, ["dialogue_top_border", "dialogue_bottom_border"] 125 ): 126 return True 127 top_border = self._row_dark_fraction(current_screen, self.DIALOGUE_TOP_Y) 128 bottom_border = self._row_dark_fraction( 129 current_screen, self.DIALOGUE_BOTTOM_Y 130 ) 131 text_density = self._region_dark_fraction(current_screen, 12, 108, 148, 132) 132 background_brightness = self._region_bright_fraction( 133 current_screen, 12, 108, 148, 132 134 ) 135 return ( 136 top_border > 0.45 137 and bottom_border > 0.45 138 and text_density > 0.06 139 and background_brightness > 0.30 140 )
Inherited Members
- gameboy_worlds.emulation.parser.StateParser
- named_screen_regions
- image_references
- bit_count
- read_m
- read_bits
- read_bit
- read_m_bit
- get_raised_flags
- get_current_frame
- capture_box
- capture_square_centered
- draw_box
- draw_square_centered
- capture_named_region
- compare_named_region_against_target
- named_region_matches_target
- compare_named_region_against_multi_target
- named_region_matches_multi_target
- draw_named_region
- draw_grid_overlay
- capture_grid_cells
- reform_image
- get_quadrant_frame
- get_image_reference
162class HamtaroHamHamHeartbreakParser(HamtaroStateParser): 163 VARIANT = "hamtaro_ham_ham_heartbreak"
Hamtaro parser with optional reference-region matching.
If captured reference regions are available under rom_data/captures, they are used for menu and dialogue detection. Otherwise the parser falls back to broad visual heuristics so dev_play remains usable before bootstrapping.
Inherited Members
- HamtaroStateParser
- HamtaroStateParser
- REGIONS
- DIALOGUE_TOP_Y
- DIALOGUE_BOTTOM_Y
- is_in_dialogue
- get_agent_state
- gameboy_worlds.emulation.parser.StateParser
- named_screen_regions
- image_references
- bit_count
- read_m
- read_bits
- read_bit
- read_m_bit
- get_raised_flags
- get_current_frame
- capture_box
- capture_square_centered
- draw_box
- draw_square_centered
- capture_named_region
- compare_named_region_against_target
- named_region_matches_target
- compare_named_region_against_multi_target
- named_region_matches_multi_target
- draw_named_region
- draw_grid_overlay
- capture_grid_cells
- reform_image
- get_quadrant_frame
- get_image_reference
Hamtaro parser with optional reference-region matching.
If captured reference regions are available under rom_data/captures, they are used for menu and dialogue detection. Otherwise the parser falls back to broad visual heuristics so dev_play remains usable before bootstrapping.
Inherited Members
- HamtaroStateParser
- HamtaroStateParser
- REGIONS
- DIALOGUE_TOP_Y
- DIALOGUE_BOTTOM_Y
- is_in_dialogue
- get_agent_state
- gameboy_worlds.emulation.parser.StateParser
- named_screen_regions
- image_references
- bit_count
- read_m
- read_bits
- read_bit
- read_m_bit
- get_raised_flags
- get_current_frame
- capture_box
- capture_square_centered
- draw_box
- draw_square_centered
- capture_named_region
- compare_named_region_against_target
- named_region_matches_target
- compare_named_region_against_multi_target
- named_region_matches_multi_target
- draw_named_region
- draw_grid_overlay
- capture_grid_cells
- reform_image
- get_quadrant_frame
- get_image_reference