summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryan Bishop <kanzure@gmail.com>2013-11-11 12:38:52 -0600
committerBryan Bishop <kanzure@gmail.com>2013-11-11 12:38:52 -0600
commite0991710ef720ee73bc8042671ba92159002f236 (patch)
treef8cdf8595411ad8b16eca1f293a1c3e724eef45c
parentfcfde94ba84a6a29bb22dd97b62af2e3c72276bd (diff)
parenta11b084a2824dbe9c1df84d9ea205b8495f3da13 (diff)
Merge branch 'github/master' into master
-rw-r--r--pokemontools/vba/autoplayer.py820
-rw-r--r--pokemontools/vba/battle.py193
-rw-r--r--pokemontools/vba/vba.py1024
-rw-r--r--tests/bootstrapping.py54
-rw-r--r--tests/setup_vba.py4
-rw-r--r--tests/test_vba.py276
-rw-r--r--tests/test_vba_battle.py93
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()