Last active
April 25, 2022 22:52
-
-
Save dermorz/dc5082ac1252130ce4baf355cd735c39 to your computer and use it in GitHub Desktop.
Simple local Starcraft 2 replay analyser. Has some bugs but works for me. :)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
"""Simple Starcraft 2 replay analysis | |
When playing I run... | |
$ watch ./parse_replay.py | |
...which always shows me the data of the most recent replay. | |
""" | |
import glob | |
import os | |
import pathlib | |
import sys | |
from zephyrus_sc2_parser import parse_replay | |
from zephyrus_sc2_parser.exceptions import MissingMmrError | |
# Hardcoded Battle.net identity | |
# Find it in your Starcraft Folder (e.g. $HOME/Starcraft II/Accounts/<some number>/<TOON_HANDLE> | |
TOON_HANDLE = "2-S2-1-1122334" | |
# Not sure if this is correct. I Eyeballed the value from a test replay. | |
ONE_BASE_MINERALS = 940 | |
def get_player_id(players): | |
"""Determine our player id""" | |
for i, p in players.items(): | |
if f"{p.region_id}-S2-{p.realm_id}-{p.profile_id}" == TOON_HANDLE: | |
return p.player_id | |
# Fallback if neither player is us (parsing replay from a friend) | |
choice = input(f"1 ({players[1].name}) or 2 ({players[2].name}): ") | |
if choice not in ("1", "2"): | |
print("Valid values: 1 or 2, dummy.") | |
sys.exit(1) | |
return int(choice) | |
def timestamp(frame): | |
"""Convert starcraft time unit into human readable timestamp""" | |
if frame < 0: | |
return "n/a" | |
seconds = round(frame / 22.4, 1) | |
return f"{int(seconds // 60)}:{int(seconds % 60):02}" | |
def analyze(replay): | |
"""HUGE hacky method to do all the stuff. Getting data and printing it.""" | |
players, timeline, engagements, summary, metadata = replay | |
player_id = get_player_id(players) | |
for p in players.values(): | |
print( | |
f"{p.name:<20}{p.race:<10}{summary['mmr'][p.player_id] or 'Game vs AI':>10}" | |
) | |
print() | |
print( | |
f"{'Duration:':<25}{replay.metadata['game_length']//60}:{replay.metadata['game_length']%60:0>2}" | |
) | |
print(f"{'Supply Block:':<20}{int(summary['supply_block'][player_id]):>9}s") | |
print( | |
f"{'Average idle larva:':<20}{summary['race'][player_id]['avg_idle_larva']:>10}" | |
) | |
inject_efficiency = summary["race"][player_id]["inject_efficiency"] | |
avg_inject_efficiency = sum(hatch[0] for hatch in inject_efficiency) / len( | |
inject_efficiency | |
) | |
print(f"{'Inject efficiency:':<20}{avg_inject_efficiency:>10.2%}") | |
print(f"{'SQ:':<20}{summary['sq'][player_id]:>10}") | |
print(f"{'Workers created:':<20}{summary['workers_produced'][player_id]:>10}") | |
print() | |
maxed_at = "n/a" | |
sixtysix_workers_at = "n/a" | |
one_base_saturation_at = "n/a" | |
two_base_saturation_at = "n/a" | |
three_base_saturation_at = "n/a" | |
supply_counts = { | |
4707: None, # 3:30 | |
5376: None, # 4:00 | |
6720: None, # 5:00 | |
8072: None, # 6:00 | |
} | |
for tick in timeline: | |
now = tick[player_id] | |
if maxed_at == "n/a" and now["supply"] >= 199: | |
maxed_at = timestamp(now["gameloop"]) | |
if sixtysix_workers_at == "n/a" and now["workers_active"] >= 66: | |
sixtysix_workers_at = timestamp(now["gameloop"]) | |
if ( | |
one_base_saturation_at == "n/a" | |
and now["resource_collection_rate"]["minerals"] >= ONE_BASE_MINERALS | |
): | |
one_base_saturation_at = timestamp(now["gameloop"]) | |
if ( | |
two_base_saturation_at == "n/a" | |
and now["resource_collection_rate"]["minerals"] >= 2 * ONE_BASE_MINERALS | |
): | |
two_base_saturation_at = timestamp(now["gameloop"]) | |
if ( | |
three_base_saturation_at == "n/a" | |
and now["resource_collection_rate"]["minerals"] >= 3 * ONE_BASE_MINERALS | |
): | |
three_base_saturation_at = timestamp(now["gameloop"]) | |
for k, v in supply_counts.items(): | |
if now["gameloop"] <= k <= now["gameloop"] + 50 and v is None: | |
supply_counts[k] = { | |
"supply": f"{str(now['supply'])+'/'+str(now['supply_cap']):>7}", | |
"workers": now["workers_active"], | |
} | |
for k, v in supply_counts.items(): | |
print(f"{timestamp(k)} " f"{v['supply']} " f"({v['workers']} Workers)") | |
print() | |
print(f"{'66 Workers at:':<33}{sixtysix_workers_at}") | |
print(f"{'Maxed at:':<33}{maxed_at}") | |
print(f"{'1 base saturation (minerals):':<33}{one_base_saturation_at}") | |
print(f"{'2 base saturation (minerals):':<33}{two_base_saturation_at}") | |
print(f"{'3 base saturation (minerals):':<33}{three_base_saturation_at}") | |
if __name__ == "__main__": | |
if len(sys.argv) < 2: | |
# Pick the latest replay file matching ~/replays/*.SC2Replay | |
# You might want to adjust this to your replay folder. | |
replays = glob.glob(os.path.join(pathlib.Path.home(), "replays", "*.SC2Replay")) | |
replay_file = max(replays, key=os.path.getctime) | |
else: | |
# or the replay file passed as argument. | |
replay_file = sys.argv[1] | |
try: | |
replay = parse_replay(replay_file, tick=22.4, network=False) | |
except MissingMmrError: | |
# Game vs AI | |
replay = parse_replay(replay_file, tick=22.4, local=True, network=False) | |
except Exception as e: | |
print("Could not load replay file:", e) | |
sys.exit(1) | |
analyze(replay) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment