diff options
author | Bryan Bishop <kanzure@gmail.com> | 2013-11-11 12:38:52 -0600 |
---|---|---|
committer | Bryan Bishop <kanzure@gmail.com> | 2013-11-11 12:38:52 -0600 |
commit | e0991710ef720ee73bc8042671ba92159002f236 (patch) | |
tree | f8cdf8595411ad8b16eca1f293a1c3e724eef45c | |
parent | fcfde94ba84a6a29bb22dd97b62af2e3c72276bd (diff) | |
parent | a11b084a2824dbe9c1df84d9ea205b8495f3da13 (diff) |
Merge branch 'github/master' into master
-rw-r--r-- | pokemontools/vba/autoplayer.py | 820 | ||||
-rw-r--r-- | pokemontools/vba/battle.py | 193 | ||||
-rw-r--r-- | pokemontools/vba/vba.py | 1024 | ||||
-rw-r--r-- | tests/bootstrapping.py | 54 | ||||
-rw-r--r-- | tests/setup_vba.py | 4 | ||||
-rw-r--r-- | tests/test_vba.py | 276 | ||||
-rw-r--r-- | tests/test_vba_battle.py | 93 |
7 files changed, 1834 insertions, 630 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..87cd7b1 --- /dev/null +++ b/pokemontools/vba/battle.py @@ -0,0 +1,193 @@ +""" +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 + + if "CANCEL Which ?" in self.emulator.get_text(): + return True + else: + 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() + + # let the text draw so that the state is more obvious + self.emulator.vba.step(count=10) + + 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 4b0bbee..10513c6 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -9,12 +9,14 @@ 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 @@ -30,94 +32,379 @@ if not os.path.exists(rom_path): 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 calculate_bank(address): + """ + Which bank does this address exist in? + """ + return address / 0x4000 + +def calculate_address(address): + """ + Gives the relative address once the bank is loaded. + + This is not the same as the calculate_pointer in the + pokemontools.crystal.pointers module. + """ + return (address % 0x4000) + 0x4000 + 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): - """ - Press a button. - - Use steplimit to say for how many steps you want to press - the button (try leaving it at the default, 1). +def translate_text(text, chars=chars): """ - 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): + Converts text to the in-game byte coding. """ - Jumps into a function at a certain address. + output = [] + for given_char in text: + for (byte, char) in chars.iteritems(): + if char == given_char: + output.append(byte) + break + else: + raise Exception( + "no match for {0}".format(given_char) + ) + return output - 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(): +class crystal(object): """ - Return a list of functions on the stack. + 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. """ - addresses = [] - sp = registers.sp - 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) + def __init__(self, config=None): + """ + Launch the VBA controller. + """ + if not config: + config = configuration.Config() + + 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, address, bank=None): + """ + 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. + """ + if bank is None: + bank = calculate_bank(address) + + 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)) + + 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 inject_asm_into_rom(self, asm=[], address=0x75 * 0x4000, has_finished_address=0xdb75): + """ + Writes asm to the loaded ROM. Calls the asm. + + :param address: ROM address for where to store the injected asm script. + The default value is an address in pokecrystal that isn't used for + anything. + + :param has_finished_address: address for where to store whether the + script executed or not. This value is restored when the script has been + confirmed to work. It's conceivable that some injected asm might need + to change that address if the asm needs to access the original wram + value itself. + """ + if len(asm) > 0x4000: + raise Exception("too much asm") + + # temporarily use wram + cached_wram_value = self.vba.memory[has_finished_address] + + # set the value at has_finished_address to 0 + reset_wram_mem = list(self.vba.memory) + reset_wram_mem[has_finished_address] = 0 + self.vba.memory = reset_wram_mem + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0x3e, 1, + + # ld [has_finished], a + 0xea, has_finished_address & 0xff, has_finished_address >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # TODO: check if asm ends with a byte that causes a return or call or + # other "ender". Raise an exception if it already returns on its own. + + # combine the given asm with the setter bytes + total_asm = asm + set_has_finished + + # get a copy of the current rom + rom = list(self.vba.rom) + + # inject the asm + rom[address : address + len(total_asm)] = total_asm + + # set the rom with the injected asm + self.vba.rom = rom + + # call the injected asm + self.call(calculate_address(address), bank=calculate_bank(address)) + + # make the emulator step forward + self.vba.step(count=20) + + # check if the script has executed (see below) + current_mem = self.vba.memory + + # reset the wram value to its original value + another_mem = list(self.vba.memory) + another_mem[has_finished_address] = cached_wram_value + self.vba.memory = another_mem + + # check if the script has actually executed + # TODO: should this raise an exception if the script didn't finish? + if current_mem[has_finished_address] == 0: + return False + elif current_mem[has_finished_address] == 1: + return True + else: + raise Exception( + "has_finished_address at {has_finished_address} was overwritten with an unexpected value {value}".format( + has_finished_address=hex(has_finished_address), + value=current_mem[has_finished_address], + ) + ) + + def inject_asm_into_wram(self, asm=[], address=0xdfcf): + """ + Writes asm to memory. Makes the emulator run the asm. + + This function will append "ret" to the list of bytes. Before returning, + it updates the value at the first byte to indicate that the function + has executed. + + The first byte at the given address is reserved for whether the asm has + finished executing. + """ + memory = list(self.vba.memory) + + # the first byte is reserved for whether the script has finished + has_finished = address + memory[has_finished] = 0 + + # the second byte is where the script will be stored + script_address = address + 1 + + # TODO: error checking; make sure the last byte doesn't already return. + # Use some functions from gbz80disasm to perform this check. + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0x3e, 1, + + # ld [has_finished], a + 0xea, has_finished & 0xff, has_finished >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # append the last opcodes to the script + asm = bytearray(asm) + bytearray(set_has_finished) + + memory[script_address : script_address + len(asm)] = asm + self.vba.memory = memory + + # make the emulator call the script + self.call(script_address, bank=0) + + # make the emulator step forward + self.vba.step(count=50) + + # check if the script has executed + # TODO: should this raise an exception if the script didn't finish? + if self.vba.memory[has_finished] == 0: + return False + elif self.vba.memory[has_finished] == 1: + return True + else: + raise Exception( + "has_finished at {has_finished} was overwritten with an unexpected value {value}".format( + has_finished=hex(has_finished), + value=self.vba.memory[has_finished], + ) + ) + + def call_script(self, address, bank=None, wram=False, force=False): + """ + Sets wram values so that the engine plays a script. - @staticmethod - def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): + :param address: address of the map script + :param bank: override for bank calculation (based on address) + :param wram: force bank to 0 + :param force: override an already-running script + """ + + ScriptFlags = 0xd434 + ScriptMode = 0xd437 + ScriptRunning = 0xd438 + ScriptBank = 0xd439 + ScriptPos = 0xd43a + NumScriptParents = 0xd43c + ScriptParents = 0xd43d + + num_possible_parents = 4 + len_parent = 3 + + mem = list(self.vba.memory) + + if mem[ScriptRunning] == 0xff: + if force: + # wipe the parent routine array + mem[NumScriptParents] = 0 + for i in xrange(num_possible_parents * len_parent): + mem[ScriptParents + i] = 0 + else: + raise Exception("a script is already running, use force=True") + + if wram: + bank = 0 + elif not bank: + bank = calculate_bank(address) + address = address % 0x4000 + 0x4000 * bool(bank) + + mem[ScriptFlags] |= 4 + mem[ScriptMode] = 1 + mem[ScriptRunning] = 0xff + + mem[ScriptBank] = bank + mem[ScriptPos] = address % 0x100 + mem[ScriptPos+1] = address / 0x100 + + self.vba.memory = mem + + 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 +421,27 @@ 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=50) # check if CurSFX is SFX_READ_TEXT_2 - if get_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) + if self.vba.read_memory_at(0xc2bf) == 0x8: + if "CANCEL Which" in self.get_text(): + print "probably the 'switch pokemon' menu" + return + else: + print "cursfx is set to SFX_READ_TEXT_2, looping.." + print self.get_text() + 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 +452,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 +471,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 +494,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 +511,68 @@ 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) + def lower_enemy_hp(self): + """ + Dramatically lower the enemy's HP. + """ + self.vba.write_memory_at(0xd216, 0) + self.vba.write_memory_at(0xd217, 1) - @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() - #call(0x1, 0x1078) - step() + self.walk_through_walls() + #call(0x1078) + 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 +581,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 +616,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 +691,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 +787,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 +806,314 @@ 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. + + Consider using start_trainer_battle instead. + """ + self.warp(map_group, map_id, x, y) + + # finish loading the map, might not be necessary? + self.nstep(100) + + # 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]) + + # 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 start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1, text_win="YOU WIN", text_address=0xdb90): + """ + Start a trainer battle with the trainer located by trainer_group and + trainer_id. + + :param trainer_group: trainer group id + :param trainer_id: trainer id within the group + :param text_win: text to show if player wins + :param text_address: where to store text_win in wram + """ + # where the script will be written + rom_address = 0x75 * 0x4000 + + # battle win message + translated_text = translate_text(text_win) + + # also include the first and last bytes needed for text + translated_text = [0] + translated_text + [0x57] + + mem = self.vba.memory + + # create a backup of the current data + wram_backup = mem[text_address : text_address + len(translated_text)] + + # manipulate the memory + mem[text_address : text_address + len(translated_text)] = translated_text + self.vba.memory = mem + + text_pointer_hi = text_address / 0x100 + text_pointer_lo = text_address % 0x100 + + script = [ + # loadfont + #0x47, + + # winlosstext address, address + 0x64, text_pointer_lo, text_pointer_hi, 0, 0, + + # loadtrainer group, id + 0x5e, trainer_group, trainer_id, + + # startbattle + 0x5f, + + # returnafterbattle + 0x60, + + # reloadmapmusic + 0x83, + + # reloadmap + 0x7B, + ] + + # Now make the script restore wram at the end (after the text has been + # used). The assumption here is that this particular subset of wram + # data would not be needed during the bulk of the script. + address = text_address + for byte in wram_backup: + address_hi = address / 0x100 + address_lo = address % 0x100 + + script += [ + # loadvar + 0x1b, address_lo, address_hi, byte, + ] + + address += 1 + + script += [ + # end + 0x91, + ] + + # Use a different wram address because the default is something related + # to trainers. + # use a higher loop limit because otherwise it doesn't start fast enough? + self.inject_script_into_rom(asm=script, rom_address=rom_address, wram_address=0xdb75, limit=1000) + + def set_script(self, address): + """ + Sets the current script in wram to whatever address. + """ + ScriptBank = 0xd439 + ScriptPos = 0xd43a + + memory = self.vba.memory + memory[ScriptBank] = calculate_bank(address) + pointer = calculate_address(address) + memory[ScriptPos] = (calculate_address(address) & 0xff00) >> 8 + memory[ScriptPos] = calculate_address(address) & 0xff + + # TODO: determine if this is necessary + #memory[ScriptRunning] = 0xff - 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() + self.vba.memory = memory + + def inject_script_into_rom(self, asm=[0x91], rom_address=0x75 * 0x4000, wram_address=0xd280, limit=50): + """ + Writes a script to the ROM in a blank location. Calls call_script to + make the game engine aware of the script. Then executes the script and + looks for confirmation thta the script has started to run. + + The script must end itself. + + :param asm: scripting command bytes + :param rom_address: rom location to write asm to + :param wram_address: temporary storage for indicating if the script has + started yet + :param limit: number of frames to emulate before giving up on the start + script + """ + execution_pending = 0 + execution_started = 1 + valid_execution_states = (execution_pending, execution_started) -class TestEmulator(unittest.TestCase): - def test_PlaceString(self): - call(0, 0x1078) + # location for byte for whether script has started executing + execution_indicator_address = wram_address - # where to draw the text - registers["hl"] = 0xc4a0 + # backup whatever exists at the current wram location + backup_wram = self.vba.read_memory_at(execution_indicator_address) - # what text to read from - registers["de"] = 0x1276 + # .. and set it to "pending" + self.vba.write_memory_at(execution_indicator_address, execution_pending) - nstep(10) + # initial script that runs first to tell python that execution started + execution_indicator_script = [ + # loadvar address, value + 0x1b, execution_indicator_address & 0xff, execution_indicator_address >> 8, execution_started, + ] - text = crystal.get_text() + # make the indicator script run before the user script + full_script = execution_indicator_script + asm - self.assertTrue("TRAINER" in text) + # inject the asm + rom = list(self.vba.rom) + rom[rom_address : rom_address + len(full_script)] = full_script -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"] + # set the rom with the injected bytes + self.vba.rom = rom - self.assertEqual(len(expected_result), len(button_sequence)) - self.assertEqual(expected_result, button_sequence) + # setup the script for execution + self.call_script(rom_address) -if __name__ == "__main__": - unittest.main() + status = execution_pending + while status != execution_started and limit > 0: + # emulator time travel + self.vba.step(count=1) + + # get latest wram + status = self.vba.read_memory_at(execution_indicator_address) + if status not in valid_execution_states: + raise Exception( + "The execution indicator at {addr} has invalid state {value}".format( + addr=hex(execution_indicator_address), + value=status, + ) + ) + elif status == execution_started: + break # hooray + + limit -= 1 + + if status == execution_pending and limit == 0: + raise Exception( + "Emulation timeout while waiting for script to start." + ) + + # The script has started so it's okay to reset wram back to whatever it + # was. + self.vba.write_memory_at(execution_indicator_address, backup_wram) + + return True + + def givepoke(self, pokemon_id, level, nickname=None, wram=False): + """ + Give the player a pokemon. + """ + if isinstance(nickname, str): + if len(nickname) == 0: + raise Exception("invalid nickname") + elif len(nickname) > 11: + raise Exception("nickname too long") + else: + if not nickname: + nickname = False + else: + raise Exception("nickname needs to be a string, False or None") + + # script to inject into wram + script = [ + 0x47, # loadfont + #0x55, # keeptextopen + + # givepoke pokemon_id, level, 0, 0 + 0x2d, pokemon_id, level, 0, 0, + + #0x54, # closetext + 0x49, # loadmovesprites + 0x91, # end + ] + + # picked this region of wram because it looks like it's probably unused + # in situations where givepoke will work. + #address = 0xd073 + #address = 0xc000 + #address = 0xd8f1 + address = 0xd280 + + if not wram: + self.inject_script_into_rom(asm=script, wram_address=address) + else: + mem = list(self.vba.memory) + backup_wram = mem[address : address + len(script)] + mem[address : address + len(script)] = script + self.vba.memory = mem + + self.call_script(address, wram=True) + + # "would you like to give it a nickname?" + self.text_wait() + + if nickname: + # yes + self.vba.press("a", hold=10) + + # wait for the keyboard to appear + # TODO: this wait should be moved into write() + self.vba.step(count=20) + + # type the requested nicknameb + self.write(nickname) + + self.vba.press("start", hold=5, after=10) + self.vba.press("a", hold=5, after=50) + else: + # no nickname + self.vba.press("b", hold=10, after=20) + + if wram: + # Wait for the script to end in the engine before copying the original + # wram values back in. + self.vba.step(count=100) + + # reset whatever was in wram before this script was called + mem = list(self.vba.memory) + mem[address : address + len(script)] = backup_wram + self.vba.memory = mem + + def start_random_battle_by_rocksmash_battle_script(self): + """ + Initiates a wild battle using the same function that using rocksmash + would call. + """ + RockSmashBattleScript_address = 0x97cf9 + self.call_script(RockSmashBattleScript_address) diff --git a/tests/bootstrapping.py b/tests/bootstrapping.py new file mode 100644 index 0000000..b71c19a --- /dev/null +++ b/tests/bootstrapping.py @@ -0,0 +1,54 @@ +""" +Functions to bootstrap the emulator state +""" + +from setup_vba import ( + vba, + autoplayer, +) + +def bootstrap(): + """ + Every test needs to be run against a certain minimum context. That context + is constructed by this function. + """ + + cry = vba.crystal(config=None) + runner = autoplayer.SpeedRunner(cry=cry) + + # skip=False means run the skip_intro function instead of just skipping to + # a saved state. + runner.skip_intro(skip=True) + + state = cry.vba.state + + # clean everything up again + cry.vba.shutdown() + + return state + +def bootstrap_trainer_battle(): + """ + Start a trainer battle. + """ + # setup + cry = vba.crystal(config=None) + runner = autoplayer.SpeedRunner(cry=cry) + + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=True) + runner.handle_elm("totodile", skip=True) + + # levelgrind a pokemon + # TODO: make new_bark_level_grind able to figure out how to construct its + # initial state if none is provided. + runner.new_bark_level_grind(17, skip=True) + + cry.givepoke(64, 31, "kAdAbRa") + cry.givepoke(224, 60, "OcTiLlErY") + cry.givepoke(126, 87, "magmar") + + cry.start_trainer_battle() + + return runner.cry.vba.state diff --git a/tests/setup_vba.py b/tests/setup_vba.py new file mode 100644 index 0000000..6e615e2 --- /dev/null +++ b/tests/setup_vba.py @@ -0,0 +1,4 @@ +import pokemontools.vba.vba as vba +import pokemontools.vba.keyboard as keyboard +import pokemontools.vba.autoplayer as autoplayer +autoplayer.vba = vba diff --git a/tests/test_vba.py b/tests/test_vba.py index 56a71e3..caa1867 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -4,81 +4,96 @@ Tests for VBA automation tools import unittest -import pokemontools.vba.vba as vba +from setup_vba import ( + vba, + autoplayer, + keyboard, +) -try: - import pokemontools.vba.vba_autoplayer -except ImportError: - import pokemontools.vba.autoplayer as vba_autoplayer - -vba_autoplayer.vba = vba +from bootstrapping import ( + bootstrap, + bootstrap_trainer_battle, +) def setup_wram(): """ Loads up some default addresses. Should eventually be replaced with the actual wram parser. """ + # TODO: this should just be parsed straight out of wram.asm wram = {} wram["PlayerDirection"] = 0xd4de wram["PlayerAction"] = 0xd4e1 wram["MapX"] = 0xd4e6 wram["MapY"] = 0xd4e7 + + wram["WarpNumber"] = 0xdcb4 + wram["MapGroup"] = 0xdcb5 + wram["MapNumber"] = 0xdcb6 + wram["YCoord"] = 0xdcb7 + wram["XCoord"] = 0xdcb8 + return wram -def bootstrap(): - """ - Every test needs to be run against a certain minimum context. That context - is constructed by this function. - """ +class OtherVbaTests(unittest.TestCase): + def test_keyboard_planner(self): + button_sequence = keyboard.plan_typing("an") + expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] - # reset the rom - vba.shutdown() - vba.load_rom() + self.assertEqual(len(expected_result), len(button_sequence)) + self.assertEqual(expected_result, button_sequence) - # skip=False means run the skip_intro function instead of just skipping to - # a saved state. - vba_autoplayer.skip_intro() +class VbaTests(unittest.TestCase): + cry = None + wram = None - state = vba.get_state() + @classmethod + def setUpClass(cls): + cls.bootstrap_state = bootstrap() - # clean everything up again - vba.shutdown() + cls.wram = setup_wram() - return state + cls.cry = vba.crystal() + cls.vba = cls.cry.vba -class VbaTests(unittest.TestCase): - # unittest in jython2.5 doesn't seem to have setUpClass ?? Man, why am I on - # jython2.5? This is ancient. - #@classmethod - #def setUpClass(cls): - # # get a good game state - # cls.state = bootstrap() - # - # # figure out addresses - # cls.wram = setup_wram() - - # FIXME: work around jython2.5 unittest - state = bootstrap() - wram = setup_wram() + cls.vba.state = cls.bootstrap_state - def get_wram_value(self, name): - return vba.get_memory_at(self.wram[name]) + @classmethod + def tearDownClass(cls): + cls.vba.shutdown() def setUp(self): - # clean the state - vba.shutdown() - vba.load_rom() - # reset to whatever the bootstrapper created - vba.set_state(self.state) + self.vba.state = self.bootstrap_state + + def get_wram_value(self, name): + return self.vba.memory[self.wram[name]] + + def check_movement(self, direction="d"): + """ + Check if (y, x) before attempting to move and (y, x) after attempting + to move are the same. + """ + start = (self.get_wram_value("MapY"), self.get_wram_value("MapX")) + self.cry.move(direction) + end = (self.get_wram_value("MapY"), self.get_wram_value("MapX")) + return start != end + + def bootstrap_name_prompt(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(stop_at_name_selection=True, skip=False, override=False) + + self.cry.vba.press("a", hold=20) - def tearDown(self): - vba.shutdown() + # wait for "Your name?" to show up + while "YOUR NAME?" not in self.cry.get_text(): + self.cry.step(count=50) def test_movement_changes_player_direction(self): player_direction = self.get_wram_value("PlayerDirection") - vba.crystal.move("u") + self.cry.move("u") # direction should have changed self.assertNotEqual(player_direction, self.get_wram_value("PlayerDirection")) @@ -86,7 +101,7 @@ class VbaTests(unittest.TestCase): def test_movement_changes_y_coord(self): first_map_y = self.get_wram_value("MapY") - vba.crystal.move("u") + self.cry.move("u") # y location should be different second_map_y = self.get_wram_value("MapY") @@ -96,11 +111,176 @@ class VbaTests(unittest.TestCase): # should start with standing self.assertEqual(self.get_wram_value("PlayerAction"), 1) - vba.crystal.move("l") + self.cry.move("l") # should be standing player_action = self.get_wram_value("PlayerAction") self.assertEqual(player_action, 1) # 1 = standing + def test_PlaceString(self): + self.cry.call(0, 0x1078) + + # where to draw the text + self.cry.registers["hl"] = 0xc4a0 + + # what text to read from + self.cry.registers["de"] = 0x1276 + + self.cry.vba.step(count=10) + + text = self.cry.get_text() + + self.assertTrue("TRAINER" in text) + + def test_speedrunner_constructor(self): + runner = autoplayer.SpeedRunner(cry=self.cry) + + def test_speedrunner_handle_mom(self): + # TODO: why can't i pass in the current state of the emulator? + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=False) + + # confirm that handle_mom is done by attempting to move on the map + self.assertTrue(self.check_movement("d")) + + def test_speedrunner_walk_into_new_bark_town(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # test that the game is in a state such that the player can walk + self.assertTrue(self.check_movement("d")) + + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_speedrunner_handle_elm(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # go through the Elm's Lab sequence + runner.handle_elm("cyndaquil", skip=False) + + # test again if the game is in a state where the player can walk + self.assertTrue(self.check_movement("u")) + + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 5) + + def test_moving_back_and_forth(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # must be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + runner.cry.move("l") + runner.cry.move("l") + runner.cry.move("l") + runner.cry.move("d") + runner.cry.move("d") + + for x in range(0, 10): + runner.cry.move("l") + runner.cry.move("d") + runner.cry.move("r") + runner.cry.move("u") + + # must still be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_crystal_move_list(self): + runner = autoplayer.SpeedRunner(cry=None) + runner.setup() + runner.skip_intro(skip=True) + runner.handle_mom(skip=True) + runner.walk_into_new_bark_town(skip=False) + + # must be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + first_map_x = self.get_wram_value("MapX") + + runner.cry.move(["l", "l", "l"]) + + # x location should be different + second_map_x = self.get_wram_value("MapX") + self.assertNotEqual(first_map_x, second_map_x) + + # must still be in New Bark Town + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) + + def test_keyboard_typing_dumb_name(self): + self.bootstrap_name_prompt() + + name = "tRaInEr" + self.cry.write(name) + + # save this selection + self.cry.vba.press("a", hold=20) + + self.assertEqual(name, self.cry.get_player_name()) + + def test_keyboard_typing_cap_name(self): + names = [ + "trainer", + "TRAINER", + "TrAiNeR", + "tRaInEr", + "ExAmPlE", + "Chris", + "Kris", + "beepaaa", + "chris", + "CHRIS", + "Python", + "pYthon", + "pyThon", + "pytHon", + "pythOn", + "pythoN", + "python", + "PyThOn", + "Zot", + "Death", + "Hiro", + "HIRO", + ] + + self.bootstrap_name_prompt() + start_state = self.cry.vba.state + + for name in names: + print "Writing name: " + name + + self.cry.vba.state = start_state + + sequence = self.cry.write(name) + + print "sequence is: " + str(sequence) + + # save this selection + self.cry.vba.press("start", hold=20) + self.cry.vba.press("a", hold=20) + + pname = self.cry.get_player_name().replace("@", "") + self.assertEqual(name, pname) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py new file mode 100644 index 0000000..61c0297 --- /dev/null +++ b/tests/test_vba_battle.py @@ -0,0 +1,93 @@ +""" +Tests for the battle controller +""" + +import unittest + +from setup_vba import ( + vba, + autoplayer, +) + +from pokemontools.vba.battle import ( + Battle, + BattleException, +) + +from bootstrapping import ( + bootstrap, + bootstrap_trainer_battle, +) + +class BattleTests(unittest.TestCase): + cry = None + vba = None + bootstrap_state = None + + @classmethod + def setUpClass(cls): + cls.cry = vba.crystal() + cls.vba = cls.cry.vba + + cls.bootstrap_state = bootstrap_trainer_battle() + cls.vba.state = cls.bootstrap_state + + @classmethod + def tearDownClass(cls): + cls.vba.shutdown() + + def setUp(self): + # reset to whatever the bootstrapper created + self.vba.state = self.bootstrap_state + self.battle = Battle(emulator=self.cry) + self.battle.skip_start_text() + + def test_is_in_battle(self): + self.assertTrue(self.battle.is_in_battle()) + + def test_is_player_turn(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # the initial state should be the player's turn + self.assertTrue(self.battle.is_player_turn()) + + def test_is_mandatory_switch_initial(self): + # should not be asking for a switch so soon in the battle + self.assertFalse(self.battle.is_mandatory_switch()) + + def test_is_mandatory_switch(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + # set partymon1 hp to very low + self.vba.write_memory_at(0xc63c, 0) + self.vba.write_memory_at(0xc63d, 1) + + # let the enemy attack and kill the pokemon + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_mandatory_switch()) + + def test_attack_loop(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_player_turn()) + +if __name__ == "__main__": + unittest.main() |