Merge branch 'github/master' into master

This commit is contained in:
Bryan Bishop 2013-11-11 12:38:52 -06:00
commit e0991710ef
7 changed files with 1887 additions and 683 deletions

View File

@ -4,492 +4,580 @@ Programmatic speedrun of Pokémon Crystal
"""
import os
import pokemontools.configuration as configuration
# bring in the emulator and basic tools
import vba
def main():
"""
Start the game.
"""
vba.load_rom()
# 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)
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()
# select "Boy"
vba.press("a", holdsteps=50, aftersteps=1)
# text until "What time is it?"
vba.crystal.text_wait()
# select 10 o'clock
vba.press("a", holdsteps=50, aftersteps=1)
# yes i mean it
vba.press("a", holdsteps=50, aftersteps=1)
# "How many minutes?" 0 min.
vba.press("a", holdsteps=50, aftersteps=1)
# "Who! 0 min.?" yes/no select yes
vba.press("a", holdsteps=50, aftersteps=1)
# read text until name selection
vba.crystal.text_wait()
# select "Chris"
vba.press("d", holdsteps=10, aftersteps=1)
vba.press("a", holdsteps=50, aftersteps=1)
def overworldcheck():
def setup(self):
"""
A basic check for when the game starts.
Configure this ``Runner`` instance to contain a reference to an active
emulator session.
"""
return vba.get_memory_at(0xcfb1) != 0
if not self.cry:
self.cry = _vba.crystal(config=self.config)
# go until the introduction is done
vba.crystal.text_wait(callback=overworldcheck)
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)
return
# walk to mom and handle her text
self.handle_mom(skip=True)
@skippable
def handle_mom():
"""
Walk to mom. Handle her speech and questions.
"""
# walk outside into new bark town
self.walk_into_new_bark_town(skip=True)
vba.crystal.move("r")
vba.crystal.move("r")
vba.crystal.move("r")
vba.crystal.move("r")
# walk to elm and do whatever he wants
self.handle_elm("totodile", skip=True)
vba.crystal.move("u")
vba.crystal.move("u")
vba.crystal.move("u")
self.new_bark_level_grind(17, skip=False)
vba.crystal.move("d")
vba.crystal.move("d")
@skippable
def skip_intro(self, stop_at_name_selection=False):
"""
Skip the game boot intro sequence.
"""
# move into mom's line of sight
vba.crystal.move("d")
# copyright sequence
self.cry.nstep(400)
# let mom talk until "What day is it?"
vba.crystal.text_wait()
# skip the ditto sequence
self.cry.vba.press("a")
self.cry.nstep(100)
# "What day is it?" Sunday
vba.press("a", holdsteps=10) # Sunday
# skip the start screen
self.cry.vba.press("start")
self.cry.nstep(100)
vba.crystal.text_wait()
# click "new game"
self.cry.vba.press("a", hold=50, after=1)
# "SUNDAY, is it?" yes/no
vba.press("a", holdsteps=10) # yes
# skip text up to "Are you a boy? Or are you a girl?"
self.cry.text_wait()
vba.crystal.text_wait()
# select "Boy"
self.cry.vba.press("a", hold=50, after=1)
# "Is it Daylight Saving Time now?" yes/no
vba.press("a", holdsteps=10) # yes
# text until "What time is it?"
self.cry.text_wait()
vba.crystal.text_wait()
# select 10 o'clock
self.cry.vba.press("a", hold=50, after=1)
# "AM DST, is that OK?" yes/no
vba.press("a", holdsteps=10) # yes
# yes i mean it
self.cry.vba.press("a", hold=50, after=1)
# text until "know how to use the PHONE?" yes/no
vba.crystal.text_wait()
# "How many minutes?" 0 min.
self.cry.vba.press("a", hold=50, after=1)
# press yes
vba.press("a", holdsteps=10)
# "Who! 0 min.?" yes/no select yes
self.cry.vba.press("a", hold=50, after=1)
# wait until mom is done talking
vba.crystal.text_wait()
# read text until name selection
self.cry.text_wait()
# wait until the script is done running
vba.crystal.wait_for_script_running()
if stop_at_name_selection:
return
return
# select "Chris"
self.cry.vba.press("d", hold=10, after=1)
self.cry.vba.press("a", hold=50, after=1)
@skippable
def walk_into_new_bark_town():
"""
Walk outside after talking with mom.
"""
def overworldcheck():
"""
A basic check for when the game starts.
"""
return self.cry.vba.memory[0xcfb1] != 0
vba.crystal.move("d")
vba.crystal.move("d")
vba.crystal.move("d")
vba.crystal.move("l")
vba.crystal.move("l")
# go until the introduction is done
self.cry.text_wait(callback=overworldcheck)
# walk outside
vba.crystal.move("d")
@skippable
def handle_elm(starter_choice):
"""
Walk to Elm's Lab and get a starter.
"""
# 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")
# walk into the lab
vba.crystal.move("u")
# talk to elm
vba.crystal.text_wait()
# "that I recently caught." yes/no
vba.press("a", holdsteps=10) # yes
# talk to elm some more
vba.crystal.text_wait()
# talking isn't done yet..
vba.crystal.text_wait()
vba.crystal.text_wait()
vba.crystal.text_wait()
# wait until the script is done running
vba.crystal.wait_for_script_running()
# move toward the pokeballs
vba.crystal.move("r")
# move to cyndaquil
vba.crystal.move("r")
moves = 0
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")
# face the pokeball
vba.crystal.move("u")
# select it
vba.press("a", holdsteps=10, aftersteps=0)
# wait for the image to pop up
vba.crystal.text_wait()
# wait for the image to close
vba.crystal.text_wait()
# wait for the yes/no box
vba.crystal.text_wait()
# press yes
vba.press("a", holdsteps=10, aftersteps=0)
# wait for elm to talk a bit
vba.crystal.text_wait()
# TODO: why didn't that finish his talking?
vba.crystal.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 this wait until he was completely done?
vba.crystal.text_wait()
vba.crystal.text_wait()
# get the phone number
vba.crystal.text_wait()
# talk with elm a bit more
vba.crystal.text_wait()
# TODO: and again.. wtf?
vba.crystal.text_wait()
# wait until the script is done running
vba.crystal.wait_for_script_running()
# move down
vba.crystal.move("d")
vba.crystal.move("d")
vba.crystal.move("d")
vba.crystal.move("d")
# move into the researcher's line of sight
vba.crystal.move("d")
# get the potion from the person
vba.crystal.text_wait()
vba.crystal.text_wait()
# wait for the script to end
vba.crystal.wait_for_script_running()
vba.crystal.move("d")
vba.crystal.move("d")
vba.crystal.move("d")
# go outside
vba.crystal.move("d")
return
@skippable
def new_bark_level_grind(level):
"""
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..
"""
# walk to the grass area
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)
# wait for wild battle to completely start
vba.crystal.text_wait()
attacks = 5
while attacks > 0:
# FIGHT
vba.press("a", holdsteps=10, aftersteps=1)
# wait to select a move
vba.crystal.text_wait()
# SCRATCH
vba.press("a", holdsteps=10, aftersteps=1)
# wait for the move to be over
vba.crystal.text_wait()
hp = ((vba.get_memory_at(0xd218) << 8) | vba.get_memory_at(0xd217))
print "enemy hp is: " + str(hp)
if hp == 0:
print "enemy hp is zero, exiting"
break
else:
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
@skippable
def new_bark_level_grind_walk_to_grass():
@skippable
def handle_mom(self):
"""
Walk to mom. Handle her speech and questions.
"""
self.cry.move("r")
self.cry.move("r")
self.cry.move("r")
self.cry.move("r")
self.cry.move("u")
self.cry.move("u")
self.cry.move("u")
self.cry.move("d")
self.cry.move("d")
# move into mom's line of sight
self.cry.move("d")
# let mom talk until "What day is it?"
self.cry.text_wait()
# "What day is it?" Sunday
self.cry.vba.press("a", hold=10) # Sunday
self.cry.text_wait()
# "SUNDAY, is it?" yes/no
self.cry.vba.press("a", hold=10) # yes
self.cry.text_wait()
# "Is it Daylight Saving Time now?" yes/no
self.cry.vba.press("a", hold=10) # yes
self.cry.text_wait()
# "AM DST, is that OK?" yes/no
self.cry.vba.press("a", hold=10) # yes
# text until "know how to use the PHONE?" yes/no
self.cry.text_wait()
# press yes
self.cry.vba.press("a", hold=10)
# wait until mom is done talking
self.cry.text_wait()
# wait until the script is done running
self.cry.wait_for_script_running()
return
@skippable
def walk_into_new_bark_town(self):
"""
Walk outside after talking with mom.
"""
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")
moves = 0
if starter_choice.lower() == "cyndaquil":
moves = 0
elif starter_choice.lower() == "totodile":
moves = 1
else:
moves = 2
for each in range(0, moves):
self.cry.move("r")
# face the pokeball
self.cry.move("u")
# select it
self.cry.vba.press("a", hold=10, after=0)
# wait for the image to pop up
self.cry.text_wait()
# wait for the image to close
self.cry.text_wait()
# wait for the yes/no box
self.cry.text_wait()
# press yes
self.cry.vba.press("a", hold=10, after=0)
# wait for elm to talk a bit
self.cry.text_wait()
# TODO: why didn't that finish his talking?
self.cry.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
# TODO: why didn't this wait until he was completely done?
self.cry.text_wait()
self.cry.text_wait()
# get the phone number
self.cry.text_wait()
# talk with elm a bit more
self.cry.text_wait()
# wait until the script is done running
self.cry.wait_for_script_running()
# 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
self.cry.move("d")
# get the potion from the person
self.cry.text_wait()
self.cry.text_wait()
# wait for the script to end
self.cry.wait_for_script_running()
self.cry.move("d")
self.cry.move("d")
self.cry.move("d")
# go outside
self.cry.move("d")
return
@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..
"""
# walk to the grass area
if walk_to_grass:
self.new_bark_level_grind_walk_to_grass(skip=False)
last_direction = "u"
# 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"
self.cry.move(direction)
last_direction = direction
# wait for wild battle to completely start
self.cry.text_wait()
attacks = 5
while attacks > 0:
# FIGHT
self.cry.vba.press("a", hold=10, after=1)
# wait to select a move
self.cry.text_wait()
# 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)
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")
# 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()
vba.crystal.move("d")
vba.crystal.move("d")
# skip=False means always run the skip_intro function regardless of the
# presence of a saved after state.
runner.skip_intro(skip=True)
vba.crystal.move("l")
vba.crystal.move("l")
# keep a reference of the current state
state = runner.cry.vba.state
vba.crystal.move("d")
vba.crystal.move("d")
runner.cry.vba.shutdown()
vba.crystal.move("l")
vba.crystal.move("l")
return state
# 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")
def main():
"""
Setup a basic ``SpeedRunner`` instance and then run the runner.
"""
runner = SpeedRunner()
runner.setup()
return runner.main()
if __name__ == "__main__":
main()

193
pokemontools/vba/battle.py Normal file
View File

@ -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)

File diff suppressed because it is too large Load Diff

54
tests/bootstrapping.py Normal file
View File

@ -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

4
tests/setup_vba.py Normal file
View File

@ -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

View File

@ -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()
# skip=False means run the skip_intro function instead of just skipping to
# a saved state.
vba_autoplayer.skip_intro()
state = vba.get_state()
# clean everything up again
vba.shutdown()
return state
self.assertEqual(len(expected_result), len(button_sequence))
self.assertEqual(expected_result, button_sequence)
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()
cry = None
wram = None
# FIXME: work around jython2.5 unittest
state = bootstrap()
wram = setup_wram()
@classmethod
def setUpClass(cls):
cls.bootstrap_state = bootstrap()
def get_wram_value(self, name):
return vba.get_memory_at(self.wram[name])
cls.wram = setup_wram()
cls.cry = vba.crystal()
cls.vba = cls.cry.vba
cls.vba.state = cls.bootstrap_state
@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 tearDown(self):
vba.shutdown()
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)
# 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()

93
tests/test_vba_battle.py Normal file
View File

@ -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()