gameboy_worlds.emulation.runes_of_virtue.parsers

Runes of Virtue specific game state parser implementations.

Visual screen-region matching is used (mirroring the Pokemon module's design) so that adding additional Runes of Virtue variants only requires per-variant capture data and optional region overrides, not a fork of the parser logic.

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

  1"""
  2Runes of Virtue specific game state parser implementations.
  3
  4Visual screen-region matching is used (mirroring the Pokemon module's design) so that
  5adding additional Runes of Virtue variants only requires per-variant capture data and
  6optional region overrides, not a fork of the parser logic.
  7
  8CORE DESIGN PRINCIPLE: Never branch the parser subclasses for a given variant. The inheritance tree for a parser after the game variant parser should always be a tree with only one child per layer.
  9This is to ensure that we don't double effort, any capability added to a parser will always be valid for that game variant.
 10If this principle is followed, any state tracker can always use the STRONGEST (lowest level) parser for a given variant without concern for missing functionality.
 11"""
 12
 13import os
 14from abc import ABC, abstractmethod
 15from enum import Enum
 16from typing import Dict, List, Tuple
 17
 18import numpy as np
 19from pyboy import PyBoy
 20
 21from gameboy_worlds.emulation.parser import (
 22    NamedScreenRegion,
 23    StateParser,
 24    _get_proper_regions,
 25)
 26from gameboy_worlds.utils import log_error, verify_parameters
 27
 28
 29class AgentState(Enum):
 30    """
 31    0. FREE_ROAM: The agent is freely roaming the game world.
 32    1. IN_MENU: The agent is in the inventory/status menu.
 33    2. IN_DIALOGUE: The agent is in an NPC dialogue overlay.
 34    """
 35
 36    FREE_ROAM = 0
 37    IN_MENU = 1
 38    IN_DIALOGUE = 2
 39
 40
 41class RunesOfVirtueStateParser(StateParser, ABC):
 42    """
 43    Base class for Runes of Virtue game state parsers. Uses visual screen regions to parse game state.
 44    Defines common named screen regions and methods for determining game states such as being in a menu.
 45
 46    Can be used to determine the exact AgentState.
 47    """
 48
 49    COMMON_REGIONS: List[Tuple[str, int, int, int, int]] = []
 50    """ List of common named screen regions for Runes of Virtue games. Subclasses extend this with variant-specific regions via override_regions. """
 51
 52    COMMON_MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = []
 53    """ List of common multi-target named screen regions for Runes of Virtue games. """
 54
 55    COMMON_MULTI_TARGETS: Dict[str, List[str]] = {}
 56    """ Common multi-targets for the common multi-target named screen regions. """
 57
 58    def __init__(
 59        self,
 60        variant: str,
 61        pyboy: PyBoy,
 62        parameters: dict,
 63        additional_named_screen_region_details: List[
 64            Tuple[str, int, int, int, int]
 65        ] = [],
 66        additional_multi_target_named_screen_region_details: List[
 67            Tuple[str, int, int, int, int]
 68        ] = [],
 69        override_multi_targets: Dict[str, List[str]] = {},
 70    ):
 71        """
 72        Initializes the RunesOfVirtueStateParser.
 73        Args:
 74            variant (str): The variant of the Runes of Virtue game.
 75            pyboy (PyBoy): The PyBoy emulator instance.
 76            parameters (dict): Configuration parameters for the emulator.
 77            additional_named_screen_region_details: Additional named screen region tuples to register.
 78            additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
 79            override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
 80        """
 81        verify_parameters(parameters)
 82        regions = _get_proper_regions(
 83            override_regions=additional_named_screen_region_details,
 84            base_regions=self.COMMON_REGIONS,
 85        )
 86        self.variant = variant
 87        if f"{variant}_rom_data_path" not in parameters:
 88            log_error(
 89                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/rom_data_path_vars.yaml for an example",
 90                parameters,
 91            )
 92        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
 93        """ Path to the ROM data directory for the specific Runes of Virtue variant."""
 94        captures_dir = os.path.join(self.rom_data_path, "captures")
 95        named_screen_regions = []
 96        for region_name, x, y, w, h in regions:
 97            region = NamedScreenRegion(
 98                region_name,
 99                x,
100                y,
101                w,
102                h,
103                parameters=parameters,
104                target_path=os.path.join(captures_dir, region_name),
105            )
106            named_screen_regions.append(region)
107        multi_target_regions = _get_proper_regions(
108            override_regions=additional_multi_target_named_screen_region_details,
109            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
110        )
111        multi_target_region_names = [region[0] for region in multi_target_regions]
112        multi_targets = {k: list(v) for k, v in self.COMMON_MULTI_TARGETS.items()}
113        for key in override_multi_targets:
114            if key in multi_targets:
115                multi_targets[key].extend(override_multi_targets[key])
116            else:
117                multi_targets[key] = list(override_multi_targets[key])
118        if not set(multi_targets.keys()).issubset(set(multi_target_region_names)):
119            log_error(
120                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {list(multi_targets.keys())}, Defined: {multi_target_region_names}",
121                parameters,
122            )
123        for region_name, x, y, w, h in multi_target_regions:
124            region_target_paths = {}
125            subdir = os.path.join(captures_dir, region_name)
126            for target_name in multi_targets.get(region_name, []):
127                region_target_paths[target_name] = os.path.join(subdir, target_name)
128            region = NamedScreenRegion(
129                region_name,
130                x,
131                y,
132                w,
133                h,
134                parameters=parameters,
135                multi_target_paths=region_target_paths,
136            )
137            named_screen_regions.append(region)
138        super().__init__(pyboy, parameters, named_screen_regions)
139
140    @abstractmethod
141    def is_in_menu(self, current_screen: np.ndarray) -> bool:
142        """
143        Determines if the inventory/status menu is currently open.
144
145        Args:
146            current_screen (np.ndarray): The current screen frame from the emulator.
147        Returns:
148            bool: True if the menu is open, False otherwise.
149        """
150        raise NotImplementedError
151
152    @abstractmethod
153    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
154        """
155        Determines if an NPC dialogue overlay is currently visible.
156
157        Args:
158            current_screen (np.ndarray): The current screen frame from the emulator.
159        Returns:
160            bool: True if any dialogue overlay is visible, False otherwise.
161        """
162        raise NotImplementedError
163
164    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
165        """
166        Determines the current agent state based on the screen.
167
168        Args:
169            current_screen (np.ndarray): The current screen frame from the emulator.
170
171        Returns:
172            AgentState: The current agent state.
173        """
174        if self.is_in_menu(current_screen):
175            return AgentState.IN_MENU
176        if self.is_in_dialogue(current_screen):
177            return AgentState.IN_DIALOGUE
178        return AgentState.FREE_ROAM
179
180    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
181        """
182        Determines if a dialogue box is currently open.
183        """
184        box = self.capture_named_region(
185            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
186        )
187        white_rows = np.mean(box == 255, axis=(1, 2))
188        return np.count_nonzero(white_rows > 0.98) >= 10
189
190    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
191        """
192        Determines if the dialogue OCR region has no meaningful text content.
193        """
194        box = self.capture_named_region(
195            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
196        )
197        return np.mean(box < 255) < 0.05
198
199    def get_dialogue_ocr_region_name(self) -> str:
200        return "dialogue_ocr_region"
201
202    def __repr__(self) -> str:
203        return f"<RunesOfVirtueParser(variant={self.variant})>"
204
205
206class RunesOfVirtue1StateParser(RunesOfVirtueStateParser):
207    """
208    State parser for Ultima: Runes of Virtue (1991).
209
210    Screen regions:
211    - menu_indicator: A region (y=30-50, x=20-60) that is uniformly white when the
212      inventory/status menu is open (triggered by START), and contains game world
213      content during normal gameplay.
214    - dialog_indicator: A multi-target dialogue box region shared by NPC dialogue
215      targets with the same screen coordinates.
216    - dialogue_ocr_region: Dialogue text area captured for OCR.
217    """
218
219    REGIONS = [
220        ("menu_indicator", 20, 30, 40, 20),
221    ]
222    """ Additional named screen regions specific to Runes of Virtue 1.
223    - menu_indicator: A patch above the dialogue box that goes white when the inventory/status menu opens. Open the START menu to capture this.
224    """
225
226    MULTI_TARGET_REGIONS = [
227        ("king_dialog_indicator", 5, 100, 115, 40),
228        ("dialog_indicator", 5, 90, 115, 40),
229        ("cave_indicator", 0, 0, 100, 70),
230        ("playfield_indicator", 0, 0, 144, 144),
231        ("top_playfield_indicator", 0, 0, 144, 24),
232        ("telescope_view_indicator", 40, 40, 80, 60),
233        ("death_screen_indicator", 60, 100, 30, 30),
234        ("dialogue_ocr_region", 5, 90, 135, 54),
235    ]
236    """ Additional multi-target named screen regions specific to Runes of Virtue 1. """
237
238    MULTI_TARGETS = {
239        "king_dialog_indicator": ["king_dialog"],
240        "dialog_indicator": [
241            "chuckles_dialog",
242            "gnu_gnu_1_dialog",
243            "gnu_gnu_2_dialog",
244            "sherry_dialog",
245            "dr_cat_cats_lair_dialog",
246            "cavern_of_cowardice_sherry_floor_4_dialog",
247        ],
248        "cave_indicator": [
249            "cavern_of_hatred",
250            "cavern_of_deceit",
251            "cavern_of_cowardice",
252        ],
253        "playfield_indicator": [
254            "king_dialog",
255            "chuckles_dialog",
256            "gnu_gnu_1_dialog",
257            "gnu_gnu_2_dialog",
258            "sherry_dialog",
259            "dr_cat_cats_lair_dialog",
260            "cavern_of_cowardice_sherry_floor_4_dialog",
261            "cavern_of_hatred_chest_floor_1_opened",
262            "dr_cat_dialog",
263            "ship_ridden",
264            "basement_ladder_unlocked",
265            "basement_chest_opened",
266            "cavern_of_cowardice_enter_floor_2",
267            "cavern_of_cowardice_enter_floor_3",
268            "cavern_of_cowardice_floor_3_chest_opened",
269            "cavern_of_cowardice_take_stew_floor_4",
270            "cavern_of_cowardice_obtain_coin_floor_4",
271            "cavern_of_hatred_enter_floor_2",
272            "cavern_of_hatred_sherry_floor_2_dialog",
273            "cavern_of_hatred_choose_door_with_sherry",
274            "cavern_of_hatred_choose_right_door_melissa_dialog",
275            "cavern_of_hatred_enter_floor_3",
276        ],
277        "top_playfield_indicator": [
278            "cavern_of_hatred_enter_floor_4",
279            "cavern_of_cowardice_enter_floor_4",
280        ],
281        "telescope_view_indicator": ["telescope_view"],
282        "death_screen_indicator": ["death_screen"],
283    }
284    """ Multi-target names for Runes of Virtue 1 regions. """
285
286    def __init__(
287        self,
288        pyboy: PyBoy,
289        parameters: dict,
290        override_regions: List[Tuple[str, int, int, int, int]] = [],
291        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
292        override_multi_targets: Dict[str, List[str]] = {},
293    ):
294        regions = _get_proper_regions(
295            override_regions=override_regions, base_regions=self.REGIONS
296        )
297        multi_target_regions = _get_proper_regions(
298            override_regions=override_multi_target_regions,
299            base_regions=self.MULTI_TARGET_REGIONS,
300        )
301        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
302        for key in override_multi_targets:
303            if key in multi_targets:
304                multi_targets[key].extend(override_multi_targets[key])
305            else:
306                multi_targets[key] = list(override_multi_targets[key])
307        super().__init__(
308            variant="runes_of_virtue_1",
309            pyboy=pyboy,
310            parameters=parameters,
311            additional_named_screen_region_details=regions,
312            additional_multi_target_named_screen_region_details=multi_target_regions,
313            override_multi_targets=multi_targets,
314        )
315
316    _DIALOG_TARGETS = (
317        ("king_dialog_indicator", "king_dialog"),
318        ("dialog_indicator", "chuckles_dialog"),
319        ("dialog_indicator", "gnu_gnu_1_dialog"),
320        ("dialog_indicator", "gnu_gnu_2_dialog"),
321        ("dialog_indicator", "sherry_dialog"),
322        ("dialog_indicator", "dr_cat_cats_lair_dialog"),
323        ("dialog_indicator", "cavern_of_cowardice_sherry_floor_4_dialog"),
324    )
325    """ Multi-target screen regions that indicate an NPC dialogue overlay is visible. """
326
327    def is_in_menu(self, current_screen: np.ndarray) -> bool:
328        return self.named_region_matches_target(current_screen, "menu_indicator")
329
330    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
331        return any(
332            self.named_region_matches_multi_target(current_screen, name, target_name)
333            for name, target_name in self._DIALOG_TARGETS
334        )
335
336
337class RunesOfVirtue2StateParser(RunesOfVirtueStateParser):
338    """
339    State parser for Ultima: Runes of Virtue II.
340
341    Screen regions:
342    - menu_indicator: A first-pass region matching the RoV1 menu detector. Recapture
343      this for RoV2 before relying on the task.
344    - playfield_indicator: Playfield multi-target for opened-book and location screens,
345      excluding the right HUD strip.
346    - dialog_indicator: The left multi-target dialogue panel used for NPC dialogue.
347    - death_screen_indicator: Small death/game-over screen indicator matching RoV1's
348      task region.
349    """
350
351    REGIONS: List[Tuple[str, int, int, int, int]] = [
352        ("menu_indicator", 20, 30, 40, 20),
353    ]
354    """ Additional named screen regions specific to Runes of Virtue 2. """
355
356    MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [
357        ("playfield_indicator", 0, 0, 144, 144),
358        ("left_playfield_indicator", 0, 0, 56, 144),
359        ("dialog_indicator", 5, 5, 135, 136),
360        ("death_screen_indicator", 60, 100, 30, 30),
361    ]
362    """ Additional multi-target named screen regions specific to Runes of Virtue 2. """
363
364    MULTI_TARGETS = {
365        "playfield_indicator": [
366            "book_open",
367            "cave_of_dishonour",
368            "cavern_of_hatred",
369            "cave_of_dishonour_enter_floor_2",
370            "cave_of_dishonour_enter_floor_3",
371            "kitchen_cheese_grabbed",
372            "cheese_given_to_sherry",
373            "ladder_behind_locked_door_climbed",
374            "table_map_interacted",
375            "castle_ladder_back_found",
376            "cavern_of_hatred_gate_1_unlocked",
377            "blocked_room_entered",
378            "cavern_of_hatred_ladder_room_2",
379            "cavern_of_hatred_ladder_2",
380            "cavern_of_hatred_grab_key",
381            "cavern_of_hatred_enter_floor_4",
382            "cavern_of_hatred_enter_floor_5",
383            "cavern_of_hatred_enter_floor_6",
384            "cavern_of_hatred_enter_floor_7",
385        ],
386        "left_playfield_indicator": ["cavern_of_hatred_ladder_out_found"],
387        "dialog_indicator": [
388            "nystul_dialog",
389            "blacksmith_fail_buy_shield",
390            "sherry_mouse_dialog",
391            "sandy_cook_dialog",
392            "lord_whitsaber_dialog",
393            "tholden_saved",
394            "tholden_brought_back_to_king",
395            "castle_ceremony_attended",
396        ],
397        "death_screen_indicator": ["death_screen"],
398    }
399    """ Multi-target names for Runes of Virtue 2 regions. """
400
401    def __init__(
402        self,
403        pyboy: PyBoy,
404        parameters: dict,
405        override_regions: List[Tuple[str, int, int, int, int]] = [],
406        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
407        override_multi_targets: Dict[str, List[str]] = {},
408    ):
409        regions = _get_proper_regions(
410            override_regions=override_regions, base_regions=self.REGIONS
411        )
412        multi_target_regions = _get_proper_regions(
413            override_regions=override_multi_target_regions,
414            base_regions=self.MULTI_TARGET_REGIONS,
415        )
416        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
417        for key in override_multi_targets:
418            if key in multi_targets:
419                multi_targets[key].extend(override_multi_targets[key])
420            else:
421                multi_targets[key] = list(override_multi_targets[key])
422        super().__init__(
423            variant="runes_of_virtue_2",
424            pyboy=pyboy,
425            parameters=parameters,
426            additional_named_screen_region_details=regions,
427            additional_multi_target_named_screen_region_details=multi_target_regions,
428            override_multi_targets=multi_targets,
429        )
430
431    _DIALOG_TARGETS = (
432        ("dialog_indicator", "nystul_dialog"),
433        ("dialog_indicator", "blacksmith_fail_buy_shield"),
434        ("dialog_indicator", "sherry_mouse_dialog"),
435        ("dialog_indicator", "sandy_cook_dialog"),
436        ("dialog_indicator", "lord_whitsaber_dialog"),
437        ("dialog_indicator", "tholden_saved"),
438        ("dialog_indicator", "tholden_brought_back_to_king"),
439        ("dialog_indicator", "castle_ceremony_attended"),
440    )
441    """ Multi-target screen regions that indicate an NPC dialogue overlay is visible. """
442
443    def is_in_menu(self, current_screen: np.ndarray) -> bool:
444        if "menu_indicator" not in self.named_screen_regions:
445            return False
446        return self.named_region_matches_target(current_screen, "menu_indicator")
447
448    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
449        return any(
450            self.named_region_matches_multi_target(current_screen, name, target_name)
451            for name, target_name in self._DIALOG_TARGETS
452        )
453
454    def get_dialogue_ocr_region_name(self) -> str:
455        return "dialog_indicator"
class AgentState(enum.Enum):
30class AgentState(Enum):
31    """
32    0. FREE_ROAM: The agent is freely roaming the game world.
33    1. IN_MENU: The agent is in the inventory/status menu.
34    2. IN_DIALOGUE: The agent is in an NPC dialogue overlay.
35    """
36
37    FREE_ROAM = 0
38    IN_MENU = 1
39    IN_DIALOGUE = 2
  1. FREE_ROAM: The agent is freely roaming the game world.
  2. IN_MENU: The agent is in the inventory/status menu.
  3. IN_DIALOGUE: The agent is in an NPC dialogue overlay.
FREE_ROAM = <AgentState.FREE_ROAM: 0>
IN_MENU = <AgentState.IN_MENU: 1>
IN_DIALOGUE = <AgentState.IN_DIALOGUE: 2>
class RunesOfVirtueStateParser(gameboy_worlds.emulation.parser.StateParser, abc.ABC):
 42class RunesOfVirtueStateParser(StateParser, ABC):
 43    """
 44    Base class for Runes of Virtue game state parsers. Uses visual screen regions to parse game state.
 45    Defines common named screen regions and methods for determining game states such as being in a menu.
 46
 47    Can be used to determine the exact AgentState.
 48    """
 49
 50    COMMON_REGIONS: List[Tuple[str, int, int, int, int]] = []
 51    """ List of common named screen regions for Runes of Virtue games. Subclasses extend this with variant-specific regions via override_regions. """
 52
 53    COMMON_MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = []
 54    """ List of common multi-target named screen regions for Runes of Virtue games. """
 55
 56    COMMON_MULTI_TARGETS: Dict[str, List[str]] = {}
 57    """ Common multi-targets for the common multi-target named screen regions. """
 58
 59    def __init__(
 60        self,
 61        variant: str,
 62        pyboy: PyBoy,
 63        parameters: dict,
 64        additional_named_screen_region_details: List[
 65            Tuple[str, int, int, int, int]
 66        ] = [],
 67        additional_multi_target_named_screen_region_details: List[
 68            Tuple[str, int, int, int, int]
 69        ] = [],
 70        override_multi_targets: Dict[str, List[str]] = {},
 71    ):
 72        """
 73        Initializes the RunesOfVirtueStateParser.
 74        Args:
 75            variant (str): The variant of the Runes of Virtue game.
 76            pyboy (PyBoy): The PyBoy emulator instance.
 77            parameters (dict): Configuration parameters for the emulator.
 78            additional_named_screen_region_details: Additional named screen region tuples to register.
 79            additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
 80            override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
 81        """
 82        verify_parameters(parameters)
 83        regions = _get_proper_regions(
 84            override_regions=additional_named_screen_region_details,
 85            base_regions=self.COMMON_REGIONS,
 86        )
 87        self.variant = variant
 88        if f"{variant}_rom_data_path" not in parameters:
 89            log_error(
 90                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/rom_data_path_vars.yaml for an example",
 91                parameters,
 92            )
 93        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
 94        """ Path to the ROM data directory for the specific Runes of Virtue variant."""
 95        captures_dir = os.path.join(self.rom_data_path, "captures")
 96        named_screen_regions = []
 97        for region_name, x, y, w, h in regions:
 98            region = NamedScreenRegion(
 99                region_name,
100                x,
101                y,
102                w,
103                h,
104                parameters=parameters,
105                target_path=os.path.join(captures_dir, region_name),
106            )
107            named_screen_regions.append(region)
108        multi_target_regions = _get_proper_regions(
109            override_regions=additional_multi_target_named_screen_region_details,
110            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
111        )
112        multi_target_region_names = [region[0] for region in multi_target_regions]
113        multi_targets = {k: list(v) for k, v in self.COMMON_MULTI_TARGETS.items()}
114        for key in override_multi_targets:
115            if key in multi_targets:
116                multi_targets[key].extend(override_multi_targets[key])
117            else:
118                multi_targets[key] = list(override_multi_targets[key])
119        if not set(multi_targets.keys()).issubset(set(multi_target_region_names)):
120            log_error(
121                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {list(multi_targets.keys())}, Defined: {multi_target_region_names}",
122                parameters,
123            )
124        for region_name, x, y, w, h in multi_target_regions:
125            region_target_paths = {}
126            subdir = os.path.join(captures_dir, region_name)
127            for target_name in multi_targets.get(region_name, []):
128                region_target_paths[target_name] = os.path.join(subdir, target_name)
129            region = NamedScreenRegion(
130                region_name,
131                x,
132                y,
133                w,
134                h,
135                parameters=parameters,
136                multi_target_paths=region_target_paths,
137            )
138            named_screen_regions.append(region)
139        super().__init__(pyboy, parameters, named_screen_regions)
140
141    @abstractmethod
142    def is_in_menu(self, current_screen: np.ndarray) -> bool:
143        """
144        Determines if the inventory/status menu is currently open.
145
146        Args:
147            current_screen (np.ndarray): The current screen frame from the emulator.
148        Returns:
149            bool: True if the menu is open, False otherwise.
150        """
151        raise NotImplementedError
152
153    @abstractmethod
154    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
155        """
156        Determines if an NPC dialogue overlay is currently visible.
157
158        Args:
159            current_screen (np.ndarray): The current screen frame from the emulator.
160        Returns:
161            bool: True if any dialogue overlay is visible, False otherwise.
162        """
163        raise NotImplementedError
164
165    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
166        """
167        Determines the current agent state based on the screen.
168
169        Args:
170            current_screen (np.ndarray): The current screen frame from the emulator.
171
172        Returns:
173            AgentState: The current agent state.
174        """
175        if self.is_in_menu(current_screen):
176            return AgentState.IN_MENU
177        if self.is_in_dialogue(current_screen):
178            return AgentState.IN_DIALOGUE
179        return AgentState.FREE_ROAM
180
181    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
182        """
183        Determines if a dialogue box is currently open.
184        """
185        box = self.capture_named_region(
186            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
187        )
188        white_rows = np.mean(box == 255, axis=(1, 2))
189        return np.count_nonzero(white_rows > 0.98) >= 10
190
191    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
192        """
193        Determines if the dialogue OCR region has no meaningful text content.
194        """
195        box = self.capture_named_region(
196            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
197        )
198        return np.mean(box < 255) < 0.05
199
200    def get_dialogue_ocr_region_name(self) -> str:
201        return "dialogue_ocr_region"
202
203    def __repr__(self) -> str:
204        return f"<RunesOfVirtueParser(variant={self.variant})>"

Base class for Runes of Virtue game state parsers. Uses visual screen regions to parse game state. Defines common named screen regions and methods for determining game states such as being in a menu.

Can be used to determine the exact AgentState.

RunesOfVirtueStateParser( variant: str, pyboy: pyboy.pyboy.PyBoy, parameters: dict, additional_named_screen_region_details: List[Tuple[str, int, int, int, int]] = [], additional_multi_target_named_screen_region_details: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
 59    def __init__(
 60        self,
 61        variant: str,
 62        pyboy: PyBoy,
 63        parameters: dict,
 64        additional_named_screen_region_details: List[
 65            Tuple[str, int, int, int, int]
 66        ] = [],
 67        additional_multi_target_named_screen_region_details: List[
 68            Tuple[str, int, int, int, int]
 69        ] = [],
 70        override_multi_targets: Dict[str, List[str]] = {},
 71    ):
 72        """
 73        Initializes the RunesOfVirtueStateParser.
 74        Args:
 75            variant (str): The variant of the Runes of Virtue game.
 76            pyboy (PyBoy): The PyBoy emulator instance.
 77            parameters (dict): Configuration parameters for the emulator.
 78            additional_named_screen_region_details: Additional named screen region tuples to register.
 79            additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
 80            override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
 81        """
 82        verify_parameters(parameters)
 83        regions = _get_proper_regions(
 84            override_regions=additional_named_screen_region_details,
 85            base_regions=self.COMMON_REGIONS,
 86        )
 87        self.variant = variant
 88        if f"{variant}_rom_data_path" not in parameters:
 89            log_error(
 90                f"ROM data path not found for variant: {variant}. Add {variant}_rom_data_path to the config files. See configs/rom_data_path_vars.yaml for an example",
 91                parameters,
 92            )
 93        self.rom_data_path = parameters[f"{variant}_rom_data_path"]
 94        """ Path to the ROM data directory for the specific Runes of Virtue variant."""
 95        captures_dir = os.path.join(self.rom_data_path, "captures")
 96        named_screen_regions = []
 97        for region_name, x, y, w, h in regions:
 98            region = NamedScreenRegion(
 99                region_name,
100                x,
101                y,
102                w,
103                h,
104                parameters=parameters,
105                target_path=os.path.join(captures_dir, region_name),
106            )
107            named_screen_regions.append(region)
108        multi_target_regions = _get_proper_regions(
109            override_regions=additional_multi_target_named_screen_region_details,
110            base_regions=self.COMMON_MULTI_TARGET_REGIONS,
111        )
112        multi_target_region_names = [region[0] for region in multi_target_regions]
113        multi_targets = {k: list(v) for k, v in self.COMMON_MULTI_TARGETS.items()}
114        for key in override_multi_targets:
115            if key in multi_targets:
116                multi_targets[key].extend(override_multi_targets[key])
117            else:
118                multi_targets[key] = list(override_multi_targets[key])
119        if not set(multi_targets.keys()).issubset(set(multi_target_region_names)):
120            log_error(
121                f"Multi-target regions provided in multi_targets do not match the defined multi-target regions. Provided: {list(multi_targets.keys())}, Defined: {multi_target_region_names}",
122                parameters,
123            )
124        for region_name, x, y, w, h in multi_target_regions:
125            region_target_paths = {}
126            subdir = os.path.join(captures_dir, region_name)
127            for target_name in multi_targets.get(region_name, []):
128                region_target_paths[target_name] = os.path.join(subdir, target_name)
129            region = NamedScreenRegion(
130                region_name,
131                x,
132                y,
133                w,
134                h,
135                parameters=parameters,
136                multi_target_paths=region_target_paths,
137            )
138            named_screen_regions.append(region)
139        super().__init__(pyboy, parameters, named_screen_regions)

Initializes the RunesOfVirtueStateParser.

Arguments:
  • variant (str): The variant of the Runes of Virtue game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details: Additional named screen region tuples to register.
  • additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
  • override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
COMMON_REGIONS: List[Tuple[str, int, int, int, int]] = []

List of common named screen regions for Runes of Virtue games. Subclasses extend this with variant-specific regions via override_regions.

COMMON_MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = []

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

COMMON_MULTI_TARGETS: Dict[str, List[str]] = {}

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

variant
rom_data_path

Path to the ROM data directory for the specific Runes of Virtue variant.

@abstractmethod
def is_in_menu(self, current_screen: numpy.ndarray) -> bool:
141    @abstractmethod
142    def is_in_menu(self, current_screen: np.ndarray) -> bool:
143        """
144        Determines if the inventory/status menu is currently open.
145
146        Args:
147            current_screen (np.ndarray): The current screen frame from the emulator.
148        Returns:
149            bool: True if the menu is open, False otherwise.
150        """
151        raise NotImplementedError

Determines if the inventory/status menu is currently open.

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

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

@abstractmethod
def is_in_dialogue(self, current_screen: numpy.ndarray) -> bool:
153    @abstractmethod
154    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
155        """
156        Determines if an NPC dialogue overlay is currently visible.
157
158        Args:
159            current_screen (np.ndarray): The current screen frame from the emulator.
160        Returns:
161            bool: True if any dialogue overlay is visible, False otherwise.
162        """
163        raise NotImplementedError

Determines if an NPC dialogue overlay is currently visible.

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

bool: True if any dialogue overlay is visible, False otherwise.

def get_agent_state( self, current_screen: numpy.ndarray) -> AgentState:
165    def get_agent_state(self, current_screen: np.ndarray) -> AgentState:
166        """
167        Determines the current agent state based on the screen.
168
169        Args:
170            current_screen (np.ndarray): The current screen frame from the emulator.
171
172        Returns:
173            AgentState: The current agent state.
174        """
175        if self.is_in_menu(current_screen):
176            return AgentState.IN_MENU
177        if self.is_in_dialogue(current_screen):
178            return AgentState.IN_DIALOGUE
179        return AgentState.FREE_ROAM

Determines the current agent state based on the screen.

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

AgentState: The current agent state.

def dialogue_box_open(self, current_screen: numpy.ndarray) -> bool:
181    def dialogue_box_open(self, current_screen: np.ndarray) -> bool:
182        """
183        Determines if a dialogue box is currently open.
184        """
185        box = self.capture_named_region(
186            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
187        )
188        white_rows = np.mean(box == 255, axis=(1, 2))
189        return np.count_nonzero(white_rows > 0.98) >= 10

Determines if a dialogue box is currently open.

def dialogue_box_empty(self, current_screen: numpy.ndarray) -> bool:
191    def dialogue_box_empty(self, current_screen: np.ndarray) -> bool:
192        """
193        Determines if the dialogue OCR region has no meaningful text content.
194        """
195        box = self.capture_named_region(
196            current_frame=current_screen, name=self.get_dialogue_ocr_region_name()
197        )
198        return np.mean(box < 255) < 0.05

Determines if the dialogue OCR region has no meaningful text content.

def get_dialogue_ocr_region_name(self) -> str:
200    def get_dialogue_ocr_region_name(self) -> str:
201        return "dialogue_ocr_region"
class RunesOfVirtue1StateParser(RunesOfVirtueStateParser):
207class RunesOfVirtue1StateParser(RunesOfVirtueStateParser):
208    """
209    State parser for Ultima: Runes of Virtue (1991).
210
211    Screen regions:
212    - menu_indicator: A region (y=30-50, x=20-60) that is uniformly white when the
213      inventory/status menu is open (triggered by START), and contains game world
214      content during normal gameplay.
215    - dialog_indicator: A multi-target dialogue box region shared by NPC dialogue
216      targets with the same screen coordinates.
217    - dialogue_ocr_region: Dialogue text area captured for OCR.
218    """
219
220    REGIONS = [
221        ("menu_indicator", 20, 30, 40, 20),
222    ]
223    """ Additional named screen regions specific to Runes of Virtue 1.
224    - menu_indicator: A patch above the dialogue box that goes white when the inventory/status menu opens. Open the START menu to capture this.
225    """
226
227    MULTI_TARGET_REGIONS = [
228        ("king_dialog_indicator", 5, 100, 115, 40),
229        ("dialog_indicator", 5, 90, 115, 40),
230        ("cave_indicator", 0, 0, 100, 70),
231        ("playfield_indicator", 0, 0, 144, 144),
232        ("top_playfield_indicator", 0, 0, 144, 24),
233        ("telescope_view_indicator", 40, 40, 80, 60),
234        ("death_screen_indicator", 60, 100, 30, 30),
235        ("dialogue_ocr_region", 5, 90, 135, 54),
236    ]
237    """ Additional multi-target named screen regions specific to Runes of Virtue 1. """
238
239    MULTI_TARGETS = {
240        "king_dialog_indicator": ["king_dialog"],
241        "dialog_indicator": [
242            "chuckles_dialog",
243            "gnu_gnu_1_dialog",
244            "gnu_gnu_2_dialog",
245            "sherry_dialog",
246            "dr_cat_cats_lair_dialog",
247            "cavern_of_cowardice_sherry_floor_4_dialog",
248        ],
249        "cave_indicator": [
250            "cavern_of_hatred",
251            "cavern_of_deceit",
252            "cavern_of_cowardice",
253        ],
254        "playfield_indicator": [
255            "king_dialog",
256            "chuckles_dialog",
257            "gnu_gnu_1_dialog",
258            "gnu_gnu_2_dialog",
259            "sherry_dialog",
260            "dr_cat_cats_lair_dialog",
261            "cavern_of_cowardice_sherry_floor_4_dialog",
262            "cavern_of_hatred_chest_floor_1_opened",
263            "dr_cat_dialog",
264            "ship_ridden",
265            "basement_ladder_unlocked",
266            "basement_chest_opened",
267            "cavern_of_cowardice_enter_floor_2",
268            "cavern_of_cowardice_enter_floor_3",
269            "cavern_of_cowardice_floor_3_chest_opened",
270            "cavern_of_cowardice_take_stew_floor_4",
271            "cavern_of_cowardice_obtain_coin_floor_4",
272            "cavern_of_hatred_enter_floor_2",
273            "cavern_of_hatred_sherry_floor_2_dialog",
274            "cavern_of_hatred_choose_door_with_sherry",
275            "cavern_of_hatred_choose_right_door_melissa_dialog",
276            "cavern_of_hatred_enter_floor_3",
277        ],
278        "top_playfield_indicator": [
279            "cavern_of_hatred_enter_floor_4",
280            "cavern_of_cowardice_enter_floor_4",
281        ],
282        "telescope_view_indicator": ["telescope_view"],
283        "death_screen_indicator": ["death_screen"],
284    }
285    """ Multi-target names for Runes of Virtue 1 regions. """
286
287    def __init__(
288        self,
289        pyboy: PyBoy,
290        parameters: dict,
291        override_regions: List[Tuple[str, int, int, int, int]] = [],
292        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
293        override_multi_targets: Dict[str, List[str]] = {},
294    ):
295        regions = _get_proper_regions(
296            override_regions=override_regions, base_regions=self.REGIONS
297        )
298        multi_target_regions = _get_proper_regions(
299            override_regions=override_multi_target_regions,
300            base_regions=self.MULTI_TARGET_REGIONS,
301        )
302        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
303        for key in override_multi_targets:
304            if key in multi_targets:
305                multi_targets[key].extend(override_multi_targets[key])
306            else:
307                multi_targets[key] = list(override_multi_targets[key])
308        super().__init__(
309            variant="runes_of_virtue_1",
310            pyboy=pyboy,
311            parameters=parameters,
312            additional_named_screen_region_details=regions,
313            additional_multi_target_named_screen_region_details=multi_target_regions,
314            override_multi_targets=multi_targets,
315        )
316
317    _DIALOG_TARGETS = (
318        ("king_dialog_indicator", "king_dialog"),
319        ("dialog_indicator", "chuckles_dialog"),
320        ("dialog_indicator", "gnu_gnu_1_dialog"),
321        ("dialog_indicator", "gnu_gnu_2_dialog"),
322        ("dialog_indicator", "sherry_dialog"),
323        ("dialog_indicator", "dr_cat_cats_lair_dialog"),
324        ("dialog_indicator", "cavern_of_cowardice_sherry_floor_4_dialog"),
325    )
326    """ Multi-target screen regions that indicate an NPC dialogue overlay is visible. """
327
328    def is_in_menu(self, current_screen: np.ndarray) -> bool:
329        return self.named_region_matches_target(current_screen, "menu_indicator")
330
331    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
332        return any(
333            self.named_region_matches_multi_target(current_screen, name, target_name)
334            for name, target_name in self._DIALOG_TARGETS
335        )

State parser for Ultima: Runes of Virtue (1991).

Screen regions:

  • menu_indicator: A region (y=30-50, x=20-60) that is uniformly white when the inventory/status menu is open (triggered by START), and contains game world content during normal gameplay.
  • dialog_indicator: A multi-target dialogue box region shared by NPC dialogue targets with the same screen coordinates.
  • dialogue_ocr_region: Dialogue text area captured for OCR.
RunesOfVirtue1StateParser( pyboy: pyboy.pyboy.PyBoy, parameters: dict, override_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
287    def __init__(
288        self,
289        pyboy: PyBoy,
290        parameters: dict,
291        override_regions: List[Tuple[str, int, int, int, int]] = [],
292        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
293        override_multi_targets: Dict[str, List[str]] = {},
294    ):
295        regions = _get_proper_regions(
296            override_regions=override_regions, base_regions=self.REGIONS
297        )
298        multi_target_regions = _get_proper_regions(
299            override_regions=override_multi_target_regions,
300            base_regions=self.MULTI_TARGET_REGIONS,
301        )
302        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
303        for key in override_multi_targets:
304            if key in multi_targets:
305                multi_targets[key].extend(override_multi_targets[key])
306            else:
307                multi_targets[key] = list(override_multi_targets[key])
308        super().__init__(
309            variant="runes_of_virtue_1",
310            pyboy=pyboy,
311            parameters=parameters,
312            additional_named_screen_region_details=regions,
313            additional_multi_target_named_screen_region_details=multi_target_regions,
314            override_multi_targets=multi_targets,
315        )

Initializes the RunesOfVirtueStateParser.

Arguments:
  • variant (str): The variant of the Runes of Virtue game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details: Additional named screen region tuples to register.
  • additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
  • override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
REGIONS = [('menu_indicator', 20, 30, 40, 20)]

Additional named screen regions specific to Runes of Virtue 1.

  • menu_indicator: A patch above the dialogue box that goes white when the inventory/status menu opens. Open the START menu to capture this.
MULTI_TARGET_REGIONS = [('king_dialog_indicator', 5, 100, 115, 40), ('dialog_indicator', 5, 90, 115, 40), ('cave_indicator', 0, 0, 100, 70), ('playfield_indicator', 0, 0, 144, 144), ('top_playfield_indicator', 0, 0, 144, 24), ('telescope_view_indicator', 40, 40, 80, 60), ('death_screen_indicator', 60, 100, 30, 30), ('dialogue_ocr_region', 5, 90, 135, 54)]

Additional multi-target named screen regions specific to Runes of Virtue 1.

MULTI_TARGETS = {'king_dialog_indicator': ['king_dialog'], 'dialog_indicator': ['chuckles_dialog', 'gnu_gnu_1_dialog', 'gnu_gnu_2_dialog', 'sherry_dialog', 'dr_cat_cats_lair_dialog', 'cavern_of_cowardice_sherry_floor_4_dialog'], 'cave_indicator': ['cavern_of_hatred', 'cavern_of_deceit', 'cavern_of_cowardice'], 'playfield_indicator': ['king_dialog', 'chuckles_dialog', 'gnu_gnu_1_dialog', 'gnu_gnu_2_dialog', 'sherry_dialog', 'dr_cat_cats_lair_dialog', 'cavern_of_cowardice_sherry_floor_4_dialog', 'cavern_of_hatred_chest_floor_1_opened', 'dr_cat_dialog', 'ship_ridden', 'basement_ladder_unlocked', 'basement_chest_opened', 'cavern_of_cowardice_enter_floor_2', 'cavern_of_cowardice_enter_floor_3', 'cavern_of_cowardice_floor_3_chest_opened', 'cavern_of_cowardice_take_stew_floor_4', 'cavern_of_cowardice_obtain_coin_floor_4', 'cavern_of_hatred_enter_floor_2', 'cavern_of_hatred_sherry_floor_2_dialog', 'cavern_of_hatred_choose_door_with_sherry', 'cavern_of_hatred_choose_right_door_melissa_dialog', 'cavern_of_hatred_enter_floor_3'], 'top_playfield_indicator': ['cavern_of_hatred_enter_floor_4', 'cavern_of_cowardice_enter_floor_4'], 'telescope_view_indicator': ['telescope_view'], 'death_screen_indicator': ['death_screen']}

Multi-target names for Runes of Virtue 1 regions.

def is_in_menu(self, current_screen: numpy.ndarray) -> bool:
328    def is_in_menu(self, current_screen: np.ndarray) -> bool:
329        return self.named_region_matches_target(current_screen, "menu_indicator")

Determines if the inventory/status menu is currently open.

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

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

def is_in_dialogue(self, current_screen: numpy.ndarray) -> bool:
331    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
332        return any(
333            self.named_region_matches_multi_target(current_screen, name, target_name)
334            for name, target_name in self._DIALOG_TARGETS
335        )

Determines if an NPC dialogue overlay is currently visible.

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

bool: True if any dialogue overlay is visible, False otherwise.

class RunesOfVirtue2StateParser(RunesOfVirtueStateParser):
338class RunesOfVirtue2StateParser(RunesOfVirtueStateParser):
339    """
340    State parser for Ultima: Runes of Virtue II.
341
342    Screen regions:
343    - menu_indicator: A first-pass region matching the RoV1 menu detector. Recapture
344      this for RoV2 before relying on the task.
345    - playfield_indicator: Playfield multi-target for opened-book and location screens,
346      excluding the right HUD strip.
347    - dialog_indicator: The left multi-target dialogue panel used for NPC dialogue.
348    - death_screen_indicator: Small death/game-over screen indicator matching RoV1's
349      task region.
350    """
351
352    REGIONS: List[Tuple[str, int, int, int, int]] = [
353        ("menu_indicator", 20, 30, 40, 20),
354    ]
355    """ Additional named screen regions specific to Runes of Virtue 2. """
356
357    MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [
358        ("playfield_indicator", 0, 0, 144, 144),
359        ("left_playfield_indicator", 0, 0, 56, 144),
360        ("dialog_indicator", 5, 5, 135, 136),
361        ("death_screen_indicator", 60, 100, 30, 30),
362    ]
363    """ Additional multi-target named screen regions specific to Runes of Virtue 2. """
364
365    MULTI_TARGETS = {
366        "playfield_indicator": [
367            "book_open",
368            "cave_of_dishonour",
369            "cavern_of_hatred",
370            "cave_of_dishonour_enter_floor_2",
371            "cave_of_dishonour_enter_floor_3",
372            "kitchen_cheese_grabbed",
373            "cheese_given_to_sherry",
374            "ladder_behind_locked_door_climbed",
375            "table_map_interacted",
376            "castle_ladder_back_found",
377            "cavern_of_hatred_gate_1_unlocked",
378            "blocked_room_entered",
379            "cavern_of_hatred_ladder_room_2",
380            "cavern_of_hatred_ladder_2",
381            "cavern_of_hatred_grab_key",
382            "cavern_of_hatred_enter_floor_4",
383            "cavern_of_hatred_enter_floor_5",
384            "cavern_of_hatred_enter_floor_6",
385            "cavern_of_hatred_enter_floor_7",
386        ],
387        "left_playfield_indicator": ["cavern_of_hatred_ladder_out_found"],
388        "dialog_indicator": [
389            "nystul_dialog",
390            "blacksmith_fail_buy_shield",
391            "sherry_mouse_dialog",
392            "sandy_cook_dialog",
393            "lord_whitsaber_dialog",
394            "tholden_saved",
395            "tholden_brought_back_to_king",
396            "castle_ceremony_attended",
397        ],
398        "death_screen_indicator": ["death_screen"],
399    }
400    """ Multi-target names for Runes of Virtue 2 regions. """
401
402    def __init__(
403        self,
404        pyboy: PyBoy,
405        parameters: dict,
406        override_regions: List[Tuple[str, int, int, int, int]] = [],
407        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
408        override_multi_targets: Dict[str, List[str]] = {},
409    ):
410        regions = _get_proper_regions(
411            override_regions=override_regions, base_regions=self.REGIONS
412        )
413        multi_target_regions = _get_proper_regions(
414            override_regions=override_multi_target_regions,
415            base_regions=self.MULTI_TARGET_REGIONS,
416        )
417        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
418        for key in override_multi_targets:
419            if key in multi_targets:
420                multi_targets[key].extend(override_multi_targets[key])
421            else:
422                multi_targets[key] = list(override_multi_targets[key])
423        super().__init__(
424            variant="runes_of_virtue_2",
425            pyboy=pyboy,
426            parameters=parameters,
427            additional_named_screen_region_details=regions,
428            additional_multi_target_named_screen_region_details=multi_target_regions,
429            override_multi_targets=multi_targets,
430        )
431
432    _DIALOG_TARGETS = (
433        ("dialog_indicator", "nystul_dialog"),
434        ("dialog_indicator", "blacksmith_fail_buy_shield"),
435        ("dialog_indicator", "sherry_mouse_dialog"),
436        ("dialog_indicator", "sandy_cook_dialog"),
437        ("dialog_indicator", "lord_whitsaber_dialog"),
438        ("dialog_indicator", "tholden_saved"),
439        ("dialog_indicator", "tholden_brought_back_to_king"),
440        ("dialog_indicator", "castle_ceremony_attended"),
441    )
442    """ Multi-target screen regions that indicate an NPC dialogue overlay is visible. """
443
444    def is_in_menu(self, current_screen: np.ndarray) -> bool:
445        if "menu_indicator" not in self.named_screen_regions:
446            return False
447        return self.named_region_matches_target(current_screen, "menu_indicator")
448
449    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
450        return any(
451            self.named_region_matches_multi_target(current_screen, name, target_name)
452            for name, target_name in self._DIALOG_TARGETS
453        )
454
455    def get_dialogue_ocr_region_name(self) -> str:
456        return "dialog_indicator"

State parser for Ultima: Runes of Virtue II.

Screen regions:

  • menu_indicator: A first-pass region matching the RoV1 menu detector. Recapture this for RoV2 before relying on the task.
  • playfield_indicator: Playfield multi-target for opened-book and location screens, excluding the right HUD strip.
  • dialog_indicator: The left multi-target dialogue panel used for NPC dialogue.
  • death_screen_indicator: Small death/game-over screen indicator matching RoV1's task region.
RunesOfVirtue2StateParser( pyboy: pyboy.pyboy.PyBoy, parameters: dict, override_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [], override_multi_targets: Dict[str, List[str]] = {})
402    def __init__(
403        self,
404        pyboy: PyBoy,
405        parameters: dict,
406        override_regions: List[Tuple[str, int, int, int, int]] = [],
407        override_multi_target_regions: List[Tuple[str, int, int, int, int]] = [],
408        override_multi_targets: Dict[str, List[str]] = {},
409    ):
410        regions = _get_proper_regions(
411            override_regions=override_regions, base_regions=self.REGIONS
412        )
413        multi_target_regions = _get_proper_regions(
414            override_regions=override_multi_target_regions,
415            base_regions=self.MULTI_TARGET_REGIONS,
416        )
417        multi_targets = {k: list(v) for k, v in self.MULTI_TARGETS.items()}
418        for key in override_multi_targets:
419            if key in multi_targets:
420                multi_targets[key].extend(override_multi_targets[key])
421            else:
422                multi_targets[key] = list(override_multi_targets[key])
423        super().__init__(
424            variant="runes_of_virtue_2",
425            pyboy=pyboy,
426            parameters=parameters,
427            additional_named_screen_region_details=regions,
428            additional_multi_target_named_screen_region_details=multi_target_regions,
429            override_multi_targets=multi_targets,
430        )

Initializes the RunesOfVirtueStateParser.

Arguments:
  • variant (str): The variant of the Runes of Virtue game.
  • pyboy (PyBoy): The PyBoy emulator instance.
  • parameters (dict): Configuration parameters for the emulator.
  • additional_named_screen_region_details: Additional named screen region tuples to register.
  • additional_multi_target_named_screen_region_details: Additional multi-target named screen region tuples to register.
  • override_multi_targets: Dictionary mapping region names to lists of target names for multi-target regions.
REGIONS: List[Tuple[str, int, int, int, int]] = [('menu_indicator', 20, 30, 40, 20)]

Additional named screen regions specific to Runes of Virtue 2.

MULTI_TARGET_REGIONS: List[Tuple[str, int, int, int, int]] = [('playfield_indicator', 0, 0, 144, 144), ('left_playfield_indicator', 0, 0, 56, 144), ('dialog_indicator', 5, 5, 135, 136), ('death_screen_indicator', 60, 100, 30, 30)]

Additional multi-target named screen regions specific to Runes of Virtue 2.

MULTI_TARGETS = {'playfield_indicator': ['book_open', 'cave_of_dishonour', 'cavern_of_hatred', 'cave_of_dishonour_enter_floor_2', 'cave_of_dishonour_enter_floor_3', 'kitchen_cheese_grabbed', 'cheese_given_to_sherry', 'ladder_behind_locked_door_climbed', 'table_map_interacted', 'castle_ladder_back_found', 'cavern_of_hatred_gate_1_unlocked', 'blocked_room_entered', 'cavern_of_hatred_ladder_room_2', 'cavern_of_hatred_ladder_2', 'cavern_of_hatred_grab_key', 'cavern_of_hatred_enter_floor_4', 'cavern_of_hatred_enter_floor_5', 'cavern_of_hatred_enter_floor_6', 'cavern_of_hatred_enter_floor_7'], 'left_playfield_indicator': ['cavern_of_hatred_ladder_out_found'], 'dialog_indicator': ['nystul_dialog', 'blacksmith_fail_buy_shield', 'sherry_mouse_dialog', 'sandy_cook_dialog', 'lord_whitsaber_dialog', 'tholden_saved', 'tholden_brought_back_to_king', 'castle_ceremony_attended'], 'death_screen_indicator': ['death_screen']}

Multi-target names for Runes of Virtue 2 regions.

def is_in_menu(self, current_screen: numpy.ndarray) -> bool:
444    def is_in_menu(self, current_screen: np.ndarray) -> bool:
445        if "menu_indicator" not in self.named_screen_regions:
446            return False
447        return self.named_region_matches_target(current_screen, "menu_indicator")

Determines if the inventory/status menu is currently open.

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

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

def is_in_dialogue(self, current_screen: numpy.ndarray) -> bool:
449    def is_in_dialogue(self, current_screen: np.ndarray) -> bool:
450        return any(
451            self.named_region_matches_multi_target(current_screen, name, target_name)
452            for name, target_name in self._DIALOG_TARGETS
453        )

Determines if an NPC dialogue overlay is currently visible.

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

bool: True if any dialogue overlay is visible, False otherwise.

def get_dialogue_ocr_region_name(self) -> str:
455    def get_dialogue_ocr_region_name(self) -> str:
456        return "dialog_indicator"