Introduction

In this chapter, I review the GameEvent and CommandEvent objects as sources of information about the players' tactical characteristics. Here I also develop the functions contained in handle_command_events module. The module can be used to process some of the player's tactical performance indicators.

In the previous modules, I focussed on the indicators I could extract and build based on the information in a replay's PlayerStatsEvents. For the most part, these indicators revolve around economic and building indicators, which point to the game's strategic (i.e. macro) dimension. Meanwhile, beyond what a player's build orders and army composition may tangentially suggest, these indicators don't seem to reveal much about the game's tactical (i.e. micro) dimension. Moreover, the game itself and other applications like sc2replaystats offer players little information in this regard.

Exportable Members

APM

One indicator gets some attention in this respect, the average actions per minute (APM). This indicator points to how fast players can play. The assumption around this marker is that if players have high APMs, this indicates that they use many of the game commands directly to control their unit's actions. Thus, I will take this indicator into account when building the player's profiles. Thankfully, sc2reader includes the APMTracker pug-in that facilitates the collection of this information (Kim, 2015, p. 16-17).

from sc2reader.engine.plugins import APMTracker
sc2reader.engine.register_plugin(APMTracker())

The following code loads this notebook's sample replays and demonstrates the use of APMTracker to extract this indicator.

# Load sample replays
RPS_PATH = Path("./test_replays")

game_path = str(RPS_PATH/"Jagannatha LE.SC2Replay")
single_replay = sc2reader.load_replay(game_path)
single_replay

ta_test = sc2reader.load_replay(str(RPS_PATH/'Terran_abilities.SC2Replay'))
pa_test = sc2reader.load_replay(str(RPS_PATH/'ProtossAbilities.SC2Replay'))

tms_test = sc2reader.load_replay(str(RPS_PATH/'TMovesSelect.SC2Replay'))
pms_test = sc2reader.load_replay(str(RPS_PATH/'p_move_test.SC2Replay'))
# Given APMTracker is enable Player objects have a avg_apm attribute
p1_avg_apm = single_replay.player[1].avg_apm
print(p1_avg_apm)
79.32203389830521

GameEvents

The problem is that this APM tells the observer little of what actions players use and when. Looking beyond this indicator, in this module, I define the functions calc_spe_abil_ratios, get_prefered_spec_abil and calc_attack_ratio . These functions use the information stored in the Replay's GameEvents to infer several measurements that quantify some aspects of the players' tactical preferences.

As explained in sc2reader's documentation:

:Game events are what the Starcraft II engine uses to reconstruct games (Kim, 2015, p. 22). In particular, I focus on three of the GameEvent's sub-classes, CommandEvents and, SelectionEvents and ControlGroupEvents (see <<Chapter 6 - Tracking Control Groups>>).

First, I use the information stored in the CommandEvents to measure two elements: the ratio of attack orders and special abilities. On the one hand, the balance of attack orders vs the total number of commands partially describes a player's tactical aggressiveness.

On the other, the use of special abilities suggests a level of awareness of the possibilities offered by different units and enough control to activate and use them. Furthermore, tallying the times a player uses the abilities provided by their race, I can infer part of their tactical preferences.

In the following code, I can extract its GameEvents.

TEST_MATCH = pa_test
match_ge = [event for event in TEST_MATCH.events 
            if isinstance(event, sc2reader.events.game.GameEvent)]
len(match_ge)
5076

CommandEvents

CommandEvents are generated and recorded every time a player orders a unit to do something during a match (Kim, 2015, p. 22-24). These orders include building orders, some basic common commands or the use of a unit's special abilities.

For my analysis, I group the common commands (i.e. move, stop, patrol, hold position, follow, collect, attack) to expose play patterns shared by all units. Similarly, I review the use of special abilities separately because it can unveil some of the player's preferences regarding the unique potential of their play race.

The following code shows how I use the events' attributes to classify the command events. In it, I use some internal constants that store multiple lists I use to filter the different categories of commands. The constants include such as ABILITIES, COMMON_ABILITIES and MOVE_COMMANDS. The reader may review the implementation and values of these constants in the module's source code or the development notebook. Regardless, it is worth clarifying that ABILITIES lists capabilities that belong to specific units or buildings not automatically executed by the game. These abilities requiere the players' direct orders to be performed.

# The following is a temporary variable for the examples.
race_abilities = ABILITIES[TEST_MATCH.players[0].play_race]

# First, I can use the events' types to extract all CommandEvents appart 
# from other GameEvents.
commands = [com_e for com_e in match_ge
            if isinstance(com_e, sc2reader.events.game.CommandEvent)
            and com_e.pid == 0]

# From that first list, I extract the commands linked to special abilities
# used during the game as follows.
special_comm = [com_e for com_e in match_ge 
                if isinstance(com_e, sc2reader.events.game.CommandEvent)
                and com_e.pid == 0
                and com_e.ability_name in race_abilities
                and com_e.ability_name not in COMMON_ABILITIES]

# I can also extract the commands related to upgrades and tech research.
upgrades = [com_e for com_e in match_ge 
            if isinstance(com_e, sc2reader.events.game.CommandEvent)
            and com_e.pid == 0
            and com_e.has_ability
            and not com_e.ability.is_build
            and com_e.ability_name not in race_abilities
            and com_e.ability_name not in COMMON_ABILITIES]


# The following lists the common actions related to unit direction.
common_comm = [com_e for com_e in match_ge 
             if isinstance(com_e, sc2reader.events.game.CommandEvent)
             and com_e.pid == 0
             and not com_e.ability.is_build
             and com_e.ability.name in COMMON_ABILITIES
             and com_e.ability_name in COMMON_ABILITIES]

# While the code bellow lists the commands that are related to building.
# In this case, I need two lists. 
# The first lsit is composed of the abilities that are labeled as
# "is_build" 
build_comm1 = [com_e for com_e in match_ge
            if isinstance(com_e, sc2reader.events.game.CommandEvent)
            and com_e.pid == 0
            and com_e.has_ability
            and not com_e.ability_name in race_abilities
            and com_e.ability.is_build]

# The second list has no linked ability, but it includes commands that 
# order the construction of each race's vespene gas extraction facilities.
build_comm2 = [com_e for com_e in match_ge
            if isinstance(com_e, sc2reader.events.game.CommandEvent)
            and com_e.pid == 0
            and not com_e.has_ability
            and (not com_e.ability.is_build
               and 'Build' in com_e.ability_name)]

I can verify the validity of this classification by adding all the lists lengths and confirming they have the same number of elements as the CommandEvent list.

extras = [com_e for com_e in match_ge
        if isinstance(com_e, sc2reader.events.game.CommandEvent)
        and com_e.pid == 0
        and (com_e not in special_comm
            and com_e not in upgrades
            and com_e not in common_comm
            and com_e not in build_comm1
            and com_e not in build_comm2)]

print(f'Special abilities commands: {len(special_comm)}')
print(f'Special abilities upgrades commands: {len(upgrades)}')
print(f'Common commands: {len(common_comm)}')
print(f'Build Commands: {len(build_comm1)}')
print(f'Build Vespene Extractor Facility Commands: {len(build_comm2)}')
print(f'Extras: {len(extras)}')

command_lists=[special_comm, 
              upgrades,
              common_comm,
              build_comm1,
              build_comm2,
              extras]

sum_lists = sum([len(c_list) for c_list in command_lists])

print(f'Total sum: {sum_lists}')
print(f'Total Commands: {len(commands)}')
Special abilities commands: 64
Special abilities upgrades commands: 30
Common commands: 180
Build Commands: 78
Build Vespene Extractor Facility Commands: 3
Extras: 0
Total sum: 355
Total Commands: 355

Additionally, I run the following test to ensure that the sum above counts each element once to avoid double-counting some while ignoring others.

lists=[special_comm, upgrades, common_comm, build_comm1, build_comm2]
repeats = []
for ind1, l1 in enumerate(lists):
    for ind2, l2 in enumerate(lists):
        if l1 != l2:
            for e in l1:
                if e in l2:
                    repeats.append(e)

ft.test_eq(len(repeats), 0)

Classifying common commands

Beyond the command classification above, I can also sub-divide the common commands into distinct types. Upon some examination, I realise that these events relate to direct attacks, unit movement orders, or collection orders that tell units to gather Minerals or Vespene Gas.

The following code illustrates how I can list the common commands into these categories.

# Group attack orders
attacks = [att for att in common_comm
        if att.ability.name == 'Attack']

# Collection orders
resources = ['Mineral', 'Vespene', 'Extractor', 'Refinary', 'Assimilator']
collects = [coll for coll in common_comm
           if hasattr(coll, 'target')
           and (lambda event: any(map(lambda rsc: rsc in event.target.name, 
                                      resources))
                if hasattr(event, 'target') else True)(coll)]

# Orders to follow other units
follows = [foll for foll in common_comm
           if hasattr(foll, 'target')
           and not (lambda event: any(map(lambda rsc: rsc in event.target.name,
                                         resources))
                if hasattr(event, 'target') else True)(foll)]

# Other unit movement orders
moves_names = ['Stop', 'Patrol', 'HoldPosition', 'RightClick']
moves = [move for move in common_comm
        if move.ability_name in moves_names
        and move not in collects
        and move not in follows]

Once more, the following code verifies the classification.

extras = [ext for ext in common_comm
        if not(lambda x: any(map(lambda e_list: x not in e_list, 
                                 [attacks, moves, collects])))(ext)]

print(f'Attacks: {len(attacks)}')
print(f'Collects: {len(collects)}')
print(f'Follows: {len(collects)}')
print(f'Moves: {len(moves)}')
print(f'Extras: {len(extras)}')

command_lists=[attacks, 
              collects,
              moves,
              extras]

sum_lists = sum([len(c_list) for c_list in command_lists])

print(f'Common Commands: {len(common_comm)}')
print(f'Total sum: {sum_lists}')

ft.test_eq(len(extras),0)
Attacks: 12
Collects: 23
Follows: 23
Moves: 116
Extras: 0
Common Commands: 180
Total sum: 151
lists=[attacks, collects, follows, moves, extras]

repeats = []
for ind1, l1 in enumerate(lists):
    for ind2, l2 in enumerate(lists):
        if l1 != l2:
            for e in l1:
                if e in l2:
                    repeats.append(e)

ft.test_eq(len(repeats), 0)

Exportable Functions

In this section, I define the handle_command_events module's exportable functions. These functions use CommandEvents, as discussed above, to calculate several micro-game performance indicators.

  • calc_apms: This function uses sc2reader's APMTracker plugin to extract the average APM of a specific player during the whole game and the early, mid and late stages of the game.
  • calc_spe_abil_ratios: This function uses the special abilities list to calculate the ratio between commands involving this abilities and the total commands executed by the player. With this ratio, I can quantify the use and awareness of these abilities.
  • get_prefered_spec_abil: This function calculates the player's first and second preferred abilities, if they use any.
  • calc_attack_ratio: This function estimates a player's aggressiveness.

calc_apms[source]

calc_apms(rpl:Replay, pid:int)

Extracts the average APM of a specific player during the whole game and the early, mid and late stages of the game.

The function uses sc2reader APMTracker plugin to extract the minute-to-minute APM measurement of the player. Based on this it calculates the average values for the whole, early, mid and late game stages.

Args

- rpl (sc2reader.resources.Replay)
    Replay being analysed
- pid (int)
    Player id of the player being considered by the function

Returns

- dict[str, float]
    Dictionary with the game stage names as key and the Average
    APMs measurements of each stage as values

The following is a sample run of the calc_apm function.

pprint(calc_apms(single_replay, 2))
{'early_APM': 246.40000000000046,
 'late_APM': 187.60000000000045,
 'mid_APM': 287.46666666666664,
 'whole_APM': 363.9050847457606}
player_ = single_replay.player[1]
player_.is_human
True

calc_spe_abil_ratios[source]

calc_spe_abil_ratios(rpl:Replay, pid:int)

Extracts a ratio from 0 to 1 that quantifies the use use of special abilities.

The special abilities ratio (sar) indicates the proportion of special abilities to general commands executed by the player.

Args

- rpl (sc2reader.resources.Replay)
    The replay being analysed.
- pid (int)
    In-game player ID of the player being considered in the
    analysis.

Returns

- dict[float]
    A dictionary containing the special abilities ratio (sar)
    values for the whole, early, mid and late game.

The following is a sample run of the calc_spe_abil_ratios function.

test_restult = calc_spe_abil_ratios(TEST_MATCH, 1)
pprint(test_restult)
{'early_sar': 0.045454545454545456,
 'late_sar': 0.22007722007722008,
 'mid_sar': 0.09615384615384616,
 'whole_sar': 0.18028169014084508}

Meanwhile, the function get_prefered_spec_abil calculates the player's first and second preferred abilities, if they use any. Again, this function separates the results for the early, mid and late games.

get_prefered_spec_abil[source]

get_prefered_spec_abil(rpl:Replay, pid:int)

Extracts the names of the two special abilities a player uses the most during the whole, early, mid and late games.

Args

- rpl (sc2reader.resources.Replay)
    The replay being analysed.
- pid (int)
    In-game player ID of the player being considered in the
    analysis.

Returns

- dict[str, tuple[str, int]]
    The keys of the dictionary separate the preferences according
    to the game stages. The dictionary values contain a tuple with
    the first and second abilities the player uses the most in
    that order.
# get_prefered_spec_abil sample run
prefered_abilities_test = get_prefered_spec_abil(TEST_MATCH, 1)
prefered_abilities_test
{'whole_pref_sab': ('GravitonBeam', 'Feedback'),
 'early_pref_sab': ('ChronoBoostEnergyCost', None),
 'mid_pref_sab': ('GravitonBeam', 'AdeptPhaseShift'),
 'late_pref_sab': ('GravitonBeam', 'Feedback')}

Meanwhile, I define calc_attack_ratio to estimate a player's aggressiveness. This function calculates the ratio between attack commands and the number of common commands as a potential indicator of a player's aggressiveness.

calc_attack_ratio[source]

calc_attack_ratio(rpl:Replay, pid:int)

Calculates the ratio between a player's attack orders and their common commands.

Offers a ratio between attacks and other common commands such as move, follow, stop, and hold position as a measurement of a player's tactical aggresiveness.

Args

- rpl (sc2reader.resources.Replay)
    The replay being analysed.
- pid (int)
    In-game player ID of the player being considered in the
    analysis.

Returns

- dict[str, float]
    A dictionary that separates a player's attack ratios for the
    different stages of a match.
# calc_attack_ratio sample run
test_attack_ratios = calc_attack_ratio(TEST_MATCH, 1)
test_attack_ratios
{'whole_att_ratio': 0.067,
 'early_att_ratio': 0.0,
 'mid_att_ratio': 0.03,
 'late_att_ratio': 0.084}

References