Engine Library (pyAIAgent)

This library contains the core "engine" that powers the Lass agent. It handles low-level interaction with the emulator, ROM data parsing, and LLM communication.

Game State & RAM

pyAIAgent.game.state

Functions

get_active_battle_pokemon(sock)

Get current active player Pokemon in battle.

Parameters:

Name Type Description Default
sock Any

Socket connection to mGBA

required
Source code in pyAIAgent/game/state.py
def get_active_battle_pokemon(sock: Any) -> Optional[Dict[str, Any]]:
    """
    Get current active player Pokemon in battle.

    Args:
        sock: Socket connection to mGBA
    """
    _flush_socket(sock)
    try:
        battle_flag = readrange(sock, "0xD057", "1")[0]
        if battle_flag == 0:
            return None

        party_idx = readrange(sock, "0xD014", "1")[0]
        hp_data = readrange(sock, "0xD015", "2")  # Current HP in battle
        level = readrange(sock, "0xD022", "1")[0]  # Current level in battle
        moves_data = readrange(sock, "0xD01C", "4")

        current_hp = struct.unpack(">H", hp_data)[0]

        # Get max HP from party data to calculating percentage
        # Party struct is 44 bytes, starts at 0xD163 + 8
        # Max HP is at offset 0x22 (34)
        party_start = 0xD163 + 0x08 + (party_idx * 44)
        max_hp_addr = party_start + 0x22
        max_hp_data = readrange(sock, hex(max_hp_addr), "2")
        max_hp = struct.unpack(">H", max_hp_data)[0]

        move_map = get_move_map()
        moves = []
        for mid in moves_data:
            if mid != 0:
                moves.append(move_map.get(mid, f"Move(0x{mid:02X})"))

        return {
            "index": party_idx,
            "level": level,
            "hp": current_hp,
            "max_hp": max_hp,
            "moves": moves,
        }
    except Exception as e:
        log.warning(f"Error reading active battle pokemon: {e}")
        return None

get_battle_state(sock)

Read battle-related memory.

Returns:

Type Description
dict

dict with:

dict
  • in_battle: True if in a battle
dict
  • battle_type: Type of battle (wild, trainer, gym, etc.)
dict
  • turn_count: Number of turns in current battle
dict
  • move_menu_type: 0 = regular, 1 = mimic, other = text boxes
Source code in pyAIAgent/game/state.py
def get_battle_state(sock) -> dict:
    """
    Read battle-related memory.

    Returns:
        dict with:
        - in_battle: True if in a battle
        - battle_type: Type of battle (wild, trainer, gym, etc.)
        - turn_count: Number of turns in current battle
        - move_menu_type: 0 = regular, 1 = mimic, other = text boxes
    """
    _flush_socket(sock)
    try:
        battle_flag = readrange(sock, "0xD057", "1")[0]
        battle_type = readrange(sock, "0xD05A", "1")[0] if battle_flag else 0
        turn_count = readrange(sock, "0xCCD5", "1")[0] if battle_flag else 0
        move_menu = readrange(sock, "0xCCDB", "1")[0] if battle_flag else 0

        # Decode battle type
        battle_types = {
            0xF0: "wild",
            0xED: "trainer",
            0xEA: "gym_leader",
            0xF3: "final",
            0xF6: "defeated_trainer",
            0xF9: "defeated_wild",
            0xFC: "defeated_champion",
        }

        return {
            "in_battle": battle_flag != 0,
            "battle_type": battle_types.get(battle_type, f"unknown_{battle_type:02X}"),
            "turn_count": turn_count,
            "move_menu_type": move_menu,
        }
    except Exception as e:
        log.warning(f"Error reading battle state: {e}")
        return {
            "in_battle": False,
            "battle_type": None,
            "turn_count": 0,
            "move_menu_type": 0,
        }

get_battle_status_flags(sock)

Read detailed battle status flags (Phase 3). Player: 0xD062-D064 (3 bytes) (Wait, map says D062..D064) Enemy: 0xD067-D069 (3 bytes)

Status bits (approximate): Byte 0: Sleep (0-2), Poison (3), Burn (4), Freeze (5), Paralyze (6) Byte 1-2: Volatile statuses (Confusion, Leech Seed, etc)

Source code in pyAIAgent/game/state.py
def get_battle_status_flags(sock) -> dict:
    """
    Read detailed battle status flags (Phase 3).
    Player: 0xD062-D064 (3 bytes) (Wait, map says D062..D064)
    Enemy: 0xD067-D069 (3 bytes)

    Status bits (approximate):
    Byte 0: Sleep (0-2), Poison (3), Burn (4), Freeze (5), Paralyze (6)
    Byte 1-2: Volatile statuses (Confusion, Leech Seed, etc)
    """
    _flush_socket(sock)
    try:
        # Check battle flag first
        if readrange(sock, "0xD057", "1")[0] == 0:
            return None

        p_status = readrange(sock, "0xD062", "3")
        e_status = readrange(
            sock, "0xD067", "3"
        )  # Assuming +5 offset for enemy? Need to verify distance
        # Standard Red RAM: Enemy is usually after Player data.
        # Player Battle Status: D062-D064?
        # Actually standard main status (SLP/PSN/etc) is D16F for active pokemon?
        # D062 is "Player Pokemon Status" in battle RAM usually.

        def parse_main_status(byte_val):
            if byte_val == 0:
                return "OK"
            statuses = []
            if byte_val & 0x07:
                statuses.append(f"Sleep({byte_val & 0x07})")
            if byte_val & 0x08:
                statuses.append("Poison")
            if byte_val & 0x10:
                statuses.append("Burn")
            if byte_val & 0x20:
                statuses.append("Freeze")
            if byte_val & 0x40:
                statuses.append("Paralyze")
            return ", ".join(statuses)

        return {
            "player_status_raw": [f"{b:02X}" for b in p_status],
            "enemy_status_raw": [f"{b:02X}" for b in e_status],
            "player_main_status": parse_main_status(p_status[0]),
            "enemy_main_status": parse_main_status(e_status[0]),
        }
    except Exception as e:
        log.warning(f"Error reading battle status: {e}")
        return None

get_dialog_text(sock)

Read the tile screen buffer to extract dialog text.

The dialog box in Pokemon Red occupies the bottom 4 rows of the screen. Screen buffer is at 0xC3A0-C507 (360 bytes for 20x18 tiles).

Returns the decoded dialog text, or None if no dialog detected.

Source code in pyAIAgent/game/state.py
def get_dialog_text(sock) -> str | None:
    """
    Read the tile screen buffer to extract dialog text.

    The dialog box in Pokemon Red occupies the bottom 4 rows of the screen.
    Screen buffer is at 0xC3A0-C507 (360 bytes for 20x18 tiles).

    Returns the decoded dialog text, or None if no dialog detected.
    """
    _flush_socket(sock)
    try:
        # Read the tile buffer (360 bytes = 20 columns x 18 rows)
        tiles = readrange(sock, "0xC3A0", "360")

        # Dialog box is typically in the bottom 4 rows (rows 14-17)
        # Each row is 20 tiles
        dialog_rows = []
        for row in range(14, 18):  # Bottom 4 rows
            start = row * 20
            end = start + 20
            row_tiles = tiles[start:end]
            row_text = decode_pokemon_text(bytes(row_tiles))
            if row_text.strip():
                dialog_rows.append(row_text.strip())

        if dialog_rows:
            return " ".join(dialog_rows)
        return None
    except Exception as e:
        log.warning(f"Error reading dialog text: {e}")
        return None

get_enemy_pokemon(sock)

Get current enemy Pokemon in battle.

Source code in pyAIAgent/game/state.py
def get_enemy_pokemon(sock) -> dict | None:
    """
    Get current enemy Pokemon in battle.
    """
    _flush_socket(sock)
    try:
        # Check if in battle first
        battle_flag = readrange(sock, "0xD057", "1")[0]
        if battle_flag == 0:
            return None

        species_id = readrange(sock, "0xCFE5", "1")[0]
        hp_data = readrange(sock, "0xCFE6", "2")
        level = readrange(sock, "0xCFE8", "1")[0]
        # Moves: 4 bytes at 0xCFED
        moves_data = readrange(sock, "0xCFED", "4")

        hp = struct.unpack(">H", hp_data)[0]

        species_map = get_species_map()
        move_map = get_move_map()

        dex, name, type1, type2 = species_map.get(
            species_id, (0, "Unknown", "Normal", None)
        )

        moves = []
        for mid in moves_data:
            if mid != 0:
                moves.append(move_map.get(mid, f"Move(0x{mid:02X})"))

        return {
            "name": name,
            "level": level,
            "hp": hp,
            "types": [t for t in [type1, type2] if t],
            "moves": moves,
            "species_id": species_id,
        }
    except Exception as e:
        log.warning(f"Error reading enemy pokemon: {e}")
        return None

get_event_flags(sock)

Read key story event flags.

Source code in pyAIAgent/game/state.py
def get_event_flags(sock) -> dict:
    """
    Read key story event flags.
    """
    _flush_socket(sock)
    flags = {}
    try:
        # Single byte reads for simplicity
        # SS Anne: 0xD803 (bit 0?) - checking non-zero for now
        # Actually D803 bit 0 is SS Anne Here
        ss_anne = readrange(sock, "0xD803", "1")[0]
        flags["ss_anne_here"] = (ss_anne & 1) != 0

        town_map = readrange(sock, "0xD5F3", "1")[0]
        flags["have_town_map"] = (town_map & 1) != 0  # Often bit 0 or non-zero

        oaks_parcel = readrange(sock, "0xD60D", "1")[0]
        flags["have_oaks_parcel"] = (
            oaks_parcel & 2
        ) != 0  # Usually bit 1 for "delivered"? Checking non-zero.
        # Actually docs say: D60D - Have Oak's Parcel? (Bit 1?) - let's assume boolean byte for now or check map

        lapras = readrange(sock, "0xD72E", "1")[0]
        flags["got_lapras"] = (lapras & 1) != 0

        # Flash ability (HM05) - usually check badge or HM in bag, but map palette 0xD35D can indicate flash usage

    except Exception as e:
        log.warning(f"Error reading event flags: {e}")

    return flags

get_inventory(sock)

Read player inventory. 0xD31D: Item count 0xD31E + (i2): Item ID 0xD31E + (i2) + 1: Quantity

Source code in pyAIAgent/game/state.py
def get_inventory(sock) -> list[dict]:
    """
    Read player inventory.
    0xD31D: Item count
    0xD31E + (i*2): Item ID
    0xD31E + (i*2) + 1: Quantity
    """
    _flush_socket(sock)
    inventory = []
    try:
        item_map = get_item_map()
        count_data = readrange(sock, "0xD31D", "1")
        count = count_data[0]

        # Limit count to avoid reading garbage if memory value is weird
        count = min(count, 20)

        if count > 0:
            # Read item list (count * 2 bytes)
            # 0xD31E is start of item list
            item_data = readrange(sock, "0xD31E", str(count * 2))

            for i in range(count):
                item_id = item_data[i * 2]
                quantity = item_data[i * 2 + 1]
                item_name = item_map.get(item_id, f"Unknown(0x{item_id:02X})")
                inventory.append(
                    {"item_id": item_id, "name": item_name, "count": quantity}
                )
    except Exception as e:
        log.warning(f"Error reading inventory: {e}")

    return inventory

get_map_state(sock)

Read map-specific state (Phase 2). 0xD365: Last Map ID (for warp handling) 0xD887: Wild Encounter Rate

Source code in pyAIAgent/game/state.py
def get_map_state(sock) -> dict:
    """
    Read map-specific state (Phase 2).
    0xD365: Last Map ID (for warp handling)
    0xD887: Wild Encounter Rate
    """
    _flush_socket(sock)
    try:
        last_map = readrange(sock, "0xD365", "1")[0]
        encounter_rate = readrange(sock, "0xD887", "1")[0]

        return {"last_map_id": last_map, "encounter_rate": encounter_rate}
    except Exception as e:
        log.warning(f"Error reading map state: {e}")
        return {"last_map_id": 0, "encounter_rate": 0}

get_menu_state(sock)

Read extended menu-related memory for tracking menu interactions. OPTIMIZED: Uses batched reads to avoid socket timeouts.

Returns:

Type Description
dict

dict with:

dict
  • selected_item: Currently selected menu item (0 = topmost)
dict
  • last_selected: Previously selected item
dict
  • menu_item_count: Total items in current menu
dict
  • cursor_x: X screen position of cursor (CC25)
dict
  • cursor_y: Y screen position of cursor (CC24)
dict
  • party_cursor: Last cursor position on party/Bill's PC screen (CC2B)
dict
  • item_cursor: Last cursor position on item screen (CC2C)
dict
  • start_cursor: Last cursor position on START/battle menu (CC2D)
dict
  • first_displayed: ID of first displayed menu item for scrolling (CC36)
dict
  • select_highlight: Item highlighted with Select button (CC35)
Source code in pyAIAgent/game/state.py
def get_menu_state(sock) -> dict:
    """
    Read extended menu-related memory for tracking menu interactions.
    OPTIMIZED: Uses batched reads to avoid socket timeouts.

    Returns:
        dict with:
        - selected_item: Currently selected menu item (0 = topmost)
        - last_selected: Previously selected item
        - menu_item_count: Total items in current menu
        - cursor_x: X screen position of cursor (CC25)
        - cursor_y: Y screen position of cursor (CC24)
        - party_cursor: Last cursor position on party/Bill's PC screen (CC2B)
        - item_cursor: Last cursor position on item screen (CC2C)
        - start_cursor: Last cursor position on START/battle menu (CC2D)
        - first_displayed: ID of first displayed menu item for scrolling (CC36)
        - select_highlight: Item highlighted with Select button (CC35)
    """
    _flush_socket(sock)
    try:
        # BATCHED READ: CC24-CC2D (10 consecutive bytes)
        # CC24=cursor_y, CC25=cursor_x, CC26=selected, CC27=?, CC28=last_item
        # CC29=?, CC2A=prev_selected, CC2B=party, CC2C=item, CC2D=start
        menu_bytes = readrange(sock, "0xCC24", "10")

        cursor_y = menu_bytes[0] if len(menu_bytes) > 0 else 0
        cursor_x = menu_bytes[1] if len(menu_bytes) > 1 else 0
        selected = menu_bytes[2] if len(menu_bytes) > 2 else 0
        last_item = menu_bytes[4] if len(menu_bytes) > 4 else 0  # CC28 = offset 4
        prev_selected = menu_bytes[6] if len(menu_bytes) > 6 else 0  # CC2A = offset 6
        party_cursor = menu_bytes[7] if len(menu_bytes) > 7 else 0  # CC2B = offset 7
        item_cursor = menu_bytes[8] if len(menu_bytes) > 8 else 0  # CC2C = offset 8
        start_cursor = menu_bytes[9] if len(menu_bytes) > 9 else 0  # CC2D = offset 9

        # SECOND READ: CC35-CC36 (2 bytes)
        scroll_bytes = readrange(sock, "0xCC35", "2")
        select_highlight = scroll_bytes[0] if len(scroll_bytes) > 0 else 0
        first_displayed = scroll_bytes[1] if len(scroll_bytes) > 1 else 0

        return {
            "selected_item": selected,
            "menu_item_count": last_item + 1,  # Last item ID + 1 = count
            "last_selected": prev_selected,
            "cursor_x": cursor_x,
            "cursor_y": cursor_y,
            "party_cursor": party_cursor,
            "item_cursor": item_cursor,
            "start_cursor": start_cursor,
            "first_displayed": first_displayed,
            "select_highlight": select_highlight,
        }
    except Exception as e:
        log.warning(f"Error reading menu state: {e}")
        return {
            "selected_item": 0,
            "menu_item_count": 0,
            "last_selected": 0,
            "cursor_x": 0,
            "cursor_y": 0,
            "party_cursor": 0,
            "item_cursor": 0,
            "start_cursor": 0,
            "first_displayed": 0,
            "select_highlight": 0,
        }

get_money(sock)

Read player money (BCD encoded, 3 bytes at 0xD347).

Source code in pyAIAgent/game/state.py
def get_money(sock) -> int:
    """
    Read player money (BCD encoded, 3 bytes at 0xD347).
    """
    _flush_socket(sock)
    try:
        data = readrange(sock, "0xD347", "3")
        # Money is stored as BCD (Binary Coded Decimal)
        # e.g., 0x12 0x34 0x56 = 123456
        money_str = "".join(f"{b:02X}" for b in data)
        return int(money_str)
    except Exception as e:
        log.warning(f"Error reading money: {e}")
        return 0

get_name_entry_state(sock, menu_state=None)

Detect if player is on name entry screen and get cursor position. OPTIMIZED: Reuses menu_state data if provided, skips expensive tile buffer read.

The name entry screen displays a character grid where the player selects letters. Detection is done by checking menu item count patterns.

Parameters:

Name Type Description Default
sock Any

Socket connection to mGBA

required
menu_state Optional[Dict[str, Any]]

Optional pre-read menu state to avoid redundant reads

None

Returns:

Type Description
Optional[Dict[str, Any]]

dict with cursor info if on name entry, None otherwise

Source code in pyAIAgent/game/state.py
def get_name_entry_state(
    sock: Any, menu_state: Optional[Dict[str, Any]] = None
) -> Optional[Dict[str, Any]]:
    """
    Detect if player is on name entry screen and get cursor position.
    OPTIMIZED: Reuses menu_state data if provided, skips expensive tile buffer read.

    The name entry screen displays a character grid where the player selects letters.
    Detection is done by checking menu item count patterns.

    Args:
        sock: Socket connection to mGBA
        menu_state: Optional pre-read menu state to avoid redundant reads

    Returns:
        dict with cursor info if on name entry, None otherwise
    """
    try:
        # If menu_state provided, use it; otherwise do a minimal read
        if menu_state:
            last_item = menu_state.get("menu_item_count", 0) - 1  # Undo the +1
            cursor_y = menu_state.get("cursor_y", 0)
            cursor_x = menu_state.get("cursor_x", 0)
            selected = menu_state.get("selected_item", 0)
            log.info(
                f"name_entry check: menu_item_count={last_item + 1}, cursor=({cursor_x},{cursor_y}), selected={selected}"
            )
        else:
            _flush_socket(sock)
            # BATCHED: Read CC24-CC28 (5 bytes) for cursor and menu count
            # AND Read CC30-CC31 (2 bytes) for cursor pointer
            # Total read: CC24-CC31 (14 bytes range, but let's just read two chunks or one bigger chunk)
            # CC24 is start. CC31 is end. Length = 31 - 24 + 1 = 8 + 6 = 14 bytes?
            # 0xCC31 - 0xCC24 = 13. +1 = 14 bytes.

            raw_data = readrange(sock, "0xCC24", "14")
            if raw_data and len(raw_data) >= 14:
                menu_bytes = raw_data[0:5]  # CC24-CC28
                # CC29, CC2A, CC2B, CC2C, CC2D, CC2E, CC2F, CC30, CC31
                # CC30 is at offset 12 (0xC = 12)
                cursor_ptr_bytes = raw_data[12:14]

                cursor_y = menu_bytes[0]
                cursor_x = menu_bytes[1]
                selected = menu_bytes[2]
                last_item = menu_bytes[4]

                cursor_ptr = struct.unpack("<H", cursor_ptr_bytes)[0]

                # Check if pointer is valid screen coordinate (C3A0 - C507)
                if 0xC3A0 <= cursor_ptr <= 0xC507:
                    offset = cursor_ptr - 0xC3A0
                    calc_y = offset // 20
                    calc_x = offset % 20
                    # If on keyboard (last_item=7), trust calculated values
                    if last_item == 7:
                        log.info(
                            f"Cursor override: Mem({cursor_x},{cursor_y}) -> Calc({calc_x},{calc_y}) from Ptr {hex(cursor_ptr)}"
                        )
                        cursor_x = calc_x
                        cursor_y = calc_y
            else:
                # Fallback to safe defaults if read failed
                cursor_y = 0
                cursor_x = 0
                selected = 0
                last_item = 0

            log.info(
                f"name_entry check (direct read): last_item={last_item}, cursor=({cursor_x},{cursor_y}), selected={selected}"
            )

        # Name entry detection heuristics:
        # Preset menu: menu_item_count is 4 or 5 (NAME header + NEW NAME + presets)
        # Keyboard: menu_item_count is exactly 7 (representing keyboard rows)

        # Check for PRESET MENU first (4-5 items)
        # Relaxed cursor_y check to catch menus positioned lower on screen
        # last_item=3 means 4 items (IDs 0-3), last_item=4 means 5 items (IDs 0-4)
        is_preset_menu = (3 <= last_item <= 5) and cursor_y < 12

        if is_preset_menu:
            # ... (Preset logic remains same) ...
            preset_options_player = ["NEW NAME", "RED", "ASH", "JACK"]
            preset_options_rival = ["NEW NAME", "BLUE", "GARY", "JOHN"]

            # Try to determine which type by checking dialog_text if available
            # For now, use player options as default
            preset_options = preset_options_player
            selected_option = (
                preset_options[selected]
                if selected < len(preset_options)
                else "UNKNOWN"
            )

            log.info(
                f"name_entry: PRESET MENU detected! selected={selected} ({selected_option}), menu_items={last_item + 1}"
            )

            return {
                "is_preset_menu": True,
                "is_keyboard": False,
                "cursor_option": selected_option,
                "cursor_index": selected,
                "menu_item_count": last_item + 1,
                "cursor_x": cursor_x,
                "cursor_y": cursor_y,
            }

        # Check for KEYBOARD (menu_item_count == 7, cursor in keyboard range)
        is_keyboard_likely = last_item == 7 and 1 <= cursor_x <= 19 and cursor_y >= 3

        if not is_keyboard_likely:
            log.info(
                f"name_entry: NOT detected (last_item={last_item}, cursor=({cursor_x},{cursor_y}))"
            )
            return None

        # ADDITIONAL CHECK: Read tile buffer to verify keyboard letters exist
        # Also detect Screen Type (Player vs Rival vs Pokemon)
        name_type = "unknown"
        try:
            _flush_socket(sock)
            # Read Row 1 (Title) and Row 6 (Keyboard start)
            # Row 1: C3A0 + 20 = C3B4 (len 20)
            # Row 6: C3A0 + 120 = C418 (len 20)

            # Read enough to cover both? C3B4 to C42C.
            # Delta = 120 - 20 + 20 = 120 bytes.
            buffer_data = readrange(sock, "0xC3B4", "120")

            # Row 1 (Title) is at offset 0 of this buffer
            row1 = buffer_data[0:20]

            # Check Signatures
            # RIVAL: 91 88 95 80 8B (R I V A L)
            # YOUR NAME: 98 8E 94 91 (Y O U R)
            # NICKNAME: 8D 88 82 8A (N I C K)

            # Helper to check sequence
            def check_seq(seq, data):
                for i in range(len(data) - len(seq) + 1):
                    if list(data[i : i + len(seq)]) == seq:
                        return True
                return False

            if check_seq([0x91, 0x88, 0x95, 0x80, 0x8B], row1):
                name_type = "rival"
            elif check_seq([0x98, 0x8E, 0x94, 0x91], row1):
                name_type = "player"
            elif check_seq([0x8D, 0x88, 0x82, 0x8A], row1):
                name_type = "pokemon"

            log.info(f"name_entry: Screen Type Detection -> {name_type}")

            # Keyboard check (Row 6) is at offset 100 in our buffer (120 - 20 = 100)
            # But wait, 0xC3B4 is Row 1 start.
            # Row 6 start is 0xC3A0 + 120 = 0xC418.
            # 0xC418 - 0xC3B4 = 100.
            # So offset 100 is start of Row 6.
            keyboard_area = buffer_data[100:120]

            # In Pokemon Red, uppercase letters A-Z are tiles 0x80-0x99
            # 'A' = 0x80, 'B' = 0x81, ... 'I' = 0x88
            # The keyboard row 1 should have 'A' at position 3 (column 2)
            has_keyboard_tiles = False
            if len(keyboard_area) >= 10:
                # Check for 'A' (0x80) and 'B' (0x81) tiles in the first half of the row
                for i in range(10):
                    tile = keyboard_area[i]
                    if 0x80 <= tile <= 0x88:  # A-I tiles
                        has_keyboard_tiles = True
                        break

            if not has_keyboard_tiles:
                log.warning(
                    f"name_entry: Keyboard tiles NOT found in buffer (items={last_item + 1}). Aborting detection to avoid false positive."
                )
                return None
        except Exception as e:
            log.warning(
                f"name_entry: Tile buffer check failed: {e} - proceeding with detection"
            )

        # KEYBOARD MODE DETECTED
        # Instead of trying to read unreliable memory addresses, we use KeyboardTracker
        # which simulates cursor position based on action history
        # BUT NOW we have reliable cursor tracking from memory!

        # We should still use KeyboardTracker for "blind typing" (pathfinding),
        # but we can UPDATE it with true position.

        from pyAIAgent.game.keyboard_tracker import get_keyboard_tracker

        kb_tracker = get_keyboard_tracker()

        # Sync tracker with truth
        # Note: cursor_x is visual column (1, 3, 5...).
        # Tracker uses logical column (0, 1, 2...).
        # Map visual X to logical col?
        # Visual: 1->0, 3->1, 5->2.
        # Formula: (x - 1) // 2

        logical_col = max(0, (cursor_x - 1) // 2)
        logical_row = max(0, cursor_y - 4)  # Assuming Row 0 starts at Y=4?
        # Wait, Cursor Y=3 for Row 0?
        # 0xCC24 said 3.
        # Screen dump said Row 03: 7F ...
        # Row 04: A B C ...
        # So Row 0 is at visual Y=4.
        # If cursor is on Row 0, Y should be 4.
        # But my memory dump said Y=3 for A?
        # User log: Cursor Y: 3.
        # Screen dump: Row 04 starts A.
        # Maybe Y=3 is the "top" of the selection box?
        # Let's trust cursor_y - 3 for now if it aligns.
        # If Y=3 -> Row 0.
        logical_row = max(0, cursor_y - 3)  # Adjusted based on observation

        # We only sync if we are SURE.
        if name_type != "unknown":
            kb_tracker.sync(logical_row, logical_col)

        # Get state from keyboard tracker (which tracks based on actions)
        tracker_state = kb_tracker.get_state_dict()

        log.info(
            f"📝 Name entry KEYBOARD detected ({name_type}): Using tracker state: row={tracker_state.get('row', 0)}, col={tracker_state.get('col', 0)}, char='{tracker_state.get('selected_char', '?')}'"
        )

        # Return keyboard state with tracker data
        return {
            "is_preset_menu": False,
            "is_keyboard": True,
            "name_type": name_type,  # NEW FIELD
            "cursor_index": tracker_state.get("cursor_index", 0),
            "cursor_x": cursor_x,  # Keep original for debugging
            "cursor_y": cursor_y,  # Keep original for debugging
            "tracked_row": tracker_state.get("tracked_row", 0),  # 0-indexed
            "tracked_col": tracker_state.get("tracked_col", 0),  # 0-indexed
            "selected_char": tracker_state.get("selected_char", "A"),
            "row": tracker_state.get("row", 1),  # 1-indexed for human readability
            "col": tracker_state.get("col", 1),  # 1-indexed for human readability
            "is_tracked": tracker_state.get("is_tracked", True),
            "is_name_entry": True,
        }
    except Exception as e:
        log.warning(f"Error reading name entry state: {e}")
        return None

get_player_movement_state(sock)

Read player movement state for biking/surfing detection.

RAM Addresses: - D700: Bike speed (0 = not biking, >0 = biking) - C100-C10F: Player sprite data (slot 0, 16 bytes) - C101: Movement status (0=uninit, 1=ready, 2=delayed, 3=moving) - C102: Sprite image index (includes facing direction and animation) - C109: Facing direction (0=down, 4=up, 8=left, C=right)

Surfing detection: When surfing, the player's sprite image changes. The sprite image index at C102 will have distinctive values for surf sprite.

Returns:

Type Description
dict

dict with:

dict
  • bike_speed: D700 value (non-zero = biking)
dict
  • movement_status: C101 (0=uninit, 1=ready, 2=delayed, 3=moving)
dict
  • sprite_image_idx: C102 (sprite image with facing/animation)
dict
  • is_biking: Derived boolean from bike_speed
dict
  • is_surfing: Derived from sprite analysis
dict
  • movement_mode: "walking", "biking", or "surfing"
Source code in pyAIAgent/game/state.py
def get_player_movement_state(sock) -> dict:
    """
    Read player movement state for biking/surfing detection.

    RAM Addresses:
    - D700: Bike speed (0 = not biking, >0 = biking)
    - C100-C10F: Player sprite data (slot 0, 16 bytes)
      - C101: Movement status (0=uninit, 1=ready, 2=delayed, 3=moving)
      - C102: Sprite image index (includes facing direction and animation)
      - C109: Facing direction (0=down, 4=up, 8=left, C=right)

    Surfing detection: When surfing, the player's sprite image changes.
    The sprite image index at C102 will have distinctive values for surf sprite.

    Returns:
        dict with:
        - bike_speed: D700 value (non-zero = biking)
        - movement_status: C101 (0=uninit, 1=ready, 2=delayed, 3=moving)
        - sprite_image_idx: C102 (sprite image with facing/animation)
        - is_biking: Derived boolean from bike_speed
        - is_surfing: Derived from sprite analysis
        - movement_mode: "walking", "biking", or "surfing"
    """
    _flush_socket(sock)
    try:
        # Bike speed flag
        bike_speed = readrange(sock, "0xD700", "1")[0]

        # Player sprite data (sprite slot 0)
        player_sprite = readrange(sock, "0xC100", "16")

        picture_id = player_sprite[0] if len(player_sprite) > 0 else 0
        movement_status = player_sprite[1] if len(player_sprite) > 1 else 0
        sprite_image_idx = player_sprite[2] if len(player_sprite) > 2 else 0

        # Derive biking state
        is_biking = bike_speed != 0

        # Surfing detection: The player's picture ID changes when surfing
        # Normal player sprite IDs are 1-4 (depending on gender/version)
        # Surfing uses a different sprite set - typically picture ID stays same
        # but we can check the tile the player is on for water tiles
        # For now, we'll use a heuristic based on sprite image patterns
        # When surfing, certain sprite image indices are used
        # This may need refinement based on testing
        is_surfing = False

        # Determine movement mode
        if is_surfing:
            movement_mode = "surfing"
        elif is_biking:
            movement_mode = "biking"
        else:
            movement_mode = "walking"

        return {
            "bike_speed": bike_speed,
            "movement_status": movement_status,
            "sprite_image_idx": sprite_image_idx,
            "picture_id": picture_id,
            "is_biking": is_biking,
            "is_surfing": is_surfing,
            "movement_mode": movement_mode,
        }
    except Exception as e:
        log.warning(f"Error reading player movement state: {e}")
        return {
            "bike_speed": 0,
            "movement_status": 0,
            "sprite_image_idx": 0,
            "picture_id": 0,
            "is_biking": False,
            "is_surfing": False,
            "movement_mode": "walking",
        }

get_rom_path()

Get ROM path from environment variable or default, relative to roms folder

Source code in pyAIAgent/game/state.py
def get_rom_path():
    """Get ROM path from environment variable or default, relative to roms folder"""
    rom_name = os.getenv("POKEMON_ROM", DEFAULT_ROM)
    # If ROM path doesn't include a directory, assume it's in roms folder
    if os.path.sep not in rom_name and not os.path.isabs(rom_name):
        return os.path.join("roms", rom_name)
    return rom_name

get_sprites(sock)

Read Sprite Table from RAM (0xC100-0xC1FF) to get NPC coordinates. There are 16 slots, each 16 bytes. Offset +0: Picture ID (0 if inactive/hidden) Offset +4: Y Coordinate (Grid Y + 4) Offset +6: X Coordinate (Grid X + 4)

Source code in pyAIAgent/game/state.py
def get_sprites(sock) -> list[tuple[int, int]]:
    """
    Read Sprite Table from RAM (0xC100-0xC1FF) to get NPC coordinates.
    There are 16 slots, each 16 bytes.
    Offset +0: Picture ID (0 if inactive/hidden)
    Offset +4: Y Coordinate (Grid Y + 4)
    Offset +6: X Coordinate (Grid X + 4)
    """
    _flush_socket(sock)
    sprites = []
    try:
        # Read all 16 slots * 16 bytes = 256 bytes
        # Using 0xC100 (SpriteStateData1) which contains positions
        data = readrange(sock, "0xC100", "256")

        for i in range(16):
            offset = i * 16
            if offset + 6 >= len(data):
                break

            picture_id = data[offset]
            # Offset 0 is picture ID. If 0, sprite is disabled/hidden.
            if picture_id != 0:
                # Valid sprite
                y_raw = data[offset + 4]
                x_raw = data[offset + 6]  # X is at +6, not +5

                # Convert to 0-indexed grid coordinates
                # RAM stores value as Grid + 4
                idx_x = x_raw - 4
                idx_y = y_raw - 4

                # Simple sanity check for map bounds (0-30 roughly)
                if 0 <= idx_x < 80 and 0 <= idx_y < 80:
                    sprites.append((idx_x, idx_y))
                    print(
                        f"DEBUG Sprite {i}: ID={picture_id} Raw=({x_raw},{y_raw}) -> ({idx_x},{idx_y})",
                        file=sys.stderr,
                    )

        # Debug log to verify we are getting data
        if sprites:
            print(f"DEBUG: Found {len(sprites)} sprites at: {sprites}", file=sys.stderr)

    except Exception as e:
        log.warning(f"Error reading sprite data: {e}. continuing without sprites.")

    return sprites

get_stat_modifiers(sock)

Read battle stat modifiers (Phase 3). Player: 0xCD1A-CD20 (7 bytes? Atk, Def, Spd, Spc, Acc, Eva?) Enemy: 0xCD2E-CD34

Values: 7 = Neutral. >7 = Buffed. <7 = Debuffed.

Source code in pyAIAgent/game/state.py
def get_stat_modifiers(sock) -> dict:
    """
    Read battle stat modifiers (Phase 3).
    Player: 0xCD1A-CD20 (7 bytes? Atk, Def, Spd, Spc, Acc, Eva?)
    Enemy: 0xCD2E-CD34

    Values: 7 = Neutral. >7 = Buffed. <7 = Debuffed.
    """
    _flush_socket(sock)
    try:
        # Check battle flag first
        if readrange(sock, "0xD057", "1")[0] == 0:
            return None

        # Reading 6 bytes: Atk, Def, Spd, Spc, Acc, Eva
        p_stats = readrange(sock, "0xCD1A", "6")
        e_stats = readrange(sock, "0xCD2E", "6")

        stat_names = ["Atk", "Def", "Spd", "Spc", "Acc", "Eva"]

        def parse_stats(data):
            mods = {}
            for i, val in enumerate(data):
                if i < len(stat_names):
                    # 7 is neutral (stage 0)
                    # 1-6 are debuffs (-6 to -1)
                    # 8-13 are buffs (+1 to +6)
                    stage = val - 7
                    if stage != 0:
                        mods[stat_names[i]] = stage
            return mods

        return {"player_mods": parse_stats(p_stats), "enemy_mods": parse_stats(e_stats)}
    except Exception as e:
        log.warning(f"Error reading stat modifiers: {e}")
        return None

get_text_state(sock)

Read text/dialog related memory flags.

Returns:

Type Description
dict

dict with:

dict
  • text_speed: Current text speed setting (0xD355)
dict
  • text_printing: Text printing flags (0xD358)
dict
  • is_printing: True if text is currently being printed
Source code in pyAIAgent/game/state.py
def get_text_state(sock) -> dict:
    """
    Read text/dialog related memory flags.

    Returns:
        dict with:
        - text_speed: Current text speed setting (0xD355)
        - text_printing: Text printing flags (0xD358)
        - is_printing: True if text is currently being printed
    """
    _flush_socket(sock)
    try:
        text_speed = readrange(sock, "0xD355", "1")[0]
        text_flags = readrange(sock, "0xD358", "1")[0]

        # Bit 0 = 0 means delay limited to 1 frame
        # Bit 1 = 0 means no delay (instant text)
        is_printing = (text_flags & 0x03) != 0x03  # Either bit cleared = text active

        return {
            "text_speed": text_speed,
            "text_flags": text_flags,
            "is_printing": is_printing,
        }
    except Exception as e:
        log.warning(f"Error reading text state: {e}")
        return {"text_speed": 0, "text_flags": 0, "is_printing": False}

ROM Data & Decoding

pyAIAgent.game.data

Classes

MapLocation

Bases: IntEnum

Maps location IDs to their names

Source code in pyAIAgent/game/data.py
class MapLocation(IntEnum):
    """Maps location IDs to their names"""

    PALLET_TOWN = 0x00
    VIRIDIAN_CITY = 0x01
    PEWTER_CITY = 0x02
    CERULEAN_CITY = 0x03
    LAVENDER_TOWN = 0x04
    VERMILION_CITY = 0x05
    CELADON_CITY = 0x06
    FUCHSIA_CITY = 0x07
    CINNABAR_ISLAND = 0x08
    INDIGO_PLATEAU = 0x09
    SAFFRON_CITY = 0x0A
    UNUSED_0B = 0x0B
    ROUTE_1 = 0x0C
    ROUTE_2 = 0x0D
    ROUTE_3 = 0x0E
    ROUTE_4 = 0x0F
    ROUTE_5 = 0x10
    ROUTE_6 = 0x11
    ROUTE_7 = 0x12
    ROUTE_8 = 0x13
    ROUTE_9 = 0x14
    ROUTE_10 = 0x15
    ROUTE_11 = 0x16
    ROUTE_12 = 0x17
    ROUTE_13 = 0x18
    ROUTE_14 = 0x19
    ROUTE_15 = 0x1A
    ROUTE_16 = 0x1B
    ROUTE_17 = 0x1C
    ROUTE_18 = 0x1D
    ROUTE_19 = 0x1E
    ROUTE_20 = 0x1F
    ROUTE_21 = 0x20
    ROUTE_22 = 0x21
    ROUTE_23 = 0x22
    ROUTE_24 = 0x23
    ROUTE_25 = 0x24
    PLAYERS_HOUSE_1F = 0x25
    PLAYERS_HOUSE_2F = 0x26
    RIVALS_HOUSE = 0x27
    OAKS_LAB = 0x28
    VIRIDIAN_POKECENTER = 0x29
    VIRIDIAN_MART = 0x2A
    VIRIDIAN_SCHOOL = 0x2B
    VIRIDIAN_HOUSE = 0x2C
    VIRIDIAN_GYM = 0x2D
    DIGLETTS_CAVE_ROUTE2 = 0x2E
    VIRIDIAN_FOREST_NORTH_GATE = 0x2F
    ROUTE_2_HOUSE = 0x30
    ROUTE_2_GATE = 0x31
    VIRIDIAN_FOREST_SOUTH_GATE = 0x32
    VIRIDIAN_FOREST = 0x33
    MUSEUM_1F = 0x34
    MUSEUM_2F = 0x35
    PEWTER_GYM = 0x36
    PEWTER_HOUSE_1 = 0x37
    PEWTER_MART = 0x38
    PEWTER_HOUSE_2 = 0x39
    PEWTER_POKECENTER = 0x3A
    MT_MOON_1F = 0x3B
    MT_MOON_B1F = 0x3C
    MT_MOON_B2F = 0x3D
    CERULEAN_TRASHED_HOUSE = 0x3E
    CERULEAN_TRADE_HOUSE = 0x3F
    CERULEAN_POKECENTER = 0x40
    CERULEAN_GYM = 0x41
    BIKE_SHOP = 0x42
    CERULEAN_MART = 0x43
    MT_MOON_POKECENTER = 0x44
    ROUTE_5_GATE = 0x46
    UNDERGROUND_PATH_ROUTE5 = 0x47
    DAYCARE = 0x48
    ROUTE_6_GATE = 0x49
    UNDERGROUND_PATH_ROUTE6 = 0x4A
    ROUTE_7_GATE = 0x4C
    UNDERGROUND_PATH_ROUTE7 = 0x4D
    ROUTE_8_GATE = 0x4F
    UNDERGROUND_PATH_ROUTE8 = 0x50
    ROCK_TUNNEL_POKECENTER = 0x51
    ROCK_TUNNEL_1F = 0x52
    POWER_PLANT = 0x53
    ROUTE_11_GATE_1F = 0x54
    DIGLETTS_CAVE_ROUTE11 = 0x55
    ROUTE_11_GATE_2F = 0x56
    ROUTE_12_GATE_1F = 0x57
    BILLS_HOUSE = 0x58
    VERMILION_POKECENTER = 0x59
    FAN_CLUB = 0x5A
    VERMILION_MART = 0x5B
    VERMILION_GYM = 0x5C
    VERMILION_HOUSE_1 = 0x5D
    VERMILION_DOCK = 0x5E
    SS_ANNE_1F = 0x5F
    SS_ANNE_2F = 0x60
    SS_ANNE_3F = 0x61
    SS_ANNE_B1F = 0x62
    SS_ANNE_BOW = 0x63
    SS_ANNE_KITCHEN = 0x64
    SS_ANNE_CAPTAINS_ROOM = 0x65
    SS_ANNE_1F_ROOMS = 0x66
    SS_ANNE_2F_ROOMS = 0x67
    SS_ANNE_B1F_ROOMS = 0x68
    VICTORY_ROAD_1F = 0x6C
    LANCE = 0x71
    HALL_OF_FAME = 0x76
    UNDERGROUND_PATH_NS = 0x77
    CHAMPIONS_ROOM = 0x78
    UNDERGROUND_PATH_WE = 0x79
    CELADON_MART_1F = 0x7A
    CELADON_MART_2F = 0x7B
    CELADON_MART_3F = 0x7C
    CELADON_MART_4F = 0x7D
    CELADON_MART_ROOF = 0x7E
    CELADON_MART_ELEVATOR = 0x7F
    CELADON_MANSION_1F = 0x80
    CELADON_MANSION_2F = 0x81
    CELADON_MANSION_3F = 0x82
    CELADON_MANSION_ROOF = 0x83
    CELADON_MANSION_ROOF_HOUSE = 0x84
    CELADON_POKECENTER = 0x85
    CELADON_GYM = 0x86
    GAME_CORNER = 0x87
    CELADON_MART_5F = 0x88
    GAME_CORNER_PRIZE_ROOM = 0x89
    CELADON_DINER = 0x8A
    CELADON_HOUSE = 0x8B
    CELADON_HOTEL = 0x8C
    LAVENDER_POKECENTER = 0x8D
    POKEMON_TOWER_1F = 0x8E
    POKEMON_TOWER_2F = 0x8F
    POKEMON_TOWER_3F = 0x90
    POKEMON_TOWER_4F = 0x91
    POKEMON_TOWER_5F = 0x92
    POKEMON_TOWER_6F = 0x93
    POKEMON_TOWER_7F = 0x94
    LAVENDER_HOUSE_1 = 0x95
    LAVENDER_MART = 0x96
    LAVENDER_HOUSE_2 = 0x97
    FUCHSIA_MART = 0x98
    FUCHSIA_HOUSE_1 = 0x99
    FUCHSIA_POKECENTER = 0x9A
    FUCHSIA_HOUSE_2 = 0x9B
    SAFARI_ZONE_ENTRANCE = 0x9C
    FUCHSIA_GYM = 0x9D
    FUCHSIA_MEETING_ROOM = 0x9E
    SEAFOAM_ISLANDS_B1F = 0x9F
    SEAFOAM_ISLANDS_B2F = 0xA0
    SEAFOAM_ISLANDS_B3F = 0xA1
    SEAFOAM_ISLANDS_B4F = 0xA2
    VERMILION_HOUSE_2 = 0xA3
    VERMILION_HOUSE_3 = 0xA4
    POKEMON_MANSION_1F = 0xA5
    CINNABAR_GYM = 0xA6
    CINNABAR_LAB_1 = 0xA7
    CINNABAR_LAB_2 = 0xA8
    CINNABAR_LAB_3 = 0xA9
    CINNABAR_LAB_4 = 0xAA
    CINNABAR_POKECENTER = 0xAB
    CINNABAR_MART = 0xAC
    INDIGO_PLATEAU_LOBBY = 0xAE
    COPYCATS_HOUSE_1F = 0xAF
    COPYCATS_HOUSE_2F = 0xB0
    FIGHTING_DOJO = 0xB1
    SAFFRON_GYM = 0xB2
    SAFFRON_HOUSE_1 = 0xB3
    SAFFRON_MART = 0xB4
    SILPH_CO_1F = 0xB5
    SAFFRON_POKECENTER = 0xB6
    SAFFRON_HOUSE_2 = 0xB7
    ROUTE_15_GATE_1F = 0xB8
    ROUTE_15_GATE_2F = 0xB9
    ROUTE_16_GATE_1F = 0xBA
    ROUTE_16_GATE_2F = 0xBB
    ROUTE_16_HOUSE = 0xBC
    ROUTE_12_HOUSE = 0xBD
    ROUTE_18_GATE_1F = 0xBE
    ROUTE_18_GATE_2F = 0xBF
    SEAFOAM_ISLANDS_1F = 0xC0
    ROUTE_22_GATE = 0xC1
    VICTORY_ROAD_2F = 0xC2
    ROUTE_12_GATE_2F = 0xC3
    VERMILION_HOUSE_4 = 0xC4
    DIGLETTS_CAVE = 0xC5
    VICTORY_ROAD_3F = 0xC6
    ROCKET_HIDEOUT_B1F = 0xC7
    ROCKET_HIDEOUT_B2F = 0xC8
    ROCKET_HIDEOUT_B3F = 0xC9
    ROCKET_HIDEOUT_B4F = 0xCA
    ROCKET_HIDEOUT_ELEVATOR = 0xCB
    SILPH_CO_2F = 0xCF
    SILPH_CO_3F = 0xD0
    SILPH_CO_4F = 0xD1
    SILPH_CO_5F = 0xD2
    SILPH_CO_6F = 0xD3
    SILPH_CO_7F = 0xD4
    SILPH_CO_8F = 0xD5
    POKEMON_MANSION_2F = 0xD6
    POKEMON_MANSION_3F = 0xD7
    POKEMON_MANSION_B1F = 0xD8
    SAFARI_ZONE_EAST = 0xD9
    SAFARI_ZONE_NORTH = 0xDA
    SAFARI_ZONE_WEST = 0xDB
    SAFARI_ZONE_CENTER = 0xDC
    SAFARI_ZONE_CENTER_REST_HOUSE = 0xDD
    SAFARI_ZONE_SECRET_HOUSE = 0xDE
    SAFARI_ZONE_WEST_REST_HOUSE = 0xDF
    SAFARI_ZONE_EAST_REST_HOUSE = 0xE0
    SAFARI_ZONE_NORTH_REST_HOUSE = 0xE1
    CERULEAN_CAVE_2F = 0xE2
    CERULEAN_CAVE_B1F = 0xE3
    CERULEAN_CAVE_1F = 0xE4
    NAME_RATERS_HOUSE = 0xE5
    CERULEAN_BADGE_HOUSE = 0xE6
    ROCK_TUNNEL_B1F = 0xE8
    SILPH_CO_9F = 0xE9
    SILPH_CO_10F = 0xEA
    SILPH_CO_11F = 0xEB
    SILPH_CO_ELEVATOR = 0xEC
    TRADE_CENTER = 0xEF
    COLOSSEUM = 0xF0
    LORELEI = 0xF5
    BRUNO = 0xF6
    AGATHA = 0xF7

Functions

decode_pokemon_text(raw_bytes)

Decode Pokemon Red/Blue text encoding to ASCII.

Pokemon Red uses custom tile indices for text: - 0x50 = String terminator - 0x7F = Space - 0x80-0x99 = A-Z (uppercase) - 0xA0-0xB9 = a-z (lowercase) - 0xF6-0xFF = Numbers 0-9 - Various special characters

Source code in pyAIAgent/game/data.py
def decode_pokemon_text(raw_bytes: bytes) -> str:
    """
    Decode Pokemon Red/Blue text encoding to ASCII.

    Pokemon Red uses custom tile indices for text:
    - 0x50 = String terminator
    - 0x7F = Space
    - 0x80-0x99 = A-Z (uppercase)
    - 0xA0-0xB9 = a-z (lowercase)
    - 0xF6-0xFF = Numbers 0-9
    - Various special characters
    """
    # Extended character mapping for dialog text
    CHAR_MAP = {
        0x00: ' ',      # Blank/padding
        0x4F: ':',      # Colon
        0x50: None,     # String terminator (stop here)
        0x51: None,     # Paragraph break
        0x52: None,     # Page break  
        0x54: 'POKĂ©',   # POKĂ© symbol
        0x55: '...',    # Ellipsis (cont'd)
        0x57: None,     # Line break
        0x58: '$',      # Pokédollar
        0x5B: 'PC',     # PC symbol
        0x5C: 'TM',     # TM symbol
        0x5D: 'TRAINER',# TRAINER
        0x5E: 'ROCKET', # ROCKET
        0x7F: ' ',      # Space
        0xE0: '\'',     # Apostrophe (or Ă©)
        0xE1: 'PK',     # PK half
        0xE2: 'MN',     # MN half
        0xE3: '-',      # Hyphen
        0xE6: '?',      # Question mark
        0xE7: '!',      # Exclamation
        0xE8: '.',      # Period
        0xEF: '♂',      # Male symbol
        0xF0: 'ÂĄ',      # Yen
        0xF1: 'Ă—',      # Multiply
        0xF3: '/',      # Slash
        0xF4: ',',      # Comma
        0xF5: '♀',      # Female symbol
        # Numbers 0-9
        0xF6: '0', 0xF7: '1', 0xF8: '2', 0xF9: '3', 0xFA: '4',
        0xFB: '5', 0xFC: '6', 0xFD: '7', 0xFE: '8', 0xFF: '9',
    }

    out = []
    for b in raw_bytes:
        # Check terminator first
        if b == 0x50:
            break
        # Check special mapping
        if b in CHAR_MAP:
            char = CHAR_MAP[b]
            if char is None:  # Break characters
                break
            out.append(char)
        # Uppercase A-Z (0x80-0x99)
        elif 0x80 <= b <= 0x99:
            out.append(chr(ord('A') + (b - 0x80)))
        # Lowercase a-z (0xA0-0xB9)
        elif 0xA0 <= b <= 0xB9:
            out.append(chr(ord('a') + (b - 0xA0)))
        # Unknown - skip silently for dialog
        else:
            pass  # Skip unknown tiles in dialog
    return ''.join(out)

get_item_map()

Returns a dictionary mapping internal Item IDs (hex) to Item Names.

Source code in pyAIAgent/game/data.py
def get_item_map():
    """
    Returns a dictionary mapping internal Item IDs (hex) to Item Names.
    """
    return {
        0x01: "Master Ball", 0x02: "Ultra Ball", 0x03: "Great Ball", 0x04: "Poké Ball",
        0x05: "Town Map", 0x06: "Bicycle", 0x07: "Surfboard", 0x08: "Safari Ball",
        0x09: "Pokédex", 0x0A: "Moon Stone", 0x0B: "Antidote", 0x0C: "Burn Heal",
        0x0D: "Ice Heal", 0x0E: "Awakening", 0x0F: "Paralyz Heal", 0x10: "Full Restore",
        0x11: "Max Potion", 0x12: "Hyper Potion", 0x13: "Super Potion", 0x14: "Potion",
        0x15: "BoulderBadge", 0x16: "CascadeBadge", 0x17: "ThunderBadge", 0x18: "RainbowBadge",
        0x19: "SoulBadge", 0x1A: "MarshBadge", 0x1B: "VolcanoBadge", 0x1C: "EarthBadge",
        0x1D: "Escape Rope", 0x1E: "Repel", 0x1F: "Old Amber", 0x20: "Fire Stone",
        0x21: "Thunder Stone", 0x22: "Water Stone", 0x23: "HP Up", 0x24: "Protein",
        0x25: "Iron", 0x26: "Carbos", 0x27: "Calcium", 0x28: "Rare Candy",
        0x29: "Dome Fossil", 0x2A: "Helix Fossil", 0x2B: "Secret Key", 0x2C: "Unknown",
        0x2D: "Bike Voucher", 0x2E: "X Accuracy", 0x2F: "Leaf Stone", 0x30: "Card Key",
        0x31: "Nugget", 0x32: "PP Up", 0x33: "Poké Doll", 0x34: "Full Heal",
        0x35: "Revive", 0x36: "Max Revive", 0x37: "Grand Special", 0x38: "Super Repel",
        0x39: "Max Repel", 0x3A: "Dire Hit", 0x3B: "Coin", 0x3C: "Fresh Water",
        0x3D: "Soda Pop", 0x3E: "Lemonade", 0x3F: "S.S. Ticket", 0x40: "Gold Teeth",
        0x41: "X Attack", 0x42: "X Defend", 0x43: "X Speed", 0x44: "X Special",
        0x45: "Coin Case", 0x46: "Oak's Parcel", 0x47: "Itemfinder", 0x48: "Silph Scope",
        0x49: "Poké Flute", 0x4A: "Lift Key", 0x4B: "Exp. All", 0x4C: "Old Rod",
        0x4D: "Good Rod", 0x4E: "Super Rod", 0x4F: "PP Up", 0x50: "Ether",
        0x51: "Max Ether", 0x52: "Elixer", 0x53: "Max Elixer", 0x54: "Floor B2F",
        0x55: "Floor B1F", 0x56: "Floor 1F", 0x57: "Floor 2F", 0x58: "Floor 3F",
        0x59: "Floor 4F", 0x5A: "Floor 5F", 0x5B: "Floor 6F", 0x5C: "Floor 7F",
        0x5D: "Floor 8F", 0x5E: "Floor 9F", 0x5F: "Floor 10F", 0x60: "Floor 11F",
        0x61: "Floor B4F",
        0xC4: "HM01", 0xC5: "HM02", 0xC6: "HM03", 0xC7: "HM04", 0xC8: "HM05",
        0xC9: "TM01", 0xCA: "TM02", 0xCB: "TM03", 0xCC: "TM04", 0xCD: "TM05",
        0xCE: "TM06", 0xCF: "TM07", 0xD0: "TM08", 0xD1: "TM09", 0xD2: "TM10",
        0xD3: "TM11", 0xD4: "TM12", 0xD5: "TM13", 0xD6: "TM14", 0xD7: "TM15",
        0xD8: "TM16", 0xD9: "TM17", 0xDA: "TM18", 0xDB: "TM19", 0xDC: "TM20",
        0xDD: "TM21", 0xDE: "TM22", 0xDF: "TM23", 0xE0: "TM24", 0xE1: "TM25",
        0xE2: "TM26", 0xE3: "TM27", 0xE4: "TM28", 0xE5: "TM29", 0xE6: "TM30",
        0xE7: "TM31", 0xE8: "TM32", 0xE9: "TM33", 0xEA: "TM34", 0xEB: "TM35",
        0xEC: "TM36", 0xED: "TM37", 0xEE: "TM38", 0xEF: "TM39", 0xF0: "TM40",
        0xF1: "TM41", 0xF2: "TM42", 0xF3: "TM43", 0xF4: "TM44", 0xF5: "TM45",
        0xF6: "TM46", 0xF7: "TM47", 0xF8: "TM48", 0xF9: "TM49", 0xFA: "TM50"
    }

get_location_name(value)

Return the enum name for a given int, or None if invalid.

Source code in pyAIAgent/game/data.py
def get_location_name(value: int) -> Optional[str]:
    """Return the enum name for a given int, or None if invalid."""
    try:
        return MapLocation(value).name
    except ValueError:
        return None

get_move_map()

Returns a dictionary mapping internal Move IDs (int) to Move Names.

Source code in pyAIAgent/game/data.py
def get_move_map():
    """
    Returns a dictionary mapping internal Move IDs (int) to Move Names.
    """
    return {
        1: "Pound", 2: "Karate Chop", 3: "Double Slap", 4: "Comet Punch", 5: "Mega Punch",
        6: "Pay Day", 7: "Fire Punch", 8: "Ice Punch", 9: "Thunder Punch", 10: "Scratch",
        11: "Vice Grip", 12: "Guillotine", 13: "Razor Wind", 14: "Swords Dance", 15: "Cut",
        16: "Gust", 17: "Wing Attack", 18: "Whirlwind", 19: "Fly", 20: "Bind",
        21: "Slam", 22: "Vine Whip", 23: "Stomp", 24: "Double Kick", 25: "Mega Kick",
        26: "Jump Kick", 27: "Rolling Kick", 28: "Sand Attack", 29: "Headbutt", 30: "Horn Attack",
        31: "Fury Attack", 32: "Horn Drill", 33: "Tackle", 34: "Body Slam", 35: "Wrap",
        36: "Take Down", 37: "Thrash", 38: "Double-Edge", 39: "Tail Whip", 40: "Poison Sting",
        41: "Twineedle", 42: "Pin Missile", 43: "Leer", 44: "Bite", 45: "Growl",
        46: "Roar", 47: "Sing", 48: "Supersonic", 49: "Sonic Boom", 50: "Disable",
        51: "Acid", 52: "Ember", 53: "Flamethrower", 54: "Mist", 55: "Water Gun",
        56: "Hydro Pump", 57: "Surf", 58: "Ice Beam", 59: "Blizzard", 60: "Psybeam",
        61: "Bubble Beam", 62: "Aurora Beam", 63: "Hyper Beam", 64: "Peck", 65: "Drill Peck",
        66: "Submission", 67: "Low Kick", 68: "Counter", 69: "Seismic Toss", 70: "Strength",
        71: "Absorb", 72: "Mega Drain", 73: "Leech Seed", 74: "Growth", 75: "Razor Leaf",
        76: "Solar Beam", 77: "Poison Powder", 78: "Stun Spore", 79: "Sleep Powder", 80: "Petal Dance",
        81: "String Shot", 82: "Dragon Rage", 83: "Fire Spin", 84: "Thunder Shock", 85: "Thunderbolt",
        86: "Thunder Wave", 87: "Thunder", 88: "Rock Throw", 89: "Earthquake", 90: "Fissure",
        91: "Dig", 92: "Toxic", 93: "Confusion", 94: "Psychic", 95: "Hypnosis",
        96: "Meditate", 97: "Agility", 98: "Quick Attack", 99: "Rage", 100: "Teleport",
        101: "Night Shade", 102: "Mimic", 103: "Screech", 104: "Double Team", 105: "Recover",
        106: "Harden", 107: "Minimize", 108: "Smokescreen", 109: "Confuse Ray", 110: "Withdraw",
        111: "Defense Curl", 112: "Barrier", 113: "Light Screen", 114: "Haze", 115: "Reflect",
        116: "Focus Energy", 117: "Bide", 118: "Metronome", 119: "Mirror Move", 120: "Self-Destruct",
        121: "Egg Bomb", 122: "Lick", 123: "Smog", 124: "Sludge", 125: "Bone Club",
        126: "Fire Blast", 127: "Waterfall", 128: "Clamp", 129: "Swift", 130: "Skull Bash",
        131: "Spike Cannon", 132: "Constrict", 133: "Amnesia", 134: "Kinesis", 135: "Soft-Boiled",
        136: "High Jump Kick", 137: "Glare", 138: "Dream Eater", 139: "Poison Gas", 140: "Barrage",
        141: "Leech Life", 142: "Lovely Kiss", 143: "Sky Attack", 144: "Transform", 145: "Bubble",
        146: "Dizzy Punch", 147: "Spore", 148: "Flash", 149: "Psywave", 150: "Splash",
        151: "Acid Armor", 152: "Crabhammer", 153: "Explosion", 154: "Fury Swipes", 155: "Bonemerang",
        156: "Rest", 157: "Rock Slide", 158: "Hyper Fang", 159: "Sharpen", 160: "Conversion",
        161: "Tri Attack", 162: "Super Fang", 163: "Slash", 164: "Substitute", 165: "Struggle"
    }

get_species_map()

Returns a dictionary mapping internal Pokémon Red/Blue IDs (hex) to a tuple containing: (Pokédex number, Name, Type1, Type2). Type2 is None if the Pokémon has only one type.

Source code in pyAIAgent/game/data.py
def get_species_map():
  """
  Returns a dictionary mapping internal Pokémon Red/Blue IDs (hex)
  to a tuple containing: (Pokédex number, Name, Type1, Type2).
  Type2 is None if the Pokémon has only one type.
  """
  return {
        # Glitch / MissingNo. Entries (No Pokédex Number)
        0x00: (None, "'M (glitch)", "Bird", "Normal"),
        0x1F: (None, "MissingNo. (Gyaoon)", "Bird", "Normal"),
        0x20: (None, "MissingNo. (Nidoran♂-like Pokémon)", "Bird", "Normal"),
        0x32: (None, "MissingNo. (Balloonda)", "Bird", "Normal"),
        0x34: (None, "MissingNo. (Buu)", "Bird", "Normal"),
        0x38: (None, "MissingNo. (Deer)", "Bird", "Normal"),
        0x3D: (None, "MissingNo. (Elephant Pokémon)", "Bird", "Normal"),
        0x3E: (None, "MissingNo. (Crocky)", "Bird", "Normal"),
        0x3F: (None, "MissingNo. (Squid Pokémon 1)", "Bird", "Normal"),
        0x43: (None, "MissingNo. (Cactus)", "Bird", "Normal"),
        0x44: (None, "MissingNo. (Jaggu)", "Bird", "Normal"),
        0x45: (None, "MissingNo. (Zubat pre-evo)", "Bird", "Normal"),
        0x4F: (None, "MissingNo. (Fish Pokémon 1)", "Bird", "Normal"),
        0x50: (None, "MissingNo. (Fish Pokémon 2)", "Bird", "Normal"),
        0x51: (None, "MissingNo. (Vulpix pre-evo)", "Bird", "Normal"),
        0x56: (None, "MissingNo. (Frog-like Pokémon 1)", "Bird", "Normal"),
        0x57: (None, "MissingNo. (Frog-like Pokémon 2)", "Bird", "Normal"),
        0x5E: (None, "MissingNo. (Lizard Pokémon 2)", "Bird", "Normal"),
        0x5F: (None, "MissingNo. (Lizard Pokémon 3)", "Bird", "Normal"),
        0x73: (None, "MissingNo. [Unknown]", "Bird", "Normal"),
        0x79: (None, "MissingNo. [Unknown]", "Bird", "Normal"),
        0x7A: (None, "MissingNo. (Squid Pokémon 2)", "Bird", "Normal"),
        0x7F: (None, "MissingNo. (Golduck mid-evo)", "Bird", "Normal"),
        0x86: (None, "MissingNo. (Meowth pre-evo)", "Bird", "Normal"),
        0x87: (None, "MissingNo. [Unknown]", "Bird", "Normal"),
        0x89: (None, "MissingNo. (Gyaoon pre-evo)", "Bird", "Normal"),
        0x8C: (None, "MissingNo. (Magneton-like Pokémon)", "Bird", "Normal"),
        0x92: (None, "MissingNo. (Marowak evo)", "Bird", "Normal"),
        0x9C: (None, "MissingNo. (Goldeen pre-evo)", "Bird", "Normal"),
        0x9F: (None, "MissingNo. (Kotora)", "Bird", "Normal"),
        0xA0: (None, "MissingNo. (Raitora)", "Bird", "Normal"),
        0xA1: (None, "MissingNo. (Raitora evo)", "Bird", "Normal"),
        0xA2: (None, "MissingNo. (Ponyta pre-evo)", "Bird", "Normal"),
        0xAC: (None, "MissingNo. (Blastoise-like Pokémon)", "Bird", "Normal"),
        0xAE: (None, "MissingNo. (Lizard Pokémon 1)", "Bird", "Normal"),
        0xAF: (None, "MissingNo. (Gorochu)", "Bird", "Normal"),
        0xB5: (None, "MissingNo. (Original Wartortle evo)", "Bird", "Normal"),
        0xB6: (None, "MissingNo. (Kabutops Fossil)", "Bird", "Normal"),
        0xB7: (None, "MissingNo. (Aerodactyl Fossil)", "Bird", "Normal"),
        0xB8: (None, "MissingNo. (Pokémon Tower Ghost)", "Bird", "Normal"),

        # Pokédex Order
        0x99: (1, "Bulbasaur", "Grass", "Poison"),
        0x09: (2, "Ivysaur", "Grass", "Poison"),
        0x9A: (3, "Venusaur", "Grass", "Poison"),
        0xB0: (4, "Charmander", "Fire", None),
        0xB2: (5, "Charmeleon", "Fire", None),
        0xB4: (6, "Charizard", "Fire", "Flying"),
        0xB1: (7, "Squirtle", "Water", None),
        0xB3: (8, "Wartortle", "Water", None),
        0x1C: (9, "Blastoise", "Water", None),
        0x7B: (10, "Caterpie", "Bug", None),
        0x7C: (11, "Metapod", "Bug", None),
        0x7D: (12, "Butterfree", "Bug", "Flying"),
        0x70: (13, "Weedle", "Bug", "Poison"),
        0x71: (14, "Kakuna", "Bug", "Poison"),
        0x72: (15, "Beedrill", "Bug", "Poison"),
        0x24: (16, "Pidgey", "Normal", "Flying"),
        0x96: (17, "Pidgeotto", "Normal", "Flying"),
        0x97: (18, "Pidgeot", "Normal", "Flying"),
        0xA5: (19, "Rattata", "Normal", None),
        0xA6: (20, "Raticate", "Normal", None),
        0x05: (21, "Spearow", "Normal", "Flying"),
        0x23: (22, "Fearow", "Normal", "Flying"),
        0x6C: (23, "Ekans", "Poison", None),
        0x2D: (24, "Arbok", "Poison", None),
        0x54: (25, "Pikachu", "Electric", None),
        0x55: (26, "Raichu", "Electric", None),
        0x60: (27, "Sandshrew", "Ground", None),
        0x61: (28, "Sandslash", "Ground", None),
        0x0F: (29, "Nidoran♀", "Poison", None),
        0xA8: (30, "Nidorina", "Poison", None),
        0x10: (31, "Nidoqueen", "Poison", "Ground"),
        0x03: (32, "Nidoran♂", "Poison", None),
        0xA7: (33, "Nidorino", "Poison", None),
        0x07: (34, "Nidoking", "Poison", "Ground"),
        0x04: (35, "Clefairy", "Normal", None),
        0x8E: (36, "Clefable", "Normal", None),
        0x52: (37, "Vulpix", "Fire", None),
        0x53: (38, "Ninetales", "Fire", None),
        0x64: (39, "Jigglypuff", "Normal", None),
        0x65: (40, "Wigglytuff", "Normal", None),
        0x6B: (41, "Zubat", "Poison", "Flying"),
        0x82: (42, "Golbat", "Poison", "Flying"),
        0xB9: (43, "Oddish", "Grass", "Poison"),
        0xBA: (44, "Gloom", "Grass", "Poison"),
        0xBB: (45, "Vileplume", "Grass", "Poison"),
        0x6D: (46, "Paras", "Bug", "Grass"),
        0x2E: (47, "Parasect", "Bug", "Grass"),
        0x41: (48, "Venonat", "Bug", "Poison"),
        0x77: (49, "Venomoth", "Bug", "Poison"),
        0x3B: (50, "Diglett", "Ground", None),
        0x76: (51, "Dugtrio", "Ground", None),
        0x4D: (52, "Meowth", "Normal", None),
        0x90: (53, "Persian", "Normal", None),
        0x2F: (54, "Psyduck", "Water", None),
        0x80: (55, "Golduck", "Water", None),
        0x39: (56, "Mankey", "Fighting", None),
        0x75: (57, "Primeape", "Fighting", None),
        0x21: (58, "Growlithe", "Fire", None),
        0x14: (59, "Arcanine", "Fire", None),
        0x47: (60, "Poliwag", "Water", None),
        0x6E: (61, "Poliwhirl", "Water", None),
        0x6F: (62, "Poliwrath", "Water", "Fighting"),
        0x94: (63, "Abra", "Psychic", None),
        0x26: (64, "Kadabra", "Psychic", None),
        0x95: (65, "Alakazam", "Psychic", None),
        0x6A: (66, "Machop", "Fighting", None),
        0x29: (67, "Machoke", "Fighting", None),
        0x7E: (68, "Machamp", "Fighting", None),
        0xBC: (69, "Bellsprout", "Grass", "Poison"),
        0xBD: (70, "Weepinbell", "Grass", "Poison"),
        0xBE: (71, "Victreebel", "Grass", "Poison"),
        0x18: (72, "Tentacool", "Water", "Poison"),
        0x9B: (73, "Tentacruel", "Water", "Poison"),
        0xA9: (74, "Geodude", "Rock", "Ground"),
        0x27: (75, "Graveler", "Rock", "Ground"),
        0x31: (76, "Golem", "Rock", "Ground"),
        0xA3: (77, "Ponyta", "Fire", None),
        0xA4: (78, "Rapidash", "Fire", None),
        0x25: (79, "Slowpoke", "Water", "Psychic"),
        0x08: (80, "Slowbro", "Water", "Psychic"),
        0xAD: (81, "Magnemite", "Electric", None),
        0x36: (82, "Magneton", "Electric", None),
        0x40: (83, "Farfetch'd", "Normal", "Flying"),
        0x46: (84, "Doduo", "Normal", "Flying"),
        0x74: (85, "Dodrio", "Normal", "Flying"),
        0x3A: (86, "Seel", "Water", None),
        0x78: (87, "Dewgong", "Water", "Ice"),
        0x0D: (88, "Grimer", "Poison", None),
        0x88: (89, "Muk", "Poison", None),
        0x17: (90, "Shellder", "Water", None),
        0x8B: (91, "Cloyster", "Water", "Ice"),
        0x19: (92, "Gastly", "Ghost", "Poison"),
        0x93: (93, "Haunter", "Ghost", "Poison"),
        0x0E: (94, "Gengar", "Ghost", "Poison"),
        0x22: (95, "Onix", "Rock", "Ground"),
        0x30: (96, "Drowzee", "Psychic", None),
        0x81: (97, "Hypno", "Psychic", None),
        0x4E: (98, "Krabby", "Water", None),
        0x8A: (99, "Kingler", "Water", None),
        0x06: (100, "Voltorb", "Electric", None),
        0x8D: (101, "Electrode", "Electric", None),
        0x0C: (102, "Exeggcute", "Grass", "Psychic"),
        0x0A: (103, "Exeggutor", "Grass", "Psychic"),
        0x11: (104, "Cubone", "Ground", None),
        0x91: (105, "Marowak", "Ground", None),
        0x2B: (106, "Hitmonlee", "Fighting", None),
        0x2C: (107, "Hitmonchan", "Fighting", None),
        0x0B: (108, "Lickitung", "Normal", None),
        0x37: (109, "Koffing", "Poison", None),
        0x8F: (110, "Weezing", "Poison", None),
        0x12: (111, "Rhyhorn", "Ground", "Rock"),
        0x01: (112, "Rhydon", "Ground", "Rock"),
        0x28: (113, "Chansey", "Normal", None),
        0x1E: (114, "Tangela", "Grass", None),
        0x02: (115, "Kangaskhan", "Normal", None),
        0x5C: (116, "Horsea", "Water", None),
        0x5D: (117, "Seadra", "Water", None),
        0x9D: (118, "Goldeen", "Water", None),
        0x9E: (119, "Seaking", "Water", None),
        0x1B: (120, "Staryu", "Water", None),
        0x98: (121, "Starmie", "Water", "Psychic"),
        0x2A: (122, "Mr. Mime", "Psychic", None),
        0x1A: (123, "Scyther", "Bug", "Flying"),
        0x48: (124, "Jynx", "Ice", "Psychic"),
        0x35: (125, "Electabuzz", "Electric", None),
        0x33: (126, "Magmar", "Fire", None),
        0x1D: (127, "Pinsir", "Bug", None),
        0x3C: (128, "Tauros", "Normal", None),
        0x85: (129, "Magikarp", "Water", None),
        0x16: (130, "Gyarados", "Water", "Flying"),
        0x13: (131, "Lapras", "Water", "Ice"),
        0x4C: (132, "Ditto", "Normal", None),
        0x66: (133, "Eevee", "Normal", None),
        0x69: (134, "Vaporeon", "Water", None),
        0x68: (135, "Jolteon", "Electric", None),
        0x67: (136, "Flareon", "Fire", None),
        0xAA: (137, "Porygon", "Normal", None),
        0x62: (138, "Omanyte", "Rock", "Water"),
        0x63: (139, "Omastar", "Rock", "Water"),
        0x5A: (140, "Kabuto", "Rock", "Water"),
        0x5B: (141, "Kabutops", "Rock", "Water"),
        0xAB: (142, "Aerodactyl", "Rock", "Flying"),
        0x84: (143, "Snorlax", "Normal", None),
        0x4A: (144, "Articuno", "Ice", "Flying"),
        0x4B: (145, "Zapdos", "Electric", "Flying"),
        0x49: (146, "Moltres", "Fire", "Flying"),
        0x58: (147, "Dratini", "Dragon", None),
        0x59: (148, "Dragonair", "Dragon", None),
        0x42: (149, "Dragonite", "Dragon", "Flying"),
        0x83: (150, "Mewtwo", "Psychic", None),
        0x15: (151, "Mew", "Psychic", None),
    }

LLM Clients

pyAIAgent.llm.client

Socket Communication

pyAIAgent.utils.socket_utils

JSON Parsing

pyAIAgent.json_parser

Functions

parse_optional_fenced_json(text)

Parses JSON from text.

The JSON may be either
  • Enclosed in triple-backtick fences
  • Plain JSON without fences

Parameters:

Name Type Description Default
text str

The text string to parse

required

Returns:

Type Description
Any

The deserialized Python object.

Source code in pyAIAgent/json_parser.py
def parse_optional_fenced_json(text: str) -> Any:
    """
    Parses JSON from `text`.

    The JSON may be either:
      - Enclosed in triple-backtick fences
      - Plain JSON without fences

    Args:
        text: The text string to parse

    Returns:
        The deserialized Python object.
    """
    # Try to find a fenced JSON block first
    fence_pattern = r"```(?:json)?\s*\n(.*?)\n```"
    m = re.search(fence_pattern, text, flags=re.DOTALL | re.IGNORECASE)
    if m:
        json_str = m.group(1)
    else:
        # No fence: assume the entire text is JSON
        json_str = text.strip()

    try:
        j = json.loads(json_str)
        return j
    except json.JSONDecodeError:
        return {}