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
start_game()
- function handling Dolphin startup to get in-game- Different
Bot
classes to use or extend livemelee.inputs
module - framework for pressing buttonslivemelee.utils
module - misc, eg. pretty prints
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 callfunc_for_cmd('a','b','10')
But there's limitations because ofargparse
:- 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 thequit
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 withmelee.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)
-
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 usingwhen
anddo
.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