Introduction
In this chapter, I explore how to use a Replay
's PlayerStatsEvent
data to infer a player's macroeconomic performance indicators in a match. Based on this indicators I export the module macro_econ_parser
, which exports functions that facilitate the rapid extraction of thsi indicators from a Replay
.
Exportable Members
Helper Fucntions
Primary function
PlayerStatsEvents
Of the different types of TrackerEvents
(see <<Chapter 2 - Handling Tracker Events>>), PlayerStatsEvents
contain several pieces of information I can use as the players' macroeconomic performance indicators in a match.
In each Replay
, StarCraft II registers a PlayerStatsEvent
for each player every ten seconds, regardless of the player's presence in the match. In other words, if a player leaves a game without terminating it, the Replay
still recordsPlayerStatsEvents
for that player up until the end of the match.
Additionally, the game generates a PlayerStatEvent
when the game starts, each time a player leaves (i.e., is defeated or surrenders) and when the match ends.
rps_path = Path("./test_replays")
game_path = str(rps_path/"Jagannatha LE.SC2Replay")
single_replay = sc2reader.load_replay(game_path)
single_replay
match_events = [event for event in single_replay.events]
tracker_e = [event for event in single_replay.events
if isinstance(event, sc2reader.events.tracker.TrackerEvent)]
PlayerStatEvent
s per player, plus two or three for start and end events. However, below I show that there are 85 such events instead of the 59 or 60 there should be. In <<Chapter 2 - Handling Tracker Events>>, I discuss this discrepancy in and how I address it with the calc_realtime_index
function. Meanwhile, in the following code, I will demonstrate how I use this function in the specific case of PlayerStatsEvents
.print(f'{"Game length in seconds:":<27}',
f'{single_replay.game_length.seconds:>6}')
print(f'{"Game frames per second:":<27}',
f'{single_replay.game_fps:>6}')
print(f'{"Total frames in the match:":<27}',
f'{single_replay.game_length.seconds * single_replay.game_fps:>6}')
p_one_state = [event for event in tracker_e
if isinstance(event, sc2reader.events.tracker.PlayerStatsEvent)
and event.pid == 1]
print(f'Number of PlayerStatsEvents for player 1: {len(p_one_state)}')
Similarly, looking at the seconds and frame count registered in the last event, it is clear that they do not match the duration of the game (590 seconds, 9440 frames).
print(f'Second recorded in the last event: ',
f'{p_one_state[len(p_one_state)-1].second}')
print(f'Frame recorded in the last event: ',
f'{p_one_state[len(p_one_state)-1].frame}')
Plotting the graph for the army value in two different ways, I can demonstrate how calc_realtime_index
can correct the time indexing discrepancy. I use the army value given that it is usually used as one of the most common measures of a player's performance. Meanwhile, comparing these graphs shows how this solution changes the information stored in these events.
First, I plot this value with the time registered in the events as the graph's horizontal axis.
pd.set_option('display.precision', 2)
# Calculate the army value (a_value), adding the amounts of minerals and
# vespene spent in the player's active army at each point during the game.
a_mineral_value = np.array([e.minerals_used_active_forces
for e in p_one_state])
a_vespene_value = np.array([e.vespene_used_active_forces
for e in p_one_state])
a_value = a_mineral_value + a_vespene_value
# Generate a DataFrame of the army value at each point in the game using
# the seconds as an index.
pstats_noprocess = pd.DataFrame({'second': [e.second for e in p_one_state],
'army_value': a_value})
pstats_noprocess.set_index('second', inplace=True)
#print(pstats_noprocess.tail(10).to_markdown())
The following table shows the last ten army_value records indexed according to the seconds registered in the PlayerStatsEvent
.
second | army_value |
---|---|
750 | 0 |
760 | 0 |
770 | 350 |
780 | 0 |
790 | 0 |
800 | 0 |
810 | 350 |
820 | 0 |
826 | 0 |
826 | 0 |
x_values = [x for x in pstats_noprocess.index if x % 50 == 0]
pstats_noprocess['army_value'].plot(figsize= (10,5), xticks= x_values,
xlabel='Time Index Recorded in Events',
ylabel='Army Value')
Next, I recalculate the index of the DataFrame based on the actual duration of the match.
# Establish the new second count as index.
pstats_change_seconds = pstats_noprocess.reset_index()
pstats_change_seconds['real_time'] = [
calc_realtime_index(event.second,
single_replay)
for event in p_one_state]
pstats_change_seconds.set_index('real_time', inplace=True)
# print(pstats_change_seconds.tail(10).to_markdown())
The following table shows the difference in the last ten army_value records between the original time index recorded in the PlayerStatsEvent
and the time indexes calculated according to replay's length.
real_time | second | army_value |
---|---|---|
535.714 | 750 | 0 |
542.857 | 760 | 0 |
550 | 770 | 350 |
557.143 | 780 | 0 |
564.286 | 790 | 0 |
571.429 | 800 | 0 |
578.571 | 810 | 350 |
585.714 | 820 | 0 |
590 | 826 | 0 |
590 | 826 | 0 |
While the difference in the index is notable, note that the form of the graph remains the same.
real_x_ticks = np.arange(0,590,50)
# Create the plot with the new time index
pstats_change_seconds['army_value'].plot(figsize= (10,5),
xticks= real_x_ticks,
xlabel='Real-time Index',
ylabel='Army Value')
Since the time index is evenly spaced in both cases, there is no change to the curve. However, the timing of the events is different, which one can see using in the graph bellow, where I use the same x_axis from the second graph with the army_values indexed by the original time index.
pstats_noprocess['army_value'].plot(figsize= (10,5),
xticks= real_x_ticks,
xlabel='Real-time Index',
ylabel='Army Value')
Data in PlayStatsEvent
Having examined how to manipulate PlayerStatsEvent
objects, I can examine the type of data they store. In general, this type of objects store macro-economic data.
Here is a list of their attributes and how they can be used to compose player stats.
minerals_used_active_forces
andvespene_used_active_forces
: As I show in the example above, these indicators can be used to calculate the players' army value.minerals_collection_rate
,vespene_collection_rate
,workers_active_count
: are important indicators about the rate of growth of the economy.minerals_current
andvespene_current
: these attributes can be added and averaged over time to extract the average unspent resources. This information is critical because the game's macro strategy is built around growing an economy and spending resources. Thus, an unspent economy can signal missed opportunities.food_used
andfood_made
: these two attributes can be used to signal moments where the players' supply has been capped. Therefore the player is blocked, preventing players from training units.- Economy use and lost indicators divide the players' resources into:
- used_in_progress: resources that have been spent in elements (structures, units or research) under development.
minerals_used_in_progress
vespene_used_in_progress
resources_used_in_progress
- used_current: resources invested in elements that are currently in play.
minerals_used_current
vespene_used_current
resources_used_current
- lost: resources invested in elements that a player has lost (by killing or destruction) during the game.
minerals_lost
vespene_lost
resources_lost
- killed: resources that an opponent had invested in elements that the player has killed.
minerals_killed
vespene_killed
resources_killed
- Additionally, each mineral and vespene resource attribute has three more associated attributes, providing more detail into the economy. These extended attributes break each indicator into army, economy and technology indicators (buildings, units and research). For example, the
minerals_used_in_progress
attribute has the following attributes that disaggregate it:minerals_used_in_progress_army
minerals_used_in_progress_economy
minerals_used_in_progress_technology
- used_in_progress: resources that have been spent in elements (structures, units or research) under development.
Helper Functions
In this module, I define various helper functions that simplify the definition of the module's main exportable functions.
get_pstatse
extracts a list of all thePlayerStatsEvent
instances related to a player from aReplay
.complete_pstatse_df
expands the initialDataFrame
that can be extracted directly from the PlayerStatsEvents with several relevant features. I use this function in theget_player_macro_econ_df
definition.Some functions that allow for the functional computation of various indicators:
get_player_macro_econ_df
extracts all significant indicators from thePlayerStatsEvents
, and compiles them into apandas.DataFrame
. ThisDataFrame
expands the information in these events with several columns. Chiefly, the data frame includes areal_time
column that allows for the correct indexing of the events based on the match's duration.
The get_player_macro_econ_df
function organises a player's PlayerStatsEvents
into a DataFrame
where the columns represent each indicators' values. In said DataFrame
the rows represent the player's events.
The code below shows the recorded data for the last event (i.e. row) of the DataFrame extracted from a sample replay using this function.
test_df = get_player_macro_econ_df(single_replay, 1)
print(test_df.iloc[-1])
The indicators extracted with this function include:
- Average unspent minerals, vespene and minerals
- Average Active Workers
- Average mineral, vespene and resource collection rate
- Average army value
- Total minerals and vespene lost
- Spending Quotient
Note that the spending quotient is a performance measurement that the community has adopted to compare and evaluate a player's macroeconomic performance (whatthefat, 2011; Spending Quotient, 2020).
Moreover, I calculate each of these indicators for the entire match and the early (first 4 minutes), mid (minutes 4-8) and late game (past minute 8).
The following are three sample results of this function used on a short, medium and long replays of the same player.
pprint(get_player_macro_econ_stats(single_replay, 1), sort_dicts=False)
short_rpl = sc2reader.load_replay('./test_replays/Oxide LE (12).SC2replay')
long_rpl = sc2reader.load_replay('./test_replays/Blackburn LE (2).SC2replay')
pprint(get_player_macro_econ_stats(short_rpl, 1), sort_dicts=False)
pprint(get_player_macro_econ_stats(long_rpl, 2), sort_dicts=False)
References
- Kim, G. (2015) 'sc2reader Documentation'. Available at: https://sc2reader.readthedocs.io/_/downloads/en/latest/pdf/.
- Spending Quotient (2020) Liquipedia - The StarCraft II Encyclopedia. Available at: https://liquipedia.net/starcraft2/Spending_quotient (Accessed: 18 June 2021).
- whatthefat (2011) Do you macro like a pro?, TL.net blog. Available at: https://tl.net/forum/starcraft-2/266019-do-you-macro-like-a-pro (Accessed: 18 June 2021).