gameboy_worlds.emulation.survival_kids.base_metrics
Base metric groups for Survival Kids.
1"""Base metric groups for Survival Kids.""" 2 3from typing import Any, Dict, Optional, Set 4 5import numpy as np 6 7from gameboy_worlds.emulation.tracker import MetricGroup, OCRegionMetric 8from gameboy_worlds.emulation.survival_kids.parsers import ( 9 AgentState, 10 SurvivalKidsParser, 11) 12 13 14class CoreSurvivalKidsMetrics(MetricGroup): 15 NAME = "survival_kids_core" 16 REQUIRED_PARSER = SurvivalKidsParser 17 18 def start(self): 19 self.total_steps_all: int = 0 20 self.episode_count: int = 0 21 super().start() 22 23 def reset(self, first: bool = False): 24 if not first: 25 self.total_steps_all += self.step_count 26 self.episode_count += 1 27 self.step_count: int = 0 28 self.agent_state: AgentState = AgentState.FREE_ROAM 29 self.in_dialogue: bool = False 30 self.in_menu: bool = False 31 32 def close(self): 33 self.final_metrics: Dict[str, Any] = { 34 "total_steps_all_episodes": self.total_steps_all + self.step_count, 35 "total_episodes": self.episode_count, 36 } 37 38 def step( 39 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 40 ): # noqa: ARG002 41 self.step_count += 1 42 self.agent_state = self.state_parser.get_agent_state(current_frame) 43 self.in_dialogue = self.agent_state == AgentState.IN_DIALOGUE 44 self.in_menu = self.agent_state == AgentState.IN_MENU 45 46 def report(self) -> Dict[str, Any]: 47 return { 48 "agent_state": self.agent_state, 49 "in_dialogue": self.in_dialogue, 50 "in_menu": self.in_menu, 51 "step_count": self.step_count, 52 } 53 54 def report_final(self) -> Dict[str, Any]: 55 return { 56 "total_steps_all_episodes": self.total_steps_all + self.step_count, 57 "total_episodes": self.episode_count, 58 } 59 60 61class SurvivalKidsExploreMetrics(MetricGroup): 62 NAME = "survival_kids_explore" 63 REQUIRED_PARSER = SurvivalKidsParser 64 65 _HASH_W = 20 66 _HASH_H = 18 67 68 def start(self): 69 self._all_seen_hashes: Set[int] = set() 70 self.total_episodes: int = 0 71 super().start() 72 73 def reset(self, first: bool = False): 74 if not first: 75 self.total_episodes += 1 76 self._episode_hashes: Set[int] = set() 77 self.steps_exploring: int = 0 78 self.step_count: int = 0 79 80 def close(self): 81 self.final_metrics: Dict[str, Any] = { 82 "unique_frames_total": len(self._all_seen_hashes), 83 "total_episodes": self.total_episodes, 84 } 85 86 @staticmethod 87 def _hash_frame(frame: np.ndarray, w: int, h: int) -> int: 88 small = frame[::144 // h, ::160 // w, 0] 89 return hash(small.tobytes()) 90 91 def step( 92 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 93 ): # noqa: ARG002 94 self.step_count += 1 95 h = self._hash_frame(current_frame, self._HASH_W, self._HASH_H) 96 if h not in self._episode_hashes: 97 self._episode_hashes.add(h) 98 self._all_seen_hashes.add(h) 99 self.steps_exploring += 1 100 101 def report(self) -> Dict[str, Any]: 102 unique = len(self._episode_hashes) 103 ratio = unique / self.step_count if self.step_count > 0 else 0.0 104 return { 105 "unique_frames_episode": unique, 106 "total_steps_exploring": self.steps_exploring, 107 "exploration_ratio": round(ratio, 4), 108 } 109 110 def report_final(self) -> Dict[str, Any]: 111 return { 112 "unique_frames_total": len(self._all_seen_hashes), 113 "total_episodes": self.total_episodes, 114 } 115 116 117class SurvivalKidsVitalMetrics(MetricGroup): 118 NAME = "survival_kids_vitals" 119 REQUIRED_PARSER = SurvivalKidsParser 120 121 _ADDR_HP: Optional[int] = None 122 _ADDR_HUNGER: Optional[int] = None 123 _ADDR_THIRST: Optional[int] = None 124 _ADDR_STAMINA: Optional[int] = None 125 _CRITICAL_THRESHOLD: int = 200 126 127 def _read(self, address: Optional[int]) -> Optional[int]: 128 if address is None: 129 return None 130 return self.state_parser.read_memory_byte(address) 131 132 def start(self): 133 super().start() 134 135 def reset(self, first: bool = False): 136 self.hp: Optional[int] = None 137 self.hunger: Optional[int] = None 138 self.thirst: Optional[int] = None 139 self.stamina: Optional[int] = None 140 self._min_hp: Optional[int] = None 141 self._times_starving: int = 0 142 self._times_dehydrated: int = 0 143 144 def close(self): 145 self.final_metrics: Dict[str, Any] = { 146 "min_hp_seen": self._min_hp, 147 "times_starving": self._times_starving, 148 "times_dehydrated": self._times_dehydrated, 149 } 150 151 def step( 152 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 153 ): # noqa: ARG002 154 self.hp = self._read(self._ADDR_HP) 155 self.hunger = self._read(self._ADDR_HUNGER) 156 self.thirst = self._read(self._ADDR_THIRST) 157 self.stamina = self._read(self._ADDR_STAMINA) 158 if self.hp is not None: 159 if self._min_hp is None or self.hp < self._min_hp: 160 self._min_hp = self.hp 161 if self.hunger is not None and self.hunger >= self._CRITICAL_THRESHOLD: 162 self._times_starving += 1 163 if self.thirst is not None and self.thirst >= self._CRITICAL_THRESHOLD: 164 self._times_dehydrated += 1 165 166 def report(self) -> Dict[str, Any]: 167 return { 168 "hp": self.hp, 169 "hunger": self.hunger, 170 "thirst": self.thirst, 171 "stamina": self.stamina, 172 } 173 174 def report_final(self) -> Dict[str, Any]: 175 return { 176 "min_hp_seen": self._min_hp, 177 "times_starving": self._times_starving, 178 "times_dehydrated": self._times_dehydrated, 179 } 180 181 182class SurvivalKidsHudMetrics(MetricGroup): 183 NAME = "survival_kids_hud" 184 REQUIRED_PARSER = SurvivalKidsParser 185 186 _REGIONS = [ 187 "status_bar", 188 "hp_area", 189 "hunger_area", 190 "thirst_area", 191 "stamina_area", 192 "equipped_items_area", 193 "equipped_item_area", 194 "pack_icon_area", 195 ] 196 _CHANGE_MAE_THRESHOLD = 10 197 198 def reset(self, first: bool = False): # noqa: ARG002 199 self._baselines: Dict[str, Optional[np.ndarray]] = { 200 region: None for region in self._REGIONS 201 } 202 self._mae: Dict[str, float] = {region: 0.0 for region in self._REGIONS} 203 self._changed: Dict[str, bool] = {region: False for region in self._REGIONS} 204 205 def close(self): 206 self.final_metrics = { 207 f"{region}_changed": self._changed[region] for region in self._REGIONS 208 } 209 210 def step( 211 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 212 ): # noqa: ARG002 213 for region in self._REGIONS: 214 cropped = self.state_parser.capture_named_region(current_frame, region) 215 if self._baselines[region] is None: 216 self._baselines[region] = cropped.copy() 217 self._mae[region] = 0.0 218 self._changed[region] = False 219 continue 220 mae = np.abs( 221 cropped.astype(float) - self._baselines[region].astype(float) 222 ).mean() 223 self._mae[region] = float(mae) 224 self._changed[region] = mae > self._CHANGE_MAE_THRESHOLD 225 226 def report(self) -> Dict[str, Any]: 227 report: Dict[str, Any] = {} 228 for region in self._REGIONS: 229 report[f"{region}_mae"] = round(self._mae[region], 4) 230 report[f"{region}_changed"] = self._changed[region] 231 return report 232 233 def report_final(self) -> Dict[str, Any]: 234 return self.final_metrics 235 236 237class SurvivalKidsOCRMetric(OCRegionMetric): 238 """Expose OCR regions only when Survival Kids shows readable UI text.""" 239 240 REQUIRED_PARSER = SurvivalKidsParser 241 242 def start(self): 243 self.kinds = { 244 "dialogue": "dialogue_area", 245 "dialogue_bottom": "screen_bottom", 246 "menu": "menu_area", 247 } 248 super().start() 249 250 def can_read_kind(self, current_frame: np.ndarray, kind: str) -> bool: 251 if kind in {"dialogue", "dialogue_bottom"}: 252 return self.state_parser.is_in_dialogue(current_frame) 253 if kind == "menu": 254 return self.state_parser.is_in_menu(current_frame) 255 return False
15class CoreSurvivalKidsMetrics(MetricGroup): 16 NAME = "survival_kids_core" 17 REQUIRED_PARSER = SurvivalKidsParser 18 19 def start(self): 20 self.total_steps_all: int = 0 21 self.episode_count: int = 0 22 super().start() 23 24 def reset(self, first: bool = False): 25 if not first: 26 self.total_steps_all += self.step_count 27 self.episode_count += 1 28 self.step_count: int = 0 29 self.agent_state: AgentState = AgentState.FREE_ROAM 30 self.in_dialogue: bool = False 31 self.in_menu: bool = False 32 33 def close(self): 34 self.final_metrics: Dict[str, Any] = { 35 "total_steps_all_episodes": self.total_steps_all + self.step_count, 36 "total_episodes": self.episode_count, 37 } 38 39 def step( 40 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 41 ): # noqa: ARG002 42 self.step_count += 1 43 self.agent_state = self.state_parser.get_agent_state(current_frame) 44 self.in_dialogue = self.agent_state == AgentState.IN_DIALOGUE 45 self.in_menu = self.agent_state == AgentState.IN_MENU 46 47 def report(self) -> Dict[str, Any]: 48 return { 49 "agent_state": self.agent_state, 50 "in_dialogue": self.in_dialogue, 51 "in_menu": self.in_menu, 52 "step_count": self.step_count, 53 } 54 55 def report_final(self) -> Dict[str, Any]: 56 return { 57 "total_steps_all_episodes": self.total_steps_all + self.step_count, 58 "total_episodes": self.episode_count, 59 }
Abstract Base class for organizing related metrics.
Documentation Guidlines:
Every subchild should document the following in their class docstrings:
- Reports (List of keys that are present in the return dict of
report) - Final Reports (List of keys that are present in the return dict of
report_final)
The StateParser which implements the minimum required functionality for this MetricGroup to work.
19 def start(self): 20 self.total_steps_all: int = 0 21 self.episode_count: int = 0 22 super().start()
Called once when environment starts. All subclasses should call super() AFTER initializing their own variables. Only variables that will persist across episodes should be initialized here.
24 def reset(self, first: bool = False): 25 if not first: 26 self.total_steps_all += self.step_count 27 self.episode_count += 1 28 self.step_count: int = 0 29 self.agent_state: AgentState = AgentState.FREE_ROAM 30 self.in_dialogue: bool = False 31 self.in_menu: bool = False
Called when environment resets.
Arguments:
- first (bool): Whether this is the first reset of the environment. If True, might need to aggregate metrics into running final totals.
33 def close(self): 34 self.final_metrics: Dict[str, Any] = { 35 "total_steps_all_episodes": self.total_steps_all + self.step_count, 36 "total_episodes": self.episode_count, 37 }
Called when environment closes. Good for computing summary stats.
Step will not be called after this.
39 def step( 40 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 41 ): # noqa: ARG002 42 self.step_count += 1 43 self.agent_state = self.state_parser.get_agent_state(current_frame) 44 self.in_dialogue = self.agent_state == AgentState.IN_DIALOGUE 45 self.in_menu = self.agent_state == AgentState.IN_MENU
Called each environment step to update metrics.
Arguments:
- current_frame (np.ndarray): The current frame rendered by the emulator.
- recent_frames (Optional[np.ndarray]): The stack of frames that were rendered during the last action. Shape is [n_frames, height, width, channels]. Can be None if rendering is disabled.
47 def report(self) -> Dict[str, Any]: 48 return { 49 "agent_state": self.agent_state, 50 "in_dialogue": self.in_dialogue, 51 "in_menu": self.in_menu, 52 "step_count": self.step_count, 53 }
Return metrics as dictionary for instantaneous variable tracking.
Returns
Dictionary of metrics
55 def report_final(self) -> Dict[str, Any]: 56 return { 57 "total_steps_all_episodes": self.total_steps_all + self.step_count, 58 "total_episodes": self.episode_count, 59 }
Return metrics as dictionary for logging. Called at end of environment (before close).
Will never be called before self.close.
Returns
Dictionary of metrics
62class SurvivalKidsExploreMetrics(MetricGroup): 63 NAME = "survival_kids_explore" 64 REQUIRED_PARSER = SurvivalKidsParser 65 66 _HASH_W = 20 67 _HASH_H = 18 68 69 def start(self): 70 self._all_seen_hashes: Set[int] = set() 71 self.total_episodes: int = 0 72 super().start() 73 74 def reset(self, first: bool = False): 75 if not first: 76 self.total_episodes += 1 77 self._episode_hashes: Set[int] = set() 78 self.steps_exploring: int = 0 79 self.step_count: int = 0 80 81 def close(self): 82 self.final_metrics: Dict[str, Any] = { 83 "unique_frames_total": len(self._all_seen_hashes), 84 "total_episodes": self.total_episodes, 85 } 86 87 @staticmethod 88 def _hash_frame(frame: np.ndarray, w: int, h: int) -> int: 89 small = frame[::144 // h, ::160 // w, 0] 90 return hash(small.tobytes()) 91 92 def step( 93 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 94 ): # noqa: ARG002 95 self.step_count += 1 96 h = self._hash_frame(current_frame, self._HASH_W, self._HASH_H) 97 if h not in self._episode_hashes: 98 self._episode_hashes.add(h) 99 self._all_seen_hashes.add(h) 100 self.steps_exploring += 1 101 102 def report(self) -> Dict[str, Any]: 103 unique = len(self._episode_hashes) 104 ratio = unique / self.step_count if self.step_count > 0 else 0.0 105 return { 106 "unique_frames_episode": unique, 107 "total_steps_exploring": self.steps_exploring, 108 "exploration_ratio": round(ratio, 4), 109 } 110 111 def report_final(self) -> Dict[str, Any]: 112 return { 113 "unique_frames_total": len(self._all_seen_hashes), 114 "total_episodes": self.total_episodes, 115 }
Abstract Base class for organizing related metrics.
Documentation Guidlines:
Every subchild should document the following in their class docstrings:
- Reports (List of keys that are present in the return dict of
report) - Final Reports (List of keys that are present in the return dict of
report_final)
The StateParser which implements the minimum required functionality for this MetricGroup to work.
69 def start(self): 70 self._all_seen_hashes: Set[int] = set() 71 self.total_episodes: int = 0 72 super().start()
Called once when environment starts. All subclasses should call super() AFTER initializing their own variables. Only variables that will persist across episodes should be initialized here.
74 def reset(self, first: bool = False): 75 if not first: 76 self.total_episodes += 1 77 self._episode_hashes: Set[int] = set() 78 self.steps_exploring: int = 0 79 self.step_count: int = 0
Called when environment resets.
Arguments:
- first (bool): Whether this is the first reset of the environment. If True, might need to aggregate metrics into running final totals.
81 def close(self): 82 self.final_metrics: Dict[str, Any] = { 83 "unique_frames_total": len(self._all_seen_hashes), 84 "total_episodes": self.total_episodes, 85 }
Called when environment closes. Good for computing summary stats.
Step will not be called after this.
92 def step( 93 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 94 ): # noqa: ARG002 95 self.step_count += 1 96 h = self._hash_frame(current_frame, self._HASH_W, self._HASH_H) 97 if h not in self._episode_hashes: 98 self._episode_hashes.add(h) 99 self._all_seen_hashes.add(h) 100 self.steps_exploring += 1
Called each environment step to update metrics.
Arguments:
- current_frame (np.ndarray): The current frame rendered by the emulator.
- recent_frames (Optional[np.ndarray]): The stack of frames that were rendered during the last action. Shape is [n_frames, height, width, channels]. Can be None if rendering is disabled.
102 def report(self) -> Dict[str, Any]: 103 unique = len(self._episode_hashes) 104 ratio = unique / self.step_count if self.step_count > 0 else 0.0 105 return { 106 "unique_frames_episode": unique, 107 "total_steps_exploring": self.steps_exploring, 108 "exploration_ratio": round(ratio, 4), 109 }
Return metrics as dictionary for instantaneous variable tracking.
Returns
Dictionary of metrics
111 def report_final(self) -> Dict[str, Any]: 112 return { 113 "unique_frames_total": len(self._all_seen_hashes), 114 "total_episodes": self.total_episodes, 115 }
Return metrics as dictionary for logging. Called at end of environment (before close).
Will never be called before self.close.
Returns
Dictionary of metrics
118class SurvivalKidsVitalMetrics(MetricGroup): 119 NAME = "survival_kids_vitals" 120 REQUIRED_PARSER = SurvivalKidsParser 121 122 _ADDR_HP: Optional[int] = None 123 _ADDR_HUNGER: Optional[int] = None 124 _ADDR_THIRST: Optional[int] = None 125 _ADDR_STAMINA: Optional[int] = None 126 _CRITICAL_THRESHOLD: int = 200 127 128 def _read(self, address: Optional[int]) -> Optional[int]: 129 if address is None: 130 return None 131 return self.state_parser.read_memory_byte(address) 132 133 def start(self): 134 super().start() 135 136 def reset(self, first: bool = False): 137 self.hp: Optional[int] = None 138 self.hunger: Optional[int] = None 139 self.thirst: Optional[int] = None 140 self.stamina: Optional[int] = None 141 self._min_hp: Optional[int] = None 142 self._times_starving: int = 0 143 self._times_dehydrated: int = 0 144 145 def close(self): 146 self.final_metrics: Dict[str, Any] = { 147 "min_hp_seen": self._min_hp, 148 "times_starving": self._times_starving, 149 "times_dehydrated": self._times_dehydrated, 150 } 151 152 def step( 153 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 154 ): # noqa: ARG002 155 self.hp = self._read(self._ADDR_HP) 156 self.hunger = self._read(self._ADDR_HUNGER) 157 self.thirst = self._read(self._ADDR_THIRST) 158 self.stamina = self._read(self._ADDR_STAMINA) 159 if self.hp is not None: 160 if self._min_hp is None or self.hp < self._min_hp: 161 self._min_hp = self.hp 162 if self.hunger is not None and self.hunger >= self._CRITICAL_THRESHOLD: 163 self._times_starving += 1 164 if self.thirst is not None and self.thirst >= self._CRITICAL_THRESHOLD: 165 self._times_dehydrated += 1 166 167 def report(self) -> Dict[str, Any]: 168 return { 169 "hp": self.hp, 170 "hunger": self.hunger, 171 "thirst": self.thirst, 172 "stamina": self.stamina, 173 } 174 175 def report_final(self) -> Dict[str, Any]: 176 return { 177 "min_hp_seen": self._min_hp, 178 "times_starving": self._times_starving, 179 "times_dehydrated": self._times_dehydrated, 180 }
Abstract Base class for organizing related metrics.
Documentation Guidlines:
Every subchild should document the following in their class docstrings:
- Reports (List of keys that are present in the return dict of
report) - Final Reports (List of keys that are present in the return dict of
report_final)
The StateParser which implements the minimum required functionality for this MetricGroup to work.
Called once when environment starts. All subclasses should call super() AFTER initializing their own variables. Only variables that will persist across episodes should be initialized here.
136 def reset(self, first: bool = False): 137 self.hp: Optional[int] = None 138 self.hunger: Optional[int] = None 139 self.thirst: Optional[int] = None 140 self.stamina: Optional[int] = None 141 self._min_hp: Optional[int] = None 142 self._times_starving: int = 0 143 self._times_dehydrated: int = 0
Called when environment resets.
Arguments:
- first (bool): Whether this is the first reset of the environment. If True, might need to aggregate metrics into running final totals.
145 def close(self): 146 self.final_metrics: Dict[str, Any] = { 147 "min_hp_seen": self._min_hp, 148 "times_starving": self._times_starving, 149 "times_dehydrated": self._times_dehydrated, 150 }
Called when environment closes. Good for computing summary stats.
Step will not be called after this.
152 def step( 153 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 154 ): # noqa: ARG002 155 self.hp = self._read(self._ADDR_HP) 156 self.hunger = self._read(self._ADDR_HUNGER) 157 self.thirst = self._read(self._ADDR_THIRST) 158 self.stamina = self._read(self._ADDR_STAMINA) 159 if self.hp is not None: 160 if self._min_hp is None or self.hp < self._min_hp: 161 self._min_hp = self.hp 162 if self.hunger is not None and self.hunger >= self._CRITICAL_THRESHOLD: 163 self._times_starving += 1 164 if self.thirst is not None and self.thirst >= self._CRITICAL_THRESHOLD: 165 self._times_dehydrated += 1
Called each environment step to update metrics.
Arguments:
- current_frame (np.ndarray): The current frame rendered by the emulator.
- recent_frames (Optional[np.ndarray]): The stack of frames that were rendered during the last action. Shape is [n_frames, height, width, channels]. Can be None if rendering is disabled.
167 def report(self) -> Dict[str, Any]: 168 return { 169 "hp": self.hp, 170 "hunger": self.hunger, 171 "thirst": self.thirst, 172 "stamina": self.stamina, 173 }
Return metrics as dictionary for instantaneous variable tracking.
Returns
Dictionary of metrics
175 def report_final(self) -> Dict[str, Any]: 176 return { 177 "min_hp_seen": self._min_hp, 178 "times_starving": self._times_starving, 179 "times_dehydrated": self._times_dehydrated, 180 }
Return metrics as dictionary for logging. Called at end of environment (before close).
Will never be called before self.close.
Returns
Dictionary of metrics
183class SurvivalKidsHudMetrics(MetricGroup): 184 NAME = "survival_kids_hud" 185 REQUIRED_PARSER = SurvivalKidsParser 186 187 _REGIONS = [ 188 "status_bar", 189 "hp_area", 190 "hunger_area", 191 "thirst_area", 192 "stamina_area", 193 "equipped_items_area", 194 "equipped_item_area", 195 "pack_icon_area", 196 ] 197 _CHANGE_MAE_THRESHOLD = 10 198 199 def reset(self, first: bool = False): # noqa: ARG002 200 self._baselines: Dict[str, Optional[np.ndarray]] = { 201 region: None for region in self._REGIONS 202 } 203 self._mae: Dict[str, float] = {region: 0.0 for region in self._REGIONS} 204 self._changed: Dict[str, bool] = {region: False for region in self._REGIONS} 205 206 def close(self): 207 self.final_metrics = { 208 f"{region}_changed": self._changed[region] for region in self._REGIONS 209 } 210 211 def step( 212 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 213 ): # noqa: ARG002 214 for region in self._REGIONS: 215 cropped = self.state_parser.capture_named_region(current_frame, region) 216 if self._baselines[region] is None: 217 self._baselines[region] = cropped.copy() 218 self._mae[region] = 0.0 219 self._changed[region] = False 220 continue 221 mae = np.abs( 222 cropped.astype(float) - self._baselines[region].astype(float) 223 ).mean() 224 self._mae[region] = float(mae) 225 self._changed[region] = mae > self._CHANGE_MAE_THRESHOLD 226 227 def report(self) -> Dict[str, Any]: 228 report: Dict[str, Any] = {} 229 for region in self._REGIONS: 230 report[f"{region}_mae"] = round(self._mae[region], 4) 231 report[f"{region}_changed"] = self._changed[region] 232 return report 233 234 def report_final(self) -> Dict[str, Any]: 235 return self.final_metrics
Abstract Base class for organizing related metrics.
Documentation Guidlines:
Every subchild should document the following in their class docstrings:
- Reports (List of keys that are present in the return dict of
report) - Final Reports (List of keys that are present in the return dict of
report_final)
The StateParser which implements the minimum required functionality for this MetricGroup to work.
199 def reset(self, first: bool = False): # noqa: ARG002 200 self._baselines: Dict[str, Optional[np.ndarray]] = { 201 region: None for region in self._REGIONS 202 } 203 self._mae: Dict[str, float] = {region: 0.0 for region in self._REGIONS} 204 self._changed: Dict[str, bool] = {region: False for region in self._REGIONS}
Called when environment resets.
Arguments:
- first (bool): Whether this is the first reset of the environment. If True, might need to aggregate metrics into running final totals.
206 def close(self): 207 self.final_metrics = { 208 f"{region}_changed": self._changed[region] for region in self._REGIONS 209 }
Called when environment closes. Good for computing summary stats.
Step will not be called after this.
211 def step( 212 self, current_frame: np.ndarray, recent_frames: Optional[np.ndarray] 213 ): # noqa: ARG002 214 for region in self._REGIONS: 215 cropped = self.state_parser.capture_named_region(current_frame, region) 216 if self._baselines[region] is None: 217 self._baselines[region] = cropped.copy() 218 self._mae[region] = 0.0 219 self._changed[region] = False 220 continue 221 mae = np.abs( 222 cropped.astype(float) - self._baselines[region].astype(float) 223 ).mean() 224 self._mae[region] = float(mae) 225 self._changed[region] = mae > self._CHANGE_MAE_THRESHOLD
Called each environment step to update metrics.
Arguments:
- current_frame (np.ndarray): The current frame rendered by the emulator.
- recent_frames (Optional[np.ndarray]): The stack of frames that were rendered during the last action. Shape is [n_frames, height, width, channels]. Can be None if rendering is disabled.
227 def report(self) -> Dict[str, Any]: 228 report: Dict[str, Any] = {} 229 for region in self._REGIONS: 230 report[f"{region}_mae"] = round(self._mae[region], 4) 231 report[f"{region}_changed"] = self._changed[region] 232 return report
Return metrics as dictionary for instantaneous variable tracking.
Returns
Dictionary of metrics
238class SurvivalKidsOCRMetric(OCRegionMetric): 239 """Expose OCR regions only when Survival Kids shows readable UI text.""" 240 241 REQUIRED_PARSER = SurvivalKidsParser 242 243 def start(self): 244 self.kinds = { 245 "dialogue": "dialogue_area", 246 "dialogue_bottom": "screen_bottom", 247 "menu": "menu_area", 248 } 249 super().start() 250 251 def can_read_kind(self, current_frame: np.ndarray, kind: str) -> bool: 252 if kind in {"dialogue", "dialogue_bottom"}: 253 return self.state_parser.is_in_dialogue(current_frame) 254 if kind == "menu": 255 return self.state_parser.is_in_menu(current_frame) 256 return False
Expose OCR regions only when Survival Kids shows readable UI text.
The StateParser which implements the minimum required functionality for this MetricGroup to work.
243 def start(self): 244 self.kinds = { 245 "dialogue": "dialogue_area", 246 "dialogue_bottom": "screen_bottom", 247 "menu": "menu_area", 248 } 249 super().start()
Assumes the child has initialized a dict called self.kinds which tracks the various kinds of OCR that could be done. self.kinds should be in the form: {kind: region_name} where region_name is the name of the region to OCR for that kind. Will track ocr captured region results in form of list of dictionaries where these kinds are keys.
251 def can_read_kind(self, current_frame: np.ndarray, kind: str) -> bool: 252 if kind in {"dialogue", "dialogue_bottom"}: 253 return self.state_parser.is_in_dialogue(current_frame) 254 if kind == "menu": 255 return self.state_parser.is_in_menu(current_frame) 256 return False
Checks if the frame has text for the given kind.
Arguments:
- frame (np.ndarray): The frame to check.
- kind (str): The kind of text to check for.