Merge branch 'master' of github.com:kanzure/pokemon-reverse-engineering-tools

This commit is contained in:
yenatch 2013-11-18 21:03:31 -05:00
commit dfc88b9ac0
19 changed files with 4703 additions and 701 deletions

View File

@ -1,3 +1,49 @@
pokemontools
==============================
``pokemontools`` is a python module that provides various reverse engineering
components for various Pokémon games. This includes:
* a utility to disassemble bytes from games into asm
* map editor
* python bindings for Pokémon games running in the vba-linux emulator
* in-game graphics converter (png, lz, 2bpp)
* preprocessor that dumps out rgbds-compatible asm
* stuff that parses and dumps data from ROMs
# installing
To install this python library in ``site-packages``:
```
pip install --upgrade pokemontools
```
And for local development work:
```
python setup.py develop
```
And of course local installation:
```
python setup.py install
```
# testing
Run the tests with:
```
nosetests-2.7
```
# see also
* [Pokémon Crystal source code](https://github.com/kanzure/pokecrystal)
* [Pokémon Red source code](https://github.com/iimarckus/pokered)
Pokémon Crystal utilities and extras
==============================

View File

@ -1,3 +1,5 @@
import configuration as config
import crystal
import preprocessor
__version__ = "1.6.0"

View File

@ -8,14 +8,19 @@ from gbz80disasm import get_global_address, get_local_address
import crystal
from crystal import music_classes as sound_classes
from crystal import Command
from crystal import load_rom
from crystal import (
Command,
SingleByteParam,
MultiByteParam,
load_rom,
)
rom = load_rom()
rom = bytearray(rom)
import config
conf = config.Config()
import configuration
conf = configuration.Config()
def sort_asms(asms):
@ -209,7 +214,7 @@ class Channel:
for class_ in sound_classes:
if class_.id == i:
return class_
if self.channel in [4. 8]: return Noise
if self.channel in [4, 8]: return Noise
return Note

View File

@ -2,7 +2,7 @@
Some old methods rescued from crystal.py
"""
import pointers
import pokemontools.pointers as pointers
map_header_byte_size = 9

View File

@ -0,0 +1,15 @@
"""
Access to data files.
"""
# hide the os import
import os as _os
# path to where these files are located
path = _os.path.abspath(_os.path.dirname(__file__))
def join(filename, path=path):
"""
Construct the absolute path to the file.
"""
return _os.path.join(path, filename)

File diff suppressed because it is too large Load Diff

View File

@ -5,12 +5,15 @@ import sys
import png
from math import sqrt, floor, ceil
import crystal
import configuration
config = configuration.Config()
import pokemon_constants
import trainers
import romstr
if __name__ != "__main__":
rom = crystal.load_rom()
rom = romstr.RomStr(filename=config.rom_path)
def split(list_, interval):

View File

@ -6,8 +6,8 @@ from ttk import Frame, Style
import PIL
from PIL import Image, ImageTk
import config
conf = config.Config()
import configuration
conf = configuration.Config()
#version = 'crystal'

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

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

@ -0,0 +1,521 @@
"""
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() or self.is_switch_prompt() or self.is_levelup_screen() or self.is_make_room_for_move_prompt()
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 select_battle_menu_action(self, action, execute=True):
"""
Moves the cursor to the requested action and selects it.
:param action: fight, pkmn, pack, run
"""
if not self.is_fight_pack_run_menu():
raise Exception(
"This isn't the fight-pack-run menu."
)
action = action.lower()
action_map = {
"fight": (1, 1),
"pkmn": (1, 2),
"pack": (2, 1),
"run": (2, 2),
}
if action not in action_map.keys():
raise Exception(
"Unexpected requested action {0}".format(action)
)
current_row = self.emulator.vba.read_memory_at(0xcfa9)
current_column = self.emulator.vba.read_memory_at(0xcfaa)
direction = None
if current_row != action_map[action][0]:
if current_row > action_map[action][0]:
direction = "u"
elif current_row < action_map[action][0]:
direction = "d"
self.emulator.vba.press(direction, hold=5, after=10)
direction = None
if current_column != action_map[action][1]:
if current_column > action_map[action][1]:
direction = "l"
elif current_column < action_map[action][1]:
direction = "r"
self.emulator.vba.press(direction, hold=5, after=10)
# now select the action
if execute:
self.emulator.vba.press("a", hold=5, after=100)
def select_attack(self, move_number=1, hold=5, after=10):
"""
Moves the cursor to the correct attack in the menu and presses the
button.
:param move_number: the attack number on the FIGHT menu. Note that this
starts from 1.
:param hold: how many frames to hold each button press
:param after: how many frames to wait after each button press
"""
# TODO: detect fight menu and make sure it's detected here.
pp_address = 0xc634 + (move_number - 1)
pp = self.emulator.vba.read_memory_at(pp_address)
# detect zero pp because i don't want to write a way to inform the
# caller that there was no more pp. Just check the pp yourself.
if pp == 0:
raise BattleException(
"Move {num} has no more PP.".format(
num=move_number,
)
)
valid_selection_states = (1, 2, 3, 4)
selection = self.emulator.vba.read_memory_at(0xcfa9)
while selection != move_number:
if selection not in valid_selection_states:
raise BattleException(
"The current selected attack is out of bounds: {num}".format(
num=selection,
)
)
direction = None
if selection > move_number:
direction = "d"
elif selection < move_number:
direction = "u"
else:
# probably never happens
raise BattleException(
"Not sure what to do here."
)
# press the arrow button
self.emulator.vba.press(direction, hold=hold, after=after)
# let's see what the current selection is
selection = self.emulator.vba.read_memory_at(0xcfa9)
# press to choose the attack
self.emulator.vba.press("a", hold=hold, after=after)
def fight(self, move_number):
"""
Select FIGHT from the flight-pack-run menu and select the move
identified by move_number.
"""
# make sure the menu is detected
if not self.is_fight_pack_run_menu():
raise BattleException(
"Wrong menu. Can't press FIGHT here."
)
# select FIGHT
self.select_battle_menu_action("fight")
# select the requested attack
self.select_attack(move_number)
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_trainer_switch_prompt(self):
"""
Detects if the battle is waiting for the player to choose whether or
not to switch pokemon. This is the prompt that asks yes/no for whether
to switch pokemon, like if the trainer is switching pokemon at the end
of a turn set.
"""
return self.emulator.is_trainer_switch_prompt()
def is_wild_switch_prompt(self):
"""
Detects if the battle is waiting for the player to choose whether or
not to continue to fight the wild pokemon.
"""
return self.emulator.is_wild_switch_prompt()
def is_switch_prompt(self):
"""
Detects both trainer and wild switch prompts (for prompting whether to
switch pokemon). This is a yes/no box and not the actual pokemon
selection menu.
"""
return self.is_trainer_switch_prompt() or self.is_wild_switch_prompt()
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 is_levelup_screen(self):
"""
Detects the levelup stats screen.
"""
# This is implemented as reading some text on the screen instead of
# using get_text() because checking every loop is really slow.
address = 0xc50f
values = [146, 143, 130, 139]
for (index, value) in enumerate(values):
if self.emulator.vba.read_memory_at(address + index) != value:
return False
else:
return True
def is_evolution_screen(self):
"""
What? MEW is evolving!
"""
address = 0xc5e4
values = [164, 181, 174, 171, 181, 168, 173, 166, 231]
for (index, value) in enumerate(values):
if self.emulator.vba.read_memory_at(address + index) != value:
return False
else:
# also check "What?"
what_address = 0xc5b9
what_values = [150, 167, 160, 179, 230]
for (index, value) in enumerate(what_values):
if self.emulator.vba.read_memory_at(what_address + index) != value:
return False
else:
return True
def is_evolved_screen(self):
"""
Checks if the screen is the "evolved into METAPOD!" screen. Note that
this only works inside of a battle. This is because there may be other
text boxes that have the same text when outside of battle. But within a
battle, this is probably the only time the text "evolved into ... !" is
seen.
"""
if not self.is_in_battle():
return False
address = 0x4bb1
values = [164, 181, 174, 171, 181, 164, 163, 127, 168, 173, 179, 174, 79]
for (index, value) in enumerate(values):
if self.emulator.vba.read_memory_at(address + index) != value:
return False
else:
return True
def is_make_room_for_move_prompt(self):
"""
Detects the prompt that asks whether to make room for a move.
"""
if not self.is_in_battle():
return False
address = 0xc5b9
values = [172, 174, 181, 164, 127, 179, 174, 127, 172, 160, 170, 164, 127, 177, 174, 174, 172]
for (index, value) in enumerate(values):
if self.emulator.vba.read_memory_at(address + index) != value:
return False
else:
return True
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.
"""
# callback causes text_wait to exit when the callback returns True
def is_in_battle_checker():
result = (self.emulator.vba.read_memory_at(0xd22d) == 0) and (self.emulator.vba.read_memory_at(0xc734) != 0)
# but also, jump out if it's the stats screen
result = result or self.is_levelup_screen()
# jump out if it's the "make room for a new move" screen
result = result or self.is_make_room_for_move_prompt()
# stay in text_wait if it's the evolution screen
result = result and not self.is_evolution_screen()
return result
while not self.is_input_required() and self.is_in_battle():
self.emulator.text_wait(callback=is_in_battle_checker)
# 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()
# skip a few hundred frames
self.emulator.vba.step(count=100)
wild = (self.emulator.vba.read_memory_at(0xd22d) == 1)
while self.is_in_battle():
self.skip_until_input_required()
if not self.is_in_battle():
continue
if self.is_player_turn():
# battle hook provides input to handle this situation
self.handle_turn()
elif self.is_trainer_switch_prompt():
self.handle_trainer_switch_prompt()
elif self.is_wild_switch_prompt():
self.handle_wild_switch_prompt()
elif self.is_mandatory_switch():
# battle hook provides input to handle this situation too
self.handle_mandatory_switch()
elif self.is_levelup_screen():
self.emulator.vba.press("a", hold=5, after=30)
elif self.is_evolved_screen():
self.emulator.vba.step(count=30)
elif self.is_make_room_for_move_prompt():
self.handle_make_room_for_move()
else:
raise BattleException("unknown state, aborting")
# "how did i lose? wah"
# TODO: this doesn't happen for wild battles
if not wild:
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_trainer_switch_prompt(self):
"""
The trainer is switching pokemon. The game asks yes/no for whether or
not the player would like to switch.
"""
raise NotImplementedError
def handle_wild_switch_prompt(self):
"""
The wild pokemon defeated the party pokemon. This is the yes/no box for
whether to switch pokemon or not.
"""
raise NotImplementedError
def handle_turn(self):
"""
Take actions inside of a battle based on the game state.
"""
raise NotImplementedError
class BattleStrategy(Battle):
"""
This class shows the relevant methods to make a battle handler.
"""
def handle_mandatory_switch(self):
"""
Something fainted, pick the next mon.
"""
raise NotImplementedError
def handle_trainer_switch_prompt(self):
"""
The trainer is switching pokemon. The game asks yes/no for whether or
not the player would like to switch.
"""
raise NotImplementedError
def handle_wild_switch_prompt(self):
"""
The wild pokemon defeated the party pokemon. This is the yes/no box for
whether to switch pokemon or not.
"""
raise NotImplementedError
def handle_turn(self):
"""
Take actions inside of a battle based on the game state.
"""
raise NotImplementedError
def handle_make_room_for_move(self):
"""
Choose yes/no then handle learning the move.
"""
raise NotImplementedError
class SpamBattleStrategy(BattleStrategy):
"""
A really simple battle strategy that always picks the first move of the
first pokemon to attack the enemy.
"""
def handle_turn(self):
"""
Always picks the first move of the current pokemon.
"""
self.fight(1)
def handle_trainer_switch_prompt(self):
"""
The trainer is switching pokemon. The game asks yes/no for whether or
not the player would like to switch.
"""
# decline
self.emulator.vba.press(["b"], hold=5, after=10)
def handle_wild_switch_prompt(self):
"""
The wild pokemon defeated the party pokemon. This is the yes/no box for
whether to switch pokemon or not.
"""
# why not just make a battle strategy that doesn't lose?
# TODO: Note that the longer "after" value is required here.
self.emulator.vba.press("a", hold=5, after=30)
self.handle_mandatory_switch()
def handle_mandatory_switch(self):
"""
Something fainted, pick the next mon.
"""
# TODO: make a better selector for which pokemon.
# now scroll down
self.emulator.vba.press("d", hold=5, after=10)
# select this mon
self.emulator.vba.press("a", hold=5, after=30)
def handle_make_room_for_move(self):
"""
Choose yes/no then handle learning the move.
"""
# make room? no
self.emulator.vba.press("b", hold=5, after=100)
# stop learning? yes
self.emulator.vba.press("a", hold=5, after=20)
self.emulator.text_wait()

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,10 @@ RGBDS BSS section and constant parsing.
import os
# TODO: parse these constants from constants.asm
NUM_OBJECTS = 0x10
OBJECT_LENGTH = 0x10
def make_wram_labels(wram_sections):
wram_labels = {}
for section in wram_sections:
@ -108,6 +112,8 @@ class WRAMProcessor(object):
self.setup_hram_constants()
self.setup_gbhw_constants()
self.reformat_wram_labels()
def read_wram_sections(self):
"""
Opens the wram file and calls read_bss_sections.
@ -162,3 +168,14 @@ class WRAMProcessor(object):
"""
self.gbhw_constants = self.read_gbhw_constants()
return self.gbhw_constants
def reformat_wram_labels(self):
"""
Flips the wram_labels dictionary the other way around to access
addresses by label.
"""
self.wram = {}
for (address, labels) in self.wram_labels.iteritems():
for label in labels:
self.wram[label] = address

View File

@ -24,7 +24,7 @@ requires = [
setup(
name="pokemontools",
version="1.4.1",
version="1.6.0",
description="Tools for compiling and disassembling Pokémon Red and Pokémon Crystal.",
long_description=open("README.md", "r").read(),
license="BSD",

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

View File

@ -42,6 +42,10 @@ from pokemontools.helpers import (
index,
)
from pokemontools.crystalparts.old_parsers import (
old_parse_map_header_at,
)
from pokemontools.crystal import (
rom,
load_rom,
@ -65,7 +69,6 @@ from pokemontools.crystal import (
all_labels,
write_all_labels,
parse_map_header_at,
old_parse_map_header_at,
process_00_subcommands,
parse_all_map_headers,
translate_command_byte,

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

117
tests/test_vba_battle.py Normal file
View File

@ -0,0 +1,117 @@
"""
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.cry.set_battle_mon_hp(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())
def test_is_battle_switch_prompt(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 enemy hp to very low
self.cry.lower_enemy_hp()
# attack the enemy and kill it
self.battle.skip_until_input_required()
# yes/no menu is present, should be detected
self.assertTrue(self.battle.is_trainer_switch_prompt())
# and input should be required
self.assertTrue(self.battle.is_input_required())
# but it's not mandatory
self.assertFalse(self.battle.is_mandatory_switch())
if __name__ == "__main__":
unittest.main()

View File

@ -38,6 +38,10 @@ from pokemontools.labels import (
find_labels_without_addresses,
)
from pokemontools.crystalparts.old_parsers import (
old_parse_map_header_at,
)
from pokemontools.helpers import (
grouper,
index,
@ -66,7 +70,6 @@ from pokemontools.crystal import (
all_labels,
write_all_labels,
parse_map_header_at,
old_parse_map_header_at,
process_00_subcommands,
parse_all_map_headers,
translate_command_byte,