Source code for pyboy.plugins.game_wrapper_super_mario_bros_deluxe

#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

import numpy as np

import pyboy
from pyboy.api.constants import TILES_CGB
from pyboy.utils import PyBoyException, PyBoyInvalidInputException, bcd_to_dec

from .base_plugin import PyBoyGameWrapper

logger = pyboy.logging.get_logger(__name__)

ADDR_LEVEL_SET = 0xC160
ADDR_SUBLEVEL = 0xC162
ADDR_LEVEL = 0xC163
ADDR_SCORE = 0xC17A
ADDR_TIME_LEFT = 0xC17D
ADDR_LIVES_LEFT = 0xC17F
ADDR_COINS = 0xC1F2
ADDR_MENU_SELECTION = 0xC1A8
ADDR_CHALLENGE_UNLOCK = 0xC18E
ADDR_PLAYER_STATE = 0xC1C1
ADDR_PLAYER_X = 0xC1CA
ADDR_PLAYER_Y = 0xFFA9
ADDR_LEVEL_X = 0xFFA7
ADDR_CAMERA_X_LOW = 0xFFB8
ADDR_CAMERA_X_HIGH = 0xFFB9
ADDR_CAMERA_Y = 0xFFBA
ADDR_MODE = 0xFFB5
SPRITE_SLOT_COUNT = 0x0F
SPRITE_WRAM_BANK = 1
ADDR_SPRITE_STATUS = 0xD000
ADDR_SPRITE_ID = 0xD00F
ADDR_SPRITE_X_LOW = 0xD01E
ADDR_SPRITE_X_HIGH = 0xD02D
ADDR_SPRITE_Y_LOW = 0xD03C
ADDR_SPRITE_Y_HIGH = 0xD04B
ADDR_SPRITE_X_SPEED = 0xD05A
ADDR_SPRITE_Y_SPEED = 0xD069
ADDR_SPRITE_SUBSTATE = 0xD186
SPRITE_SLOT_FIELDS = (
    ("status", ADDR_SPRITE_STATUS),
    ("id", ADDR_SPRITE_ID),
    ("x_low", ADDR_SPRITE_X_LOW),
    ("x_high", ADDR_SPRITE_X_HIGH),
    ("y_low", ADDR_SPRITE_Y_LOW),
    ("y_high", ADDR_SPRITE_Y_HIGH),
    ("x_speed_raw", ADDR_SPRITE_X_SPEED),
    ("y_speed_raw", ADDR_SPRITE_Y_SPEED),
    ("d078", 0xD078),
    ("d087", 0xD087),
    ("d096", 0xD096),
    ("d0a5", 0xD0A5),
    ("d0b4", 0xD0B4),
    ("d0c3", 0xD0C3),
    ("d0d2", 0xD0D2),
    ("d0e1", 0xD0E1),
    ("d0f0", 0xD0F0),
    ("d0ff", 0xD0FF),
    ("d10e", 0xD10E),
    ("d11d", 0xD11D),
    ("d12c", 0xD12C),
    ("d13b", 0xD13B),
    ("d14a", 0xD14A),
    ("d159", 0xD159),
    ("d168", 0xD168),
    ("d177", 0xD177),
    ("substate", ADDR_SPRITE_SUBSTATE),
    ("d195", 0xD195),
    ("d1a4", 0xD1A4),
    ("d1b3", 0xD1B3),
    ("d1c2", 0xD1C2),
    ("d1d1", 0xD1D1),
    ("d1e0", 0xD1E0),
    ("d1ef", 0xD1EF),
    ("d1fe", 0xD1FE),
    ("d20d", 0xD20D),
    ("d21c", 0xD21C),
    ("d22b", 0xD22B),
    ("d23a", 0xD23A),
    ("d249", 0xD249),
    ("d258", 0xD258),
    ("d267", 0xD267),
    ("d276", 0xD276),
    ("d285", 0xD285),
    ("d294", 0xD294),
    ("d2a3", 0xD2A3),
    ("d2b2", 0xD2B2),
    ("d2c1", 0xD2C1),
    ("d2d0", 0xD2D0),
)

MARIO_FIREBALL_ID = 0x0D
FIREBAR_ID_MIN = 0x0F
FIREBAR_ID_MAX = 0x16

MODE_TITLE_SCREEN = 0x03
MODE_MAIN_MENU = 0x19
MODE_FILE_SELECT = 0x17
MODE_OVERWORLD_INIT = 0x04
MODE_WORLD_MAP = 0x05
MODE_LEVEL = 0x0B
MODE_DEBUG_INIT = 0x31
MODE_DEBUG_MENU = 0x32
MODE_CHALLENGE_SELECT = 0x1E
STUCK_TIMEOUT_FRAMES = 600
MAX_PROGRESS_DELTA_PER_FRAME = 32

# The table is in ROM bank 3 at 03:6D16 (Tile16_InteractTypes).
INTERACTION_TABLE_BANK = 3
INTERACTION_TABLE_ADDRESS = 0x6D16
METATILE_MAP_BANK = 6
METATILE_MAP_ADDRESS = 0xD000
METATILE_MAP_WIDTH = 256
METATILE_MAP_HEIGHT = 16
METATILE_SCREEN_WIDTH = 16
METATILE_SCREEN_SIZE = 0x100
DEFAULT_SPRITE_OFFSET = 0

mapping_minimal = np.arange(TILES_CGB, dtype=np.uint32)
mapping_minimal[
    [38, 39, *range(68, 74), *range(84, 92), *range(96, 100), 138, 139, *range(176, 184), *range(186, 190)]
] = 0
mapping_minimal[[132, 133, 168, 169, 170, 171]] = 0  # Castle

mapping_minimal[[*range(0, 8), *range(16, 24), *range(384, 392)]] = 1  # Mario
mapping_minimal[[128, 129, 130, 131, 136, 137]] = 8  # Brick
mapping_minimal[[*range(52, 56)]] = 7  # Goomba
mapping_minimal[[100, 101]] = 14  # Star
mapping_minimal[[48, 49, *range(80, 84)]] = 15  # End of game flag
mapping_minimal[[*range(92, 96)]] = 12  # Mario fireball (and Rotating flames!!!)
mapping_minimal[[*range(194, 198)]] = 18  # Rotating flames
mapping_minimal[[46, 47, *range(114, 118)]] = 11  # Flower
mapping_minimal[[*range(56, 68)]] = 13  # Turtle
mapping_minimal[[42, 43, 44, 45]] = 9  # Mushroom
mapping_minimal[[34, 35, 124, 125, 126, 127]] = 10  # Plant
mapping_minimal[[414, 415, 416, 417, 418, 419, 482, 483]] = 16  # Trampoline
mapping_minimal[[474, 475, 476, 477, 478, 479, 480, 481]] = 17  # Flying turtle

mapping_minimal[[*range(28, 34)]] = 0  # Mario fireball explosion
mapping_minimal[[*range(56, 68)]] = 19  # Turtle
mapping_minimal[[*range(108, 114)]] = 20  # Bowser flame
mapping_minimal[[428, 429]] = 21  # Platform
mapping_minimal[[*range(496, 512), *range(400, 410)]] = 22  # Bowser
mapping_minimal[[422, 423, 424, 425, 426, 427]] = 23  # Squid
mapping_minimal[[74, 75, 76, 77, 78, 79]] = 24  # Fish
mapping_minimal[[26, 27]] = 25  # Lava fireball (also 28, but it conflicts)

mapping_minimal[
    [144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 160, 161, 162, 163, 164, 165, 166, 167]
] = 26  # Hammer guy
mapping_minimal[[*range(102, 108)]] = 27  # hammers

mapping_minimal[[438, 439, 440, 441, 442, 443, 444, 445]] = 28  # Beetle

mapping_minimal[[*range(456, 464)]] = 28  # Red spiky thing
mapping_minimal[[*range(464, 468)]] = 29  # Falling red spiky thing
mapping_minimal[[*range(448, 456)]] = 30  # Man in cloud
mapping_minimal[[*range(470, 474)]] = 31  # Bullet

object_id_mapping = np.zeros(0x65, dtype=np.uint32)
object_id_mapping[0] = 0  # Inactive
object_id_mapping[0x01] = 0  # BounceSpr01
object_id_mapping[[0x02, 0x03, 0x31]] = 19  # KoopaBuzzy
object_id_mapping[0x04] = 7  # Goomba
object_id_mapping[[*range(0x05, 0x09)]] = 24  # CheepCheepSwimming
object_id_mapping[0x09] = 15  # FlagpoleSpr
object_id_mapping[0x0A] = 0  # CastleFlag
object_id_mapping[0x0B] = 0  # CoinFromBlock
object_id_mapping[0x0C] = 0  # ItemFromBlock
object_id_mapping[0x0D] = 12  # MarioFireball
object_id_mapping[0x0E] = 8  # BrokenBrick
object_id_mapping[[*range(0x0F, 0x17)]] = 18  # Firebar
object_id_mapping[0x17] = 26  # HammerBro
object_id_mapping[0x18] = 27  # Hammer
object_id_mapping[0x19] = 10  # PiranhaUp
object_id_mapping[0x1A] = 22  # Bowser
object_id_mapping[0x1B] = 20  # BowserFire
object_id_mapping[0x1C] = 0  # ToadPeach
object_id_mapping[0x1D] = 0  # Spr1D
object_id_mapping[0x1E] = 0  # Spr1E
object_id_mapping[0x1F] = 25  # Podoboo
object_id_mapping[0x20] = 0  # VineSpr
object_id_mapping[0x21] = 0  # Firework
object_id_mapping[0x22] = 0  # Spr22
object_id_mapping[0x23] = 0  # Spr23
object_id_mapping[0x24] = 0  # BulletBillShooter
object_id_mapping[0x25] = 31  # BulletBill
object_id_mapping[[*range(0x26, 0x2C)]] = 17  # Paratroopa
object_id_mapping[0x2C] = 23  # Blooper
object_id_mapping[0x2D] = 16  # Trampoline
object_id_mapping[0x2E] = 30  # Lakitu
object_id_mapping[0x2F] = 28  # SpinyEgg
object_id_mapping[0x30] = 28  # SPiny
object_id_mapping[0x32] = 0  # BowserFireGen
object_id_mapping[0x33] = 0  # Spr33
object_id_mapping[0x34] = 0  # Spr34
object_id_mapping[0x35] = 0  # Spr35
object_id_mapping[0x36] = 0  # BulletBillGen
object_id_mapping[0x37] = 0  # Spr37
object_id_mapping[0x38] = 0  # Spr38
object_id_mapping[[*range(0x39, 0x3C)]] = 0  # ScrollCmd
object_id_mapping[0x3C] = 0  # Spr3C
object_id_mapping[0x3D] = 0  # Return027A75
object_id_mapping[0x3E] = 0  # Spr3E
object_id_mapping[0x3F] = 0  # Spr3F
object_id_mapping[0x40] = 0  # Empty06658E
object_id_mapping[0x41] = 0  # Spr41
object_id_mapping[0x42] = 0  # RaceCountdown
object_id_mapping[0x43] = 0  # Spr43
object_id_mapping[0x44] = 0  # Spr44
object_id_mapping[0x45] = 0  # CloudBonusPerfect
object_id_mapping[0x46] = 0  # Boo
object_id_mapping[0x47] = 10  # PiranhaDown
object_id_mapping[0x48] = 0  # Spr48
object_id_mapping[[*range(0x49, 0x4D)]] = 0  # MultiKoopaGoomba
object_id_mapping[0x4D] = 16  # TrampolineGreen
object_id_mapping[[0x4E, 0x4F]] = 0  # MultiParatroopa
object_id_mapping[[*range(0x50, 0x56)]] = 0  # ElevatorGen
object_id_mapping[[*range(0x56, 0x60)]] = 21  # MovingPlatform
object_id_mapping[[*range(0x60, 0x64)]] = 21  # ScaleLift
object_id_mapping[0x64] = 21  # MovingPlatform


# mapping_minimal  has 527 unmapped indexes (entries still mapping to themselves):
# 1,
# 8–15,
# 24–25,
# 36–37,
# 40–41,
# 50–51,
# 118–123,
# 134–135,
# 140–143,
# 156–159,
# 172–175,
# 184–185,
# 190–193,
# 198–383,
# 392–399,
# 410–413,
# 420–421,
# 430–437,
# 446–447,
# 468–473,
# 484–495,
# 512–767

mapping_compressed = mapping_minimal

background_mapping_minimal = np.full(0x17, 3, dtype=np.uint32)
background_mapping_minimal[0] = 0
background_mapping_minimal[2] = 2
background_mapping_minimal[[5, 9, 10]] = 4
background_mapping_minimal[[4, 11, 19]] = 5
background_mapping_minimal[[6, 20, 22]] = 6
# background_mapping_minimal[[*range(334,342)]] = 7 # Canon


[docs] class GameWrapperSuperMarioBrosDeluxe(PyBoyGameWrapper): """ Wrapper for Super Mario Bros. Deluxe. The optional ``level`` argument to :meth:`start_game` uses the game's unused level selector for normal gameplay. For the original game, levels 0 through 31 are World 1-1 through World 8-4. Set ``super_player_levels=True`` for the For Super Players levels. """ cartridge_title = "MARIO DELUXAHY" mapping_minimal = mapping_minimal mapping_compressed = mapping_compressed object_id_mapping = object_id_mapping def __init__(self, *args, **kwargs): self.world = (0, 0) self.level = 0 self.selected_level = 0 self.selected_super_player_levels = False self.level_selected = False self.coins = 0 self.lives_left = 0 self.score = 0 self.time_left = 0 self.level_progress = 0 self.stuck = False self.stuck_frames = 0 self._stuck_last_progress = 0 self._stuck_in_level = False self.custom_level_sequence = None self.custom_level_sequence_index = -1 self.custom_next_level_prepared = False super().__init__(*args, game_area_section=(0, 6, 20, 26), game_area_follow_scxy=False, **kwargs) self.metatile_interaction_types = None self.mapping = mapping_minimal def post_tick(self): self._tile_cache_invalid = True self._sprite_cache_invalid = True if ( self.custom_level_sequence is not None and self.pyboy.memory[ADDR_MODE] == MODE_OVERWORLD_INIT and not self.custom_next_level_prepared ): if 0 <= self.custom_level_sequence_index < len(self.custom_level_sequence) - 1: next_world, next_level = self.custom_level_sequence[self.custom_level_sequence_index + 1] next_level_id = (next_world - 1) * 4 + next_level - 1 self.pyboy.memory[ADDR_LEVEL_SET] = 0 self.pyboy.memory[ADDR_SUBLEVEL] = next_level_id self.pyboy.memory[ADDR_LEVEL] = next_level_id self.custom_level_sequence_index += 1 self.custom_next_level_prepared = True self.level = self.pyboy.memory[ADDR_LEVEL] self.world = (int(self.level // 4 + 1), int(self.level % 4 + 1)) if self.level < 0x20 else (0, int(self.level)) self.mapping = mapping_minimal self.coins = self.pyboy.memory[ADDR_COINS] self.lives_left = self.pyboy.memory[ADDR_LIVES_LEFT] self.score = bcd_to_dec(int.from_bytes(self.pyboy.memory[ADDR_SCORE : ADDR_SCORE + 3], "little"), byte_width=3) # Deluxe stores the countdown as a little-endian binary integer. self.time_left = int.from_bytes(self.pyboy.memory[ADDR_TIME_LEFT : ADDR_TIME_LEFT + 2], "little") self.level_progress = self.pyboy.memory[ADDR_LEVEL_X] + int.from_bytes( self.pyboy.memory[ADDR_PLAYER_X : ADDR_PLAYER_X + 2], "little" ) in_level = self.pyboy.memory[ADDR_MODE] == MODE_LEVEL if in_level: if not self._stuck_in_level: self._stuck_last_progress = self.level_progress self.stuck_frames = 0 self.stuck = False self._stuck_in_level = True elif abs(self.level_progress - self._stuck_last_progress) > MAX_PROGRESS_DELTA_PER_FRAME: # The camera coordinate wraps at the left boundary; ignore # that impossible one-frame jump when measuring movement. pass elif self.level_progress == self._stuck_last_progress: self.stuck_frames += 1 self.stuck = self.stuck_frames >= STUCK_TIMEOUT_FRAMES else: self._stuck_last_progress = self.level_progress self.stuck_frames = 0 self.stuck = False else: self._stuck_in_level = False self.stuck_frames = 0 self.stuck = False if self.pyboy.memory[ADDR_MODE] == MODE_LEVEL: self.custom_next_level_prepared = False
[docs] def reset_game(self, timer_div=None): PyBoyGameWrapper.reset_game(self, timer_div=timer_div) self.stuck = False self.stuck_frames = 0 self._stuck_last_progress = self.level_progress self._stuck_in_level = self.pyboy.memory[ADDR_MODE] == MODE_LEVEL
[docs] def object_slots(self): """Return active game objects with WRAM data and game-area coordinates.""" objects = [] section_x, section_y, _, _ = self.game_area_section camera_x = self._camera_x() camera_y = self._camera_y() for slot in range(SPRITE_SLOT_COUNT): values = {name: self.pyboy.memory[SPRITE_WRAM_BANK, address + slot] for name, address in SPRITE_SLOT_FIELDS} if values["status"] == 0: continue x = values["x_low"] | values["x_high"] << 8 y = values["y_low"] | values["y_high"] << 8 x_speed = values["x_speed_raw"] y_speed = values["y_speed_raw"] game_area_x = ((x - camera_x + 4) // 8) - section_x game_area_y = ((y + 4) // 8) - section_y screen_x = x - camera_x screen_y = y - camera_y values.update( { "slot": slot, "mapped_id": int(object_id_mapping[values["id"]]), "x": x, "y": y, "game_area_x": game_area_x, "game_area_y": game_area_y, "screen_x": screen_x, "screen_y": screen_y, "x_signed": x - 0x10000 if x & 0x8000 else x, "y_signed": y - 0x10000 if y & 0x8000 else y, "x_speed": x_speed - 0x100 if x_speed & 0x80 else x_speed, "y_speed": y_speed - 0x100 if y_speed & 0x80 else y_speed, } ) objects.append(values) return objects
[docs] def game_area_annotations(self): annotations = [ ( obj["screen_x"], obj["screen_y"], f"s{obj['slot']} i{obj['id']:02X} m{obj['mapped_id']}", ) for obj in self.object_slots() ] player_x = int.from_bytes(self.pyboy.memory[ADDR_PLAYER_X : ADDR_PLAYER_X + 2], "little") player_y = self.pyboy.memory[ADDR_PLAYER_Y] annotations.append( ( player_x - self._camera_x(), player_y - self._camera_y(), "MARIO m1", ) ) return annotations
def _fireball_mapping(self, sprite, objects): if not 92 <= sprite.tile_identifier < 96: return 0 world_x = sprite.x + self._camera_x() world_y = sprite.y + self._camera_y() for obj in objects: if obj["id"] != MARIO_FIREBALL_ID: continue if abs(world_x - obj["x"]) <= 8 and abs(world_y - obj["y"]) <= 8: return 12 if any(FIREBAR_ID_MIN <= obj["id"] <= FIREBAR_ID_MAX for obj in objects): return 18 return 0 def _game_area_tiles(self): """Return interaction types for the camera-relative background map.""" if self._tile_cache_invalid: if self.metatile_interaction_types is None: self.metatile_interaction_types = np.asarray( self.pyboy.memory[ INTERACTION_TABLE_BANK, INTERACTION_TABLE_ADDRESS : INTERACTION_TABLE_ADDRESS + 0x100 ], dtype=np.uint32, ) xx, yy, width, height = self.game_area_section tiles = np.zeros((height, width), dtype=np.uint32) camera_x = self._camera_x() for y in range(height): for x in range(width): world_x = camera_x + (xx + x) * 8 world_y = (yy + y) * 8 metatile_x = world_x // 16 metatile_y = world_y // 16 if not (0 <= metatile_x < METATILE_MAP_WIDTH * 2 and 0 <= metatile_y < METATILE_MAP_HEIGHT): continue screen = metatile_x // METATILE_SCREEN_WIDTH bank = METATILE_MAP_BANK + screen // 16 address = ( METATILE_MAP_ADDRESS + (screen % 16) * METATILE_SCREEN_SIZE + metatile_y * METATILE_SCREEN_WIDTH + metatile_x % METATILE_SCREEN_WIDTH ) metatile = self.pyboy.memory[bank, address] tiles[y, x] = self.metatile_interaction_types[metatile] self._cached_game_area_tiles = tiles self._tile_cache_invalid = False return self._cached_game_area_tiles
[docs] def game_area(self): """ Return the camera-relative background interaction types with sprites overlaid. Background values are the stable interaction codes used by the game, rather than graphics-dependent 8x8 VRAM tile IDs. The codes are defined by ``Tile16_InteractTypes`` in the game's ROM. """ tiles_matrix = self.game_area_background() sprites = self._sprites_on_screen() ambiguous_sprites = [sprite for sprite in sprites if 92 <= sprite.tile_identifier < 96] objects = self.object_slots() if ambiguous_sprites else () xx, yy, width, height = self.game_area_section camera_y = self._camera_y() for sprite in sprites: x = ((sprite.x + 4) // 8) - xx y = ((sprite.y + camera_y + 4) // 8) - yy tile_identifier = sprite.tile_identifier fireball_mapping = self._fireball_mapping(sprite, objects) sprite_mapping = fireball_mapping or self.mapping[tile_identifier] sprite_value = sprite_mapping + self.sprite_offset if 0 <= x < width and 0 <= y < height and self.mapping[tile_identifier] != 0: tiles_matrix[y, x] = sprite_value if len(sprite.tiles) == 2 and 0 <= x < width and 0 <= y + 1 < height: second_mapping = fireball_mapping or self.mapping[tile_identifier + 1] if second_mapping != 0: tiles_matrix[y + 1, x] = second_mapping + self.sprite_offset return tiles_matrix
[docs] def game_area_background(self): """Return the mapped background without sprite overlays.""" return background_mapping_minimal[np.asarray(self._game_area_tiles(), dtype=np.uint32)]
def _camera_x(self): return int.from_bytes(self.pyboy.memory[ADDR_CAMERA_X_LOW : ADDR_CAMERA_X_HIGH + 1], "little") def _camera_y(self): return self.pyboy.memory[ADDR_CAMERA_Y] def _press(self, button): self.pyboy.button_press(button) for _ in range(20): self.pyboy.tick(1, False) self.pyboy.button_release(button) def _wait_for_mode(self, *modes, spam_start=False, settle_frames=20): while self.pyboy.memory[ADDR_MODE] not in modes: if spam_start: self.pyboy.button("start") self.pyboy.tick(4, False) if settle_frames: self.pyboy.tick(settle_frames, False)
[docs] def set_lives_left(self, amount): """Set Mario's lives to a value between 0 and 99.""" if not 0 <= amount <= 99: raise PyBoyInvalidInputException(f"{amount} is out of bounds. Only values between 0 and 99 allowed.") self.pyboy.memory[ADDR_LIVES_LEFT] = amount
[docs] def set_time_left(self, time): """Set the level timer to a value between 0 and 999.""" if not 0 <= time <= 999: raise PyBoyInvalidInputException(f"{time} is out of bounds. Only values between 0 and 999 allowed.") self.pyboy.memory[ADDR_TIME_LEFT : ADDR_TIME_LEFT + 2] = time.to_bytes(2, "little")
def _set_level(self, level, super_player_levels=False): """ Select a level for the game's debug level selector. ``super_player_levels=False`` selects Super Mario Bros.; ``super_player_levels=True`` selects For Super Players. """ if not 0 <= level <= 0x63: raise PyBoyInvalidInputException(f"{level} is out of bounds. Only values between 0 and 99 allowed.") if not isinstance(super_player_levels, bool): raise PyBoyInvalidInputException("super_player_levels must be a boolean.") self.pyboy.memory[ADDR_LEVEL_SET] = int(super_player_levels) self.pyboy.memory[ADDR_SUBLEVEL] = level self.pyboy.memory[ADDR_LEVEL] = level self.selected_level = level self.selected_super_player_levels = super_player_levels self.level_selected = True
[docs] def set_world_level(self, world, level, super_player_levels=False): """ Select a world and level to start from. World and level numbers are one-based, matching the game's display. The standard game has worlds 1-8 with four levels each. """ if not 1 <= world <= 13: raise PyBoyInvalidInputException(f"{world} is out of bounds. Only worlds 1 through 13 are allowed.") if not 1 <= level <= 4: raise PyBoyInvalidInputException(f"{level} is out of bounds. Only levels 1 through 4 are allowed.") if not super_player_levels and world > 8: raise PyBoyInvalidInputException("Worlds 9 through 13 are only available in the For Super Players set.") self._set_level((world - 1) * 4 + level - 1, super_player_levels)
[docs] def start_game( self, timer_div=None, world_level=None, level=None, super_player_levels=False, challenge=False, unlock_level_select=False, custom_level_sequence=None, ): """ Start a game from the title screen. ``world_level`` selects a one-based ``(world, level)`` tuple, matching Super Mario Land. ``level`` remains available for selecting a raw Deluxe level ID. Normal gameplay is the default; ``challenge=True`` uses the game's Challenge selector. ``unlock_level_select=True`` is retained as an alias for entering the Challenge selector without launching a level. ``custom_level_sequence`` optionally overrides the normal level order with a sequence of one-based ``(world, level)`` tuples. """ if self.game_has_started: raise PyBoyException("Gamewrapper already started! Use 'reset' instead.") if world_level is not None and level is not None: raise PyBoyInvalidInputException("Specify either world_level or level, not both.") if custom_level_sequence is not None: normalized_sequence = [] for sequence_world_level in custom_level_sequence: normalized_sequence.append(tuple(sequence_world_level)) custom_level_sequence = tuple(normalized_sequence) if not custom_level_sequence: raise PyBoyInvalidInputException("custom_level_sequence must not be empty.") for sequence_world_level in custom_level_sequence: if len(sequence_world_level) != 2: raise PyBoyInvalidInputException("custom_level_sequence entries must be (world, level) tuples.") sequence_world, sequence_level = sequence_world_level if not 1 <= sequence_world <= 13: raise PyBoyInvalidInputException( f"{sequence_world} is out of bounds. Only worlds 1 through 13 are allowed." ) if not 1 <= sequence_level <= 4: raise PyBoyInvalidInputException( f"{sequence_level} is out of bounds. Only levels 1 through 4 are allowed." ) if not super_player_levels and sequence_world > 8: raise PyBoyInvalidInputException( "Worlds 9 through 13 are only available in the For Super Players set." ) self.custom_level_sequence = custom_level_sequence challenge = challenge or unlock_level_select if world_level is not None: self.set_world_level(*world_level, super_player_levels=super_player_levels) elif level is not None: self._set_level(level, super_player_levels) self._wait_for_mode(MODE_TITLE_SCREEN, spam_start=True) self._wait_for_mode(MODE_MAIN_MENU, spam_start=True) if challenge: self.pyboy.memory[ADDR_MENU_SELECTION] = 1 self._press("a") self._wait_for_mode(MODE_CHALLENGE_SELECT) self.pyboy.memory[ADDR_CHALLENGE_UNLOCK] = 1 if self.level_selected and not unlock_level_select: self._set_level(self.selected_level, self.selected_super_player_levels) self._press("a") self._wait_for_mode(MODE_WORLD_MAP) self._press("start") self._wait_for_mode(MODE_LEVEL) else: if self.level_selected: self.pyboy.memory[ADDR_MODE] = MODE_DEBUG_INIT self._wait_for_mode(MODE_DEBUG_MENU) self.pyboy.memory[ADDR_LEVEL_SET] = int(self.selected_super_player_levels) self.pyboy.memory[ADDR_SUBLEVEL] = self.selected_level self._press("a") else: self._press("a") self._wait_for_mode(MODE_FILE_SELECT) self._press("a") self._wait_for_mode(MODE_WORLD_MAP) self._press("start") self._wait_for_mode(MODE_LEVEL) PyBoyGameWrapper.start_game(self, timer_div=timer_div) self.custom_level_sequence_index = -1 if self.custom_level_sequence is not None and not challenge and not super_player_levels: current_world_level = ( int(self.pyboy.memory[ADDR_LEVEL] // 4 + 1), int(self.pyboy.memory[ADDR_LEVEL] % 4 + 1), ) if current_world_level in self.custom_level_sequence: self.custom_level_sequence_index = self.custom_level_sequence.index(current_world_level) self.custom_next_level_prepared = False
[docs] def game_over(self): return self.pyboy.memory[ADDR_PLAYER_STATE] == 3 or self.pyboy.memory[ADDR_MODE] in ( 0x0E, 0x10, 0x11, 0x14, 0x1B, )
def __repr__(self): return ( f"Super Mario Bros. Deluxe: World {'-'.join(str(i) for i in self.world)}\n" f"Coins: {self.coins}\n" f"Lives left: {self.lives_left}\n" f"Score: {self.score}\n" f"Time left: {self.time_left}\n" f"Level progress: {self.level_progress}\n" + super().__repr__() )