diff options
Diffstat (limited to 'vba')
| -rw-r--r-- | vba/vba.py | 1181 | ||||
| -rw-r--r-- | vba/vba_autoplayer.py | 495 | ||||
| -rw-r--r-- | vba/vba_config.py | 12 | ||||
| -rw-r--r-- | vba/vba_keyboard.py | 562 | 
4 files changed, 2250 insertions, 0 deletions
| diff --git a/vba/vba.py b/vba/vba.py new file mode 100644 index 0000000..ea7bb2e --- /dev/null +++ b/vba/vba.py @@ -0,0 +1,1181 @@ +#!/usr/bin/jython +# -*- encoding: utf-8 -*- +""" +vba-clojure (but really it's jython/python/jvm) + +This is jython, not python. Use jython to run this file. Before running this +file, some of the dependencies need to be constructed. These can be obtained +from the vba-clojure project. +    sudo apt-get install g++ libtool openjdk-6-jre openjdk-6-jdk libsdl1.2-dev mercurial ant autoconf jython + +    export JAVA_INCLUDE_PATH=/usr/lib/jvm/java-6-openjdk-amd64/include/ +    export JAVA_INCLUDE_PATH2=/usr/lib/jvm/java-6-openjdk-amd64/include/ + +    hg clone http://hg.bortreb.com/vba-clojure +    cd vba-clojure/ +    ./dl-libs.sh +    cd java/ +    ant all +    cd .. +    autoreconf -i +    ./configure +    make +    sudo make install + +Make sure vba-clojure bindings are in $CLASSPATH: +    export CLASSPATH=$CLASSPATH:java/dist/gb-bindings.jar + +Make sure vba-clojure is available within "java.library.path": +    sudo ln -s \ +      $HOME/local/vba-clojure/vba-clojure/src/clojure/.libs/libvba.so.0.0.0 \ +      /usr/lib/jni/libvba.so + +(In the above command, substitute the first path with the path of the vba-clojure +directory you made, if it is different.) + +Also make sure VisualBoyAdvance.cfg is somewhere in the $PATH for VBA to find. +A default configuration is provided in vba-clojure under src/. + +Usage (in jython, not python): +    import vba + +    # activate the laser beam +    vba.load_rom("/path/to/baserom.gbc") + +    # make the emulator eat some instructions +    vba.nstep(300) + +    # save the state because we're paranoid +    copyrights = vba.get_state() +    # or ... +    vba.save_state("copyrights") +    # >>> vba.load_state("copyrights") == copyrights +    # True + +    # play for a while, then press F12 +    vba.run() + +    # let's save the game again +    vba.save_state("unknown-delete-me") + +    # and let's go back to the other state +    vba.set_state(copyrights) + +    # or why not the other way around? +    vba.set_state(vba.load_state("unknown-delete-me")) + +    vba.get_memory_at(0xDCDA) +    vba.set_memory_at(0xDCDB, 0xFF) +    vba.get_memory_range(0xDCDA, 10) + +TOOD: +    [ ] set a specific register +    [ ] get a specific register +    [ ] breakpoints +    [ ] vgm stuff +    [ ] gbz80disasm integration +    [ ] pokecrystal.extras integration + +""" + +import os +import sys +import re +from array import array +import string +from copy import copy +import unittest + +# for converting bytes to readable text +from chars import chars + +from map_names import map_names + +# for _check_java_library_path +from java.lang import System + +# for passing states to the emulator +from java.nio import ByteBuffer + +# For getRegisters and other times we have to pass a java int array to a +# function. +import jarray + +# load in the vba-clojure bindings +import com.aurellem.gb.Gb as Gb + +# load the vba-clojure library +Gb.loadVBA() + +from vba_config import * + +try: +    import vba_keyboard as keyboard +except ImportError: +    print "Not loading the keyboard module (which uses networkx)." + +if not os.path.exists(rom_path): +    raise Exception("rom_path is not configured properly; edit vba_config.py? " + str(rom_path)) + +def _check_java_library_path(): +    """ +    Returns the value of java.library.path. + +    The vba-clojure library must be compiled +    and linked from this location. +    """ +    return System.getProperty("java.library.path") + +class RomList(list): + +    """ +    Simple wrapper to prevent a giant rom from being shown on screen. +    """ + +    def __init__(self, *args, **kwargs): +        list.__init__(self, *args, **kwargs) + +    def __repr__(self): +        """ +        Simplifies this object so that the output doesn't overflow stdout. +        """ +        return "RomList(too long)" + +button_masks = { +    "a": 0x0001, +    "b": 0x0002, +    "r": 0x0010, +    "l": 0x0020, +    "u": 0x0040, +    "d": 0x0080, +    "select":   0x0004, +    "start":    0x0008, +    "restart":  0x0800, +    "listen":       -1, # what? +} + +# useful for directly stating what to press +a, b, r, l, u, d, select, start, restart = "a", "b", "r", "l", "u", "d", "select", "start", "restart" + +def button_combiner(buttons): +    """ +    Combines multiple button presses into an integer. + +    This is used when sending a keypress to the emulator. +    """ +    result = 0 + +    # String inputs need to be cleaned up so that "start" doesn't get +    # recognized as "s" and "t" etc.. +    if isinstance(buttons, str): +        if "restart" in buttons: +            buttons = buttons.replace("restart", "") +            result |= button_masks["restart"] +        if "start" in buttons: +            buttons = buttons.replace("start", "") +            result |= button_masks["start"] +        if "select" in buttons: +            buttons = buttons.replace("select", "") +            result |= button_masks["select"] + +        # allow for the "a, b" and "a b" formats +        if ", " in buttons: +            buttons = buttons.split(", ") +        elif " " in buttons: +            buttons = buttons.split(" ") + +    if isinstance(buttons, list): +        if len(buttons) > 9: +            raise Exception("can't combine more than 9 buttons at a time") + +    for each in buttons: +        result |= button_masks[each] + +    #print "button: " + str(result) +    return result + +def load_rom(path=None): +    """ +    Starts the emulator with a certain ROM. + +    Defaults to rom_path if no parameters are given. +    """ +    if path == None: +        path = rom_path +    try: +        root = load_state("root") +    except: +        # "root.sav" is required because if you create it in the future, you +        # will have to shutdown the emulator and possibly lose your state. Some +        # functions require there to be at least one root state available to do +        # computations between two states. +        sys.stderr.write("ERROR: unable to read \"root.sav\", please run" +        " generate_root() or get_root() to make an initial save.\n") +    Gb.startEmulator(path) + +def shutdown(): +    """ +    Stops the emulator. Closes the window. + +    The "opposite" of this is the load_rom function. +    """ +    Gb.shutdown() + +def step(): +    """ +    Advances the emulator forward by one step. +    """ +    Gb.step() + +def nstep(steplimit): +    """ +    Step the game forward by a certain number of instructions. +    """ +    for counter in range(0, steplimit): +        Gb.step() + +def step_until_capture(): +    """ +    Loop step() until SDLK_F12 is detected. +    """ +    Gb.stepUntilCapture() + +# just some aliases for step_until_capture +run = go = step_until_capture + +def translate_chars(charz): +    result = "" +    for each in charz: +        result += chars[each] +    return result + +def _create_byte_buffer(data): +    """ +    Converts data into a ByteBuffer. + +    This is useful for interfacing with the Gb class. +    """ +    buf = ByteBuffer.allocateDirect(len(data)) +    if isinstance(data[0], int): +        for byte in data: +            buf.put(byte) +    else: +        for byte in data: +            buf.put(ord(byte)) +    return buf + +def set_state(state, do_step=False): +    """ +    Injects the given state into the emulator. + +    Use do_step if you want to call step(), which also allows +    SDL to render the latest frame. Note that the default is to +    not step, and that the screen (if it is enabled) will appear +    as if it still has the last state loaded. This is normal. +    """ +    Gb.loadState(_create_byte_buffer(state)) +    if do_step: +        step() + +def get_state(): +    """ +    Retrieves the current state of the emulator. +    """ +    buf = Gb.saveState() +    state = [buf.get(x) for x in range(0, buf.capacity())] +    arr = array("b") +    arr.extend(state) +    return arr.tostring() # instead of state + +def save_state(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 = get_state() +    if len(name) < 4 or name[-4:] != ".sav": +        name += ".sav" +    save_path = os.path.join(save_state_path, name) +    if not override and os.path.exists(save_path): +        raise Exception("oops, save state path already exists: " + str(save_path)) +    else: +        # convert the state into a reasonable output +        data = array('b') +        data.extend(state) +        output = data.tostring() + +        file_handler = open(save_path, "wb") +        file_handler.write(output) +        file_handler.close() + +def load_state(name): +    """ +    Reads a state from file based on name. + +    Looks in save_state_path for a file +    with this name (".sav" is optional). +    """ +    save_path = os.path.join(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) +    file_handler = open(save_path, "rb") +    state = file_handler.read() +    file_handler.close() +    return state + +def generate_root(): +    """ +    Restarts the emulator and saves the initial state to "root.sav". +    """ +    shutdown() +    load_rom() +    root = get_state() +    save_state("root", state=root, override=True) +    return root + +def get_root(): +    """ +    Loads the root state. + +    (Or restarts the emulator and creates a new root state.) +    """ +    try: +        root = load_state("root") +    except: +        root = generate_root() + +def get_registers(): +    """ +    Returns a list of current register values. +    """ +    register_array = jarray.zeros(Gb.NUM_REGISTERS, "i") +    Gb.getRegisters(register_array) +    return list(register_array) + +def set_registers(registers): +    """ +    Applies the set of registers to the CPU. +    """ +    Gb.writeRegisters(registers) +write_registers = set_registers + +def get_rom(): +    """ +    Returns the ROM in bytes. +    """ +    rom_array = jarray.zeros(Gb.ROM_SIZE, "i") +    Gb.getROM(rom_array) +    return RomList(rom_array) + +def get_ram(): +    """ +    Returns the RAM in bytes. +    """ +    ram_array = jarray.zeros(Gb.RAM_SIZE, "i") +    Gb.getRAM(ram_array) +    return RomList(ram_array) + +def say_hello(): +    """ +    Test that the VBA/GB bindings are working. +    """ +    Gb.sayHello() + +def get_memory(): +    """ +    Returns memory in bytes. +    """ +    memory_size = 0x10000 +    memory = jarray.zeros(memory_size, "i") +    Gb.getMemory(memory) +    return RomList(memory) + +def set_memory(memory): +    """ +    Sets memory in the emulator. + +    Use get_memory() to retrieve the current state. +    """ +    Gb.writeMemory(memory) + +def get_pixels(): +    """ +    Returns a list of pixels on the screen display. + +    Broken, probably. Use screenshot() instead. +    """ +    sys.stderr.write("ERROR: seems to be broken on VBA's end? Good luck. Use" +    " screenshot() instead.\n") +    size = Gb.DISPLAY_WIDTH * Gb.DISPLAY_HEIGHT +    pixels = jarray.zeros(size, "i") +    Gb.getPixels(pixels) +    return RomList(pixels) + +def screenshot(filename, literal=False): +    """ +    Saves a PNG screenshot to the file at filename. + +    Use literal if you want to store it in the current directory. +    Default is to save it to screenshots/ under the project. +    """ +    screenshots_path = os.path.join(project_path, "screenshots/") +    filename = os.path.join(screenshots_path, filename) +    if len(filename) < 4 or filename[-4:] != ".png": +        filename += ".png" +    Gb.nwritePNG(filename) +    print "Screenshot saved to: " + str(filename) +save_png = screenshot + +def read_memory(address): +    """ +    Read an integer at an address. +    """ +    return Gb.readMemory(address) +get_memory_at = read_memory + +def get_memory_range(start_address, byte_count): +    """ +    Returns a list of bytes. + +    start_address - address to start reading at +    byte_count - how many bytes (0 returns just 1 byte) +    """ +    bytez = [] +    for counter in range(0, byte_count): +        byte = get_memory_at(start_address + counter) +        bytez.append(byte) +    return bytez + +def set_memory_at(address, value): +    """ +    Sets a byte at a certain address in memory. + +    This directly sets the memory instead of copying +    the memory from the emulator. +    """ +    Gb.setMemoryAt(address, value) + +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 get_buttons(): +    """ +    Returns the currentButtons[0] value + +    (an integer with bits set for which +    buttons are currently pressed). +    """ +    return Gb.getCurrentButtons() + +class State(RomList): +    name = None + +class Recording: +    def __init__(self): +        self.frames = [] +        self.states = {} + +    def _get_frame_count(self): +        return len(self.frames) + +    frame_count = property(fget=_get_frame_count) + +    def save(self, name=None): +        """ +        Saves the current state. +        """ +        state = State(get_state()) +        state.name = name +        self.states[self.frame_count] = state + +    def load(self, name): +        """ +        Loads a state by name in the state list. +        """ +        for state in self.states.items(): +            if state.name == name: +                set_state(state) +                return state +        return False + +    def step(self, stepcount=1, first_frame=0, replay=False): +        """ +        Records button presses for each frame. +        """ +        if replay: +            stepcount = len(self.frames[first_name:]) + +        for counter in range(first_frame, stepcount): +            if replay: +                press(self.frames[counter], steplimit=0) +            else: +                self.frames.append(get_buttons()) +            nstep(1) + +    def replay_from(self, thing): +        """ +        Replays based on a State or the name of a saved state. +        """ +        if isinstance(thing, State): +            set_state(thing) +        else: +            thing = self.load(thing) +        frame_id = self.states.index(thing) +        self.step(first_frame=frame_id, replay=True) + +class Registers: +    order = [ +        "pc", +        "sp", +        "af", +        "bc", +        "de", +        "hl", +        "iff", +        "div", +        "tima", +        "tma", +        "tac", +        "if", +        "lcdc", +        "stat", +        "scy", +        "scx", +        "ly", +        "lyc", +        "dma", +        "wy", +        "wx", +        "vbk", +        "hdma1", +        "hdma2", +        "hdma3", +        "hdma4", +        "hdma5", +        "svbk", +        "ie", +    ] + +    def __setitem__(self, key, value): +        current_registers = get_registers() +        current_registers[Registers.order.index(key)] = value +        set_registers(current_registers) + +    def __getitem__(self, key): +        current_registers = get_registers() +        return current_registers[Registers.order.index(key)] + +    def __list__(self): +        return get_registers() + +    def _get_register(id): +        def constructed_func(self, id=copy(id)): +            return get_registers()[id] +        return constructed_func + +    def _set_register(id): +        def constructed_func(self, value, id=copy(id)): +            current_registers = get_registers() +            current_registers[id] = value +            set_registers(current_registers) +        return constructed_func + +    pc = property(fget=_get_register(0), fset=_set_register(0)) +    sp = property(fget=_get_register(1), fset=_set_register(1)) +    af = property(fget=_get_register(2), fset=_set_register(2)) +    bc = property(fget=_get_register(3), fset=_set_register(3)) +    de = property(fget=_get_register(4), fset=_set_register(4)) +    hl = property(fget=_get_register(5), fset=_set_register(5)) +    iff = property(fget=_get_register(6), fset=_set_register(6)) +    div = property(fget=_get_register(7), fset=_set_register(7)) +    tima = property(fget=_get_register(8), fset=_set_register(8)) +    tma = property(fget=_get_register(9), fset=_set_register(9)) +    tac = property(fget=_get_register(10), fset=_set_register(10)) +    _if = property(fget=_get_register(11), fset=_set_register(11)) +    lcdc = property(fget=_get_register(12), fset=_set_register(12)) +    stat = property(fget=_get_register(13), fset=_set_register(13)) +    scy = property(fget=_get_register(14), fset=_set_register(14)) +    scx = property(fget=_get_register(15), fset=_set_register(15)) +    ly = property(fget=_get_register(16), fset=_set_register(16)) +    lyc = property(fget=_get_register(17), fset=_set_register(17)) +    dma = property(fget=_get_register(18), fset=_set_register(18)) +    wy = property(fget=_get_register(19), fset=_set_register(19)) +    wx = property(fget=_get_register(20), fset=_set_register(20)) +    vbk = property(fget=_get_register(21), fset=_set_register(21)) +    hdma1 = property(fget=_get_register(22), fset=_set_register(22)) +    hdma2 = property(fget=_get_register(23), fset=_set_register(23)) +    hdma3 = property(fget=_get_register(24), fset=_set_register(24)) +    hdma4 = property(fget=_get_register(25), fset=_set_register(25)) +    hdma5 = property(fget=_get_register(26), fset=_set_register(26)) +    svbk = property(fget=_get_register(27), fset=_set_register(27)) +    ie = property(fget=_get_register(28), fset=_set_register(28)) + +    def __repr__(self): +        spacing = "\t" +        output = "Registers:\n" +        for (id, each) in enumerate(self.order): +            output += spacing + each + " = " + hex(get_registers()[id]) +            #hex(self[each]) +            output += "\n" +        return output + +registers = Registers() + +def call(bank, 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. +    """ +    push = [ +        registers.pc, +        registers.hl, +        registers.de, +        registers.bc, +        registers.af, +        0x3bb7, +    ] + +    for value in push: +        registers.sp -= 2 +        set_memory_at(registers.sp + 1, value >> 8) +        set_memory_at(registers.sp, value & 0xFF) +        if get_memory_range(registers.sp, 2) != [value & 0xFF, value >> 8]: +            print "desired memory values: " + str([value & 0xFF, value >> 8] ) +            print "actual memory values: " + str(get_memory_range(registers.sp , 2)) +            print "wrong value at " + hex(registers.sp) + " expected " + hex(value) + " but got " + hex(get_memory_at(registers.sp)) + +    if bank != 0: +        registers["af"] = (bank << 8) | (registers["af"] & 0xFF) +        registers["hl"] = address +        registers["pc"] = 0x2d63 # FarJump +    else: +        registers["pc"] = address + +class cheats: +    """ +    Helpers to manage the cheating infrastructure. + +    import vba; vba.load_rom(); vba.cheats.add_gameshark("0100CFCF", "text speedup 1"); vba.cheats.add_gameshark("0101CCCF", "text speedup 2"); vba.go() +    """ + +    @staticmethod +    def enable(id): +        """ +        void gbCheatEnable(int i) +        """ +        Gb.cheatEnable(id) + +    @staticmethod +    def disable(id): +        """ +        void gbCheatDisable(int i) +        """ +        Gb.cheatDisable(id) + +    @staticmethod +    def load_file(filename): +        """ +        Loads a .clt file. By default each cheat is disabled. +        """ +        Gb.loadCheatsFromFile(filename) + +    @staticmethod +    def remove_all(): +        """ +        Removes all cheats from memory. + +        void gbCheatRemoveAll() +        """ +        Gb.cheatRemoveAll() + +    @staticmethod +    def remove_cheat(id): +        """ +        Removes a specific cheat from memory by id. + +        void gbCheatRemove(int i) +        """ +        Gb.cheatRemove(id) + +    @staticmethod +    def add_gamegenie(code, description=""): +        """ +        void gbAddGgCheat(const char *code, const char *desc) +        """ +        Gb.cheatAddGamegenie(code, description) + +    @staticmethod +    def add_gameshark(code, description=""): +        """ +        gbAddGsCheat(const char *code, const char *desc) +        """ +        Gb.cheatAddGameshark(code, description) + +def get_stack(): +    """ +    Return a list of functions on the stack. +    """ +    addresses = [] +    sp = registers.sp + +    for x in range(0, 11): +        sp = sp - (2 * x) +        hi = get_memory_at(sp + 1) +        lo = get_memory_at(sp) +        address = ((hi << 8) | lo) +        addresses.append(address) + +    return addresses + +class crystal: +    """ +    Just a simple namespace to store a bunch of functions for Pokémon Crystal. +    """ + +    @staticmethod +    def text_wait(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. + +        The `debug` parameter is only useful when debugging this function. It +        enables the `max_wait` feature, which causes the function to exit +        instead of hanging around. + +        The `sfx_limit` parameter is useful for when the player is given an +        item during the text. Set it to 1 to not treat the sound as the end of +        text. The next loop around it will return to the normal behavior of the +        function. + +        :param step_size: number of steps per wait loop +        :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) +            address = ((hi << 8) | lo) + +            if address in range(0xa1b, 0xa46) + range(0xaaf, 0xaf5): #  0xaef: +                print "pressing, then breaking.. address is: " + str(hex(address)) + +                # set CurSFX +                set_memory_at(0xc2bf, 0) + +                press("a", holdsteps=10, aftersteps=1) + +                # check if CurSFX is SFX_READ_TEXT_2 +                if get_memory_at(0xc2bf) == 0x8: +                    print "cursfx is set to SFX_READ_TEXT_2, looping.." +                    return crystal.text_wait(step_size=step_size, max_wait=max_wait, debug=debug, callback=callback, sfx_limit=sfx_limit) +                else: +                    if sfx_limit > 0: +                        sfx_limit = sfx_limit - 1 +                        print "decreasing sfx_limit" +                    else: +                        # probably the last textbox in a sequence +                        print "cursfx is not set to SFX_READ_TEXT_2, so: breaking" + +                        break +            else: +                stack = get_stack() + +                # yes/no box or the name selection box +                if address in range(0xa46, 0xaaf): +                    print "probably at a yes/no box.. exiting." +                    break + +                # 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 + +                # "How many minutes?" selection box +                elif 0x4826 in stack: +                    print "probably at a \"How many minutes?\" box ? exiting." +                    break + +                else: +                    nstep(step_size) + +            # if there is a callback, then call the callback and exit when the +            # callback returns True. This is especially useful during the +            # OakSpeech intro where textboxes are running constantly, and then +            # suddenly the player can move around. One way to detect that is to +            # set callback to a function that returns +            # "vba.get_memory_at(0xcfb1) != 0". +            if callback != None: +                result = callback() +                if result == True: +                    print "callback returned True, exiting" +                    return + +            # only useful when debugging. When this is left on, text that +            # takes a while to print to screen will cause this function to +            # exit. +            if debug == True: +                max_wait = max_wait - 1 + +        if max_wait == 0: +            print "max_wait was hit" + +    @staticmethod +    def walk_through_walls_slow(): +        memory = get_memory() +        memory[0xC2FA] = 0 +        memory[0xC2FB] = 0 +        memory[0xC2FC] = 0 +        memory[0xC2FD] = 0 +        set_memory(memory) + +    @staticmethod +    def walk_through_walls(): +        """ +        Lets the player walk all over the map. + +        These values are probably reset by some of the map/collision +        functions when you move on to a new location, so this needs +        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) + +    #@staticmethod +    #def set_enemy_level(level): +    #    set_memory_at(0xd213, level) + +    @staticmethod +    def nstep(steplimit=500): +        """ +        Steps the CPU forward and calls some functions in between each step. + +        (For example, to manipulate memory.) This is pretty slow. +        """ +        for step_counter in range(0, steplimit): +            crystal.walk_through_walls() +            #call(0x1, 0x1078) +            step() + +    @staticmethod +    def disable_triggers(): +        set_memory_at(0x23c4, 0xAF) +        set_memory_at(0x23d0, 0xAF); + +    @staticmethod +    def disable_callbacks(): +        set_memory_at(0x23f2, 0xAF) +        set_memory_at(0x23fe, 0xAF) + +    @staticmethod +    def get_map_group_id(): +        """ +        Returns the current map group. +        """ +        return get_memory_at(0xdcb5) + +    @staticmethod +    def get_map_id(): +        """ +        Returns the map number of the current map. +        """ +        return get_memory_at(0xdcb6) + +    @staticmethod +    def get_map_name(): +        """ +        Figures out the current map name. +        """ +        map_group_id = crystal.get_map_group_id() +        map_id = crystal.get_map_id() +        return map_names[map_group_id][map_id]["name"] + +    @staticmethod +    def get_xy(): +        """ +        (x, y) coordinates of player on map. + +        Relative to top-left corner of map. +        """ +        x = get_memory_at(0xdcb8) +        y = get_memory_at(0xdcb7) +        return (x, y) + +    @staticmethod +    def menu_select(id=1): +        """ +        Sets the cursor to the given pokemon in the player's party. + +        This is under Start -> PKMN. This is useful for selecting a +        certain pokemon with fly or another skill. + +        This probably works on other menus. +        """ +        set_memory_at(0xcfa9, id) + +    @staticmethod +    def is_in_battle(): +        """ +        Checks whether or not we're in a battle. +        """ +        return (get_memory_at(0xd22d) != 0) or crystal.is_in_link_battle() + +    @staticmethod +    def is_in_link_battle(): +        return get_memory_at(0xc2dc) != 0 + +    @staticmethod +    def unlock_flypoints(): +        """ +        Unlocks different destinations for flying. + +        Note: this might start at 0xDCA4 (minus one on all addresses), but not +        sure. +        """ +        set_memory_at(0xDCA5, 0xFF) +        set_memory_at(0xDCA6, 0xFF) +        set_memory_at(0xDCA7, 0xFF) +        set_memory_at(0xDCA8, 0xFF) + +    @staticmethod +    def get_gender(): +        """ +        Returns 'male' or 'female'. +        """ +        gender = get_memory_at(0xD472) +        if gender == 0: +            return "male" +        elif gender == 1: +            return "female" +        else: +            return gender + +    @staticmethod +    def get_player_name(): +        """ +        Returns the 7 characters making up the player's name. +        """ +        bytez = get_memory_range(0xD47D, 7) +        name = translate_chars(bytez) +        return name + +    @staticmethod +    def warp(map_group_id, map_id, x, y): +        set_memory_at(0xdcb5, map_group_id) +        set_memory_at(0xdcb6, map_id) +        set_memory_at(0xdcb7, y) +        set_memory_at(0xdcb8, x) +        set_memory_at(0xd001, 0xFF) +        set_memory_at(0xff9f, 0xF1) +        set_memory_at(0xd432, 1) +        set_memory_at(0xd434, 0 & 251) + +    @staticmethod +    def warp_pokecenter(): +        crystal.warp(1, 1, 3, 3) +        crystal.nstep(200) + +    @staticmethod +    def masterballs(): +        # masterball +        set_memory_at(0xd8d8, 1) +        set_memory_at(0xd8d9, 99) + +        # ultraball +        set_memory_at(0xd8da, 2) +        set_memory_at(0xd8db, 99) + +        # pokeballs +        set_memory_at(0xd8dc, 5) +        set_memory_at(0xd8dd, 99) + +    @staticmethod +    def get_text(): +        """ +        Returns alphanumeric text on the screen. + +        Other characters will not be shown. +        """ +        output = "" +        tiles = get_memory_range(0xc4a0, 1000) +        for each in tiles: +            if each in chars.keys(): +                thing = chars[each] +                acceptable = False + +                if len(thing) == 2: +                    portion = thing[1:] +                else: +                    portion = thing + +                if portion in string.printable: +                    acceptable = True + +                if acceptable: +                    output += thing + +        # remove extra whitespace +        output = re.sub(" +", " ", output) +        output = output.strip() + +        return output + +    @staticmethod +    def keyboard_apply(button_sequence): +        """ +        Applies a sequence of buttons to the on-screen keyboard. +        """ +        for buttons in button_sequence: +            press(buttons) +            nstep(2) +            press([]) + +    @staticmethod +    def write(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]) + +    @staticmethod +    def set_partymon2(): +        """ +        This causes corruption, so it's not working yet. +        """ +        memory = get_memory() +        memory[0xdcd7] = 2 +        memory[0xdcd9] = 0x7 + +        memory[0xdd0f] = 0x7 +        memory[0xdd10] = 0x1 + +        # moves +        memory[0xdd11] = 0x1 +        memory[0xdd12] = 0x2 +        memory[0xdd13] = 0x3 +        memory[0xdd14] = 0x4 + +        # id +        memory[0xdd15] = 0x1 +        memory[0xdd16] = 0x2 + +        # experience +        memory[0xdd17] = 0x2 +        memory[0xdd18] = 0x3 +        memory[0xdd19] = 0x4 + +        # hp +        memory[0xdd1a] = 0x5 +        memory[0xdd1b] = 0x6 + +        # current hp +        memory[0xdd31] = 0x10 +        memory[0xdd32] = 0x25 + +        # max hp +        memory[0xdd33] = 0x10 +        memory[0xdd34] = 0x40 + +        set_memory(memory) + +    @staticmethod +    def wait_for_script_running(debug=False, limit=1000): +        """ +        Wait until ScriptRunning isn't -1. +        """ +        while limit > 0: +            if get_memory_at(0xd438) != 255: +                print "script is done executing" +                return +            else: +                step() + +            if debug: +                limit = limit - 1 + +        if limit == 0: +            print "limit ran out" + +    @staticmethod +    def move(cmd): +        """ +        Attempt to move the player. +        """ +        press(cmd, holdsteps=10, aftersteps=0) +        press([]) + +        memory = get_memory() +        #while memory[0xd4e1] == 2 and memory[0xd042] != 0x3e: +        while memory[0xd043] in [0, 1, 2, 3]: +        #while memory[0xd043] in [0, 1, 2, 3] or memory[0xd042] != 0x3e: +            nstep(10) +            memory = get_memory() + +class TestEmulator(unittest.TestCase): +    try: +        state = load_state("cheating-12") +    except: +        if "__name__" == "__main__": +            raise Exception("failed to setup unit tests because no save state found") + +    def setUp(self): +        load_rom() +        set_state(self.state) + +    def tearDown(self): +        shutdown() + +    def test_PlaceString(self): +        call(0, 0x1078) + +        # where to draw the text +        registers["hl"] = 0xc4a0 + +        # what text to read from +        registers["de"] = 0x1276 + +        nstep(10) + +        text = crystal.get_text() + +        self.assertTrue("TRAINER" in text) + +class TestWriter(unittest.TestCase): +    def test_very_basic(self): +        button_sequence = keyboard.plan_typing("an") +        expected_result = ["select", "a", "d", "r", "r", "r", "r", "a"] + +        self.assertEqual(len(expected_result), len(button_sequence)) +        self.assertEqual(expected_result, button_sequence) + +if __name__ == "__main__": +    unittest.main() diff --git a/vba/vba_autoplayer.py b/vba/vba_autoplayer.py new file mode 100644 index 0000000..9aa8f4a --- /dev/null +++ b/vba/vba_autoplayer.py @@ -0,0 +1,495 @@ +# -*- coding: utf-8 -*- +""" +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) + +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. +    """ +    def wrapped_function(*args, **kwargs): +        skip = True + +        if "skip" in kwargs.keys(): +            skip = kwargs["skip"] +            del kwargs["skip"] + +        # 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)): +                skip = False + +        return_value = None + +        if not skip: +            vba.save_state(func.__name__ + "-start", override=True) +            return_value = func(*args, **kwargs) +            vba.save_state(func.__name__ + "-end", override=True) +        elif skip: +            vba.set_state(vba.load_state(func.__name__ + "-end")) + +        return return_value +    return wrapped_function + +@skippable +def skip_intro(): +    """ +    Skip the game boot intro sequence. +    """ + +    # copyright sequence +    vba.nstep(400) + +    # skip the ditto sequence +    vba.press("a") +    vba.nstep(100) + +    # skip the start screen +    vba.press("start") +    vba.nstep(100) + +    # click "new game" +    vba.press("a", holdsteps=50, aftersteps=1) + +    # skip text up to "Are you a boy? Or are you a girl?" +    vba.crystal.text_wait() + +    # select "Boy" +    vba.press("a", holdsteps=50, aftersteps=1) + +    # text until "What time is it?" +    vba.crystal.text_wait() + +    # select 10 o'clock +    vba.press("a", holdsteps=50, aftersteps=1) + +    # yes i mean it +    vba.press("a", holdsteps=50, aftersteps=1) + +    # "How many minutes?" 0 min. +    vba.press("a", holdsteps=50, aftersteps=1) + +    # "Who! 0 min.?" yes/no select yes +    vba.press("a", holdsteps=50, aftersteps=1) + +    # read text until name selection +    vba.crystal.text_wait() + +    # select "Chris" +    vba.press("d", holdsteps=10, aftersteps=1) +    vba.press("a", holdsteps=50, aftersteps=1) + +    def overworldcheck(): +        """ +        A basic check for when the game starts. +        """ +        return vba.get_memory_at(0xcfb1) != 0 + +    # go until the introduction is done +    vba.crystal.text_wait(callback=overworldcheck) + +    return + +@skippable +def handle_mom(): +    """ +    Walk to mom. Handle her speech and questions. +    """ + +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") + +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") + +    vba.crystal.move("d") +    vba.crystal.move("d") + +    # move into mom's line of sight +    vba.crystal.move("d") + +    # let mom talk until "What day is it?" +    vba.crystal.text_wait() + +    # "What day is it?" Sunday +    vba.press("a", holdsteps=10) # Sunday + +    vba.crystal.text_wait() + +    # "SUNDAY, is it?" yes/no +    vba.press("a", holdsteps=10) # yes + +    vba.crystal.text_wait() + +    # "Is it Daylight Saving Time now?" yes/no +    vba.press("a", holdsteps=10) # yes + +    vba.crystal.text_wait() + +    # "AM DST, is that OK?" yes/no +    vba.press("a", holdsteps=10) # yes + +    # text until "know how to use the PHONE?" yes/no +    vba.crystal.text_wait() + +    # press yes +    vba.press("a", holdsteps=10) + +    # wait until mom is done talking +    vba.crystal.text_wait() + +    # wait until the script is done running +    vba.crystal.wait_for_script_running() + +    return + +@skippable +def walk_into_new_bark_town(): +    """ +    Walk outside after talking with mom. +    """ + +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("l") +    vba.crystal.move("l") + +    # walk outside +    vba.crystal.move("d") + +@skippable +def handle_elm(starter_choice): +    """ +    Walk to Elm's Lab and get a starter. +    """ + +    # walk to the lab +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("l") +    vba.crystal.move("u") +    vba.crystal.move("u") + +    # walk into the lab +    vba.crystal.move("u") + +    # talk to elm +    vba.crystal.text_wait() + +    # "that I recently caught." yes/no +    vba.press("a", holdsteps=10) # yes + +    # talk to elm some more +    vba.crystal.text_wait() + +    # talking isn't done yet.. +    vba.crystal.text_wait() +    vba.crystal.text_wait() +    vba.crystal.text_wait() + +    # wait until the script is done running +    vba.crystal.wait_for_script_running() + +    # move toward the pokeballs +    vba.crystal.move("r") + +    # move to cyndaquil +    vba.crystal.move("r") + +    moves = 0 + +    if starter_choice.lower() == "cyndaquil": +        moves = 0 +    if starter_choice.lower() == "totodile": +        moves = 1 +    else: +        moves = 2 + +    for each in range(0, moves): +        vba.crystal.move("r") + +    # face the pokeball +    vba.crystal.move("u") + +    # select it +    vba.press("a", holdsteps=10, aftersteps=0) + +    # wait for the image to pop up +    vba.crystal.text_wait() + +    # wait for the image to close +    vba.crystal.text_wait() + +    # wait for the yes/no box +    vba.crystal.text_wait() + +    # press yes +    vba.press("a", holdsteps=10, aftersteps=0) + +    # wait for elm to talk a bit +    vba.crystal.text_wait() + +    # TODO: why didn't that finish his talking? +    vba.crystal.text_wait() + +    # give a nickname? yes/no +    vba.press("d", holdsteps=10, aftersteps=0) # move to "no" +    vba.press("a", holdsteps=10, aftersteps=0) # no + +    # TODO: why didn't this wait until he was completely done? +    vba.crystal.text_wait() +    vba.crystal.text_wait() + +    # get the phone number +    vba.crystal.text_wait() + +    # talk with elm a bit more +    vba.crystal.text_wait() + +    # TODO: and again.. wtf? +    vba.crystal.text_wait() + +    # wait until the script is done running +    vba.crystal.wait_for_script_running() + +    # move down +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") + +    # move into the researcher's line of sight +    vba.crystal.move("d") + +    # get the potion from the person +    vba.crystal.text_wait() +    vba.crystal.text_wait() + +    # wait for the script to end +    vba.crystal.wait_for_script_running() + +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") + +    # go outside +    vba.crystal.move("d") + +    return + +@skippable +def new_bark_level_grind(level): +    """ +    Do level grinding in New Bark. + +    Starting just outside of Elm's Lab, do some level grinding until the first +    partymon level is equal to the given value.. +    """ + +    # walk to the grass area +    new_bark_level_grind_walk_to_grass(skip=False) + +    # TODO: walk around in grass, handle battles +    walk = ["d", "d", "u", "d", "u", "d"] +    for direction in walk: +        vba.crystal.move(direction) + +    # wait for wild battle to completely start +    vba.crystal.text_wait() + +    attacks = 5 + +    while attacks > 0: +        # FIGHT +        vba.press("a", holdsteps=10, aftersteps=1) + +        # wait to select a move +        vba.crystal.text_wait() + +        # SCRATCH +        vba.press("a", holdsteps=10, aftersteps=1) + +        # wait for the move to be over +        vba.crystal.text_wait() + +        hp = ((vba.get_memory_at(0xd218) << 8) | vba.get_memory_at(0xd217)) +        print "enemy hp is: " + str(hp) + +        if hp == 0: +            print "enemy hp is zero, exiting" +            break +        else: +            print "enemy hp is: " + str(hp) + +        attacks = attacks - 1 + +    while vba.get_memory_at(0xd22d) != 0: +        vba.press("a", holdsteps=10, aftersteps=1) + +    # wait for the map to finish loading +    vba.nstep(50) + +    print "okay, back in the overworld" + +    # move up +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") + +    # move into new bark town +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") + +    # move up +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") + +    # move to the door +    vba.crystal.move("r") +    vba.crystal.move("r") +    vba.crystal.move("r") + +    # walk in +    vba.crystal.move("u") + +    # move up to the healing thing +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("u") +    vba.crystal.move("l") +    vba.crystal.move("l") + +    # face it +    vba.crystal.move("u") + +    # interact +    vba.press("a", holdsteps=10, aftersteps=1) + +    # wait for yes/no box +    vba.crystal.text_wait() + +    # press yes +    vba.press("a", holdsteps=10, aftersteps=1) + +    # TODO: when is healing done? + +    # wait until the script is done running +    vba.crystal.wait_for_script_running() + +    # wait for it to be really really done +    vba.nstep(50) + +    vba.crystal.move("r") +    vba.crystal.move("r") + +    # move to the door +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") +    vba.crystal.move("d") + +    # walk out +    vba.crystal.move("d") + +    # check partymon1 level +    if vba.get_memory_at(0xdcfe) < level: +        new_bark_level_grind(level, skip=False) +    else: +        return + +@skippable +def new_bark_level_grind_walk_to_grass(): +    """ +    Move to just above the grass from outside Elm's lab. +    """ + +    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") + +if __name__ == "__main__": +    main() diff --git a/vba/vba_config.py b/vba/vba_config.py new file mode 100644 index 0000000..4433f16 --- /dev/null +++ b/vba/vba_config.py @@ -0,0 +1,12 @@ +#!/usr/bin/jython +# -*- encoding: utf-8 -*- +import os + +# by default we assume the user has vba in pokecrystal/extras +project_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..')) + +# save states are in pokecrystal/save-states/ +save_state_path = os.path.join(project_path, "save-states") + +# where is your rom? +rom_path = os.path.join(project_path, "baserom.gbc") diff --git a/vba/vba_keyboard.py b/vba/vba_keyboard.py new file mode 100644 index 0000000..7d57953 --- /dev/null +++ b/vba/vba_keyboard.py @@ -0,0 +1,562 @@ +# -*- encoding: utf-8 -*- +""" +This file constructs a networkx.DiGraph object called graph, which can be used +to find the shortest path of keypresses on the keyboard to type a word. +""" + +import itertools +import networkx + +graph = networkx.DiGraph() + +graph_data = """ +A a select +A B r +A I l +A lower-upper-column-1 u +A J d + +B b select +B A l +B C r +B lower-upper-column-2 u +B K d + +C c select +C D r +C B l +C lower-upper-column-3 u +C L d + +D d select +D E r +D C l +D del-upper-column-1 u +D M d + +E e select +E del-upper-column-2 u +E N d +E D l +E F r + +F f select +F del-upper-column-3 u +F O d +F E l +F G r + +G g select +G end-upper-column-1 u +G P d +G F l +G H r + +H h select +H end-upper-column-2 u +H Q d +H G l +H I r + +I i select +I end-upper-column-3 u +I R d +I H l +I A r + +J j select +J A u +J S d +J R l +J K r + +K k select +K B u +K T d +K J l +K L r + +L l select +L C u +L U d +L K l +L M r + +M m select +M D u +M V d +M L l +M N r + +N n select +N E u +N W d +N M l +N O r + +O o select +O F u +O X d +O N l +O P r + +P p select +P G u +P Y d +P O l +P Q r + +Q q select +Q H u +Q Z d +Q P l +Q R r + +R r select +R I u +R space-upper-x8-y2 d +R Q l +R J r + +S s select +S J u +S - d +S space-upper-x8-y2 l + +T t select +T K u +T ? d +T S l +T U r + +U u select +U L u +U ! d +U T l +U V r + +V v select +V M u +V / d +V U l +V W r + +W w select +W N u +W . d +W V l +W X r + +X x select +X O u +X , d +X W l +X Y r + +Y y select +Y P u +Y space-upper-x6-y3 d +Y X l +Y Z r + +Z z select +Z Q u +Z space-upper-x7-y3 d +Z Y l +Z space-upper-x8-y2 r + +end-upper-column-1 lower-upper-column-1 r +end-upper-column-2 lower-upper-column-1 r +end-upper-column-3 lower-upper-column-1 r +end-upper-column-1 del-upper-column-1 l +end-upper-column-2 del-upper-column-1 l +end-upper-column-3 del-upper-column-1 l +lower-upper-column-1 end-upper-column-1 l +lower-upper-column-2 end-upper-column-1 l +lower-upper-column-3 end-upper-column-1 l +lower-upper-column-1 del-upper-column-1 r +lower-upper-column-2 del-upper-column-1 r +lower-upper-column-3 del-upper-column-1 r +del-upper-column-1 lower-upper-column-1 l +del-upper-column-2 lower-upper-column-1 l +del-upper-column-3 lower-upper-column-1 l +del-upper-column-1 end-upper-column-1 r +del-upper-column-2 end-upper-column-1 r +del-upper-column-3 end-upper-column-1 r + +lower-upper-column-1 A d +lower-upper-column-2 B d +lower-upper-column-3 C d +lower-upper-column-1 - u +lower-upper-column-2 ? u +lower-upper-column-3 ! u + +del-upper-column-1 D d +del-upper-column-2 E d +del-upper-column-3 F d +del-upper-column-1 / u +del-upper-column-2 . u +del-upper-column-3 , u + +end-upper-column-1 G d +end-upper-column-2 H d +end-upper-column-3 I d +end-upper-column-1 space-upper-x6-y3 u +end-upper-column-2 space-upper-x7-y3 u +end-upper-column-3 space-upper-x8-y3 u + +space-upper-x8-y2 space-lower-x8-y2 select +space-upper-x8-y2 R u +space-upper-x8-y2 space-upper-x8-y3 d +space-upper-x8-y2 Z l +space-upper-x8-y2 S r + +space-upper-x8-y3 MN select +space-upper-x8-y3 space-upper-x8-y2 u +space-upper-x8-y3 end-upper-column-3 d +space-upper-x8-y3 space-upper-x7-y3 l +space-upper-x8-y3 - r + +space-upper-x7-y3 PK select +space-upper-x7-y3 Z u +space-upper-x7-y3 end-upper-column-2 d +space-upper-x7-y3 space-upper-x6-y3 l +space-upper-x7-y3 space-upper-x8-y3 r + +space-upper-x6-y3 ] select +space-upper-x6-y3 Y u +space-upper-x6-y3 end-upper-column-1 d +space-upper-x6-y3 , l +space-upper-x6-y3 space-upper-x7-y3 r + +end-upper-column-1 end-lower-column-1 select +end-upper-column-2 end-lower-column-2 select +end-upper-column-3 end-lower-column-3 select +lower-upper-column-1 lower-lower-column-1 select +lower-upper-column-2 lower-lower-column-2 select +lower-upper-column-3 lower-lower-column-3 select +del-upper-column-1 del-lower-column-1 select +del-upper-column-2 del-lower-column-2 select +del-upper-column-3 del-lower-column-3 select + +lower-lower-column-1 × u +lower-lower-column-2 ( u +lower-lower-column-3 ) u +lower-lower-column-1 a d +lower-lower-column-2 b d +lower-lower-column-3 c d + +end-lower-column-1 ] u +end-lower-column-2 PK u +end-lower-column-3 MN u +end-lower-column-1 g d +end-lower-column-2 h d +end-lower-column-3 i d + +del-lower-column-1 : u +del-lower-column-2 ; u +del-lower-column-3 [ u +del-lower-column-1 d d +del-lower-column-2 e d +del-lower-column-3 f d + +- × select +- S u +- lower-upper-column-1 d +- space-upper-x8-y3 l +- ? r + +? ( select +? T u +? lower-upper-column-2 d +? - l +? ! r + +! ) select +! U u +! lower-upper-column-3 d +! ? l +! / r + +/ : select +/ V u +/ del-upper-column-1 d +/ ! l +/ . r + +. ; select +. W u +. del-upper-column-2 d +. / l +. , r + +, [ select +, X u +, del-upper-column-3 d +, . l +, space-upper-x6-y3 r + +× - select +× s u +× upper-lower-column-1 d +× MN l +× ( r + +( ? select +( t u +( upper-lower-column-2 d +( × l +( ) r + +) ! select +) u u +) upper-lower-column-3 d +) ( l +) : r + +: / select +: v u +: del-lower-column-1 d +: ) l +: ; r + +; . select +; w u +; del-lower-column-2 d +; : l +; [ r + +[ , select +[ x u +[ del-lower-column-3 d +[ ; l +[ ] r + +] space-upper-x6-y3 select +] y u +] end-lower-column-1 d +] [ l +] PK r + +PK space-upper-x7-y3 select +PK z u +PK end-lower-column-2 d +PK ] l +PK MN r + +MN space-upper-x8-y3 select +MN space-lower-x8-y2 u +MN end-lower-column-3 d +MN PK l +MN × r + +space-lower-x8-y2 space-upper-x8-y2 select +space-lower-x8-y2 r u +space-lower-x8-y2 MN d +space-lower-x8-y2 z l +space-lower-x8-y2 s r + +a A select +a upper-lower-column-1 u +a j d +a i l +a b r + +b B select +b upper-lower-column-2 u +b k d +b a l +b c r + +c C select +c upper-lower-column-3 u +c l d +c b l +c d r + +d D select +d del-lower-column-1 u +d m d +d c l +d e r + +e E select +e del-lower-column-2 u +e n d +e d l +e f r + +f F select +f del-lower-column-3 u +f o d +f e l +f g r + +g G select +g end-lower-column-1 u +g p d +g f l +g h r + +h H select +h end-lower-column-2 u +h q d +h g l +h i r + +i I select +i end-lower-column-3 u +i r d +i h l +i a r + +j J select +j a u +j s d +j r l +j k r + +k K select +k b u +k t d +k j l +k l r + +l L select +l c u +l u d +l k l +l m r + +m M select +m d u +m v d +m l l +m n r + +n N select +n e u +n w d +n m l +n o r + +o O select +o f u +o x d +o n l +o p r + +p P select +p g u +p y d +p o l +p q r + +q Q select +q h u +q z d +q p l +q r r + +r R select +r i u +r space-lower-x8-y2 d +r q l +r j r + +s S select +s j u +s × d +s space-lower-x8-y2 l +s t r + +t T select +t k u +t ( d +t s l +t u r + +u U select +u l u +u ) d +u t l +u v r + +v V select +v m u +v : d +v u l +v w r + +w W select +w n u +w ; d +w v l +w x r + +x X select +x o u +x [ d +x w l +x y r + +y Y select +y p u +y ] d +y x l +y z r + +z Z select +z q u +z PK d +z y l +z space-lower-x8-y2 r""" + +for line in graph_data.split("\n"): +    if line == "": +        continue +    elif line[0] == "#": +        continue + +    (node1, node2, edge_name) = line.split(" ") +    graph.add_edge(node1, node2, key=edge_name) + +    #print "Adding edge ("+edge_name+") "+node1+" -> "+node2 + +def shortest_path(node1, node2): +    """ +    Figures out the shortest list of button presses to move from one letter to +    another. +    """ +    buttons = [] +    last = None +    path = networkx.shortest_path(graph, node1, node2) +    for each in path: +        if last != None: +            buttons.append(convert_nodes_to_button_press(last, each)) +        last = each +    return buttons +    #return [convert_nodes_to_button_press(node3, node4) for (node3, node4) in zip(*(iter(networkx.shortest_path(graph, node1, node2)),) * 2)] + +def convert_nodes_to_button_press(node1, node2): +    """ +    Determines the button necessary to switch from node1 to node2. +    """ +    print "getting button press for state transition: " + node1 + " -> " + node2 +    return graph.get_edge_data(node1, node2)["key"] + +def plan_typing(text, current="A"): +    """ +    Plans a sequence of button presses to spell out the given text. +    """ +    buttons = [] +    for target in text: +        if target == current: +            buttons.append("a") +        else: +            print "Finding the shortest path between " + current + " and " + target +            more_buttons = shortest_path(current, target) +            buttons.extend(more_buttons) +            buttons.append("a") +            current = target +    return buttons | 
