Package livemelee

An easier way to develop a SSBM bot. Built off libmelee.

This package aims to make a simpler API around libmelee and make it easier to start implementing your gameplay ideas.

Before you start

Make sure you follow the brief setup outlined in the repo - mostly just be sure to have the necessary gecko codes for Dolphin. If you still can't run the quick example, leave an issue on github.

Quick Example

# main.py
from livemelee import start_game, Bot
start_game((Bot(), Bot(), None, None))

python main.py "path/to/dolphin/" "path/to/iso"

Less trivial example, a breakdown

A typical development case might look like:

  • Create a bot and implement its main loop method:
# from livemelee import InputsBot
class MyBot(InputsBot):

    def check_frame(self, gamestate):
        if gamestate.frame % 120 == 0:
            self.make_some_inputs()
  • … by telling it when to make what inputs:
# from livemelee.inputs import *
    def make_some_inputs(self):
        self.queue = [
            (down,),
            (B,),
            (release,),
            *wait(5),
            *taunt(),
        ]
  • Define some commands for the interactive thread:
bot = MyBot()

commands = {
    'i': (bot.set_some_inputs, 'make inputs now'),
    'next': (lambda: bot.queue[0], 'print next input queued'),
}
  • Put bots in ports and start:
# from livemelee import start_game
start_game((None, bot, None, None), commands)
  • Run on command line, passing dolphin path and iso path: python main.py "path/to/dolphin/executable" "path/to/ssbm/iso"

Components of livemelee

A deeper look

Behind the scenes, start_game() implements Dolphin startup with libmelee. Additionally, you pass objects (bots) in the "ports" so they can get a controller and access the game state. It looks something like this:

# essentially in start_game(bot, None, None, None):

bot.controller = Controller()
...
while True:
  gamestate = dolphin.next_frame()
  bot.act(gamestate)

You could pass in something besides a bot if you just want access to the gamestate. Eg: humans play in ports 1,2 so a realtime parser in port 3 or 4 can make callbacks according to in-game triggers.

The base class Bot implements act with menu navigation (from libmelee) so only play_frame is to be implemented for actual gameplay. Other Bot subclasses scaffold off that, such as how InputsBot gives you check_frame so you can perform inputs.


The other feature of start_game() is realtime commands. After start_game() is called and opens Dolphin, a thread is opened in your terminal for typing commands. There's two default commands: help, which shows available commands, and quit, which closes the thread and Dolphin. You can make new commands with the format command: function or command: (function, description). The result of function() is printed when command is entered.

Notes on commands:

  • After a command, a period . is printed on a newline as a visual confirmation.
  • You could make a command function that takes multiple args. eg.
    >>> cmd a b 10 would call func_for_cmd('a','b','10')
    But there's limitations because of argparse:

    • args can't start with "-" (RIP negatives) (because it triggers parser unrecognized flag)
    • args will always be strings
  • quit causes some not-so-pretty error output, but Dolphin still closes fine.

  • Ctrl-C keyboard interrupt won't close cleanly (Dolphin will stay open). You should only use the quit command.

Notes on running the script: - example: python main.py "melee\stuff\FM-Slippi-2.3.0-Win\FM-Slippi\Slippi Dolphin.exe" "melee\stuff\Super Smash Bros. Melee (USA) (En,Ja) (v1.02).iso" - Dolphin path is to the executable, as opposed to the containing folder that libmelee wants. (I think it feels more intuitive this way) - ISO path is optional if you have a default iso set to run on Dolphin startup - Recommended - copy this command into a little script for quick reference in your working directory, eg. run.bat

More

Only offline play is supported for now, mainly assuming ports 1 + 2. Soon to change!

Read up on the API that livemelee wraps around: libmelee - get to know GameState, PlayerState, Controller, etc.

Expand source code
'''An easier way to develop a SSBM bot. Built off libmelee.

.. include:: ./documentation.md'''

# don't want to clutter namespace with their many members
from . import utils
from . import inputs

from .startup import start_game
# from .interact import LiveGameStats
from .bots import Bot, InputsBot, CheckBot, ControllableBot

# __pdoc__ = {obj: True for obj in (
#     'start_game',
#     'LiveInputsThread', 'LiveGameStats',
#     'Bot', 'InputsBot', 'CheckBot', 'ControllableBot',
#     'buttons',
#     'Stat',
#     'Inputs'
# )}

__all__ = [
    'start_game',
    # 'LiveGameStats',
    'Bot', 'InputsBot', 'CheckBot', 'ControllableBot',
    'inputs',
    'utils',
]

__pdoc__ = {
    'interact': False,
    'startup': False,
}

Sub-modules

livemelee.bots

Collection of bot classes to extend.

livemelee.inputs

Wrapping melee.Button enums for easy sequences and consumption (see usage in bots) …

livemelee.utils

Group of functions returning helpful stats, mainly info from gamestates …

Functions

def start_game(ports, cmds={}, log=True)

Main method to fully start game. Command-line first needs dolphin path and iso path, then game starts. Iso path is optional if you have a default iso set to run on Dolphin startup.

# main.py
...
start_game(...)

python main.py path/to/dolphin path/to/iso

Args

ports

tuple containing 4 bot instances or Nones.

eg. (None, my_bot, None, None)

cmds

optional.

  • dict: of custom commands 'cmd': (func, 'descrip') or 'cmd': func.
  • default: empty dict, no custom commands
  • None: no live thread desired (probably for performance)
log
bool, write game logs to file with melee.Logger if True (default)
Expand source code
def start_game(ports, cmds={}, log=True):
    '''Main method to fully start game.
    Command-line first needs dolphin path and iso path, then game starts.
    Iso path is optional if you have a default iso set to run on Dolphin startup.
    ```
    # main.py
    ...
    start_game(...)
    ```
    `python main.py path/to/dolphin path/to/iso`

    Args:
        ports: tuple containing 4 bot instances or Nones.

            eg. `(None, my_bot, None, None)`

        cmds: optional.

            - `dict`: of custom commands `'cmd': (func, 'descrip')` or `'cmd': func`.
            - default: empty dict, no custom commands
            - `None`: no live thread desired (probably for performance)

        log: `bool`, write game logs to file with `melee.Logger` if True (default)'''

    args = _start_command_line()
    dolphin_folder = str( Path(args.dolphin_path).parent )
    console = melee.Console(path=dolphin_folder) # libmelee wants the folder

    # controllers must be connected before console run/connect...
    bots = _assign_controllers(ports, console)

    console.run(iso_path=args.iso_path) # if None, relies on default Dolphin iso on startup
    console.connect()

    # ... and then controllers are connected afterward
    _connect_controllers(bots)

    logger = melee.Logger() if log else None

    live_interface = None
    if cmds is not None:
        live_interface = LiveInputsThread(commands=cmds)
        live_interface.onshutdown = _shutdown(console, logger)
        live_interface.start()

    while True:

        gamestate = console.step()
        if not gamestate:
            break

        for bot in bots:
            bot.act(gamestate)

        if live_interface:
            live_interface.update(gamestate)

        if logger:
            logger.logframe(gamestate)
            logger.log('Frame Process Time', console.processingtime)   # ms
            logger.writeframe()

Classes

class Bot (controller=None, character=Character.FOX, stage=Stage.FINAL_DESTINATION)

Framework for making controller inputs according to gamestate. Offline only implementation currently.

Attributes

controller
melee.Controller
character
melee.Character
stage
melee.Stage
Expand source code
class Bot:
    '''Framework for making controller inputs according to gamestate.
    Offline only implementation currently.

    Attributes:
        controller: melee.Controller
        character: melee.Character
        stage: melee.Stage'''

    def __init__(self, controller=None,
                 character=DEFAULT_CHAR,
                 stage=DEFAULT_STAGE):
        self.controller = controller
        self.character = character
        self.stage = stage

    def act(self, gamestate):
        '''Main function called each frame of game loop with updated gamestate.'''

        # if gamestate.menu_state in (melee.Menu.IN_GAME,
        #                             melee.Menu.SUDDEN_DEATH):
        if utils.in_game(gamestate):
            self.play_frame(gamestate)  # rand note, paused wont advance frame
        else:
            self.menu_nav(gamestate)

    def menu_nav(self, gamestate):
        '''Processes menus with given character, stage.'''
        melee.MenuHelper.menu_helper_simple(gamestate,
                                            self.controller,
                                            self.character,
                                            self.stage,
                                            '', # connect code
                                            0,  # cpu_level (0 for N/A)
                                            0,  # costume
                                            autostart=True)

    def play_frame(self, gamestate):
        '''Bot game logic implemented here.'''
        pass

Subclasses

Methods

def act(self, gamestate)

Main function called each frame of game loop with updated gamestate.

Expand source code
def act(self, gamestate):
    '''Main function called each frame of game loop with updated gamestate.'''

    # if gamestate.menu_state in (melee.Menu.IN_GAME,
    #                             melee.Menu.SUDDEN_DEATH):
    if utils.in_game(gamestate):
        self.play_frame(gamestate)  # rand note, paused wont advance frame
    else:
        self.menu_nav(gamestate)
def menu_nav(self, gamestate)

Processes menus with given character, stage.

Expand source code
def menu_nav(self, gamestate):
    '''Processes menus with given character, stage.'''
    melee.MenuHelper.menu_helper_simple(gamestate,
                                        self.controller,
                                        self.character,
                                        self.stage,
                                        '', # connect code
                                        0,  # cpu_level (0 for N/A)
                                        0,  # costume
                                        autostart=True)
def play_frame(self, gamestate)

Bot game logic implemented here.

Expand source code
def play_frame(self, gamestate):
    '''Bot game logic implemented here.'''
    pass
class CheckBot (controller=None, character=Character.FOX, stage=Stage.FINAL_DESTINATION)

Adds condition checker to main loop.

Attributes

when
(ie trigger) a condition checked every frame (func taking gamestate)
do
(ie on_trigger) func called when condition returns True

By default, stops checking upon reaching condition. set_timer() is an example of using when and do.

Eg. self.repeat(when=self.finished_inputs, do=some_func)

Expand source code
class CheckBot(InputsBot):
    '''Adds condition checker to main loop.

    Attributes:
        when: (ie trigger) a condition checked every frame (func taking gamestate)

        do: (ie on_trigger) func called when condition returns True

    By default, stops checking upon reaching condition.
    `set_timer()` is an example of using `when` and `do`.

    Eg.
    `self.repeat(when=self.finished_inputs, do=some_func)`'''

    def __init__(self, controller=None,
                 character=DEFAULT_CHAR,
                 stage=DEFAULT_STAGE):
        super().__init__(controller, character, stage)

        self.when = never
        self.do = lambda:None
        self._max_time = 30  # arbitrary init
        self._timer = self._max_time

    def check_frame(self, gamestate):
        '''Called each frame to check gamestate (and/or possibly self?) for condition,
        stopping check when True.'''
        if self.when(gamestate):
            self.when = never
            self.do()

    def set_timer(self, n, do, repeat=True):
        '''Set all required timer functions:
        n frames to wait, timer condition, callback.'''
        self._max_time = n
        self._timer = self._max_time
        if repeat:
            self.repeat(when=self._times_up,
                        do=do)
        else:
            self.when = self._times_up
            self.do = do

    def _times_up(self, gamestate):
        '''A condition check that ticks timer, returning True on expire.'''
        if self._timer > 0:
            self._timer -= 1
            return False
        else:
            self._timer = self._max_time
            return True

    def repeat(self, when, do):
        '''Keeps checking when condition (as opposed to the default stop checking).'''
        def do_and_wait_again():
            do()
            self.when = when
        self.when = when
        self.do = do_and_wait_again

    def finished_inputs(self, gamestate):
        '''A condition to loop inputs by returning True when queue is empty.

        Eg.
        ```
        self.when = self.finished_inputs
        self.do = something
        ```'''
        return len(self.queue) == 0

Ancestors

Subclasses

Methods

def check_frame(self, gamestate)

Called each frame to check gamestate (and/or possibly self?) for condition, stopping check when True.

Expand source code
def check_frame(self, gamestate):
    '''Called each frame to check gamestate (and/or possibly self?) for condition,
    stopping check when True.'''
    if self.when(gamestate):
        self.when = never
        self.do()
def finished_inputs(self, gamestate)

A condition to loop inputs by returning True when queue is empty.

Eg.

self.when = self.finished_inputs
self.do = something
Expand source code
def finished_inputs(self, gamestate):
    '''A condition to loop inputs by returning True when queue is empty.

    Eg.
    ```
    self.when = self.finished_inputs
    self.do = something
    ```'''
    return len(self.queue) == 0
def repeat(self, when, do)

Keeps checking when condition (as opposed to the default stop checking).

Expand source code
def repeat(self, when, do):
    '''Keeps checking when condition (as opposed to the default stop checking).'''
    def do_and_wait_again():
        do()
        self.when = when
    self.when = when
    self.do = do_and_wait_again
def set_timer(self, n, do, repeat=True)

Set all required timer functions: n frames to wait, timer condition, callback.

Expand source code
def set_timer(self, n, do, repeat=True):
    '''Set all required timer functions:
    n frames to wait, timer condition, callback.'''
    self._max_time = n
    self._timer = self._max_time
    if repeat:
        self.repeat(when=self._times_up,
                    do=do)
    else:
        self.when = self._times_up
        self.do = do

Inherited members

class ControllableBot (controller=None, character=Character.FALCO, stage=Stage.FINAL_DESTINATION)

Designed to easily control externally in real time, eg. from live thread or perhaps something like a chat.

Attributes

commands
dict of {'cmd': (func, 'description')} See LiveInputsThread for details.
Expand source code
class ControllableBot(InputsBot):
    '''Designed to easily control externally in real time,
    eg. from live thread or perhaps something like a chat.

    Attributes:
        commands: dict of `{'cmd': (func, 'description')}`
            See LiveInputsThread for details.'''

    def __init__(self, controller=None,
                 character=melee.Character.FALCO,
                 stage=DEFAULT_STAGE):
        super().__init__(controller, character, stage)

        self.commands = self._init_commands()
        self._curr_sequence = []

    def _init_commands(self):

        commands = {cmd: self._set_seq(make_seq) for cmd, make_seq in {
            'laser': Inputs.laser,
            'sh': Inputs.shorthop,
            'shlaser': Inputs.jump_n_laser,  #fastfall_laser_rand
            'taunt': Inputs.taunt,
            'shield': Inputs.shield,
            'dd': Inputs.dashdance,
        }.items()}
        commands.update({cmd: self._set_seq(_make_seq(btn)) for cmd, btn in {
            'release': Inputs.release,
            'center': Inputs.center,
            'down': Inputs.down,
            'up': Inputs.up,
            'left': Inputs.left,
            'right': Inputs.right,
            'A': Inputs.A,
            'B': Inputs.B,
            'Y': Inputs.Y,
            'L': Inputs.L
        }.items()})
        # commands.update({
        #     'undo': self.release_last,
        # })
        return commands

    def _set_seq(self, make_seq):
        # wrapper to set current sequence to result of sequence maker func
        return lambda: self.set_curr_seq(make_seq())

    def set_curr_seq(self, inputs):
        self._curr_sequence = inputs# [(Inputs.release,), *inputs]

    def add_to_queue(self, inputs):
        # add to any existing inputs (usually would replace them)
        self.queue.extend(inputs)

    def check_frame(self, gamestate):
        # keep doing current sequence, looping if finished

        if len(self.queue) == 0:
            self.perform(self._curr_sequence)

Ancestors

Methods

def add_to_queue(self, inputs)
Expand source code
def add_to_queue(self, inputs):
    # add to any existing inputs (usually would replace them)
    self.queue.extend(inputs)
def set_curr_seq(self, inputs)
Expand source code
def set_curr_seq(self, inputs):
    self._curr_sequence = inputs# [(Inputs.release,), *inputs]

Inherited members

class InputsBot (controller=None, character=Character.FOX, stage=Stage.FINAL_DESTINATION)

Adds inputs queue to Bot.

Inputs should be always put into queue, never called directly/instantly with controller. First queued input will happen same frame of queueing.

Attributes

queue
list of inputs as outlined in inputs.py
Expand source code
class InputsBot(Bot):
    '''Adds inputs queue to Bot.

    Inputs should be always put into queue,
    never called directly/instantly with controller.
    First queued input will happen same frame of queueing.

    Attributes:
        queue: list of inputs as outlined in inputs.py'''

    def __init__(self, controller=None,
                 character=DEFAULT_CHAR,
                 stage=DEFAULT_STAGE):
        super().__init__(controller, character, stage)
        self.queue = []

    def play_frame(self, gamestate):
        self.check_frame(gamestate)
        self.consume_next_inputs()

    def consume_next_inputs(self):
        '''Called each frame to press or release next buttons in queue.
        See inputs.py for expected inputs format.'''
        if self.queue:
            inputs = self.queue.pop(0)
            Inputs.make_inputs(inputs, self.controller)

    def perform(self, input_sequence):
        '''Set queue to a sequence of inputs.
        Useful in lambdas where assignment is not allowed.'''
        self.queue = list(input_sequence)  # need a (deep) copy for modifiable lists/tuples

    def check_frame(self, gamestate):
        '''Override this (instead of overriding play_frame).
        Decision making and input queueing happen here.'''
        pass

Ancestors

Subclasses

Methods

def check_frame(self, gamestate)

Override this (instead of overriding play_frame). Decision making and input queueing happen here.

Expand source code
def check_frame(self, gamestate):
    '''Override this (instead of overriding play_frame).
    Decision making and input queueing happen here.'''
    pass
def consume_next_inputs(self)

Called each frame to press or release next buttons in queue. See inputs.py for expected inputs format.

Expand source code
def consume_next_inputs(self):
    '''Called each frame to press or release next buttons in queue.
    See inputs.py for expected inputs format.'''
    if self.queue:
        inputs = self.queue.pop(0)
        Inputs.make_inputs(inputs, self.controller)
def perform(self, input_sequence)

Set queue to a sequence of inputs. Useful in lambdas where assignment is not allowed.

Expand source code
def perform(self, input_sequence):
    '''Set queue to a sequence of inputs.
    Useful in lambdas where assignment is not allowed.'''
    self.queue = list(input_sequence)  # need a (deep) copy for modifiable lists/tuples

Inherited members