gameboy_worlds.interface.deja_vu.actions

  1from abc import ABC
  2from gameboy_worlds.utils import log_error, log_warn
  3from typing import List, Optional, Tuple, Dict
  4import numpy as np
  5from gymnasium.spaces import Box, Discrete
  6from gameboy_worlds.emulation import LowLevelActions
  7from gameboy_worlds.emulation.deja_vu.parsers import AgentState, DejaVuStateParser
  8from gameboy_worlds.emulation.deja_vu.trackers import CoreDejaVuTracker
  9from gameboy_worlds.interface.action import HighLevelAction, SingleHighLevelAction
 10
 11HARD_MAX_STEPS = 20
 12MENU_NAV_MAX_STEPS = 8
 13
 14
 15def frame_changed(past: np.ndarray, present: np.ndarray, epsilon: float = 0.01) -> bool:
 16    """Return True if the mean pixel difference between two frames is greater than epsilon."""
 17    return np.abs(past - present).mean() > epsilon
 18
 19
 20class TickUntilStable(SingleHighLevelAction):
 21    """Tick the emulator until the screen becomes stable (no change)."""
 22
 23    REQUIRED_STATE_PARSER = DejaVuStateParser
 24    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
 25
 26    def is_valid(self, **kwargs):
 27        return True
 28
 29    def _execute(self, max_ticks: int = 30):
 30        prev = self._emulator.get_current_frame()
 31        for _ in range(max_ticks):
 32            self._emulator.step(None)
 33            curr = self._emulator.get_current_frame()
 34            if not frame_changed(prev, curr):
 35                return [self._state_tracker.report()], 0
 36            prev = curr
 37        return [self._state_tracker.report()], -1
 38
 39    @staticmethod
 40    def get_action_name() -> str:
 41        return "TickUntilStable"
 42
 43
 44class MoveCursor(SingleHighLevelAction):
 45    """Move the menu cursor in a given direction (up, down, left, right)."""
 46
 47    REQUIRED_STATE_PARSER = DejaVuStateParser
 48    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
 49    _ACTION_MAP = {
 50        "up": LowLevelActions.PRESS_ARROW_UP,
 51        "down": LowLevelActions.PRESS_ARROW_DOWN,
 52        "left": LowLevelActions.PRESS_ARROW_LEFT,
 53        "right": LowLevelActions.PRESS_ARROW_RIGHT,
 54    }
 55
 56    def is_valid(self, direction=None, **kwargs):
 57        return direction in self._ACTION_MAP
 58
 59    def _execute(self, direction):
 60        action = self._ACTION_MAP[direction]
 61        prev = self._emulator.get_current_frame()
 62        self._emulator.step(action)
 63        curr = self._emulator.get_current_frame()
 64        return [self._state_tracker.report()], 0 if frame_changed(prev, curr) else -1
 65
 66    @staticmethod
 67    def get_action_name(**kwargs) -> str:
 68        return f"MoveCursor {kwargs.get('direction', '')}"
 69
 70
 71class MoveGrid(HighLevelAction):
 72    """Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical."""
 73
 74    REQUIRED_STATE_PARSER = DejaVuStateParser
 75    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
 76
 77    def get_action_space(self):
 78        return Box(
 79            low=-HARD_MAX_STEPS // 2,
 80            high=HARD_MAX_STEPS // 2,
 81            shape=(2,),
 82            dtype=np.int8,
 83        )
 84
 85    def is_valid(self, x_steps=None, y_steps=None, **kwargs):
 86        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
 87        return state == AgentState.FREE_ROAM and (x_steps or y_steps)
 88
 89    def _execute(self, x_steps, y_steps):
 90        reports = []
 91        for axis, steps, dir_pos, dir_neg in [
 92            ("x", x_steps, "right", "left"),
 93            ("y", y_steps, "down", "up"),
 94        ]:
 95            if steps:
 96                direction = dir_pos if steps > 0 else dir_neg
 97                for _ in range(abs(steps)):
 98                    prev = self._emulator.get_current_frame()
 99                    self._emulator.step(MoveStepsAction._ACTION_MAP[direction])
100                    curr = self._emulator.get_current_frame()
101                    reports.append(self._state_tracker.report())
102                    if not frame_changed(prev, curr):
103                        return reports, -1
104        return reports, 0
105
106    @staticmethod
107    def get_action_name(x_steps: int, y_steps: int) -> str:
108        return f"MoveGrid ({x_steps}, {y_steps})"
109
110
111class OpenButtonMenu(SingleHighLevelAction):
112    """Open the bottom button menu (usually START button)."""
113
114    REQUIRED_STATE_PARSER = DejaVuStateParser
115    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
116
117    def is_valid(self, **kwargs):
118        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
119        return state == AgentState.FREE_ROAM
120
121    def _execute(self):
122        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
123        return [self._state_tracker.report()], 0
124
125    @staticmethod
126    def get_action_name() -> str:
127        return "OpenButtonMenu"
128
129
130class CloseButtonMenu(SingleHighLevelAction):
131    """Close the bottom button menu (usually B button)."""
132
133    REQUIRED_STATE_PARSER = DejaVuStateParser
134    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
135
136    def is_valid(self, **kwargs):
137        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
138        return state == AgentState.IN_MENU
139
140    def _execute(self):
141        self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
142        return [self._state_tracker.report()], 0
143
144    @staticmethod
145    def get_action_name() -> str:
146        return "CloseButtonMenu"
147
148
149class SelectMenuOption(SingleHighLevelAction):
150    """Select the current menu option (A button)."""
151
152    REQUIRED_STATE_PARSER = DejaVuStateParser
153    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
154
155    def is_valid(self, **kwargs):
156        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
157        return state == AgentState.IN_MENU
158
159    def _execute(self):
160        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
161        return [self._state_tracker.report()], 0
162
163    @staticmethod
164    def get_action_name() -> str:
165        return "SelectMenuOption"
166
167
168class AdvanceDialogue(SingleHighLevelAction):
169    """Advance dialogue (B button)."""
170
171    REQUIRED_STATE_PARSER = DejaVuStateParser
172    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
173
174    def is_valid(self, **kwargs):
175        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
176        return state == AgentState.IN_DIALOGUE
177
178    def _execute(self):
179        self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
180        return [self._state_tracker.report()], 0
181
182    @staticmethod
183    def get_action_name() -> str:
184        return "AdvanceDialogue"
185
186
187# Helper for MoveGrid
188class MoveStepsAction:
189    _ACTION_MAP = {
190        "up": LowLevelActions.PRESS_ARROW_UP,
191        "down": LowLevelActions.PRESS_ARROW_DOWN,
192        "left": LowLevelActions.PRESS_ARROW_LEFT,
193        "right": LowLevelActions.PRESS_ARROW_RIGHT,
194    }
195
196
197class InteractAction(SingleHighLevelAction):
198    """
199    Presses the A button to interact with an object in front of the agent.
200
201    Is Valid When:
202    - In Free Roam State
203
204    Action Success Interpretation:
205    - -1: Frame did not change or agent still in free roam state
206    - 1: Agent not in free roam state
207    """
208
209    REQUIRED_STATE_PARSER = DejaVuStateParser
210    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
211
212    def is_valid(self, **kwargs):
213        return (
214            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
215            == AgentState.FREE_ROAM
216        )
217
218    def _execute(self):
219        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
220        action_success = 0
221        prev_frames = []
222        for frame in frames:
223            if (
224                self._emulator.state_parser.get_agent_state(frame)
225                != AgentState.FREE_ROAM
226            ):
227                action_success = 1
228                break
229            for past_frame in prev_frames:
230                if not frame_changed(past_frame, frame):
231                    action_success = -1
232                    break
233            if action_success != 0:
234                break
235            prev_frames.append(frame)
236        if action_success == 0:
237            action_success = -1
238        return [self._state_tracker.report()], action_success
239
240    @staticmethod
241    def get_action_name() -> str:
242        return "Interact"
243
244
245class BaseMovementAction(HighLevelAction, ABC):
246    """
247    Base class for movement actions in the Deja Vu environment.
248
249    Is Valid When:
250    - In Free Roam State
251
252    Action Success Interpretation:
253    - -1: Frame did not change, even on the first step
254    - 0: Finished all steps
255    - 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle.
256    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
257
258    Action Returns:
259    - n_steps_taken (int): Number of steps actually taken
260    - rotated (bool or None): True if the player has not moved, but has rotated.
261    """
262
263    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
264    REQUIRED_STATE_PARSER = DejaVuStateParser
265
266    def _is_uniform_quadrant(self, frame, quadrant_name) -> bool:
267        mapper = {
268            "screen_quadrant_1": "tr",
269            "screen_quadrant_2": "tl",
270            "screen_quadrant_3": "bl",
271            "screen_quadrant_4": "br",
272        }
273        quadrant_cells = self._emulator.state_parser.capture_grid_cells(
274            frame, quadrant=mapper[quadrant_name]
275        )
276        keys = list(quadrant_cells.keys())
277        x_min = 1e9
278        y_min = 1e9
279        x_max = -1e9
280        y_max = -1e9
281        for key in keys:
282            x_coord, y_coord = key
283            if x_coord * y_coord == 0:
284                quadrant_cells.pop(key)
285            else:
286                x_min = min(x_min, x_coord)
287                y_min = min(y_min, y_coord)
288                x_max = max(x_max, x_coord)
289                y_max = max(y_max, y_coord)
290        x_min = int(x_min)
291        y_min = int(y_min)
292        x_max = int(x_max)
293        y_max = int(y_max)
294        keys = list(quadrant_cells.keys())
295        vertical_uniform = True
296        horizontal_uniform = True
297        for y in range(y_min + 1, y_max):
298            first_cell = None
299            for x in range(x_min + 1, x_max):
300                cell = quadrant_cells[(x, y)]
301                if first_cell is None:
302                    first_cell = cell
303                else:
304                    if first_cell.shape == cell.shape:
305                        if frame_changed(first_cell, cell):
306                            horizontal_uniform = False
307                            break
308            if not horizontal_uniform:
309                break
310        if horizontal_uniform:
311            return True
312        for x in range(x_min + 1, x_max):
313            first_cell = None
314            for y in range(y_min + 1, y_max):
315                cell = quadrant_cells[(x, y)]
316                if first_cell is None:
317                    first_cell = cell
318                else:
319                    if first_cell.shape == cell.shape:
320                        if frame_changed(first_cell, cell):
321                            vertical_uniform = False
322                            break
323            if not vertical_uniform:
324                break
325        if vertical_uniform:
326            return True
327        return False
328
329    def judge_movement(
330        self, previous_frame: np.ndarray, current_frame: np.ndarray
331    ) -> Tuple[bool, Optional[bool]]:
332        if not frame_changed(previous_frame, current_frame):
333            return False, False
334        flag = False
335        for quadrant in [
336            "screen_quadrant_1",
337            "screen_quadrant_2",
338            "screen_quadrant_3",
339            "screen_quadrant_4",
340        ]:
341            prev_quad = self._emulator.state_parser.capture_named_region(
342                previous_frame, quadrant
343            )
344            curr_quad = self._emulator.state_parser.capture_named_region(
345                current_frame, quadrant
346            )
347            prev_uniform = (
348                prev_quad.max() == prev_quad.min()
349                or self._is_uniform_quadrant(previous_frame, quadrant)
350            )
351            curr_uniform = (
352                curr_quad.max() == curr_quad.min()
353                or self._is_uniform_quadrant(current_frame, quadrant)
354            )
355            if (
356                not frame_changed(prev_quad, curr_quad)
357                and not prev_uniform
358                and not curr_uniform
359            ):
360                flag = True
361                break
362        if flag:
363            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
364                previous_frame
365            )[(0, 0)]
366            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
367                current_frame
368            )[(0, 0)]
369            if frame_changed(prev_player_cell, curr_player_cell):
370                return False, True
371            return False, False
372        return True, None
373
374    def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]:
375        action_dict = {
376            "right": LowLevelActions.PRESS_ARROW_RIGHT,
377            "down": LowLevelActions.PRESS_ARROW_DOWN,
378            "up": LowLevelActions.PRESS_ARROW_UP,
379            "left": LowLevelActions.PRESS_ARROW_LEFT,
380        }
381        if direction not in action_dict.keys():
382            log_error(f"Got invalid direction to move {direction}", self._parameters)
383        action = action_dict[direction]
384        action_success = -1
385        transition_state_dicts = []
386        transition_frames = []
387        previous_frame = self._emulator.get_current_frame()
388        n_step = 0
389        n_successful_steps = 0
390        has_rotated = None
391        agent_state = AgentState.FREE_ROAM
392        while n_step < steps and agent_state == AgentState.FREE_ROAM:
393            frames, done = self._emulator.step(action)
394            transition_state_dicts.append(self._state_tracker.report())
395            transition_frames.extend(frames)
396            current_frame = self._emulator.get_current_frame()
397            if done:
398                break
399            player_moved, player_rotated = self.judge_movement(
400                previous_frame, current_frame
401            )
402            if player_rotated is True:
403                has_rotated = True
404            if not player_moved and not player_rotated:
405                break
406            if player_moved:
407                n_successful_steps += 1
408            agent_state = self._emulator.state_parser.get_agent_state(
409                self._emulator.get_current_frame()
410            )
411            if agent_state != AgentState.FREE_ROAM:
412                break
413            n_step += 1
414            previous_frame = current_frame
415        if agent_state != AgentState.FREE_ROAM:
416            action_success = 2
417        else:
418            if n_step <= 0:
419                action_success = -1
420            elif n_step == steps:
421                action_success = 0
422            else:
423                action_success = 1
424        transition_state_dicts[-1]["core"]["action_return"] = {
425            "n_steps_taken": n_successful_steps,
426            "rotated": has_rotated,
427        }
428        return transition_state_dicts, action_success
429
430    def is_valid(self, **kwargs):
431        return (
432            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
433            == AgentState.FREE_ROAM
434        )
435
436
437class MoveStepsAction(BaseMovementAction):
438    """
439    Moves the agent in a specified cardinal direction for a specified number of steps.
440    """
441
442    def get_action_space(self):
443        return Discrete(4 * HARD_MAX_STEPS)
444
445    def space_to_parameters(self, space_action):
446        direction = None
447        steps = None
448        if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS:
449            return None
450        if space_action < HARD_MAX_STEPS:
451            direction = "up"
452            steps = space_action
453        elif space_action < 2 * HARD_MAX_STEPS:
454            direction = "down"
455            steps = space_action - HARD_MAX_STEPS
456        elif space_action < 3 * HARD_MAX_STEPS:
457            direction = "left"
458            steps = space_action - 2 * HARD_MAX_STEPS
459        else:
460            direction = "right"
461            steps = space_action - 3 * HARD_MAX_STEPS
462        return {"direction": direction, "steps": steps + 1}
463
464    def parameters_to_space(self, direction: str, steps: int):
465        if steps is None or steps <= 0 or steps > HARD_MAX_STEPS:
466            return None
467        if direction == "up":
468            return steps - 1
469        if direction == "down":
470            return HARD_MAX_STEPS + steps - 1
471        if direction == "left":
472            return 2 * HARD_MAX_STEPS + steps - 1
473        if direction == "right":
474            return 3 * HARD_MAX_STEPS + steps - 1
475        return None
476
477    def _execute(self, direction, steps):
478        transition_states, status = self.move(direction=direction, steps=steps)
479        return transition_states, status
480
481    def is_valid(self, **kwargs):
482        direction = kwargs.get("direction")
483        steps = kwargs.get("steps")
484        if direction is not None and direction not in ["up", "down", "left", "right"]:
485            return False
486        if steps is not None:
487            if not isinstance(steps, int):
488                return False
489            if steps <= 0:
490                return False
491        return super().is_valid(**kwargs)
492
493    @staticmethod
494    def get_action_name(direction: str, steps: int) -> str:
495        return f"Move {direction} {steps}"
496
497
498class MoveGridAction(BaseMovementAction):
499    """
500    Moves the agent on both axes. Will always try to move right/left first and then up/down.
501    """
502
503    def get_action_space(self):
504        return Box(
505            low=-HARD_MAX_STEPS // 2,
506            high=HARD_MAX_STEPS // 2,
507            shape=(2,),
508            dtype=np.int8,
509        )
510
511    def space_to_parameters(self, space_action):
512        right_action = space_action[0]
513        up_action = space_action[1]
514        return {"x_steps": right_action, "y_steps": up_action}
515
516    def parameters_to_space(self, x_steps, y_steps):
517        move_vec = np.zeros(2)
518        move_vec[0] = x_steps
519        move_vec[1] = y_steps
520        return move_vec
521
522    def _execute(self, x_steps, y_steps):
523        x_direction = "right" if x_steps >= 0 else "left"
524        y_direction = "up" if y_steps >= 0 else "down"
525        if x_steps != 0:
526            transition_states, status = self.move(
527                direction=x_direction, steps=abs(x_steps)
528            )
529            if status != 0:
530                return transition_states, status
531        else:
532            transition_states = []
533        if y_steps != 0:
534            more_transition_states, status = self.move(
535                direction=y_direction, steps=abs(y_steps)
536            )
537            transition_states.extend(more_transition_states)
538        try:
539            status is not None
540        except NameError:
541            log_warn(
542                "Weird case where both x_steps and y_steps are 0 in MoveGridAction or something.",
543                self._parameters,
544            )
545            transition_states = [self._state_tracker.report()]
546            status = -1
547        return transition_states, status
548
549    def is_valid(self, x_steps: int = None, y_steps: int = None):
550        if x_steps is not None and y_steps is not None:
551            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
552                return False
553            if x_steps == 0 and y_steps == 0:
554                return False
555        return super().is_valid()
556
557    @staticmethod
558    def get_action_name(x_steps: int, y_steps: int) -> str:
559        return f"MoveGrid ({x_steps}, {y_steps})"
560
561
562class MenuAction(HighLevelAction):
563    """
564    Allows navigation and option selection within Deja Vu menus.
565
566    Is Valid When:
567    - In Menu State
568
569    Action Success Interpretation:
570    - -1: Frame did not change.
571    - 0: Frame changed.
572    """
573
574    REQUIRED_STATE_PARSER = DejaVuStateParser
575    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
576
577    _MENU_ACTION_MAP = {
578        "up": LowLevelActions.PRESS_ARROW_UP,
579        "down": LowLevelActions.PRESS_ARROW_DOWN,
580        "confirm": LowLevelActions.PRESS_BUTTON_A,
581        "left": LowLevelActions.PRESS_ARROW_LEFT,
582        "right": LowLevelActions.PRESS_ARROW_RIGHT,
583        "back": LowLevelActions.PRESS_BUTTON_B,
584    }
585
586    _MENU_ACTION_KEYS = list(_MENU_ACTION_MAP.keys())
587
588    def is_valid(self, **kwargs):
589        menu_action = kwargs.get("menu_action", None)
590        if menu_action is not None and menu_action not in self._MENU_ACTION_KEYS:
591            return False
592        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
593        return state == AgentState.IN_MENU
594
595    def get_action_space(self):
596        return Discrete(len(self._MENU_ACTION_MAP))
597
598    def parameters_to_space(self, menu_action):
599        if menu_action not in self._MENU_ACTION_KEYS:
600            return None
601        return self._MENU_ACTION_KEYS.index(menu_action)
602
603    def space_to_parameters(self, space_action):
604        if space_action < 0 or space_action >= len(self._MENU_ACTION_MAP):
605            return None
606        menu_action = self._MENU_ACTION_KEYS[space_action]
607        return {"menu_action": menu_action}
608
609    def _execute(self, menu_action):
610        action = self._MENU_ACTION_MAP[menu_action]
611        current_frame = self._emulator.get_current_frame()
612        frames, done = self._emulator.step(action)
613        action_success = 0 if frame_changed(current_frame, frames[-1]) else -1
614        return [self._state_tracker.report()], action_success
615
616    @staticmethod
617    def get_action_name(menu_action: str) -> str:
618        return f"Menu {menu_action}"
619
620
621class OpenMenuAction(HighLevelAction):
622    """
623    Opens the investigation menu from free roam and optionally navigates to a section.
624
625    Is Valid When:
626    - In Free Roam State
627
628    Action Success Interpretation:
629    - -1: Could not open the menu or reach the requested section.
630    - 0: Menu opened (and section selected if specified).
631    """
632
633    OPTIONS = ["open", "case_notes", "evidence", "location"]
634
635    REQUIRED_STATE_PARSER = DejaVuStateParser
636    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
637
638    def get_action_space(self):
639        return Discrete(len(self.OPTIONS))
640
641    def space_to_parameters(self, space_action):
642        if space_action < 0 or space_action >= len(self.OPTIONS):
643            return None
644        return {"option": self.OPTIONS[space_action]}
645
646    def parameters_to_space(self, option: Optional[str] = None):
647        if option is None:
648            option = "open"
649        if option not in self.OPTIONS:
650            return None
651        return self.OPTIONS.index(option)
652
653    def is_valid(self, option: Optional[str] = None):
654        if option is not None and option not in self.OPTIONS:
655            return False
656        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
657        return state == AgentState.FREE_ROAM
658
659    def _is_target_menu(self, option: str, frame: np.ndarray) -> bool:
660        parser = self._emulator.state_parser
661        if option == "case_notes":
662            return parser.is_in_case_notes(frame)
663        if option == "evidence":
664            return parser.is_in_evidence_menu(frame)
665        if option == "location":
666            return parser.is_location_menu_open(frame)
667        return False
668
669    def _execute(self, option: Optional[str] = None):
670        if option is None:
671            option = "open"
672        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
673        state_reports = [self._state_tracker.report()]
674        current_frame = self._emulator.get_current_frame()
675        if not self._emulator.state_parser.is_in_menu(current_frame):
676            return state_reports, -1
677        if option == "open":
678            return state_reports, 0
679        if self._is_target_menu(option, current_frame):
680            return state_reports, 0
681        for action in [
682            LowLevelActions.PRESS_ARROW_RIGHT,
683            LowLevelActions.PRESS_ARROW_LEFT,
684        ]:
685            for _ in range(MENU_NAV_MAX_STEPS):
686                self._emulator.step(action)
687                state_reports.append(self._state_tracker.report())
688                current_frame = self._emulator.get_current_frame()
689                if self._is_target_menu(option, current_frame):
690                    return state_reports, 0
691        return state_reports, -1
692
693    @staticmethod
694    def get_action_name(option: str) -> str:
695        return f"OpenMenu {option}"
696
697
698class PuzzleAction(HighLevelAction):
699    """
700    Allows navigation and option selection within Deja Vu puzzle/deduction states.
701
702    Is Valid When:
703    - In Puzzle State
704
705    Action Success Interpretation:
706    - -1: Frame did not change.
707    - 0: Frame changed.
708    """
709
710    REQUIRED_STATE_PARSER = DejaVuStateParser
711    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
712
713    _PUZZLE_ACTION_MAP = {
714        "up": LowLevelActions.PRESS_ARROW_UP,
715        "down": LowLevelActions.PRESS_ARROW_DOWN,
716        "confirm": LowLevelActions.PRESS_BUTTON_A,
717        "left": LowLevelActions.PRESS_ARROW_LEFT,
718        "right": LowLevelActions.PRESS_ARROW_RIGHT,
719        "back": LowLevelActions.PRESS_BUTTON_B,
720    }
721
722    _PUZZLE_ACTION_KEYS = list(_PUZZLE_ACTION_MAP.keys())
723
724    def is_valid(self, **kwargs):
725        puzzle_action = kwargs.get("puzzle_action", None)
726        if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS:
727            return False
728        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
729        return state == AgentState.IN_PUZZLE
730
731    def get_action_space(self):
732        return Discrete(len(self._PUZZLE_ACTION_MAP))
733
734    def parameters_to_space(self, puzzle_action):
735        if puzzle_action not in self._PUZZLE_ACTION_KEYS:
736            return None
737        return self._PUZZLE_ACTION_KEYS.index(puzzle_action)
738
739    def space_to_parameters(self, space_action):
740        if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP):
741            return None
742        puzzle_action = self._PUZZLE_ACTION_KEYS[space_action]
743        return {"puzzle_action": puzzle_action}
744
745    def _execute(self, puzzle_action):
746        action = self._PUZZLE_ACTION_MAP[puzzle_action]
747        current_frame = self._emulator.get_current_frame()
748        frames, done = self._emulator.step(action)
749        action_success = 0 if frame_changed(current_frame, frames[-1]) else -1
750        return [self._state_tracker.report()], action_success
751
752    @staticmethod
753    def get_action_name(puzzle_action: str) -> str:
754        return f"Puzzle {puzzle_action}"
HARD_MAX_STEPS = 20
def frame_changed( past: numpy.ndarray, present: numpy.ndarray, epsilon: float = 0.01) -> bool:
16def frame_changed(past: np.ndarray, present: np.ndarray, epsilon: float = 0.01) -> bool:
17    """Return True if the mean pixel difference between two frames is greater than epsilon."""
18    return np.abs(past - present).mean() > epsilon

Return True if the mean pixel difference between two frames is greater than epsilon.

class TickUntilStable(gameboy_worlds.interface.action.SingleHighLevelAction):
21class TickUntilStable(SingleHighLevelAction):
22    """Tick the emulator until the screen becomes stable (no change)."""
23
24    REQUIRED_STATE_PARSER = DejaVuStateParser
25    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
26
27    def is_valid(self, **kwargs):
28        return True
29
30    def _execute(self, max_ticks: int = 30):
31        prev = self._emulator.get_current_frame()
32        for _ in range(max_ticks):
33            self._emulator.step(None)
34            curr = self._emulator.get_current_frame()
35            if not frame_changed(prev, curr):
36                return [self._state_tracker.report()], 0
37            prev = curr
38        return [self._state_tracker.report()], -1
39
40    @staticmethod
41    def get_action_name() -> str:
42        return "TickUntilStable"

Tick the emulator until the screen becomes stable (no change).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
27    def is_valid(self, **kwargs):
28        return True

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
40    @staticmethod
41    def get_action_name() -> str:
42        return "TickUntilStable"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

45class MoveCursor(SingleHighLevelAction):
46    """Move the menu cursor in a given direction (up, down, left, right)."""
47
48    REQUIRED_STATE_PARSER = DejaVuStateParser
49    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
50    _ACTION_MAP = {
51        "up": LowLevelActions.PRESS_ARROW_UP,
52        "down": LowLevelActions.PRESS_ARROW_DOWN,
53        "left": LowLevelActions.PRESS_ARROW_LEFT,
54        "right": LowLevelActions.PRESS_ARROW_RIGHT,
55    }
56
57    def is_valid(self, direction=None, **kwargs):
58        return direction in self._ACTION_MAP
59
60    def _execute(self, direction):
61        action = self._ACTION_MAP[direction]
62        prev = self._emulator.get_current_frame()
63        self._emulator.step(action)
64        curr = self._emulator.get_current_frame()
65        return [self._state_tracker.report()], 0 if frame_changed(prev, curr) else -1
66
67    @staticmethod
68    def get_action_name(**kwargs) -> str:
69        return f"MoveCursor {kwargs.get('direction', '')}"

Move the menu cursor in a given direction (up, down, left, right).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, direction=None, **kwargs):
57    def is_valid(self, direction=None, **kwargs):
58        return direction in self._ACTION_MAP

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name(**kwargs) -> str:
67    @staticmethod
68    def get_action_name(**kwargs) -> str:
69        return f"MoveCursor {kwargs.get('direction', '')}"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

 72class MoveGrid(HighLevelAction):
 73    """Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical."""
 74
 75    REQUIRED_STATE_PARSER = DejaVuStateParser
 76    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
 77
 78    def get_action_space(self):
 79        return Box(
 80            low=-HARD_MAX_STEPS // 2,
 81            high=HARD_MAX_STEPS // 2,
 82            shape=(2,),
 83            dtype=np.int8,
 84        )
 85
 86    def is_valid(self, x_steps=None, y_steps=None, **kwargs):
 87        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
 88        return state == AgentState.FREE_ROAM and (x_steps or y_steps)
 89
 90    def _execute(self, x_steps, y_steps):
 91        reports = []
 92        for axis, steps, dir_pos, dir_neg in [
 93            ("x", x_steps, "right", "left"),
 94            ("y", y_steps, "down", "up"),
 95        ]:
 96            if steps:
 97                direction = dir_pos if steps > 0 else dir_neg
 98                for _ in range(abs(steps)):
 99                    prev = self._emulator.get_current_frame()
100                    self._emulator.step(MoveStepsAction._ACTION_MAP[direction])
101                    curr = self._emulator.get_current_frame()
102                    reports.append(self._state_tracker.report())
103                    if not frame_changed(prev, curr):
104                        return reports, -1
105        return reports, 0
106
107    @staticmethod
108    def get_action_name(x_steps: int, y_steps: int) -> str:
109        return f"MoveGrid ({x_steps}, {y_steps})"

Move the agent on the grid by (x_steps, y_steps). X is horizontal, Y is vertical.

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def get_action_space(self):
78    def get_action_space(self):
79        return Box(
80            low=-HARD_MAX_STEPS // 2,
81            high=HARD_MAX_STEPS // 2,
82            shape=(2,),
83            dtype=np.int8,
84        )

Returns the Gym defined Space that characterizes the high level action's parameter space.

You can use this API to get a Space for sampling high level actions of this type.

Returns:

Space: The Gym space that characterizes the high level action's parameter space.

def is_valid(self, x_steps=None, y_steps=None, **kwargs):
86    def is_valid(self, x_steps=None, y_steps=None, **kwargs):
87        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
88        return state == AgentState.FREE_ROAM and (x_steps or y_steps)

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name(x_steps: int, y_steps: int) -> str:
107    @staticmethod
108    def get_action_name(x_steps: int, y_steps: int) -> str:
109        return f"MoveGrid ({x_steps}, {y_steps})"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class OpenButtonMenu(gameboy_worlds.interface.action.SingleHighLevelAction):
112class OpenButtonMenu(SingleHighLevelAction):
113    """Open the bottom button menu (usually START button)."""
114
115    REQUIRED_STATE_PARSER = DejaVuStateParser
116    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
117
118    def is_valid(self, **kwargs):
119        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
120        return state == AgentState.FREE_ROAM
121
122    def _execute(self):
123        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
124        return [self._state_tracker.report()], 0
125
126    @staticmethod
127    def get_action_name() -> str:
128        return "OpenButtonMenu"

Open the bottom button menu (usually START button).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
118    def is_valid(self, **kwargs):
119        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
120        return state == AgentState.FREE_ROAM

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
126    @staticmethod
127    def get_action_name() -> str:
128        return "OpenButtonMenu"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class CloseButtonMenu(gameboy_worlds.interface.action.SingleHighLevelAction):
131class CloseButtonMenu(SingleHighLevelAction):
132    """Close the bottom button menu (usually B button)."""
133
134    REQUIRED_STATE_PARSER = DejaVuStateParser
135    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
136
137    def is_valid(self, **kwargs):
138        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
139        return state == AgentState.IN_MENU
140
141    def _execute(self):
142        self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
143        return [self._state_tracker.report()], 0
144
145    @staticmethod
146    def get_action_name() -> str:
147        return "CloseButtonMenu"

Close the bottom button menu (usually B button).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
137    def is_valid(self, **kwargs):
138        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
139        return state == AgentState.IN_MENU

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
145    @staticmethod
146    def get_action_name() -> str:
147        return "CloseButtonMenu"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class SelectMenuOption(gameboy_worlds.interface.action.SingleHighLevelAction):
150class SelectMenuOption(SingleHighLevelAction):
151    """Select the current menu option (A button)."""
152
153    REQUIRED_STATE_PARSER = DejaVuStateParser
154    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
155
156    def is_valid(self, **kwargs):
157        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
158        return state == AgentState.IN_MENU
159
160    def _execute(self):
161        self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
162        return [self._state_tracker.report()], 0
163
164    @staticmethod
165    def get_action_name() -> str:
166        return "SelectMenuOption"

Select the current menu option (A button).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
156    def is_valid(self, **kwargs):
157        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
158        return state == AgentState.IN_MENU

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
164    @staticmethod
165    def get_action_name() -> str:
166        return "SelectMenuOption"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class AdvanceDialogue(gameboy_worlds.interface.action.SingleHighLevelAction):
169class AdvanceDialogue(SingleHighLevelAction):
170    """Advance dialogue (B button)."""
171
172    REQUIRED_STATE_PARSER = DejaVuStateParser
173    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
174
175    def is_valid(self, **kwargs):
176        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
177        return state == AgentState.IN_DIALOGUE
178
179    def _execute(self):
180        self._emulator.step(LowLevelActions.PRESS_BUTTON_B)
181        return [self._state_tracker.report()], 0
182
183    @staticmethod
184    def get_action_name() -> str:
185        return "AdvanceDialogue"

Advance dialogue (B button).

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
175    def is_valid(self, **kwargs):
176        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
177        return state == AgentState.IN_DIALOGUE

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
183    @staticmethod
184    def get_action_name() -> str:
185        return "AdvanceDialogue"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class MoveStepsAction(BaseMovementAction):
189class MoveStepsAction:
190    _ACTION_MAP = {
191        "up": LowLevelActions.PRESS_ARROW_UP,
192        "down": LowLevelActions.PRESS_ARROW_DOWN,
193        "left": LowLevelActions.PRESS_ARROW_LEFT,
194        "right": LowLevelActions.PRESS_ARROW_RIGHT,
195    }

Moves the agent in a specified cardinal direction for a specified number of steps.

def is_valid(self, **kwargs):
482    def is_valid(self, **kwargs):
483        direction = kwargs.get("direction")
484        steps = kwargs.get("steps")
485        if direction is not None and direction not in ["up", "down", "left", "right"]:
486            return False
487        if steps is not None:
488            if not isinstance(steps, int):
489                return False
490            if steps <= 0:
491                return False
492        return super().is_valid(**kwargs)

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

def get_action_space(self):
443    def get_action_space(self):
444        return Discrete(4 * HARD_MAX_STEPS)

Returns the Gym defined Space that characterizes the high level action's parameter space.

You can use this API to get a Space for sampling high level actions of this type.

Returns:

Space: The Gym space that characterizes the high level action's parameter space.

def space_to_parameters(self, space_action):
446    def space_to_parameters(self, space_action):
447        direction = None
448        steps = None
449        if space_action < 0 or space_action >= 4 * HARD_MAX_STEPS:
450            return None
451        if space_action < HARD_MAX_STEPS:
452            direction = "up"
453            steps = space_action
454        elif space_action < 2 * HARD_MAX_STEPS:
455            direction = "down"
456            steps = space_action - HARD_MAX_STEPS
457        elif space_action < 3 * HARD_MAX_STEPS:
458            direction = "left"
459            steps = space_action - 2 * HARD_MAX_STEPS
460        else:
461            direction = "right"
462            steps = space_action - 3 * HARD_MAX_STEPS
463        return {"direction": direction, "steps": steps + 1}

Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.

Parameters
  • space_action: The action in the high level action's parameter space.
Returns

The high level action's parameters corresponding to the space action.

def parameters_to_space(self, direction: str, steps: int):
465    def parameters_to_space(self, direction: str, steps: int):
466        if steps is None or steps <= 0 or steps > HARD_MAX_STEPS:
467            return None
468        if direction == "up":
469            return steps - 1
470        if direction == "down":
471            return HARD_MAX_STEPS + steps - 1
472        if direction == "left":
473            return 2 * HARD_MAX_STEPS + steps - 1
474        if direction == "right":
475            return 3 * HARD_MAX_STEPS + steps - 1
476        return None

Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.

Parameters
  • kwargs: The high level action's parameters.
Returns

The action in the high level action's parameter space.

@staticmethod
def get_action_name(direction: str, steps: int) -> str:
494    @staticmethod
495    def get_action_name(direction: str, steps: int) -> str:
496        return f"Move {direction} {steps}"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class InteractAction(gameboy_worlds.interface.action.SingleHighLevelAction):
198class InteractAction(SingleHighLevelAction):
199    """
200    Presses the A button to interact with an object in front of the agent.
201
202    Is Valid When:
203    - In Free Roam State
204
205    Action Success Interpretation:
206    - -1: Frame did not change or agent still in free roam state
207    - 1: Agent not in free roam state
208    """
209
210    REQUIRED_STATE_PARSER = DejaVuStateParser
211    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
212
213    def is_valid(self, **kwargs):
214        return (
215            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
216            == AgentState.FREE_ROAM
217        )
218
219    def _execute(self):
220        frames, done = self._emulator.step(LowLevelActions.PRESS_BUTTON_A)
221        action_success = 0
222        prev_frames = []
223        for frame in frames:
224            if (
225                self._emulator.state_parser.get_agent_state(frame)
226                != AgentState.FREE_ROAM
227            ):
228                action_success = 1
229                break
230            for past_frame in prev_frames:
231                if not frame_changed(past_frame, frame):
232                    action_success = -1
233                    break
234            if action_success != 0:
235                break
236            prev_frames.append(frame)
237        if action_success == 0:
238            action_success = -1
239        return [self._state_tracker.report()], action_success
240
241    @staticmethod
242    def get_action_name() -> str:
243        return "Interact"

Presses the A button to interact with an object in front of the agent.

Is Valid When:

  • In Free Roam State

Action Success Interpretation:

  • -1: Frame did not change or agent still in free roam state
  • 1: Agent not in free roam state

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
213    def is_valid(self, **kwargs):
214        return (
215            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
216            == AgentState.FREE_ROAM
217        )

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name() -> str:
241    @staticmethod
242    def get_action_name() -> str:
243        return "Interact"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class BaseMovementAction(gameboy_worlds.interface.action.HighLevelAction, abc.ABC):
246class BaseMovementAction(HighLevelAction, ABC):
247    """
248    Base class for movement actions in the Deja Vu environment.
249
250    Is Valid When:
251    - In Free Roam State
252
253    Action Success Interpretation:
254    - -1: Frame did not change, even on the first step
255    - 0: Finished all steps
256    - 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle.
257    - 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.
258
259    Action Returns:
260    - n_steps_taken (int): Number of steps actually taken
261    - rotated (bool or None): True if the player has not moved, but has rotated.
262    """
263
264    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
265    REQUIRED_STATE_PARSER = DejaVuStateParser
266
267    def _is_uniform_quadrant(self, frame, quadrant_name) -> bool:
268        mapper = {
269            "screen_quadrant_1": "tr",
270            "screen_quadrant_2": "tl",
271            "screen_quadrant_3": "bl",
272            "screen_quadrant_4": "br",
273        }
274        quadrant_cells = self._emulator.state_parser.capture_grid_cells(
275            frame, quadrant=mapper[quadrant_name]
276        )
277        keys = list(quadrant_cells.keys())
278        x_min = 1e9
279        y_min = 1e9
280        x_max = -1e9
281        y_max = -1e9
282        for key in keys:
283            x_coord, y_coord = key
284            if x_coord * y_coord == 0:
285                quadrant_cells.pop(key)
286            else:
287                x_min = min(x_min, x_coord)
288                y_min = min(y_min, y_coord)
289                x_max = max(x_max, x_coord)
290                y_max = max(y_max, y_coord)
291        x_min = int(x_min)
292        y_min = int(y_min)
293        x_max = int(x_max)
294        y_max = int(y_max)
295        keys = list(quadrant_cells.keys())
296        vertical_uniform = True
297        horizontal_uniform = True
298        for y in range(y_min + 1, y_max):
299            first_cell = None
300            for x in range(x_min + 1, x_max):
301                cell = quadrant_cells[(x, y)]
302                if first_cell is None:
303                    first_cell = cell
304                else:
305                    if first_cell.shape == cell.shape:
306                        if frame_changed(first_cell, cell):
307                            horizontal_uniform = False
308                            break
309            if not horizontal_uniform:
310                break
311        if horizontal_uniform:
312            return True
313        for x in range(x_min + 1, x_max):
314            first_cell = None
315            for y in range(y_min + 1, y_max):
316                cell = quadrant_cells[(x, y)]
317                if first_cell is None:
318                    first_cell = cell
319                else:
320                    if first_cell.shape == cell.shape:
321                        if frame_changed(first_cell, cell):
322                            vertical_uniform = False
323                            break
324            if not vertical_uniform:
325                break
326        if vertical_uniform:
327            return True
328        return False
329
330    def judge_movement(
331        self, previous_frame: np.ndarray, current_frame: np.ndarray
332    ) -> Tuple[bool, Optional[bool]]:
333        if not frame_changed(previous_frame, current_frame):
334            return False, False
335        flag = False
336        for quadrant in [
337            "screen_quadrant_1",
338            "screen_quadrant_2",
339            "screen_quadrant_3",
340            "screen_quadrant_4",
341        ]:
342            prev_quad = self._emulator.state_parser.capture_named_region(
343                previous_frame, quadrant
344            )
345            curr_quad = self._emulator.state_parser.capture_named_region(
346                current_frame, quadrant
347            )
348            prev_uniform = (
349                prev_quad.max() == prev_quad.min()
350                or self._is_uniform_quadrant(previous_frame, quadrant)
351            )
352            curr_uniform = (
353                curr_quad.max() == curr_quad.min()
354                or self._is_uniform_quadrant(current_frame, quadrant)
355            )
356            if (
357                not frame_changed(prev_quad, curr_quad)
358                and not prev_uniform
359                and not curr_uniform
360            ):
361                flag = True
362                break
363        if flag:
364            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
365                previous_frame
366            )[(0, 0)]
367            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
368                current_frame
369            )[(0, 0)]
370            if frame_changed(prev_player_cell, curr_player_cell):
371                return False, True
372            return False, False
373        return True, None
374
375    def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]:
376        action_dict = {
377            "right": LowLevelActions.PRESS_ARROW_RIGHT,
378            "down": LowLevelActions.PRESS_ARROW_DOWN,
379            "up": LowLevelActions.PRESS_ARROW_UP,
380            "left": LowLevelActions.PRESS_ARROW_LEFT,
381        }
382        if direction not in action_dict.keys():
383            log_error(f"Got invalid direction to move {direction}", self._parameters)
384        action = action_dict[direction]
385        action_success = -1
386        transition_state_dicts = []
387        transition_frames = []
388        previous_frame = self._emulator.get_current_frame()
389        n_step = 0
390        n_successful_steps = 0
391        has_rotated = None
392        agent_state = AgentState.FREE_ROAM
393        while n_step < steps and agent_state == AgentState.FREE_ROAM:
394            frames, done = self._emulator.step(action)
395            transition_state_dicts.append(self._state_tracker.report())
396            transition_frames.extend(frames)
397            current_frame = self._emulator.get_current_frame()
398            if done:
399                break
400            player_moved, player_rotated = self.judge_movement(
401                previous_frame, current_frame
402            )
403            if player_rotated is True:
404                has_rotated = True
405            if not player_moved and not player_rotated:
406                break
407            if player_moved:
408                n_successful_steps += 1
409            agent_state = self._emulator.state_parser.get_agent_state(
410                self._emulator.get_current_frame()
411            )
412            if agent_state != AgentState.FREE_ROAM:
413                break
414            n_step += 1
415            previous_frame = current_frame
416        if agent_state != AgentState.FREE_ROAM:
417            action_success = 2
418        else:
419            if n_step <= 0:
420                action_success = -1
421            elif n_step == steps:
422                action_success = 0
423            else:
424                action_success = 1
425        transition_state_dicts[-1]["core"]["action_return"] = {
426            "n_steps_taken": n_successful_steps,
427            "rotated": has_rotated,
428        }
429        return transition_state_dicts, action_success
430
431    def is_valid(self, **kwargs):
432        return (
433            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
434            == AgentState.FREE_ROAM
435        )

Base class for movement actions in the Deja Vu environment.

Is Valid When:

  • In Free Roam State

Action Success Interpretation:

  • -1: Frame did not change, even on the first step
  • 0: Finished all steps
  • 1: Took some steps, but not all, and then frame stopped changing OR the frame starts oscillating (trying to check for jitter). This usually means we ran into an obstacle.
  • 2: Took some steps, but agent state changed from free roam. This often means we entered a cutscene or battle.

Action Returns:

  • n_steps_taken (int): Number of steps actually taken
  • rotated (bool or None): True if the player has not moved, but has rotated.

The state tracker that tracks the minimal state information required for the action to function.

The state parser that parses the minimal state information required for the action to function.

def judge_movement( self, previous_frame: numpy.ndarray, current_frame: numpy.ndarray) -> Tuple[bool, Optional[bool]]:
330    def judge_movement(
331        self, previous_frame: np.ndarray, current_frame: np.ndarray
332    ) -> Tuple[bool, Optional[bool]]:
333        if not frame_changed(previous_frame, current_frame):
334            return False, False
335        flag = False
336        for quadrant in [
337            "screen_quadrant_1",
338            "screen_quadrant_2",
339            "screen_quadrant_3",
340            "screen_quadrant_4",
341        ]:
342            prev_quad = self._emulator.state_parser.capture_named_region(
343                previous_frame, quadrant
344            )
345            curr_quad = self._emulator.state_parser.capture_named_region(
346                current_frame, quadrant
347            )
348            prev_uniform = (
349                prev_quad.max() == prev_quad.min()
350                or self._is_uniform_quadrant(previous_frame, quadrant)
351            )
352            curr_uniform = (
353                curr_quad.max() == curr_quad.min()
354                or self._is_uniform_quadrant(current_frame, quadrant)
355            )
356            if (
357                not frame_changed(prev_quad, curr_quad)
358                and not prev_uniform
359                and not curr_uniform
360            ):
361                flag = True
362                break
363        if flag:
364            prev_player_cell = self._emulator.state_parser.capture_grid_cells(
365                previous_frame
366            )[(0, 0)]
367            curr_player_cell = self._emulator.state_parser.capture_grid_cells(
368                current_frame
369            )[(0, 0)]
370            if frame_changed(prev_player_cell, curr_player_cell):
371                return False, True
372            return False, False
373        return True, None
def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]:
375    def move(self, direction: str, steps: int) -> Tuple[List[Dict], int]:
376        action_dict = {
377            "right": LowLevelActions.PRESS_ARROW_RIGHT,
378            "down": LowLevelActions.PRESS_ARROW_DOWN,
379            "up": LowLevelActions.PRESS_ARROW_UP,
380            "left": LowLevelActions.PRESS_ARROW_LEFT,
381        }
382        if direction not in action_dict.keys():
383            log_error(f"Got invalid direction to move {direction}", self._parameters)
384        action = action_dict[direction]
385        action_success = -1
386        transition_state_dicts = []
387        transition_frames = []
388        previous_frame = self._emulator.get_current_frame()
389        n_step = 0
390        n_successful_steps = 0
391        has_rotated = None
392        agent_state = AgentState.FREE_ROAM
393        while n_step < steps and agent_state == AgentState.FREE_ROAM:
394            frames, done = self._emulator.step(action)
395            transition_state_dicts.append(self._state_tracker.report())
396            transition_frames.extend(frames)
397            current_frame = self._emulator.get_current_frame()
398            if done:
399                break
400            player_moved, player_rotated = self.judge_movement(
401                previous_frame, current_frame
402            )
403            if player_rotated is True:
404                has_rotated = True
405            if not player_moved and not player_rotated:
406                break
407            if player_moved:
408                n_successful_steps += 1
409            agent_state = self._emulator.state_parser.get_agent_state(
410                self._emulator.get_current_frame()
411            )
412            if agent_state != AgentState.FREE_ROAM:
413                break
414            n_step += 1
415            previous_frame = current_frame
416        if agent_state != AgentState.FREE_ROAM:
417            action_success = 2
418        else:
419            if n_step <= 0:
420                action_success = -1
421            elif n_step == steps:
422                action_success = 0
423            else:
424                action_success = 1
425        transition_state_dicts[-1]["core"]["action_return"] = {
426            "n_steps_taken": n_successful_steps,
427            "rotated": has_rotated,
428        }
429        return transition_state_dicts, action_success
def is_valid(self, **kwargs):
431    def is_valid(self, **kwargs):
432        return (
433            self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
434            == AgentState.FREE_ROAM
435        )

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

class MoveGridAction(BaseMovementAction):
499class MoveGridAction(BaseMovementAction):
500    """
501    Moves the agent on both axes. Will always try to move right/left first and then up/down.
502    """
503
504    def get_action_space(self):
505        return Box(
506            low=-HARD_MAX_STEPS // 2,
507            high=HARD_MAX_STEPS // 2,
508            shape=(2,),
509            dtype=np.int8,
510        )
511
512    def space_to_parameters(self, space_action):
513        right_action = space_action[0]
514        up_action = space_action[1]
515        return {"x_steps": right_action, "y_steps": up_action}
516
517    def parameters_to_space(self, x_steps, y_steps):
518        move_vec = np.zeros(2)
519        move_vec[0] = x_steps
520        move_vec[1] = y_steps
521        return move_vec
522
523    def _execute(self, x_steps, y_steps):
524        x_direction = "right" if x_steps >= 0 else "left"
525        y_direction = "up" if y_steps >= 0 else "down"
526        if x_steps != 0:
527            transition_states, status = self.move(
528                direction=x_direction, steps=abs(x_steps)
529            )
530            if status != 0:
531                return transition_states, status
532        else:
533            transition_states = []
534        if y_steps != 0:
535            more_transition_states, status = self.move(
536                direction=y_direction, steps=abs(y_steps)
537            )
538            transition_states.extend(more_transition_states)
539        try:
540            status is not None
541        except NameError:
542            log_warn(
543                "Weird case where both x_steps and y_steps are 0 in MoveGridAction or something.",
544                self._parameters,
545            )
546            transition_states = [self._state_tracker.report()]
547            status = -1
548        return transition_states, status
549
550    def is_valid(self, x_steps: int = None, y_steps: int = None):
551        if x_steps is not None and y_steps is not None:
552            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
553                return False
554            if x_steps == 0 and y_steps == 0:
555                return False
556        return super().is_valid()
557
558    @staticmethod
559    def get_action_name(x_steps: int, y_steps: int) -> str:
560        return f"MoveGrid ({x_steps}, {y_steps})"

Moves the agent on both axes. Will always try to move right/left first and then up/down.

def get_action_space(self):
504    def get_action_space(self):
505        return Box(
506            low=-HARD_MAX_STEPS // 2,
507            high=HARD_MAX_STEPS // 2,
508            shape=(2,),
509            dtype=np.int8,
510        )

Returns the Gym defined Space that characterizes the high level action's parameter space.

You can use this API to get a Space for sampling high level actions of this type.

Returns:

Space: The Gym space that characterizes the high level action's parameter space.

def space_to_parameters(self, space_action):
512    def space_to_parameters(self, space_action):
513        right_action = space_action[0]
514        up_action = space_action[1]
515        return {"x_steps": right_action, "y_steps": up_action}

Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.

Parameters
  • space_action: The action in the high level action's parameter space.
Returns

The high level action's parameters corresponding to the space action.

def parameters_to_space(self, x_steps, y_steps):
517    def parameters_to_space(self, x_steps, y_steps):
518        move_vec = np.zeros(2)
519        move_vec[0] = x_steps
520        move_vec[1] = y_steps
521        return move_vec

Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.

Parameters
  • kwargs: The high level action's parameters.
Returns

The action in the high level action's parameter space.

def is_valid(self, x_steps: int = None, y_steps: int = None):
550    def is_valid(self, x_steps: int = None, y_steps: int = None):
551        if x_steps is not None and y_steps is not None:
552            if not isinstance(x_steps, int) or not isinstance(y_steps, int):
553                return False
554            if x_steps == 0 and y_steps == 0:
555                return False
556        return super().is_valid()

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name(x_steps: int, y_steps: int) -> str:
558    @staticmethod
559    def get_action_name(x_steps: int, y_steps: int) -> str:
560        return f"MoveGrid ({x_steps}, {y_steps})"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class OpenMenuAction(gameboy_worlds.interface.action.HighLevelAction):
622class OpenMenuAction(HighLevelAction):
623    """
624    Opens the investigation menu from free roam and optionally navigates to a section.
625
626    Is Valid When:
627    - In Free Roam State
628
629    Action Success Interpretation:
630    - -1: Could not open the menu or reach the requested section.
631    - 0: Menu opened (and section selected if specified).
632    """
633
634    OPTIONS = ["open", "case_notes", "evidence", "location"]
635
636    REQUIRED_STATE_PARSER = DejaVuStateParser
637    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
638
639    def get_action_space(self):
640        return Discrete(len(self.OPTIONS))
641
642    def space_to_parameters(self, space_action):
643        if space_action < 0 or space_action >= len(self.OPTIONS):
644            return None
645        return {"option": self.OPTIONS[space_action]}
646
647    def parameters_to_space(self, option: Optional[str] = None):
648        if option is None:
649            option = "open"
650        if option not in self.OPTIONS:
651            return None
652        return self.OPTIONS.index(option)
653
654    def is_valid(self, option: Optional[str] = None):
655        if option is not None and option not in self.OPTIONS:
656            return False
657        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
658        return state == AgentState.FREE_ROAM
659
660    def _is_target_menu(self, option: str, frame: np.ndarray) -> bool:
661        parser = self._emulator.state_parser
662        if option == "case_notes":
663            return parser.is_in_case_notes(frame)
664        if option == "evidence":
665            return parser.is_in_evidence_menu(frame)
666        if option == "location":
667            return parser.is_location_menu_open(frame)
668        return False
669
670    def _execute(self, option: Optional[str] = None):
671        if option is None:
672            option = "open"
673        self._emulator.step(LowLevelActions.PRESS_BUTTON_START)
674        state_reports = [self._state_tracker.report()]
675        current_frame = self._emulator.get_current_frame()
676        if not self._emulator.state_parser.is_in_menu(current_frame):
677            return state_reports, -1
678        if option == "open":
679            return state_reports, 0
680        if self._is_target_menu(option, current_frame):
681            return state_reports, 0
682        for action in [
683            LowLevelActions.PRESS_ARROW_RIGHT,
684            LowLevelActions.PRESS_ARROW_LEFT,
685        ]:
686            for _ in range(MENU_NAV_MAX_STEPS):
687                self._emulator.step(action)
688                state_reports.append(self._state_tracker.report())
689                current_frame = self._emulator.get_current_frame()
690                if self._is_target_menu(option, current_frame):
691                    return state_reports, 0
692        return state_reports, -1
693
694    @staticmethod
695    def get_action_name(option: str) -> str:
696        return f"OpenMenu {option}"

Opens the investigation menu from free roam and optionally navigates to a section.

Is Valid When:

  • In Free Roam State

Action Success Interpretation:

  • -1: Could not open the menu or reach the requested section.
  • 0: Menu opened (and section selected if specified).
OPTIONS = ['open', 'case_notes', 'evidence', 'location']

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def get_action_space(self):
639    def get_action_space(self):
640        return Discrete(len(self.OPTIONS))

Returns the Gym defined Space that characterizes the high level action's parameter space.

You can use this API to get a Space for sampling high level actions of this type.

Returns:

Space: The Gym space that characterizes the high level action's parameter space.

def space_to_parameters(self, space_action):
642    def space_to_parameters(self, space_action):
643        if space_action < 0 or space_action >= len(self.OPTIONS):
644            return None
645        return {"option": self.OPTIONS[space_action]}

Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.

Parameters
  • space_action: The action in the high level action's parameter space.
Returns

The high level action's parameters corresponding to the space action.

def parameters_to_space(self, option: Optional[str] = None):
647    def parameters_to_space(self, option: Optional[str] = None):
648        if option is None:
649            option = "open"
650        if option not in self.OPTIONS:
651            return None
652        return self.OPTIONS.index(option)

Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.

Parameters
  • kwargs: The high level action's parameters.
Returns

The action in the high level action's parameter space.

def is_valid(self, option: Optional[str] = None):
654    def is_valid(self, option: Optional[str] = None):
655        if option is not None and option not in self.OPTIONS:
656            return False
657        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
658        return state == AgentState.FREE_ROAM

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

@staticmethod
def get_action_name(option: str) -> str:
694    @staticmethod
695    def get_action_name(option: str) -> str:
696        return f"OpenMenu {option}"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.

class PuzzleAction(gameboy_worlds.interface.action.HighLevelAction):
699class PuzzleAction(HighLevelAction):
700    """
701    Allows navigation and option selection within Deja Vu puzzle/deduction states.
702
703    Is Valid When:
704    - In Puzzle State
705
706    Action Success Interpretation:
707    - -1: Frame did not change.
708    - 0: Frame changed.
709    """
710
711    REQUIRED_STATE_PARSER = DejaVuStateParser
712    REQUIRED_STATE_TRACKER = CoreDejaVuTracker
713
714    _PUZZLE_ACTION_MAP = {
715        "up": LowLevelActions.PRESS_ARROW_UP,
716        "down": LowLevelActions.PRESS_ARROW_DOWN,
717        "confirm": LowLevelActions.PRESS_BUTTON_A,
718        "left": LowLevelActions.PRESS_ARROW_LEFT,
719        "right": LowLevelActions.PRESS_ARROW_RIGHT,
720        "back": LowLevelActions.PRESS_BUTTON_B,
721    }
722
723    _PUZZLE_ACTION_KEYS = list(_PUZZLE_ACTION_MAP.keys())
724
725    def is_valid(self, **kwargs):
726        puzzle_action = kwargs.get("puzzle_action", None)
727        if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS:
728            return False
729        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
730        return state == AgentState.IN_PUZZLE
731
732    def get_action_space(self):
733        return Discrete(len(self._PUZZLE_ACTION_MAP))
734
735    def parameters_to_space(self, puzzle_action):
736        if puzzle_action not in self._PUZZLE_ACTION_KEYS:
737            return None
738        return self._PUZZLE_ACTION_KEYS.index(puzzle_action)
739
740    def space_to_parameters(self, space_action):
741        if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP):
742            return None
743        puzzle_action = self._PUZZLE_ACTION_KEYS[space_action]
744        return {"puzzle_action": puzzle_action}
745
746    def _execute(self, puzzle_action):
747        action = self._PUZZLE_ACTION_MAP[puzzle_action]
748        current_frame = self._emulator.get_current_frame()
749        frames, done = self._emulator.step(action)
750        action_success = 0 if frame_changed(current_frame, frames[-1]) else -1
751        return [self._state_tracker.report()], action_success
752
753    @staticmethod
754    def get_action_name(puzzle_action: str) -> str:
755        return f"Puzzle {puzzle_action}"

Allows navigation and option selection within Deja Vu puzzle/deduction states.

Is Valid When:

  • In Puzzle State

Action Success Interpretation:

  • -1: Frame did not change.
  • 0: Frame changed.

The state parser that parses the minimal state information required for the action to function.

The state tracker that tracks the minimal state information required for the action to function.

def is_valid(self, **kwargs):
725    def is_valid(self, **kwargs):
726        puzzle_action = kwargs.get("puzzle_action", None)
727        if puzzle_action is not None and puzzle_action not in self._PUZZLE_ACTION_KEYS:
728            return False
729        state = self._state_tracker.get_episode_metric(("dejavu_core", "agent_state"))
730        return state == AgentState.IN_PUZZLE

Checks if the high level action can be performed in the current state. If kwargs is empty, then must check whether there exists any valid way to perform the action.

Arguments:
  • **kwargs: Additional arguments required for the specific high level action.
Returns:

bool: Whether the action is valid in the current state.

def get_action_space(self):
732    def get_action_space(self):
733        return Discrete(len(self._PUZZLE_ACTION_MAP))

Returns the Gym defined Space that characterizes the high level action's parameter space.

You can use this API to get a Space for sampling high level actions of this type.

Returns:

Space: The Gym space that characterizes the high level action's parameter space.

def parameters_to_space(self, puzzle_action):
735    def parameters_to_space(self, puzzle_action):
736        if puzzle_action not in self._PUZZLE_ACTION_KEYS:
737            return None
738        return self._PUZZLE_ACTION_KEYS.index(puzzle_action)

Converts high level action parameters into a Gym space action. If the provided parameters are invalid, return None.

Parameters
  • kwargs: The high level action's parameters.
Returns

The action in the high level action's parameter space.

def space_to_parameters(self, space_action):
740    def space_to_parameters(self, space_action):
741        if space_action < 0 or space_action >= len(self._PUZZLE_ACTION_MAP):
742            return None
743        puzzle_action = self._PUZZLE_ACTION_KEYS[space_action]
744        return {"puzzle_action": puzzle_action}

Converts a Gym space action into high level action parameters. If the provided space action is invalid, return None.

Parameters
  • space_action: The action in the high level action's parameter space.
Returns

The high level action's parameters corresponding to the space action.

@staticmethod
def get_action_name(puzzle_action: str) -> str:
753    @staticmethod
754    def get_action_name(puzzle_action: str) -> str:
755        return f"Puzzle {puzzle_action}"

Returns a human readable name for the high level action with the given parameters.

Parameters
  • kwargs: The high level action's parameters.
Returns

A human readable name for the high level action.