gameboy_worlds.emulation.sword_of_hope.parsers

  1import os
  2from typing import List, Tuple, Dict
  3
  4from gameboy_worlds.utils import verify_parameters, log_error
  5from gameboy_worlds.emulation.parser import StateParser, NamedScreenRegion, _get_proper_regions
  6
  7
  8class _BaseSwordOfHopeParser(StateParser):
  9    """
 10    Base parser for Sword of Hope variants.
 11    Defines common screen regions for the SoH UI layout:
 12      - Top stats bar (LV, HP, MP)
 13      - Game viewport + Gold/EX
 14      - Status bar (MOVE | ROOM NAME)
 15      - Command grid (Look/Use/Open/Magic/Hit/Power + directional pad)
 16    """
 17
 18    VARIANT = ""
 19
 20    COMMON_MULTI_TARGET_REGIONS = [
 21        ("room_label", 48, 72, 104, 16),
 22        ("game_viewport", 4, 16, 92, 56),
 23        ("status_bar", 4, 72, 152, 16),
 24        ("command_area", 4, 88, 152, 56),
 25        ("battle_full", 4, 16, 152, 128),
 26        ("battle_viewport", 4, 16, 152, 56),
 27        ("battle_command", 4, 74, 152, 70),
 28        ("status_command", 4, 72, 152, 72),
 29        ("full_screen", 0, 0, 160, 144),
 30        ("soh2_enemy_viewport", 4, 0, 152, 68),
 31        ("soh2_battle_command", 4, 68, 152, 19),
 32        ("soh2_party_stats", 4, 87, 152, 48),
 33        ("soh2_room_label", 8, 4, 144, 20),
 34    ]
 35    """
 36    - room_label: The room name text on the right side of the MOVE|ROOM status bar. Used for location-based termination.
 37    - game_viewport: The rendered scene area (top-left quadrant of the screen, below stats).
 38    - status_bar: The full MOVE | ROOM NAME bar.
 39    - command_area: The bottom command grid (directional pad + action commands).
 40    - battle_full: Everything below the LV/HP/MP stats bar (enemy sprites + battle commands). Used for battle detection.
 41    - battle_viewport: Full-width enemy sprite area (wider than game_viewport to include right side).
 42    - battle_command: Battle command area including FIGHT/AUTO row + MAGIC/USE/ESCAPE + enemy list.
 43    - status_command: Status bar + command area combined (y=72 to y=144). Useful for detecting interaction results where text changes span both areas.
 44    - soh2_enemy_viewport: SoH2 only. Enemy sprite area; SoH2 has no top stats bar so the sprite extends to y=0.
 45    - soh2_battle_command: SoH2 only. Horizontal command row (CLASH/FLEE/AUTO solo, MAGIC/ITEM/etc with party).
 46    - soh2_party_stats: SoH2 only. Live LV/HP/MP table at the bottom. Values change per frame so this is mainly for VLM/OCR, not for .npy match termination.
 47    - soh2_room_label: SoH2 only. The room name header at the TOP of the screen (SoH2 puts the room name above the viewport, not in the bottom MOVE bar like SoH1).
 48    """
 49
 50    COMMON_MULTI_TARGETS = {}
 51
 52    def __init__(
 53        self,
 54        pyboy,
 55        parameters,
 56        additional_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
 57        override_multi_targets: Dict[str, List[str]] = {},
 58    ):
 59        verify_parameters(parameters)
 60        variant = self.VARIANT
 61        if f"{variant}_rom_data_path" not in parameters:
 62            log_error(
 63                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to config files.",
 64                parameters,
 65            )
 66        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
 67        captures_dir = os.path.join(self.rom_data_path, "captures")
 68
 69        multi_target_regions = _get_proper_regions(
 70            override_regions=additional_multi_target_regions,
 71            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
 72        )
 73
 74        multi_targets = {}
 75        for key, val in self.COMMON_MULTI_TARGETS.items():
 76            multi_targets[key] = list(val)
 77        for key in override_multi_targets:
 78            if key in multi_targets:
 79                multi_targets[key].extend(override_multi_targets[key])
 80            else:
 81                multi_targets[key] = override_multi_targets[key]
 82
 83        named_screen_regions = []
 84        for region_name, x, y, w, h in multi_target_regions:
 85            region_target_paths = {}
 86            subdir = os.path.join(captures_dir, region_name)
 87            for target_name in multi_targets.get(region_name, []):
 88                region_target_paths[target_name] = os.path.join(subdir, target_name)
 89            region = NamedScreenRegion(
 90                region_name,
 91                x,
 92                y,
 93                w,
 94                h,
 95                parameters=parameters,
 96                multi_target_paths=region_target_paths,
 97            )
 98            named_screen_regions.append(region)
 99
100        super().__init__(pyboy, parameters, named_screen_regions)
101
102    def __repr__(self) -> str:
103        return f"{self.__class__.__name__}(variant={self.VARIANT})"
104
105
106class SwordOfHope1Parser(_BaseSwordOfHopeParser):
107    VARIANT = "sword_of_hope_1"
108
109    COMMON_MULTI_TARGETS = {
110        "room_label": [
111            "mill_room",
112            "oldman_house",
113            "in_forest",
114            "shaman_house",
115            "shaman_room",
116        ],
117        "command_area": [
118            "dialogue_active",
119            "dialogue_cleared",
120            "look_selected",
121            "exploration_menu",
122            "menu_open",
123        ],
124        "battle_command": [
125            "battle_active",
126            "battle_won",
127            "battle_magic_menu",
128            "escape_confirmed",
129            "cursor_on_auto",
130            "cursor_on_firebal",
131            "cursor_on_firebal2",
132            "cursor_on_stripall",
133        ],
134        "status_command": [
135            "look_target_options",
136            "item_found",
137            "shop_menu_open",
138            "purchase_confirmed",
139            "dialogue_visible",
140            "dialogue_initiated",
141            "dialogue_advanced",
142            "magic_menu_open",
143            "teleport_result",
144            "teleport_dest_cursor",
145            "teleport_landed",
146            "cursor_on_look",
147            "cursor_on_open",
148            "cursor_on_use",
149            "cursor_on_magic",
150            "cursor_on_hit",
151            "cursor_on_power",
152            "power_stats_first_page",
153            "cursor_on_teleport_oldman",
154            "cursor_on_herb_use",
155            "cursor_on_keym_use",
156            "power_stats_last_page",
157        ],
158        "battle_full": [
159            "mistress_first_dialogue",
160            "mistress_second_dialogue",
161            "save_prompt_visible",
162            "save_confirmed",
163            "look_path_target",
164            "herb_received",
165            "treant_defeated",
166            "key_m_selected",
167            "gate_opened",
168            "in_backroom",
169            "scroll_received",
170            "grace_selected",
171            "charm_received",
172        ],
173    }
174
175
176class SwordOfHope2Parser(_BaseSwordOfHopeParser):
177    VARIANT = "sword_of_hope_2"
178
179    COMMON_MULTI_TARGETS = {
180        "status_command": [
181            "dialogue_active",
182            "dialogue_cleared",
183            "dialogue_initiated",
184            "dialogue_advanced",
185            "dialogue_visible",
186            "battle_won",
187            "escape_confirmed",
188            "wheat_purchased",
189            "motion_result",
190            "battle_magic_menu",
191            "cpr_sword_purchased",
192            "wheat_from_tree",
193            "wheat_consumed",
194            "cursor_on_power",
195            "power_stats_first_page",
196            "power_stats_exp_page",
197            "cursor_on_look",
198            "cursor_on_item",
199            "cursor_on_open",
200            "cursor_on_magic",
201            "cursor_on_hit",
202            "cursor_on_motion",
203            "cursor_on_theo",
204        ],
205        "full_screen": [
206            "shop_menu_open",
207            "magic_menu_open",
208            "weapons_shop_menu_open",
209            "look_shopkeeper",
210            "shop_buy_sell_menu",
211            "cursor_on_cpr_sword",
212            "hit_tree_target",
213            "item_menu_open",
214            "use_menu_open",
215            "cursor_on_wheat",
216            "cursor_on_shop_first_item",
217            "cursor_on_shop_third_item",
218            "cursor_on_first_weapon",
219            "cursor_on_second_weapon",
220            "cursor_on_third_weapon",
221            "look_tree_target",
222        ],
223        "command_area": [
224            "exploration_menu",
225            "power_menu_last_slide",
226        ],
227        "soh2_room_label": [
228            "kings_room",
229            "riccar_castle",
230            "temple_1f",
231            "outside_temple",
232        ],
233        "soh2_battle_command": [
234            "battle_active",
235            "cursor_on_auto",
236            "cursor_on_clash",
237        ],
238    }
class SwordOfHope1Parser(_BaseSwordOfHopeParser):
107class SwordOfHope1Parser(_BaseSwordOfHopeParser):
108    VARIANT = "sword_of_hope_1"
109
110    COMMON_MULTI_TARGETS = {
111        "room_label": [
112            "mill_room",
113            "oldman_house",
114            "in_forest",
115            "shaman_house",
116            "shaman_room",
117        ],
118        "command_area": [
119            "dialogue_active",
120            "dialogue_cleared",
121            "look_selected",
122            "exploration_menu",
123            "menu_open",
124        ],
125        "battle_command": [
126            "battle_active",
127            "battle_won",
128            "battle_magic_menu",
129            "escape_confirmed",
130            "cursor_on_auto",
131            "cursor_on_firebal",
132            "cursor_on_firebal2",
133            "cursor_on_stripall",
134        ],
135        "status_command": [
136            "look_target_options",
137            "item_found",
138            "shop_menu_open",
139            "purchase_confirmed",
140            "dialogue_visible",
141            "dialogue_initiated",
142            "dialogue_advanced",
143            "magic_menu_open",
144            "teleport_result",
145            "teleport_dest_cursor",
146            "teleport_landed",
147            "cursor_on_look",
148            "cursor_on_open",
149            "cursor_on_use",
150            "cursor_on_magic",
151            "cursor_on_hit",
152            "cursor_on_power",
153            "power_stats_first_page",
154            "cursor_on_teleport_oldman",
155            "cursor_on_herb_use",
156            "cursor_on_keym_use",
157            "power_stats_last_page",
158        ],
159        "battle_full": [
160            "mistress_first_dialogue",
161            "mistress_second_dialogue",
162            "save_prompt_visible",
163            "save_confirmed",
164            "look_path_target",
165            "herb_received",
166            "treant_defeated",
167            "key_m_selected",
168            "gate_opened",
169            "in_backroom",
170            "scroll_received",
171            "grace_selected",
172            "charm_received",
173        ],
174    }

Base parser for Sword of Hope variants.

Defines common screen regions for the SoH UI layout:
  • Top stats bar (LV, HP, MP)
  • Game viewport + Gold/EX
  • Status bar (MOVE | ROOM NAME)
  • Command grid (Look/Use/Open/Magic/Hit/Power + directional pad)
VARIANT = 'sword_of_hope_1'
COMMON_MULTI_TARGETS = {'room_label': ['mill_room', 'oldman_house', 'in_forest', 'shaman_house', 'shaman_room'], 'command_area': ['dialogue_active', 'dialogue_cleared', 'look_selected', 'exploration_menu', 'menu_open'], 'battle_command': ['battle_active', 'battle_won', 'battle_magic_menu', 'escape_confirmed', 'cursor_on_auto', 'cursor_on_firebal', 'cursor_on_firebal2', 'cursor_on_stripall'], 'status_command': ['look_target_options', 'item_found', 'shop_menu_open', 'purchase_confirmed', 'dialogue_visible', 'dialogue_initiated', 'dialogue_advanced', 'magic_menu_open', 'teleport_result', 'teleport_dest_cursor', 'teleport_landed', 'cursor_on_look', 'cursor_on_open', 'cursor_on_use', 'cursor_on_magic', 'cursor_on_hit', 'cursor_on_power', 'power_stats_first_page', 'cursor_on_teleport_oldman', 'cursor_on_herb_use', 'cursor_on_keym_use', 'power_stats_last_page'], 'battle_full': ['mistress_first_dialogue', 'mistress_second_dialogue', 'save_prompt_visible', 'save_confirmed', 'look_path_target', 'herb_received', 'treant_defeated', 'key_m_selected', 'gate_opened', 'in_backroom', 'scroll_received', 'grace_selected', 'charm_received']}
class SwordOfHope2Parser(_BaseSwordOfHopeParser):
177class SwordOfHope2Parser(_BaseSwordOfHopeParser):
178    VARIANT = "sword_of_hope_2"
179
180    COMMON_MULTI_TARGETS = {
181        "status_command": [
182            "dialogue_active",
183            "dialogue_cleared",
184            "dialogue_initiated",
185            "dialogue_advanced",
186            "dialogue_visible",
187            "battle_won",
188            "escape_confirmed",
189            "wheat_purchased",
190            "motion_result",
191            "battle_magic_menu",
192            "cpr_sword_purchased",
193            "wheat_from_tree",
194            "wheat_consumed",
195            "cursor_on_power",
196            "power_stats_first_page",
197            "power_stats_exp_page",
198            "cursor_on_look",
199            "cursor_on_item",
200            "cursor_on_open",
201            "cursor_on_magic",
202            "cursor_on_hit",
203            "cursor_on_motion",
204            "cursor_on_theo",
205        ],
206        "full_screen": [
207            "shop_menu_open",
208            "magic_menu_open",
209            "weapons_shop_menu_open",
210            "look_shopkeeper",
211            "shop_buy_sell_menu",
212            "cursor_on_cpr_sword",
213            "hit_tree_target",
214            "item_menu_open",
215            "use_menu_open",
216            "cursor_on_wheat",
217            "cursor_on_shop_first_item",
218            "cursor_on_shop_third_item",
219            "cursor_on_first_weapon",
220            "cursor_on_second_weapon",
221            "cursor_on_third_weapon",
222            "look_tree_target",
223        ],
224        "command_area": [
225            "exploration_menu",
226            "power_menu_last_slide",
227        ],
228        "soh2_room_label": [
229            "kings_room",
230            "riccar_castle",
231            "temple_1f",
232            "outside_temple",
233        ],
234        "soh2_battle_command": [
235            "battle_active",
236            "cursor_on_auto",
237            "cursor_on_clash",
238        ],
239    }

Base parser for Sword of Hope variants.

Defines common screen regions for the SoH UI layout:
  • Top stats bar (LV, HP, MP)
  • Game viewport + Gold/EX
  • Status bar (MOVE | ROOM NAME)
  • Command grid (Look/Use/Open/Magic/Hit/Power + directional pad)
VARIANT = 'sword_of_hope_2'
COMMON_MULTI_TARGETS = {'status_command': ['dialogue_active', 'dialogue_cleared', 'dialogue_initiated', 'dialogue_advanced', 'dialogue_visible', 'battle_won', 'escape_confirmed', 'wheat_purchased', 'motion_result', 'battle_magic_menu', 'cpr_sword_purchased', 'wheat_from_tree', 'wheat_consumed', 'cursor_on_power', 'power_stats_first_page', 'power_stats_exp_page', 'cursor_on_look', 'cursor_on_item', 'cursor_on_open', 'cursor_on_magic', 'cursor_on_hit', 'cursor_on_motion', 'cursor_on_theo'], 'full_screen': ['shop_menu_open', 'magic_menu_open', 'weapons_shop_menu_open', 'look_shopkeeper', 'shop_buy_sell_menu', 'cursor_on_cpr_sword', 'hit_tree_target', 'item_menu_open', 'use_menu_open', 'cursor_on_wheat', 'cursor_on_shop_first_item', 'cursor_on_shop_third_item', 'cursor_on_first_weapon', 'cursor_on_second_weapon', 'cursor_on_third_weapon', 'look_tree_target'], 'command_area': ['exploration_menu', 'power_menu_last_slide'], 'soh2_room_label': ['kings_room', 'riccar_castle', 'temple_1f', 'outside_temple'], 'soh2_battle_command': ['battle_active', 'cursor_on_auto', 'cursor_on_clash']}