diff --git a/pyboy/plugins/game_wrapper_castlevania_the_adventure.pxd b/pyboy/plugins/game_wrapper_castlevania_the_adventure.pxd new file mode 100644 index 000000000..cec6ae0e0 --- /dev/null +++ b/pyboy/plugins/game_wrapper_castlevania_the_adventure.pxd @@ -0,0 +1,20 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# +cimport cython +from libc.stdint cimport uint8_t + +from pyboy.logging.logging cimport Logger +from pyboy.plugins.base_plugin cimport PyBoyGameWrapper + + +cdef Logger logger + +cdef class GameWrapperCastlevaniaTheAdventure(PyBoyGameWrapper): + cdef readonly int level_score + cdef readonly int time_left + cdef readonly int lives_left + cdef readonly int health + cdef readonly int whipe_level + cdef readonly int invincible_timer diff --git a/pyboy/plugins/game_wrapper_castlevania_the_adventure.py b/pyboy/plugins/game_wrapper_castlevania_the_adventure.py new file mode 100644 index 000000000..ebbcf6df7 --- /dev/null +++ b/pyboy/plugins/game_wrapper_castlevania_the_adventure.py @@ -0,0 +1,122 @@ +# +# License: See LICENSE.md file +# GitHub: https://github.com/Baekalfen/PyBoy +# + +__pdoc__ = { + "GameWrapperCastlevaniaTheAdventure.cartridge_title": False, + "GameWrapperCastlevaniaTheAdventure.post_tick": False, +} + +import numpy as np + +import pyboy +from pyboy.utils import bcd_to_dec + +from .base_plugin import PyBoyGameWrapper + +logger = pyboy.logging.get_logger(__name__) + +ADDR_LEVEL_SCORE = 0xC034 # BCD +LEVEL_SCORE_BYTE_WIDTH = 3 +MAX_LEVEL_SCORE = 99999 + +ADDR_TIME_LEFT_SECONDS = 0xC436 # BCD +ADDR_TIME_LEFT_MINUTES = 0xC437 # BCD + +ADDR_LIVES_LEFT = 0xC040 # BCD +MAX_LIVES = 0x99 + +ADDR_HEALTH = 0xC519 +MAX_HEALTH = 10 + +ADDR_WHIPE_LEVEL = 0xC51C # 00, 01 or 02 +ADDR_WHIPE_THROW_BULLET = 0xC51D # 00 False, 0x80 True (if ADDR_WHIPE_LEVEL >= 02) +MAX_WHIPE_LEVEL = 2 + +ADDR_INVINCIBLE_TIMER = 0xC02C + + +class GameWrapperCastlevaniaTheAdventure(PyBoyGameWrapper): + """ + This class wraps Castlevania The Adventure, and provides easy access for AIs. + + If you call `print` on an instance of this object, it will show an overview of everything this object provides. + """ + + cartridge_title = "CASTLEVANIA AD" + + def __init__(self, *args, **kwargs): + self.level_score = 0 + """The level score provided by the game""" + self.time_left = 0 + """Time remaining (in seconds) provided by the game""" + self.lives_left = 0 + """The lives remaining provided by the game""" + self.health = 0 + """The health provided by the game""" + self.whipe_level = 0 + """The whipe level provided by the game. Can be 0, 1 or 2.""" + self.invincible_timer = 0 + """The timer for invincible mode provided by the game. Player is invincible if timer is higher than 0""" + + super().__init__(*args, game_area_section=(0, 0, 19, 16), game_area_follow_scxy=True, **kwargs) + + def post_tick(self): + self._tile_cache_invalid = True + self._sprite_cache_invalid = True + + self.level_score = bcd_to_dec( + int.from_bytes(self.pyboy.memory[ADDR_LEVEL_SCORE:ADDR_LEVEL_SCORE + LEVEL_SCORE_BYTE_WIDTH], "little"), + byte_width=LEVEL_SCORE_BYTE_WIDTH + ) + self.time_left = ( + bcd_to_dec(self.pyboy.memory[ADDR_TIME_LEFT_SECONDS]) + + bcd_to_dec(self.pyboy.memory[ADDR_TIME_LEFT_MINUTES]) * 60 + ) + self.lives_left = bcd_to_dec(self.pyboy.memory[ADDR_LIVES_LEFT]) + self.whipe_level = self.pyboy.memory[ADDR_WHIPE_LEVEL] + self.invincible_timer = self.pyboy.memory[ADDR_INVINCIBLE_TIMER] + self.health = self.pyboy.memory[ADDR_HEALTH] + + def start_game(self, timer_div=None): + """ + Call this function right after initializing PyBoy. This will navigate through menus to start the game at the + first playable state. + + The state of the emulator is saved, and using `reset_game`, you can get back to this point of the game + instantly. + + Kwargs: + * timer_div (int): Replace timer's DIV register with this value. Use `None` to randomize. + """ + + # Boot screen + while True: + self.pyboy.tick(1, False) + if self.tilemap_background[1, 6:10] == [256, 327, 334, 256]: # 'KONAMI' logo on the first screen + break + + self.pyboy.button("start") # Skip brand + self.pyboy.tick(7, False) # Wait for transition to finish (start screen) + self.pyboy.button("start") # Start level + self.pyboy.tick(300, False) # Skip level transition + + PyBoyGameWrapper.start_game(self, timer_div=timer_div) + + def game_over(self): + return self.health == 0 + + def __repr__(self): + # yapf: disable + return ( + f"Castlevania The Adventure:\n\n" + + f"Score: {self.level_score}\n" + + f"Time left: {self.time_left}\n" + + f"Lives left: {self.lives_left}\n" + + f"Health: {self.health}\n" + + f"Whipe level: {self.whipe_level}\n"+ + f"Invincible timer: {self.invincible_timer}\n" + + super().__repr__() + ) + # yapf: enable diff --git a/pyboy/plugins/manager.pxd b/pyboy/plugins/manager.pxd index a770eedf7..98167ee08 100644 --- a/pyboy/plugins/manager.pxd +++ b/pyboy/plugins/manager.pxd @@ -23,6 +23,7 @@ from pyboy.plugins.game_wrapper_tetris cimport GameWrapperTetris from pyboy.plugins.game_wrapper_kirby_dream_land cimport GameWrapperKirbyDreamLand from pyboy.plugins.game_wrapper_pokemon_gen1 cimport GameWrapperPokemonGen1 from pyboy.plugins.game_wrapper_pokemon_pinball cimport GameWrapperPokemonPinball +from pyboy.plugins.game_wrapper_castlevania_the_adventure cimport GameWrapperCastlevaniaTheAdventure # imports end @@ -48,6 +49,7 @@ cdef class PluginManager: cdef public GameWrapperKirbyDreamLand game_wrapper_kirby_dream_land cdef public GameWrapperPokemonGen1 game_wrapper_pokemon_gen1 cdef public GameWrapperPokemonPinball game_wrapper_pokemon_pinball + cdef public GameWrapperCastlevaniaTheAdventure game_wrapper_castlevania_the_adventure cdef bint window_sdl2_enabled cdef bint window_open_gl_enabled cdef bint window_null_enabled @@ -64,6 +66,7 @@ cdef class PluginManager: cdef bint game_wrapper_kirby_dream_land_enabled cdef bint game_wrapper_pokemon_gen1_enabled cdef bint game_wrapper_pokemon_pinball_enabled + cdef bint game_wrapper_castlevania_the_adventure_enabled # plugin_cdef end cdef list handle_events(self, list) noexcept diff --git a/pyboy/plugins/manager.py b/pyboy/plugins/manager.py index df279c8da..2df33702e 100644 --- a/pyboy/plugins/manager.py +++ b/pyboy/plugins/manager.py @@ -22,6 +22,7 @@ from pyboy.plugins.game_wrapper_kirby_dream_land import GameWrapperKirbyDreamLand # isort:skip from pyboy.plugins.game_wrapper_pokemon_gen1 import GameWrapperPokemonGen1 # isort:skip from pyboy.plugins.game_wrapper_pokemon_pinball import GameWrapperPokemonPinball # isort:skip +from pyboy.plugins.game_wrapper_castlevania_the_adventure import GameWrapperCastlevaniaTheAdventure # isort:skip # imports end @@ -43,6 +44,7 @@ def parser_arguments(): yield GameWrapperKirbyDreamLand.argv yield GameWrapperPokemonGen1.argv yield GameWrapperPokemonPinball.argv + yield GameWrapperCastlevaniaTheAdventure.argv # yield_plugins end pass @@ -86,6 +88,8 @@ def __init__(self, pyboy, mb, pyboy_argv): self.game_wrapper_pokemon_gen1_enabled = self.game_wrapper_pokemon_gen1.enabled() self.game_wrapper_pokemon_pinball = GameWrapperPokemonPinball(pyboy, mb, pyboy_argv) self.game_wrapper_pokemon_pinball_enabled = self.game_wrapper_pokemon_pinball.enabled() + self.game_wrapper_castlevania_the_adventure = GameWrapperCastlevaniaTheAdventure(pyboy, mb, pyboy_argv) + self.game_wrapper_castlevania_the_adventure_enabled = self.game_wrapper_castlevania_the_adventure.enabled() # plugins_enabled end def gamewrapper(self): @@ -95,6 +99,7 @@ def gamewrapper(self): if self.game_wrapper_kirby_dream_land_enabled: return self.game_wrapper_kirby_dream_land if self.game_wrapper_pokemon_gen1_enabled: return self.game_wrapper_pokemon_gen1 if self.game_wrapper_pokemon_pinball_enabled: return self.game_wrapper_pokemon_pinball + if self.game_wrapper_castlevania_the_adventure_enabled: return self.game_wrapper_castlevania_the_adventure # gamewrapper end self.generic_game_wrapper_enabled = True return self.generic_game_wrapper @@ -135,6 +140,8 @@ def handle_events(self, events): events = self.game_wrapper_pokemon_gen1.handle_events(events) if self.game_wrapper_pokemon_pinball_enabled: events = self.game_wrapper_pokemon_pinball.handle_events(events) + if self.game_wrapper_castlevania_the_adventure_enabled: + events = self.game_wrapper_castlevania_the_adventure.handle_events(events) # foreach end if self.generic_game_wrapper_enabled: events = self.generic_game_wrapper.handle_events(events) @@ -166,6 +173,8 @@ def post_tick(self): self.game_wrapper_pokemon_gen1.post_tick() if self.game_wrapper_pokemon_pinball_enabled: self.game_wrapper_pokemon_pinball.post_tick() + if self.game_wrapper_castlevania_the_adventure_enabled: + self.game_wrapper_castlevania_the_adventure.post_tick() # foreach end if self.generic_game_wrapper_enabled: self.generic_game_wrapper.post_tick() @@ -253,6 +262,8 @@ def window_title(self): title += self.game_wrapper_pokemon_gen1.window_title() if self.game_wrapper_pokemon_pinball_enabled: title += self.game_wrapper_pokemon_pinball.window_title() + if self.game_wrapper_castlevania_the_adventure_enabled: + title += self.game_wrapper_castlevania_the_adventure.window_title() # foreach end return title @@ -292,6 +303,8 @@ def stop(self): self.game_wrapper_pokemon_gen1.stop() if self.game_wrapper_pokemon_pinball_enabled: self.game_wrapper_pokemon_pinball.stop() + if self.game_wrapper_castlevania_the_adventure_enabled: + self.game_wrapper_castlevania_the_adventure.stop() # foreach end if self.generic_game_wrapper_enabled: self.generic_game_wrapper.stop() diff --git a/pyboy/plugins/manager_gen.py b/pyboy/plugins/manager_gen.py index 93fef23f6..6e8893ee9 100644 --- a/pyboy/plugins/manager_gen.py +++ b/pyboy/plugins/manager_gen.py @@ -9,7 +9,7 @@ windows = ["WindowSDL2", "WindowOpenGL", "WindowNull", "Debug"] game_wrappers = [ "GameWrapperSuperMarioLand", "GameWrapperTetris", "GameWrapperKirbyDreamLand", "GameWrapperPokemonGen1", - "GameWrapperPokemonPinball" + "GameWrapperPokemonPinball", "GameWrapperCastlevaniaTheAdventure" ] plugins = [ "DisableInput", "AutoPause", "RecordReplay", "Rewind", "ScreenRecorder", "ScreenshotRecorder", "DebugPrompt"