gameboy_worlds.emulation.survival_kids.parsers

Survival Kids game state parsers.

  1"""
  2Survival Kids game state parsers.
  3"""
  4
  5import os
  6from enum import Enum
  7from typing import Dict, List, Optional, Tuple
  8
  9import numpy as np
 10
 11from gameboy_worlds.emulation.parser import (
 12    NamedScreenRegion,
 13    StateParser,
 14    _get_proper_regions,
 15)
 16from gameboy_worlds.utils import log_error, verify_parameters
 17
 18
 19def _merge_multi_targets(
 20    *multi_targets: Dict[str, List[str]],
 21) -> Dict[str, List[str]]:
 22    merged: Dict[str, List[str]] = {}
 23    for targets in multi_targets:
 24        for region_name, target_names in targets.items():
 25            region_targets = merged.setdefault(region_name, [])
 26            for target_name in target_names:
 27                if target_name not in region_targets:
 28                    region_targets.append(target_name)
 29    return merged
 30
 31
 32class AgentState(Enum):
 33    FREE_ROAM = 0
 34    IN_DIALOGUE = 1
 35    IN_MENU = 2
 36
 37
 38class SurvivalKidsParser(StateParser):
 39    """Game state parser for Survival Kids 1 (GBC)."""
 40
 41    VARIANT = "survival_kids_1"
 42    LOAD_ONLY_EXISTING_MULTI_TARGETS = False
 43    FORCE_LOAD_MISSING_MULTI_TARGETS: Dict[str, List[str]] = {}
 44
 45    MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [
 46        ("screen", 0, 0, 160, 144),
 47        ("screen_bottom", 0, 108, 160, 36),
 48        ("game_viewport", 0, 0, 160, 128),
 49        ("status_bar", 0, 128, 160, 16),
 50        ("hp_area", 0, 128, 40, 16),
 51        ("hunger_area", 0, 136, 42, 8),
 52        ("thirst_area", 42, 136, 42, 8),
 53        ("stamina_area", 84, 136, 42, 8),
 54        ("equipped_items_area", 20, 128, 24, 8),
 55        ("equipped_item_area", 0, 105, 160, 39),
 56        ("pack_icon_area", 144, 128, 16, 16),
 57        ("bag_icon_area", 64, 32, 48, 48),
 58        ("object_area", 64, 32, 48, 48),
 59        ("choose_item_area", 0, 20, 160, 80),
 60        ("dialogue_area", 8, 112, 144, 28),
 61        ("inventory_select_area", 0, 0, 88, 72),
 62        ("inventory_description_area", 8, 98, 144, 30),
 63        ("item_action_menu", 0, 0, 64, 56),
 64        ("item_action_menu_two_options", 0, 0, 64, 40),
 65        ("item_action_menu_three_options", 0, 0, 64, 57),
 66        ("inventory_item_action_menu", 8, 98, 144, 38),
 67        ("item_use_menu_area", 0, 84, 160, 56),
 68        ("merge_menu_area", 0, 24, 160, 76),
 69        ("item_action_cursor", 6, 8, 12, 8),
 70        ("item_action_options", 18, 8, 42, 28),
 71        ("menu_area", 0, 0, 160, 144),
 72        ("merge_confirm_area", 0, 101, 160, 31),
 73    ]
 74
 75    MULTI_TARGETS: Dict[str, List[str]] = {
 76        "screen": [
 77            "after_drinking_water",
 78            "after_filling_water",
 79            "day_reference",
 80            "entered_shelter",
 81            "found_river",
 82            "got_the_brdfeather",
 83            "got_the_stick",
 84            "got_the_tree_bark",
 85            "got_the_water",
 86            "inventory_select_item",
 87            "night_reference",
 88            "water_menu_open",
 89        ],
 90        "menu_area": [
 91            "inventory_after_fire_lit",
 92            "inventory_open",
 93        ],
 94        "merge_menu_area": [
 95            "merge_menu",
 96            "select_stick",
 97        ],
 98        "merge_confirm_area": [
 99            "merge_confirm",
100        ],
101        "item_action_menu_two_options": [
102            "select_take",
103            "take_leave_menu",
104            "feather_take_leave_menu",
105        ],
106        "dialogue_area": [
107            "pickup_item_dialogue",
108            "canteen_pickup_dialogue",
109            "cooked_meat_eaten_dialogue",
110            "cooked_meat_stored",
111            "got_the_clam",
112            "meat_cooked_dialogue",
113        ],
114        "bag_icon_area": [
115            "bag_icon",
116        ],
117        "equipped_item_area": [
118            "knife_equipped",
119            "use_kindling",
120        ],
121        "choose_item_area": [
122            "canteen_chosen",
123            "knife_chosen",
124            "select_kindling",
125        ],
126        "inventory_select_area": [
127            "kindling_merged",
128        ],
129        "inventory_description_area": [
130            "select_meat",
131        ],
132        "item_use_menu_area": [
133            "canteen_action_menu",
134            "canteen_drink_selected",
135            "canteen_use_selected",
136        ],
137        "item_action_menu": [
138            "canteen_take_leave_menu",
139            "cooked_meat_action_menu",
140            "cooked_meat_eat_selected",
141            "meat_take_eat_leave_menu",
142            "select_take",
143        ],
144        "inventory_item_action_menu": [
145            "burn_confirm",
146            "meat_burn_selected",
147            "select_drop",
148        ],
149        "game_viewport": [
150            "animal_killed",
151            "chapter1_path_cleared",
152            "fire_lit",
153            "grass_cut",
154            "in_the_shelter",
155            "path_after_blocking_grass",
156        ],
157    }
158
159    _DARK_PIXEL_THRESHOLD = 24
160    _BRIGHT_PIXEL_THRESHOLD = 220
161    _DIALOGUE_BRIGHT_RATIO_THRESHOLD = 0.55
162    _MENU_DARK_RATIO_THRESHOLD = 0.08
163    _MENU_BRIGHT_RATIO_THRESHOLD = 0.18
164    _FULL_MENU_BRIGHT_RATIO_THRESHOLD = 0.75
165    _BOTTOM_MENU_DARK_RATIO_MAX = 0.025
166
167    def __init__(
168        self,
169        pyboy,
170        parameters: dict,
171        additional_multi_target_regions: Optional[
172            List[Tuple[str, int, int, int, int]]
173        ] = None,
174        override_multi_targets: Optional[Dict[str, List[str]]] = None,
175    ):
176        verify_parameters(parameters)
177        additional_multi_target_regions = additional_multi_target_regions or []
178        override_multi_targets = override_multi_targets or {}
179        if f"{self.VARIANT}_rom_data_path" not in parameters:
180            log_error(
181                f"ROM data path not found for variant: {self.VARIANT}. "
182                f"Add {self.VARIANT}_rom_data_path to the config files.",
183                parameters,
184            )
185        self.rom_data_path = parameters[f"{self.VARIANT}_rom_data_path"]
186        captures_dir = os.path.join(self.rom_data_path, "captures")
187
188        multi_target_regions = _get_proper_regions(
189            override_regions=additional_multi_target_regions,
190            base_regions=self.MULTI_TARGET_REGIONS,
191        )
192
193        multi_targets: Dict[str, List[str]] = {}
194        for key, val in self.MULTI_TARGETS.items():
195            multi_targets[key] = list(val)
196        for key, val in override_multi_targets.items():
197            if key in multi_targets:
198                multi_targets[key].extend(val)
199            else:
200                multi_targets[key] = list(val)
201
202        named_screen_regions: List[NamedScreenRegion] = []
203        for region_name, x, y, w, h in multi_target_regions:
204            target_paths: Dict[str, str] = {}
205            subdir = os.path.join(captures_dir, region_name)
206            for target_name in multi_targets.get(region_name, []):
207                target_path = os.path.join(subdir, target_name)
208                root_target_path = os.path.join(captures_dir, target_name)
209                if (
210                    not os.path.exists(f"{target_path}.npy")
211                    and os.path.exists(f"{root_target_path}.npy")
212                ):
213                    target_path = root_target_path
214                force_load_missing = target_name in self.FORCE_LOAD_MISSING_MULTI_TARGETS.get(
215                    region_name, []
216                )
217                if (
218                    self.LOAD_ONLY_EXISTING_MULTI_TARGETS
219                    and not force_load_missing
220                    and not os.path.exists(f"{target_path}.npy")
221                ):
222                    continue
223                target_paths[target_name] = target_path
224            region = NamedScreenRegion(
225                region_name,
226                x,
227                y,
228                w,
229                h,
230                parameters=parameters,
231                multi_target_paths=target_paths,
232            )
233            named_screen_regions.append(region)
234
235        super().__init__(pyboy, parameters, named_screen_regions)
236
237    @staticmethod
238    def _as_gray(current_screen: np.ndarray) -> np.ndarray:
239        if current_screen.ndim == 3:
240            return current_screen[:, :, 0]
241        return current_screen
242
243    def _region_gray(self, current_screen: np.ndarray, region_name: str) -> np.ndarray:
244        return self.capture_named_region(current_screen, region_name)[:, :, 0]
245
246    def _has_text_box_like_region(self, region: np.ndarray) -> bool:
247        dark_ratio = np.mean(region <= self._DARK_PIXEL_THRESHOLD)
248        bright_ratio = np.mean(region >= self._BRIGHT_PIXEL_THRESHOLD)
249        return (
250            bright_ratio >= self._DIALOGUE_BRIGHT_RATIO_THRESHOLD
251            and dark_ratio >= 0.02
252        )
253
254    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
255        dialogue_area = self._region_gray(current_screen, "dialogue_area")
256        return self._has_text_box_like_region(dialogue_area)
257
258    def is_in_menu(self, current_screen: np.ndarray) -> bool:
259        screen = self._as_gray(current_screen)
260        top_area = screen[:96, :]
261        bottom_dialogue = self._region_gray(current_screen, "dialogue_area")
262        top_dark_ratio = np.mean(top_area <= self._DARK_PIXEL_THRESHOLD)
263        top_bright_ratio = np.mean(top_area >= self._BRIGHT_PIXEL_THRESHOLD)
264        bottom_dark_ratio = np.mean(bottom_dialogue <= self._DARK_PIXEL_THRESHOLD)
265        if top_bright_ratio >= self._FULL_MENU_BRIGHT_RATIO_THRESHOLD:
266            return True
267        return (
268            top_dark_ratio >= self._MENU_DARK_RATIO_THRESHOLD
269            and top_bright_ratio >= self._MENU_BRIGHT_RATIO_THRESHOLD
270            and bottom_dark_ratio <= self._BOTTOM_MENU_DARK_RATIO_MAX
271        )
272
273    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
274        if self.is_in_menu(current_screen):
275            return AgentState.IN_MENU
276        if self.is_in_dialogue(current_screen):
277            return AgentState.IN_DIALOGUE
278        return AgentState.FREE_ROAM
279
280    def read_memory_byte(self, address: int) -> int:
281        return self._pyboy.memory[address]
282
283    def __repr__(self) -> str:
284        return f"<SurvivalKidsParser(variant={self.VARIANT})>"
285
286
287class SurvivalKids2Parser(SurvivalKidsParser):
288    """Game state parser for Survival Kids 2 (GBC)."""
289
290    VARIANT = "survival_kids_2"
291    LOAD_ONLY_EXISTING_MULTI_TARGETS = True
292    FORCE_LOAD_MISSING_MULTI_TARGETS = {
293        "dialogue_area": [
294            "got_the_log",
295            "log_found_dialogue",
296            "water_available_dialogue",
297        ],
298        "screen": ["knife_equipped"],
299        "game_viewport": [
300            "grass_cut",
301            "grass_cut_before_pickup_log",
302            "grass_cut_before_sharp_stone",
303            "helmet_found",
304        ],
305    }
306    MULTI_TARGET_REGIONS = _get_proper_regions(
307        override_regions=[
308            ("dialogue_area", 0, 104, 160, 40),
309            ("merge_confirm_area", 0, 104, 160, 40),
310        ],
311        base_regions=SurvivalKidsParser.MULTI_TARGET_REGIONS,
312    )
313    MULTI_TARGETS: Dict[str, List[str]] = _merge_multi_targets(
314        SurvivalKidsParser.MULTI_TARGETS,
315        {
316            "dialogue_area": [
317                "fruit_found_dialogue",
318                "fruit_taken_dialogue",
319                "got_the_log",
320                "log_found_dialogue",
321                "pry_stone_dialogue",
322                "tree_bark_pickup_dialogue",
323                "water_available_dialogue",
324            ],
325            "game_viewport": [
326                "fruit_eaten",
327                "grass_cut",
328                "grass_cut_before_pickup_log",
329                "grass_cut_before_push_stone_2",
330                "grass_cut_before_sharp_stone",
331                "helmet_found",
332                "near_pry_stone",
333                "new_path_1_found",
334                "new_path_2_found",
335                "path_after_pried_stone",
336                "path_after_pushed_stone_2",
337                "path_after_blocking_grass_c1",
338                "sharp_stone_found",
339                "stone_pushed_open_2",
340            ],
341            "hunger_area": [
342                "fruit_eaten",
343            ],
344            "item_action_menu": [
345                "fruit_action_menu",
346                "fruit_eat_selected",
347                "log_action_menu",
348                "log_select_take",
349            ],
350            "item_action_menu_two_options": [
351                "fruit_action_menu",
352                "fruit_eat_selected",
353            ],
354            "object_area": [
355                "bag_icon",
356            ],
357            "equipped_item_area": [
358                "knife_equipped",
359            ],
360            "screen": [
361                "afternoon_reference",
362                "club_equipped",
363                "day_reference",
364                "fruit_eaten",
365                "got_the_sharp_stone",
366                "got_the_stone",
367                "got_the_vine",
368                "knife_equipped",
369                "near_push_stone_2",
370                "night_reference",
371                "push_stone_dialogue_2",
372                "select_club_near_pry_stone",
373            ],
374            "menu_area": [
375                "inventory_open_with_club",
376                "inventory_open_with_club_near_pry_stone",
377                "inventory_open_with_club_near_push_stone_2",
378                "inventory_select_item",
379            ],
380            "inventory_description_area": [
381                "select_club",
382                "select_log",
383            ],
384            "inventory_item_action_menu": [
385                "log_action_menu",
386                "log_select_take",
387            ],
388        },
389    )
390
391    def __repr__(self) -> str:
392        return f"<SurvivalKids2Parser(variant={self.VARIANT})>"
class AgentState(enum.Enum):
33class AgentState(Enum):
34    FREE_ROAM = 0
35    IN_DIALOGUE = 1
36    IN_MENU = 2
FREE_ROAM = <AgentState.FREE_ROAM: 0>
IN_DIALOGUE = <AgentState.IN_DIALOGUE: 1>
IN_MENU = <AgentState.IN_MENU: 2>
class SurvivalKidsParser(gameboy_worlds.emulation.parser.StateParser):
 39class SurvivalKidsParser(StateParser):
 40    """Game state parser for Survival Kids 1 (GBC)."""
 41
 42    VARIANT = "survival_kids_1"
 43    LOAD_ONLY_EXISTING_MULTI_TARGETS = False
 44    FORCE_LOAD_MISSING_MULTI_TARGETS: Dict[str, List[str]] = {}
 45
 46    MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [
 47        ("screen", 0, 0, 160, 144),
 48        ("screen_bottom", 0, 108, 160, 36),
 49        ("game_viewport", 0, 0, 160, 128),
 50        ("status_bar", 0, 128, 160, 16),
 51        ("hp_area", 0, 128, 40, 16),
 52        ("hunger_area", 0, 136, 42, 8),
 53        ("thirst_area", 42, 136, 42, 8),
 54        ("stamina_area", 84, 136, 42, 8),
 55        ("equipped_items_area", 20, 128, 24, 8),
 56        ("equipped_item_area", 0, 105, 160, 39),
 57        ("pack_icon_area", 144, 128, 16, 16),
 58        ("bag_icon_area", 64, 32, 48, 48),
 59        ("object_area", 64, 32, 48, 48),
 60        ("choose_item_area", 0, 20, 160, 80),
 61        ("dialogue_area", 8, 112, 144, 28),
 62        ("inventory_select_area", 0, 0, 88, 72),
 63        ("inventory_description_area", 8, 98, 144, 30),
 64        ("item_action_menu", 0, 0, 64, 56),
 65        ("item_action_menu_two_options", 0, 0, 64, 40),
 66        ("item_action_menu_three_options", 0, 0, 64, 57),
 67        ("inventory_item_action_menu", 8, 98, 144, 38),
 68        ("item_use_menu_area", 0, 84, 160, 56),
 69        ("merge_menu_area", 0, 24, 160, 76),
 70        ("item_action_cursor", 6, 8, 12, 8),
 71        ("item_action_options", 18, 8, 42, 28),
 72        ("menu_area", 0, 0, 160, 144),
 73        ("merge_confirm_area", 0, 101, 160, 31),
 74    ]
 75
 76    MULTI_TARGETS: Dict[str, List[str]] = {
 77        "screen": [
 78            "after_drinking_water",
 79            "after_filling_water",
 80            "day_reference",
 81            "entered_shelter",
 82            "found_river",
 83            "got_the_brdfeather",
 84            "got_the_stick",
 85            "got_the_tree_bark",
 86            "got_the_water",
 87            "inventory_select_item",
 88            "night_reference",
 89            "water_menu_open",
 90        ],
 91        "menu_area": [
 92            "inventory_after_fire_lit",
 93            "inventory_open",
 94        ],
 95        "merge_menu_area": [
 96            "merge_menu",
 97            "select_stick",
 98        ],
 99        "merge_confirm_area": [
100            "merge_confirm",
101        ],
102        "item_action_menu_two_options": [
103            "select_take",
104            "take_leave_menu",
105            "feather_take_leave_menu",
106        ],
107        "dialogue_area": [
108            "pickup_item_dialogue",
109            "canteen_pickup_dialogue",
110            "cooked_meat_eaten_dialogue",
111            "cooked_meat_stored",
112            "got_the_clam",
113            "meat_cooked_dialogue",
114        ],
115        "bag_icon_area": [
116            "bag_icon",
117        ],
118        "equipped_item_area": [
119            "knife_equipped",
120            "use_kindling",
121        ],
122        "choose_item_area": [
123            "canteen_chosen",
124            "knife_chosen",
125            "select_kindling",
126        ],
127        "inventory_select_area": [
128            "kindling_merged",
129        ],
130        "inventory_description_area": [
131            "select_meat",
132        ],
133        "item_use_menu_area": [
134            "canteen_action_menu",
135            "canteen_drink_selected",
136            "canteen_use_selected",
137        ],
138        "item_action_menu": [
139            "canteen_take_leave_menu",
140            "cooked_meat_action_menu",
141            "cooked_meat_eat_selected",
142            "meat_take_eat_leave_menu",
143            "select_take",
144        ],
145        "inventory_item_action_menu": [
146            "burn_confirm",
147            "meat_burn_selected",
148            "select_drop",
149        ],
150        "game_viewport": [
151            "animal_killed",
152            "chapter1_path_cleared",
153            "fire_lit",
154            "grass_cut",
155            "in_the_shelter",
156            "path_after_blocking_grass",
157        ],
158    }
159
160    _DARK_PIXEL_THRESHOLD = 24
161    _BRIGHT_PIXEL_THRESHOLD = 220
162    _DIALOGUE_BRIGHT_RATIO_THRESHOLD = 0.55
163    _MENU_DARK_RATIO_THRESHOLD = 0.08
164    _MENU_BRIGHT_RATIO_THRESHOLD = 0.18
165    _FULL_MENU_BRIGHT_RATIO_THRESHOLD = 0.75
166    _BOTTOM_MENU_DARK_RATIO_MAX = 0.025
167
168    def __init__(
169        self,
170        pyboy,
171        parameters: dict,
172        additional_multi_target_regions: Optional[
173            List[Tuple[str, int, int, int, int]]
174        ] = None,
175        override_multi_targets: Optional[Dict[str, List[str]]] = None,
176    ):
177        verify_parameters(parameters)
178        additional_multi_target_regions = additional_multi_target_regions or []
179        override_multi_targets = override_multi_targets or {}
180        if f"{self.VARIANT}_rom_data_path" not in parameters:
181            log_error(
182                f"ROM data path not found for variant: {self.VARIANT}. "
183                f"Add {self.VARIANT}_rom_data_path to the config files.",
184                parameters,
185            )
186        self.rom_data_path = parameters[f"{self.VARIANT}_rom_data_path"]
187        captures_dir = os.path.join(self.rom_data_path, "captures")
188
189        multi_target_regions = _get_proper_regions(
190            override_regions=additional_multi_target_regions,
191            base_regions=self.MULTI_TARGET_REGIONS,
192        )
193
194        multi_targets: Dict[str, List[str]] = {}
195        for key, val in self.MULTI_TARGETS.items():
196            multi_targets[key] = list(val)
197        for key, val in override_multi_targets.items():
198            if key in multi_targets:
199                multi_targets[key].extend(val)
200            else:
201                multi_targets[key] = list(val)
202
203        named_screen_regions: List[NamedScreenRegion] = []
204        for region_name, x, y, w, h in multi_target_regions:
205            target_paths: Dict[str, str] = {}
206            subdir = os.path.join(captures_dir, region_name)
207            for target_name in multi_targets.get(region_name, []):
208                target_path = os.path.join(subdir, target_name)
209                root_target_path = os.path.join(captures_dir, target_name)
210                if (
211                    not os.path.exists(f"{target_path}.npy")
212                    and os.path.exists(f"{root_target_path}.npy")
213                ):
214                    target_path = root_target_path
215                force_load_missing = target_name in self.FORCE_LOAD_MISSING_MULTI_TARGETS.get(
216                    region_name, []
217                )
218                if (
219                    self.LOAD_ONLY_EXISTING_MULTI_TARGETS
220                    and not force_load_missing
221                    and not os.path.exists(f"{target_path}.npy")
222                ):
223                    continue
224                target_paths[target_name] = target_path
225            region = NamedScreenRegion(
226                region_name,
227                x,
228                y,
229                w,
230                h,
231                parameters=parameters,
232                multi_target_paths=target_paths,
233            )
234            named_screen_regions.append(region)
235
236        super().__init__(pyboy, parameters, named_screen_regions)
237
238    @staticmethod
239    def _as_gray(current_screen: np.ndarray) -> np.ndarray:
240        if current_screen.ndim == 3:
241            return current_screen[:, :, 0]
242        return current_screen
243
244    def _region_gray(self, current_screen: np.ndarray, region_name: str) -> np.ndarray:
245        return self.capture_named_region(current_screen, region_name)[:, :, 0]
246
247    def _has_text_box_like_region(self, region: np.ndarray) -> bool:
248        dark_ratio = np.mean(region <= self._DARK_PIXEL_THRESHOLD)
249        bright_ratio = np.mean(region >= self._BRIGHT_PIXEL_THRESHOLD)
250        return (
251            bright_ratio >= self._DIALOGUE_BRIGHT_RATIO_THRESHOLD
252            and dark_ratio >= 0.02
253        )
254
255    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
256        dialogue_area = self._region_gray(current_screen, "dialogue_area")
257        return self._has_text_box_like_region(dialogue_area)
258
259    def is_in_menu(self, current_screen: np.ndarray) -> bool:
260        screen = self._as_gray(current_screen)
261        top_area = screen[:96, :]
262        bottom_dialogue = self._region_gray(current_screen, "dialogue_area")
263        top_dark_ratio = np.mean(top_area <= self._DARK_PIXEL_THRESHOLD)
264        top_bright_ratio = np.mean(top_area >= self._BRIGHT_PIXEL_THRESHOLD)
265        bottom_dark_ratio = np.mean(bottom_dialogue <= self._DARK_PIXEL_THRESHOLD)
266        if top_bright_ratio >= self._FULL_MENU_BRIGHT_RATIO_THRESHOLD:
267            return True
268        return (
269            top_dark_ratio >= self._MENU_DARK_RATIO_THRESHOLD
270            and top_bright_ratio >= self._MENU_BRIGHT_RATIO_THRESHOLD
271            and bottom_dark_ratio <= self._BOTTOM_MENU_DARK_RATIO_MAX
272        )
273
274    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
275        if self.is_in_menu(current_screen):
276            return AgentState.IN_MENU
277        if self.is_in_dialogue(current_screen):
278            return AgentState.IN_DIALOGUE
279        return AgentState.FREE_ROAM
280
281    def read_memory_byte(self, address: int) -> int:
282        return self._pyboy.memory[address]
283
284    def __repr__(self) -> str:
285        return f"<SurvivalKidsParser(variant={self.VARIANT})>"

Game state parser for Survival Kids 1 (GBC).

SurvivalKidsParser( pyboy, parameters: dict, additional_multi_target_regions: Optional[List[Tuple[str, int, int, int, int]]] = None, override_multi_targets: Optional[Dict[str, List[str]]] = None)
168    def __init__(
169        self,
170        pyboy,
171        parameters: dict,
172        additional_multi_target_regions: Optional[
173            List[Tuple[str, int, int, int, int]]
174        ] = None,
175        override_multi_targets: Optional[Dict[str, List[str]]] = None,
176    ):
177        verify_parameters(parameters)
178        additional_multi_target_regions = additional_multi_target_regions or []
179        override_multi_targets = override_multi_targets or {}
180        if f"{self.VARIANT}_rom_data_path" not in parameters:
181            log_error(
182                f"ROM data path not found for variant: {self.VARIANT}. "
183                f"Add {self.VARIANT}_rom_data_path to the config files.",
184                parameters,
185            )
186        self.rom_data_path = parameters[f"{self.VARIANT}_rom_data_path"]
187        captures_dir = os.path.join(self.rom_data_path, "captures")
188
189        multi_target_regions = _get_proper_regions(
190            override_regions=additional_multi_target_regions,
191            base_regions=self.MULTI_TARGET_REGIONS,
192        )
193
194        multi_targets: Dict[str, List[str]] = {}
195        for key, val in self.MULTI_TARGETS.items():
196            multi_targets[key] = list(val)
197        for key, val in override_multi_targets.items():
198            if key in multi_targets:
199                multi_targets[key].extend(val)
200            else:
201                multi_targets[key] = list(val)
202
203        named_screen_regions: List[NamedScreenRegion] = []
204        for region_name, x, y, w, h in multi_target_regions:
205            target_paths: Dict[str, str] = {}
206            subdir = os.path.join(captures_dir, region_name)
207            for target_name in multi_targets.get(region_name, []):
208                target_path = os.path.join(subdir, target_name)
209                root_target_path = os.path.join(captures_dir, target_name)
210                if (
211                    not os.path.exists(f"{target_path}.npy")
212                    and os.path.exists(f"{root_target_path}.npy")
213                ):
214                    target_path = root_target_path
215                force_load_missing = target_name in self.FORCE_LOAD_MISSING_MULTI_TARGETS.get(
216                    region_name, []
217                )
218                if (
219                    self.LOAD_ONLY_EXISTING_MULTI_TARGETS
220                    and not force_load_missing
221                    and not os.path.exists(f"{target_path}.npy")
222                ):
223                    continue
224                target_paths[target_name] = target_path
225            region = NamedScreenRegion(
226                region_name,
227                x,
228                y,
229                w,
230                h,
231                parameters=parameters,
232                multi_target_paths=target_paths,
233            )
234            named_screen_regions.append(region)
235
236        super().__init__(pyboy, parameters, named_screen_regions)

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.
VARIANT = 'survival_kids_1'
LOAD_ONLY_EXISTING_MULTI_TARGETS = False
FORCE_LOAD_MISSING_MULTI_TARGETS: Dict[str, List[str]] = {}
MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [('screen', 0, 0, 160, 144), ('screen_bottom', 0, 108, 160, 36), ('game_viewport', 0, 0, 160, 128), ('status_bar', 0, 128, 160, 16), ('hp_area', 0, 128, 40, 16), ('hunger_area', 0, 136, 42, 8), ('thirst_area', 42, 136, 42, 8), ('stamina_area', 84, 136, 42, 8), ('equipped_items_area', 20, 128, 24, 8), ('equipped_item_area', 0, 105, 160, 39), ('pack_icon_area', 144, 128, 16, 16), ('bag_icon_area', 64, 32, 48, 48), ('object_area', 64, 32, 48, 48), ('choose_item_area', 0, 20, 160, 80), ('dialogue_area', 8, 112, 144, 28), ('inventory_select_area', 0, 0, 88, 72), ('inventory_description_area', 8, 98, 144, 30), ('item_action_menu', 0, 0, 64, 56), ('item_action_menu_two_options', 0, 0, 64, 40), ('item_action_menu_three_options', 0, 0, 64, 57), ('inventory_item_action_menu', 8, 98, 144, 38), ('item_use_menu_area', 0, 84, 160, 56), ('merge_menu_area', 0, 24, 160, 76), ('item_action_cursor', 6, 8, 12, 8), ('item_action_options', 18, 8, 42, 28), ('menu_area', 0, 0, 160, 144), ('merge_confirm_area', 0, 101, 160, 31)]
MULTI_TARGETS: Dict[str, List[str]] = {'screen': ['after_drinking_water', 'after_filling_water', 'day_reference', 'entered_shelter', 'found_river', 'got_the_brdfeather', 'got_the_stick', 'got_the_tree_bark', 'got_the_water', 'inventory_select_item', 'night_reference', 'water_menu_open'], 'menu_area': ['inventory_after_fire_lit', 'inventory_open'], 'merge_menu_area': ['merge_menu', 'select_stick'], 'merge_confirm_area': ['merge_confirm'], 'item_action_menu_two_options': ['select_take', 'take_leave_menu', 'feather_take_leave_menu'], 'dialogue_area': ['pickup_item_dialogue', 'canteen_pickup_dialogue', 'cooked_meat_eaten_dialogue', 'cooked_meat_stored', 'got_the_clam', 'meat_cooked_dialogue'], 'bag_icon_area': ['bag_icon'], 'equipped_item_area': ['knife_equipped', 'use_kindling'], 'choose_item_area': ['canteen_chosen', 'knife_chosen', 'select_kindling'], 'inventory_select_area': ['kindling_merged'], 'inventory_description_area': ['select_meat'], 'item_use_menu_area': ['canteen_action_menu', 'canteen_drink_selected', 'canteen_use_selected'], 'item_action_menu': ['canteen_take_leave_menu', 'cooked_meat_action_menu', 'cooked_meat_eat_selected', 'meat_take_eat_leave_menu', 'select_take'], 'inventory_item_action_menu': ['burn_confirm', 'meat_burn_selected', 'select_drop'], 'game_viewport': ['animal_killed', 'chapter1_path_cleared', 'fire_lit', 'grass_cut', 'in_the_shelter', 'path_after_blocking_grass']}
rom_data_path

Path to the rom data directory for the game variant.

def is_in_dialogue(self, current_screen: numpy.ndarray) -> bool:
255    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
256        dialogue_area = self._region_gray(current_screen, "dialogue_area")
257        return self._has_text_box_like_region(dialogue_area)
def is_in_menu(self, current_screen: numpy.ndarray) -> bool:
259    def is_in_menu(self, current_screen: np.ndarray) -> bool:
260        screen = self._as_gray(current_screen)
261        top_area = screen[:96, :]
262        bottom_dialogue = self._region_gray(current_screen, "dialogue_area")
263        top_dark_ratio = np.mean(top_area <= self._DARK_PIXEL_THRESHOLD)
264        top_bright_ratio = np.mean(top_area >= self._BRIGHT_PIXEL_THRESHOLD)
265        bottom_dark_ratio = np.mean(bottom_dialogue <= self._DARK_PIXEL_THRESHOLD)
266        if top_bright_ratio >= self._FULL_MENU_BRIGHT_RATIO_THRESHOLD:
267            return True
268        return (
269            top_dark_ratio >= self._MENU_DARK_RATIO_THRESHOLD
270            and top_bright_ratio >= self._MENU_BRIGHT_RATIO_THRESHOLD
271            and bottom_dark_ratio <= self._BOTTOM_MENU_DARK_RATIO_MAX
272        )
def get_agent_state( self, current_screen: numpy.ndarray) -> AgentState:
274    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
275        if self.is_in_menu(current_screen):
276            return AgentState.IN_MENU
277        if self.is_in_dialogue(current_screen):
278            return AgentState.IN_DIALOGUE
279        return AgentState.FREE_ROAM
def read_memory_byte(self, address: int) -> int:
281    def read_memory_byte(self, address: int) -> int:
282        return self._pyboy.memory[address]
class SurvivalKids2Parser(SurvivalKidsParser):
288class SurvivalKids2Parser(SurvivalKidsParser):
289    """Game state parser for Survival Kids 2 (GBC)."""
290
291    VARIANT = "survival_kids_2"
292    LOAD_ONLY_EXISTING_MULTI_TARGETS = True
293    FORCE_LOAD_MISSING_MULTI_TARGETS = {
294        "dialogue_area": [
295            "got_the_log",
296            "log_found_dialogue",
297            "water_available_dialogue",
298        ],
299        "screen": ["knife_equipped"],
300        "game_viewport": [
301            "grass_cut",
302            "grass_cut_before_pickup_log",
303            "grass_cut_before_sharp_stone",
304            "helmet_found",
305        ],
306    }
307    MULTI_TARGET_REGIONS = _get_proper_regions(
308        override_regions=[
309            ("dialogue_area", 0, 104, 160, 40),
310            ("merge_confirm_area", 0, 104, 160, 40),
311        ],
312        base_regions=SurvivalKidsParser.MULTI_TARGET_REGIONS,
313    )
314    MULTI_TARGETS: Dict[str, List[str]] = _merge_multi_targets(
315        SurvivalKidsParser.MULTI_TARGETS,
316        {
317            "dialogue_area": [
318                "fruit_found_dialogue",
319                "fruit_taken_dialogue",
320                "got_the_log",
321                "log_found_dialogue",
322                "pry_stone_dialogue",
323                "tree_bark_pickup_dialogue",
324                "water_available_dialogue",
325            ],
326            "game_viewport": [
327                "fruit_eaten",
328                "grass_cut",
329                "grass_cut_before_pickup_log",
330                "grass_cut_before_push_stone_2",
331                "grass_cut_before_sharp_stone",
332                "helmet_found",
333                "near_pry_stone",
334                "new_path_1_found",
335                "new_path_2_found",
336                "path_after_pried_stone",
337                "path_after_pushed_stone_2",
338                "path_after_blocking_grass_c1",
339                "sharp_stone_found",
340                "stone_pushed_open_2",
341            ],
342            "hunger_area": [
343                "fruit_eaten",
344            ],
345            "item_action_menu": [
346                "fruit_action_menu",
347                "fruit_eat_selected",
348                "log_action_menu",
349                "log_select_take",
350            ],
351            "item_action_menu_two_options": [
352                "fruit_action_menu",
353                "fruit_eat_selected",
354            ],
355            "object_area": [
356                "bag_icon",
357            ],
358            "equipped_item_area": [
359                "knife_equipped",
360            ],
361            "screen": [
362                "afternoon_reference",
363                "club_equipped",
364                "day_reference",
365                "fruit_eaten",
366                "got_the_sharp_stone",
367                "got_the_stone",
368                "got_the_vine",
369                "knife_equipped",
370                "near_push_stone_2",
371                "night_reference",
372                "push_stone_dialogue_2",
373                "select_club_near_pry_stone",
374            ],
375            "menu_area": [
376                "inventory_open_with_club",
377                "inventory_open_with_club_near_pry_stone",
378                "inventory_open_with_club_near_push_stone_2",
379                "inventory_select_item",
380            ],
381            "inventory_description_area": [
382                "select_club",
383                "select_log",
384            ],
385            "inventory_item_action_menu": [
386                "log_action_menu",
387                "log_select_take",
388            ],
389        },
390    )
391
392    def __repr__(self) -> str:
393        return f"<SurvivalKids2Parser(variant={self.VARIANT})>"

Game state parser for Survival Kids 2 (GBC).

VARIANT = 'survival_kids_2'
LOAD_ONLY_EXISTING_MULTI_TARGETS = True
FORCE_LOAD_MISSING_MULTI_TARGETS = {'dialogue_area': ['got_the_log', 'log_found_dialogue', 'water_available_dialogue'], 'screen': ['knife_equipped'], 'game_viewport': ['grass_cut', 'grass_cut_before_pickup_log', 'grass_cut_before_sharp_stone', 'helmet_found']}
MULTI_TARGET_REGIONS = [('dialogue_area', 0, 104, 160, 40), ('merge_confirm_area', 0, 104, 160, 40), ('screen', 0, 0, 160, 144), ('screen_bottom', 0, 108, 160, 36), ('game_viewport', 0, 0, 160, 128), ('status_bar', 0, 128, 160, 16), ('hp_area', 0, 128, 40, 16), ('hunger_area', 0, 136, 42, 8), ('thirst_area', 42, 136, 42, 8), ('stamina_area', 84, 136, 42, 8), ('equipped_items_area', 20, 128, 24, 8), ('equipped_item_area', 0, 105, 160, 39), ('pack_icon_area', 144, 128, 16, 16), ('bag_icon_area', 64, 32, 48, 48), ('object_area', 64, 32, 48, 48), ('choose_item_area', 0, 20, 160, 80), ('inventory_select_area', 0, 0, 88, 72), ('inventory_description_area', 8, 98, 144, 30), ('item_action_menu', 0, 0, 64, 56), ('item_action_menu_two_options', 0, 0, 64, 40), ('item_action_menu_three_options', 0, 0, 64, 57), ('inventory_item_action_menu', 8, 98, 144, 38), ('item_use_menu_area', 0, 84, 160, 56), ('merge_menu_area', 0, 24, 160, 76), ('item_action_cursor', 6, 8, 12, 8), ('item_action_options', 18, 8, 42, 28), ('menu_area', 0, 0, 160, 144)]
MULTI_TARGETS: Dict[str, List[str]] = {'screen': ['after_drinking_water', 'after_filling_water', 'day_reference', 'entered_shelter', 'found_river', 'got_the_brdfeather', 'got_the_stick', 'got_the_tree_bark', 'got_the_water', 'inventory_select_item', 'night_reference', 'water_menu_open', 'afternoon_reference', 'club_equipped', 'fruit_eaten', 'got_the_sharp_stone', 'got_the_stone', 'got_the_vine', 'knife_equipped', 'near_push_stone_2', 'push_stone_dialogue_2', 'select_club_near_pry_stone'], 'menu_area': ['inventory_after_fire_lit', 'inventory_open', 'inventory_open_with_club', 'inventory_open_with_club_near_pry_stone', 'inventory_open_with_club_near_push_stone_2', 'inventory_select_item'], 'merge_menu_area': ['merge_menu', 'select_stick'], 'merge_confirm_area': ['merge_confirm'], 'item_action_menu_two_options': ['select_take', 'take_leave_menu', 'feather_take_leave_menu', 'fruit_action_menu', 'fruit_eat_selected'], 'dialogue_area': ['pickup_item_dialogue', 'canteen_pickup_dialogue', 'cooked_meat_eaten_dialogue', 'cooked_meat_stored', 'got_the_clam', 'meat_cooked_dialogue', 'fruit_found_dialogue', 'fruit_taken_dialogue', 'got_the_log', 'log_found_dialogue', 'pry_stone_dialogue', 'tree_bark_pickup_dialogue', 'water_available_dialogue'], 'bag_icon_area': ['bag_icon'], 'equipped_item_area': ['knife_equipped', 'use_kindling'], 'choose_item_area': ['canteen_chosen', 'knife_chosen', 'select_kindling'], 'inventory_select_area': ['kindling_merged'], 'inventory_description_area': ['select_meat', 'select_club', 'select_log'], 'item_use_menu_area': ['canteen_action_menu', 'canteen_drink_selected', 'canteen_use_selected'], 'item_action_menu': ['canteen_take_leave_menu', 'cooked_meat_action_menu', 'cooked_meat_eat_selected', 'meat_take_eat_leave_menu', 'select_take', 'fruit_action_menu', 'fruit_eat_selected', 'log_action_menu', 'log_select_take'], 'inventory_item_action_menu': ['burn_confirm', 'meat_burn_selected', 'select_drop', 'log_action_menu', 'log_select_take'], 'game_viewport': ['animal_killed', 'chapter1_path_cleared', 'fire_lit', 'grass_cut', 'in_the_shelter', 'path_after_blocking_grass', 'fruit_eaten', 'grass_cut_before_pickup_log', 'grass_cut_before_push_stone_2', 'grass_cut_before_sharp_stone', 'helmet_found', 'near_pry_stone', 'new_path_1_found', 'new_path_2_found', 'path_after_pried_stone', 'path_after_pushed_stone_2', 'path_after_blocking_grass_c1', 'sharp_stone_found', 'stone_pushed_open_2'], 'hunger_area': ['fruit_eaten'], 'object_area': ['bag_icon']}