Introduction

Beyond CommandEvents (see <<Chapter 5 - Handling Command Events>>), another potential indicator that I can use to profile the players' tactical play (i.e. their micro-game) is their selection behaviour. For instance, in StarCraft II, players can select units in two ways; they can use their mouse in conjunction with some hotkeys like ctrl or use control groups to assemble sets of units for custom and fast selection.

This chapter explores how I can extract information about this behaviour with sc2reader's SelectionEvents and ControlGroupEvents. Using these events, I define a set of functions to quantify this characteristic of the players' play.

Exported Functions:

Extracting the Control Group Compositions

Before examining the structure of the ControlGroupEvents and the SelectionEvents, I must explain how to load Replays to include enough information to quantify the abovementioned indicators. The issue is that loading Replays with sc2reader as I have in previous modules does not record the composition of the players' control groups over time. This can be remediated to an extent using the sc2reader plug-in SelectionTracker, which is similar to the APMTracker I use in <<Chapter 5 -Handling Command Events>>. However, in contrast with APMTracker, SelectionTracker is meant as an input for user-defined plug-ins that specify its behaviour.

class CtrlGroupTracker[source]

CtrlGroupTracker()

Tracks the composition of the Replay's Players Control Groups.

Using this plug-in, the Replay object will include the ctrl_grp_trk attribute. This attribute will store a dictionary of the control group compositions using the replay's human players' ids (pid) as keys. i.e.: dict[pid (int): control_group_compositions (dict)]

Each of these dictionaries uses the second attribute of a ControlGroupEvent as an index and organises the control group composition as one more dictionary, indexed with the integers 1 to 9, which stores a list of units that compose each one of the nine control groups of the player. i.e.

dict[pid(int): dict[second(int): dict[group(int): list_of_units]]]

Selection Behaviour Events

As stated above, in this chapter, I take a look into two kinds of GameEvents that I can use to get a notion of the players' selection behaviours, i.e. ControlGroupEvents and SelectonEvents.

Handling ControlGroupEvents

I will start by loading the sample Replays I use in this notebook to analyse this type of event.

# Register CtrlGroupTracker plug-in
sc2reader.engine.register_plugin(CtrlGroupTracker())

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

game_path = str(RPS_PATH/"Jagannatha LE.SC2Replay")
single_replay = sc2reader.load_replay(game_path)
ctrl_grp_test = sc2reader.load_replay(str(RPS_PATH/'ctrl_grp_t.SC2Replay'))
ctrl_grp_test_2 = sc2reader.load_replay(str(RPS_PATH/'ctrl_grp_t_2.SC2Replay'))

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'))

With Replays loaded, one can see that they now have the ctrl_grp_trk attribute. Calling this attribute, I can choose to look at each of the players' control group compositions using their pid as an index. Afterwards, I can look at each composition independently using the value of the second attribute of the ControlGroupEvent that generated it as an index.

TEST_MATCH = single_replay
TEST_PID = 2
sample_ctrlg_comp = TEST_MATCH.ctrl_grp_trk[TEST_PID][221]
print('LOAD RESULTS:')
print(f'Ctrl group compositions: {sample_ctrlg_comp}')
print('---------------------------')
print('Sample Ctrl group composition:')
pprint(TEST_MATCH.ctrl_grp_trk[TEST_PID][221])
print('----------------------------')

sample_event = [e for e in TEST_MATCH.events 
              if isinstance(e, sc2reader.events.game.ControlGroupEvent)
              and e.pid == (TEST_PID - 1)
              and e.second == 221]

print(f'Sample {sample_event[0].name}')
print(f'Generated by: {sample_event[0].player}')
print(f'Recorded time: {sample_event[0].second}second')
LOAD RESULTS:
Ctrl group compositions: {1: [OrbitalCommand [3100001]], 2: [SCV [3440001]], 3: [OrbitalCommand [3100001]], 4: [Factory [3B80001]], 5: [OrbitalCommand [3100001]], 6: [], 7: [], 8: [], 9: []}
---------------------------
Sample Ctrl group composition:
{1: [OrbitalCommand [3100001]],
 2: [SCV [3440001]],
 3: [OrbitalCommand [3100001]],
 4: [Factory [3B80001]],
 5: [OrbitalCommand [3100001]],
 6: [],
 7: [],
 8: [],
 9: []}
----------------------------
Sample GetControlGroupEvent
Generated by: Player 2 - MxChrisxM (Terran)
Recorded time: 221second

Beyond the control group compositions, I can use simple list comprehensions to extract the ControlGroupEvents from the Replay's event list. With this technique, I can also segregate these events into three sub-types:

  • SetControlGroupEvent: created each time a player assigns a control group.
  • GetControlGroupEvent: registered each time a player summons, i.e. uses a hot-key to select a control group.
  • AddToControlGroupEvent: registered when a player uses a hot-key to assign selected units to an existing control group.

This classification is helpful to distinguish between events that select units and those that do not.

#List all ControlGroupEvents
ctrl_grp_e = [e for e in TEST_MATCH.events 
              if isinstance(e, sc2reader.events.game.ControlGroupEvent)
              and e.pid == (TEST_PID - 1)]

#List ControlGroupEvents sub-types
set_ctrl_grp = [e for e in ctrl_grp_e
                if isinstance(e, sc2reader.events.game.SetControlGroupEvent)]

get_ctrl_grp = [e for e in ctrl_grp_e
                if isinstance(e, sc2reader.events.game.GetControlGroupEvent)]

add_ctrl_grp = [e for e in ctrl_grp_e
                if isinstance(e, 
                              sc2reader.events.game.AddToControlGroupEvent)]

print(f'ControlGroupEvents: {len(ctrl_grp_e)}')
print(f'SetControlGroupEvents: {len(set_ctrl_grp)}')
print(f'GetControlGroupEvents: {len(get_ctrl_grp)}')
print(f'AddControlGroupEvents: {len(add_ctrl_grp)}')
ControlGroupEvents: 100
SetControlGroupEvents: 14
GetControlGroupEvents: 86
AddControlGroupEvents: 0

Meanwhile, with a simple loop I can go through a control group composition group-lists to count how many of them actually have units assign to them as shown in the code bellow.

count = 0 
for l in sample_ctrlg_comp.values():
    if l:
        count += 1 

count
5

In this module, I define an internal function called count_active_groups that carries out this task.

# count_active_groups sample run
count_active_groups(sample_ctrlg_comp)
5

Handling SelectionEvents

As explained in sc2reader's documentation,

:"SelectionEvents are generated when ever the active selection of the player is updated. Unlike other game events, these events can also be generated by non-player actions like unit deaths or transformations. [...] selection events targetting control group buffers are also generated when control group selections are modified by non-player actions. When a player action updates a control group a ControlGroupEvent is generated." (Kim, 2015, p. 48) Hence, SelectionEvents events can refer to game situations that also trigger ControlGroupEvents, but they are essentially different.

Below, I extract the sample match's SelectionEvents. I also use their control_group attribute to see what control groups they are linked to. Note that there is a control group number ten according to the list of control groups linked to the selections. This group refers to the player's current active selection, whatever it was at the moment. In other words, if the player selects units just by clicking, it would trigger a SelectionEvent but not a ControlGroupEvent. Said SelectionEvent would be linked to the current selection instead of a control group.

select_e = [e for e in TEST_MATCH.events 
              if isinstance(e, sc2reader.events.game.SelectionEvent)
              and e.pid == (TEST_PID - 1)]

print(f'SelectionEvents in sample replay: {len(select_e)}')
print(f'Groups referenced by the Selection Events:')
print(set([s.control_group for s in select_e]))
SelectionEvents in sample replay: 215
Groups referenced by the Selection Events:
{1, 3, 4, 5, 10}

Similarly, the following code exposes the intersection between the two sets. It shows how many SelectionEvents and ControlGroupEvents sc2reader registered independently and how many were triggered by the same game event.

Recognizing this link is crucial if I want to count the two types of events together. In this case, I need to make sure that I only count each game event once. The following code shows how the sets of events intersect, how simply adding the sets miss-counts the elements, and how I can use set-union to count them correctly.

sel_e_times = set([s.second for s in select_e])
ctrl_grp_times = set([c.second for c in ctrl_grp_e])
print(f'select_e_times_indexes: {len(sel_e_times)}')
print(f'ctrl_grp_times_indexes: {len(ctrl_grp_times)}')

intersect = sel_e_times.intersection(ctrl_grp_times)
not_intersect_set = sel_e_times.symmetric_difference(ctrl_grp_times)
print(f'Number of elements that intersect: {len(intersect)}')
print(f'Number of elements that do not intersect: {len(not_intersect_set)}')
print(f'Sum of set counts: {len(sel_e_times) + len(ctrl_grp_times)}')
print(f'Union of select and ctrl_group: {len(sel_e_times | ctrl_grp_times)}')
select_e_times_indexes: 179
ctrl_grp_times_indexes: 85
Number of elements that intersect: 7
Number of elements that do not intersect: 250
Sum of set counts: 264
Union of select and ctrl_group: 257

Functions

Helper functions

Internally the module defines the helper functions:

  • build_ctrlg_df that builds a DataFrame with the Replay's list of ControlGroupEvents.

The table results from a sample run of build_ctrlg_df called on the notebooks TEST_MATCH.

real_time second pid name control_group
0 4.28571 6 2 SetControlGroupEvent 1
1 4.28571 6 2 SetControlGroupEvent 2
2 4.28571 6 2 SetControlGroupEvent 3
3 5 7 2 SetControlGroupEvent 4
4 5 7 2 SetControlGroupEvent 5

Exportable Functions

Considering all of the above, I define multiple functions to extract the players' seclection behaviour indicators.

These functions include:

count_max_active_groups[source]

count_max_active_groups(rpl:Replay, pid:int)

Counts the maximum number of active control groups during the different stages of the game.

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, int]
    Maximum number of active control groups at each game stage
    indexed with the keys [stage]_max_act_grps
count_max_active_groups(TEST_MATCH, TEST_PID)
{'whole_max_act_grps': 5,
 'early_max_act_grps': 5,
 'mid_max_act_grps': 5,
 'late_max_act_grps': 5}

calc_ctrlg_ratio[source]

calc_ctrlg_ratio(rpl:Replay, pid:int)

Calculates the ratio between ControlGroupEvents and the union of the CommandEvents, SelectionEvents and ControlGroupCommand sets to quantify the players' level of awareness and use of this tactical feature.

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])
calc_ctrlg_ratio(TEST_MATCH, TEST_PID)
{'ctrlg_ratio': 0.19630484988452657}

The next two functions use the union between GetControlGroupEvent and SelectEvent sets to quantify if the player preferes to select units based on mouse clicks or using control groups.

calc_get_ctrl_grp_ratio[source]

calc_get_ctrl_grp_ratio(rpl:Replay, pid:int)

Calculates the ratio between GetControlGroupEvent to the all section events (i.e. the union of GetControlGroupEvent and SelectEvent).

calc_get_ctrl_grp_ratio(TEST_MATCH, TEST_PID)
{'get_ctrl_grp_ratio': 0.308}

calc_select_ratio[source]

calc_select_ratio(rpl:Replay, pid:int)

Calculates the ratio between SelectEvent to the all section events (i.e. the union of GetControlGroupEvent and SelectEvent).

calc_select_ratio sample run:

calc_select_ratio(TEST_MATCH, TEST_PID)
{'select_ratio': 0.716}

References