/py-slippi

Python library for parsing SSBM replay files

Primary LanguagePythonMIT LicenseMIT

py-slippi-stats

This is not 1:1 compatible with py-slippi and does not have py-slippi as a dependancy. It's based on the py-slippi parser, but I've made several breaking changes to the API that require >=python 3.10. Some names and structures are different as well (e.g. "damage" -> "percent", Metadata.Player.Netplay class has been flattened into Metadata.Player, etc.)

Py-slippi is a Python parser for .slp game replay files for Super Smash Brothers Melee for the Nintendo GameCube. These replays are generated by Jas Laferriere's Slippi recording code, which runs on a Wii or the Dolphin emulator.

Usage

Combos

Combo output works using a ComboComputer object - similarly to the official Slippi-js parser, but with altered logic for higher quality output. See combo_example.py, py for simple multiprocessed example. Additionally, the combo detection logic is pretty heavily commented for those interested in the nuts and bolts. Everything should be pretty easy to modify for those looking to make bespoke detection logic.

To create the object that handles combo computing:

example = ComboComputer()

To load parse the replay and load the relevant data into the object:

example.prime_replay(Path)

To generate the list of combos for a specific slippi connect code. At the moment, the connect code is NOT optional. Additional optional keyword arguments allow toggling of the various checks that extend a combo. All are boolean flags set to True by default (hitstun_check, hitlag_check, tech_check, downed_check, offstage_check, dodge_check, shield_check, shield_break_check, ledge_check)

example.combo_compute("CODE#123")

Filter these combos for desired criteria (helper functions included), for example, ever combo that killed that: have more than 5 hits and does more than 40%, or does more than 70%:

for c in example.combos:
    if(
        c.did_kill and
        (c.minimum_length(5) and c.minimum_damage(40)) or
        (c.minimum_damage(60))
        ):

You can also reference the original parsed replay's metadata - contained within the StatsComputer object - to filter for character, stage, date range, opponent name/character, ranked/unranked, etc.

combo_example.py exports this filtered list to json format that is compatible with Project Clippi, and can be loaded in for playback with no fuss.

Combos are stored as a ComboData object (see: slippi/combo.py). Combo data includes the connect code of the player, a list of the moves used, a boolean flag for a kill, the start/end percent, the start/end frame, the death direction, and stock counts for both players. This allows for a variety of precise filtering options such as: combos contain/do not contain one or multiple instances of a move, filtering for the first, second, etc. or final move of a combo, filtering for combo frame-length, and more.

Stats

Stats calculation works through a StatsComputer, much like Combos in the previous section.

To create the object that handles stat computing:

example = StatsComputer()

To load parse the replay and load the relevant data into the object:

example.prime_replay(Path)

To generate the stats, either a specific stat function can be called or all available stat functions can be called. The latter also has default keyword args to selectively disable stat calculations

example.wavedash_compute("CODE#123")

example.take_hit_compute("CODE#123")

example.stats_compute("CODE#123", tech=False)

Stats can be accessed at any time through the .data member of the StatsComputer. .data contains lists of stat-objects which can be iterated over and filtered similarly to combo objects. The __dict__ of these objects can be used to output to Polars or Pandas dataframes. See stats_example.py for more info.

Basic Parsing Overview

py-slippi-stats supports both event-based and object-based parsing. Object-based parsing is generally easier, but event-based parsing is more efficient and supports reading partial or in-progress games.

Object-based parsing::

    >>> from slippi import Game
    >>> Game('test/replays/game.slp')
    Game(
        end=End(
            lras_initiator=None,
            method=Method.CONCLUSIVE),
        frames=[...](5209),
        metadata=Metadata(
            console_name=None,
            date=2018-06-22 07:52:59+00:00,
            duration=5209,
            platform=Platform.DOLPHIN,
            players=(
                Player(
                    characters={InGameCharacter.MARTH: 5209},
                    netplay_name=None),
                Player(
                    characters={InGameCharacter.FOX: 5209},
                    netplay_name=None),
                None,
                None)),
        start=Start(
            is_frozen_ps=None,
            is_pal=None,
            is_teams=False,
            players=(
                Player(
                    character=CSSCharacter.MARTH,
                    costume=3,
                    stocks=4,
                    tag=,
                    team=None,
                    type=Type.HUMAN,
                    ucf=UCF(
                        dash_back=DashBack.OFF,
                        shield_drop=ShieldDrop.OFF)),
                Player(
                    character=CSSCharacter.FOX,
                    costume=0,
                    stocks=4,
                    tag=,
                    team=None,
                    type=Type.CPU,
                    ucf=UCF(
                        dash_back=DashBack.OFF,
                        shield_drop=ShieldDrop.OFF)),
                None,
                None),
            random_seed=3803194226,
            slippi=Slippi(
                version=1.0.0),
            stage=Stage.YOSHIS_STORY))

Frame data is elided when you print games, but you can inspect a sample frame with e.g. :code:game.frames[0].

    >>> from slippi import Game
    # pass `skip_frames=True` to skip frame data, for a significant speedup
    >>> Game('test/replays/game.slp', skip_frames=True)

Event-driven API::

    >>> from slippi.parse import parse
    >>> from slippi.parse import ParseEvent
    >>> handlers = {ParseEvent.METADATA: print}
    >>> parse('test/replays/game.slp', handlers)
    Metadata(
        console_name=None,
        date=2018-06-22 07:52:59+00:00,
        duration=5209,
        platform=Platform.DOLPHIN,
        players=(
            Player(
                characters={InGameCharacter.MARTH: 5209},
                netplay_name=None),
            Player(
                characters={InGameCharacter.FOX: 5209},
                netplay_name=None),
            None,
            None))

👉 You can pass a stream to :code:parse, such as :code:sys.stdin.buffer! This is useful for e.g. decompressing with :code:gunzip, or reading from an in-progress replay via :code:tail -c+1 -f.