diff options
Diffstat (limited to 'pokemontools')
| -rw-r--r-- | pokemontools/vba/autoplayer.py | 820 | ||||
| -rw-r--r-- | pokemontools/vba/battle.py | 188 | ||||
| -rw-r--r-- | pokemontools/vba/vba.py | 601 | 
3 files changed, 1021 insertions, 588 deletions
| diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 9aa8f4a..af14d47 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -4,492 +4,580 @@ Programmatic speedrun of Pokémon Crystal  """  import os -# bring in the emulator and basic tools -import vba - -def main(): -    """ -    Start the game. -    """ -    vba.load_rom() +import pokemontools.configuration as configuration -    # get past the opening sequence -    skip_intro() - -    # walk to mom and handle her text -    handle_mom() - -    # walk outside into new bark town -    walk_into_new_bark_town() - -    # walk to elm and do whatever he wants -    handle_elm("totodile") - -    new_bark_level_grind(10, skip=False) +# bring in the emulator and basic tools +import vba as _vba  def skippable(func):      """      Makes a function skippable. -    Saves the state before and after the function runs. -    Pass "skip=True" to the function to load the previous save -    state from when the function finished. +    Saves the state before and after the function runs. Pass "skip=True" to the +    function to load the previous save state from when the function finished.      """      def wrapped_function(*args, **kwargs): +        self = args[0]          skip = True +        override = True          if "skip" in kwargs.keys():              skip = kwargs["skip"]              del kwargs["skip"] +        if "override" in kwargs.keys(): +            override = kwargs["override"] +            del kwargs["override"] +          # override skip if there's no save          if skip:              full_name = func.__name__ + "-end.sav" -            if not os.path.exists(os.path.join(vba.save_state_path, full_name)): +            if not os.path.exists(os.path.join(self.config.save_state_path, full_name)):                  skip = False          return_value = None          if not skip: -            vba.save_state(func.__name__ + "-start", override=True) +            if override: +                self.cry.save_state(func.__name__ + "-start", override=override) +              return_value = func(*args, **kwargs) -            vba.save_state(func.__name__ + "-end", override=True) + +            if override: +                self.cry.save_state(func.__name__ + "-end", override=override)          elif skip: -            vba.set_state(vba.load_state(func.__name__ + "-end")) +            self.cry.vba.state = self.cry.load_state(func.__name__ + "-end")          return return_value      return wrapped_function -@skippable -def skip_intro(): +class Runner(object):      """ -    Skip the game boot intro sequence. +    ``Runner`` is used to represent a set of functions that control an instance +    of the emulator. This allows for automated runs of games.      """ +    pass -    # copyright sequence -    vba.nstep(400) +class SpeedRunner(Runner): +    def __init__(self, cry=None, config=None): +        super(SpeedRunner, self).__init__() -    # skip the ditto sequence -    vba.press("a") -    vba.nstep(100) +        self.cry = cry -    # skip the start screen -    vba.press("start") -    vba.nstep(100) +        if not config: +            config = configuration.Config() -    # click "new game" -    vba.press("a", holdsteps=50, aftersteps=1) +        self.config = config -    # skip text up to "Are you a boy? Or are you a girl?" -    vba.crystal.text_wait() +    def setup(self): +        """ +        Configure this ``Runner`` instance to contain a reference to an active +        emulator session. +        """ +        if not self.cry: +            self.cry = _vba.crystal(config=self.config) -    # select "Boy" -    vba.press("a", holdsteps=50, aftersteps=1) +    def main(self): +        """ +        Main entry point for complete control of the game as the main player. +        """ +        # get past the opening sequence +        self.skip_intro(skip=True) -    # text until "What time is it?" -    vba.crystal.text_wait() +        # walk to mom and handle her text +        self.handle_mom(skip=True) -    # select 10 o'clock -    vba.press("a", holdsteps=50, aftersteps=1) +        # walk outside into new bark town +        self.walk_into_new_bark_town(skip=True) -    # yes i mean it -    vba.press("a", holdsteps=50, aftersteps=1) +        # walk to elm and do whatever he wants +        self.handle_elm("totodile", skip=True) -    # "How many minutes?" 0 min. -    vba.press("a", holdsteps=50, aftersteps=1) +        self.new_bark_level_grind(17, skip=False) -    # "Who! 0 min.?" yes/no select yes -    vba.press("a", holdsteps=50, aftersteps=1) +    @skippable +    def skip_intro(self, stop_at_name_selection=False): +        """ +        Skip the game boot intro sequence. +        """ -    # read text until name selection -    vba.crystal.text_wait() +        # copyright sequence +        self.cry.nstep(400) -    # select "Chris" -    vba.press("d", holdsteps=10, aftersteps=1) -    vba.press("a", holdsteps=50, aftersteps=1) +        # skip the ditto sequence +        self.cry.vba.press("a") +        self.cry.nstep(100) -    def overworldcheck(): -        """ -        A basic check for when the game starts. -        """ -        return vba.get_memory_at(0xcfb1) != 0 +        # skip the start screen +        self.cry.vba.press("start") +        self.cry.nstep(100) -    # go until the introduction is done -    vba.crystal.text_wait(callback=overworldcheck) +        # click "new game" +        self.cry.vba.press("a", hold=50, after=1) -    return +        # skip text up to "Are you a boy? Or are you a girl?" +        self.cry.text_wait() -@skippable -def handle_mom(): -    """ -    Walk to mom. Handle her speech and questions. -    """ +        # select "Boy" +        self.cry.vba.press("a", hold=50, after=1) -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") +        # text until "What time is it?" +        self.cry.text_wait() -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") +        # select 10 o'clock +        self.cry.vba.press("a", hold=50, after=1) -    vba.crystal.move("d") -    vba.crystal.move("d") +        # yes i mean it +        self.cry.vba.press("a", hold=50, after=1) -    # move into mom's line of sight -    vba.crystal.move("d") +        # "How many minutes?" 0 min. +        self.cry.vba.press("a", hold=50, after=1) -    # let mom talk until "What day is it?" -    vba.crystal.text_wait() +        # "Who! 0 min.?" yes/no select yes +        self.cry.vba.press("a", hold=50, after=1) -    # "What day is it?" Sunday -    vba.press("a", holdsteps=10) # Sunday +        # read text until name selection +        self.cry.text_wait() -    vba.crystal.text_wait() +        if stop_at_name_selection: +            return -    # "SUNDAY, is it?" yes/no -    vba.press("a", holdsteps=10) # yes +        # select "Chris" +        self.cry.vba.press("d", hold=10, after=1) +        self.cry.vba.press("a", hold=50, after=1) -    vba.crystal.text_wait() +        def overworldcheck(): +            """ +            A basic check for when the game starts. +            """ +            return self.cry.vba.memory[0xcfb1] != 0 -    # "Is it Daylight Saving Time now?" yes/no -    vba.press("a", holdsteps=10) # yes +        # go until the introduction is done +        self.cry.text_wait(callback=overworldcheck) -    vba.crystal.text_wait() +        return -    # "AM DST, is that OK?" yes/no -    vba.press("a", holdsteps=10) # yes +    @skippable +    def handle_mom(self): +        """ +        Walk to mom. Handle her speech and questions. +        """ -    # text until "know how to use the PHONE?" yes/no -    vba.crystal.text_wait() +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") -    # press yes -    vba.press("a", holdsteps=10) +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") -    # wait until mom is done talking -    vba.crystal.text_wait() +        self.cry.move("d") +        self.cry.move("d") -    # wait until the script is done running -    vba.crystal.wait_for_script_running() +        # move into mom's line of sight +        self.cry.move("d") -    return +        # let mom talk until "What day is it?" +        self.cry.text_wait() -@skippable -def walk_into_new_bark_town(): -    """ -    Walk outside after talking with mom. -    """ +        # "What day is it?" Sunday +        self.cry.vba.press("a", hold=10) # Sunday -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("l") -    vba.crystal.move("l") +        self.cry.text_wait() -    # walk outside -    vba.crystal.move("d") +        # "SUNDAY, is it?" yes/no +        self.cry.vba.press("a", hold=10) # yes -@skippable -def handle_elm(starter_choice): -    """ -    Walk to Elm's Lab and get a starter. -    """ +        self.cry.text_wait() -    # walk to the lab -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("u") -    vba.crystal.move("u") +        # "Is it Daylight Saving Time now?" yes/no +        self.cry.vba.press("a", hold=10) # yes -    # walk into the lab -    vba.crystal.move("u") +        self.cry.text_wait() -    # talk to elm -    vba.crystal.text_wait() +        # "AM DST, is that OK?" yes/no +        self.cry.vba.press("a", hold=10) # yes -    # "that I recently caught." yes/no -    vba.press("a", holdsteps=10) # yes +        # text until "know how to use the PHONE?" yes/no +        self.cry.text_wait() -    # talk to elm some more -    vba.crystal.text_wait() +        # press yes +        self.cry.vba.press("a", hold=10) -    # talking isn't done yet.. -    vba.crystal.text_wait() -    vba.crystal.text_wait() -    vba.crystal.text_wait() +        # wait until mom is done talking +        self.cry.text_wait() -    # wait until the script is done running -    vba.crystal.wait_for_script_running() +        # wait until the script is done running +        self.cry.wait_for_script_running() -    # move toward the pokeballs -    vba.crystal.move("r") +        return -    # move to cyndaquil -    vba.crystal.move("r") +    @skippable +    def walk_into_new_bark_town(self): +        """ +        Walk outside after talking with mom. +        """ -    moves = 0 +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("l") +        self.cry.move("l") + +        # walk outside +        self.cry.move("d") + +    @skippable +    def handle_elm(self, starter_choice): +        """ +        Walk to Elm's Lab and get a starter. +        """ + +        # walk to the lab +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("u") +        self.cry.move("u") + +        # walk into the lab +        self.cry.move("u") + +        # talk to elm +        self.cry.text_wait() + +        # "that I recently caught." yes/no +        self.cry.vba.press("a", hold=10) # yes + +        # talk to elm some more +        self.cry.text_wait() + +        # talking isn't done yet.. +        self.cry.text_wait() +        self.cry.text_wait() +        self.cry.text_wait() + +        # wait until the script is done running +        self.cry.wait_for_script_running() + +        # move toward the pokeballs +        self.cry.move("r") + +        # move to cyndaquil +        self.cry.move("r") -    if starter_choice.lower() == "cyndaquil":          moves = 0 -    if starter_choice.lower() == "totodile": -        moves = 1 -    else: -        moves = 2 -    for each in range(0, moves): -        vba.crystal.move("r") +        if starter_choice.lower() == "cyndaquil": +            moves = 0 +        elif starter_choice.lower() == "totodile": +            moves = 1 +        else: +            moves = 2 -    # face the pokeball -    vba.crystal.move("u") +        for each in range(0, moves): +            self.cry.move("r") -    # select it -    vba.press("a", holdsteps=10, aftersteps=0) +        # face the pokeball +        self.cry.move("u") -    # wait for the image to pop up -    vba.crystal.text_wait() +        # select it +        self.cry.vba.press("a", hold=10, after=0) -    # wait for the image to close -    vba.crystal.text_wait() +        # wait for the image to pop up +        self.cry.text_wait() -    # wait for the yes/no box -    vba.crystal.text_wait() +        # wait for the image to close +        self.cry.text_wait() -    # press yes -    vba.press("a", holdsteps=10, aftersteps=0) +        # wait for the yes/no box +        self.cry.text_wait() -    # wait for elm to talk a bit -    vba.crystal.text_wait() +        # press yes +        self.cry.vba.press("a", hold=10, after=0) -    # TODO: why didn't that finish his talking? -    vba.crystal.text_wait() +        # wait for elm to talk a bit +        self.cry.text_wait() -    # give a nickname? yes/no -    vba.press("d", holdsteps=10, aftersteps=0) # move to "no" -    vba.press("a", holdsteps=10, aftersteps=0) # no +        # TODO: why didn't that finish his talking? +        self.cry.text_wait() -    # TODO: why didn't this wait until he was completely done? -    vba.crystal.text_wait() -    vba.crystal.text_wait() +        # give a nickname? yes/no +        self.cry.vba.press("d", hold=10, after=0) # move to "no" +        self.cry.vba.press("a", hold=10, after=0) # no -    # get the phone number -    vba.crystal.text_wait() +        # TODO: why didn't this wait until he was completely done? +        self.cry.text_wait() +        self.cry.text_wait() -    # talk with elm a bit more -    vba.crystal.text_wait() +        # get the phone number +        self.cry.text_wait() -    # TODO: and again.. wtf? -    vba.crystal.text_wait() +        # talk with elm a bit more +        self.cry.text_wait() -    # wait until the script is done running -    vba.crystal.wait_for_script_running() +        # wait until the script is done running +        self.cry.wait_for_script_running() -    # move down -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") +        # move down +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") -    # move into the researcher's line of sight -    vba.crystal.move("d") +        # move into the researcher's line of sight +        self.cry.move("d") -    # get the potion from the person -    vba.crystal.text_wait() -    vba.crystal.text_wait() +        # get the potion from the person +        self.cry.text_wait() +        self.cry.text_wait() -    # wait for the script to end -    vba.crystal.wait_for_script_running() +        # wait for the script to end +        self.cry.wait_for_script_running() -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") -    # go outside -    vba.crystal.move("d") +        # go outside +        self.cry.move("d") -    return +        return -@skippable -def new_bark_level_grind(level): -    """ -    Do level grinding in New Bark. +    @skippable +    def new_bark_level_grind(self, level, walk_to_grass=True): +        """ +        Do level grinding in New Bark. -    Starting just outside of Elm's Lab, do some level grinding until the first -    partymon level is equal to the given value.. -    """ +        Starting just outside of Elm's Lab, do some level grinding until the +        first partymon level is equal to the given value.. +        """ -    # walk to the grass area -    new_bark_level_grind_walk_to_grass(skip=False) +        # walk to the grass area +        if walk_to_grass: +            self.new_bark_level_grind_walk_to_grass(skip=False) -    # TODO: walk around in grass, handle battles -    walk = ["d", "d", "u", "d", "u", "d"] -    for direction in walk: -        vba.crystal.move(direction) +        last_direction = "u" -    # wait for wild battle to completely start -    vba.crystal.text_wait() +        # walk around in the grass until a battle happens +        while self.cry.vba.memory[0xd22d] == 0: +            if last_direction == "u": +                direction = "d" +            else: +                direction = "u" -    attacks = 5 +            self.cry.move(direction) -    while attacks > 0: -        # FIGHT -        vba.press("a", holdsteps=10, aftersteps=1) +            last_direction = direction -        # wait to select a move -        vba.crystal.text_wait() +        # wait for wild battle to completely start +        self.cry.text_wait() -        # SCRATCH -        vba.press("a", holdsteps=10, aftersteps=1) +        attacks = 5 -        # wait for the move to be over -        vba.crystal.text_wait() +        while attacks > 0: +            # FIGHT +            self.cry.vba.press("a", hold=10, after=1) -        hp = ((vba.get_memory_at(0xd218) << 8) | vba.get_memory_at(0xd217)) -        print "enemy hp is: " + str(hp) +            # wait to select a move +            self.cry.text_wait() -        if hp == 0: -            print "enemy hp is zero, exiting" -            break -        else: +            # SCRATCH +            self.cry.vba.press("a", hold=10, after=1) + +            # wait for the move to be over +            self.cry.text_wait() + +            hp = self.cry.get_enemy_hp()              print "enemy hp is: " + str(hp) -        attacks = attacks - 1 - -    while vba.get_memory_at(0xd22d) != 0: -        vba.press("a", holdsteps=10, aftersteps=1) - -    # wait for the map to finish loading -    vba.nstep(50) - -    print "okay, back in the overworld" - -    # move up -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") - -    # move into new bark town -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") - -    # move up -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") - -    # move to the door -    vba.crystal.move("r") -    vba.crystal.move("r") -    vba.crystal.move("r") - -    # walk in -    vba.crystal.move("u") - -    # move up to the healing thing -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("u") -    vba.crystal.move("l") -    vba.crystal.move("l") - -    # face it -    vba.crystal.move("u") - -    # interact -    vba.press("a", holdsteps=10, aftersteps=1) - -    # wait for yes/no box -    vba.crystal.text_wait() - -    # press yes -    vba.press("a", holdsteps=10, aftersteps=1) - -    # TODO: when is healing done? - -    # wait until the script is done running -    vba.crystal.wait_for_script_running() - -    # wait for it to be really really done -    vba.nstep(50) - -    vba.crystal.move("r") -    vba.crystal.move("r") - -    # move to the door -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") - -    # walk out -    vba.crystal.move("d") - -    # check partymon1 level -    if vba.get_memory_at(0xdcfe) < level: -        new_bark_level_grind(level, skip=False) -    else: -        return +            if hp == 0: +                print "enemy hp is zero, exiting" +                break +            else: +                print "enemy hp is: " + str(hp) + +            attacks = attacks - 1 + +        while self.cry.vba.memory[0xd22d] != 0: +            self.cry.vba.press("a", hold=10, after=1) + +        # wait for the map to finish loading +        self.cry.vba.step(count=50) + +        # This is used to handle any additional textbox that might be up on the +        # screen. The debug parameter is set to True so that max_wait is +        # enabled. This might be a textbox that is still waiting around because +        # of some faint during the battle. I am not completely sure why this +        # happens. +        self.cry.text_wait(max_wait=30, debug=True) + +        print "okay, back in the overworld" + +        cur_hp = ((self.cry.vba.memory[0xdd01] << 8) | self.cry.vba.memory[0xdd02]) +        move_pp = self.cry.vba.memory[0xdcf6] # move 1 pp + +        # if pokemon health is >20, just continue +        # if move 1 PP is 0, just continue +        if cur_hp > 20 and move_pp > 5 and self.cry.vba.memory[0xdcfe] < level: +            self.cry.move("u") +            return self.new_bark_level_grind(level, walk_to_grass=False, skip=False) + +        # move up +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") + +        # move into new bark town +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") + +        # move up +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") + +        # move to the door +        self.cry.move("r") +        self.cry.move("r") +        self.cry.move("r") + +        # walk in +        self.cry.move("u") + +        # move up to the healing thing +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("u") +        self.cry.move("l") +        self.cry.move("l") + +        # face it +        self.cry.move("u") + +        # interact +        self.cry.vba.press("a", hold=10, after=1) + +        # wait for yes/no box +        self.cry.text_wait() + +        # press yes +        self.cry.vba.press("a", hold=10, after=1) + +        # TODO: when is healing done? + +        # wait until the script is done running +        self.cry.wait_for_script_running() + +        # wait for it to be really really done +        self.cry.vba.step(count=50) + +        self.cry.move("r") +        self.cry.move("r") + +        # move to the door +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") + +        # walk out +        self.cry.move("d") + +        # check partymon1 level +        if self.cry.vba.memory[0xdcfe] < level: +            self.new_bark_level_grind(level, skip=False) +        else: +            return + +    @skippable +    def new_bark_level_grind_walk_to_grass(self): +        """ +        Move to just above the grass from outside Elm's lab. +        """ + +        self.cry.move("d") +        self.cry.move("d") + +        self.cry.move("l") +        self.cry.move("l") + +        self.cry.move("d") +        self.cry.move("d") + +        self.cry.move("l") +        self.cry.move("l") -@skippable -def new_bark_level_grind_walk_to_grass(): +        # move to route 29 past the trees +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") +        self.cry.move("l") + +        # move to just above the grass +        self.cry.move("d") +        self.cry.move("d") +        self.cry.move("d") + +def bootstrap(runner=None, cry=None):      """ -    Move to just above the grass from outside Elm's lab. +    Setup the initial game and return the state. This skips the intro and +    performs some other actions to get the game to a reasonable starting state.      """ +    if not runner: +        runner = SpeedRunner(cry=cry) +    runner.setup() + +    # skip=False means always run the skip_intro function regardless of the +    # presence of a saved after state. +    runner.skip_intro(skip=True) + +    # keep a reference of the current state +    state = runner.cry.vba.state + +    runner.cry.vba.shutdown() -    vba.crystal.move("d") -    vba.crystal.move("d") - -    vba.crystal.move("l") -    vba.crystal.move("l") - -    vba.crystal.move("d") -    vba.crystal.move("d") - -    vba.crystal.move("l") -    vba.crystal.move("l") - -    # move to route 29 past the trees -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") -    vba.crystal.move("l") - -    # move to just above the grass -    vba.crystal.move("d") -    vba.crystal.move("d") -    vba.crystal.move("d") +    return state + +def main(): +    """ +    Setup a basic ``SpeedRunner`` instance and then run the runner. +    """ +    runner = SpeedRunner() +    runner.setup() +    return runner.main()  if __name__ == "__main__":      main() diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py new file mode 100644 index 0000000..3bda1a5 --- /dev/null +++ b/pokemontools/vba/battle.py @@ -0,0 +1,188 @@ +""" +Code that attempts to model a battle. +""" + +from pokemontools.vba.vba import crystal as emulator +import pokemontools.vba.vba as vba + +class BattleException(Exception): +    """ +    Something went terribly wrong in a battle. +    """ + +class EmulatorController(object): +    """ +    Controls the emulator. I don't have a good reason for this. +    """ + +class Battle(EmulatorController): +    """ +    Wrapper around the battle routine inside of the game. This object controls +    the emulator and provides a sanitized interface for interacting with a +    battle through python. +    """ + +    def __init__(self, emulator=None): +        """ +        Setup the battle. +        """ +        self.emulator = emulator + +    def is_in_battle(self): +        """ +        @rtype: bool +        """ +        return self.emulator.is_in_battle() + +    def is_input_required(self): +        """ +        Detects if the battle is waiting for player input. +        """ +        return self.is_player_turn() or self.is_mandatory_switch() + +    def is_fight_pack_run_menu(self): +        """ +        Attempts to detect if the current menu is fight-pack-run. This is only +        for whether or not the player needs to choose what to do next. +        """ +        signs = ["FIGHT", "PACK", "RUN"] +        screentext = self.emulator.get_text() +        return all([sign in screentext for sign in signs]) + +    def is_player_turn(self): +        """ +        Detects if the battle is waiting for the player to choose an attack. +        """ +        return self.is_fight_pack_run_menu() + +    def is_mandatory_switch(self): +        """ +        Detects if the battle is waiting for the player to choose a next +        pokemon. +        """ +        # TODO: test when "no" fails to escape for wild battles. +        # trainer battles: menu asks to select the next mon +        # wild battles: yes/no box first +        # The following conditions are probably sufficient: +        #   1) current pokemon hp is 0 +        #   2) game is polling for input + +        # TODO: detect the mandatory switch menu +        return False + +    def skip_start_text(self, max_loops=20): +        """ +        Skip any initial conversation until the player can select an action. +        This includes skipping any text that appears on a map from an NPC as +        well as text that appears prior to the first time the action selection +        menu appears. +        """ +        if not self.is_in_battle(): +            while not self.is_in_battle() and max_loops > 0: +                self.emulator.text_wait() +                max_loops -= 1 + +            if max_loops <= 0: +                raise Exception("Couldn't start the battle.") +        else: +            self.emulator.text_wait() + +    def skip_end_text(self, loops=20): +        """ +        Skip through any text that appears after the final attack. +        """ +        if not self.is_in_battle(): +            # TODO: keep talking until the character can move? A battle can be +            # triggered inside of a script, and after the battle is ver the +            # player may not be able to move until the script is done. The +            # script might only finish after other player input is given, so +            # using "text_wait() until the player can move" is a bad idea here. +            self.emulator.text_wait() +        else: +            while self.is_in_battle() and loops > 0: +                self.emulator.text_wait() +                loops -= 1 + +            if loops <= 0: +                raise Exception("Couldn't get out of the battle.") + +    def skip_until_input_required(self): +        """ +        Waits until the battle needs player input. +        """ +        while not self.is_input_required(): +            self.emulator.text_wait() + +    def run(self): +        """ +        Step through the entire battle. +        """ +        # Advance to the battle from either of these states: +        #   1) the player is talking with an npc +        #   2) the battle has already started but there's initial text +        # xyz wants to battle, a wild foobar appeared +        self.skip_start_text() + +        while self.is_in_battle(): +            self.skip_until_input_required() + +            if self.is_player_turn(): +                # battle hook provides input to handle this situation +                self.handle_turn() +            elif self.is_mandatory_switch(): +                # battle hook provides input to handle this situation too +                self.handle_mandatory_switch() +            else: +                raise BattleException("unknown state, aborting") + +        # "how did i lose? wah" +        self.skip_end_text() + +        # TODO: return should indicate win/loss (blackout) + +    def handle_mandatory_switch(self): +        """ +        Something fainted, pick the next mon. +        """ +        raise NotImplementedError + +    def handle_turn(self): +        """ +        Take actions inside of a battle based on the game state. +        """ +        raise NotImplementedError + +class BattleStrategy(Battle): +    """ +    Throw a pokeball until everyone dies. +    """ + +    def handle_mandatory_switch(self): +        """ +        Something fainted, pick the next mon. +        """ +        for pokemon in self.emulator.party: +            if pokemon.hp > 0: +                break +        else: +            # the game failed to do a blackout.. not sure what to do now. +            raise BattleException("No partymons left. wtf?") + +       return pokemon.id + +    def handle_turn(self): +        """ +        Take actions inside of a battle based on the game state. +        """ +        self.throw_pokeball() + +class SimpleBattleStrategy(BattleStrategy): +    """ +    Attack the enemy with the first move. +    """ + +    def handle_turn(self): +        """ +        Always attack the enemy with the first move. +        """ +        self.attack(self.battle.party[0].moves[0].name) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d7fdf1d..3367dee 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -9,115 +9,160 @@ import re  import string  from copy import copy -import unittest -  # for converting bytes to readable text -from pokemontools.chars import chars +from pokemontools.chars import ( +    chars, +) -from pokemontools.map_names import map_names +from pokemontools.map_names import ( +    map_names, +)  import keyboard  # just use a default config for now until the globals are removed completely -import pokemontools.config as conf -config = conf.Config() -project_path = config.path -save_state_path = config.save_state_path -rom_path = config.rom_path - -if not os.path.exists(rom_path): -    raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) +import pokemontools.configuration as configuration  import vba_wrapper -vba = vba_wrapper.VBA(rom_path) -registers = vba_wrapper.core.registers.Registers(vba) -  button_masks = vba_wrapper.core.VBA.button_masks  button_combiner = vba_wrapper.core.VBA.button_combine  def translate_chars(charz): +    """ +    Translate a string from the in-game format to readable form. This is +    accomplished through the same lookup table that the preprocessors use. +    """      result = ""      for each in charz:          result += chars[each]      return result -def press(buttons, holdsteps=1, aftersteps=1): +class crystal(object):      """ -    Press a button. - -    Use steplimit to say for how many steps you want to press -    the button (try leaving it at the default, 1). -    """ -    if hasattr(buttons, "__len__"): -        number = button_combiner(buttons) -    elif isinstance(buttons, int): -        number = buttons -    else: -        number = buttons -    for step_counter in range(0, holdsteps): -        Gb.step(number) - -    # clear the button press -    if aftersteps > 0: -        for step_counter in range(0, aftersteps): -            Gb.step(0) - -def call(bank, address): +    Just a simple namespace to store a bunch of functions for Pokémon Crystal. +    There can only be one running instance of the emulator per process because +    it's a poorly written shared library.      """ -    Jumps into a function at a certain address. -    Go into the start menu, pause the game and try call(1, 0x1078) to see a -    string printed to the screen. -    """ -    push = [ -        registers.pc, -        registers.hl, -        registers.de, -        registers.bc, -        registers.af, -        0x3bb7, -    ] - -    for value in push: -        registers.sp -= 2 -        set_memory_at(registers.sp + 1, value >> 8) -        set_memory_at(registers.sp, value & 0xFF) -        if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: -            print "desired memory values: " + str([value & 0xFF, value >> 8] ) -            print "actual memory values: " + str(get_memory_range(registers.sp , 2)) -            print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(get_memory_at(registers.sp)) - -    if bank != 0: -        registers["af"] = (bank << 8) | (registers["af"] & 0xFF) -        registers["hl"] = address -        registers["pc"] = 0x2d63 # FarJump -    else: -        registers["pc"] = address - -def get_stack(): -    """ -    Return a list of functions on the stack. -    """ -    addresses = [] -    sp = registers.sp +    def __init__(self, config=None): +        """ +        Launch the VBA controller. +        """ +        if not config: +            config = configuration.Config() -    for x in range(0, 11): -        sp = sp - (2 * x) -        hi = get_memory_at(sp + 1) -        lo = get_memory_at(sp) -        address = ((hi << 8) | lo) -        addresses.append(address) +        self.config = config -    return addresses +        self.vba = vba_wrapper.VBA(self.config.rom_path) +        self.registers = vba_wrapper.core.registers.Registers(self.vba) -class crystal: -    """ -    Just a simple namespace to store a bunch of functions for Pokémon Crystal. -    """ +        if not os.path.exists(self.config.rom_path): +            raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + +    def shutdown(self): +        """ +        Reset the emulator. +        """ +        self.vba.shutdown() + +    def save_state(self, name, state=None, override=False): +        """ +        Saves the given state to save_state_path. + +        The file format must be ".sav", and this will be appended to your +        string if necessary. +        """ +        if state == None: +            state = self.vba.state + +        if len(name) < 4 or name[-4:] != ".sav": +            name += ".sav" + +        save_path = os.path.join(self.config.save_state_path, name) + +        if not override and os.path.exists(save_path): +            raise Exception("oops, save state path already exists: {0}".format(save_path)) + +        with open(save_path, "wb") as file_handler: +            file_handler.write(state) + +    def load_state(self, name, loadit=True): +        """ +        Read a state from file based on the name of the state. + +        Looks in save_state_path for a file with this name (".sav" is +        optional). + +        @param loadit: whether or not to set the emulator to this state +        """ +        save_path = os.path.join(self.config.save_state_path, name) + +        if not os.path.exists(save_path): +            if len(name) < 4 or name[-4:] != ".sav": +                name += ".sav" +                save_path = os.path.join(self.config.save_state_path, name) + +        with open(save_path, "rb") as file_handler: +            state = file_handler.read() + +        if loadit: +            self.vba.state = state + +        return state + +    def call(self, bank, address): +        """ +        Jumps into a function at a certain address. + +        Go into the start menu, pause the game and try call(1, 0x1078) to see a +        string printed to the screen. +        """ +        push = [ +            self.registers.pc, +            self.registers.hl, +            self.registers.de, +            self.registers.bc, +            self.registers.af, +            0x3bb7, +        ] + +        self.push_stack(push) + +        if bank != 0: +            self.registers["af"] = (bank << 8) | (self.registers["af"] & 0xFF) +            self.registers["hl"] = address +            self.registers["pc"] = 0x2d63 # FarJump +        else: +            self.registers["pc"] = address + +    def push_stack(self, push): +        for value in push: +            self.registers["sp"] -= 2 +            self.vba.write_memory_at(self.registers.sp + 1, value >> 8) +            self.vba.write_memory_at(self.registers.sp, value & 0xFF) +            if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: +                print "desired memory values: " + str([value & 0xFF, value >> 8] ) +                print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) +                print "wrong value at " + hex(self.registers.sp) + " expected " + hex(value) + " but got " + hex(self.vba.read_memory_at(self.registers.sp)) -    @staticmethod -    def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): +    def get_stack(self): +        """ +        Return a list of functions on the stack. +        """ +        addresses = [] +        sp = self.registers.sp + +        for x in range(0, 11): +            sp = sp - (2 * x) +            hi = self.vba.read_memory_at(sp + 1) +            lo = self.vba.read_memory_at(sp) +            address = ((hi << 8) | lo) +            addresses.append(address) + +        return addresses + +    def text_wait(self, step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None):          """          Presses the "A" button when text is done being drawn to screen. @@ -134,22 +179,22 @@ class crystal:          :param max_wait: number of wait loops to perform          """          while max_wait > 0: -            hi = get_memory_at(registers.sp + 1) -            lo = get_memory_at(registers.sp) +            hi = self.vba.read_memory_at(self.registers.sp + 1) +            lo = self.vba.read_memory_at(self.registers.sp)              address = ((hi << 8) | lo)              if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): #  0xaef:                  print "pressing, then breaking.. address is: " + str(hex(address))                  # set CurSFX -                set_memory_at(0xc2bf, 0) +                self.vba.write_memory_at(0xc2bf, 0) -                press("a", holdsteps=10, aftersteps=1) +                self.vba.press("a", hold=10, after=1)                  # check if CurSFX is SFX_READ_TEXT_2 -                if get_memory_at(0xc2bf) == 0x8: +                if self.vba.read_memory_at(0xc2bf) == 0x8:                      print "cursfx is set to SFX_READ_TEXT_2, looping.." -                    return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) +                    return self.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit)                  else:                      if sfx_limit > 0:                          sfx_limit = sfx_limit - 1 @@ -160,7 +205,7 @@ class crystal:                          break              else: -                stack = get_stack() +                stack = self.get_stack()                  # yes/no box or the name selection box                  if address in range(0xa46, 0xaaf): @@ -179,14 +224,14 @@ class crystal:                      break                  else: -                    nstep(step_size) +                    self.vba.step(count=step_size)              # if there is a callback, then call the callback and exit when the              # callback returns True. This is especially useful during the              # OakSpeech intro where textboxes are running constantly, and then              # suddenly the player can move around. One way to detect that is to              # set callback to a function that returns -            # "vba.get_memory_at(0xcfb1) != 0". +            # "vba.read_memory_at(0xcfb1) != 0".              if callback != None:                  result = callback()                  if result == True: @@ -202,17 +247,15 @@ class crystal:          if max_wait == 0:              print "max_wait was hit" -    @staticmethod -    def walk_through_walls_slow(): -        memory = get_memory() +    def walk_through_walls_slow(self): +        memory = self.vba.memory          memory[0xC2FA] = 0          memory[0xC2FB] = 0          memory[0xC2FC] = 0          memory[0xC2FD] = 0 -        set_memory(memory) +        self.vba.memory = memory -    @staticmethod -    def walk_through_walls(): +    def walk_through_walls(self):          """          Lets the player walk all over the map. @@ -221,73 +264,65 @@ class crystal:          to be executed each step/tick if continuous walk-through-walls          is desired.          """ -        set_memory_at(0xC2FA, 0) -        set_memory_at(0xC2FB, 0) -        set_memory_at(0xC2FC, 0) -        set_memory_at(0xC2FD, 0) +        self.vba.write_memory_at(0xC2FA, 0) +        self.vba.write_memory_at(0xC2FB, 0) +        self.vba.write_memory_at(0xC2FC, 0) +        self.vba.write_memory_at(0xC2FD, 0)      #@staticmethod      #def set_enemy_level(level): -    #    set_memory_at(0xd213, level) +    #    vba.write_memory_at(0xd213, level) -    @staticmethod -    def nstep(steplimit=500): +    def nstep(self, steplimit=500):          """          Steps the CPU forward and calls some functions in between each step.          (For example, to manipulate memory.) This is pretty slow.          """          for step_counter in range(0, steplimit): -            crystal.walk_through_walls() +            self.walk_through_walls()              #call(0x1, 0x1078) -            step() +            self.vba.step() -    @staticmethod -    def disable_triggers(): -        set_memory_at(0x23c4, 0xAF) -        set_memory_at(0x23d0, 0xAF); +    def disable_triggers(self): +        self.vba.write_memory_at(0x23c4, 0xAF) +        self.vba.write_memory_at(0x23d0, 0xAF); -    @staticmethod -    def disable_callbacks(): -        set_memory_at(0x23f2, 0xAF) -        set_memory_at(0x23fe, 0xAF) +    def disable_callbacks(self): +        self.vba.write_memory_at(0x23f2, 0xAF) +        self.vba.write_memory_at(0x23fe, 0xAF) -    @staticmethod -    def get_map_group_id(): +    def get_map_group_id(self):          """          Returns the current map group.          """ -        return get_memory_at(0xdcb5) +        return self.vba.read_memory_at(0xdcb5) -    @staticmethod -    def get_map_id(): +    def get_map_id(self):          """          Returns the map number of the current map.          """ -        return get_memory_at(0xdcb6) +        return self.vba.read_memory_at(0xdcb6) -    @staticmethod -    def get_map_name(): +    def get_map_name(self, map_names=map_names):          """          Figures out the current map name.          """ -        map_group_id = crystal.get_map_group_id() -        map_id = crystal.get_map_id() +        map_group_id = self.get_map_group_id() +        map_id = self.get_map_id()          return map_names[map_group_id][map_id]["name"] -    @staticmethod -    def get_xy(): +    def get_xy(self):          """          (x, y) coordinates of player on map.          Relative to top-left corner of map.          """ -        x = get_memory_at(0xdcb8) -        y = get_memory_at(0xdcb7) +        x = self.vba.read_memory_at(0xdcb8) +        y = self.vba.read_memory_at(0xdcb7)          return (x, y) -    @staticmethod -    def menu_select(id=1): +    def menu_select(self, id=1):          """          Sets the cursor to the given pokemon in the player's party. @@ -296,38 +331,34 @@ class crystal:          This probably works on other menus.          """ -        set_memory_at(0xcfa9, id) +        self.vba.write_memory_at(0xcfa9, id) -    @staticmethod -    def is_in_battle(): +    def is_in_battle(self):          """          Checks whether or not we're in a battle.          """ -        return (get_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() +        return (self.vba.read_memory_at(0xd22d) != 0) or self.is_in_link_battle() -    @staticmethod -    def is_in_link_battle(): -        return get_memory_at(0xc2dc) != 0 +    def is_in_link_battle(self): +        return self.vba.read_memory_at(0xc2dc) != 0 -    @staticmethod -    def unlock_flypoints(): +    def unlock_flypoints(self):          """          Unlocks different destinations for flying.          Note: this might start at 0xDCA4 (minus one on all addresses), but not          sure.          """ -        set_memory_at(0xDCA5, 0xFF) -        set_memory_at(0xDCA6, 0xFF) -        set_memory_at(0xDCA7, 0xFF) -        set_memory_at(0xDCA8, 0xFF) +        self.vba.write_memory_at(0xDCA5, 0xFF) +        self.vba.write_memory_at(0xDCA6, 0xFF) +        self.vba.write_memory_at(0xDCA7, 0xFF) +        self.vba.write_memory_at(0xDCA8, 0xFF) -    @staticmethod -    def get_gender(): +    def get_gender(self):          """          Returns 'male' or 'female'.          """ -        gender = get_memory_at(0xD472) +        gender = self.vba.read_memory_at(0xD472)          if gender == 0:              return "male"          elif gender == 1: @@ -335,54 +366,59 @@ class crystal:          else:              return gender -    @staticmethod -    def get_player_name(): +    def get_player_name(self):          """          Returns the 7 characters making up the player's name.          """ -        bytez = get_memory_range(0xD47D, 7) +        bytez = self.vba.memory[0xD47D:0xD47D + 7]          name = translate_chars(bytez)          return name -    @staticmethod -    def warp(map_group_id, map_id, x, y): -        set_memory_at(0xdcb5, map_group_id) -        set_memory_at(0xdcb6, map_id) -        set_memory_at(0xdcb7, y) -        set_memory_at(0xdcb8, x) -        set_memory_at(0xd001, 0xFF) -        set_memory_at(0xff9f, 0xF1) -        set_memory_at(0xd432, 1) -        set_memory_at(0xd434, 0 & 251) - -    @staticmethod -    def warp_pokecenter(): -        crystal.warp(1, 1, 3, 3) -        crystal.nstep(200) - -    @staticmethod -    def masterballs(): +    def warp(self, map_group_id, map_id, x, y): +        """ +        Warp into another map. +        """ +        self.vba.write_memory_at(0xdcb5, map_group_id) +        self.vba.write_memory_at(0xdcb6, map_id) +        self.vba.write_memory_at(0xdcb7, y) +        self.vba.write_memory_at(0xdcb8, x) +        self.vba.write_memory_at(0xd001, 0xFF) +        self.vba.write_memory_at(0xff9f, 0xF1) +        self.vba.write_memory_at(0xd432, 1) +        self.vba.write_memory_at(0xd434, 0 & 251) + +    def warp_pokecenter(self): +        """ +        Warp straight into a pokecenter. +        """ +        self.warp(1, 1, 3, 3) +        self.nstep(200) + +    def masterballs(self): +        """ +        Deposit some pokeballs into the first few slots of the pack. This +        overrides whatever items were previously there. +        """          # masterball -        set_memory_at(0xd8d8, 1) -        set_memory_at(0xd8d9, 99) +        self.vba.write_memory_at(0xd8d8, 1) +        self.vba.write_memory_at(0xd8d9, 99)          # ultraball -        set_memory_at(0xd8da, 2) -        set_memory_at(0xd8db, 99) +        self.vba.write_memory_at(0xd8da, 2) +        self.vba.write_memory_at(0xd8db, 99)          # pokeballs -        set_memory_at(0xd8dc, 5) -        set_memory_at(0xd8dd, 99) +        self.vba.write_memory_at(0xd8dc, 5) +        self.vba.write_memory_at(0xd8dd, 99) -    @staticmethod -    def get_text(): +    def get_text(self, chars=chars):          """          Returns alphanumeric text on the screen.          Other characters will not be shown.          """          output = "" -        tiles = get_memory_range(0xc4a0, 1000) +        tiles = self.vba.memory[0xc4a0:0xc4a0 + 1000]          for each in tiles:              if each in chars.keys():                  thing = chars[each] @@ -405,32 +441,69 @@ class crystal:          return output -    @staticmethod -    def keyboard_apply(button_sequence): +    def is_showing_stats_screen(self): +        """ +        This is meant to detect whether or not the stats screen is showing. +        This is the menu that pops up after leveling up. +        """ +        # These words must be on the screen if the stats screen is currently +        # displayed. +        parts = [ +            "ATTACK", +            "DEFENSE", +            "SPCL.ATK", +            "SPCL.DEF", +            "SPEED", +        ] + +        # get the current text on the screen +        text = self.get_text() + +        if all([part in text for part in parts]): +            return True +        else: +            return False + +    def handle_stats_screen(self, force=False): +        """ +        Attempts to bypass a stats screen. Set force=True if you want to make +        the attempt regardless of whether or not the system thinks a stats +        screen is showing. +        """ +        if self.is_showing_stats_screen() or force: +            self.vba.press("a") +            self.vba.step(count=20) + +    def keyboard_apply(self, button_sequence):          """          Applies a sequence of buttons to the on-screen keyboard.          """          for buttons in button_sequence: -            press(buttons) -            nstep(2) -            press([]) +            self.vba.press(buttons) + +            if buttons == "select": +                self.vba.step(count=5) +            else: +                self.vba.step(count=2) -    @staticmethod -    def write(something="TrAiNeR"): +            self.vba.press([]) + +    def write(self, something="TrAiNeR"):          """          Types out a word.          Uses a planning algorithm to do this in the most efficient way possible.          """          button_sequence = keyboard.plan_typing(something) -        crystal.keyboard_apply([[x] for x in button_sequence]) +        self.vba.step(count=10) +        self.keyboard_apply([[x] for x in button_sequence]) +        return button_sequence -    @staticmethod -    def set_partymon2(): +    def set_partymon2(self):          """          This causes corruption, so it's not working yet.          """ -        memory = get_memory() +        memory = self.vba.memory          memory[0xdcd7] = 2          memory[0xdcd9] = 0x7 @@ -464,19 +537,18 @@ class crystal:          memory[0xdd33] = 0x10          memory[0xdd34] = 0x40 -        set_memory(memory) +        self.vba.memory = memory -    @staticmethod -    def wait_for_script_running(debug=False, limit=1000): +    def wait_for_script_running(self, debug=False, limit=1000):          """          Wait until ScriptRunning isn't -1.          """          while limit > 0: -            if get_memory_at(0xd438) != 255: +            if self.vba.read_memory_at(0xd438) != 255:                  print "script is done executing"                  return              else: -                step() +                self.vba.step()              if debug:                  limit = limit - 1 @@ -484,44 +556,129 @@ class crystal:          if limit == 0:              print "limit ran out" -    @staticmethod -    def move(cmd): +    def move(self, cmd):          """          Attempt to move the player.          """ -        press(cmd, holdsteps=10, aftersteps=0) -        press([]) +        if isinstance(cmd, list): +            for command in cmd: +                self.move(command) +        else: +            self.vba.press(cmd, hold=10, after=0) +            self.vba.press([]) + +            memory = self.vba.memory +            #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: +            while memory[0xd043] in [0, 1, 2, 3]: +            #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: +                self.vba.step(count=10) +                memory = self.vba.memory + +    def get_enemy_hp(self): +        """ +        Returns the HP of the current enemy. +        """ +        hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) +        return hp + +    def start_trainer_battle_lamely(self, map_group=0x1, map_id=0xc, x=6, y=8, direction="l", loop_limit=10): +        """ +        Starts a trainer battle by warping into a map at the designated +        coordinates, pressing the direction button for a full walk step (which +        ideally should be blocked, this is mainly to establish direction), and +        then pressing "a" to initiate the trainer battle. +        """ +        self.warp(map_group, map_id, x, y) + +        # finish loading the map, might not be necessary? +        self.nstep(100) -        memory = get_memory() -        #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: -        while memory[0xd043] in [0, 1, 2, 3]: -        #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: -            nstep(10) -            memory = get_memory() +        # face towards the trainer (or whatever direction was specified). If +        # this direction is blocked, then this will only change which direction +        # the character is facing. However, if this direction is not blocked by +        # the map or by an npc, then this will cause an entire step to be +        # taken. +        self.vba.press([direction]) -class TestEmulator(unittest.TestCase): -    def test_PlaceString(self): -        call(0, 0x1078) +        # talk to the trainer, don't assume line of sight will be triggered +        self.vba.press(["a"]) +        self.vba.press([]) + +        # trainer might talk, skip any text until the player can choose moves +        while not self.is_in_battle() and loop_limit > 0: +            self.text_wait() +            loop_limit -= 1 + +    def broken_start_random_battle(self): +        self.push_stack([self.registers.pc]) +        self.registers["pc"] = address % 0x4000 +        self.call(address / 0x4000, address % 0x4000) + +    def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): +        """ +        This will fail after the first mon is defeated. +        """ +        Script_startbattle_address = 0x97436 + +        # setup the battle +        memory = self.vba.memory +        memory[0xd459] = 0x81 +        memory[0xd22f] = trainer_group +        memory[0xd231] = trainer_id +        self.vba.memory = memory + +        self.call(0x25, (Script_startbattle_address % 0x4000) + 0x4000) + +    def broken_start_random_battle_by_rocksmash_battle_script(self): +        """ +        This doesn't start a battle. +        """ +        CallScript_address = 0x261f +        RockSmashBattleScript_address = 0x97cf9 +        ScriptRunning = 0xd438 +        ScriptBank = 0xd439 +        ScriptPos = 0xd43a -        # where to draw the text -        registers["hl"] = 0xc4a0 +        memory = self.vba.memory +        memory[ScriptBank] = RockSmashBattleScript_address / 0x4000 +        memory[ScriptPos] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff00) >> 8 +        memory[ScriptPos+1] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff) +        memory[ScriptRunning] = 0xff +        self.vba.memory = memory -        # what text to read from -        registers["de"] = 0x1276 +        self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) +        self.vba.registers["hl"] = (RockSmashBattleScript_address % 0x4000) + 0x4000 +        self.call(0x0, CallScript_address) -        nstep(10) +    #def attempt_start_battle_by_startbattle(self): +    #    StartBattle_address = 0x3f4c1 +    #    self.call(StartBattle_address / 0x4000, (StartBattle_address % 0x4000) + 0x4000) -        text = crystal.get_text() +    #def attempt_start_random_battle_by_wild_battle(self): +    #    start_wild_battle = 0x3f4dd +    #    #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) +    #    #self.vba.registers["pc"] = ... -        self.assertTrue("TRAINER" in text) +    def old_crap(self): +        CallScript_address = 0x261f +        RockSmashBattleScript_address = 0x97cf9 +        RockSmashEncounter_address = 0x97cc0 +        StartBattle_address = 0x3f4c1 +        ScriptRunning = 0xd438 +        ScriptBank = 0xd439 +        ScriptPos = 0xd43a +        start_wild_battle = 0x3f4dd +        script = 0x1a1dc6 -class TestWriter(unittest.TestCase): -    def test_very_basic(self): -        button_sequence = keyboard.plan_typing("an") -        expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] +        #self.call(StartBattle_address / 0x4000, StartBattle_address % 0x4000) +        #self.call(RockSmashEncounter_address / 0x4000, RockSmashEncounter_address % 0x4000) -        self.assertEqual(len(expected_result), len(button_sequence)) -        self.assertEqual(expected_result, button_sequence) +        #self.push_stack([self.registers.pc]) +        #memory[ScriptBank] = script / 0x4000 +        #memory[ScriptPos] = ((script % 0x4000) & 0xff00) >> 8 +        #memory[ScriptPos+1] = ((script % 0x4000) & 0xff) +        #memory[ScriptRunning] = 0xff + +        #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) -if __name__ == "__main__": -    unittest.main() +        #self.vba.memory = memory | 
