From 51ff03e95298fbb42f860e1fb896ea881a71a0d2 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:42:54 -0500 Subject: fix up some import lines --- pokemontools/vba/vba.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d7fdf1d..9569bc2 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -12,9 +12,13 @@ from copy import copy import unittest # for converting bytes to readable text -from pokemontools.chars import chars +from pokemontools.chars import ( + chars, +) -from pokemontools.map_names import map_names +from pokemontools.map_names import ( + map_names, +) import keyboard -- cgit v1.2.3 From ef1709a8bed631e1b01cffb65fb651d8f60938c5 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:47:39 -0500 Subject: remove the custom press() implementation This is now handled in vba_wrapper. --- pokemontools/vba/vba.py | 31 +++++-------------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 9569bc2..dbbe3c3 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -46,27 +46,6 @@ def translate_chars(charz): result += chars[each] return result -def press(buttons, holdsteps=1, aftersteps=1): - """ - Press a button. - - Use steplimit to say for how many steps you want to press - the button (try leaving it at the default, 1). - """ - if hasattr(buttons, "__len__"): - number = button_combiner(buttons) - elif isinstance(buttons, int): - number = buttons - else: - number = buttons - for step_counter in range(0, holdsteps): - Gb.step(number) - - # clear the button press - if aftersteps > 0: - for step_counter in range(0, aftersteps): - Gb.step(0) - def call(bank, address): """ Jumps into a function at a certain address. @@ -148,7 +127,7 @@ class crystal: # set CurSFX set_memory_at(0xc2bf, 0) - press("a", holdsteps=10, aftersteps=1) + vba.press("a", hold=10, after=1) # check if CurSFX is SFX_READ_TEXT_2 if get_memory_at(0xc2bf) == 0x8: @@ -415,9 +394,9 @@ class crystal: Applies a sequence of buttons to the on-screen keyboard. """ for buttons in button_sequence: - press(buttons) + vba.press(buttons) nstep(2) - press([]) + vba.press([]) @staticmethod def write(something="TrAiNeR"): @@ -493,8 +472,8 @@ class crystal: """ Attempt to move the player. """ - press(cmd, holdsteps=10, aftersteps=0) - press([]) + vba.press(cmd, hold=10, after=0) + vba.press([]) memory = get_memory() #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: -- cgit v1.2.3 From 115ce6781e2750d3b326e6a994677c80d0613029 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:52:44 -0500 Subject: use vba.step() instead of step() --- pokemontools/vba/vba.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index dbbe3c3..b2c164d 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -162,7 +162,7 @@ class crystal: break else: - nstep(step_size) + vba.step(count=step_size) # if there is a callback, then call the callback and exit when the # callback returns True. This is especially useful during the @@ -223,7 +223,7 @@ class crystal: for step_counter in range(0, steplimit): crystal.walk_through_walls() #call(0x1, 0x1078) - step() + vba.step() @staticmethod def disable_triggers(): @@ -395,7 +395,7 @@ class crystal: """ for buttons in button_sequence: vba.press(buttons) - nstep(2) + vba.step(count=2) vba.press([]) @staticmethod @@ -479,7 +479,7 @@ class crystal: #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: while memory[0xd043] in [0, 1, 2, 3]: #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: - nstep(10) + vba.step(count=10) memory = get_memory() class TestEmulator(unittest.TestCase): @@ -492,7 +492,7 @@ class TestEmulator(unittest.TestCase): # what text to read from registers["de"] = 0x1276 - nstep(10) + vba.step(count=10) text = crystal.get_text() -- cgit v1.2.3 From 48896b14568aac35955baee6d5f47a5701f7825b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:53:34 -0500 Subject: replace one more step() with vba.step() --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index b2c164d..814c853 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -459,7 +459,7 @@ class crystal: print "script is done executing" return else: - step() + vba.step() if debug: limit = limit - 1 -- cgit v1.2.3 From 645cce62c8e7ea3784c7f493f7ad6413bcd64ba8 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:55:37 -0500 Subject: set_memory_at -> vba.write_memory_at The set_memory_at function was moved into vba_wrapper. There's no reason for that one to be defined in pokemontools. --- pokemontools/vba/vba.py | 62 ++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 814c853..75b3aad 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -64,8 +64,8 @@ def call(bank, address): for value in push: registers.sp -= 2 - set_memory_at(registers.sp + 1, value >> 8) - set_memory_at(registers.sp, value & 0xFF) + vba.write_memory_at(registers.sp + 1, value >> 8) + vba.write_memory_at(registers.sp, value & 0xFF) if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) print "actual memory values: " + str(get_memory_range(registers.sp , 2)) @@ -125,7 +125,7 @@ class crystal: print "pressing, then breaking.. address is: " + str(hex(address)) # set CurSFX - set_memory_at(0xc2bf, 0) + vba.write_memory_at(0xc2bf, 0) vba.press("a", hold=10, after=1) @@ -204,14 +204,14 @@ class crystal: to be executed each step/tick if continuous walk-through-walls is desired. """ - set_memory_at(0xC2FA, 0) - set_memory_at(0xC2FB, 0) - set_memory_at(0xC2FC, 0) - set_memory_at(0xC2FD, 0) + vba.write_memory_at(0xC2FA, 0) + vba.write_memory_at(0xC2FB, 0) + vba.write_memory_at(0xC2FC, 0) + vba.write_memory_at(0xC2FD, 0) #@staticmethod #def set_enemy_level(level): - # set_memory_at(0xd213, level) + # vba.write_memory_at(0xd213, level) @staticmethod def nstep(steplimit=500): @@ -227,13 +227,13 @@ class crystal: @staticmethod def disable_triggers(): - set_memory_at(0x23c4, 0xAF) - set_memory_at(0x23d0, 0xAF); + vba.write_memory_at(0x23c4, 0xAF) + vba.write_memory_at(0x23d0, 0xAF); @staticmethod def disable_callbacks(): - set_memory_at(0x23f2, 0xAF) - set_memory_at(0x23fe, 0xAF) + vba.write_memory_at(0x23f2, 0xAF) + vba.write_memory_at(0x23fe, 0xAF) @staticmethod def get_map_group_id(): @@ -279,7 +279,7 @@ class crystal: This probably works on other menus. """ - set_memory_at(0xcfa9, id) + vba.write_memory_at(0xcfa9, id) @staticmethod def is_in_battle(): @@ -300,10 +300,10 @@ class crystal: Note: this might start at 0xDCA4 (minus one on all addresses), but not sure. """ - set_memory_at(0xDCA5, 0xFF) - set_memory_at(0xDCA6, 0xFF) - set_memory_at(0xDCA7, 0xFF) - set_memory_at(0xDCA8, 0xFF) + vba.write_memory_at(0xDCA5, 0xFF) + vba.write_memory_at(0xDCA6, 0xFF) + vba.write_memory_at(0xDCA7, 0xFF) + vba.write_memory_at(0xDCA8, 0xFF) @staticmethod def get_gender(): @@ -329,14 +329,14 @@ class crystal: @staticmethod def warp(map_group_id, map_id, x, y): - set_memory_at(0xdcb5, map_group_id) - set_memory_at(0xdcb6, map_id) - set_memory_at(0xdcb7, y) - set_memory_at(0xdcb8, x) - set_memory_at(0xd001, 0xFF) - set_memory_at(0xff9f, 0xF1) - set_memory_at(0xd432, 1) - set_memory_at(0xd434, 0 & 251) + vba.write_memory_at(0xdcb5, map_group_id) + vba.write_memory_at(0xdcb6, map_id) + vba.write_memory_at(0xdcb7, y) + vba.write_memory_at(0xdcb8, x) + vba.write_memory_at(0xd001, 0xFF) + vba.write_memory_at(0xff9f, 0xF1) + vba.write_memory_at(0xd432, 1) + vba.write_memory_at(0xd434, 0 & 251) @staticmethod def warp_pokecenter(): @@ -346,16 +346,16 @@ class crystal: @staticmethod def masterballs(): # masterball - set_memory_at(0xd8d8, 1) - set_memory_at(0xd8d9, 99) + vba.write_memory_at(0xd8d8, 1) + vba.write_memory_at(0xd8d9, 99) # ultraball - set_memory_at(0xd8da, 2) - set_memory_at(0xd8db, 99) + vba.write_memory_at(0xd8da, 2) + vba.write_memory_at(0xd8db, 99) # pokeballs - set_memory_at(0xd8dc, 5) - set_memory_at(0xd8dd, 99) + vba.write_memory_at(0xd8dc, 5) + vba.write_memory_at(0xd8dd, 99) @staticmethod def get_text(): -- cgit v1.2.3 From fecf601fd9fa912bcd9c374eb7db8901f9e6e791 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 15:57:24 -0500 Subject: get_memory_at -> vba.read_memory_at --- pokemontools/vba/vba.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 75b3aad..e0ebe61 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -69,7 +69,7 @@ def call(bank, address): if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) print "actual memory values: " + str(get_memory_range(registers.sp , 2)) - print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(get_memory_at(registers.sp)) + print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) if bank != 0: registers["af"] = (bank << 8) | (registers["af"] & 0xFF) @@ -87,8 +87,8 @@ def get_stack(): for x in range(0, 11): sp = sp - (2 * x) - hi = get_memory_at(sp + 1) - lo = get_memory_at(sp) + hi = vba.read_memory_at(sp + 1) + lo = vba.read_memory_at(sp) address = ((hi << 8) | lo) addresses.append(address) @@ -117,8 +117,8 @@ class crystal: :param max_wait: number of wait loops to perform """ while max_wait > 0: - hi = get_memory_at(registers.sp + 1) - lo = get_memory_at(registers.sp) + hi = vba.read_memory_at(registers.sp + 1) + lo = vba.read_memory_at(registers.sp) address = ((hi << 8) | lo) if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): # 0xaef: @@ -130,7 +130,7 @@ class crystal: vba.press("a", hold=10, after=1) # check if CurSFX is SFX_READ_TEXT_2 - if get_memory_at(0xc2bf) == 0x8: + if vba.read_memory_at(0xc2bf) == 0x8: print "cursfx is set to SFX_READ_TEXT_2, looping.." return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) else: @@ -169,7 +169,7 @@ class crystal: # OakSpeech intro where textboxes are running constantly, and then # suddenly the player can move around. One way to detect that is to # set callback to a function that returns - # "vba.get_memory_at(0xcfb1) != 0". + # "vba.read_memory_at(0xcfb1) != 0". if callback != None: result = callback() if result == True: @@ -240,14 +240,14 @@ class crystal: """ Returns the current map group. """ - return get_memory_at(0xdcb5) + return vba.read_memory_at(0xdcb5) @staticmethod def get_map_id(): """ Returns the map number of the current map. """ - return get_memory_at(0xdcb6) + return vba.read_memory_at(0xdcb6) @staticmethod def get_map_name(): @@ -265,8 +265,8 @@ class crystal: Relative to top-left corner of map. """ - x = get_memory_at(0xdcb8) - y = get_memory_at(0xdcb7) + x = vba.read_memory_at(0xdcb8) + y = vba.read_memory_at(0xdcb7) return (x, y) @staticmethod @@ -286,11 +286,11 @@ class crystal: """ Checks whether or not we're in a battle. """ - return (get_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() + return (vba.read_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() @staticmethod def is_in_link_battle(): - return get_memory_at(0xc2dc) != 0 + return vba.read_memory_at(0xc2dc) != 0 @staticmethod def unlock_flypoints(): @@ -310,7 +310,7 @@ class crystal: """ Returns 'male' or 'female'. """ - gender = get_memory_at(0xD472) + gender = vba.read_memory_at(0xD472) if gender == 0: return "male" elif gender == 1: @@ -455,7 +455,7 @@ class crystal: Wait until ScriptRunning isn't -1. """ while limit > 0: - if get_memory_at(0xd438) != 255: + if vba.read_memory_at(0xd438) != 255: print "script is done executing" return else: -- cgit v1.2.3 From d29f193037cad147fe99aca6216a2e35346f5780 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 16:02:10 -0500 Subject: placeholder for get_memory_range But really, the old calls to get_memory_range should just be replaced with code that uses vba.memory[:] directly. --- pokemontools/vba/vba.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index e0ebe61..e107d90 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -40,6 +40,13 @@ registers = vba_wrapper.core.registers.Registers(vba) button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine +def get_memory_range(address, length): + """ + This is just a lame way to avoid converting some of the old + get_memory_range calls to use the vba.memory property. + """ + return list(vba.memory[address:address+length]) + def translate_chars(charz): result = "" for each in charz: -- cgit v1.2.3 From 55a436a1445027f7bf512cbf2b54392bbc0f99e0 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 16:07:51 -0500 Subject: fix some memory manipulation to use vba_wrapper --- pokemontools/vba/vba.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index e107d90..8fdff08 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -194,12 +194,12 @@ class crystal: @staticmethod def walk_through_walls_slow(): - memory = get_memory() + memory = vba.memory memory[0xC2FA] = 0 memory[0xC2FB] = 0 memory[0xC2FC] = 0 memory[0xC2FD] = 0 - set_memory(memory) + vba.memory = memory @staticmethod def walk_through_walls(): @@ -420,7 +420,7 @@ class crystal: """ This causes corruption, so it's not working yet. """ - memory = get_memory() + memory = vba.memory memory[0xdcd7] = 2 memory[0xdcd9] = 0x7 @@ -454,7 +454,7 @@ class crystal: memory[0xdd33] = 0x10 memory[0xdd34] = 0x40 - set_memory(memory) + vba.memory = memory @staticmethod def wait_for_script_running(debug=False, limit=1000): @@ -482,12 +482,12 @@ class crystal: vba.press(cmd, hold=10, after=0) vba.press([]) - memory = get_memory() + memory = vba.memory #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: while memory[0xd043] in [0, 1, 2, 3]: #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: vba.step(count=10) - memory = get_memory() + memory = vba.memory class TestEmulator(unittest.TestCase): def test_PlaceString(self): -- cgit v1.2.3 From 7f5151989b504d9561f041aaacff9916a9c68568 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:29:52 -0500 Subject: pass vba into get_memory_range --- pokemontools/vba/vba.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 8fdff08..b403236 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -40,7 +40,7 @@ registers = vba_wrapper.core.registers.Registers(vba) button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine -def get_memory_range(address, length): +def get_memory_range(vba, address, length): """ This is just a lame way to avoid converting some of the old get_memory_range calls to use the vba.memory property. @@ -73,9 +73,9 @@ def call(bank, address): registers.sp -= 2 vba.write_memory_at(registers.sp + 1, value >> 8) vba.write_memory_at(registers.sp, value & 0xFF) - if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: + if get_memory_range(vba, registers.sp, 2) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(get_memory_range(registers.sp , 2)) + print "actual memory values: " + str(get_memory_range(vba, registers.sp, 2)) print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) if bank != 0: @@ -330,7 +330,7 @@ class crystal: """ Returns the 7 characters making up the player's name. """ - bytez = get_memory_range(0xD47D, 7) + bytez = get_memory_range(vba, 0xD47D, 7) name = translate_chars(bytez) return name @@ -372,7 +372,7 @@ class crystal: Other characters will not be shown. """ output = "" - tiles = get_memory_range(0xc4a0, 1000) + tiles = get_memory_range(vba, 0xc4a0, 1000) for each in tiles: if each in chars.keys(): thing = chars[each] -- cgit v1.2.3 From 975eb021754c3a1ad6a5aa447358212564382351 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:31:44 -0500 Subject: pass vba and registers into call() --- pokemontools/vba/vba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index b403236..f6a0f7b 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -53,7 +53,7 @@ def translate_chars(charz): result += chars[each] return result -def call(bank, address): +def call(bank, address, vba=vba, registers=registers): """ Jumps into a function at a certain address. @@ -491,7 +491,7 @@ class crystal: class TestEmulator(unittest.TestCase): def test_PlaceString(self): - call(0, 0x1078) + call(vba, 0, 0x1078) # where to draw the text registers["hl"] = 0xc4a0 -- cgit v1.2.3 From fc3b2ded897e9bb01180a2f5ea91b6f3ca037c09 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:32:15 -0500 Subject: pass vba and registers into get_stack --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index f6a0f7b..eafbaa1 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -85,7 +85,7 @@ def call(bank, address, vba=vba, registers=registers): else: registers["pc"] = address -def get_stack(): +def get_stack(vba=vba, registers=registers): """ Return a list of functions on the stack. """ -- cgit v1.2.3 From 2f9df7133f39b286afe222f635be32c0699c87bf Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:34:46 -0500 Subject: move get_stack into vba.crystal --- pokemontools/vba/vba.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index eafbaa1..a4c7e27 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -85,26 +85,27 @@ def call(bank, address, vba=vba, registers=registers): else: registers["pc"] = address -def get_stack(vba=vba, registers=registers): +class crystal: """ - Return a list of functions on the stack. + Just a simple namespace to store a bunch of functions for Pokémon Crystal. """ - addresses = [] - sp = registers.sp - for x in range(0, 11): - sp = sp - (2 * x) - hi = vba.read_memory_at(sp + 1) - lo = vba.read_memory_at(sp) - address = ((hi << 8) | lo) - addresses.append(address) + @staticmethod + def get_stack(vba=vba, registers=registers): + """ + Return a list of functions on the stack. + """ + addresses = [] + sp = registers.sp - return addresses + for x in range(0, 11): + sp = sp - (2 * x) + hi = vba.read_memory_at(sp + 1) + lo = vba.read_memory_at(sp) + address = ((hi << 8) | lo) + addresses.append(address) -class crystal: - """ - Just a simple namespace to store a bunch of functions for Pokémon Crystal. - """ + return addresses @staticmethod def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): @@ -150,7 +151,7 @@ class crystal: break else: - stack = get_stack() + stack = crystal.get_stack() # yes/no box or the name selection box if address in range(0xa46, 0xaaf): -- cgit v1.2.3 From b1c34e88d51a0d1f45659c75920a58e997346e54 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:41:41 -0500 Subject: move call into vba.crystal --- pokemontools/vba/vba.py | 67 +++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index a4c7e27..e206d8a 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -53,43 +53,44 @@ def translate_chars(charz): result += chars[each] return result -def call(bank, address, vba=vba, registers=registers): - """ - Jumps into a function at a certain address. - - Go into the start menu, pause the game and try call(1, 0x1078) to see a - string printed to the screen. - """ - push = [ - registers.pc, - registers.hl, - registers.de, - registers.bc, - registers.af, - 0x3bb7, - ] - - for value in push: - registers.sp -= 2 - vba.write_memory_at(registers.sp + 1, value >> 8) - vba.write_memory_at(registers.sp, value & 0xFF) - if get_memory_range(vba, registers.sp, 2) != [value & 0xFF, value >> 8]: - print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(get_memory_range(vba, registers.sp, 2)) - print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) - - if bank != 0: - registers["af"] = (bank << 8) | (registers["af"] & 0xFF) - registers["hl"] = address - registers["pc"] = 0x2d63 # FarJump - else: - registers["pc"] = address - class crystal: """ Just a simple namespace to store a bunch of functions for Pokémon Crystal. """ + @staticmethod + def call(bank, address, vba=vba, registers=registers): + """ + Jumps into a function at a certain address. + + Go into the start menu, pause the game and try call(1, 0x1078) to see a + string printed to the screen. + """ + push = [ + registers.pc, + registers.hl, + registers.de, + registers.bc, + registers.af, + 0x3bb7, + ] + + for value in push: + registers.sp -= 2 + vba.write_memory_at(registers.sp + 1, value >> 8) + vba.write_memory_at(registers.sp, value & 0xFF) + if get_memory_range(vba, registers.sp, 2) != [value & 0xFF, value >> 8]: + print "desired memory values: " + str([value & 0xFF, value >> 8] ) + print "actual memory values: " + str(get_memory_range(vba, registers.sp, 2)) + print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) + + if bank != 0: + registers["af"] = (bank << 8) | (registers["af"] & 0xFF) + registers["hl"] = address + registers["pc"] = 0x2d63 # FarJump + else: + registers["pc"] = address + @staticmethod def get_stack(vba=vba, registers=registers): """ @@ -492,7 +493,7 @@ class crystal: class TestEmulator(unittest.TestCase): def test_PlaceString(self): - call(vba, 0, 0x1078) + crystal.call(0, 0x1078) # where to draw the text registers["hl"] = 0xc4a0 -- cgit v1.2.3 From 8ca4bcf37e5183455c9cc23e385597b92b4e6d29 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 19:45:35 -0500 Subject: remove get_memory_range --- pokemontools/vba/vba.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index e206d8a..7d6a588 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -40,13 +40,6 @@ registers = vba_wrapper.core.registers.Registers(vba) button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine -def get_memory_range(vba, address, length): - """ - This is just a lame way to avoid converting some of the old - get_memory_range calls to use the vba.memory property. - """ - return list(vba.memory[address:address+length]) - def translate_chars(charz): result = "" for each in charz: @@ -79,9 +72,9 @@ class crystal: registers.sp -= 2 vba.write_memory_at(registers.sp + 1, value >> 8) vba.write_memory_at(registers.sp, value & 0xFF) - if get_memory_range(vba, registers.sp, 2) != [value & 0xFF, value >> 8]: + if list(vba.memory[registers.sp : registers.sp + 2]) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(get_memory_range(vba, registers.sp, 2)) + print "actual memory values: " + str(list(vba.memory[registers.sp : registers.sp + 2])) print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) if bank != 0: @@ -332,7 +325,7 @@ class crystal: """ Returns the 7 characters making up the player's name. """ - bytez = get_memory_range(vba, 0xD47D, 7) + bytez = vba.memory[0xD47D:0xD47D + 7] name = translate_chars(bytez) return name @@ -374,7 +367,7 @@ class crystal: Other characters will not be shown. """ output = "" - tiles = get_memory_range(vba, 0xc4a0, 1000) + tiles = vba.memory[0xc4a0:0xc4a0 + 1000] for each in tiles: if each in chars.keys(): thing = chars[each] -- cgit v1.2.3 From 7fdf53f162dc8027619803b36d4447cdc2f294bd Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 21:24:38 -0500 Subject: switch vba.crystal to have instance methods --- pokemontools/vba/vba.py | 279 ++++++++++++++++++++++-------------------------- 1 file changed, 128 insertions(+), 151 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 7d6a588..7e3f0a7 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -23,20 +23,10 @@ from pokemontools.map_names import ( import keyboard # just use a default config for now until the globals are removed completely -import pokemontools.config as conf -config = conf.Config() -project_path = config.path -save_state_path = config.save_state_path -rom_path = config.rom_path - -if not os.path.exists(rom_path): - raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) +import pokemontools.configuration as configuration import vba_wrapper -vba = vba_wrapper.VBA(rom_path) -registers = vba_wrapper.core.registers.Registers(vba) - button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine @@ -46,13 +36,26 @@ def translate_chars(charz): result += chars[each] return result -class crystal: +class crystal(object): """ Just a simple namespace to store a bunch of functions for Pokémon Crystal. + There can only be one running instance of the emulator per process because + it's a poorly written shared library. """ - @staticmethod - def call(bank, address, vba=vba, registers=registers): + def __init__(self): + """ + Launch the VBA controller. + """ + self.config = configuration.Config() + + self.vba = vba_wrapper.VBA(self.config.rom_path) + self.registers = vba_wrapper.core.registers.Registers(self.vba) + + if not os.path.exists(rom_path): + raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + + def call(self, bank, address): """ Jumps into a function at a certain address. @@ -60,49 +63,47 @@ class crystal: string printed to the screen. """ push = [ - registers.pc, - registers.hl, - registers.de, - registers.bc, - registers.af, + self.registers.pc, + self.registers.hl, + self.registers.de, + self.registers.bc, + self.registers.af, 0x3bb7, ] for value in push: - registers.sp -= 2 - vba.write_memory_at(registers.sp + 1, value >> 8) - vba.write_memory_at(registers.sp, value & 0xFF) - if list(vba.memory[registers.sp : registers.sp + 2]) != [value & 0xFF, value >> 8]: + self.registers.sp -= 2 + self.vba.write_memory_at(registers.sp + 1, value >> 8) + self.vba.write_memory_at(registers.sp, value & 0xFF) + if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(list(vba.memory[registers.sp : registers.sp + 2])) - print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(vba.read_memory_at(registers.sp)) + print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) + print "wrong value at " + hex(self.registers.sp) + " expected " + hex(value) + " but got " + hex(self.vba.read_memory_at(self.registers.sp)) if bank != 0: - registers["af"] = (bank << 8) | (registers["af"] & 0xFF) - registers["hl"] = address - registers["pc"] = 0x2d63 # FarJump + self.registers["af"] = (bank << 8) | (self.registers["af"] & 0xFF) + self.registers["hl"] = address + self.registers["pc"] = 0x2d63 # FarJump else: - registers["pc"] = address + self.registers["pc"] = address - @staticmethod - def get_stack(vba=vba, registers=registers): + def get_stack(self): """ Return a list of functions on the stack. """ addresses = [] - sp = registers.sp + sp = self.registers.sp for x in range(0, 11): sp = sp - (2 * x) - hi = vba.read_memory_at(sp + 1) - lo = vba.read_memory_at(sp) + hi = self.vba.read_memory_at(sp + 1) + lo = self.vba.read_memory_at(sp) address = ((hi << 8) | lo) addresses.append(address) return addresses - @staticmethod - def text_wait(step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): + def text_wait(self, step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): """ Presses the "A" button when text is done being drawn to screen. @@ -119,22 +120,22 @@ class crystal: :param max_wait: number of wait loops to perform """ while max_wait > 0: - hi = vba.read_memory_at(registers.sp + 1) - lo = vba.read_memory_at(registers.sp) + hi = self.vba.read_memory_at(registers.sp + 1) + lo = self.vba.read_memory_at(registers.sp) address = ((hi << 8) | lo) if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): # 0xaef: print "pressing, then breaking.. address is: " + str(hex(address)) # set CurSFX - vba.write_memory_at(0xc2bf, 0) + self.vba.write_memory_at(0xc2bf, 0) - vba.press("a", hold=10, after=1) + self.vba.press("a", hold=10, after=1) # check if CurSFX is SFX_READ_TEXT_2 - if vba.read_memory_at(0xc2bf) == 0x8: + if self.vba.read_memory_at(0xc2bf) == 0x8: print "cursfx is set to SFX_READ_TEXT_2, looping.." - return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) + return self.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) else: if sfx_limit > 0: sfx_limit = sfx_limit - 1 @@ -145,7 +146,7 @@ class crystal: break else: - stack = crystal.get_stack() + stack = self.get_stack() # yes/no box or the name selection box if address in range(0xa46, 0xaaf): @@ -164,7 +165,7 @@ class crystal: break else: - vba.step(count=step_size) + self.vba.step(count=step_size) # if there is a callback, then call the callback and exit when the # callback returns True. This is especially useful during the @@ -187,17 +188,15 @@ class crystal: if max_wait == 0: print "max_wait was hit" - @staticmethod - def walk_through_walls_slow(): - memory = vba.memory + def walk_through_walls_slow(self): + memory = self.vba.memory memory[0xC2FA] = 0 memory[0xC2FB] = 0 memory[0xC2FC] = 0 memory[0xC2FD] = 0 - vba.memory = memory + self.vba.memory = memory - @staticmethod - def walk_through_walls(): + def walk_through_walls(self): """ Lets the player walk all over the map. @@ -206,73 +205,65 @@ class crystal: to be executed each step/tick if continuous walk-through-walls is desired. """ - vba.write_memory_at(0xC2FA, 0) - vba.write_memory_at(0xC2FB, 0) - vba.write_memory_at(0xC2FC, 0) - vba.write_memory_at(0xC2FD, 0) + self.vba.write_memory_at(0xC2FA, 0) + self.vba.write_memory_at(0xC2FB, 0) + self.vba.write_memory_at(0xC2FC, 0) + self.vba.write_memory_at(0xC2FD, 0) #@staticmethod #def set_enemy_level(level): # vba.write_memory_at(0xd213, level) - @staticmethod - def nstep(steplimit=500): + def nstep(self, steplimit=500): """ Steps the CPU forward and calls some functions in between each step. (For example, to manipulate memory.) This is pretty slow. """ for step_counter in range(0, steplimit): - crystal.walk_through_walls() + self.walk_through_walls() #call(0x1, 0x1078) - vba.step() + self.vba.step() - @staticmethod - def disable_triggers(): - vba.write_memory_at(0x23c4, 0xAF) - vba.write_memory_at(0x23d0, 0xAF); + def disable_triggers(self): + self.vba.write_memory_at(0x23c4, 0xAF) + self.vba.write_memory_at(0x23d0, 0xAF); - @staticmethod - def disable_callbacks(): - vba.write_memory_at(0x23f2, 0xAF) - vba.write_memory_at(0x23fe, 0xAF) + def disable_callbacks(self): + self.vba.write_memory_at(0x23f2, 0xAF) + self.vba.write_memory_at(0x23fe, 0xAF) - @staticmethod - def get_map_group_id(): + def get_map_group_id(self): """ Returns the current map group. """ - return vba.read_memory_at(0xdcb5) + return self.vba.read_memory_at(0xdcb5) - @staticmethod - def get_map_id(): + def get_map_id(self): """ Returns the map number of the current map. """ - return vba.read_memory_at(0xdcb6) + return self.vba.read_memory_at(0xdcb6) - @staticmethod - def get_map_name(): + def get_map_name(self, map_names=map_names): """ Figures out the current map name. """ - map_group_id = crystal.get_map_group_id() - map_id = crystal.get_map_id() + map_group_id = self.get_map_group_id() + map_id = self.get_map_id() return map_names[map_group_id][map_id]["name"] - @staticmethod - def get_xy(): + def get_xy(self): """ (x, y) coordinates of player on map. Relative to top-left corner of map. """ - x = vba.read_memory_at(0xdcb8) - y = vba.read_memory_at(0xdcb7) + x = self.vba.read_memory_at(0xdcb8) + y = self.vba.read_memory_at(0xdcb7) return (x, y) - @staticmethod - def menu_select(id=1): + def menu_select(self, id=1): """ Sets the cursor to the given pokemon in the player's party. @@ -281,38 +272,34 @@ class crystal: This probably works on other menus. """ - vba.write_memory_at(0xcfa9, id) + self.vba.write_memory_at(0xcfa9, id) - @staticmethod - def is_in_battle(): + def is_in_battle(self): """ Checks whether or not we're in a battle. """ - return (vba.read_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() + return (self.vba.read_memory_at(0xd22d) != 0) or self.is_in_link_battle() - @staticmethod - def is_in_link_battle(): - return vba.read_memory_at(0xc2dc) != 0 + def is_in_link_battle(self): + return self.vba.read_memory_at(0xc2dc) != 0 - @staticmethod - def unlock_flypoints(): + def unlock_flypoints(self): """ Unlocks different destinations for flying. Note: this might start at 0xDCA4 (minus one on all addresses), but not sure. """ - vba.write_memory_at(0xDCA5, 0xFF) - vba.write_memory_at(0xDCA6, 0xFF) - vba.write_memory_at(0xDCA7, 0xFF) - vba.write_memory_at(0xDCA8, 0xFF) + self.vba.write_memory_at(0xDCA5, 0xFF) + self.vba.write_memory_at(0xDCA6, 0xFF) + self.vba.write_memory_at(0xDCA7, 0xFF) + self.vba.write_memory_at(0xDCA8, 0xFF) - @staticmethod - def get_gender(): + def get_gender(self): """ Returns 'male' or 'female'. """ - gender = vba.read_memory_at(0xD472) + gender = self.vba.read_memory_at(0xD472) if gender == 0: return "male" elif gender == 1: @@ -320,54 +307,49 @@ class crystal: else: return gender - @staticmethod - def get_player_name(): + def get_player_name(self): """ Returns the 7 characters making up the player's name. """ - bytez = vba.memory[0xD47D:0xD47D + 7] + bytez = self.vba.memory[0xD47D:0xD47D + 7] name = translate_chars(bytez) return name - @staticmethod - def warp(map_group_id, map_id, x, y): - vba.write_memory_at(0xdcb5, map_group_id) - vba.write_memory_at(0xdcb6, map_id) - vba.write_memory_at(0xdcb7, y) - vba.write_memory_at(0xdcb8, x) - vba.write_memory_at(0xd001, 0xFF) - vba.write_memory_at(0xff9f, 0xF1) - vba.write_memory_at(0xd432, 1) - vba.write_memory_at(0xd434, 0 & 251) - - @staticmethod - def warp_pokecenter(): - crystal.warp(1, 1, 3, 3) - crystal.nstep(200) - - @staticmethod - def masterballs(): + def warp(self, map_group_id, map_id, x, y): + self.vba.write_memory_at(0xdcb5, map_group_id) + self.vba.write_memory_at(0xdcb6, map_id) + self.vba.write_memory_at(0xdcb7, y) + self.vba.write_memory_at(0xdcb8, x) + self.vba.write_memory_at(0xd001, 0xFF) + self.vba.write_memory_at(0xff9f, 0xF1) + self.vba.write_memory_at(0xd432, 1) + self.vba.write_memory_at(0xd434, 0 & 251) + + def warp_pokecenter(self): + self.warp(1, 1, 3, 3) + self.nstep(200) + + def masterballs(self): # masterball - vba.write_memory_at(0xd8d8, 1) - vba.write_memory_at(0xd8d9, 99) + self.vba.write_memory_at(0xd8d8, 1) + self.vba.write_memory_at(0xd8d9, 99) # ultraball - vba.write_memory_at(0xd8da, 2) - vba.write_memory_at(0xd8db, 99) + self.vba.write_memory_at(0xd8da, 2) + self.vba.write_memory_at(0xd8db, 99) # pokeballs - vba.write_memory_at(0xd8dc, 5) - vba.write_memory_at(0xd8dd, 99) + self.vba.write_memory_at(0xd8dc, 5) + self.vba.write_memory_at(0xd8dd, 99) - @staticmethod - def get_text(): + def get_text(self, chars=chars): """ Returns alphanumeric text on the screen. Other characters will not be shown. """ output = "" - tiles = vba.memory[0xc4a0:0xc4a0 + 1000] + tiles = self.vba.memory[0xc4a0:0xc4a0 + 1000] for each in tiles: if each in chars.keys(): thing = chars[each] @@ -390,32 +372,29 @@ class crystal: return output - @staticmethod - def keyboard_apply(button_sequence): + def keyboard_apply(self, button_sequence): """ Applies a sequence of buttons to the on-screen keyboard. """ for buttons in button_sequence: - vba.press(buttons) - vba.step(count=2) - vba.press([]) + self.vba.press(buttons) + self.vba.step(count=2) + self.vba.press([]) - @staticmethod - def write(something="TrAiNeR"): + def write(self, something="TrAiNeR"): """ Types out a word. Uses a planning algorithm to do this in the most efficient way possible. """ button_sequence = keyboard.plan_typing(something) - crystal.keyboard_apply([[x] for x in button_sequence]) + self.keyboard_apply([[x] for x in button_sequence]) - @staticmethod - def set_partymon2(): + def set_partymon2(self): """ This causes corruption, so it's not working yet. """ - memory = vba.memory + memory = self.vba.memory memory[0xdcd7] = 2 memory[0xdcd9] = 0x7 @@ -449,19 +428,18 @@ class crystal: memory[0xdd33] = 0x10 memory[0xdd34] = 0x40 - vba.memory = memory + self.vba.memory = memory - @staticmethod - def wait_for_script_running(debug=False, limit=1000): + def wait_for_script_running(self, debug=False, limit=1000): """ Wait until ScriptRunning isn't -1. """ while limit > 0: - if vba.read_memory_at(0xd438) != 255: + if self.vba.read_memory_at(0xd438) != 255: print "script is done executing" return else: - vba.step() + self.vba.step() if debug: limit = limit - 1 @@ -469,20 +447,19 @@ class crystal: if limit == 0: print "limit ran out" - @staticmethod - def move(cmd): + def move(self, cmd): """ Attempt to move the player. """ - vba.press(cmd, hold=10, after=0) - vba.press([]) + self.vba.press(cmd, hold=10, after=0) + self.vba.press([]) - memory = vba.memory + memory = self.vba.memory #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: while memory[0xd043] in [0, 1, 2, 3]: #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: - vba.step(count=10) - memory = vba.memory + self.vba.step(count=10) + memory = self.vba.memory class TestEmulator(unittest.TestCase): def test_PlaceString(self): -- cgit v1.2.3 From e3a596e7d8c9de64162da2e0a05d5b17942d4694 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 22:12:37 -0500 Subject: fix a vba.py test (test_PlaceString) --- pokemontools/vba/vba.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 7e3f0a7..b775970 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -462,18 +462,21 @@ class crystal(object): memory = self.vba.memory class TestEmulator(unittest.TestCase): + def setUp(self): + self.cry = crystal() + def test_PlaceString(self): - crystal.call(0, 0x1078) + self.cry.call(0, 0x1078) # where to draw the text - registers["hl"] = 0xc4a0 + self.cry.registers["hl"] = 0xc4a0 # what text to read from - registers["de"] = 0x1276 + self.cry.registers["de"] = 0x1276 - vba.step(count=10) + self.cry.vba.step(count=10) - text = crystal.get_text() + text = self.cry.get_text() self.assertTrue("TRAINER" in text) -- cgit v1.2.3 From 2a439694d6af7416732b76fe37e8ea9fe0e9faff Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 23:12:38 -0500 Subject: combine some vba tests --- pokemontools/vba/vba.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index b775970..a308ea8 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -52,7 +52,7 @@ class crystal(object): self.vba = vba_wrapper.VBA(self.config.rom_path) self.registers = vba_wrapper.core.registers.Registers(self.vba) - if not os.path.exists(rom_path): + if not os.path.exists(self.config.rom_path): raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) def call(self, bank, address): @@ -73,8 +73,8 @@ class crystal(object): for value in push: self.registers.sp -= 2 - self.vba.write_memory_at(registers.sp + 1, value >> 8) - self.vba.write_memory_at(registers.sp, value & 0xFF) + self.vba.write_memory_at(self.registers.sp + 1, value >> 8) + self.vba.write_memory_at(self.registers.sp, value & 0xFF) if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: print "desired memory values: " + str([value & 0xFF, value >> 8] ) print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) @@ -462,8 +462,12 @@ class crystal(object): memory = self.vba.memory class TestEmulator(unittest.TestCase): - def setUp(self): - self.cry = crystal() + @classmethod + def setUpClass(cls): + cls.cry = crystal() + + # advance it forward past the intro sequences + cls.cry.vba.step(count=3500) def test_PlaceString(self): self.cry.call(0, 0x1078) @@ -480,8 +484,7 @@ class TestEmulator(unittest.TestCase): self.assertTrue("TRAINER" in text) -class TestWriter(unittest.TestCase): - def test_very_basic(self): + def test_keyboard_planner(self): button_sequence = keyboard.plan_typing("an") expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] -- cgit v1.2.3 From f0e75972a119812ec37ec27fcdcd00afc45edf98 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 23:14:58 -0500 Subject: move tests into test_vba.py They didn't belong in pokemontools/vba/vba.py in the first place. --- pokemontools/vba/vba.py | 35 ----------------------------------- tests/test_vba.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index a308ea8..863e16c 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -9,8 +9,6 @@ import re import string from copy import copy -import unittest - # for converting bytes to readable text from pokemontools.chars import ( chars, @@ -460,36 +458,3 @@ class crystal(object): #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: self.vba.step(count=10) memory = self.vba.memory - -class TestEmulator(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.cry = crystal() - - # advance it forward past the intro sequences - cls.cry.vba.step(count=3500) - - 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_keyboard_planner(self): - button_sequence = keyboard.plan_typing("an") - expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] - - self.assertEqual(len(expected_result), len(button_sequence)) - self.assertEqual(expected_result, button_sequence) - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_vba.py b/tests/test_vba.py index 56a71e3..12dd51b 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -102,5 +102,35 @@ class VbaTests(unittest.TestCase): player_action = self.get_wram_value("PlayerAction") self.assertEqual(player_action, 1) # 1 = standing +class TestEmulator(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.cry = crystal() + + # advance it forward past the intro sequences + cls.cry.vba.step(count=3500) + + 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_keyboard_planner(self): + button_sequence = keyboard.plan_typing("an") + expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] + + self.assertEqual(len(expected_result), len(button_sequence)) + self.assertEqual(expected_result, button_sequence) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From d0c53213c416a1c77891fb10f11e6316817101e7 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 21 Sep 2013 23:54:55 -0500 Subject: make the vba autoplayer use the new methods --- pokemontools/vba/autoplayer.py | 742 +++++++++++++++++++++-------------------- 1 file changed, 378 insertions(+), 364 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 9aa8f4a..a63caa8 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -5,27 +5,7 @@ Programmatic speedrun of Pokémon Crystal import os # bring in the emulator and basic tools -import vba - -def main(): - """ - Start the game. - """ - vba.load_rom() - - # 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): """ @@ -51,445 +31,479 @@ def skippable(func): return_value = None if not skip: - vba.save_state(func.__name__ + "-start", override=True) + _vba.save_state(func.__name__ + "-start", override=True) return_value = func(*args, **kwargs) - vba.save_state(func.__name__ + "-end", override=True) + _vba.save_state(func.__name__ + "-end", override=True) elif skip: - vba.set_state(vba.load_state(func.__name__ + "-end")) + _vba.set_state(vba.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 setup(self): + self.cry = _vba.crystal() - # skip the ditto sequence - vba.press("a") - vba.nstep(100) + def main(self): + """ + Start the game. + """ + # get past the opening sequence + self.skip_intro() - # skip the start screen - vba.press("start") - vba.nstep(100) + # walk to mom and handle her text + self.handle_mom() - # click "new game" - vba.press("a", holdsteps=50, aftersteps=1) + # walk outside into new bark town + self.walk_into_new_bark_town() - # skip text up to "Are you a boy? Or are you a girl?" - vba.crystal.text_wait() + # walk to elm and do whatever he wants + self.handle_elm("totodile") - # select "Boy" - vba.press("a", holdsteps=50, aftersteps=1) + self.new_bark_level_grind(10, skip=False) - # text until "What time is it?" - vba.crystal.text_wait() + @skippable + def skip_intro(self): + """ + Skip the game boot intro sequence. + """ - # select 10 o'clock - vba.press("a", holdsteps=50, aftersteps=1) + # copyright sequence + self.cry.nstep(400) - # yes i mean it - vba.press("a", holdsteps=50, aftersteps=1) + # skip the ditto sequence + self.cry.press("a") + self.cry.nstep(100) - # "How many minutes?" 0 min. - vba.press("a", holdsteps=50, aftersteps=1) + # skip the start screen + self.cry.press("start") + self.cry.nstep(100) - # "Who! 0 min.?" yes/no select yes - vba.press("a", holdsteps=50, aftersteps=1) + # click "new game" + self.cry.press("a", holdsteps=50, aftersteps=1) - # read text until name selection - vba.crystal.text_wait() + # skip text up to "Are you a boy? Or are you a girl?" + self.cry.text_wait() - # select "Chris" - vba.press("d", holdsteps=10, aftersteps=1) - vba.press("a", holdsteps=50, aftersteps=1) + # select "Boy" + self.cry.press("a", holdsteps=50, aftersteps=1) - def overworldcheck(): - """ - A basic check for when the game starts. - """ - return vba.get_memory_at(0xcfb1) != 0 + # text until "What time is it?" + self.cry.text_wait() - # go until the introduction is done - vba.crystal.text_wait(callback=overworldcheck) + # select 10 o'clock + self.cry.press("a", holdsteps=50, aftersteps=1) - return + # yes i mean it + self.cry.press("a", holdsteps=50, aftersteps=1) -@skippable -def handle_mom(): - """ - Walk to mom. Handle her speech and questions. - """ + # "How many minutes?" 0 min. + self.cry.press("a", holdsteps=50, aftersteps=1) - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") + # "Who! 0 min.?" yes/no select yes + self.cry.press("a", holdsteps=50, aftersteps=1) - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") + # read text until name selection + self.cry.text_wait() - vba.crystal.move("d") - vba.crystal.move("d") + # select "Chris" + self.cry.press("d", holdsteps=10, aftersteps=1) + self.cry.press("a", holdsteps=50, aftersteps=1) - # move into mom's line of sight - vba.crystal.move("d") + def overworldcheck(): + """ + A basic check for when the game starts. + """ + return self.cry.vba.memory[0xcfb1] != 0 - # let mom talk until "What day is it?" - vba.crystal.text_wait() + # go until the introduction is done + self.cry.text_wait(callback=overworldcheck) - # "What day is it?" Sunday - vba.press("a", holdsteps=10) # Sunday + return - vba.crystal.text_wait() + @skippable + def handle_mom(self): + """ + Walk to mom. Handle her speech and questions. + """ - # "SUNDAY, is it?" yes/no - vba.press("a", holdsteps=10) # yes + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") + self.cry.move("r") - vba.crystal.text_wait() + self.cry.move("u") + self.cry.move("u") + self.cry.move("u") - # "Is it Daylight Saving Time now?" yes/no - vba.press("a", holdsteps=10) # yes + self.cry.move("d") + self.cry.move("d") - vba.crystal.text_wait() + # move into mom's line of sight + self.cry.move("d") - # "AM DST, is that OK?" yes/no - vba.press("a", holdsteps=10) # yes + # let mom talk until "What day is it?" + self.cry.text_wait() - # text until "know how to use the PHONE?" yes/no - vba.crystal.text_wait() + # "What day is it?" Sunday + self.cry.vba.press("a", holdsteps=10) # Sunday - # press yes - vba.press("a", holdsteps=10) + self.cry.text_wait() - # wait until mom is done talking - vba.crystal.text_wait() + # "SUNDAY, is it?" yes/no + self.cry.vba.press("a", holdsteps=10) # yes - # wait until the script is done running - vba.crystal.wait_for_script_running() + self.cry.text_wait() - return + # "Is it Daylight Saving Time now?" yes/no + self.cry.vba.press("a", holdsteps=10) # yes -@skippable -def walk_into_new_bark_town(): - """ - Walk outside after talking with mom. - """ + self.cry.text_wait() - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("l") - vba.crystal.move("l") + # "AM DST, is that OK?" yes/no + self.cry.vba.press("a", holdsteps=10) # yes - # walk outside - vba.crystal.move("d") + # text until "know how to use the PHONE?" yes/no + self.cry.text_wait() -@skippable -def handle_elm(starter_choice): - """ - Walk to Elm's Lab and get a starter. - """ + # press yes + self.cry.vba.press("a", holdsteps=10) - # 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") + # wait until mom is done talking + self.cry.text_wait() - # walk into the lab - vba.crystal.move("u") + # wait until the script is done running + self.cry.wait_for_script_running() - # talk to elm - vba.crystal.text_wait() + return - # "that I recently caught." yes/no - vba.press("a", holdsteps=10) # yes + @skippable + def walk_into_new_bark_town(self): + """ + Walk outside after talking with mom. + """ - # talk to elm some more - vba.crystal.text_wait() + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("l") + self.cry.move("l") - # talking isn't done yet.. - vba.crystal.text_wait() - vba.crystal.text_wait() - vba.crystal.text_wait() + # walk outside + self.cry.move("d") - # wait until the script is done running - vba.crystal.wait_for_script_running() + @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", holdsteps=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() - # move toward the pokeballs - vba.crystal.move("r") + # wait until the script is done running + self.cry.wait_for_script_running() - # move to cyndaquil - vba.crystal.move("r") + # move toward the pokeballs + self.cry.move("r") - moves = 0 + # move to cyndaquil + self.cry.move("r") - if starter_choice.lower() == "cyndaquil": moves = 0 - if starter_choice.lower() == "totodile": - moves = 1 - else: - moves = 2 - for each in range(0, moves): - vba.crystal.move("r") + if starter_choice.lower() == "cyndaquil": + moves = 0 + if starter_choice.lower() == "totodile": + moves = 1 + else: + moves = 2 - # face the pokeball - vba.crystal.move("u") + for each in range(0, moves): + self.cry.move("r") - # select it - vba.press("a", holdsteps=10, aftersteps=0) + # face the pokeball + self.cry.move("u") - # wait for the image to pop up - vba.crystal.text_wait() + # select it + self.cry.vba.press("a", holdsteps=10, aftersteps=0) - # wait for the image to close - vba.crystal.text_wait() + # wait for the image to pop up + self.cry.text_wait() - # wait for the yes/no box - vba.crystal.text_wait() + # wait for the image to close + self.cry.text_wait() - # press yes - vba.press("a", holdsteps=10, aftersteps=0) + # wait for the yes/no box + self.cry.text_wait() - # wait for elm to talk a bit - vba.crystal.text_wait() + # press yes + self.cry.vba.press("a", holdsteps=10, aftersteps=0) - # TODO: why didn't that finish his talking? - vba.crystal.text_wait() + # wait for elm to talk a bit + self.cry.text_wait() - # give a nickname? yes/no - vba.press("d", holdsteps=10, aftersteps=0) # move to "no" - vba.press("a", holdsteps=10, aftersteps=0) # no + # TODO: why didn't that finish his talking? + self.cry.text_wait() - # TODO: why didn't this wait until he was completely done? - vba.crystal.text_wait() - vba.crystal.text_wait() + # give a nickname? yes/no + self.cry.vba.press("d", holdsteps=10, aftersteps=0) # move to "no" + self.cry.vba.press("a", holdsteps=10, aftersteps=0) # no - # get the phone number - vba.crystal.text_wait() + # TODO: why didn't this wait until he was completely done? + self.cry.text_wait() + self.cry.text_wait() - # talk with elm a bit more - vba.crystal.text_wait() + # get the phone number + self.cry.text_wait() - # TODO: and again.. wtf? - vba.crystal.text_wait() + # talk with elm a bit more + self.cry.text_wait() - # wait until the script is done running - vba.crystal.wait_for_script_running() + # TODO: and again.. wtf? + self.cry.text_wait() - # move down - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + # wait until the script is done running + self.cry.wait_for_script_running() - # move into the researcher's line of sight - vba.crystal.move("d") + # move down + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") - # get the potion from the person - vba.crystal.text_wait() - vba.crystal.text_wait() + # move into the researcher's line of sight + self.cry.move("d") - # wait for the script to end - vba.crystal.wait_for_script_running() + # get the potion from the person + self.cry.text_wait() + self.cry.text_wait() - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + # wait for the script to end + self.cry.wait_for_script_running() - # go outside - vba.crystal.move("d") + self.cry.move("d") + self.cry.move("d") + self.cry.move("d") - return + # go outside + self.cry.move("d") -@skippable -def new_bark_level_grind(level): - """ - Do level grinding in New Bark. + return - Starting just outside of Elm's Lab, do some level grinding until the first - partymon level is equal to the given value.. - """ + @skippable + def new_bark_level_grind(self, level): + """ + Do level grinding in New Bark. - # walk to the grass area - new_bark_level_grind_walk_to_grass(skip=False) + Starting just outside of Elm's Lab, do some level grinding until the + first partymon level is equal to the given value.. + """ - # TODO: walk around in grass, handle battles - walk = ["d", "d", "u", "d", "u", "d"] - for direction in walk: - vba.crystal.move(direction) + # walk to the grass area + self.new_bark_level_grind_walk_to_grass(skip=False) - # wait for wild battle to completely start - vba.crystal.text_wait() + # TODO: walk around in grass, handle battles + walk = ["d", "d", "u", "d", "u", "d"] + for direction in walk: + self.cry.move(direction) - attacks = 5 + # wait for wild battle to completely start + self.cry.text_wait() - while attacks > 0: - # FIGHT - vba.press("a", holdsteps=10, aftersteps=1) + attacks = 5 - # wait to select a move - vba.crystal.text_wait() + while attacks > 0: + # FIGHT + self.cry.vba.press("a", holdsteps=10, aftersteps=1) - # SCRATCH - vba.press("a", holdsteps=10, aftersteps=1) + # wait to select a move + self.cry.text_wait() - # wait for the move to be over - vba.crystal.text_wait() + # SCRATCH + self.cry.vba.press("a", holdsteps=10, aftersteps=1) - hp = ((vba.get_memory_at(0xd218) << 8) | vba.get_memory_at(0xd217)) - print "enemy hp is: " + str(hp) + # wait for the move to be over + self.cry.text_wait() - if hp == 0: - print "enemy hp is zero, exiting" - break - else: + hp = ((self.cry.vba.get_memory_at(0xd218) << 8) | self.cry.vba.get_memory_at(0xd217)) print "enemy hp is: " + str(hp) - attacks = attacks - 1 - - while vba.get_memory_at(0xd22d) != 0: - vba.press("a", holdsteps=10, aftersteps=1) - - # wait for the map to finish loading - vba.nstep(50) - - print "okay, back in the overworld" - - # move up - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - - # move into new bark town - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - - # move up - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - - # move to the door - vba.crystal.move("r") - vba.crystal.move("r") - vba.crystal.move("r") - - # walk in - vba.crystal.move("u") - - # move up to the healing thing - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("u") - vba.crystal.move("l") - vba.crystal.move("l") - - # face it - vba.crystal.move("u") - - # interact - vba.press("a", holdsteps=10, aftersteps=1) - - # wait for yes/no box - vba.crystal.text_wait() - - # press yes - vba.press("a", holdsteps=10, aftersteps=1) - - # TODO: when is healing done? - - # wait until the script is done running - vba.crystal.wait_for_script_running() - - # wait for it to be really really done - vba.nstep(50) - - vba.crystal.move("r") - vba.crystal.move("r") - - # move to the door - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") - - # walk out - vba.crystal.move("d") - - # check partymon1 level - if vba.get_memory_at(0xdcfe) < level: - new_bark_level_grind(level, skip=False) - else: - return + if hp == 0: + print "enemy hp is zero, exiting" + break + else: + print "enemy hp is: " + str(hp) + + attacks = attacks - 1 + + while self.cry.vba.get_memory_at(0xd22d) != 0: + self.cry.vba.press("a", holdsteps=10, aftersteps=1) + + # wait for the map to finish loading + self.cry.vba.nstep(50) + + print "okay, back in the overworld" + + # 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", holdsteps=10, aftersteps=1) + + # wait for yes/no box + self.cry.text_wait() + + # press yes + self.cry.vba.press("a", holdsteps=10, aftersteps=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.nstep(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.get_memory_at(0xdcfe) < level: + self.new_bark_level_grind(level, skip=False) + else: + return -@skippable -def new_bark_level_grind_walk_to_grass(): - """ - Move to just above the grass from outside Elm's lab. - """ + @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") - vba.crystal.move("d") - vba.crystal.move("d") - - vba.crystal.move("l") - vba.crystal.move("l") - - vba.crystal.move("d") - vba.crystal.move("d") - - vba.crystal.move("l") - vba.crystal.move("l") - - # move to route 29 past the trees - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - vba.crystal.move("l") - - # move to just above the grass - vba.crystal.move("d") - vba.crystal.move("d") - vba.crystal.move("d") + 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 main(): + runner = SpeedRunner() + runner.setup() + return runner.main() if __name__ == "__main__": main() -- cgit v1.2.3 From 713c4ceb7736c620cb1626745d0c5bbfec3e22b1 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 00:10:07 -0500 Subject: fix the autoplayer bootstrapper for test_vba.py --- tests/test_vba.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 12dd51b..d11108d 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -7,11 +7,11 @@ import unittest import pokemontools.vba.vba as vba try: - import pokemontools.vba.vba_autoplayer + import pokemontools.vba.vba_autoplayer as autoplayer except ImportError: - import pokemontools.vba.autoplayer as vba_autoplayer + import pokemontools.vba.autoplayer as autoplayer -vba_autoplayer.vba = vba +autoplayer.vba = vba def setup_wram(): """ @@ -31,18 +31,17 @@ def bootstrap(): is constructed by this function. """ - # reset the rom - vba.shutdown() - vba.load_rom() + cry = vba.crystal() + runner = autoplayer.SpeedRunner(cry=cry) # skip=False means run the skip_intro function instead of just skipping to # a saved state. - vba_autoplayer.skip_intro() + runner.skip_intro() - state = vba.get_state() + state = cry.get_state() # clean everything up again - vba.shutdown() + cry.vba.shutdown() return state -- cgit v1.2.3 From c44ab9b5565a46004b17d87fdfefcd515b0adbf9 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 00:28:43 -0500 Subject: make the skippable decorator use config Use the pokemontools configuration to determine where to save the save states. --- pokemontools/vba/autoplayer.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index a63caa8..ee1a0c7 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -4,6 +4,8 @@ Programmatic speedrun of Pokémon Crystal """ import os +import pokemontools.configuration as configuration + # bring in the emulator and basic tools import vba as _vba @@ -11,11 +13,11 @@ 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 if "skip" in kwargs.keys(): @@ -25,17 +27,17 @@ def skippable(func): # 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) + self.cry.save_state(func.__name__ + "-start", override=True) return_value = func(*args, **kwargs) - _vba.save_state(func.__name__ + "-end", override=True) + self.cry.save_state(func.__name__ + "-end", override=True) elif skip: - _vba.set_state(vba.load_state(func.__name__ + "-end")) + self.cry.vba.set_state(self.cry.vba.load_state(func.__name__ + "-end")) return return_value return wrapped_function @@ -48,8 +50,17 @@ class Runner(object): pass class SpeedRunner(Runner): + def __init__(self, cry=None, config=None): + self.cry = cry + + if not config: + config = configuration.Config() + + self.config = config + def setup(self): - self.cry = _vba.crystal() + if not self.cry: + self.cry = _vba.crystal(config=config) def main(self): """ -- cgit v1.2.3 From 6b56969ccf00262187071e95568cee89f637692b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 00:44:35 -0500 Subject: make vba.crystal accept config --- pokemontools/vba/vba.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 863e16c..d821999 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -41,11 +41,14 @@ class crystal(object): it's a poorly written shared library. """ - def __init__(self): + def __init__(self, config=None): """ Launch the VBA controller. """ - self.config = configuration.Config() + if not config: + config = configuration.Config() + + self.config = config self.vba = vba_wrapper.VBA(self.config.rom_path) self.registers = vba_wrapper.core.registers.Registers(self.vba) -- cgit v1.2.3 From dc63d8d51dd451b13fb5e1386edfc7bac0874ee6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 00:49:44 -0500 Subject: re-implement save_state This can be used to dump state to a file based on the current configuration of the running instance. --- pokemontools/vba/vba.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d821999..d8ceebb 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -56,6 +56,27 @@ class crystal(object): if not os.path.exists(self.config.rom_path): raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + def save_state(self, name, state=None, override=False): + """ + Saves the given state to save_state_path. + + The file format must be ".sav", and this will be appended to your + string if necessary. + """ + if state == None: + state = self.vba.state + + if len(name) < 4 or name[-4:] != ".sav": + name += ".sav" + + save_path = os.path.join(self.config.save_state_path, name) + + if not override and os.path.exists(save_path): + raise Exception("oops, save state path already exists: {0}".format(save_path)) + + with open(save_path, "wb") as file_handler: + file_handler.write(state) + def call(self, bank, address): """ Jumps into a function at a certain address. -- cgit v1.2.3 From 6568c299fd59b72bf6bcb66df23b3631e2e0a88e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 00:59:19 -0500 Subject: fix how autoplayer calls hold/press on buttons --- pokemontools/vba/autoplayer.py | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index ee1a0c7..a143f1e 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -90,43 +90,43 @@ class SpeedRunner(Runner): self.cry.nstep(400) # skip the ditto sequence - self.cry.press("a") + self.cry.vba.press("a") self.cry.nstep(100) # skip the start screen - self.cry.press("start") + self.cry.vba.press("start") self.cry.nstep(100) # click "new game" - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # skip text up to "Are you a boy? Or are you a girl?" self.cry.text_wait() # select "Boy" - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # text until "What time is it?" self.cry.text_wait() # select 10 o'clock - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # yes i mean it - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # "How many minutes?" 0 min. - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # "Who! 0 min.?" yes/no select yes - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("a", hold=50, after=1) # read text until name selection self.cry.text_wait() # select "Chris" - self.cry.press("d", holdsteps=10, aftersteps=1) - self.cry.press("a", holdsteps=50, aftersteps=1) + self.cry.vba.press("d", hold=10, after=1) + self.cry.vba.press("a", hold=50, after=1) def overworldcheck(): """ @@ -164,28 +164,28 @@ class SpeedRunner(Runner): self.cry.text_wait() # "What day is it?" Sunday - self.cry.vba.press("a", holdsteps=10) # Sunday + self.cry.vba.press("a", hold=10) # Sunday self.cry.text_wait() # "SUNDAY, is it?" yes/no - self.cry.vba.press("a", holdsteps=10) # yes + 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", holdsteps=10) # yes + self.cry.vba.press("a", hold=10) # yes self.cry.text_wait() # "AM DST, is that OK?" yes/no - self.cry.vba.press("a", holdsteps=10) # yes + 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", holdsteps=10) + self.cry.vba.press("a", hold=10) # wait until mom is done talking self.cry.text_wait() @@ -234,7 +234,7 @@ class SpeedRunner(Runner): self.cry.text_wait() # "that I recently caught." yes/no - self.cry.vba.press("a", holdsteps=10) # yes + self.cry.vba.press("a", hold=10) # yes # talk to elm some more self.cry.text_wait() @@ -269,7 +269,7 @@ class SpeedRunner(Runner): self.cry.move("u") # select it - self.cry.vba.press("a", holdsteps=10, aftersteps=0) + self.cry.vba.press("a", hold=10, after=0) # wait for the image to pop up self.cry.text_wait() @@ -281,7 +281,7 @@ class SpeedRunner(Runner): self.cry.text_wait() # press yes - self.cry.vba.press("a", holdsteps=10, aftersteps=0) + self.cry.vba.press("a", hold=10, after=0) # wait for elm to talk a bit self.cry.text_wait() @@ -290,8 +290,8 @@ class SpeedRunner(Runner): self.cry.text_wait() # give a nickname? yes/no - self.cry.vba.press("d", holdsteps=10, aftersteps=0) # move to "no" - self.cry.vba.press("a", holdsteps=10, aftersteps=0) # 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() @@ -358,13 +358,13 @@ class SpeedRunner(Runner): while attacks > 0: # FIGHT - self.cry.vba.press("a", holdsteps=10, aftersteps=1) + self.cry.vba.press("a", hold=10, after=1) # wait to select a move self.cry.text_wait() # SCRATCH - self.cry.vba.press("a", holdsteps=10, aftersteps=1) + self.cry.vba.press("a", hold=10, after=1) # wait for the move to be over self.cry.text_wait() @@ -381,7 +381,7 @@ class SpeedRunner(Runner): attacks = attacks - 1 while self.cry.vba.get_memory_at(0xd22d) != 0: - self.cry.vba.press("a", holdsteps=10, aftersteps=1) + self.cry.vba.press("a", hold=10, after=1) # wait for the map to finish loading self.cry.vba.nstep(50) @@ -438,13 +438,13 @@ class SpeedRunner(Runner): self.cry.move("u") # interact - self.cry.vba.press("a", holdsteps=10, aftersteps=1) + 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", holdsteps=10, aftersteps=1) + self.cry.vba.press("a", hold=10, after=1) # TODO: when is healing done? -- cgit v1.2.3 From 8e0a4f922906a118bc0a7bcc2e8680c6ba9060df Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:01:03 -0500 Subject: use self.registers in text_wait --- pokemontools/vba/vba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d8ceebb..0818b93 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -142,8 +142,8 @@ class crystal(object): :param max_wait: number of wait loops to perform """ while max_wait > 0: - hi = self.vba.read_memory_at(registers.sp + 1) - lo = self.vba.read_memory_at(registers.sp) + hi = self.vba.read_memory_at(self.registers.sp + 1) + lo = self.vba.read_memory_at(self.registers.sp) address = ((hi << 8) | lo) if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): # 0xaef: -- cgit v1.2.3 From 2614df2587f3ccd9ea3cdf9604c0025f5113507b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:02:56 -0500 Subject: use the state property during test bootstrapping --- tests/test_vba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index d11108d..1d185fa 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -31,14 +31,14 @@ def bootstrap(): is constructed by this function. """ - cry = vba.crystal() + 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() - state = cry.get_state() + state = cry.vba.state # clean everything up again cry.vba.shutdown() -- cgit v1.2.3 From f83fc26b499a55d5611e16726a356981b682dd63 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:04:16 -0500 Subject: fix skippable decorator emulator state setter --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index a143f1e..13565d4 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -37,7 +37,7 @@ def skippable(func): return_value = func(*args, **kwargs) self.cry.save_state(func.__name__ + "-end", override=True) elif skip: - self.cry.vba.set_state(self.cry.vba.load_state(func.__name__ + "-end")) + self.cry.vba.state = self.cry.vba.load_state(func.__name__ + "-end") return return_value return wrapped_function -- cgit v1.2.3 From 7bfbadc5687a1eb297db0ff5956478a8bd38178f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:10:46 -0500 Subject: implement a vba helper func for state loading This re-implements the load_state method that previously existed. I forget why it was removed, but basically a similar function is needed again, and it doesn't entirely belong in the emulator or in the emulator wrapper because these save states are game-specific. --- pokemontools/vba/vba.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 0818b93..7dc0d8f 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -77,6 +77,30 @@ class crystal(object): with open(save_path, "wb") as file_handler: file_handler.write(state) + def load_state(self, name, loadit=True): + """ + Read a state from file based on the name of the state. + + Looks in save_state_path for a file with this name (".sav" is + optional). + + @param loadit: whether or not to set the emulator to this state + """ + save_path = os.path.join(self.config.save_state_path, name) + + if not os.path.exists(save_path): + if len(name) < 4 or name[-4:] != ".sav": + name += ".sav" + save_path = os.path.join(save_state_path, name) + + with open(save_path, "rb") as file_handler: + state = file_handler.read() + + if loadit: + self.vba.state = state + + return state + def call(self, bank, address): """ Jumps into a function at a certain address. -- cgit v1.2.3 From 81504f340187fe6e3fff7ed13d947216fe36030c Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:11:53 -0500 Subject: load_state was called on the wrong object --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 13565d4..6755db9 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -37,7 +37,7 @@ def skippable(func): return_value = func(*args, **kwargs) self.cry.save_state(func.__name__ + "-end", override=True) elif skip: - self.cry.vba.state = self.cry.vba.load_state(func.__name__ + "-end") + self.cry.vba.state = self.cry.load_state(func.__name__ + "-end") return return_value return wrapped_function -- cgit v1.2.3 From bf20b9982b19535044a7d40848bd46a04731694e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:12:55 -0500 Subject: save_state_path is only on self.config --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 7dc0d8f..1b33ba7 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -91,7 +91,7 @@ class crystal(object): if not os.path.exists(save_path): if len(name) < 4 or name[-4:] != ".sav": name += ".sav" - save_path = os.path.join(save_state_path, name) + save_path = os.path.join(self.config.save_state_path, name) with open(save_path, "rb") as file_handler: state = file_handler.read() -- cgit v1.2.3 From a3bab14b2657582675b2a94c99e55ae3148e93eb Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:28:06 -0500 Subject: weird, why was there no shutdown command? --- pokemontools/vba/vba.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 1b33ba7..3c8c084 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -56,6 +56,12 @@ class crystal(object): if not os.path.exists(self.config.rom_path): raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + def shutdown(self): + """ + Reset the emulator. + """ + self.vba.shutdown() + def save_state(self, name, state=None, override=False): """ Saves the given state to save_state_path. -- cgit v1.2.3 From 5aa423ee6294cfba1f4b98d1a8720da909346436 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 01:55:26 -0500 Subject: make the vba tests pass --- tests/test_vba.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 1d185fa..fcd5c61 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -11,6 +11,8 @@ try: except ImportError: import pokemontools.vba.autoplayer as autoplayer +import pokemontools.vba.keyboard as keyboard + autoplayer.vba = vba def setup_wram(): @@ -36,7 +38,7 @@ def bootstrap(): # skip=False means run the skip_intro function instead of just skipping to # a saved state. - runner.skip_intro() + runner.skip_intro(skip=True) state = cry.vba.state @@ -56,28 +58,40 @@ class VbaTests(unittest.TestCase): # # figure out addresses # cls.wram = setup_wram() - # FIXME: work around jython2.5 unittest - state = bootstrap() - wram = setup_wram() + cry = None + wram = None + + @classmethod + def setUpClass(cls): + cls.bootstrap_state = bootstrap() + + cls.wram = setup_wram() + + cls.cry = vba.crystal() + cls.vba = cls.cry.vba + + cls.vba.state = cls.bootstrap_state def get_wram_value(self, name): - return vba.get_memory_at(self.wram[name]) + return self.vba.memory[self.wram[name]] def setUp(self): - # clean the state - vba.shutdown() - vba.load_rom() + #if self.cry: + # # clean up the emulator's state + # self.cry.shutdown() + #self.cry = vba.crystal() + #self.vba = self.cry.vba # reset to whatever the bootstrapper created - vba.set_state(self.state) + self.vba.state = self.bootstrap_state - def tearDown(self): - vba.shutdown() + #def tearDown(self): + # self.vba.shutdown() 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")) @@ -85,7 +99,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") @@ -95,7 +109,7 @@ 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") @@ -104,7 +118,7 @@ class VbaTests(unittest.TestCase): class TestEmulator(unittest.TestCase): @classmethod def setUpClass(cls): - cls.cry = crystal() + cls.cry = vba.crystal() # advance it forward past the intro sequences cls.cry.vba.step(count=3500) -- cgit v1.2.3 From f52f6148d775e51a9cb68201effef905c0880c75 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:06:38 -0500 Subject: combine some tests together --- tests/test_vba.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index fcd5c61..3a2de72 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -47,6 +47,14 @@ def bootstrap(): return state +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"] + + 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. @@ -115,14 +123,6 @@ class VbaTests(unittest.TestCase): player_action = self.get_wram_value("PlayerAction") self.assertEqual(player_action, 1) # 1 = standing -class TestEmulator(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.cry = vba.crystal() - - # advance it forward past the intro sequences - cls.cry.vba.step(count=3500) - def test_PlaceString(self): self.cry.call(0, 0x1078) @@ -138,12 +138,5 @@ class TestEmulator(unittest.TestCase): self.assertTrue("TRAINER" in text) - def test_keyboard_planner(self): - button_sequence = keyboard.plan_typing("an") - expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] - - self.assertEqual(len(expected_result), len(button_sequence)) - self.assertEqual(expected_result, button_sequence) - if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From ffad17245b4b2ffa33a97fde0bd51bd643fc1faa Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:07:43 -0500 Subject: remove old jython comments from the vba tests --- tests/test_vba.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 3a2de72..1b151b7 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -56,16 +56,6 @@ class OtherVbaTests(unittest.TestCase): 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 @@ -80,21 +70,16 @@ class VbaTests(unittest.TestCase): cls.vba.state = cls.bootstrap_state - def get_wram_value(self, name): - return self.vba.memory[self.wram[name]] + @classmethod + def tearDownClass(cls): + cls.vba.shutdown() def setUp(self): - #if self.cry: - # # clean up the emulator's state - # self.cry.shutdown() - #self.cry = vba.crystal() - #self.vba = self.cry.vba - # reset to whatever the bootstrapper created self.vba.state = self.bootstrap_state - #def tearDown(self): - # self.vba.shutdown() + def get_wram_value(self, name): + return self.vba.memory[self.wram[name]] def test_movement_changes_player_direction(self): player_direction = self.get_wram_value("PlayerDirection") -- cgit v1.2.3 From 912efe7fd0127a0abd5c92c51990d732b18d32a6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:12:11 -0500 Subject: make SpeedRunner.setup use the right config ref --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 6755db9..aa57825 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -60,7 +60,7 @@ class SpeedRunner(Runner): def setup(self): if not self.cry: - self.cry = _vba.crystal(config=config) + self.cry = _vba.crystal(config=self.config) def main(self): """ -- cgit v1.2.3 From 80cda61ec4478db3253741bc32beae0f6edd5b78 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:17:02 -0500 Subject: test autoplayer handle_mom --- tests/test_vba.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index 1b151b7..ec74a55 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -123,5 +123,24 @@ class VbaTests(unittest.TestCase): self.assertTrue("TRAINER" in text) + def test_speedrunner_constructor(self): + runner = autoplayer.SpeedRunner(cry=self.cry) + + def test_speedrunner_handle_mom(self): + self.vba.shutdown() + + # 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 + first_map_y = self.get_wram_value("MapY") + runner.cry.move("d") + second_map_y = self.get_wram_value("MapY") + + self.assertNotEqual(first_map_y, second_map_y) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 3e0a674817cd4345c9b20d910ee81db406717234 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:21:08 -0500 Subject: test the walk_into_new_bark_town vba method --- tests/test_vba.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index ec74a55..e9ff73c 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -142,5 +142,21 @@ class VbaTests(unittest.TestCase): self.assertNotEqual(first_map_y, second_map_y) + 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 again if the game is in a state where the player can walk + first_map_y = self.get_wram_value("MapY") + runner.cry.move("d") + second_map_y = self.get_wram_value("MapY") + + self.assertNotEqual(first_map_y, second_map_y) + + # TODO: test the current map id against what it should be + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 57dc8c4fd0b14e4cca190061c47993e1231a931d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:27:57 -0500 Subject: add more defaults to setup_wram for testing This is sorta absurd, it should just load these values by parsing wram.asm on its own. --- tests/test_vba.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index e9ff73c..5098a7d 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -20,11 +20,19 @@ 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(): -- cgit v1.2.3 From 7d3994ae753d3fa208401afbab6678328d6ecc8e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:33:02 -0500 Subject: test that the current map is correct --- tests/test_vba.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 5098a7d..9f7b8f1 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -162,9 +162,12 @@ class VbaTests(unittest.TestCase): runner.cry.move("d") second_map_y = self.get_wram_value("MapY") + # check that the player has moved self.assertNotEqual(first_map_y, second_map_y) - # TODO: test the current map id against what it should be + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 4) if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From a04f6ceb4b98d9b37390905b7ab60bd5e35fcc21 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:46:40 -0500 Subject: fix cyndaquil selection in Elm's Lab --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index aa57825..5bbf91e 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -257,7 +257,7 @@ class SpeedRunner(Runner): if starter_choice.lower() == "cyndaquil": moves = 0 - if starter_choice.lower() == "totodile": + elif starter_choice.lower() == "totodile": moves = 1 else: moves = 2 -- cgit v1.2.3 From 50997c8b3849d70483266dce2661e3f076558cc6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:48:26 -0500 Subject: get rid of a text_wait when talking to Elm --- pokemontools/vba/autoplayer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 5bbf91e..744b9e3 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -303,9 +303,6 @@ class SpeedRunner(Runner): # talk with elm a bit more self.cry.text_wait() - # TODO: and again.. wtf? - self.cry.text_wait() - # wait until the script is done running self.cry.wait_for_script_running() -- cgit v1.2.3 From 742bdf8c2fffccf1c216f08192693dab7241a597 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:48:41 -0500 Subject: test that the Elm's Lab sequence works --- tests/test_vba.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index 9f7b8f1..55489fb 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -169,5 +169,29 @@ class VbaTests(unittest.TestCase): self.assertEqual(self.get_wram_value("MapGroup"), 24) self.assertEqual(self.get_wram_value("MapNumber"), 4) + def test_speedrunner_handle_elm(self): + self.vba.shutdown() + + 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 + first_map_y = self.get_wram_value("MapY") + runner.cry.move("d") + second_map_y = self.get_wram_value("MapY") + + # check that the player has moved + self.assertNotEqual(first_map_y, second_map_y) + + # check that the map is correct + self.assertEqual(self.get_wram_value("MapGroup"), 24) + self.assertEqual(self.get_wram_value("MapNumber"), 5) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 840e447557548b67d0c6436949d86c789f0e8fe9 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:55:08 -0500 Subject: change the walk test after Elm's Lab The walk test is useful to see if the player is able to move, but it shouldn't walk down and out of the building because the test is comparing against the MapNumber for Elm's Lab and not New Bark Town. --- tests/test_vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 55489fb..0232b1d 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -183,7 +183,7 @@ class VbaTests(unittest.TestCase): # test again if the game is in a state where the player can walk first_map_y = self.get_wram_value("MapY") - runner.cry.move("d") + runner.cry.move("u") second_map_y = self.get_wram_value("MapY") # check that the player has moved -- cgit v1.2.3 From 3151acd42c6f7f448499b6cdf20a603ff7c150e8 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 02:58:25 -0500 Subject: remove unnecessary emulator shutdown --- tests/test_vba.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 0232b1d..9a772cd 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -170,8 +170,6 @@ class VbaTests(unittest.TestCase): self.assertEqual(self.get_wram_value("MapNumber"), 4) def test_speedrunner_handle_elm(self): - self.vba.shutdown() - runner = autoplayer.SpeedRunner(cry=None) runner.setup() runner.skip_intro(skip=True) -- cgit v1.2.3 From 1d92396ca92d25cf19bdf47ce467e467e8638d08 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 10:10:46 -0500 Subject: reduce some duplicated code inside some tests Those movement checks are now collapsed into a single function that each test can individually call. --- tests/test_vba.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 9a772cd..f82576b 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -89,6 +89,16 @@ class VbaTests(unittest.TestCase): 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 test_movement_changes_player_direction(self): player_direction = self.get_wram_value("PlayerDirection") @@ -144,11 +154,7 @@ class VbaTests(unittest.TestCase): runner.handle_mom(skip=False) # confirm that handle_mom is done by attempting to move on the map - first_map_y = self.get_wram_value("MapY") - runner.cry.move("d") - second_map_y = self.get_wram_value("MapY") - - self.assertNotEqual(first_map_y, second_map_y) + self.assertTrue(self.check_movement("d")) def test_speedrunner_walk_into_new_bark_town(self): runner = autoplayer.SpeedRunner(cry=None) @@ -157,13 +163,8 @@ class VbaTests(unittest.TestCase): runner.handle_mom(skip=True) runner.walk_into_new_bark_town(skip=False) - # test again if the game is in a state where the player can walk - first_map_y = self.get_wram_value("MapY") - runner.cry.move("d") - second_map_y = self.get_wram_value("MapY") - - # check that the player has moved - self.assertNotEqual(first_map_y, second_map_y) + # 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) @@ -180,12 +181,7 @@ class VbaTests(unittest.TestCase): runner.handle_elm("cyndaquil", skip=False) # test again if the game is in a state where the player can walk - first_map_y = self.get_wram_value("MapY") - runner.cry.move("u") - second_map_y = self.get_wram_value("MapY") - - # check that the player has moved - self.assertNotEqual(first_map_y, second_map_y) + self.assertTrue(self.check_movement("u")) # check that the map is correct self.assertEqual(self.get_wram_value("MapGroup"), 24) -- cgit v1.2.3 From 13a1da5e84364435895d43b96cc5c821227dc688 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 10:14:17 -0500 Subject: remove vba.shutdown() from another test --- tests/test_vba.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index f82576b..a2a17d6 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -145,8 +145,6 @@ class VbaTests(unittest.TestCase): runner = autoplayer.SpeedRunner(cry=self.cry) def test_speedrunner_handle_mom(self): - self.vba.shutdown() - # TODO: why can't i pass in the current state of the emulator? runner = autoplayer.SpeedRunner(cry=None) runner.setup() -- cgit v1.2.3 From c1fbb59696876011be7fff57986e8e97f15ece35 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 10:21:09 -0500 Subject: a test for moving in circles Well, it's more like a square. --- tests/test_vba.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index a2a17d6..787a151 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -185,5 +185,32 @@ class VbaTests(unittest.TestCase): 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) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From a4c418bc7a2b99ad3da7f421bba8cf7415f9f021 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 10:50:21 -0500 Subject: fix some func calls in the auto level grinder --- pokemontools/vba/autoplayer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 744b9e3..5ee2ac1 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -366,7 +366,7 @@ class SpeedRunner(Runner): # wait for the move to be over self.cry.text_wait() - hp = ((self.cry.vba.get_memory_at(0xd218) << 8) | self.cry.vba.get_memory_at(0xd217)) + hp = ((self.cry.vba.memory[0xd218] << 8) | self.cry.vba.memory[0xd217]) print "enemy hp is: " + str(hp) if hp == 0: @@ -377,11 +377,11 @@ class SpeedRunner(Runner): attacks = attacks - 1 - while self.cry.vba.get_memory_at(0xd22d) != 0: + 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.nstep(50) + self.cry.vba.step(count=50) print "okay, back in the overworld" @@ -449,7 +449,7 @@ class SpeedRunner(Runner): self.cry.wait_for_script_running() # wait for it to be really really done - self.cry.vba.nstep(50) + self.cry.vba.step(count=50) self.cry.move("r") self.cry.move("r") @@ -469,7 +469,7 @@ class SpeedRunner(Runner): self.cry.move("d") # check partymon1 level - if self.cry.vba.get_memory_at(0xdcfe) < level: + if self.cry.vba.memory[0xdcfe] < level: self.new_bark_level_grind(level, skip=False) else: return -- cgit v1.2.3 From 6a1fc607718e70a5e8c50309c51d6b2e4bfd54a1 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 10:55:44 -0500 Subject: better IsInBattle detection for level grinding --- pokemontools/vba/autoplayer.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 5ee2ac1..987395a 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -343,11 +343,19 @@ class SpeedRunner(Runner): # walk to the grass area self.new_bark_level_grind_walk_to_grass(skip=False) - # TODO: walk around in grass, handle battles - walk = ["d", "d", "u", "d", "u", "d"] - for direction in walk: + 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() -- cgit v1.2.3 From d0b0b9c5f2e0d302b9150e6eeb5be9b2467fb792 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 11:12:38 -0500 Subject: only heal if HP is low or move1 PP is low --- pokemontools/vba/autoplayer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 987395a..b47634e 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -332,7 +332,7 @@ class SpeedRunner(Runner): return @skippable - def new_bark_level_grind(self, level): + def new_bark_level_grind(self, level, walk_to_grass=True): """ Do level grinding in New Bark. @@ -341,7 +341,8 @@ class SpeedRunner(Runner): """ # walk to the grass area - self.new_bark_level_grind_walk_to_grass(skip=False) + if walk_to_grass: + self.new_bark_level_grind_walk_to_grass(skip=False) last_direction = "u" @@ -391,8 +392,24 @@ class SpeedRunner(Runner): # 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: + 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") -- cgit v1.2.3 From 54579ad69962d62a7af874635ff4e3e5fb4738b7 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 11:38:35 -0500 Subject: make skippable decorator not always save state There are some runs where the "skippable" decorator should not save the state of the game before and after, like if the function is given different parameters and the after state should not be the canonical after state. --- pokemontools/vba/autoplayer.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index b47634e..c1e36bc 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -19,11 +19,16 @@ def skippable(func): 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" @@ -33,9 +38,13 @@ def skippable(func): return_value = None if not skip: - self.cry.save_state(func.__name__ + "-start", override=True) + if override: + self.cry.save_state(func.__name__ + "-start", override=override) + return_value = func(*args, **kwargs) - self.cry.save_state(func.__name__ + "-end", override=True) + + if override: + self.cry.save_state(func.__name__ + "-end", override=override) elif skip: self.cry.vba.state = self.cry.load_state(func.__name__ + "-end") @@ -81,7 +90,7 @@ class SpeedRunner(Runner): self.new_bark_level_grind(10, skip=False) @skippable - def skip_intro(self): + def skip_intro(self, stop_at_name_selection=False): """ Skip the game boot intro sequence. """ @@ -124,6 +133,9 @@ class SpeedRunner(Runner): # read text until name selection self.cry.text_wait() + if stop_at_name_selection: + return + # select "Chris" self.cry.vba.press("d", hold=10, after=1) self.cry.vba.press("a", hold=50, after=1) -- cgit v1.2.3 From e3f6d91d0672764ffdcb2cb17483b9cba8845437 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 11:39:34 -0500 Subject: basic keyboard writing test --- tests/test_vba.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index 787a151..b9ce832 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -212,5 +212,26 @@ class VbaTests(unittest.TestCase): self.assertEqual(self.get_wram_value("MapGroup"), 24) self.assertEqual(self.get_wram_value("MapNumber"), 4) + def test_keyboard_typing(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 + text = self.cry.get_text() + + while "YOUR NAME?" not in text: + self.cry.step(count=50) + text = self.cry.get_text() + + self.cry.write() + + # save this selection + self.cry.vba.press("a", hold=20) + + # TODO: confirm the test was real + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 6e4c7d5a0f1e5d416b204e83348df3a619631d8f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 12:14:49 -0500 Subject: pause a few frames before typing on a keyboard For names that weren't starting with a capletter, the "select" button to switch to downcase was happening too soon. So add in a small delay to get the keyboard writing to work. --- pokemontools/vba/vba.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 3c8c084..654292e 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -430,7 +430,12 @@ class crystal(object): """ for buttons in button_sequence: self.vba.press(buttons) - self.vba.step(count=2) + + if buttons == "select": + self.vba.step(count=5) + else: + self.vba.step(count=2) + self.vba.press([]) def write(self, something="TrAiNeR"): @@ -440,7 +445,9 @@ class crystal(object): Uses a planning algorithm to do this in the most efficient way possible. """ button_sequence = keyboard.plan_typing(something) + self.vba.step(count=10) self.keyboard_apply([[x] for x in button_sequence]) + return button_sequence def set_partymon2(self): """ -- cgit v1.2.3 From e048f192d9a81bf99d6eb5d7f78714cc825b4859 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 12:15:33 -0500 Subject: test keyboard typing functions This tests 18 different names being typed on the keyboard. These are supposed to be typed using the shortest possible sequence of button presses to get to the right letter selection. --- tests/test_vba.py | 72 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index b9ce832..8527e7b 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -99,6 +99,17 @@ class VbaTests(unittest.TestCase): 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") @@ -212,26 +223,57 @@ class VbaTests(unittest.TestCase): self.assertEqual(self.get_wram_value("MapGroup"), 24) self.assertEqual(self.get_wram_value("MapNumber"), 4) - def test_keyboard_typing(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 - text = self.cry.get_text() - - while "YOUR NAME?" not in text: - self.cry.step(count=50) - text = self.cry.get_text() + def test_keyboard_typing_dumb_name(self): + self.bootstrap_name_prompt() - self.cry.write() + name = "tRaInEr" + self.cry.write(name) # save this selection self.cry.vba.press("a", hold=20) - # TODO: confirm the test was real + 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", + ] + + 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() -- cgit v1.2.3 From 0b7ed30a33fb9775b2b5e1f3245a2e28608c9fc6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 12:42:02 -0500 Subject: go heal if level reached the target --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index c1e36bc..0049021 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -418,7 +418,7 @@ class SpeedRunner(Runner): # if pokemon health is >20, just continue # if move 1 PP is 0, just continue - if cur_hp > 20 and move_pp > 5: + 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) -- cgit v1.2.3 From 3c4207d777a31914eeb76fb19e9ddd4ac34575f0 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 17:26:56 -0500 Subject: basic level-up stats screen detection --- pokemontools/vba/vba.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 654292e..ce4bef2 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -424,6 +424,39 @@ class crystal(object): return output + def is_showing_stats_screen(self): + """ + This is meant to detect whether or not the stats screen is showing. + This is the menu that pops up after leveling up. + """ + # These words must be on the screen if the stats screen is currently + # displayed. + parts = [ + "ATTACK", + "DEFENSE", + "SPCL.ATK", + "SPCL.DEF", + "SPEED", + ] + + # get the current text on the screen + text = self.get_text() + + if all([part in text for part in parts]): + return True + else: + return False + + def handle_stats_screen(self, force=False): + """ + Attempts to bypass a stats screen. Set force=True if you want to make + the attempt regardless of whether or not the system thinks a stats + screen is showing. + """ + if self.is_showing_stats_screen() or force: + self.vba.press("a") + self.vba.step(count=20) + def keyboard_apply(self, button_sequence): """ Applies a sequence of buttons to the on-screen keyboard. -- cgit v1.2.3 From 2268dd43b599c691dc38c45afa5569eaf02963ef Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 17:27:21 -0500 Subject: additional words for the keyboard testing --- tests/test_vba.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index 8527e7b..f8bdaa6 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -254,6 +254,10 @@ class VbaTests(unittest.TestCase): "pythoN", "python", "PyThOn", + "Zot", + "Death", + "Hiro", + "HIRO", ] self.bootstrap_name_prompt() -- cgit v1.2.3 From 38ecfa8705fb6140a00988ef8fc88272974cbe36 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 22 Sep 2013 17:27:35 -0500 Subject: use explicit skips in vba autoplayer --- pokemontools/vba/autoplayer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 0049021..479924c 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -76,18 +76,18 @@ class SpeedRunner(Runner): Start the game. """ # get past the opening sequence - self.skip_intro() + self.skip_intro(skip=True) # walk to mom and handle her text - self.handle_mom() + self.handle_mom(skip=True) # walk outside into new bark town - self.walk_into_new_bark_town() + self.walk_into_new_bark_town(skip=True) # walk to elm and do whatever he wants - self.handle_elm("totodile") + self.handle_elm("totodile", skip=True) - self.new_bark_level_grind(10, skip=False) + self.new_bark_level_grind(6, skip=False) @skippable def skip_intro(self, stop_at_name_selection=False): -- cgit v1.2.3 From 27ea6f652796a177ea4b102dbea504a459e11cf5 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 26 Sep 2013 01:03:58 -0500 Subject: grind to a higher level --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 479924c..6aa9494 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -87,7 +87,7 @@ class SpeedRunner(Runner): # walk to elm and do whatever he wants self.handle_elm("totodile", skip=True) - self.new_bark_level_grind(6, skip=False) + self.new_bark_level_grind(17, skip=False) @skippable def skip_intro(self, stop_at_name_selection=False): -- cgit v1.2.3 From c2d13dab14153c405657471d44e49d83f36e00e6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:17:57 -0500 Subject: write get_enemy_hp to calculate current hp --- pokemontools/vba/vba.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index ce4bef2..433df40 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -552,3 +552,10 @@ class crystal(object): #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: self.vba.step(count=10) memory = self.vba.memory + + def get_enemy_hp(self): + """ + Returns the HP of the current enemy. + """ + hp = ((self.cry.vba.memory[0xd218] << 8) | self.cry.vba.memory[0xd217]) + return hp -- cgit v1.2.3 From de5a585e5e1f1829ee26bca6e5b7d8aa9dceee25 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:18:52 -0500 Subject: oops, made a mistake in get_enemy_hp --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 433df40..007c132 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -557,5 +557,5 @@ class crystal(object): """ Returns the HP of the current enemy. """ - hp = ((self.cry.vba.memory[0xd218] << 8) | self.cry.vba.memory[0xd217]) + hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) return hp -- cgit v1.2.3 From 80fe5be90664faf03992c160055b9fa061eb74af Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:22:53 -0500 Subject: use get_enemy_hp instead of a custom check --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 6aa9494..847ebbd 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -387,7 +387,7 @@ class SpeedRunner(Runner): # wait for the move to be over self.cry.text_wait() - hp = ((self.cry.vba.memory[0xd218] << 8) | self.cry.vba.memory[0xd217]) + hp = self.cry.get_enemy_hp() print "enemy hp is: " + str(hp) if hp == 0: -- cgit v1.2.3 From 2328b2bd3addc7034e2a7d14dc39e39b7627df82 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:25:39 -0500 Subject: call super __init__() in SpeedRunner --- pokemontools/vba/autoplayer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 847ebbd..428b85d 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -60,6 +60,8 @@ class Runner(object): class SpeedRunner(Runner): def __init__(self, cry=None, config=None): + super(SpeedRunner, self).__init__() + self.cry = cry if not config: -- cgit v1.2.3 From 819d16726ec2e4cbe04c6803539b4c13014717b5 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:29:03 -0500 Subject: improve some VBA-related docstrings --- pokemontools/vba/autoplayer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 428b85d..2d62433 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -70,12 +70,16 @@ class SpeedRunner(Runner): self.config = config def setup(self): + """ + Configure this ``Runner`` instance to contain a reference to an active + emulator session. + """ if not self.cry: self.cry = _vba.crystal(config=self.config) def main(self): """ - Start the game. + Main entry point for complete control of the game as the main player. """ # get past the opening sequence self.skip_intro(skip=True) @@ -548,6 +552,9 @@ class SpeedRunner(Runner): self.cry.move("d") def main(): + """ + Setup a basic ``SpeedRunner`` instance and then run the runner. + """ runner = SpeedRunner() runner.setup() return runner.main() -- cgit v1.2.3 From 49c51e03d059bb5a93b4dcd494bd145f5f96428a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:31:05 -0500 Subject: even more docstrings --- pokemontools/vba/vba.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 007c132..e30616f 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -368,6 +368,9 @@ class crystal(object): return name def warp(self, map_group_id, map_id, x, y): + """ + Warp into another map. + """ self.vba.write_memory_at(0xdcb5, map_group_id) self.vba.write_memory_at(0xdcb6, map_id) self.vba.write_memory_at(0xdcb7, y) @@ -378,10 +381,17 @@ class crystal(object): self.vba.write_memory_at(0xd434, 0 & 251) def warp_pokecenter(self): + """ + Warp straight into a pokecenter. + """ self.warp(1, 1, 3, 3) self.nstep(200) def masterballs(self): + """ + Deposit some pokeballs into the first few slots of the pack. This + overrides whatever items were previously there. + """ # masterball self.vba.write_memory_at(0xd8d8, 1) self.vba.write_memory_at(0xd8d9, 99) -- cgit v1.2.3 From e13cdeab1104260e2dcfd04aa9dbaa5688f357d0 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:33:52 -0500 Subject: another minor docstring --- pokemontools/vba/vba.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index e30616f..81b27a3 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -29,6 +29,10 @@ button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine def translate_chars(charz): + """ + Translate a string from the in-game format to readable form. This is + accomplished through the same lookup table that the preprocessors use. + """ result = "" for each in charz: result += chars[each] -- cgit v1.2.3 From 5fdd27030e0268b6fa494b9219d3a1ac8c2a35cb Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 12 Oct 2013 16:41:50 -0500 Subject: move() can now take a list of movements to make --- pokemontools/vba/vba.py | 18 +++++++++++------- tests/test_vba.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 81b27a3..81ffe4e 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -557,15 +557,19 @@ class crystal(object): """ Attempt to move the player. """ - self.vba.press(cmd, hold=10, after=0) - self.vba.press([]) + if isinstance(cmd, list): + for command in cmd: + self.move(cmd) + else: + self.vba.press(cmd, hold=10, after=0) + self.vba.press([]) - memory = self.vba.memory - #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: - while memory[0xd043] in [0, 1, 2, 3]: - #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: - self.vba.step(count=10) memory = self.vba.memory + #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: + while memory[0xd043] in [0, 1, 2, 3]: + #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: + self.vba.step(count=10) + memory = self.vba.memory def get_enemy_hp(self): """ diff --git a/tests/test_vba.py b/tests/test_vba.py index f8bdaa6..b68e24b 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -223,6 +223,29 @@ class VbaTests(unittest.TestCase): 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() -- cgit v1.2.3 From 0283856044c98792fd62db6672f01f81876d5e26 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 00:05:57 -0500 Subject: a basic battle handling framework --- pokemontools/vba/battle.py | 105 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 pokemontools/vba/battle.py diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py new file mode 100644 index 0000000..3d4bb55 --- /dev/null +++ b/pokemontools/vba/battle.py @@ -0,0 +1,105 @@ +""" +Code that attempts to model a battle. +""" + +from pokemontools.vba.vba import crystal as emulator + +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. + """ + + # Maybe this should be an instance variable instead, but since there can + # only be one emulator per instance (??) it doesn't really matter right + # now. + emulator = emulator + + def __init__(self, hook): + """ + Setup the battle. + + @param hook: object that implements handle_turn and handle_mandatory_switch + @type hook: BattleHook + """ + self.hook = hook + + @classmethod + def is_in_battle(cls): + """ + @rtype: bool + """ + return cls.emulator.is_in_battle() + + def run(self): + """ + Step through the entire battle. + """ + # xyz wants to battle + self.skip_start_text() + + while self.is_in_battle(): + self.skip_crap() + + if self.is_player_turn(): + self.hook.handle_turn() + elif self.is_mandatory_switch(): + self.hook.handle_mandatory_switch() + else: + raise BattleException("unknown state, aborting") + + # "how did i lose? wah" + self.skip_end_text() + +class BattleHook(object): + """ + Hooks that are called during a battle. + """ + + def __init__(self, battle): + """ + Makes references to some common objects. + """ + self.battle = battle + self.emulator = battle.emulator + + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + for pokemon in self.emulator.party: + if pokemon.hp > 0: + break + else: + # the game failed to do a blackout.. not sure what to do now. + raise BattleException("No partymons left. wtf?") + + return pokemon.id + + def handle_turn(self): + """ + Take actions inside of a battle based on the game state. + """ + self.battle.throw_pokeball() + +class SimpleBattleHook(BattleHook): + """ + Attack the enemy with the first move. + """ + + def handle_turn(self): + """ + Always attack the enemy with the first move. + """ + self.battle.attack(self.battle.party[0].moves[0].name) -- cgit v1.2.3 From 9eb78d9c5a4e3c63960c7fb990df2c5bf859f591 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 00:14:37 -0500 Subject: simplify the number of battle-related classes --- pokemontools/vba/battle.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 3d4bb55..9546378 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -26,14 +26,11 @@ class Battle(EmulatorController): # now. emulator = emulator - def __init__(self, hook): + def __init__(self, emulator=emulator): """ Setup the battle. - - @param hook: object that implements handle_turn and handle_mandatory_switch - @type hook: BattleHook """ - self.hook = hook + self.emulator = emulator @classmethod def is_in_battle(cls): @@ -62,17 +59,22 @@ class Battle(EmulatorController): # "how did i lose? wah" self.skip_end_text() -class BattleHook(object): - """ - Hooks that are called during a battle. - """ + def handle_mandatory_switch(self): + """ + Something fainted, pick the next mon. + """ + raise NotImplementedError - def __init__(self, battle): + def handle_turn(self): """ - Makes references to some common objects. + Take actions inside of a battle based on the game state. """ - self.battle = battle - self.emulator = battle.emulator + raise NotImplementedError + +class BattleStrategy(Battle): + """ + Throw a pokeball until everyone dies. + """ def handle_mandatory_switch(self): """ @@ -93,7 +95,7 @@ class BattleHook(object): """ self.battle.throw_pokeball() -class SimpleBattleHook(BattleHook): +class SimpleBattleStrategy(BattleStrategy): """ Attack the enemy with the first move. """ @@ -102,4 +104,4 @@ class SimpleBattleHook(BattleHook): """ Always attack the enemy with the first move. """ - self.battle.attack(self.battle.party[0].moves[0].name) + self.attack(self.battle.party[0].moves[0].name) -- cgit v1.2.3 From 016d0b886444f864cf494c8d91f9de883ec9a57d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 13:54:35 -0500 Subject: fix some hook calls in Battle.run --- pokemontools/vba/battle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 9546378..8897ccb 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -50,9 +50,9 @@ class Battle(EmulatorController): self.skip_crap() if self.is_player_turn(): - self.hook.handle_turn() + self.handle_turn() elif self.is_mandatory_switch(): - self.hook.handle_mandatory_switch() + self.handle_mandatory_switch() else: raise BattleException("unknown state, aborting") -- cgit v1.2.3 From 1f185185ca15389417474f6becfddde56e8e80eb Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 14:00:04 -0500 Subject: autoplayer.bootstrap to call skip_intro There are situations other than just testing where making a bootstrapped game state is a useful ability. --- pokemontools/vba/autoplayer.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index 2d62433..a20744e 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -551,6 +551,26 @@ class SpeedRunner(Runner): self.cry.move("d") self.cry.move("d") +def bootstrap(runner=None, cry=None): + """ + Setup the initial game and return the state. This skips the intro and + performs some other actions to get the game to a reasonable starting state. + """ + if not runner: + runner = SpeedRunner(cry=cry) + runner.setup() + + # skip=False means always run the skip_intro function regardless of the + # presence of a saved after state. + runner.skip_intro(skip=True) + + # keep a reference of the current state + state = runner.cry.vba.state + + cry.vba.shutdown() + + return state + def main(): """ Setup a basic ``SpeedRunner`` instance and then run the runner. -- cgit v1.2.3 From 185856adb4a10c66a1fc206c13862ffa6958cec6 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 14:01:01 -0500 Subject: fix a call to vba.shutdown in bootstrap() --- pokemontools/vba/autoplayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/autoplayer.py b/pokemontools/vba/autoplayer.py index a20744e..af14d47 100644 --- a/pokemontools/vba/autoplayer.py +++ b/pokemontools/vba/autoplayer.py @@ -567,7 +567,7 @@ def bootstrap(runner=None, cry=None): # keep a reference of the current state state = runner.cry.vba.state - cry.vba.shutdown() + runner.cry.vba.shutdown() return state -- cgit v1.2.3 From 211da4dcb3ac2d8ca370d6b840e2368f59b5114d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 14:41:48 -0500 Subject: attempt to start a trainer battle --- pokemontools/vba/vba.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 81ffe4e..1af2364 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -577,3 +577,17 @@ class crystal(object): """ hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) return hp + + def start_trainer_battle(self, trainer_group, trainer_id): + memory = self.vba.memory + + # setup the battle + memory[0xd459] = 0x81 + memory[0xd22f] = trainer_group + memory[0xd231] = trainer_id + + self.vba.memory = memory + + Script_startbattle_address = 0x97436 + + self.call(0x25, Script_startbattle_address) -- cgit v1.2.3 From 0d4f9340b08766ae3a445b1428cbfe4b4a33031a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 20:56:40 -0500 Subject: a naive implementation of start_trainer_battle This is a really dumb way to start a battle, but the other methods aren't working yet. --- pokemontools/vba/vba.py | 91 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 1af2364..bb468b8 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -127,14 +127,7 @@ class crystal(object): 0x3bb7, ] - for value in push: - self.registers.sp -= 2 - self.vba.write_memory_at(self.registers.sp + 1, value >> 8) - self.vba.write_memory_at(self.registers.sp, value & 0xFF) - if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: - print "desired memory values: " + str([value & 0xFF, value >> 8] ) - print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) - print "wrong value at " + hex(self.registers.sp) + " expected " + hex(value) + " but got " + hex(self.vba.read_memory_at(self.registers.sp)) + self.push_stack(push) if bank != 0: self.registers["af"] = (bank << 8) | (self.registers["af"] & 0xFF) @@ -143,6 +136,16 @@ class crystal(object): else: self.registers["pc"] = address + def push_stack(self, push): + for value in push: + self.registers["sp"] -= 2 + self.vba.write_memory_at(self.registers.sp + 1, value >> 8) + self.vba.write_memory_at(self.registers.sp, value & 0xFF) + if list(self.vba.memory[self.registers.sp : self.registers.sp + 2]) != [value & 0xFF, value >> 8]: + print "desired memory values: " + str([value & 0xFF, value >> 8] ) + print "actual memory values: " + str(list(self.vba.memory[self.registers.sp : self.registers.sp + 2])) + print "wrong value at " + hex(self.registers.sp) + " expected " + hex(value) + " but got " + hex(self.vba.read_memory_at(self.registers.sp)) + def get_stack(self): """ Return a list of functions on the stack. @@ -578,16 +581,74 @@ class crystal(object): hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) return hp - def start_trainer_battle(self, trainer_group, trainer_id): + def start_trainer_battle(self, map_group=0x1, map_id=0xc, x=6, y=8, direction="l", loop_limit=10): + """ + Starts a trainer battle by warping into a map at the designated + coordinates, pressing the direction button for a full walk step (which + ideally should be blocked, this is mainly to establish direction), and + then pressing "a" to initiate the trainer battle. + """ + self.warp(map_group, map_id, x, y) + + # finish loading the map, might not be necessary? + self.nstep(100) + + # face towards the trainer (or whatever direction was specified). If + # this direction is blocked, then this will only change which direction + # the character is facing. However, if this direction is not blocked by + # the map or by an npc, then this will cause an entire step to be + # taken. + self.vba.press([direction]) + + # talk to the trainer, don't assume line of sight will be triggered + self.vba.press(["a"]) + self.vba.press([]) + + # trainer might talk, skip any text until the player can choose moves + while not self.is_in_battle() and loop_limit > 0: + self.text_wait() + loop_limit -= 1 + + def broken_start_battle(self): + # go to a map with wildmons + self.warp(0x1, 0xc, 6, 8) + self.nstep(200) + memory = self.vba.memory + Script_startbattle_address = 0x97436 + CallScript_address = 0x261f + RockSmashBattleScript_address = 0x97cf9 + RockSmashEncounter_address = 0x97cc0 + StartBattle_address = 0x3f4c1 + ScriptRunning = 0xd438 + ScriptBank = 0xd439 + ScriptPos = 0xd43a + start_wild_battle = 0x3f4dd + script = 0x1a1dc6 + # setup the battle - memory[0xd459] = 0x81 - memory[0xd22f] = trainer_group - memory[0xd231] = trainer_id + #memory[0xd459] = 0x81 + #memory[0xd22f] = trainer_group + #memory[0xd231] = trainer_id - self.vba.memory = memory + #self.vba.memory = memory - Script_startbattle_address = 0x97436 + #self.call(0x25, Script_startbattle_address % 0x4000) + + #self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) + #self.vba.registers["hl"] = RockSmashBattleScript_address % 0x4000 + #self.call(0x0, CallScript_address) + + #self.call(StartBattle_address / 0x4000, StartBattle_address % 0x4000) + #self.call(RockSmashEncounter_address / 0x4000, RockSmashEncounter_address % 0x4000) + + #self.push_stack([self.registers.pc]) + #memory[ScriptBank] = script / 0x4000 + #memory[ScriptPos] = ((script % 0x4000) & 0xff00) >> 8 + #memory[ScriptPos+1] = ((script % 0x4000) & 0xff) + #memory[ScriptRunning] = 0xff + + #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) - self.call(0x25, Script_startbattle_address) + #self.vba.memory = memory -- cgit v1.2.3 From a9a9568d142b81d07a6aa37721fe29ebbbb898ee Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 2 Nov 2013 23:12:14 -0500 Subject: a broken attempt at starting random battles --- pokemontools/vba/vba.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index bb468b8..a64a75a 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -609,6 +609,11 @@ class crystal(object): self.text_wait() loop_limit -= 1 + def broken_start_random_battle(self): + self.push_stack([self.registers.pc]) + self.registers["pc"] = address % 0x4000 + self.call(address / 0x4000, address % 0x4000) + def broken_start_battle(self): # go to a map with wildmons self.warp(0x1, 0xc, 6, 8) -- cgit v1.2.3 From a4179b5337ef767d215185d65a0f7af0f4cd4dbd Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 3 Nov 2013 01:41:07 -0500 Subject: attempting a few more trainer battles --- pokemontools/vba/vba.py | 63 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index a64a75a..05d4fad 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -581,7 +581,7 @@ class crystal(object): hp = ((self.vba.memory[0xd218] << 8) | self.vba.memory[0xd217]) return hp - def start_trainer_battle(self, map_group=0x1, map_id=0xc, x=6, y=8, direction="l", loop_limit=10): + def start_trainer_battle_lamely(self, map_group=0x1, map_id=0xc, x=6, y=8, direction="l", loop_limit=10): """ Starts a trainer battle by warping into a map at the designated coordinates, pressing the direction button for a full walk step (which @@ -614,36 +614,61 @@ class crystal(object): self.registers["pc"] = address % 0x4000 self.call(address / 0x4000, address % 0x4000) - def broken_start_battle(self): - # go to a map with wildmons - self.warp(0x1, 0xc, 6, 8) - self.nstep(200) + def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): + """ + This will fail after the first mon is defeated. + """ + Script_startbattle_address = 0x97436 + # setup the battle memory = self.vba.memory + memory[0xd459] = 0x81 + memory[0xd22f] = trainer_group + memory[0xd231] = trainer_id + self.vba.memory = memory - Script_startbattle_address = 0x97436 + self.call(0x25, (Script_startbattle_address % 0x4000) + 0x4000) + + def broken_start_random_battle_by_rocksmash_battle_script(self): + """ + This doesn't start a battle. + """ CallScript_address = 0x261f RockSmashBattleScript_address = 0x97cf9 - RockSmashEncounter_address = 0x97cc0 - StartBattle_address = 0x3f4c1 ScriptRunning = 0xd438 ScriptBank = 0xd439 ScriptPos = 0xd43a - start_wild_battle = 0x3f4dd - script = 0x1a1dc6 - # setup the battle - #memory[0xd459] = 0x81 - #memory[0xd22f] = trainer_group - #memory[0xd231] = trainer_id + memory = self.vba.memory + memory[ScriptBank] = RockSmashBattleScript_address / 0x4000 + memory[ScriptPos] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff00) >> 8 + memory[ScriptPos+1] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff) + memory[ScriptRunning] = 0xff + self.vba.memory = memory - #self.vba.memory = memory + self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) + self.vba.registers["hl"] = (RockSmashBattleScript_address % 0x4000) + 0x4000 + self.call(0x0, CallScript_address) - #self.call(0x25, Script_startbattle_address % 0x4000) + #def attempt_start_battle_by_startbattle(self): + # StartBattle_address = 0x3f4c1 + # self.call(StartBattle_address / 0x4000, (StartBattle_address % 0x4000) + 0x4000) - #self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) - #self.vba.registers["hl"] = RockSmashBattleScript_address % 0x4000 - #self.call(0x0, CallScript_address) + #def attempt_start_random_battle_by_wild_battle(self): + # start_wild_battle = 0x3f4dd + # #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) + # #self.vba.registers["pc"] = ... + + def old_crap(self): + CallScript_address = 0x261f + RockSmashBattleScript_address = 0x97cf9 + RockSmashEncounter_address = 0x97cc0 + StartBattle_address = 0x3f4c1 + ScriptRunning = 0xd438 + ScriptBank = 0xd439 + ScriptPos = 0xd43a + start_wild_battle = 0x3f4dd + script = 0x1a1dc6 #self.call(StartBattle_address / 0x4000, StartBattle_address % 0x4000) #self.call(RockSmashEncounter_address / 0x4000, RockSmashEncounter_address % 0x4000) -- cgit v1.2.3 From 2b7e903f847e1c6de7e63549f915a89b0a5795e3 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 3 Nov 2013 01:53:04 -0500 Subject: implement more parts of the battler --- pokemontools/vba/battle.py | 58 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 8897ccb..08d0b93 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -3,6 +3,7 @@ Code that attempts to model a battle. """ from pokemontools.vba.vba import crystal as emulator +import pokemontools.vba.vba as vba class BattleException(Exception): """ @@ -21,23 +22,62 @@ class Battle(EmulatorController): battle through python. """ - # Maybe this should be an instance variable instead, but since there can - # only be one emulator per instance (??) it doesn't really matter right - # now. - emulator = emulator - - def __init__(self, emulator=emulator): + def __init__(self, emulator=None): """ Setup the battle. """ self.emulator = emulator - @classmethod - def is_in_battle(cls): + def is_in_battle(self): """ @rtype: bool """ - return cls.emulator.is_in_battle() + return self.emulator.is_in_battle() + + def is_fight_pack_run_menu(self): + """ + Attempts to detect if the current menu is fight-pack-run. This is only + for whether or not the player needs to choose what to do next. + """ + signs = ["FIGHT", "PACK", "RUN"] + screentext = self.emulator.get_text() + return all([sign in screentext for sign in signs]) + + def is_player_turn(self): + return is_fight_pack_run_menu() + + def is_mandatory_switch(self): + return False # TODO + + def skip_start_text(self, max_loops=20): + """ + Skip any initial conversation until the battle has begun. + """ + 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): + if not self.is_in_battle(): + # TODO: keep talking until the character can move? + 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_crap(self): + while not self.is_flight_pack_run_menu(): + self.emulator.text_wait() def run(self): """ -- cgit v1.2.3 From 1ac0e437d19963d46bd5360e0790b2294ade72b8 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 14:44:36 -0600 Subject: add a bunch of docstrings in the battler --- pokemontools/vba/battle.py | 49 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 08d0b93..1949fbb 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -34,6 +34,12 @@ class Battle(EmulatorController): """ return self.emulator.is_in_battle() + def is_input_required(self): + """ + Detects if the battle is waiting for player input. + """ + return self.is_player_turn() or self.is_mandatory_switch() + def is_fight_pack_run_menu(self): """ Attempts to detect if the current menu is fight-pack-run. This is only @@ -44,14 +50,28 @@ class Battle(EmulatorController): return all([sign in screentext for sign in signs]) def is_player_turn(self): - return is_fight_pack_run_menu() + """ + Detects if the battle is waiting for the player to choose an attack. + """ + return self.is_fight_pack_run_menu() def is_mandatory_switch(self): - return False # TODO + """ + Detects if the battle is waiting for the player to choose a next + pokemon. + """ + # TODO: detect the mandatry switch menu + # The following conditions are probably sufficient: + # 1) current pokemon hp is 0 + # 2) game is polling for input + return False def skip_start_text(self, max_loops=20): """ - Skip any initial conversation until the battle has begun. + 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: @@ -64,6 +84,9 @@ class Battle(EmulatorController): 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? self.emulator.text_wait() @@ -75,23 +98,31 @@ class Battle(EmulatorController): if loops <= 0: raise Exception("Couldn't get out of the battle.") - def skip_crap(self): - while not self.is_flight_pack_run_menu(): + def skip_until_input_required(self): + """ + Waits until the battle needs player input. + """ + while not self.is_input_required(): self.emulator.text_wait() def run(self): """ Step through the entire battle. """ - # xyz wants to battle + # Advance to the battle from either of these states: + # 1) the player is talking with an npc + # 2) the battle has already started but there's initial text + # xyz wants to battle, a wild foobar appeared self.skip_start_text() while self.is_in_battle(): - self.skip_crap() + self.skip_until_input_required() if self.is_player_turn(): + # battle hook provides input to handle this situation self.handle_turn() elif self.is_mandatory_switch(): + # battle hook provides input to handle this situation too self.handle_mandatory_switch() else: raise BattleException("unknown state, aborting") @@ -99,6 +130,8 @@ class Battle(EmulatorController): # "how did i lose? wah" self.skip_end_text() + # TODO: return should indicate win/loss (blackout) + def handle_mandatory_switch(self): """ Something fainted, pick the next mon. @@ -133,7 +166,7 @@ class BattleStrategy(Battle): """ Take actions inside of a battle based on the game state. """ - self.battle.throw_pokeball() + self.throw_pokeball() class SimpleBattleStrategy(BattleStrategy): """ -- cgit v1.2.3 From 6a1699d32e6fb88401bd3079a5c1ddcf4e8711f5 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 14:50:55 -0600 Subject: even more comments in the battle routines --- pokemontools/vba/battle.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 1949fbb..3bda1a5 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -60,10 +60,14 @@ class Battle(EmulatorController): Detects if the battle is waiting for the player to choose a next pokemon. """ - # TODO: detect the mandatry switch menu + # 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 + + # TODO: detect the mandatory switch menu return False def skip_start_text(self, max_loops=20): @@ -88,7 +92,11 @@ class Battle(EmulatorController): Skip through any text that appears after the final attack. """ if not self.is_in_battle(): - # TODO: keep talking until the character can move? + # 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: -- cgit v1.2.3 From 11664e2ac6660a706e1241b543121822fd40166d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:08:04 -0600 Subject: a basic battle test --- tests/test_vba.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_vba.py b/tests/test_vba.py index b68e24b..13f1c2c 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -55,6 +55,29 @@ def bootstrap(): 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) + + # TODO: figure out a better way to start a trainer battle :( + runner.cry.start_trainer_battle_lamely() + + return runner.cry.vba.state + class OtherVbaTests(unittest.TestCase): def test_keyboard_planner(self): button_sequence = keyboard.plan_typing("an") @@ -302,5 +325,13 @@ class VbaTests(unittest.TestCase): pname = self.cry.get_player_name().replace("@", "") self.assertEqual(name, pname) + def test_battle_is_player_turn(self): + state = bootstrap_trainer_battle() + self.cry.vba.state = state + + battle = Battle(emulator=self.cry) + + self.assertTrue(battle.is_player_turn()) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From fd61c8f460d6e7453a79f945768f5952599ba724 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:09:21 -0600 Subject: fix move() for lists of commands --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 05d4fad..3367dee 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -562,7 +562,7 @@ class crystal(object): """ if isinstance(cmd, list): for command in cmd: - self.move(cmd) + self.move(command) else: self.vba.press(cmd, hold=10, after=0) self.vba.press([]) -- cgit v1.2.3 From cddfe3804dcf7ef0f2a39e6d9af84f98b10355ef Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:10:06 -0600 Subject: fix a few imports for testing an old func --- pokemontools/crystalparts/old_parsers.py | 2 +- tests/integration/tests.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pokemontools/crystalparts/old_parsers.py b/pokemontools/crystalparts/old_parsers.py index e07082d..2c1d2b2 100644 --- a/pokemontools/crystalparts/old_parsers.py +++ b/pokemontools/crystalparts/old_parsers.py @@ -2,7 +2,7 @@ Some old methods rescued from crystal.py """ -import pointers +import pokemontools.pointers as pointers map_header_byte_size = 9 diff --git a/tests/integration/tests.py b/tests/integration/tests.py index 40933e5..4f96699 100644 --- a/tests/integration/tests.py +++ b/tests/integration/tests.py @@ -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, -- cgit v1.2.3 From cc403392982cfef8ccc9ed2b54ecda5934873a4f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:16:34 -0600 Subject: import Battle for testing --- tests/test_vba.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_vba.py b/tests/test_vba.py index 13f1c2c..9c12cc1 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -6,15 +6,19 @@ import unittest import pokemontools.vba.vba as vba +from pokemontools.vba.battle import ( + Battle, + BattleException, +) + try: import pokemontools.vba.vba_autoplayer as autoplayer except ImportError: import pokemontools.vba.autoplayer as autoplayer +autoplayer.vba = vba import pokemontools.vba.keyboard as keyboard -autoplayer.vba = vba - def setup_wram(): """ Loads up some default addresses. Should eventually be replaced with the -- cgit v1.2.3 From b363e6cca00c05687c34d735abb93d5117f7b209 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:18:55 -0600 Subject: fix a syntax error in battle.py --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 3bda1a5..5aeb923 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -168,7 +168,7 @@ class BattleStrategy(Battle): # the game failed to do a blackout.. not sure what to do now. raise BattleException("No partymons left. wtf?") - return pokemon.id + return pokemon.id def handle_turn(self): """ -- cgit v1.2.3 From efd71a5cd311a7ad7e7d4aaa4f24eead65cce040 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:21:43 -0600 Subject: fix some configuration in vba/vba.py --- pokemontools/vba/vba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d7fdf1d..4b0bbee 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -19,8 +19,8 @@ from pokemontools.map_names import map_names import keyboard # just use a default config for now until the globals are removed completely -import pokemontools.config as conf -config = conf.Config() +import pokemontools.configuration as configuration +config = configuration.Config() project_path = config.path save_state_path = config.save_state_path rom_path = config.rom_path -- cgit v1.2.3 From 8f0b06f22ec09ad7c7ffbd73e7ae54eb2bb2c03d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:21:59 -0600 Subject: fix an import in tests.py --- tests/tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/tests.py b/tests/tests.py index 7919a66..4398f03 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -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, -- cgit v1.2.3 From 3868fb1e0622e5d3cda0c093ce3114c485fd0096 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:45:03 -0600 Subject: simplify the vba-related tests The imports for the emulator-related tests are now simplified in the tests/ folder. The bootstrapping.py file contains some shared functions that multiple test files might choose to use. Those functions probably belong in the actual module instead of in tests/. The battle-related tests have been separated from the other emulator tests. --- tests/bootstrapping.py | 51 ++++++++++++++++++++++++++++++++++ tests/setup_vba.py | 4 +++ tests/test_vba.py | 71 ++++++------------------------------------------ tests/test_vba_battle.py | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 63 deletions(-) create mode 100644 tests/bootstrapping.py create mode 100644 tests/setup_vba.py create mode 100644 tests/test_vba_battle.py diff --git a/tests/bootstrapping.py b/tests/bootstrapping.py new file mode 100644 index 0000000..17a2945 --- /dev/null +++ b/tests/bootstrapping.py @@ -0,0 +1,51 @@ +""" +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) + + # TODO: figure out a better way to start a trainer battle :( + runner.cry.start_trainer_battle_lamely() + + return runner.cry.vba.state diff --git a/tests/setup_vba.py b/tests/setup_vba.py new file mode 100644 index 0000000..6e615e2 --- /dev/null +++ b/tests/setup_vba.py @@ -0,0 +1,4 @@ +import pokemontools.vba.vba as vba +import pokemontools.vba.keyboard as keyboard +import pokemontools.vba.autoplayer as autoplayer +autoplayer.vba = vba diff --git a/tests/test_vba.py b/tests/test_vba.py index 9c12cc1..caa1867 100644 --- a/tests/test_vba.py +++ b/tests/test_vba.py @@ -4,20 +4,16 @@ Tests for VBA automation tools import unittest -import pokemontools.vba.vba as vba - -from pokemontools.vba.battle import ( - Battle, - BattleException, +from setup_vba import ( + vba, + autoplayer, + keyboard, ) -try: - import pokemontools.vba.vba_autoplayer as autoplayer -except ImportError: - import pokemontools.vba.autoplayer as autoplayer -autoplayer.vba = vba - -import pokemontools.vba.keyboard as keyboard +from bootstrapping import ( + bootstrap, + bootstrap_trainer_battle, +) def setup_wram(): """ @@ -39,49 +35,6 @@ def setup_wram(): return wram -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) - - # TODO: figure out a better way to start a trainer battle :( - runner.cry.start_trainer_battle_lamely() - - return runner.cry.vba.state - class OtherVbaTests(unittest.TestCase): def test_keyboard_planner(self): button_sequence = keyboard.plan_typing("an") @@ -329,13 +282,5 @@ class VbaTests(unittest.TestCase): pname = self.cry.get_player_name().replace("@", "") self.assertEqual(name, pname) - def test_battle_is_player_turn(self): - state = bootstrap_trainer_battle() - self.cry.vba.state = state - - battle = Battle(emulator=self.cry) - - self.assertTrue(battle.is_player_turn()) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py new file mode 100644 index 0000000..6d3b504 --- /dev/null +++ b/tests/test_vba_battle.py @@ -0,0 +1,51 @@ +""" +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 + + def test_battle_is_player_turn(self): + self.cry.vba.state = self.bootstrap_state + + battle = Battle(emulator=self.cry) + + self.assertTrue(battle.is_player_turn()) + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From eb31936aa758a9a8534a3fd53a4987c0d5d9ca0b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:53:17 -0600 Subject: simplify the battle tests --- tests/test_vba_battle.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 6d3b504..acf9d7d 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -39,13 +39,16 @@ class BattleTests(unittest.TestCase): def setUp(self): # reset to whatever the bootstrapper created self.vba.state = self.bootstrap_state + self.battle = Battle(emulator=self.cry) - def test_battle_is_player_turn(self): - self.cry.vba.state = self.bootstrap_state + def test_is_in_battle(self): + self.assertTrue(self.battle.is_in_battle()) - battle = Battle(emulator=self.cry) + def test_is_player_turn(self): + self.battle.skip_start_text() - self.assertTrue(battle.is_player_turn()) + # the initial state should be the player's turn + self.assertTrue(self.battle.is_player_turn()) if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From d5989b755aeedb3d553d225e29c3b8c7c4d8640f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 15:59:16 -0600 Subject: write another quick battle test --- tests/test_vba_battle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index acf9d7d..12b991c 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -46,9 +46,14 @@ class BattleTests(unittest.TestCase): 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()) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From 982cff40af88af1b0c04d1e135ef8ec84007e41a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 17:55:30 -0600 Subject: draw the menu in skip_until_input_required Step forward 10 frames so that the menu actually draws. Otherwise the user will probably be confused about the actual state of the battle. --- pokemontools/vba/battle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 5aeb923..824f27a 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -113,6 +113,9 @@ class Battle(EmulatorController): while not self.is_input_required(): self.emulator.text_wait() + # let the text draw so that the state is more obvious + self.emulator.vba.step(count=10) + def run(self): """ Step through the entire battle. -- cgit v1.2.3 From 5495504228d9ebb46c9d94a79692b4a2522d2042 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 9 Nov 2013 17:57:23 -0600 Subject: make sure an attack works (new test) --- tests/test_vba_battle.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 12b991c..be62fb5 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -55,5 +55,19 @@ class BattleTests(unittest.TestCase): # should not be asking for a switch so soon in the battle self.assertFalse(self.battle.is_mandatory_switch()) + def test_attack_loop(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_player_turn()) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From a6118071a0b0f3c0a43754f3336693f198d2a6da Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 13:53:24 -0600 Subject: broken attempt at calling givepoke --- pokemontools/vba/vba.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index cb0070d..515ac8d 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -636,6 +636,36 @@ class crystal(object): self.call(0x25, (Script_startbattle_address % 0x4000) + 0x4000) + def set_script(self, address): + """ + Sets the current script in wram to whatever address. + """ + ScriptBank = 0xd439 + ScriptPos = 0xd43a + + memory = self.vba.memory + memory[ScriptBank] = address / 0x4000 + memory[ScriptPos] = (((address % 0x4000) + 0x4000) & 0xff00) >> 8 + memory[ScriptPos] = ((address % 0x4000) + 0x4000) & 0xff + + # TODO: determine if this is necessary + #memory[ScriptRunning] = 0xff + + self.vba.memory = memory + + def attempt_call_givepoke(self): + """ + An attempt at calling the givepoke command directly. + """ + givepoke_address = 0x97932 + + # 0, 50, 0, 0 + givepoke_data_address = 0x6ca5 + + self.set_script(givepoke_data_address) + + self.call(givepoke_address / 0x4000, (givepoke_address % 0x4000) + 0x4000) + def broken_start_random_battle_by_rocksmash_battle_script(self): """ This doesn't start a battle. -- cgit v1.2.3 From 271cce531902e381f1207122060f7fef898a2afc Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 13:56:29 -0600 Subject: make call() calculate bank addresses --- pokemontools/vba/vba.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 515ac8d..3249ce3 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -118,13 +118,16 @@ class crystal(object): return state - def call(self, bank, address): + def call(self, address): """ Jumps into a function at a certain address. Go into the start menu, pause the game and try call(1, 0x1078) to see a string printed to the screen. """ + bank = address / 0x4000 + address = (address % 0x4000) + 0x4000 + push = [ self.registers.pc, self.registers.hl, @@ -288,7 +291,7 @@ class crystal(object): """ for step_counter in range(0, steplimit): self.walk_through_walls() - #call(0x1, 0x1078) + #call(0x1078) self.vba.step() def disable_triggers(self): @@ -617,9 +620,10 @@ class crystal(object): loop_limit -= 1 def broken_start_random_battle(self): + address = None self.push_stack([self.registers.pc]) self.registers["pc"] = address % 0x4000 - self.call(address / 0x4000, address % 0x4000) + self.call(address) def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): """ @@ -634,7 +638,7 @@ class crystal(object): memory[0xd231] = trainer_id self.vba.memory = memory - self.call(0x25, (Script_startbattle_address % 0x4000) + 0x4000) + self.call(Script_startbattle_address) def set_script(self, address): """ @@ -664,7 +668,7 @@ class crystal(object): self.set_script(givepoke_data_address) - self.call(givepoke_address / 0x4000, (givepoke_address % 0x4000) + 0x4000) + self.call(givepoke_address) def broken_start_random_battle_by_rocksmash_battle_script(self): """ @@ -685,15 +689,15 @@ class crystal(object): self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) self.vba.registers["hl"] = (RockSmashBattleScript_address % 0x4000) + 0x4000 - self.call(0x0, CallScript_address) + self.call(CallScript_address) #def attempt_start_battle_by_startbattle(self): # StartBattle_address = 0x3f4c1 - # self.call(StartBattle_address / 0x4000, (StartBattle_address % 0x4000) + 0x4000) + # self.call(StartBattle_address) #def attempt_start_random_battle_by_wild_battle(self): # start_wild_battle = 0x3f4dd - # #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) + # #self.call(start_wild_battle) # #self.vba.registers["pc"] = ... def old_crap(self): @@ -707,8 +711,8 @@ class crystal(object): start_wild_battle = 0x3f4dd script = 0x1a1dc6 - #self.call(StartBattle_address / 0x4000, StartBattle_address % 0x4000) - #self.call(RockSmashEncounter_address / 0x4000, RockSmashEncounter_address % 0x4000) + #self.call(StartBattle_address) + #self.call(RockSmashEncounter_address) #self.push_stack([self.registers.pc]) #memory[ScriptBank] = script / 0x4000 @@ -716,6 +720,6 @@ class crystal(object): #memory[ScriptPos+1] = ((script % 0x4000) & 0xff) #memory[ScriptRunning] = 0xff - #self.call(start_wild_battle / 0x4000, start_wild_battle % 0x4000) + #self.call(start_wild_battle) #self.vba.memory = memory -- cgit v1.2.3 From b36e42494df301452594534a87bdebc2296d4cc0 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 14:13:20 -0600 Subject: stop writing 0x4000 everywhere --- pokemontools/vba/vba.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 3249ce3..b3224bb 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -35,6 +35,21 @@ import vba_wrapper button_masks = vba_wrapper.core.VBA.button_masks button_combiner = vba_wrapper.core.VBA.button_combine +def calculate_bank(address): + """ + Which bank does this address exist in? + """ + return address / 0x4000 + +def calculate_address(address): + """ + Gives the relative address once the bank is loaded. + + This is not the same as the calculate_pointer in the + pokemontools.crystal.pointers module. + """ + return (address % 0x4000) + 0x4000 + def translate_chars(charz): """ Translate a string from the in-game format to readable form. This is @@ -118,15 +133,15 @@ class crystal(object): return state - def call(self, address): + def call(self, address, bank=None): """ Jumps into a function at a certain address. Go into the start menu, pause the game and try call(1, 0x1078) to see a string printed to the screen. """ - bank = address / 0x4000 - address = (address % 0x4000) + 0x4000 + if not bank: + bank = calculate_bank(address) push = [ self.registers.pc, @@ -619,12 +634,6 @@ class crystal(object): self.text_wait() loop_limit -= 1 - def broken_start_random_battle(self): - address = None - self.push_stack([self.registers.pc]) - self.registers["pc"] = address % 0x4000 - self.call(address) - def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): """ This will fail after the first mon is defeated. @@ -648,9 +657,10 @@ class crystal(object): ScriptPos = 0xd43a memory = self.vba.memory - memory[ScriptBank] = address / 0x4000 - memory[ScriptPos] = (((address % 0x4000) + 0x4000) & 0xff00) >> 8 - memory[ScriptPos] = ((address % 0x4000) + 0x4000) & 0xff + memory[ScriptBank] = calculate_bank(address) + pointer = calculate_address(address) + memory[ScriptPos] = (calculate_address(address) & 0xff00) >> 8 + memory[ScriptPos] = calculate_address(address) & 0xff # TODO: determine if this is necessary #memory[ScriptRunning] = 0xff @@ -681,14 +691,14 @@ class crystal(object): ScriptPos = 0xd43a memory = self.vba.memory - memory[ScriptBank] = RockSmashBattleScript_address / 0x4000 - memory[ScriptPos] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff00) >> 8 - memory[ScriptPos+1] = ((RockSmashBattleScript_address % 0x4000 + 0x4000) & 0xff) + memory[ScriptBank] = calculate_bank(RockSmashBattleScript_address) + memory[ScriptPos] = (calculate_address(RockSmashBattleScript_address) & 0xff00) >> 8 + memory[ScriptPos+1] = calculate_address(RockSmashBattleScript_address) & 0xff memory[ScriptRunning] = 0xff self.vba.memory = memory - self.vba.registers["af"] = ((RockSmashBattleScript_address / 0x4000) << 8) | (self.vba.registers.af & 0xff) - self.vba.registers["hl"] = (RockSmashBattleScript_address % 0x4000) + 0x4000 + self.vba.registers["af"] = (calculate_bank(RockSmashBattleScript_address) << 8) | (self.vba.registers.af & 0xff) + self.vba.registers["hl"] = calculate_address(RockSmashBattleScript_address) self.call(CallScript_address) #def attempt_start_battle_by_startbattle(self): @@ -715,9 +725,7 @@ class crystal(object): #self.call(RockSmashEncounter_address) #self.push_stack([self.registers.pc]) - #memory[ScriptBank] = script / 0x4000 - #memory[ScriptPos] = ((script % 0x4000) & 0xff00) >> 8 - #memory[ScriptPos+1] = ((script % 0x4000) & 0xff) + #self.set_script(script) #memory[ScriptRunning] = 0xff #self.call(start_wild_battle) -- cgit v1.2.3 From 699ed8c6c1fa58a7d3b3edf58542541e2e6f34af Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 14:18:54 -0600 Subject: fix a few call() calls --- pokemontools/vba/vba.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index b3224bb..c32e8bd 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -647,7 +647,7 @@ class crystal(object): memory[0xd231] = trainer_id self.vba.memory = memory - self.call(Script_startbattle_address) + self.call(calculate_address(Script_startbattle_address), bank=calculate_bank(Script_startbattle_address,)) def set_script(self, address): """ @@ -678,7 +678,7 @@ class crystal(object): self.set_script(givepoke_data_address) - self.call(givepoke_address) + self.call(calculate_address(givepoke_address), bank=calculate_bank(givepoke_address)) def broken_start_random_battle_by_rocksmash_battle_script(self): """ @@ -699,11 +699,11 @@ class crystal(object): self.vba.registers["af"] = (calculate_bank(RockSmashBattleScript_address) << 8) | (self.vba.registers.af & 0xff) self.vba.registers["hl"] = calculate_address(RockSmashBattleScript_address) - self.call(CallScript_address) + self.call(calculate_address(CallScript_address), bank=calculate_bank(CallScript_address)) #def attempt_start_battle_by_startbattle(self): # StartBattle_address = 0x3f4c1 - # self.call(StartBattle_address) + # self.call(calculate_address(StartBattle_address), bank=calculate_bank(StartBattle_address)) #def attempt_start_random_battle_by_wild_battle(self): # start_wild_battle = 0x3f4dd -- cgit v1.2.3 From 8b637503a560c7028ac066d50b7aa3923e49082a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 14:19:26 -0600 Subject: allow bank=0 in call() --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index c32e8bd..2a2c4ca 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -140,7 +140,7 @@ class crystal(object): Go into the start menu, pause the game and try call(1, 0x1078) to see a string printed to the screen. """ - if not bank: + if bank is None: bank = calculate_bank(address) push = [ -- cgit v1.2.3 From cab54cc723ee66f962d36d2e7c6d844a3df9b3b0 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 15:04:31 -0600 Subject: crude attempt at injecting asm into wram --- pokemontools/vba/vba.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 2a2c4ca..bd737da 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -187,6 +187,73 @@ class crystal(object): return addresses + def inject_asm(self, asm, address=0xc6d4): + """ + Writes asm to memory. Makes the emulator run the asm. + + This function will append "ret" to the list of bytes. Before returning, + it updates the value at the first byte to indicate that the function + has executed. + + The first byte at the given address is reserved for whether the asm has + finished executing. + """ + memory = self.vba.memory + + # the first byte is reserved for whether the script has finished + has_finished = address + memory[has_finished] = 0 + + # the second byte is where the script will be stored + script_address = address + 1 + + # TODO: error checking; make sure the last byte doesn't already return. + # Use some functions from gbz80disasm to perform this check. + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0xfa, 1, + + # ld [has_finished], a + 0x77, has_finished & 0xff, has_finished >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # append the last opcodes to the script + asm.extend(set_has_finished) + + memory[script_address : script_address + len(asm)] = asm + self.vba.memory = memory + + # make the emulator call the script + self.call(script_address, bank=0) + + # make the emulator step forward + self.vba.step(count=1) + + # check if the script has executed + # TODO: should this raise an exception if the script didn't finish? + if self.vba.memory[has_finished] == 0: + return False + elif self.vba.memory[has_finished] == 1: + return True + else: + raise Exception( + "has_finished at {has_finished} was overwritten with an unexpected value {value}".format( + has_finished=hex(has_finished), + value=self.vba.memory[has_finished], + ) + ) + def text_wait(self, step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): """ Presses the "A" button when text is done being drawn to screen. -- cgit v1.2.3 From e700c37844142b5017feddce6f25dbbba5e89f04 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 15:19:15 -0600 Subject: a working inject_asm implementation --- pokemontools/vba/vba.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index bd737da..605a69d 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -187,7 +187,7 @@ class crystal(object): return addresses - def inject_asm(self, asm, address=0xc6d4): + def inject_asm(self, asm=[], address=0xdfcf): """ Writes asm to memory. Makes the emulator run the asm. @@ -198,7 +198,7 @@ class crystal(object): The first byte at the given address is reserved for whether the asm has finished executing. """ - memory = self.vba.memory + memory = list(self.vba.memory) # the first byte is reserved for whether the script has finished has_finished = address @@ -216,10 +216,10 @@ class crystal(object): 0xf5, # ld a, 1 - 0xfa, 1, + 0x3e, 1, # ld [has_finished], a - 0x77, has_finished & 0xff, has_finished >> 8, + 0xea, has_finished & 0xff, has_finished >> 8, # pop af 0xf1, @@ -229,7 +229,7 @@ class crystal(object): ] # append the last opcodes to the script - asm.extend(set_has_finished) + asm = bytearray(asm) + bytearray(set_has_finished) memory[script_address : script_address + len(asm)] = asm self.vba.memory = memory @@ -238,7 +238,7 @@ class crystal(object): self.call(script_address, bank=0) # make the emulator step forward - self.vba.step(count=1) + self.vba.step(count=50) # check if the script has executed # TODO: should this raise an exception if the script didn't finish? -- cgit v1.2.3 From b71fbf3852fb1274b13b548e13b27303e155658c Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 15:53:12 -0600 Subject: write inject_asm_into_rom This method injects asm straight into the ROM loaded in the emulator. It does not overwrite the ROM on the file system. This method is much slower than the wram version because it involves copying memory multiples and copying the entire ROM into python and then sending it back to the emulator. --- pokemontools/vba/vba.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 605a69d..f69595b 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -187,7 +187,93 @@ class crystal(object): return addresses - def inject_asm(self, asm=[], address=0xdfcf): + def inject_asm_into_rom(self, asm=[], address=0x75 * 0x4000, has_finished_address=0xdb75): + """ + Writes asm to the loaded ROM. Calls the asm. + + :param address: ROM address for where to store the injected asm script. + The default value is an address in pokecrystal that isn't used for + anything. + + :param has_finished_address: address for where to store whether the + script executed or not. This value is restored when the script has been + confirmed to work. It's conceivable that some injected asm might need + to change that address if the asm needs to access the original wram + value itself. + """ + if len(asm) > 0x4000: + raise Exception("too much asm") + + # temporarily use wram + cached_wram_value = self.vba.memory[has_finished_address] + + # set the value at has_finished_address to 0 + reset_wram_mem = list(self.vba.memory) + reset_wram_mem[has_finished_address] = 0 + self.vba.memory = reset_wram_mem + + # set a value to indicate that the script has executed + set_has_finished = [ + # push af + 0xf5, + + # ld a, 1 + 0x3e, 1, + + # ld [has_finished], a + 0xea, has_finished_address & 0xff, has_finished_address >> 8, + + # pop af + 0xf1, + + # ret + 0xc9, + ] + + # TODO: check if asm ends with a byte that causes a return or call or + # other "ender". Raise an exception if it already returns on its own. + + # combine the given asm with the setter bytes + total_asm = asm + set_has_finished + + # get a copy of the current rom + rom = list(self.vba.rom) + + # inject the asm + rom[address : address + len(total_asm)] = total_asm + + # set the rom with the injected asm + self.vba.rom = rom + + # call the injected asm + self.call(calculate_address(address), bank=calculate_bank(address)) + + # make the emulator step forward + self.vba.step(count=20) + + # check if the script has executed (see below) + current_mem = self.vba.memory + + # reset the wram value to its original value + another_mem = list(self.vba.memory) + another_mem[has_finished_address] = cached_wram_value + self.vba.memory = another_mem + + # check if the script has actually executed + # TODO: should this raise an exception if the script didn't finish? + if current_mem[has_finished_address] == 0: + return False + elif current_mem[has_finished_address] == 1: + return True + else: + raise Exception( + "has_finished_address at {has_finished_address} was overwritten with an unexpected value {value}".format( + has_finished_address=hex(has_finished_address), + value=current_mem[has_finished_address], + ) + ) + + def inject_asm_into_wram(self, asm=[], address=0xdfcf): """ Writes asm to memory. Makes the emulator run the asm. -- cgit v1.2.3 From 6b0d8ee855af252b48709c88a46c748c956af740 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 16:29:23 -0600 Subject: implement call_script to call CallScript This is the entry point for calling in-game scripts. --- pokemontools/vba/vba.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index f69595b..42f5e5b 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -340,6 +340,54 @@ class crystal(object): ) ) + def call_script(self, address, bank=None, wram=False, force=False): + """ + Sets wram values so that the engine plays a script. + + :param address: address of the map script + :param bank: override for bank calculation (based on address) + :param wram: force bank to 0 + :param force: override an already-running script + """ + + ScriptFlags = 0xd434 + ScriptMode = 0xd437 + ScriptRunning = 0xd438 + ScriptBank = 0xd439 + ScriptPos = 0xd43a + NumScriptParents = 0xd43c + ScriptParents = 0xd43d + + num_possible_parents = 4 + len_parent = 3 + + mem = list(self.vba.memory) + + if mem[ScriptRunning] == 0xff: + if force: + # wipe the parent routine array + mem[NumScriptParents] = 0 + for i in xrange(num_possible_parents * len_parent): + mem[ScriptParents + i] = 0 + else: + raise Exception("a script is already running, use force=True") + + if wram: + bank = 0 + elif not bank: + bank = calculate_bank(address) + address = address % 0x4000 + 0x4000 * bool(bank) + + mem[ScriptFlags] |= 4 + mem[ScriptMode] = 1 + mem[ScriptRunning] = 0xff + + mem[ScriptBank] = bank + mem[ScriptPos] = address % 0x100 + mem[ScriptPos+1] = address / 0x100 + + self.vba.memory = mem + def text_wait(self, step_size=1, max_wait=200, sfx_limit=0, debug=False, callback=None): """ Presses the "A" button when text is done being drawn to screen. -- cgit v1.2.3 From c76156f3f0d48a6eed324c7f99e20a365f2da416 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 16:29:55 -0600 Subject: method to lower enemy hp during battle --- pokemontools/vba/vba.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 42f5e5b..a3c8b7b 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -495,9 +495,12 @@ class crystal(object): self.vba.write_memory_at(0xC2FC, 0) self.vba.write_memory_at(0xC2FD, 0) - #@staticmethod - #def set_enemy_level(level): - # vba.write_memory_at(0xd213, level) + def lower_enemy_hp(self): + """ + Dramatically lower the enemy's HP. + """ + self.vba.write_memory_at(0xd216, 0) + self.vba.write_memory_at(0xd217, 1) def nstep(self, steplimit=500): """ -- cgit v1.2.3 From 4fab1088d9718ef94bb72caa3463a0648aff848a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 16:32:16 -0600 Subject: function to start a battle by rocksmash 01:04 < padz> u cunt --- pokemontools/vba/vba.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index a3c8b7b..60cd0ab 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -884,26 +884,13 @@ class crystal(object): self.call(calculate_address(givepoke_address), bank=calculate_bank(givepoke_address)) - def broken_start_random_battle_by_rocksmash_battle_script(self): + def start_random_battle_by_rocksmash_battle_script(self): """ - This doesn't start a battle. + Initiates a wild battle using the same function that using rocksmash + would call. """ - CallScript_address = 0x261f RockSmashBattleScript_address = 0x97cf9 - ScriptRunning = 0xd438 - ScriptBank = 0xd439 - ScriptPos = 0xd43a - - memory = self.vba.memory - memory[ScriptBank] = calculate_bank(RockSmashBattleScript_address) - memory[ScriptPos] = (calculate_address(RockSmashBattleScript_address) & 0xff00) >> 8 - memory[ScriptPos+1] = calculate_address(RockSmashBattleScript_address) & 0xff - memory[ScriptRunning] = 0xff - self.vba.memory = memory - - self.vba.registers["af"] = (calculate_bank(RockSmashBattleScript_address) << 8) | (self.vba.registers.af & 0xff) - self.vba.registers["hl"] = calculate_address(RockSmashBattleScript_address) - self.call(calculate_address(CallScript_address), bank=calculate_bank(CallScript_address)) + self.call_script(RockSmashBattleScript_address) #def attempt_start_battle_by_startbattle(self): # StartBattle_address = 0x3f4c1 -- cgit v1.2.3 From bc30cd1e9a95342cbb77c1a05a2656f2505e817a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 17:38:55 -0600 Subject: an attempt at givepoke --- pokemontools/vba/vba.py | 66 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 60cd0ab..62c16da 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -871,18 +871,70 @@ class crystal(object): self.vba.memory = memory - def attempt_call_givepoke(self): + def givepoke(self, pokemon_id, level, nickname): """ - An attempt at calling the givepoke command directly. + Give the player a pokemon. """ - givepoke_address = 0x97932 + if isinstance(nickname, str): + if len(nickname) == 0: + raise Exception("invalid nickname") + elif len(nickname) > 11: + raise Exception("nickname too long") + else: + if not nickname: + nickname = False + else: + raise Exception("nickname needs to be a string, False or None") + + # script to inject into wram + script = [ + 0x47, # loadfont + #0x55, # keeptextopen + + # givepoke pokemon_id, level, 0, 0 + 0x2d, pokemon_id, level, 0, 0, + + #0x54, # closetext + 0x49, # loadmovesprites + 0x91, # end + ] + + #address = 0xd073 + #address = 0xc000 + address = 0xd8f1 + + mem = list(self.vba.memory) + backup_wram = mem[address : address + len(script)] + mem[address : address + len(script)] = script + self.vba.memory = mem + + self.call_script(address, wram=True) + + # "would you like to give it a nickname?" + self.text_wait() - # 0, 50, 0, 0 - givepoke_data_address = 0x6ca5 + if nickname: + # yes + self.vba.press("a", hold=10) + + # wait for the keyboard to appear + # TODO: this wait should be moved into write() + self.vba.step(count=20) - self.set_script(givepoke_data_address) + # type the requested nicknameb + self.write(nickname) - self.call(calculate_address(givepoke_address), bank=calculate_bank(givepoke_address)) + self.vba.press("start", hold=5, after=10) + self.vba.press("a", hold=5, after=50) + else: + # no nickname + self.vba.press("d", hold=10, after=20) + self.vba.press("a", hold=5, after=30) + + # reset whatever was in wram before this script was called + mem = list(self.vba.memory) + mem[address : address + len(script)] = backup_wram + self.vba.memory = mem def start_random_battle_by_rocksmash_battle_script(self): """ -- cgit v1.2.3 From d1da18652d96212c05be10a9dfb22bc02071f84e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 17:53:06 -0600 Subject: make givepoke work (h/t padz) --- pokemontools/vba/vba.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 62c16da..9d2be23 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -871,7 +871,7 @@ class crystal(object): self.vba.memory = memory - def givepoke(self, pokemon_id, level, nickname): + def givepoke(self, pokemon_id, level, nickname=None): """ Give the player a pokemon. """ @@ -899,9 +899,12 @@ class crystal(object): 0x91, # end ] + # picked this region of wram because it looks like it's probably unused + # in situations where givepoke will work. #address = 0xd073 #address = 0xc000 - address = 0xd8f1 + #address = 0xd8f1 + address = 0xd280 mem = list(self.vba.memory) backup_wram = mem[address : address + len(script)] @@ -928,8 +931,11 @@ class crystal(object): self.vba.press("a", hold=5, after=50) else: # no nickname - self.vba.press("d", hold=10, after=20) - self.vba.press("a", hold=5, after=30) + self.vba.press("b", hold=10, after=20) + + # Wait for the script to end in the engine before copying the original + # wram values back in. + self.vba.step(count=100) # reset whatever was in wram before this script was called mem = list(self.vba.memory) -- cgit v1.2.3 From 9310699565243442a8c363012b8bb09f000ddf29 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 19:49:39 -0600 Subject: write script to ROM and execute This handles all of the usual tasks that will be required for injecting and running custom scripts. --- pokemontools/vba/vba.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 9d2be23..0d678da 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -838,7 +838,7 @@ class crystal(object): self.text_wait() loop_limit -= 1 - def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): + def broken_start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): """ This will fail after the first mon is defeated. """ @@ -871,6 +871,86 @@ class crystal(object): self.vba.memory = memory + def inject_script_into_rom(self, asm=[0x91], rom_address=0x75 * 0x4000, wram_address=0xd280, limit=50): + """ + Writes a script to the ROM in a blank location. Calls call_script to + make the game engine aware of the script. Then executes the script and + looks for confirmation thta the script has started to run. + + The script must end itself. + + :param asm: scripting command bytes + :param rom_address: rom location to write asm to + :param wram_address: temporary storage for indicating if the script has + started yet + :param limit: number of frames to emulate before giving up on the start + script + """ + execution_pending = 0 + execution_started = 1 + valid_execution_states = (execution_pending, execution_started) + + # location for byte for whether script has started executing + execution_indicator_address = wram_address + + # backup whatever exists at the current wram location + backup_wram = self.vba.read_memory_at(execution_indicator_address) + + # .. and set it to "pending" + self.vba.write_memory_at(execution_indicator_address, execution_pending) + + # initial script that runs first to tell python that execution started + execution_indicator_script = [ + # writebyte to say that the script has executed + 0x15, execution_started, + + # copyvartobyte + 0x1a, execution_indicator_address & 0xff, execution_indicator_address >> 8, + ] + + # make the indicator script run before the user script + full_script = execution_indicator_script + asm + + # inject the asm + rom = list(self.vba.rom) + rom[rom_address : rom_address + len(full_script)] = full_script + + # set the rom with the injected bytes + self.vba.rom = rom + + # setup the script for execution + self.call_script(rom_address) + + status = execution_pending + while status != execution_started and limit > 0: + # emulator time travel + self.vba.step(count=1) + + # get latest wram + status = self.vba.read_memory_at(execution_indicator_address) + if status not in valid_execution_states: + raise Exception( + "The execution indicator at {addr} has invalid state {value}".format( + addr=hex(execution_indicator_address), + value=status, + ) + ) + elif status == execution_started: + break # hooray + + limit -= 1 + + if status == execution_pending and limit == 0: + raise Exception( + "Emulation timeout while waiting for script to start." + ) + + # The script has started so it's okay to reset wram back to whatever it + # was. + self.vba.write_memory_at(execution_indicator_address, backup_wram) + + return True + def givepoke(self, pokemon_id, level, nickname=None): """ Give the player a pokemon. -- cgit v1.2.3 From 9bf214f963113a40b2a6e3037e06c71abae2e69a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 19:56:03 -0600 Subject: simplify givepoke by using inject_script_into_rom --- pokemontools/vba/vba.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 0d678da..9376c6a 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -951,7 +951,7 @@ class crystal(object): return True - def givepoke(self, pokemon_id, level, nickname=None): + def givepoke(self, pokemon_id, level, nickname=None, wram=False): """ Give the player a pokemon. """ @@ -986,12 +986,15 @@ class crystal(object): #address = 0xd8f1 address = 0xd280 - mem = list(self.vba.memory) - backup_wram = mem[address : address + len(script)] - mem[address : address + len(script)] = script - self.vba.memory = mem + if not wram: + self.inject_script_into_rom(asm=script, wram_address=address) + else: + mem = list(self.vba.memory) + backup_wram = mem[address : address + len(script)] + mem[address : address + len(script)] = script + self.vba.memory = mem - self.call_script(address, wram=True) + self.call_script(address, wram=True) # "would you like to give it a nickname?" self.text_wait() @@ -1013,14 +1016,15 @@ class crystal(object): # no nickname self.vba.press("b", hold=10, after=20) - # Wait for the script to end in the engine before copying the original - # wram values back in. - self.vba.step(count=100) - - # reset whatever was in wram before this script was called - mem = list(self.vba.memory) - mem[address : address + len(script)] = backup_wram - self.vba.memory = mem + if wram: + # Wait for the script to end in the engine before copying the original + # wram values back in. + self.vba.step(count=100) + + # reset whatever was in wram before this script was called + mem = list(self.vba.memory) + mem[address : address + len(script)] = backup_wram + self.vba.memory = mem def start_random_battle_by_rocksmash_battle_script(self): """ -- cgit v1.2.3 From 409eab17163b2a85f07d4fe3ea58ed7e4a39b00b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 22:43:48 -0600 Subject: start_trainer_battle spawns a battle --- pokemontools/vba/vba.py | 102 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 9376c6a..17472f0 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -60,6 +60,22 @@ def translate_chars(charz): result += chars[each] return result +def translate_text(text, chars=chars): + """ + Converts text to the in-game byte coding. + """ + output = [] + for given_char in text: + for (byte, char) in chars.iteritems(): + if char == given_char: + output.append(byte) + break + else: + raise Exception( + "no match for {0}".format(given_char) + ) + return output + class crystal(object): """ Just a simple namespace to store a bunch of functions for Pokémon Crystal. @@ -853,6 +869,85 @@ class crystal(object): self.call(calculate_address(Script_startbattle_address), bank=calculate_bank(Script_startbattle_address,)) + def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1, text_win="YOU WIN", text_address=0xdb90): + """ + Start a trainer battle with the trainer located by trainer_group and + trainer_id. + + :param trainer_group: trainer group id + :param trainer_id: trainer id within the group + :param text_win: text to show if player wins + :param text_address: where to store text_win in wram + """ + # where the script will be written + rom_address = 0x75 * 0x4000 + + # battle win message + translated_text = translate_text(text_win) + + # also include the first and last bytes needed for text + translated_text = [0] + translated_text + [0x57] + + mem = self.vba.memory + + # create a backup of the current data + wram_backup = mem[text_address : text_address + len(translated_text)] + + # manipulate the memory + mem[text_address : text_address + len(translated_text)] = translated_text + self.vba.memory = mem + + text_pointer_hi = text_address / 0x100 + text_pointer_lo = text_address % 0x100 + + script = [ + # loadfont + #0x47, + + # winlosstext address, address + 0x64, text_pointer_lo, text_pointer_hi, 0, 0, + + # loadtrainer group, id + 0x5e, trainer_group, trainer_id, + + # startbattle + 0x5f, + + # returnafterbattle + 0x60, + + # reloadmapmusic + 0x83, + + # reloadmap + 0x7B, + ] + + # Now make the script restore wram at the end (after the text has been + # used). The assumption here is that this particular subset of wram + # data would not be needed during the bulk of the script. + address = text_address + for byte in wram_backup: + address_hi = address / 0x100 + address_lo = address % 0x100 + + script += [ + # loadvar + 0x1b, address_lo, address_hi, byte, + ] + + address += 1 + + script += [ + # end + 0x91, + ] + + # Use a different wram address because the default is something related + # to trainers. + # use a higher loop limit because otherwise it doesn't start fast enough? + self.inject_script_into_rom(asm=script, rom_address=rom_address, wram_address=0xdb75, limit=1000) + def set_script(self, address): """ Sets the current script in wram to whatever address. @@ -901,11 +996,8 @@ class crystal(object): # initial script that runs first to tell python that execution started execution_indicator_script = [ - # writebyte to say that the script has executed - 0x15, execution_started, - - # copyvartobyte - 0x1a, execution_indicator_address & 0xff, execution_indicator_address >> 8, + # loadvar address, value + 0x1b, execution_indicator_address & 0xff, execution_indicator_address >> 8, execution_started, ] # make the indicator script run before the user script -- cgit v1.2.3 From 0729ed3554f3d79bd59b626c82ccedb88b063860 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 22:45:12 -0600 Subject: remove a dead function --- pokemontools/vba/vba.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 17472f0..a564ac2 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -832,6 +832,8 @@ class crystal(object): coordinates, pressing the direction button for a full walk step (which ideally should be blocked, this is mainly to establish direction), and then pressing "a" to initiate the trainer battle. + + Consider using start_trainer_battle instead. """ self.warp(map_group, map_id, x, y) @@ -854,21 +856,6 @@ class crystal(object): self.text_wait() loop_limit -= 1 - def broken_start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1): - """ - This will fail after the first mon is defeated. - """ - Script_startbattle_address = 0x97436 - - # setup the battle - memory = self.vba.memory - memory[0xd459] = 0x81 - memory[0xd22f] = trainer_group - memory[0xd231] = trainer_id - self.vba.memory = memory - - self.call(calculate_address(Script_startbattle_address), bank=calculate_bank(Script_startbattle_address,)) - def start_trainer_battle(self, trainer_group=0x1, trainer_id=0x1, text_win="YOU WIN", text_address=0xdb90): """ Start a trainer battle with the trainer located by trainer_group and -- cgit v1.2.3 From 44ef6852f4d24f075ae58373d329e681151ccb62 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 22:49:19 -0600 Subject: remove old functions --- pokemontools/vba/vba.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index a564ac2..3e3d66d 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -1113,15 +1113,12 @@ class crystal(object): RockSmashBattleScript_address = 0x97cf9 self.call_script(RockSmashBattleScript_address) - #def attempt_start_battle_by_startbattle(self): - # StartBattle_address = 0x3f4c1 - # self.call(calculate_address(StartBattle_address), bank=calculate_bank(StartBattle_address)) - #def attempt_start_random_battle_by_wild_battle(self): # start_wild_battle = 0x3f4dd # #self.call(start_wild_battle) # #self.vba.registers["pc"] = ... + # why is this here? def old_crap(self): CallScript_address = 0x261f RockSmashBattleScript_address = 0x97cf9 @@ -1132,14 +1129,3 @@ class crystal(object): ScriptPos = 0xd43a start_wild_battle = 0x3f4dd script = 0x1a1dc6 - - #self.call(StartBattle_address) - #self.call(RockSmashEncounter_address) - - #self.push_stack([self.registers.pc]) - #self.set_script(script) - #memory[ScriptRunning] = 0xff - - #self.call(start_wild_battle) - - #self.vba.memory = memory -- cgit v1.2.3 From b94aaa38829dc96804ca35b01d1db92fe77f914b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 23:05:31 -0600 Subject: remove more dead code --- pokemontools/vba/vba.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 3e3d66d..0dac63f 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -1112,20 +1112,3 @@ class crystal(object): """ RockSmashBattleScript_address = 0x97cf9 self.call_script(RockSmashBattleScript_address) - - #def attempt_start_random_battle_by_wild_battle(self): - # start_wild_battle = 0x3f4dd - # #self.call(start_wild_battle) - # #self.vba.registers["pc"] = ... - - # why is this here? - def old_crap(self): - CallScript_address = 0x261f - RockSmashBattleScript_address = 0x97cf9 - RockSmashEncounter_address = 0x97cc0 - StartBattle_address = 0x3f4c1 - ScriptRunning = 0xd438 - ScriptBank = 0xd439 - ScriptPos = 0xd43a - start_wild_battle = 0x3f4dd - script = 0x1a1dc6 -- cgit v1.2.3 From 9249ffd2d01ec38ace0caf8d5b3d0f0a7cad23f2 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 10 Nov 2013 23:14:42 -0600 Subject: bootstrap the battle tests with more mons Use the givepoke function to add a few more pokemon to the team before starting the battle-related tests. There are some features of the battle handling code that require more than one pokemon to be present in the party. --- tests/bootstrapping.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/bootstrapping.py b/tests/bootstrapping.py index 17a2945..986a20b 100644 --- a/tests/bootstrapping.py +++ b/tests/bootstrapping.py @@ -45,6 +45,10 @@ def bootstrap_trainer_battle(): # 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") + # TODO: figure out a better way to start a trainer battle :( runner.cry.start_trainer_battle_lamely() -- cgit v1.2.3 From 1d9ecfa00f91d602073b56a64f9aa129b95a8c2e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 00:04:32 -0600 Subject: switch tests to use new battle starter --- tests/bootstrapping.py | 3 +-- tests/test_vba_battle.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bootstrapping.py b/tests/bootstrapping.py index 986a20b..b71c19a 100644 --- a/tests/bootstrapping.py +++ b/tests/bootstrapping.py @@ -49,7 +49,6 @@ def bootstrap_trainer_battle(): cry.givepoke(224, 60, "OcTiLlErY") cry.givepoke(126, 87, "magmar") - # TODO: figure out a better way to start a trainer battle :( - runner.cry.start_trainer_battle_lamely() + cry.start_trainer_battle() return runner.cry.vba.state diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index be62fb5..e86b53c 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -40,6 +40,7 @@ class BattleTests(unittest.TestCase): # 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()) -- cgit v1.2.3 From 966985411f01b799fa71f4823da7a8cd6d9cc47b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 00:55:54 -0600 Subject: detect the "mandatory switch" menu This requires a slightly slower text_wait function. There is probably a way to refactor that function in a way that doesn't cause cancer. --- pokemontools/vba/battle.py | 6 ++++-- pokemontools/vba/vba.py | 11 ++++++++--- tests/test_vba_battle.py | 19 +++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 824f27a..87cd7b1 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -67,8 +67,10 @@ class Battle(EmulatorController): # 1) current pokemon hp is 0 # 2) game is polling for input - # TODO: detect the mandatory switch menu - return False + if "CANCEL Which ?" in self.emulator.get_text(): + return True + else: + return False def skip_start_text(self, max_loops=20): """ diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 0dac63f..10513c6 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -431,12 +431,17 @@ class crystal(object): # set CurSFX self.vba.write_memory_at(0xc2bf, 0) - self.vba.press("a", hold=10, after=1) + self.vba.press("a", hold=10, after=50) # check if CurSFX is SFX_READ_TEXT_2 if self.vba.read_memory_at(0xc2bf) == 0x8: - print "cursfx is set to SFX_READ_TEXT_2, looping.." - return self.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) + if "CANCEL Which" in self.get_text(): + print "probably the 'switch pokemon' menu" + return + else: + print "cursfx is set to SFX_READ_TEXT_2, looping.." + print self.get_text() + return self.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) else: if sfx_limit > 0: sfx_limit = sfx_limit - 1 diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index e86b53c..61c0297 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -56,6 +56,25 @@ class BattleTests(unittest.TestCase): # should not be asking for a switch so soon in the battle self.assertFalse(self.battle.is_mandatory_switch()) + def test_is_mandatory_switch(self): + self.battle.skip_start_text() + self.battle.skip_until_input_required() + + # press "FIGHT" + self.vba.press(["a"], after=20) + + # press the first move ("SCRATCH") + self.vba.press(["a"], after=20) + + # set partymon1 hp to very low + self.vba.write_memory_at(0xc63c, 0) + self.vba.write_memory_at(0xc63d, 1) + + # let the enemy attack and kill the pokemon + self.battle.skip_until_input_required() + + self.assertTrue(self.battle.is_mandatory_switch()) + def test_attack_loop(self): self.battle.skip_start_text() self.battle.skip_until_input_required() -- cgit v1.2.3 From fcfde94ba84a6a29bb22dd97b62af2e3c72276bd Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 12:38:17 -0600 Subject: version bump to: v1.5.0 --- pokemontools/__init__.py | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pokemontools/__init__.py b/pokemontools/__init__.py index 293e2f2..1125d4b 100644 --- a/pokemontools/__init__.py +++ b/pokemontools/__init__.py @@ -1,3 +1,5 @@ import configuration as config import crystal import preprocessor + +__version__ = "1.5.0" diff --git a/setup.py b/setup.py index 65c9842..4b28c20 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ requires = [ setup( name="pokemontools", - version="1.4.1", + version="1.5.0", description="Tools for compiling and disassembling Pokémon Red and Pokémon Crystal.", long_description=open("README.md", "r").read(), license="BSD", -- cgit v1.2.3 From fd10868994edf46180a95329f2eaa48df9ccfd7a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 12:39:50 -0600 Subject: version bump to: v1.6.0 --- pokemontools/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/__init__.py b/pokemontools/__init__.py index 1125d4b..dc4346a 100644 --- a/pokemontools/__init__.py +++ b/pokemontools/__init__.py @@ -2,4 +2,4 @@ import configuration as config import crystal import preprocessor -__version__ = "1.5.0" +__version__ = "1.6.0" diff --git a/setup.py b/setup.py index 4b28c20..19d4af9 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ requires = [ setup( name="pokemontools", - version="1.5.0", + 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", -- cgit v1.2.3 From ab1a60ef461a3ca3ecb621f858fde59a9a9fbc64 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 20:40:28 -0600 Subject: README: update introduction to pokemontools The previous version of the README was leftover from when all of the source code was just files inside of extras/ in the pokecrystal repo. --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index 760731b..9c3ca92 100644 --- a/README.md +++ b/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 ============================== -- cgit v1.2.3 From 4c6a904db191c182c3b9cecdcbc0d5ea3b5c2173 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:22:15 -0600 Subject: a quick function to set pokemon hp in battle --- pokemontools/vba/vba.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 10513c6..47cadd7 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -523,6 +523,13 @@ class crystal(object): self.vba.write_memory_at(0xd216, 0) self.vba.write_memory_at(0xd217, 1) + def set_battle_mon_hp(self, hp): + """ + Set the BattleMonHP variable to the given hp. + """ + self.vba.write_memory_at(0xc63c, hp / 0x100) + self.vba.write_memory_at(0xc63c + 1, hp % 0x100) + def nstep(self, steplimit=500): """ Steps the CPU forward and calls some functions in between each step. -- cgit v1.2.3 From 18449eea21096cf8b90e21ee7bcdc6da5e047df4 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:27:28 -0600 Subject: function to check if it's the yes/no prompt This is the prompt that appears during battles for whether or not to switch pokemon when the other trainer is sending something else out. --- pokemontools/vba/battle.py | 7 +++++++ pokemontools/vba/vba.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 87cd7b1..17c0d5d 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -55,6 +55,13 @@ class Battle(EmulatorController): """ return self.is_fight_pack_run_menu() + def is_switch_prompt(self): + """ + Detects if the battle is waiting for the player to choose whether or + not to switch pokemon. + """ + return self.emulator.is_battle_switch_prompt() + def is_mandatory_switch(self): """ Detects if the battle is waiting for the player to choose a next diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 47cadd7..79eb77e 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -599,6 +599,23 @@ class crystal(object): def is_in_link_battle(self): return self.vba.read_memory_at(0xc2dc) != 0 + def is_battle_switch_prompt(self): + """ + Checks if the game is currently displaying the yes/no prompt for + whether or not to switch pokemon. + """ + # get on-screen text + text = self.get_text() + + requirements = [ + "YES", + "NO", + "Will ", + "change POKMON?", + ] + + return all([requirement in text for requirement in requirements]) + def unlock_flypoints(self): """ Unlocks different destinations for flying. -- cgit v1.2.3 From e61643e6814d49c4244c49fa561745dc3fc3e80a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:33:14 -0600 Subject: use set_battle_mon_hp in a test --- tests/test_vba_battle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 61c0297..8b02cef 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -67,8 +67,7 @@ class BattleTests(unittest.TestCase): self.vba.press(["a"], after=20) # set partymon1 hp to very low - self.vba.write_memory_at(0xc63c, 0) - self.vba.write_memory_at(0xc63d, 1) + self.cry.set_battle_mon_hp(1) # let the enemy attack and kill the pokemon self.battle.skip_until_input_required() -- cgit v1.2.3 From 2801a3545ffd4cde947cb6fee212fe3a4f759543 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:48:01 -0600 Subject: update docstrings about the switch prompt --- pokemontools/vba/battle.py | 4 +++- pokemontools/vba/vba.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 17c0d5d..a5d9d52 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -58,7 +58,9 @@ class Battle(EmulatorController): def is_switch_prompt(self): """ Detects if the battle is waiting for the player to choose whether or - not to switch pokemon. + 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_battle_switch_prompt() diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 79eb77e..58ed1bd 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -602,8 +602,12 @@ class crystal(object): def is_battle_switch_prompt(self): """ Checks if the game is currently displaying the yes/no prompt for - whether or not to switch pokemon. + whether or not to switch pokemon. This happens when the trainer is + switching pokemon out. """ + # TODO: this method should return False if the game options have been + # set to not use the battle switching style. + # get on-screen text text = self.get_text() -- cgit v1.2.3 From ba6eb203b4afef6179d30a13f6f4037349d79272 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:48:29 -0600 Subject: a test for detecting the yes/no prompt --- tests/test_vba_battle.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 8b02cef..8299bea 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -88,5 +88,30 @@ class BattleTests(unittest.TestCase): 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() + + import time + time.sleep(1) + + # yes/no menu is present, should be detected + self.assertTrue(self.battle.is_switch_prompt()) + + # but it's not mandatory + self.assertFalse(self.battle.is_mandatory_switch()) + if __name__ == "__main__": unittest.main() -- cgit v1.2.3 From cc4c24816767caaaf1a6c2d280184505565eb688 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:52:12 -0600 Subject: add is_switch_prompt to the input poll detector --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index a5d9d52..a453478 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -38,7 +38,7 @@ class Battle(EmulatorController): """ Detects if the battle is waiting for player input. """ - return self.is_player_turn() or self.is_mandatory_switch() + return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() def is_fight_pack_run_menu(self): """ -- cgit v1.2.3 From 993eaa34517c271f3e076744f96aa479008fdf16 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:52:33 -0600 Subject: fix the is_switch_prompt test --- tests/test_vba_battle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 8299bea..a0338e5 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -104,12 +104,12 @@ class BattleTests(unittest.TestCase): # attack the enemy and kill it self.battle.skip_until_input_required() - import time - time.sleep(1) - # yes/no menu is present, should be detected self.assertTrue(self.battle.is_switch_prompt()) + # and input should be required + self.assertTrue(self.is_input_required()) + # but it's not mandatory self.assertFalse(self.battle.is_mandatory_switch()) -- cgit v1.2.3 From 9f72be1a0f4186ca49e3bf9c6148afbe6477e391 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:54:06 -0600 Subject: handle the yes/no prompt in the battle run loop --- pokemontools/vba/battle.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index a453478..8f4e5eb 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -143,6 +143,8 @@ class Battle(EmulatorController): if self.is_player_turn(): # battle hook provides input to handle this situation self.handle_turn() + elif self.is_switch_prompt(): + self.handle_switch_prompt() elif self.is_mandatory_switch(): # battle hook provides input to handle this situation too self.handle_mandatory_switch() @@ -160,6 +162,13 @@ class Battle(EmulatorController): """ raise NotImplementedError + def handle_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_turn(self): """ Take actions inside of a battle based on the game state. -- cgit v1.2.3 From d9bb01f2c0aef6d270bc4e2da93af94ea210e8db Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:56:34 -0600 Subject: rename the switch prompt detector --- pokemontools/vba/battle.py | 12 ++++++------ pokemontools/vba/vba.py | 2 +- tests/test_vba_battle.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 8f4e5eb..9fe4a84 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -38,7 +38,7 @@ class Battle(EmulatorController): """ Detects if the battle is waiting for player input. """ - return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() + return self.is_player_turn() or self.is_mandatory_switch() or self.is_trainer_switch_prompt() def is_fight_pack_run_menu(self): """ @@ -55,14 +55,14 @@ class Battle(EmulatorController): """ return self.is_fight_pack_run_menu() - def is_switch_prompt(self): + 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_battle_switch_prompt() + return self.emulator.is_trainer_switch_prompt() def is_mandatory_switch(self): """ @@ -143,8 +143,8 @@ class Battle(EmulatorController): if self.is_player_turn(): # battle hook provides input to handle this situation self.handle_turn() - elif self.is_switch_prompt(): - self.handle_switch_prompt() + elif self.is_trainer_switch_prompt(): + self.handle_trainer_switch_prompt() elif self.is_mandatory_switch(): # battle hook provides input to handle this situation too self.handle_mandatory_switch() @@ -162,7 +162,7 @@ class Battle(EmulatorController): """ raise NotImplementedError - def handle_switch_prompt(self): + 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. diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 58ed1bd..67d3cf1 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -599,7 +599,7 @@ class crystal(object): def is_in_link_battle(self): return self.vba.read_memory_at(0xc2dc) != 0 - def is_battle_switch_prompt(self): + def is_trainer_switch_prompt(self): """ Checks if the game is currently displaying the yes/no prompt for whether or not to switch pokemon. This happens when the trainer is diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index a0338e5..0faa26e 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -105,7 +105,7 @@ class BattleTests(unittest.TestCase): self.battle.skip_until_input_required() # yes/no menu is present, should be detected - self.assertTrue(self.battle.is_switch_prompt()) + self.assertTrue(self.battle.is_trainer_switch_prompt()) # and input should be required self.assertTrue(self.is_input_required()) -- cgit v1.2.3 From 2ff3478bfda330fbc379f5278a6d37d7065dd9df Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 21:57:08 -0600 Subject: fix a typo in the switch prompt test --- tests/test_vba_battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_vba_battle.py b/tests/test_vba_battle.py index 0faa26e..c6debc3 100644 --- a/tests/test_vba_battle.py +++ b/tests/test_vba_battle.py @@ -108,7 +108,7 @@ class BattleTests(unittest.TestCase): self.assertTrue(self.battle.is_trainer_switch_prompt()) # and input should be required - self.assertTrue(self.is_input_required()) + self.assertTrue(self.battle.is_input_required()) # but it's not mandatory self.assertFalse(self.battle.is_mandatory_switch()) -- cgit v1.2.3 From d6cf3454c44fab60c6f83bc8fa3109f0c793fd2f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 22:21:23 -0600 Subject: prepare for a wild-based detector This one isn't implemented yet but might as well get the wrapper functions out of the way. --- pokemontools/vba/battle.py | 15 +++++++++++++++ pokemontools/vba/vba.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 9fe4a84..6b52bce 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -64,6 +64,21 @@ class Battle(EmulatorController): """ 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 diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 67d3cf1..6f863b6 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -620,6 +620,21 @@ class crystal(object): return all([requirement in text for requirement in requirements]) + 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. + """ + # TODO: make a better implementation + return False + + def is_switch_prompt(self): + """ + Detects both the trainer-style switch prompt and the wild-style switch + prompt. This is the yes/no prompt for whether to switch pokemon. + """ + return self.is_trainer_switch_prompt() or self.is_wild_switch_prompt() + def unlock_flypoints(self): """ Unlocks different destinations for flying. -- cgit v1.2.3 From 4087851f7c9c92791182ffa695073827c5433829 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 22:39:37 -0600 Subject: wild switch prompt detector --- pokemontools/vba/vba.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 6f863b6..17d3be6 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -625,8 +625,16 @@ class crystal(object): Detects if the battle is waiting for the player to choose whether or not to continue to fight the wild pokemon. """ - # TODO: make a better implementation - return False + # get on-screen text + screen_text = self.get_text() + + requirements = [ + "YES", + "NO", + "Use next POKMON?", + ] + + return all([requirement in text for requirement in requirements]) def is_switch_prompt(self): """ -- cgit v1.2.3 From fd5544f79094ddddce59754de7bbe3738622e8fe Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 23:04:31 -0600 Subject: handle wild yes/no prompt during battle --- pokemontools/vba/battle.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 6b52bce..c0c886e 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -160,6 +160,8 @@ class Battle(EmulatorController): 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() @@ -167,6 +169,7 @@ class Battle(EmulatorController): raise BattleException("unknown state, aborting") # "how did i lose? wah" + # TODO: this doesn't happen for wild battles self.skip_end_text() # TODO: return should indicate win/loss (blackout) @@ -184,6 +187,13 @@ class Battle(EmulatorController): """ 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. -- cgit v1.2.3 From 5b35595c7a13792db782f8ebc21dccfcc1b56b9a Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 23:04:44 -0600 Subject: minor function to set battle type This needs to be replaced with something that loads variable names from wram.asm instead of manually repeating everything in python. --- pokemontools/vba/vba.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 17d3be6..e339f57 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -655,6 +655,12 @@ class crystal(object): self.vba.write_memory_at(0xDCA7, 0xFF) self.vba.write_memory_at(0xDCA8, 0xFF) + def set_battle_type(self, battle_type): + """ + Changes the battle type value. + """ + self.vba.write_memory_at(0xd230, battle_type) + def get_gender(self): """ Returns 'male' or 'female'. -- cgit v1.2.3 From 4d5696251ce7feac89cb15d2c875e10e6cc8b831 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Mon, 11 Nov 2013 23:22:09 -0600 Subject: make a class that shows off the handler methods It doesn't do anything except show some of the methods that need to be implemented. --- pokemontools/vba/battle.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index c0c886e..5d05d8c 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -202,35 +202,31 @@ class Battle(EmulatorController): class BattleStrategy(Battle): """ - Throw a pokeball until everyone dies. + This class shows the relevant methods to make a battle handler. """ def handle_mandatory_switch(self): """ Something fainted, pick the next mon. """ - for pokemon in self.emulator.party: - if pokemon.hp > 0: - break - else: - # the game failed to do a blackout.. not sure what to do now. - raise BattleException("No partymons left. wtf?") - - return pokemon.id + raise NotImplementedError - def handle_turn(self): + def handle_trainer_switch_prompt(self): """ - Take actions inside of a battle based on the game state. + The trainer is switching pokemon. The game asks yes/no for whether or + not the player would like to switch. """ - self.throw_pokeball() + raise NotImplementedError -class SimpleBattleStrategy(BattleStrategy): - """ - Attack the enemy with the first move. - """ + 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): """ - Always attack the enemy with the first move. + Take actions inside of a battle based on the game state. """ - self.attack(self.battle.party[0].moves[0].name) + raise NotImplementedError -- cgit v1.2.3 From d193e02a11ea5c4b86d9ac42a473b55e255c67ff Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Tue, 12 Nov 2013 02:11:09 -0600 Subject: select a battle menu option --- pokemontools/vba/battle.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 5d05d8c..b1ede97 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -49,6 +49,56 @@ class Battle(EmulatorController): 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.vba.read_memory_at(0xcfa9) + current_column = self.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.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.vba.press(direction, hold=5, after=10) + + # now select the action + if execute: + self.vba.press(a, hold=5, after=100) + def is_player_turn(self): """ Detects if the battle is waiting for the player to choose an attack. -- cgit v1.2.3 From b44e7b6dee19c9029e68747315e4fc84e741a86b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Tue, 12 Nov 2013 02:11:23 -0600 Subject: incomplete simple battle strategy --- pokemontools/vba/battle.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index b1ede97..b1ac0fe 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -280,3 +280,15 @@ class BattleStrategy(Battle): Take actions inside of a battle based on the game state. """ 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. + """ + pass -- cgit v1.2.3 From 5355c3ce04f8fd5cf3b3ff638c6d0a2e7e993016 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Tue, 12 Nov 2013 11:03:18 -0600 Subject: add todo comment to givepoke --- pokemontools/vba/vba.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 10513c6..02fd99d 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -1073,6 +1073,8 @@ class crystal(object): if not wram: self.inject_script_into_rom(asm=script, wram_address=address) else: + # TODO: move this into a separate function. Maybe use a context + # manager to restore wram at the end. mem = list(self.vba.memory) backup_wram = mem[address : address + len(script)] mem[address : address + len(script)] = script -- cgit v1.2.3 From 4ca83d72bf1b289160796734666966af8bd1612c Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Tue, 12 Nov 2013 21:36:31 -0600 Subject: store pokecrystal wram.asm in the repository --- pokemontools/data/__init__.py | 9 + pokemontools/data/pokecrystal/wram.asm | 2293 ++++++++++++++++++++++++++++++++ 2 files changed, 2302 insertions(+) create mode 100644 pokemontools/data/__init__.py create mode 100644 pokemontools/data/pokecrystal/wram.asm diff --git a/pokemontools/data/__init__.py b/pokemontools/data/__init__.py new file mode 100644 index 0000000..1a0bf7b --- /dev/null +++ b/pokemontools/data/__init__.py @@ -0,0 +1,9 @@ +""" +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__)) diff --git a/pokemontools/data/pokecrystal/wram.asm b/pokemontools/data/pokecrystal/wram.asm new file mode 100644 index 0000000..3796969 --- /dev/null +++ b/pokemontools/data/pokecrystal/wram.asm @@ -0,0 +1,2293 @@ +SECTION "tiles0",VRAM[$8000],BANK[0] +VTiles0: +SECTION "tiles1",VRAM[$8800],BANK[0] +VTiles1: +SECTION "tiles2",VRAM[$9000],BANK[0] +VTiles2: +SECTION "bgmap0",VRAM[$9800],BANK[0] +VBGMap0: +SECTION "bgmap1",VRAM[$9C00],BANK[0] +VBGMap1: + + + +SECTION "WRAMBank0",WRAM0[$c000] + +SECTION "stack",WRAM0[$c0ff] +Stack: ; c0ff + ds -$100 + + +SECTION "audio",WRAM0[$c100] +MusicPlaying: ; c100 +; nonzero if playing + ds 1 + +Channels: +Channel1: +Channel1MusicID: ; c101 + ds 2 +Channel1MusicBank: ; c103 + ds 1 +Channel1Flags: ; c104 +; 0: on/off +; 1: subroutine +; 2: +; 3: +; 4: noise sampling on/off +; 5: +; 6: +; 7: + ds 1 +Channel1Flags2: ; c105 +; 0: vibrato on/off +; 1: +; 2: duty cycle on/off +; 3: +; 4: +; 5: +; 6: +; 7: + ds 1 +Channel1Flags3: ; c106 +; 0: vibrato up/down +; 1: +; 2: +; 3: +; 4: +; 5: +; 6: +; 7: + ds 1 +Channel1MusicAddress: ; c107 + ds 2 +Channel1LastMusicAddress: ; c109 + ds 2 +; could have been meant as a third-level address + ds 2 +Channel1NoteFlags: ; c10d +; 0: +; 1: +; 2: +; 3: +; 4: +; 5: rest +; 6: +; 7: + ds 1 +Channel1Condition: ; c10e +; used for conditional jumps + ds 1 +Channel1DutyCycle: ; c10f +; uses top 2 bits only +; 0: 12.5% +; 1: 25% +; 2: 50% +; 3: 75% + ds 1 +Channel1Intensity: ; c110 +; hi: pressure +; lo: velocity + ds 1 +Channel1Frequency: +; 11 bits +Channel1FrequencyLo: ; c111 + ds 1 +Channel1FrequencyHi: ; c112 + ds 1 +Channel1Pitch: ; c113 +; 0: rest +; 1: C +; 2: C# +; 3: D +; 4: D# +; 5: E +; 6: F +; 7: F# +; 8: G +; 9: G# +; a: A +; b: A# +; c: B + ds 1 +Channel1Octave: ; c114 +; 0: highest +; 7: lowest + ds 1 +Channel1StartingOctave: ; c115 +; raises existing octaves by this value +; used for repeating phrases in a higher octave to save space + ds 1 +Channel1NoteDuration: ; c116 +; number of frames remaining in the current note + ds 1 +; c117 + ds 1 +; c118 + ds 1 +Channel1LoopCount: ; c119 + ds 1 +Channel1Tempo: ; c11a + ds 2 +Channel1Tracks: ; c11c +; hi: l +; lo: r + ds 1 +; c11d + ds 1 + +Channel1VibratoDelayCount: ; c11e +; initialized at the value in VibratoDelay +; decrements each frame +; at 0, vibrato starts + ds 1 +Channel1VibratoDelay: ; c11f +; number of frames a note plays until vibrato starts + ds 1 +Channel1VibratoExtent: ; c120 +; difference in + ds 1 +Channel1VibratoRate: ; c121 +; counts down from a max of 15 frames +; over which the pitch is alternated +; hi: init frames +; lo: frame count + ds 1 + +; c122 + ds 1 +; c123 + ds 1 +; c124 + ds 1 +; c125 + ds 1 +; c126 + ds 1 +; c127 + ds 1 +Channel1CryPitch: ; c128 + ds 1 +Channel1CryEcho: ; c129 + ds 1 + ds 4 +Channel1NoteLength: ; c12e +; # frames per 16th note + ds 1 +; c12f + ds 1 +; c130 + ds 1 +; c131 + ds 1 +; c132 + ds 1 +; end + +Channel2: ; c133 + ds 50 +Channel3: ; c165 + ds 50 +Channel4: ; c197 + ds 50 + +SFXChannels: +Channel5: ; c1c9 + ds 50 +Channel6: ; c1fb + ds 50 +Channel7: ; c22d + ds 50 +Channel8: ; c25f + ds 50 + +; c291 + ds 1 +; c292 + ds 1 +; c293 + ds 1 +; c294 + ds 1 +; c295 + ds 1 +; c296 + ds 1 +; c297 + ds 1 + +CurMusicByte: ; c298 + ds 1 +CurChannel: ; c299 + ds 1 +Volume: ; c29a +; corresponds to $ff24 +; Channel control / ON-OFF / Volume (R/W) +; bit 7 - Vin->SO2 ON/OFF +; bit 6-4 - SO2 output level (volume) (# 0-7) +; bit 3 - Vin->SO1 ON/OFF +; bit 2-0 - SO1 output level (volume) (# 0-7) + ds 1 +SoundOutput: ; c29b +; corresponds to $ff25 +; bit 4-7: ch1-4 so2 on/off +; bit 0-3: ch1-4 so1 on/off + ds 1 +SoundInput: ; c29c +; corresponds to $ff26 +; bit 7: global on/off +; bit 0: ch1 on/off +; bit 1: ch2 on/off +; bit 2: ch3 on/off +; bit 3: ch4 on/off + ds 1 + +MusicID: +MusicIDLo: ; c29d + ds 1 +MusicIDHi: ; c29e + ds 1 +MusicBank: ; c29f + ds 1 +NoiseSampleAddress: +NoiseSampleAddressLo: ; c2a0 + ds 1 +NoiseSampleAddressHi: ; c2a1 + ds 1 +; noise delay? ; c2a2 + ds 1 +; c2a3 + ds 1 +MusicNoiseSampleSet: ; c2a4 + ds 1 +SFXNoiseSampleSet: ; c2a5 + ds 1 +Danger: ; c2a6 +; bit 7: on/off +; bit 4: pitch +; bit 0-3: counter + ds 1 +MusicFade: ; c2a7 +; fades volume over x frames +; bit 7: fade in/out +; bit 0-5: number of frames for each volume level +; $00 = none (default) + ds 1 +MusicFadeCount: ; c2a8 + ds 1 +MusicFadeID: +MusicFadeIDLo: ; c2a9 + ds 1 +MusicFadeIDHi: ; c2aa + ds 1 + ds 5 +CryPitch: ; c2b0 + ds 1 +CryEcho: ; c2b1 + ds 1 +CryLength: ; c2b2 + ds 2 +LastVolume: ; c2b4 + ds 1 + ds 1 +SFXPriority: ; c2b6 +; if nonzero, turn off music when playing sfx + ds 1 + ds 6 +CryTracks: ; c2bd +; plays only in left or right track depending on what side the monster is on +; both tracks active outside of battle + ds 1 + ds 1 +CurSFX: ; c2bf +; id of sfx currently playing + ds 1 +CurMusic: ; c2c0 +; id of music currently playing + ds 1 + +SECTION "auto",WRAM0[$c2c7] +InputType: ; c2c7 + ds 1 +AutoInputAddress: ; c2c8 + ds 2 +AutoInputBank: ; c2ca + ds 1 +AutoInputLength: ; c2cb + ds 1 + +SECTION "linkbattle",WRAM0[$c2dc] +InLinkBattle: ; c2dc +; 0 not in link battle +; 1 link battle +; 4 mobile battle + ds 1 + +SECTION "scriptengine",WRAM0[$c2dd] +ScriptVar: ; c2dd + ds 1 + + +SECTION "tiles",WRAM0[$c2fa] +TileDown: ; c2fa + ds 1 +TileUp: ; c2fb + ds 1 +TileLeft: ; c2fc + ds 1 +TileRight: ; c2fd + ds 1 + +TilePermissions: ; c2fe +; set if tile behavior prevents +; you from walking in that direction +; bit 3: down +; bit 2: up +; bit 1: left +; bit 0: right + ds 1 + +SECTION "icons",WRAM0[$c3b6] + +CurIcon: ; c3b6 + ds 1 + +SECTION "gfx",WRAM0[$c400] + +Sprites: ; c400 +; 4 bytes per sprite +; 40 sprites +; struct: +; y in pixels +; x in pixels +; tile id +; attributes: +; bit 7: priority +; bit 6: y flip +; bit 5: x flip +; bit 4: pal # (non-cgb) +; bit 3: vram bank (cgb only) +; bit 2-0: pal # (cgb only) + ds 160 +SpritesEnd: + +TileMap: ; c4a0 +; 20x18 grid of 8x8 tiles + ds 360 + + +SECTION "BattleMons",WRAM0[$c608] + +EnemyMoveStruct: +EnemyMoveAnimation: ; c608 + ds 1 +EnemyMoveEffect: ; c609 + ds 1 +EnemyMovePower: ; c60a + ds 1 +EnemyMoveType: ; c60b + ds 1 +EnemyMoveAccuracy: ; c60c + ds 1 +EnemyMovePP: ; c60d + ds 1 +EnemyMoveEffectChance: ; c60e + ds 1 + +PlayerMoveStruct: +PlayerMoveAnimation: ; c60f + ds 1 +PlayerMoveEffect: ; c610 + ds 1 +PlayerMovePower: ; c611 + ds 1 +PlayerMoveType: ; c612 + ds 1 +PlayerMoveAccuracy: ; c613 + ds 1 +PlayerMovePP: ; c614 + ds 1 +PlayerMoveEffectChance: ; c615 + ds 1 + +EnemyMonNick: ; c616 + ds 11 +BattleMonNick: ; c621 + ds 11 + + +BattleMonSpecies: ; c62c + ds 1 +BattleMonItem: ; c62d + ds 1 + +BattleMonMoves: +BattleMonMove1: ; c62e + ds 1 +BattleMonMove2: ; c62f + ds 1 +BattleMonMove3: ; c630 + ds 1 +BattleMonMove4: ; c631 + ds 1 + +BattleMonDVs: +BattleMonAtkDefDV: ; c632 + ds 1 +BattleMonSpdSpclDV: ; c633 + ds 1 + +BattleMonPP: +BattleMonPPMove1: ; c634 + ds 1 +BattleMonPPMove2: ; c635 + ds 1 +BattleMonPPMove3: ; c636 + ds 1 +BattleMonPPMove4: ; c637 + ds 1 + +BattleMonHappiness: ; c638 + ds 1 +BattleMonLevel: ; c639 + ds 1 + +BattleMonStatus: ; c63a + ds 2 + +BattleMonHP: ; c63c + ds 2 +BattleMonMaxHP: ; c63e + ds 2 + +BattleMonAtk: ; c640 + ds 2 +BattleMonDef: ; c642 + ds 2 +BattleMonSpd: ; c644 + ds 2 +BattleMonSpclAtk: ; c646 + ds 2 +BattleMonSpclDef: ; c648 + ds 2 + +BattleMonType1: ; c64a + ds 1 +BattleMonType2: ; c64b + ds 1 + + ds 10 + +OTName: ; c656 + ds 13 + +CurOTMon: ; c663 + ds 1 + + ds 1 + +TypeModifier: ; c665 +; >10: super-effective +; 10: normal +; <10: not very effective + +; bit 7: stab + ds 1 + +CriticalHit: ; c666 +; nonzero for a critical hit + ds 1 + +AttackMissed: ; c667 +; nonzero for a miss + ds 1 + +PlayerSubStatus1: ; c668 +; bit +; 7 attract +; 6 encore +; 5 endure +; 4 perish song +; 3 identified +; 2 protect +; 1 curse +; 0 nightmare + ds 1 +PlayerSubStatus2: ; c669 +; bit +; 7 +; 6 +; 5 +; 4 +; 3 +; 2 +; 1 +; 0 curled + ds 1 +PlayerSubStatus3: ; c66a +; bit +; 7 confused +; 6 flying +; 5 underground +; 4 charged +; 3 flinch +; 2 +; 1 rollout +; 0 bide + ds 1 +PlayerSubStatus4: ; c66b +; bit +; 7 leech seed +; 6 rage +; 5 recharge +; 4 substitute +; 3 +; 2 focus energy +; 1 mist +; 0 bide: unleashed energy + ds 1 +PlayerSubStatus5: ; c66c +; bit +; 7 cant run +; 6 destiny bond +; 5 lock-on +; 4 +; 3 +; 2 +; 1 +; 0 toxic + ds 1 + +EnemySubStatus1: ; c66d +; see PlayerSubStatus1 + ds 1 +EnemySubStatus2: ; c66e +; see PlayerSubStatus2 + ds 1 +EnemySubStatus3: ; c66f +; see PlayerSubStatus3 + ds 1 +EnemySubStatus4: ; c670 +; see PlayerSubStatus4 + ds 1 +EnemySubStatus5: ; c671 +; see PlayerSubStatus5 + ds 1 + +PlayerRolloutCount: ; c672 + ds 1 +PlayerConfuseCount: ; c673 + ds 1 + ds 1 +PlayerDisableCount: ; c675 + ds 1 +PlayerEncoreCount: ; c676 + ds 1 +PlayerPerishCount: ; c677 + ds 1 +PlayerFuryCutterCount: ; c678 + ds 1 +PlayerProtectCount: ; c679 + ds 1 + +EnemyRolloutCount: ; c67a + ds 1 +EnemyConfuseCount: ; c67b + ds 1 + ds 1 +EnemyDisableCount: ; c67d + ds 1 +EnemyEncoreCount: ; c67e + ds 1 +EnemyPerishCount: ; c67f + ds 1 +EnemyFuryCutterCount: ; c680 + ds 1 +EnemyProtectCount: ; c681 + ds 1 + +PlayerDamageTaken: ; c682 + ds 2 +EnemyDamageTaken: ; c684 + ds 2 + + ds 3 + + ds 1 + +BattleScriptBuffer: ; c68a + ds 40 + +BattleScriptBufferLoc: ; c6b2 + ds 2 + + ds 2 + +PlayerStats: ; c6b6 + ds 10 + ds 1 +EnemyStats: ; c6c1 + ds 10 + ds 1 + +PlayerStatLevels: ; c6cc +; 07 neutral +PlayerAtkLevel: ; c6cc + ds 1 +PlayerDefLevel: ; c6cd + ds 1 +PlayerSpdLevel: ; c6ce + ds 1 +PlayerSAtkLevel: ; c6cf + ds 1 +PlayerSDefLevel: ; c6d0 + ds 1 +PlayerAccLevel: ; c6d1 + ds 1 +PlayerEvaLevel: ; c6d2 + ds 1 +; c6d3 + ds 1 +PlayerStatLevelsEnd: + +EnemyStatLevels: ; c6d4 +; 07 neutral +EnemyAtkLevel: ; c6d4 + ds 1 +EnemyDefLevel: ; c6d5 + ds 1 +EnemySpdLevel: ; c6d6 + ds 1 +EnemySAtkLevel: ; c6d7 + ds 1 +EnemySDefLevel: ; c6d8 + ds 1 +EnemyAccLevel: ; c6d9 + ds 1 +EnemyEvaLevel: ; c6da + ds 1 +; c6db + ds 1 + +EnemyTurnsTaken: ; c6dc + ds 1 +PlayerTurnsTaken: ; c6dd + ds 1 + + ds 5 + +CurPlayerMove: ; c6e3 + ds 1 +CurEnemyMove: ; c6e4 + ds 1 + +LinkBattleRNCount: ; c6e5 +; how far through the prng stream + ds 1 + + ds 3 + +CurEnemyMoveNum: ; c6e9 + ds 1 + + ds 10 + +AlreadyDisobeyed: ; c6f4 + ds 1 + +DisabledMove: ; c6f5 + ds 1 +EnemyDisabledMove: ; c6f6 + ds 1 + ds 1 + +; exists so you can't counter on switch +LastEnemyCounterMove: ; c6f8 + ds 1 +LastPlayerCounterMove: ; c6f9 + ds 1 + + ds 1 + +AlreadyFailed: ; c6fb + ds 1 + + ds 3 + +PlayerScreens: ; c6ff +; bit +; 4 reflect +; 3 light screen +; 2 safeguard +; 0 spikes + ds 1 + +EnemyScreens: ; c700 +; see PlayerScreens + ds 1 + + ds 1 + +PlayerLightScreenCount: ; c702 + ds 1 +PlayerReflectCount: ; c703 + ds 1 + + ds 2 + +EnemyLightScreenCount: ; c706 + ds 1 +EnemyReflectCount: ; c707 + ds 1 + + ds 2 + +Weather: ; c70a +; 00 normal +; 01 rain +; 02 sun +; 03 sandstorm +; 04 rain stopped +; 05 sunliight faded +; 06 sandstorm subsided + ds 1 + +WeatherCount: ; c70b +; # turns remaining + ds 1 + +LoweredStat: ; c70c + ds 1 +EffectFailed: ; c70d + ds 1 +FailedMessage: ; c70e + ds 1 + + ds 3 + +PlayerUsedMoves: ; c712 +; add a move that has been used once by the player +; added in order of use + ds 4 + + ds 5 + +LastPlayerMove: ; c71b + ds 1 +LastEnemyMove: ; c71c + ds 1 + + +SECTION "battle",WRAM0[$c734] +BattleEnded: ; c734 + ds 1 + + +SECTION "overworldmap",WRAM0[$c800] +OverworldMap: ; c800 + ds 1300 +OverworldMapEnd: + + ds 12 + +SECTION "gfx2",WRAM0[$cd20] +CreditsPos: +BGMapBuffer: ; cd20 + ds 2 +CreditsTimer: ; cd22 + ds 1 + ds 37 + +BGMapPalBuffer: ; cd48 + ds 40 + +BGMapBufferPtrs: ; cd70 +; 20 bg map addresses (16x8 tiles) + ds 40 + +SGBPredef: ; cd98 + ds 1 +PlayerHPPal: ; cd99 + ds 1 +EnemyHPPal: ; cd9a + ds 1 + + ds 62 + +AttrMap: ; cdd9 +; 20x18 grid of palettes for 8x8 tiles +; read horizontally from the top row +; bit 3: vram bank +; bit 0-2: palette id + ds 360 + + ds 30 + +MonType: ; cf5f + ds 1 + +CurSpecies: ; cf60 + ds 1 + + ds 6 + +Requested2bpp: ; cf67 + ds 1 +Requested2bppSource: ; cf68 + ds 2 +Requested2bppDest: ; cf6a + ds 2 + +Requested1bpp: ; cf6c + ds 1 +Requested1bppSource: ; cf6d + ds 2 +Requested1bppDest: ; cf6f + ds 2 + + ds 3 + +MenuSelection:; cf74 + ds 1 + + + +SECTION "VBlank",WRAM0[$cfb1] +OverworldDelay: ; cfb1 + ds 1 +TextDelayFrames: ; cfb2 + ds 1 +VBlankOccurred: ; cfb3 + ds 1 + +PredefID: ; cfb4 + ds 1 +PredefTemp: ; cfb5 + ds 2 +PredefAddress: ; cfb7 + ds 2 + + ds 3 + +GameTimerPause: ; cfbc +; bit 0 + ds 1 + +SECTION "Engine",WRAM0[$cfc2] +FXAnimID: +FXAnimIDLo: ; cfc2 + ds 1 +FXAnimIDHi: ; cfc3 + ds 1 + + ds 2 + +TileAnimationTimer: ; cfc6 + ds 1 + + ds 5 + +Options: ; cfcc +; bit 0-2: number of frames to delay when printing text +; fast 1; mid 3; slow 5 +; bit 3: ? +; bit 4: no text delay +; bit 5: stereo off/on +; bit 6: battle style shift/set +; bit 7: battle scene off/on + ds 1 + + ds 1 + +TextBoxFrame: ; cfce +; bits 0-2: textbox frame 0-7 + ds 1 + + ds 1 + +GBPrinter: ; cfd0 +; bit 0-6: brightness +; lightest: $00 +; lighter: $20 +; normal: $40 (default) +; darker: $60 +; darkest: $7F + ds 1 + +Options2: ; cfd1 +; bit 1: menu account off/on + ds 1 + + ds 46 + + +SECTION "WRAMBank1",WRAMX[$d000],BANK[1] + + ds 2 + +DefaultFlypoint: ; d002 + ds 1 +; d003 + ds 1 +; d004 + ds 1 +StartFlypoint: ; d005 + ds 1 +EndFlypoint: ; d006 + ds 1 + +MovementBuffer: ; d007 + + ds 55 + +MenuItemsList: +CurFruitTree: +CurInput: +EngineBuffer1: ; d03e + ds 1 +CurFruit: ; d03f + ds 1 + +MartPointer: ; d040 + ds 2 + +MovementAnimation: ; d042 + ds 1 + +WalkingDirection: ; d043 + ds 1 + +FacingDirection: ; d044 + ds 1 + +WalkingX: ; d045 + ds 1 +WalkingY: ; d046 + ds 1 +WalkingTile: ; d047 + ds 1 + + ds 43 + +StringBuffer1: ; d073 + ds 19 +StringBuffer2: ; d086 + ds 19 +StringBuffer3: ; d099 + ds 19 +StringBuffer4: ; d0ac + ds 19 + + ds 21 + +CurBattleMon: ; d0d4 + ds 1 +CurMoveNum: ; d0d5 + ds 1 + + ds 23 + +VramState: ; d0ed +; bit 0: overworld sprite updating on/off +; bit 6: something to do with text +; bit 7: on when surf initiates +; flickers when climbing waterfall + ds 1 + + ds 2 + +CurMart: ; d0f0 + ds 16 +CurMartEnd: + + ds 6 + +CurItem: ; d106 + ds 1 + + ds 1 + +CurPartySpecies: ; d108 + ds 1 + +CurPartyMon: ; d109 +; contains which monster in a party +; is being dealt with at the moment +; 0-5 + ds 1 + + ds 4 + +TempMon: +TempMonSpecies: ; d10e + ds 1 +TempMonItem: ; d10f + ds 1 +TempMonMoves: ; d110 +TempMonMove1: ; d110 + ds 1 +TempMonMove2: ; d111 + ds 1 +TempMonMove3: ; d112 + ds 1 +TempMonMove4: ; d113 + ds 1 +TempMonID: ; d114 + ds 2 +TempMonExp: ; d116 + ds 3 +TempMonHPExp: ; d119 + ds 2 +TempMonAtkExp: ; d11b + ds 2 +TempMonDefExp: ; d11d + ds 2 +TempMonSpdExp: ; d11f + ds 2 +TempMonSpclExp: ; d121 + ds 2 +TempMonDVs: ; d123 +; hp = %1000 for each dv + ds 1 ; atk/def + ds 1 ; spd/spc +TempMonPP: ; d125 + ds 4 +TempMonHappiness: ; d129 + ds 1 +TempMonPokerusStatus: ; d12a + ds 1 +TempMonCaughtData: ; d12b +TempMonCaughtTime: ; d12b +TempMonCaughtLevel: ; d12b + ds 1 +TempMonCaughtGender: ; d12c +TempMonCaughtLocation: ; d12c + ds 1 +TempMonLevel: ; d12d + ds 1 +TempMonStatus: ; d12e + ds 1 +; d12f + ds 1 +TempMonCurHP: ; d130 + ds 2 +TempMonMaxHP: ; d132 + ds 2 +TempMonAtk: ; d134 + ds 2 +TempMonDef: ; d136 + ds 2 +TempMonSpd: ; d138 + ds 2 +TempMonSpclAtk: ; d13a + ds 2 +TempMonSpclDef: ; d13c + ds 2 +TempMonEnd: ; d13e + + ds 3 + +PartyMenuActionText: ; d141 + ds 1 + ds 1 + +CurPartyLevel: ; d143 + ds 1 + + +SECTION "UsedSprites",WRAMX[$d154],BANK[1] +UsedSprites: ; d154 + ds 32 + +SECTION "map",WRAMX[$d19d],BANK[1] + +; both are in blocks (2x2 walkable tiles, 4x4 graphics tiles) +MapHeader: ; d19d +MapBorderBlock: ; d19d + ds 1 +MapHeight: ; d19e + ds 1 +MapWidth: ; d19f + ds 1 +MapBlockDataBank: ; d1a0 + ds 1 +MapBlockDataPointer: ; d1a1 + ds 2 +MapScriptHeaderBank: ; d1a3 + ds 1 +MapScriptHeaderPointer: ; d1a4 + ds 2 +MapEventHeaderPointer: ; d1a6 + ds 2 +; bit set +MapConnections: ; d1a8 + ds 1 +NorthMapConnection: ; d1a9 +NorthConnectedMapGroup: ; d1a9 + ds 1 +NorthConnectedMapNumber: ; d1aa + ds 1 +NorthConnectionStripPointer: ; d1ab + ds 2 +NorthConnectionStripLocation: ; d1ad + ds 2 +NorthConnectionStripLength: ; d1af + ds 1 +NorthConnectedMapWidth: ; d1b0 + ds 1 +NorthConnectionStripYOffset: ; d1b1 + ds 1 +NorthConnectionStripXOffset: ; d1b2 + ds 1 +NorthConnectionWindow: ; d1b3 + ds 2 + +SouthMapConnection: ; d1b5 +SouthConnectedMapGroup: ; d1b5 + ds 1 +SouthConnectedMapNumber: ; d1b6 + ds 1 +SouthConnectionStripPointer: ; d1b7 + ds 2 +SouthConnectionStripLocation: ; d1b9 + ds 2 +SouthConnectionStripLength: ; d1bb + ds 1 +SouthConnectedMapWidth: ; d1bc + ds 1 +SouthConnectionStripYOffset: ; d1bd + ds 1 +SouthConnectionStripXOffset: ; d1be + ds 1 +SouthConnectionWindow: ; d1bf + ds 2 + +WestMapConnection: ; d1c1 +WestConnectedMapGroup: ; d1c1 + ds 1 +WestConnectedMapNumber: ; d1c2 + ds 1 +WestConnectionStripPointer: ; d1c3 + ds 2 +WestConnectionStripLocation: ; d1c5 + ds 2 +WestConnectionStripLength: ; d1c7 + ds 1 +WestConnectedMapWidth: ; d1c8 + ds 1 +WestConnectionStripYOffset: ; d1c9 + ds 1 +WestConnectionStripXOffset: ; d1ca + ds 1 +WestConnectionWindow: ; d1cb + ds 2 + +EastMapConnection: ; d1cd +EastConnectedMapGroup: ; d1cd + ds 1 +EastConnectedMapNumber: ; d1ce + ds 1 +EastConnectionStripPointer: ; d1cf + ds 2 +EastConnectionStripLocation: ; d1d1 + ds 2 +EastConnectionStripLength: ; d1d3 + ds 1 +EastConnectedMapWidth: ; d1d4 + ds 1 +EastConnectionStripYOffset: ; d1d5 + ds 1 +EastConnectionStripXOffset: ; d1d6 + ds 1 +EastConnectionWindow: ; d1d7 + ds 2 + + +TilesetHeader: +TilesetBank: ; d1d9 + ds 1 +TilesetAddress: ; d1da + ds 2 +TilesetBlocksBank: ; d1dc + ds 1 +TilesetBlocksAddress: ; d1dd + ds 2 +TilesetCollisionBank: ; d1df + ds 1 +TilesetCollisionAddress: ; d1e0 + ds 2 +TilesetAnim: ; d1e2 +; bank 3f + ds 2 +; unused ; d1e4 + ds 2 +TilesetPalettes: ; d1e6 +; bank 3f + ds 2 + +EvolvableFlags: ; d1e8 + ds 1 + + ds 1 + +MagikarpLength: +Buffer1: ; d1ea + ds 1 +MovementType: +Buffer2: ; d1eb + ds 1 + +SECTION "BattleMons2",WRAMX[$d1fa],BANK[1] +LinkBattleRNs: ; d1fa + ds 10 + +TempEnemyMonSpecies: ; d204 + ds 1 +TempBattleMonSpecies: ; d205 + ds 1 + +EnemyMon: +EnemyMonSpecies: ; d206 + ds 1 +EnemyMonItem: ; d207 + ds 1 + +EnemyMonMoves: +EnemyMonMove1: ; d208 + ds 1 +EnemyMonMove2: ; d209 + ds 1 +EnemyMonMove3: ; d20a + ds 1 +EnemyMonMove4: ; d20b + ds 1 +EnemyMonMovesEnd: + +EnemyMonDVs: +EnemyMonAtkDefDV: ; d20c + ds 1 +EnemyMonSpdSpclDV: ; d20d + ds 1 + +EnemyMonPP: +EnemyMonPPMove1: ; d20e + ds 1 +EnemyMonPPMove2: ; d20f + ds 1 +EnemyMonPPMove3: ; d210 + ds 1 +EnemyMonPPMove4: ; d211 + ds 1 + +EnemyMonHappiness: ; d212 + ds 1 +EnemyMonLevel: ; d213 + ds 1 + +EnemyMonStatus: ; d214 + ds 2 + +EnemyMonHP: +EnemyMonHPHi: ; d216 + ds 1 +EnemyMonHPLo: ; d217 + ds 1 + +EnemyMonMaxHP: +EnemyMonMaxHPHi: ; d218 + ds 1 +EnemyMonMaxHPLo: ; d219 + ds 1 + +EnemyMonStats: +EnemyMonAtk: ; d21a + ds 2 +EnemyMonDef: ; d21c + ds 2 +EnemyMonSpd: ; d21e + ds 2 +EnemyMonSpclAtk: ; d220 + ds 2 +EnemyMonSpclDef: ; d222 + ds 2 +EnemyMonStatsEnd: + +EnemyMonType1: ; d224 + ds 1 +EnemyMonType2: ; d225 + ds 1 + +EnemyMonBaseStats: ; d226 + ds 5 + +EnemyMonCatchRate: ; d22b + ds 1 +EnemyMonBaseExp: ; d22c + ds 1 + +EnemyMonEnd: + + +IsInBattle: ; d22d +; 0: overworld +; 1: wild battle +; 2: trainer battle + ds 1 + + ds 1 + +OtherTrainerClass: ; d22f +; class (Youngster, Bug Catcher, etc.) of opposing trainer +; 0 if opponent is a wild Pokémon, not a trainer + ds 1 + +BattleType: ; d230 +; $00 normal +; $01 +; $02 +; $03 dude +; $04 fishing +; $05 roaming +; $06 +; $07 shiny +; $08 headbutt/rock smash +; $09 +; $0a force Item1 +; $0b +; $0c suicune + ds 1 + +OtherTrainerID: ; d231 +; which trainer of the class that you're fighting +; (Joey, Mikey, Albert, etc.) + ds 1 + + ds 1 + +TrainerClass: ; d233 + ds 1 + +UnownLetter: ; d234 + ds 1 + + ds 1 + +CurBaseData: ; d236 +BaseDexNo: ; d236 + ds 1 +BaseStats: ; d237 +BaseHP: ; d237 + ds 1 +BaseAttack: ; d238 + ds 1 +BaseDefense: ; d239 + ds 1 +BaseSpeed: ; d23a + ds 1 +BaseSpecialAttack: ; d23b + ds 1 +BaseSpecialDefense: ; d23c + ds 1 +BaseType: ; d23d +BaseType1: ; d23d + ds 1 +BaseType2: ; d23e + ds 1 +BaseCatchRate: ; d23f + ds 1 +BaseExp: ; d240 + ds 1 +BaseItems: ; d241 + ds 2 +BaseGender: ; d243 + ds 1 +BaseUnknown1: ; d244 + ds 1 +BaseEggSteps: ; d245 + ds 1 +BaseUnknown2: ; d246 + ds 1 +BasePicSize: ; d247 + ds 1 +BasePadding: ; d248 + ds 4 +BaseGrowthRate: ; d24c + ds 1 +BaseEggGroups: ; d24d + ds 1 +BaseTMHM: ; d24e + ds 8 + + +CurDamage: ; d256 + ds 2 + + +SECTION "TimeOfDay",WRAMX[$d269],BANK[1] + +TimeOfDay: ; d269 + ds 1 + +SECTION "OTParty",WRAMX[$d280],BANK[1] + +OTPartyCount: ; d280 + ds 1 ; number of Pokémon in party +OTPartySpecies: ; d281 + ds 6 ; species of each Pokémon in party +; d287 + ds 1 ; any empty slots including the 7th must be FF + ; or the routine will keep going + +OTPartyMon1: +OTPartyMon1Species: ; d288 + ds 1 +OTPartyMon1Item: ; d289 + ds 1 + +OTPartyMon1Moves: ; d28a +OTPartyMon1Move1: ; d28a + ds 1 +OTPartyMon1Move2: ; d28b + ds 1 +OTPartyMon1Move3: ; d28c + ds 1 +OTPartyMon1Move4: ; d28d + ds 1 + +OTPartyMon1ID: ; d28e + ds 2 +OTPartyMon1Exp: ; d290 + ds 3 +OTPartyMon1HPExp: ; d293 + ds 2 +OTPartyMon1AtkExp: ; d295 + ds 2 +OTPartyMon1DefExp: ; d297 + ds 2 +OTPartyMon1SpdExp: ; d299 + ds 2 +OTPartyMon1SpclExp: ; d29b + ds 2 + +OTPartyMon1DVs: ; d29d +OTPartyMon1AtkDefDV: ; d29d + ds 1 +OTPartyMon1SpdSpclDV: ; d29e + ds 1 + +OTPartyMon1PP: ; d29f +OTPartyMon1PPMove1: ; d29f + ds 1 +OTPartyMon1PPMove2: ; d2a0 + ds 1 +OTPartyMon1PPMove3: ; d2a1 + ds 1 +OTPartyMon1PPMove4: ; d2a2 + ds 1 + +OTPartyMon1Happiness: ; d2a3 + ds 1 +OTPartyMon1PokerusStatus: ; d2a4 + ds 1 + +OTPartyMon1CaughtData: ; d2a5 +OTPartyMon1CaughtGender: ; d2a5 +OTPartyMon1CaughtLocation: ; d2a5 + ds 1 +OTPartyMon1CaughtTime: ; d2a6 + ds 1 +OTPartyMon1Level: ; d2a7 + ds 1 + +OTPartyMon1Status: ; d2a8 + ds 1 +OTPartyMon1Unused: ; d2a9 + ds 1 +OTPartyMon1CurHP: ; d2aa + ds 2 +OTPartyMon1MaxHP: ; d2ac + ds 2 +OTPartyMon1Atk: ; d2ae + ds 2 +OTPartyMon1Def: ; d2b0 + ds 2 +OTPartyMon1Spd: ; d2b2 + ds 2 +OTPartyMon1SpclAtk: ; d2b4 + ds 2 +OTPartyMon1SpclDef: ; d2b6 + ds 2 + +OTPartyMon2: ; d2b8 + ds 48 +OTPartyMon3: ; d2e8 + ds 48 +OTPartyMon4: ; d318 + ds 48 +OTPartyMon5: ; d348 + ds 48 +OTPartyMon6: ; d378 + ds 48 + + +OTPartyMonOT: +OTPartyMon1OT: ; d3a8 + ds 11 +OTPartyMon2OT: ; d3b3 + ds 11 +OTPartyMon3OT: ; d3be + ds 11 +OTPartyMon4OT: ; d3c9 + ds 11 +OTPartyMon5OT: ; d3d4 + ds 11 +OTPartyMon6OT: ; d3df + ds 11 + +OTPartyMonNicknames: +OTPartyMon1Nickname: ; d3ea + ds 11 +OTPartyMon2Nickname: ; d3f5 + ds 11 +OTPartyMon3Nickname: ; d400 + ds 11 +OTPartyMon4Nickname: ; d40b + ds 11 +OTPartyMon5Nickname: ; d416 + ds 11 +OTPartyMon6Nickname: ; d421 + ds 11 + +SECTION "Scripting",WRAMX[$d434],BANK[1] +ScriptFlags: ; d434 + ds 1 +ScriptFlags2: ; d435 + ds 1 +ScriptFlags3: ; d436 + ds 1 + +ScriptMode: ; d437 + ds 1 +ScriptRunning: ; d438 + ds 1 +ScriptBank: ; d439 + ds 1 +ScriptPos: ; d43a + ds 2 + + ds 17 + +ScriptDelay: ; d44d + ds 1 + +SECTION "Player",WRAMX[$d472],BANK[1] +PlayerGender: ; d472 +; bit 0: +; 0 male +; 1 female + ds 1 + ds 8 +PlayerID: ; d47b + ds 2 + +PlayerName: ; d47d + ds 11 +MomsName: ; d488 + ds 11 +RivalName: ; d493 + ds 11 +RedsName: ; d49e + ds 11 +GreensName: ; d4a9 + ds 11 + + ds 2 + +; init time set at newgame +StartDay: ; d4b6 + ds 1 +StartHour: ; d4b7 + ds 1 +StartMinute: ; d4b8 + ds 1 +StartSecond: ; d4b9 + ds 1 + + ds 9 + +GameTimeCap: ; d4c3 + ds 1 +GameTimeHours: ; d4c4 + ds 2 +GameTimeMinutes: ; d4c6 + ds 1 +GameTimeSeconds: ; d4c7 + ds 1 +GameTimeFrames: ; d4c8 + ds 1 + + ds 2 + +CurDay: ; d4cb + ds 1 + + ds 10 + + ds 2 + +PlayerSprite: ; d4d8 + ds 1 + + ds 3 + +PlayerPalette: ; d4dc + ds 1 + + ds 1 + +PlayerDirection: ; d4de +; uses bits 2 and 3 / $0c / %00001100 +; %00 down +; %01 up +; %10 left +; $11 right + ds 1 + + ds 2 + +PlayerAction: ; d4e1 +; 1 standing +; 2 walking +; 4 spinning +; 6 fishing + ds 1 + + ds 2 + +StandingTile: ; d4e4 + ds 1 +StandingTile2: ; d4e5 + ds 1 + +; relative to the map struct including borders +MapX: ; d4e6 + ds 1 +MapY: ; d4e7 + ds 1 +MapX2: ; d4e8 + ds 1 +MapY2: ; d4e9 + ds 1 + + ds 3 + +; relative to the bg map, in px +PlayerSpriteX: ; d4ed + ds 1 +PlayerSpriteY: ; d4ee + ds 1 + + +SECTION "Objects",WRAMX[$d71e],BANK[1] +MapObjects: ; d71e + ds OBJECT_LENGTH * NUM_OBJECTS + + +SECTION "VariableSprites",WRAMX[$d82e],BANK[1] +VariableSprites: ; d82e + ds $10 + + +SECTION "Status",WRAMX[$d841],BANK[1] +TimeOfDayPal: ; d841 + ds 1 + ds 4 +; d846 + ds 1 + ds 1 +CurTimeOfDay: ; d848 + ds 1 + + ds 3 + +StatusFlags: ; d84c + ds 1 +StatusFlags2: ; d84d + ds 1 + +Money: ; d84e + ds 3 + + ds 4 + +Coins: ; d855 + ds 2 + +Badges: +JohtoBadges: ; d857 + ds 1 +KantoBadges: ; d858 + ds 1 + +SECTION "Items",WRAMX[$d859],BANK[1] +TMsHMs: ; d859 + ds 57 +TMsHMsEnd: + +NumItems: ; d892 + ds 1 +Items: ; d893 + ds 41 +ItemsEnd: + +NumKeyItems: ; d8bc + ds 1 +KeyItems: ; d8bd + ds 26 +KeyItemsEnd: + +NumBalls: ; d8d7 + ds 1 +Balls: ; d8d8 + ds 25 +BallsEnd: + +PCItems: ; d8f1 + ds 101 +PCItemsEnd: + + +SECTION "overworld",WRAMX[$d95b],BANK[1] +WhichRegisteredItem: ; d95b + ds 1 +RegisteredItem: ; d95c + ds 1 + +PlayerState: ; d95d + ds 1 + +SECTION "scriptram",WRAMX[$d962],BANK[1] +MooMooBerries: ; d962 + ds 1 ; how many berries fed to MooMoo +UndergroundSwitchPositions: ; d963 + ds 1 ; which positions the switches are in +FarfetchdPosition: ; d964 + ds 1 ; which position the ilex farfetch'd is in + +SECTION "Events",WRAMX[$da72],BANK[1] + +EventFlags: ; da72 +;RoomDecorations: ; dac6 +;TeamRocketAzaleaTownAttackEvent: ; db51 +;PoliceAtElmsLabEvent: ; db52 +;SalesmanMahoganyTownEvent: ; db5c +;RedGyaradosEvent: ; db5c + ds 250 +; db6c + +SECTION "BoxNames",WRAMX[$db75],BANK[1] +; 8 chars + $50 +Box1Name: ; db75 + ds 9 +Box2Name: ; db7e + ds 9 +Box3Name: ; db87 + ds 9 +Box4Name: ; db90 + ds 9 +Box5Name: ; db99 + ds 9 +Box6Name: ; dba2 + ds 9 +Box7Name: ; dbab + ds 9 +Box8Name: ; dbb4 + ds 9 +Box9Name: ; dbbd + ds 9 +Box10Name: ; dbc6 + ds 9 +Box11Name: ; dbcf + ds 9 +Box12Name: ; dbd8 + ds 9 +Box13Name: ; dbe1 + ds 9 +Box14Name: ; dbea + ds 9 + +SECTION "bike", WRAMX[$dbf5],BANK[1] +BikeFlags: ; dbf5 +; bit 1: always on bike +; bit 2: downhill + ds 1 + +SECTION "decorations", WRAMX[$dc0f],BANK[1] +; Sprite id of each decoration +Bed: ; dc0f + ds 1 +Carpet: ; dc10 + ds 1 +Plant: ; dc11 + ds 1 +Poster: ; dc12 + ds 1 +Console: ; dc13 + ds 1 +LeftOrnament: ; dc14 + ds 1 +RightOrnament: ; dc15 + ds 1 +BigDoll: ; dc16 + ds 1 + +SECTION "fruittrees", WRAMX[$dc27],BANK[1] +FruitTreeFlags: ; dc27 + ds 1 + +SECTION "steps", WRAMX[$dc73],BANK[1] +StepCount: ; dc73 + ds 1 +PoisonStepCount: ; dc74 + ds 1 + +SECTION "FlypointPermissions", WRAMX[$dca5],BANK[1] +FlypointPerms: ; dca5 + ds 4 + +SECTION "BackupMapInfo", WRAMX[$dcad],BANK[1] + +; used on maps like second floor pokécenter, which are reused, so we know which +; map to return to +BackupMapGroup: ; dcad + ds 1 +BackupMapNumber: ; dcae + ds 1 + +SECTION "PlayerMapInfo", WRAMX[$dcb4],BANK[1] + +WarpNumber: ; dcb4 + ds 1 +MapGroup: ; dcb5 + ds 1 ; map group of current map +MapNumber: ; dcb6 + ds 1 ; map number of current map +YCoord: ; dcb7 + ds 1 ; current y coordinate relative to top-left corner of current map +XCoord: ; dcb8 + ds 1 ; current x coordinate relative to top-left corner of current map + +SECTION "PlayerParty",WRAMX[$dcd7],BANK[1] + +PartyCount: ; dcd7 + ds 1 ; number of Pokémon in party +PartySpecies: ; dcd8 + ds 6 ; species of each Pokémon in party +PartyEnd: ; dcde + ds 1 ; legacy functions don't check PartyCount + +PartyMons: +PartyMon1: +PartyMon1Species: ; dcdf + ds 1 +PartyMon1Item: ; dce0 + ds 1 + +PartyMon1Moves: ; dce1 +PartyMon1Move1: ; dce1 + ds 1 +PartyMon1Move2: ; dce2 + ds 1 +PartyMon1Move3: ; dce3 + ds 1 +PartyMon1Move4: ; dce4 + ds 1 + +PartyMon1ID: ; dce5 + ds 2 +PartyMon1Exp: ; dce7 + ds 3 + +PartyMon1HPExp: ; dcea + ds 2 +PartyMon1AtkExp: ; dcec + ds 2 +PartyMon1DefExp: ; dcee + ds 2 +PartyMon1SpdExp: ; dcf0 + ds 2 +PartyMon1SpclExp: ; dcf2 + ds 2 + +PartyMon1DVs: ; dcf4 +; hp = %1000 for each dv + ds 1 ; atk/def + ds 1 ; spd/spc +PartyMon1PP: ; dcf6 + ds 4 +PartyMon1Happiness: ; dcfa + ds 1 +PartyMon1PokerusStatus: ; dcfb + ds 1 +PartyMon1CaughtData: ; dcfc +PartyMon1CaughtTime: ; dcfc +PartyMon1CaughtLevel: ; dcfc + ds 1 +PartyMon1CaughtGender: ; dcfd +PartyMon1CaughtLocation: ; dcfd + ds 1 +PartyMon1Level: ; dcfe + ds 1 +PartyMon1Status: ; dcff + ds 1 +; dd00 unused + ds 1 +PartyMon1CurHP: ; dd01 + ds 2 +PartyMon1MaxHP: ; dd03 + ds 2 +PartyMon1Atk: ; dd05 + ds 2 +PartyMon1Def: ; dd07 + ds 2 +PartyMon1Spd: ; dd09 + ds 2 +PartyMon1SpclAtk: ; dd0b + ds 2 +PartyMon1SpclDef: ; dd0d + ds 2 + + +PartyMon2: ; dd0f + ds 48 +PartyMon3: ; dd3f + ds 48 +PartyMon4: ; dd6f + ds 48 +PartyMon5: ; dd9f + ds 48 +PartyMon6: ; ddcf + ds 48 + +PartyMonOT: +PartyMon1OT: ; ddff + ds 11 +PartyMon2OT: ; de0a + ds 11 +PartyMon3OT: ; de15 + ds 11 +PartyMon4OT: ; de20 + ds 11 +PartyMon5OT: ; de2b + ds 11 +PartyMon6OT: ; de36 + ds 11 + +PartyMonNicknames: +PartyMon1Nickname: ; de41 + ds 11 +PartyMon2Nickname: ; de4c + ds 11 +PartyMon3Nickname: ; de57 + ds 11 +PartyMon4Nickname: ; de62 + ds 11 +PartyMon5Nickname: ; de6d + ds 11 +PartyMon6Nickname: ; de78 + ds 11 +PartyMonNicknamesEnd: + +SECTION "Pokedex",WRAMX[$de99],BANK[1] +PokedexCaught: ; de99 + ds 32 +EndPokedexCaught: +PokedexSeen: ; deb9 + ds 32 +EndPokedexSeen: +UnownDex: ; ded9 + ds 26 +UnlockedUnowns: ; def3 + ds 1 + +SECTION "Breeding",WRAMX[$def5],BANK[1] +DaycareMan: ; def5 +; bit 7: active +; bit 6: monsters are compatible +; bit 5: egg ready +; bit 0: monster 1 in daycare + ds 1 + +BreedMon1: +BreedMon1Nick: ; def6 + ds 11 +BreedMon1OT: ; df01 + ds 11 +BreedMon1Stats: +BreedMon1Species: ; df0c + ds 1 + ds 31 + +DaycareLady: ; df2c +; bit 7: active +; bit 0: monster 2 in daycare + ds 1 + +StepsToEgg: ; df2d + ds 1 +DittoInDaycare: ; df2e +; z: yes +; nz: no + ds 1 + +BreedMon2: +BreedMon2Nick: ; df2f + ds 11 +BreedMon2OT: ; df3a + ds 11 +BreedMon2Stats: +BreedMon2Species: ; df45 + ds 1 + ds 31 + +EggNick: ; df65 +; EGG@ + ds 11 +EggOT: ; df70 + ds 11 +EggStats: +EggSpecies: ; df7b + ds 1 + ds 31 + +SECTION "RoamMons",WRAMX[$dfcf],BANK[1] +RoamMon1Species: ; dfcf + ds 1 +RoamMon1Level: ; dfd0 + ds 1 +RoamMon1MapGroup: ; dfd1 + ds 1 +RoamMon1MapNumber: ; dfd2 + ds 1 +RoamMon1CurHP: ; dfd3 + ds 1 +RoamMon1DVs: ; dfd4 + ds 2 + +RoamMon2Species: ; dfd6 + ds 1 +RoamMon2Level: ; dfd7 + ds 1 +RoamMon2MapGroup: ; dfd8 + ds 1 +RoamMon2MapNumber: ; dfd9 + ds 1 +RoamMon2CurHP: ; dfda + ds 1 +RoamMon2DVs: ; dfdb + ds 2 + +RoamMon3Species: ; dfdd + ds 1 +RoamMon3Level: ; dfde + ds 1 +RoamMon3MapGroup: ; dfdf + ds 1 +RoamMon3MapNumber: ; dfe0 + ds 1 +RoamMon3CurHP: ; dfe1 + ds 1 +RoamMon3DVs: ; dfe2 + ds 2 + + + +SECTION "WRAMBank5",WRAMX[$d000],BANK[5] + +; 8 4-color palettes +Unkn1Pals: ; d000 + ds $40 +Unkn2Pals: ; d040 + ds $40 +BGPals: ; d080 + ds $40 +OBPals: ; d0c0 + ds $40 + +LYOverrides: ; d100 + ds 144 +LYOverridesEnd: + + +SECTION "SRAMBank1",SRAM,BANK[1] + +SECTION "BoxMons",SRAM[$ad10],BANK[1] +BoxCount: ; ad10 + ds 1 +BoxSpecies: ; ad11 + ds 20 + ds 1 +BoxMons: +BoxMon1: +BoxMon1Species: ; ad26 + ds 1 +BoxMon1Item: ; ad27 + ds 1 +BoxMon1Moves: ; ad28 + ds 4 +BoxMon1ID: ; ad2c + ds 2 +BoxMon1Exp: ; ad2e + ds 3 +BoxMon1HPExp: ; ad31 + ds 2 +BoxMon1AtkExp: ; ad33 + ds 2 +BoxMon1DefExp: ; ad35 + ds 2 +BoxMon1SpdExp: ; ad37 + ds 2 +BoxMon1SpcExp: ; ad39 + ds 2 +BoxMon1DVs: ; ad3b + ds 2 +BoxMon1PP: ; ad3d + ds 4 +BoxMon1Happiness: ; ad41 + ds 1 +BoxMon1PokerusStatus: ; ad42 + ds 1 +BoxMon1CaughtData: +BoxMon1CaughtTime: +BoxMon1CaughtLevel: ; ad43 + ds 1 +BoxMon1CaughtGender: +BoxMon1CaughtLocation: ; ad44 + ds 1 +BoxMon1Level: ; ad45 + ds 1 + +BoxMon2: ; ad46 + ds 32 +BoxMon3: ; ad66 + ds 32 +BoxMon4: ; ad86 + ds 32 +BoxMon5: ; ada6 + ds 32 +BoxMon6: ; adc6 + ds 32 +BoxMon7: ; ade6 + ds 32 +BoxMon8: ; ae06 + ds 32 +BoxMon9: ; ae26 + ds 32 +BoxMon10: ; ae46 + ds 32 +BoxMon11: ; ae66 + ds 32 +BoxMon12: ; ae86 + ds 32 +BoxMon13: ; aea6 + ds 32 +BoxMon14: ; aec6 + ds 32 +BoxMon15: ; aee6 + ds 32 +BoxMon16: ; af06 + ds 32 +BoxMon17: ; af26 + ds 32 +BoxMon18: ; af46 + ds 32 +BoxMon19: ; af66 + ds 32 +BoxMon20: ; af86 + ds 32 + +BoxMonOT: +BoxMon1OT: ; afa6 + ds 11 +BoxMon2OT: ; afb1 + ds 11 +BoxMon3OT: ; afbc + ds 11 +BoxMon4OT: ; afc7 + ds 11 +BoxMon5OT: ; afd2 + ds 11 +BoxMon6OT: ; afdd + ds 11 +BoxMon7OT: ; afe8 + ds 11 +BoxMon8OT: ; aff3 + ds 11 +BoxMon9OT: ; affe + ds 11 +BoxMon10OT: ; b009 + ds 11 +BoxMon11OT: ; b014 + ds 11 +BoxMon12OT: ; b01f + ds 11 +BoxMon13OT: ; b02a + ds 11 +BoxMon14OT: ; b035 + ds 11 +BoxMon15OT: ; b040 + ds 11 +BoxMon16OT: ; b04b + ds 11 +BoxMon17OT: ; b056 + ds 11 +BoxMon18OT: ; b061 + ds 11 +BoxMon19OT: ; b06c + ds 11 +BoxMon20OT: ; b077 + ds 11 + +BoxMonNicknames: +BoxMon1Nickname: ; b082 + ds 11 +BoxMon2Nickname: ; b08d + ds 11 +BoxMon3Nickname: ; b098 + ds 11 +BoxMon4Nickname: ; b0a3 + ds 11 +BoxMon5Nickname: ; b0ae + ds 11 +BoxMon6Nickname: ; b0b9 + ds 11 +BoxMon7Nickname: ; b0c4 + ds 11 +BoxMon8Nickname: ; b0cf + ds 11 +BoxMon9Nickname: ; b0da + ds 11 +BoxMon10Nickname: ; b0e5 + ds 11 +BoxMon11Nickname: ; b0f0 + ds 11 +BoxMon12Nickname: ; b0fb + ds 11 +BoxMon13Nickname: ; b106 + ds 11 +BoxMon14Nickname: ; b111 + ds 11 +BoxMon15Nickname: ; b11c + ds 11 +BoxMon16Nickname: ; b127 + ds 11 +BoxMon17Nickname: ; b132 + ds 11 +BoxMon18Nickname: ; b13d + ds 11 +BoxMon19Nickname: ; b148 + ds 11 +BoxMon20Nickname: ; b153 + ds 11 +BoxMonNicknamesEnd: + -- cgit v1.2.3 From 9c56fcd97d73e96d43d96a661113a2d8b723dccd Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Tue, 12 Nov 2013 23:42:24 -0600 Subject: make a simple join function in data/ --- pokemontools/data/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pokemontools/data/__init__.py b/pokemontools/data/__init__.py index 1a0bf7b..fcc59e9 100644 --- a/pokemontools/data/__init__.py +++ b/pokemontools/data/__init__.py @@ -7,3 +7,9 @@ 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) -- cgit v1.2.3 From 2b23c09e57fbf1d3079326298bcc8ec4e867e69f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 00:09:05 -0600 Subject: include some constants for wram.asm UNfortunately these aren't being parsed from files at the moment. --- pokemontools/wram.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 60001aa..6d2c2cc 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -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: -- cgit v1.2.3 From 51b1f6e642436f1f45c36c86243e58769fe38cd4 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 00:24:15 -0600 Subject: flip the wram labels dictionary --- pokemontools/wram.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 6d2c2cc..80244e3 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -112,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. @@ -166,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_constants dictionary the other way around to access + addresses by label. + """ + self.wram = {} + + for (address, labels) in self.wram_constants.iteritems(): + for label in labels: + self.wram[label] = address -- cgit v1.2.3 From e4067faa60b58793076da8ff4b4170c4a911822f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 00:28:59 -0600 Subject: use the correct wram_labels variable in wram.py --- pokemontools/wram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 80244e3..e1b9212 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -171,11 +171,11 @@ class WRAMProcessor(object): def reformat_wram_labels(self): """ - Flips the wram_constants dictionary the other way around to access + Flips the wram_labels dictionary the other way around to access addresses by label. """ self.wram = {} - for (address, labels) in self.wram_constants.iteritems(): + for (address, labels) in self.wram_labels.iteritems(): for label in labels: self.wram[label] = address -- cgit v1.2.3 From 84c106e118e809884319a871972b61f038540a83 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 19:43:47 -0600 Subject: battle move selection --- pokemontools/vba/battle.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index b1ac0fe..c2cb5c3 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -99,6 +99,80 @@ class Battle(EmulatorController): if execute: self.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.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.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.vba.press(direction, hold=hold, after=after) + + # let's see what the current selection is + selection = self.vba.read_memory_at(0xcfa9) + + # press to choose the attack + self.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. -- cgit v1.2.3 From e8371152c9071c1e8966512c22ec63d61b7c8b8b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 19:52:40 -0600 Subject: self.vba -> self.emulator.vba --- pokemontools/vba/battle.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index c2cb5c3..180230d 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -74,8 +74,8 @@ class Battle(EmulatorController): "Unexpected requested action {0}".format(action) ) - current_row = self.vba.read_memory_at(0xcfa9) - current_column = self.vba.read_memory_at(0xcfaa) + 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]: @@ -84,7 +84,7 @@ class Battle(EmulatorController): elif current_row < action_map[action][0]: direction = "d" - self.vba.press(direction, hold=5, after=10) + self.emulator.vba.press(direction, hold=5, after=10) direction = None if current_column != action_map[action][1]: @@ -93,11 +93,11 @@ class Battle(EmulatorController): elif current_column < action_map[action][1]: direction = "r" - self.vba.press(direction, hold=5, after=10) + self.emulator.vba.press(direction, hold=5, after=10) # now select the action if execute: - self.vba.press(a, hold=5, after=100) + self.emulator.vba.press(a, hold=5, after=100) def select_attack(self, move_number=1, hold=5, after=10): """ @@ -112,7 +112,7 @@ class Battle(EmulatorController): # TODO: detect fight menu and make sure it's detected here. pp_address = 0xc634 + (move_number - 1) - pp = self.vba.read_memory_at(pp_address) + 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. @@ -125,7 +125,7 @@ class Battle(EmulatorController): valid_selection_states = (1, 2, 3, 4) - selection = self.vba.read_memory_at(0xcfa9) + selection = self.emulator.vba.read_memory_at(0xcfa9) while selection != move_number: if selection not in valid_selection_states: @@ -148,13 +148,13 @@ class Battle(EmulatorController): ) # press the arrow button - self.vba.press(direction, hold=hold, after=after) + self.emulator.vba.press(direction, hold=hold, after=after) # let's see what the current selection is - selection = self.vba.read_memory_at(0xcfa9) + selection = self.emulator.vba.read_memory_at(0xcfa9) # press to choose the attack - self.vba.press("a", hold=hold, after=after) + self.emulator.vba.press("a", hold=hold, after=after) def fight(self, move_number): """ -- cgit v1.2.3 From 257ddde525c4cbe6c4623a444c5d82ef8039bb45 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 21:02:34 -0600 Subject: press the "a" button (fix typo) --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 180230d..a0e111f 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -97,7 +97,7 @@ class Battle(EmulatorController): # now select the action if execute: - self.emulator.vba.press(a, hold=5, after=100) + self.emulator.vba.press("a", hold=5, after=100) def select_attack(self, move_number=1, hold=5, after=10): """ -- cgit v1.2.3 From 28393a5342a3f6ad3c1511f81ff86ab4fe9216a2 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 21:44:21 -0600 Subject: fix a variable typo in wild prompt detector --- pokemontools/vba/vba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 57e5e1b..d221fef 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -634,7 +634,7 @@ class crystal(object): "Use next POKMON?", ] - return all([requirement in text for requirement in requirements]) + return all([requirement in screen_text for requirement in requirements]) def is_switch_prompt(self): """ -- cgit v1.2.3 From 79bc5088895d087b5338cb4519d7e5d748ce1faf Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 21:45:05 -0600 Subject: better "is input required" detector --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index a0e111f..409d9a8 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -38,7 +38,7 @@ class Battle(EmulatorController): """ Detects if the battle is waiting for player input. """ - return self.is_player_turn() or self.is_mandatory_switch() or self.is_trainer_switch_prompt() + return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() def is_fight_pack_run_menu(self): """ -- cgit v1.2.3 From dddc61e026163808da93ae3ca7121ec64b2bba8f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 22:43:35 -0600 Subject: a really simple, broken battle strategy --- pokemontools/vba/battle.py | 50 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 409d9a8..1b582dd 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -260,8 +260,12 @@ class Battle(EmulatorController): """ Waits until the battle needs player input. """ - while not self.is_input_required(): - self.emulator.text_wait() + # callback causes text_wait to exit when the callback returns True + def is_in_battle_checker(): + return (self.emulator.vba.read_memory_at(0xd22d) == 0) + + 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) @@ -276,9 +280,14 @@ class Battle(EmulatorController): # xyz wants to battle, a wild foobar appeared self.skip_start_text() + 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() @@ -294,7 +303,8 @@ class Battle(EmulatorController): # "how did i lose? wah" # TODO: this doesn't happen for wild battles - self.skip_end_text() + if not wild: + self.skip_end_text() # TODO: return should indicate win/loss (blackout) @@ -365,4 +375,36 @@ class SpamBattleStrategy(BattleStrategy): """ Always picks the first move of the current pokemon. """ - pass + 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) -- cgit v1.2.3 From 367485303a0b40a4aec1ead83adf7f90fce59834 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 22:44:04 -0600 Subject: improve text_wait for in-battle situations --- pokemontools/vba/vba.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index d221fef..5106004 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -462,16 +462,17 @@ class crystal(object): # date/time box (day choice) # 0x47ab is the one from the intro, 0x49ab is the one from mom. elif 0x47ab in stack or 0x49ab in stack: # was any([x in stack for x in range(0x46EE, 0x47AB)]) - print "probably at a date/time box ? exiting." - break + # if not in battle + if self.vba.read_memory_at(0xd22d) == 0: + print "probably at a date/time box ? exiting." + break # "How many minutes?" selection box elif 0x4826 in stack: print "probably at a \"How many minutes?\" box ? exiting." break - else: - self.vba.step(count=step_size) + self.vba.step(count=step_size) # if there is a callback, then call the callback and exit when the # callback returns True. This is especially useful during the -- cgit v1.2.3 From 811630a5dd82c571836c03131b00ff2608ea5d25 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 23:07:26 -0600 Subject: a better in-battle check for text_wait --- pokemontools/vba/battle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 1b582dd..7c788b9 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -262,7 +262,7 @@ class Battle(EmulatorController): """ # callback causes text_wait to exit when the callback returns True def is_in_battle_checker(): - return (self.emulator.vba.read_memory_at(0xd22d) == 0) + return (self.emulator.vba.read_memory_at(0xd22d) == 0) and (self.emulator.vba.read_memory_at(0xc734) != 0) while not self.is_input_required() and self.is_in_battle(): self.emulator.text_wait(callback=is_in_battle_checker) -- cgit v1.2.3 From d8ab944d4354546d44d098b2c30ba82b6ff509ee Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Wed, 13 Nov 2013 23:07:44 -0600 Subject: simplify the date/clock check in text_wait --- pokemontools/vba/vba.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index 5106004..c7a0bf9 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -462,8 +462,7 @@ class crystal(object): # date/time box (day choice) # 0x47ab is the one from the intro, 0x49ab is the one from mom. elif 0x47ab in stack or 0x49ab in stack: # was any([x in stack for x in range(0x46EE, 0x47AB)]) - # if not in battle - if self.vba.read_memory_at(0xd22d) == 0: + if not self.is_in_battle(): print "probably at a date/time box ? exiting." break -- cgit v1.2.3 From 106f45ac7acd657834c5ac2cd5a7d1198ddea95c Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 00:43:35 -0600 Subject: a really slow way to check for stats screen --- pokemontools/vba/battle.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 7c788b9..898a594 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -220,6 +220,22 @@ class Battle(EmulatorController): else: return False + def is_levelup_screen(self): + """ + Detects the levelup stats screen. + """ + requirements = [ + "ATTACK ", + "DEFENSE ", + "SPCL.ATK ", + "SPCL.DEF ", + "SPEED ", + ] + + screen_text = self.emulator.get_text() + + return all([requirement in screen_text for requirement in requirements]) + def skip_start_text(self, max_loops=20): """ Skip any initial conversation until the player can select an action. @@ -262,7 +278,12 @@ class Battle(EmulatorController): """ # callback causes text_wait to exit when the callback returns True def is_in_battle_checker(): - return (self.emulator.vba.read_memory_at(0xd22d) == 0) and (self.emulator.vba.read_memory_at(0xc734) != 0) + 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() + + return result while not self.is_input_required() and self.is_in_battle(): self.emulator.text_wait(callback=is_in_battle_checker) @@ -298,6 +319,8 @@ class Battle(EmulatorController): 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) else: raise BattleException("unknown state, aborting") -- cgit v1.2.3 From dc0a7ac670915a089d1b1ba27df0b5daca2b56f3 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 00:43:52 -0600 Subject: make get_text slightly more configurable Add params to the get_text() function to dump text tiles from screen. --- pokemontools/vba/vba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/vba/vba.py b/pokemontools/vba/vba.py index c7a0bf9..204e102 100644 --- a/pokemontools/vba/vba.py +++ b/pokemontools/vba/vba.py @@ -718,14 +718,14 @@ class crystal(object): self.vba.write_memory_at(0xd8dc, 5) self.vba.write_memory_at(0xd8dd, 99) - def get_text(self, chars=chars): + def get_text(self, chars=chars, offset=0, bounds=1000): """ Returns alphanumeric text on the screen. Other characters will not be shown. """ output = "" - tiles = self.vba.memory[0xc4a0:0xc4a0 + 1000] + tiles = self.vba.memory[0xc4a0 + offset:0xc4a0 + offset + bounds] for each in tiles: if each in chars.keys(): thing = chars[each] -- cgit v1.2.3 From 1bbfb2869fd011416d4ca4883105d7308a562655 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 01:08:08 -0600 Subject: a faster way to detect the stats screen in battles The other way was way too slow since it had to parse 1000 characters every frame. --- pokemontools/vba/battle.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 898a594..8ba534c 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -38,7 +38,7 @@ class Battle(EmulatorController): """ Detects if the battle is waiting for player input. """ - return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() + return self.is_player_turn() or self.is_mandatory_switch() or self.is_switch_prompt() or self.is_levelup_screen() def is_fight_pack_run_menu(self): """ @@ -224,17 +224,17 @@ class Battle(EmulatorController): """ Detects the levelup stats screen. """ - requirements = [ - "ATTACK ", - "DEFENSE ", - "SPCL.ATK ", - "SPCL.DEF ", - "SPEED ", - ] + # This is implemented as reading some text on the screen instead of + # using get_text() because checking every loop is really slow. - screen_text = self.emulator.get_text() + address = 0xc50f + values = [146, 143, 130, 139] - return all([requirement in screen_text for requirement in requirements]) + 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): """ -- cgit v1.2.3 From 038a9d8ec093e7c73f0a596d0cc996e3971d7d0f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 01:44:40 -0600 Subject: a quick method to detect the "is evolving" message --- pokemontools/vba/battle.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 8ba534c..b800df0 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -236,6 +236,27 @@ class Battle(EmulatorController): 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 skip_start_text(self, max_loops=20): """ Skip any initial conversation until the player can select an action. @@ -283,6 +304,9 @@ class Battle(EmulatorController): # but also, jump out if it's the stats screen result = result or self.is_levelup_screen() + # 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(): -- cgit v1.2.3 From 05faa7784ad03cb057e9ac478606d211c4cd88eb Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 19:57:49 -0600 Subject: quick check for the evolved screen This is the message that appears after the pokemon evolved. --- pokemontools/vba/battle.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index b800df0..3339a9f 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -257,6 +257,26 @@ class Battle(EmulatorController): 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 skip_start_text(self, max_loops=20): """ Skip any initial conversation until the player can select an action. -- cgit v1.2.3 From 3bf53fa87facc5471ee0b06c496fee6e8eaceefb Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 20:00:59 -0600 Subject: detect the evolved screen text --- pokemontools/vba/battle.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 3339a9f..4b7fb68 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -365,6 +365,8 @@ class Battle(EmulatorController): 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) else: raise BattleException("unknown state, aborting") -- cgit v1.2.3 From b2785948d16acd06197db5fcffea45d54778bdba Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 20:04:22 -0600 Subject: fast check for the "move to make room for" text --- pokemontools/vba/battle.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index 4b7fb68..f8457c9 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -277,6 +277,22 @@ class Battle(EmulatorController): 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. -- cgit v1.2.3 From 1b9212a7e47a8eca382249bd7bb116c69fc789d3 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Thu, 14 Nov 2013 20:16:35 -0600 Subject: never learn a new move --- pokemontools/vba/battle.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pokemontools/vba/battle.py b/pokemontools/vba/battle.py index f8457c9..39d7047 100644 --- a/pokemontools/vba/battle.py +++ b/pokemontools/vba/battle.py @@ -38,7 +38,7 @@ class Battle(EmulatorController): """ 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() + 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): """ @@ -340,6 +340,9 @@ class Battle(EmulatorController): # 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() @@ -361,6 +364,9 @@ class Battle(EmulatorController): # 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(): @@ -383,6 +389,8 @@ class Battle(EmulatorController): 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") @@ -450,6 +458,12 @@ class BattleStrategy(Battle): """ 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 @@ -493,3 +507,15 @@ class SpamBattleStrategy(BattleStrategy): # 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() -- cgit v1.2.3 From af6b87ae494668ff32f6398bca0b5d5c746e0115 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 11:09:24 -0600 Subject: fix syntax error in audio.py --- pokemontools/audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 1cce1fe..e783b8f 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -209,7 +209,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 -- cgit v1.2.3 From 726d6f213470b09347326fb9dddb988de1e143b9 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 11:09:36 -0600 Subject: fix configuration import in audio.py --- pokemontools/audio.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index e783b8f..38fd65f 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -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): -- cgit v1.2.3 From 9f55db88039fd7020508ab9a28c8740ef053967d Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 11:20:09 -0600 Subject: make gfx.py not depend on crystal.py --- pokemontools/gfx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index c7c6bec..1113b1a 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -5,12 +5,14 @@ import sys import png from math import sqrt, floor, ceil -import crystal +import configuration +config = configuration.Config() + import pokemon_constants import trainers if __name__ != "__main__": - rom = crystal.load_rom() + rom = romstr.RomStr(filename=config.rom_path) def hex_dump(input, debug=True): """ -- cgit v1.2.3 From f80e3a864a0ffae468c1e0e9401ac2aef9587d9f Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 13:24:02 -0600 Subject: be sure to import romstr into gfx.py --- pokemontools/gfx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 1113b1a..d830259 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -10,6 +10,7 @@ config = configuration.Config() import pokemon_constants import trainers +import romstr if __name__ != "__main__": rom = romstr.RomStr(filename=config.rom_path) -- cgit v1.2.3 From 2ca1c4b59c8c4692360c646d4d1da2c38f10d347 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 15:01:26 -0600 Subject: fix configuration import line in map_editor.py --- pokemontools/map_editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/map_editor.py b/pokemontools/map_editor.py index 4c0bec5..566d422 100644 --- a/pokemontools/map_editor.py +++ b/pokemontools/map_editor.py @@ -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' -- cgit v1.2.3