Source code for metrics.sc2metric

import math

import metrics.util


[docs]class Sc2MetricAnalyzer(object): """A class that derives useful metrics from replay files This class is an extension to the ``sc2reader`` player class. It is meant to be used to offer useful metric data to each player in a Starcraft II replay. The class contains raw data gathered from each replay to derive its metrics. The raw data is filled from the ``plugins`` associated with this package. Todo: - #TotalSupply : Total supply created by the player. (This is NOT current supply at given time) - #Workers : Total number of workers created by the player. - #Army : Total army supply created by the player. - #Upgrades : Total count of upgrades. (+1/2/3 weapons, psionic storm, charge, etc.) - TimeTo66Workers : Time at which the user created 66 workers (3-base saturation). - TimeTo75Workers : Time at which the user created 75 workers. - TimeTo3Bases : Time at which the 3 bases are finished. - TimeTo4Bases : Time at which the 4 bases are finished. - TimeToMax : Time at which the total supply created is 199 or above (stops counting workers above 75) - SupplyCapped : Time spent supply blocked. - #SupplyCreateRate : Average rate at which supply buildings are created until 200 supply - AvgAPM : Average APM - #AvgEPM : Average EPM - #AvgSPM : Average SPM - #AvgMacroCycleTime : Average time spent issuing macro commands (vs army commands) (or maybe the avg time between giving a worker a command and issuing another command not to the worker) - #AvgHarassDeflection : A score that rates the player's ability to deflect and minimize damage incurred from harassment attacks. (works off of minerals/probes/tech/mining time lost?) - #IdleBaseTime66 : Total time town halls are idle (not making workers) before 66 workers - #IdleBaseTime75 : Total time town halls are idle (not making workers) before 75 workers - AvgSQ : Spending Quotient. SQ(i,u)=35(0.00137i-ln(u))+240, where i=resource collection rate, u=average unspent resources - AvgSQPreMax : Spending Quotient before maxed out. - AvgUnspent : Average unspent resources during the game. - AvgUnspentPreMax : Average unspent resources before maxed out. - AvgColRate : Average resource collection rate during the game. - AvgColRatePreMax : Average resource collection rate before maxed out. - #Units : Dictionary of all the units created, keyed by the units' names. - #PPM : average(mean) PAC per minute - #PAL : PAC action latency. e.g.: how long it takes you to take your first action after each fixation shift. (mean average) - #APP : Actions per PAC. The average(mean) number of actions you take each PAC - #GAP : How long it takes you, after finishing your actions in one PAC to establish a new fixaction. (mean average) """ def __init__(self): """Initialization of the class""" #: A list of :class:`~metrics.metric_containers.SupplyCount` for each army supply created. self.army_created = [] #: A list of :class:`~metrics.metric_containers.SupplyCount` for each worker created. self.workers_created = [] #: A list of :class:`~metrics.metric_containers.SupplyCount` for each unit created. self.supply_created = [] #: A list of :class:`~metrics.metric_containers.BaseCount` for each base created. self.bases_created = [] #: A list of :class:`~metrics.metric_containers.FoodCount` for each unit and supply provider #: created. self.supply = [] #: A list of :class:`~metrics.metric_containers.ResourceCount` for resources collected and #: resources unspent every 10 seconds. self.resources = [] #: The player's average apm self.avg_apm = 0 #: The player's average spm self.avg_spm = 0
[docs] def metrics(self): """A dictionairy of basic metrics.""" return {'TimeToMax' : self.time_to_supply_created_max_workers(198, 75), 'TimeTo3Bases' : self.time_to_bases_created(3), 'TimeTo4Bases' : self.time_to_bases_created(4), 'TimeTo66Workers' : self.time_to_workers_created(66), 'TimeTo75Workers' : self.time_to_workers_created(75), 'AvgAPM' : self.avg_apm, 'AvgSQ' : self.avg_sq(), 'AvgSQPreMax' : self.avg_sq_pre_max(), 'AvgUnspent' : self.aur(), 'AvgUnspentPreMax' : self.aur_pre_max(), 'AvgColRate' : self.avg_rcr(), 'AvgColRatePreMax' : self.avg_rcr_pre_max(), 'SupplyCapped' : self.supply_capped() }
[docs] def first_max(self): """Returns the time at which the player first reached max supply.""" return next((sp.second for sp in self.supply if sp.supply_used >= 200), -1)
def _sq(self, resources): sum_res_col_rate = 0 sum_unspent_res = 0 for res in resources: sum_res_col_rate += res.res_col sum_unspent_res += res.res_unspent avg_col_rate = sum_res_col_rate / len(resources) avg_unspent = sum_unspent_res / len(resources) # SQ(i,u)=35(0.00137i-ln(u))+240, where i=resource collection rate, # u=average unspent resources sq = 35 * (0.00137 * avg_col_rate - math.log(avg_unspent)) + 240 return sq
[docs] def avg_sq(self): """Average Spending Quotient Calculates the average Spending Quotient (SQ). Returns: int: The average Spending Quotient (SQ). """ return self._sq(self.resources)
[docs] def avg_sq_at_time(self, time_s): """Average Spending Quotient up to a specified time Calculates the average Spending Quotient (SQ) up until the specified time. Args: time_s (int): The time in the replay to stop calculating spending quotient. Returns: int: The average Spending Quotient (SQ) up until the specified time. """ resources = list(filter(lambda res: res.second <= time_s, self.resources)) if len(resources) > 0: return self._sq(resources) else: return 0
[docs] def avg_sq_pre_max(self): """Average Spending Quotient before max supply Calculates the average Spending Quotient (SQ) up until the player first reaches max supply. Returns: int: The average Spending Quotient (SQ) up until the player first maxes. """ max_s = self.first_max() if max_s >= 0: return self.avg_sq_at_time(max_s) else: return self.avg_sq()
def _aur(self, resources): if len(resources) > 0: sum_aur = 0 for res in resources: sum_aur += res.res_unspent return sum_aur / len(resources)
[docs] def aur(self): """Average Unspent Resources Calculates the Average Unspent Resources (AUR) during the game. Returns: int: The Average Unspent Resources (AUR) """ return self._aur(self.resources)
[docs] def aur_at_time(self, time_s): """Averague Unspent Resources up to a specified time Calculates the Average Unspent Resources (AUR) up until the specified time. Args: time_s (int): The time in the replay to stop calculating average unspent resources. Returns: int: The Average Unspent Resources (AUR) up until the specified time. """ resources = list(filter(lambda res: res.second <= time_s, self.resources)) return self._aur(resources)
[docs] def aur_pre_max(self): """Average Unspent Resources before max supply Calculates the Average Unspent Resources (AUR) up until the player first reaches max supply. Returns: int: The Average Unspent Resources (AUR) up until the player first maxes. """ max_s = self.first_max() if max_s >= 0: return self.aur_at_time(max_s) else: return self.aur()
def _avg_rcr(self, resources): if len(resources) > 0: sum_rcr = 0 for res in resources: sum_rcr += res.res_col return sum_rcr / len(resources)
[docs] def avg_rcr(self): """Average Resource Collection Rate Calculates the average Resource Collection Rate (RCR) during the game. Returns: int: The average Resource Collection Rate (RCR) """ return self._avg_rcr(self.resources)
[docs] def avg_rcr_at_time(self, time_s): """Average Resource Collection Rate up to a specified time Calculates the average Resource Collection Rate (RCR) up until the specified time. Args: time_s (int): The time in the replay to stop calculating average resource collection rate. Returns: int: The average Resource Collection Rate (RCR) up until the specified time. """ resources = list(filter(lambda res: res.second <= time_s, self.resources)) return self._avg_rcr(resources)
[docs] def avg_rcr_pre_max(self): """Average Resource Collection Rate before max supply Calculates the average Resource Collection Rate (RCR) up until the player first reaches max supply. Returns: int: The average Resource Collection Rate (RCR) up until the player first maxes. """ max_s = self.first_max() if max_s >= 0: return self.avg_rcr_at_time(max_s) else: return self.avg_rcr()
[docs] def supply_capped(self): """Time spent supply capped Determines the amount of time the player was supply capped this game. Supply capped is acknowledged when supply of units = supply created when supply created < 200. Returns: int: The amount of time spent supply capped. """ supply_blocked = False time_of_supply_block = 0 sc = 0 for sp in self.supply: if (supply_blocked == False and sp.supply_used >= sp.supply_made and sp.supply_made < 200): supply_blocked = True time_of_supply_block = sp.second elif (supply_blocked == True and (sp.supply_used < sp.supply_made or sp.supply_made >= 200)): sc += (sp.second - time_of_supply_block) supply_blocked = False time_of_supply_block = 0 return sc
[docs] def supply_at_time(self, real_time_s): """The current supply used at the specified time. Calculates the current supply used at the time. Args: real_time_s (int): The time (in seconds) of the replay to count supply. Returns: int: The current amount of supply used in the given time. """ return next((sp.supply_used for sp in reversed(self.supply) if sp.second <= real_time_s), 0)
[docs] def first_time_to_supply(self, supply): """The first time that the specified supply was reached. Finds the time when the specified supply was reached. Args: supply (int): The supply desired. Returns: int: The time when the supply was first reached. """ return next((sp.second for sp in self.supply if sp.supply_used >= supply), -1)
[docs] def workers_created_at_time(self, time_s): """Number of workers created up to the specified time Determines the total number of workers created at the specified time. This function is accumulative and does not handle workers lost. Args: time_s (int): The time (in seconds) of the replay to count workers. Returns: int: The number of workers created. """ workers = 0 idx = 0 while (len(self.workers_created) > idx and self.workers_created[idx].second <= time_s): workers += 1 idx += 1 if (len(self.workers_created) > 0 and self.workers_created[0].second <= time_s): return workers else: return 0
[docs] def army_created_at_time(self, game_time_s): """The army supply created up to a specified time Calculate the total army supply created at the specified time. Args: game_time_s (int): The time (in seconds) of the replay to count army supply. Returns: int: The amount of army supply created in the given time. """ idx = 0 while (len(self.army_created) > idx and self.army_created[idx].second <= game_time_s): idx += 1 if (len(self.army_created) > 0 and self.army_created[0].second <= game_time_s): return self.army_created[idx-1].supply else: return 0
[docs] def supply_created_at_time(self, real_time_s): """The total supply created up to a specified time Calculate the total supply created at the specified time. Args: real_time_s (int): The time (in seconds) of the replay to count supply. Returns: int: The amount of supply created in the given time. """ idx = 0 while (len(self.supply_created) > idx and self.supply_created[idx].second <= real_time_s): idx += 1 if (len(self.supply_created) > 0 and self.supply_created[0].second <= real_time_s): return self.supply_created[idx-1].supply else: return 0
[docs] def time_to_workers_created(self, worker_count): """The time at which the number of workers had been created Finds the time that the specified number of workers have been created. This does not account for loss of workers. Args: worker_count (int): The number of workers to find the time created. Returns: int: The time at which the total number of workers were created. """ if worker_count > 0 and worker_count <= len(self.workers_created): return self.workers_created[worker_count-1].second else: return None
[docs] def time_to_supply_created(self, supply_count): """The time at which the specified supply had been created Finds the time when the specified supply was created. This does not take into account supply lost. Args: supply_count (int): The supply desired. Returns: int: The time when the supply was created in the replay. """ #supp = list(filter(lambda sp: sp.supply <= supply_count, # self.supply_created)) #supp = [] #for sp in self.supply_created: # if sp.supply < supply_count: # supp.append(sp) # elif sp.supply >= supply_count and len(supp) > 0: # supp.append(sp) # break # #if len(supp) > 0: # return supp[len(supp)-1].second #else: # return None for sc in self.supply_created: if sc.supply >= supply_count: return sc.second return None
[docs] def time_to_supply_created_max_workers(self, supply_count, max_workers_counted): """The time at which the specified supply had been created Finds the time when the specified supply was reached, but only counts a specified number of workers towards that supply created count. This does not take into account supply lost. Args: supply_count (int): The supply desired. max_workers_counted (int): The maximum number of workers to count towards the supply created. Returns: int: The time when the supply was created in the replay. """ if (supply_count <= 0 or (len(self.supply_created) > 0 and self.supply_created[len(self.supply_created)-1].supply < (supply_count - max_workers_counted))): return None if len(self.supply_created) == 0: return None supp = 0 workers = 0 for sc in self.supply_created: if not sc.is_worker or workers < max_workers_counted: supp += sc.unit_supply if supp >= supply_count: return sc.second if sc.is_worker: workers += 1 return self.supply_created[len(self.supply_created)-1].second
[docs] def time_to_bases_created(self, base_count): """The time at which the number of bases had been created Finds the time when the specified number of bases has been reached. This does not take into account any bases lost. Args: base_count (int): The number of bases desired. Returns: int: The time when the number of bases had been created. """ if base_count <= len(self.bases_created) and base_count > 0: return self.bases_created[base_count-1].second else: return None
## def units_created(self, real_time_s): ## if not 'UnitBornEvent' in self._events or not 'UnitInitEvent' in self._events: ## return 0 ## units_created = {"Probe": 0, "Zealot": 0, "Sentry": 0, "Stalker": 0, "Adept": 0, "HighTemplar": 0, ## "DarkTemplar": 0, "Archon": 0, "Observer": 0, "Immortal": 0, "Colossus": 0, ## "Disruptor": 0, "Phoenix": 0, "Oracle": 0, "VoidRay": 0, "Carrier": 0, "Tempest": 0} ## ## unit_born_events = self._events['UnitBornEvent'] ## unit_init_events = self._events['UnitInitEvent'] ## game_time_s = util.convert_to_gametime_r(self._replay, real_time_s) ## ## player_ube = list(filter(lambda ube: ube.control_pid == self._player_id, unit_born_events)) ## player_uie = list(filter(lambda uie: uie.control_pid == self._player_id, unit_init_events)) ## ## for ube in player_ube: ## if (ube.second <= game_time_s) and (ube.unit.name in units_created) and (not self._isHallucinated(ube.unit)): ## units_created[ube.unit.name] += 1 ## ## for uie in player_uie: ## if (uie.second <= game_time_s) and (uie.unit.name in units_created) and (not self._isHallucinated(uie.unit)): ## units_created[uie.unit.name] += 1 ## ## return units_created ## def encode_json(self): ## if isinstance(self, Sc2MetricAnalyzer): ## res_dict = {'__ResourceCount__': True} ## for res in self.resources: ## res_dict.update(res.to_dict()) ## ## ## ## return {'__Sc2MetricAnalyzer__': True, ## 'army_created': self.army_created, ## 'workers_created': self.workers_created, ## 'supply_created': self.supply_created, ## 'bases_created': self.bases_created, ## 'current_food_used': self.current_food_used, ## 'current_food_made': self.current_food_made, ## 'resources': self.resources, ## 'avg_apm': self.avg_apm ## } ## else: ## type_name = z.__class__.__name__ ## raise TypeError(f"Object of type '{type_name}' is not JSON serializable") ########## Testing ########### if __name__ == '__main__': ma = Sc2MetricAnalyzer("..\\test\\test_replays\\Year Zero LE (9).SC2Replay", 1)