Skip to main content
Glama
emulator.py•6.8 kB
""" PyBoy Emulator Wrapper Provides a high-level interface for PyBoy emulation with LLM-friendly error handling. This module abstracts PyBoy's complexity and provides consistent error messages that help LLMs understand and recover from common issues. """ import logging from pathlib import Path from typing import Any from pyboy import PyBoy logger = logging.getLogger(__name__) class EmulatorError(Exception): """Base exception for emulator-related errors with LLM-friendly messages.""" pass class ROMError(EmulatorError): """ROM-related errors with specific guidance for LLMs.""" pass class EmulatorStateError(EmulatorError): """Emulator state-related errors with recovery suggestions.""" pass class PyBoyEmulator: """ PyBoy emulator wrapper with LLM-friendly interface. Provides lifecycle management, error handling, and consistent state management for Game Boy emulation. Designed to give LLMs clear feedback on operations and suggest recovery actions when things go wrong. """ def __init__(self, headless: bool = True) -> None: """ Initialize PyBoy emulator wrapper. Args: headless: Run emulator without GUI window (default: True for LLM use) """ self.headless = headless self.pyboy: PyBoy | None = None self.current_rom_path: Path | None = None self.is_running = False logger.info(f"PyBoy emulator wrapper initialized (headless={headless})") def start(self) -> None: """ Start the emulator without loading a ROM. Raises: EmulatorStateError: If emulator is already running """ if self.is_running: raise EmulatorStateError( "Emulator is already running. Use stop() first or load a ROM directly." ) logger.info("Starting PyBoy emulator...") self.is_running = True def stop(self) -> None: """ Stop the emulator and clean up resources. This method is safe to call multiple times and will handle cleanup gracefully even if the emulator is already stopped. """ if self.pyboy is not None: try: self.pyboy.stop() logger.info("PyBoy emulator stopped") except Exception as e: logger.warning(f"Error stopping PyBoy: {e}") finally: self.pyboy = None self.current_rom_path = None self.is_running = False def load_rom(self, rom_path: str | Path) -> None: """ Load a Game Boy ROM file into the emulator. Args: rom_path: Path to the ROM file (.gb or .gbc) Raises: ROMError: If ROM file is invalid or cannot be loaded EmulatorStateError: If emulator fails to initialize """ rom_path = Path(rom_path) # Validate ROM file exists if not rom_path.exists(): raise ROMError( f"ROM file not found: {rom_path}. " f"Check the file path and ensure the ROM file exists." ) # Validate ROM file extension if rom_path.suffix.lower() not in {".gb", ".gbc"}: raise ROMError( f"Invalid ROM file extension: {rom_path.suffix}. " f"Only .gb and .gbc files are supported." ) # Stop current emulator if running if self.pyboy is not None: self.stop() try: # Initialize PyBoy with the ROM logger.info(f"Loading ROM: {rom_path}") # Configure PyBoy based on headless mode if self.headless: self.pyboy = PyBoy(str(rom_path), window="null") else: self.pyboy = PyBoy(str(rom_path)) self.current_rom_path = rom_path self.is_running = True logger.info(f"ROM loaded successfully: {rom_path.name}") except Exception as e: self.pyboy = None self.current_rom_path = None self.is_running = False # Convert PyBoy exceptions to LLM-friendly messages error_msg = str(e).lower() if "invalid rom" in error_msg or "corrupt" in error_msg: raise ROMError( f"ROM file appears to be corrupted or invalid: {rom_path.name}. " f"Try a different ROM file or verify the file isn't damaged." ) from e elif "permission" in error_msg or "access" in error_msg: raise ROMError( f"Cannot access ROM file: {rom_path}. " f"Check file permissions and ensure it's not in use by another program." ) from e else: raise EmulatorStateError( f"Failed to initialize emulator with ROM: {e}. " f"This might be a PyBoy compatibility issue or system resource problem." ) from e def get_rom_info(self) -> dict[str, Any]: """ Get information about the currently loaded ROM. Returns: dict: ROM information including name, path, and status Raises: EmulatorStateError: If no ROM is loaded """ if not self.is_running or self.current_rom_path is None: raise EmulatorStateError( "No ROM is currently loaded. Use load_rom() to load a ROM first." ) return { "name": self.current_rom_path.name, "path": str(self.current_rom_path), "size": self.current_rom_path.stat().st_size, "extension": self.current_rom_path.suffix, "is_running": self.is_running, } def get_pyboy_instance(self) -> PyBoy: """ Get the underlying PyBoy instance for direct access. Returns: PyBoy: The PyBoy emulator instance Raises: EmulatorStateError: If emulator is not running """ if not self.is_running or self.pyboy is None: raise EmulatorStateError( "Emulator is not running. Use load_rom() to start emulation first." ) return self.pyboy def is_ready(self) -> bool: """ Check if the emulator is ready for operations. Returns: bool: True if emulator is running and ready """ return self.is_running and self.pyboy is not None def __enter__(self) -> "PyBoyEmulator": """Context manager entry.""" return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Context manager exit with cleanup.""" self.stop()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ssimonitch/mcp-pyboy'

If you have feedback or need assistance with the MCP directory API, please join our Discord server