3636
3737
3838class BananasEnv :
39- """PyBoy wrapper for bananas.gb .
39+ """Headless PyBoy environment for the Bananas ROM .
4040
41- Observation: grayscale (144, 160) uint8 in [0, 3] (0 = lightest).
42- Action: a `GameInput` (preferred) or any length-8 bool/0-1 sequence over
43- (UP, DOWN, LEFT, RIGHT, A, B, START, SELECT).
44- Step: presses the action mask for `frame_skip` ticks, then releases it .
41+ Observations are grayscale `` uint8`` arrays with shape ``(144, 160)`` and
42+ values ``0..3`` where ``0`` is lightest. Actions are either ``GameInput``
43+ values or length-8 bool/0-1 sequences in `` (UP, DOWN, LEFT, RIGHT, A, B,
44+ START, SELECT)`` order .
4545 """
4646
4747 def __init__ (
@@ -54,6 +54,22 @@ def __init__(
5454 sound : bool = False ,
5555 no_input : bool = True ,
5656 ) -> None :
57+ """Create a Bananas environment.
58+
59+ Args:
60+ rom_path: Optional path to a ``.gb`` ROM. Defaults to the packaged
61+ Bananas ROM.
62+ window: PyBoy window backend. Use ``"null"`` for headless training.
63+ scale: Display scale used when a visible PyBoy window is enabled.
64+ frame_skip: Emulator frames advanced by each ``step`` call.
65+ skip_splash: If true, advance from the splash screen to gameplay.
66+ sound: Whether PyBoy should emulate sound.
67+ no_input: Whether PyBoy should ignore host keyboard input.
68+
69+ Raises:
70+ FileNotFoundError: If the ROM or matching ``.sym`` file is missing.
71+ ValueError: If ``frame_skip`` is less than 1.
72+ """
5773 if rom_path is not None :
5874 rom = validate_rom (Path (rom_path ))
5975 else :
@@ -91,10 +107,16 @@ def __init__(
91107 # ------------------------------------------------------------------ core
92108
93109 def reset (self ) -> np .ndarray :
94- """Reload the boot state, idle a random number of frames on the splash
95- screen so the hardware DIV register varies, then press START. The game
96- seeds its RNG from DIV_REG at the moment START is pressed, producing
97- unique terrain each episode."""
110+ """Start a new randomized episode and return the first observation.
111+
112+ Reloads the boot state, idles for a random number of splash-screen
113+ frames, then presses START. This varies the hardware DIV register used
114+ by the game seed so terrain changes between episodes.
115+
116+ Returns:
117+ Grayscale ``uint8`` observation with shape ``(144, 160)`` and
118+ values ``0..3``.
119+ """
98120 self ._pyboy .load_state (io .BytesIO (self ._boot_state ))
99121 warmup = int (self ._rng .integers (0 , SPLASH_WARMUP_MAX ))
100122 self ._pyboy .tick (count = warmup , render = False )
@@ -103,6 +125,18 @@ def reset(self) -> np.ndarray:
103125 return self .observe ()
104126
105127 def step (self , action ) -> np .ndarray :
128+ """Apply an action for one environment step.
129+
130+ Args:
131+ action: ``GameInput`` or any length-8 bool/0-1 sequence in
132+ ``(UP, DOWN, LEFT, RIGHT, A, B, START, SELECT)`` order.
133+
134+ Returns:
135+ Next grayscale ``uint8`` observation with shape ``(144, 160)``.
136+
137+ Raises:
138+ ValueError: If ``action`` is not a valid 8-button mask.
139+ """
106140 mask = _action_mask (action )
107141 pressed = [name for bit , name in zip (mask , BUTTON_NAMES , strict = True ) if bit ]
108142 for name in pressed :
@@ -115,41 +149,63 @@ def step(self, action) -> np.ndarray:
115149 return self .observe ()
116150
117151 def observe (self ) -> np .ndarray :
152+ """Return the current frame without advancing the emulator.
153+
154+ Returns:
155+ Grayscale ``uint8`` observation with shape ``(144, 160)`` and
156+ values ``0..3``.
157+ """
118158 return (255 - self ._pyboy .screen .ndarray [..., 0 ]) >> 6
119159
120160 # ---------------------------------------------------------------- state
121161
122162 def game_state (self ) -> GameState :
163+ """Return the decoded game state from emulator memory."""
123164 return decode_game_state (self .g_game_bytes ())
124165
125166 def g_game_bytes (self ) -> bytes :
167+ """Return raw bytes for the ROM's ``g_game`` state struct."""
126168 return bytes (self ._pyboy .memory [self ._g_game_addr : self ._g_game_addr + GAME_STATE_SIZE ])
127169
128170 @property
129171 def g_game_addr (self ) -> int :
172+ """Memory address of the ROM's ``g_game`` state struct."""
130173 return self ._g_game_addr
131174
132175 # ----------------------------------------------------------- emulation
133176
134177 def tick (self , count : int = 1 , render : bool = True ) -> bool :
135- """Advance the emulator by * count* frames.
178+ """Advance the emulator by `` count`` frames.
136179
137- Returns ``False`` when the emulator window has been closed.
180+ Args:
181+ count: Number of emulator frames to advance.
182+ render: Whether PyBoy should render frames while ticking.
183+
184+ Returns:
185+ ``False`` when the emulator window has been closed, otherwise
186+ ``True``.
138187 """
139188 return self ._pyboy .tick (count = count , render = render )
140189
141190 def set_emulation_speed (self , speed : int ) -> None :
142- """Set the target emulation speed (1 = real-time, 0 = unlimited)."""
191+ """Set target emulation speed.
192+
193+ Args:
194+ speed: PyBoy speed multiplier. ``1`` is real-time; ``0`` is
195+ unlimited.
196+ """
143197 self ._pyboy .set_emulation_speed (speed )
144198
145199 # -------------------------------------------------------------- lifecycle
146200
147201 def close (self ) -> None :
202+ """Stop the PyBoy emulator and release environment resources."""
148203 if self ._pyboy is not None :
149204 self ._pyboy .stop ()
150205 self ._pyboy = None # type: ignore[assignment]
151206
152207 def __enter__ (self ) -> BananasEnv :
208+ """Return this environment for ``with`` statement use."""
153209 return self
154210
155211 def __exit__ (
@@ -158,6 +214,7 @@ def __exit__(
158214 exc : BaseException | None ,
159215 tb : TracebackType | None ,
160216 ) -> None :
217+ """Close the environment when leaving a ``with`` block."""
161218 self .close ()
162219
163220
@@ -166,7 +223,6 @@ def __exit__(
166223
167224
168225def skip_through_splash (pyboy : PyBoy , g_game_addr : int ) -> None :
169- """Pulse START until g_game.state == AIM and players[0].lives == 3."""
170226 for _ in range (0 , SPLASH_MAX_FRAMES , 8 ):
171227 if _in_game (pyboy , g_game_addr ):
172228 return
0 commit comments