mirror of
https://github.com/pret/pokemon-reverse-engineering-tools.git
synced 2026-03-21 17:24:42 -05:00
Merge branch 'master' of github.com:kanzure/pokemon-reverse-engineering-tools
This commit is contained in:
commit
dfc88b9ac0
46
README.md
46
README.md
|
|
@ -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
|
||||
==============================
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import configuration as config
|
||||
import crystal
|
||||
import preprocessor
|
||||
|
||||
__version__ = "1.6.0"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
Some old methods rescued from crystal.py
|
||||
"""
|
||||
|
||||
import pointers
|
||||
import pokemontools.pointers as pointers
|
||||
|
||||
map_header_byte_size = 9
|
||||
|
||||
|
|
|
|||
15
pokemontools/data/__init__.py
Normal file
15
pokemontools/data/__init__.py
Normal 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)
|
||||
2293
pokemontools/data/pokecrystal/wram.asm
Normal file
2293
pokemontools/data/pokecrystal/wram.asm
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
521
pokemontools/vba/battle.py
Normal 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
|
|
@ -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
|
||||
|
|
|
|||
2
setup.py
2
setup.py
|
|
@ -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
54
tests/bootstrapping.py
Normal 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
|
||||
|
|
@ -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
4
tests/setup_vba.py
Normal 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
|
||||
|
|
@ -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
117
tests/test_vba_battle.py
Normal 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()
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user