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).
APMTracker
one has to import it into the module and setit up as part of sc2reader’s engine, as shown in the following code: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)
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
andControlGroupEvents
(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)
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.
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)}')
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)
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 usessc2reader'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.
The following is a sample run of the calc_apm
function.
pprint(calc_apms(single_replay, 2))
player_ = single_replay.player[1]
player_.is_human
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)
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 sample run
prefered_abilities_test = get_prefered_spec_abil(TEST_MATCH, 1)
prefered_abilities_test
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 sample run
test_attack_ratios = calc_attack_ratio(TEST_MATCH, 1)
test_attack_ratios
References
- Kim, G. (2015) 'sc2reader Documentation'. Available at: https://sc2reader.readthedocs.io/_/downloads/en/latest/pdf/.