/
trackerevts.go
196 lines (161 loc) · 5.26 KB
/
trackerevts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
/*
Type describing the tracker events.
*/
package rep
import (
"math"
"github.com/icza/s2prot"
)
const (
// TrackerEvtIDPlayerStats is the ID of the Player Stats tracker event
TrackerEvtIDPlayerStats = 0
// TrackerEvtIDUnitBorn is the ID of the Unit Born tracker event
TrackerEvtIDUnitBorn = 1
// TrackerEvtIDPlayerSetup is the ID of the Player Setup tracker event
TrackerEvtIDPlayerSetup = 9
)
// TrackerEvts contains tracker events and some metrics and data calculated from them.
type TrackerEvts struct {
// Evts contains the tracker events
Evts []s2prot.Event
// PIDPlayerDescMap is a PlayerDesc map mapped from player ID.
PIDPlayerDescMap map[int64]*PlayerDesc
// ToonPlayerDescMap is a PlayerDesc map mapped from toon.
ToonPlayerDescMap map[string]*PlayerDesc `json:"-"`
}
// PlayerDesc contains calculated, derived data from tracker events.
type PlayerDesc struct {
// PlayerID is the ID of the player this PlayerDesc belongs to.
PlayerID int64
// SlotID is the slot ID of the player
SlotID int64
// UserID is the user ID of the player
UserID int64
// Start location of the player
StartLocX, StartLocY int64
// StartDir is the start direction of the player, expressed in clock,
// e.g. 1 o'clock, 3 o'clock etcc, in range of 1..12
StartDir int32
// SQ (Spending Quotient) of the player
SQ int32
// SupplyCappedPercent is the supply-capped percent of the player
SupplyCappedPercent int32
}
// init initializes / preprocesses the tracker events.
func (t *TrackerEvts) init(rep *Rep) {
pidPlayerDescMap := make(map[int64]*PlayerDesc)
t.PIDPlayerDescMap = pidPlayerDescMap
// stats per player
type stats struct {
samples int64 // stats samples count
unspents int64 // Unspent resources
incomes int64 // Resource income
supCapped int64 // supply capped
}
pidStats := make(map[int64]*stats)
// first read Player setup events:
for _, e := range t.Evts {
if e.Loop() > 0 {
break
}
if e.ID != TrackerEvtIDPlayerSetup {
continue
}
pid := e.Int("playerId")
pd := pidPlayerDescMap[pid]
if pd == nil {
pd = &PlayerDesc{PlayerID: pid, SlotID: e.Int("slotId"), UserID: e.Int("userId")}
pidPlayerDescMap[pid] = pd
pidStats[pid] = &stats{}
}
}
// Read start locations and player stats
cx := rep.InitData.GameDescription.MapSizeX() / 2
cy := rep.InitData.GameDescription.MapSizeY() / 2
for _, e := range t.Evts {
if e.Loop() == 0 && e.ID == TrackerEvtIDUnitBorn {
if isMainBuilding(e.Stringv("unitTypeName")) {
pd := pidPlayerDescMap[e.Int("controlPlayerId")]
if pd != nil {
pd.StartLocX = e.Int("x")
pd.StartLocY = e.Int("y")
pd.StartDir = angleToClock(math.Atan2(float64(pd.StartLocY-cy), float64(pd.StartLocX-cx)))
}
}
}
if e.ID == TrackerEvtIDPlayerStats {
pid := e.Int("playerId")
st := pidStats[pid]
if st != nil {
ss := e.Structv("stats")
st.samples++
st.unspents += ss.Int("scoreValueMineralsCurrent") + ss.Int("scoreValueVespeneCurrent")
st.incomes += ss.Int("scoreValueMineralsCollectionRate") + ss.Int("scoreValueVespeneCollectionRate")
if ss.Int("scoreValueFoodUsed") >= ss.Int("scoreValueFoodMade") {
st.supCapped++
}
}
}
}
// Finish SQ and supply-capped calculations
for pid, pd := range pidPlayerDescMap {
st := pidStats[pid]
if st == nil || st.samples == 0 {
continue
}
pd.SQ = calcSQ(st.unspents/st.samples, st.incomes/st.samples)
pd.SupplyCappedPercent = int32(st.supCapped * 100 / st.samples)
}
// Fill ToonPlayerDescMap
t.ToonPlayerDescMap = make(map[string]*PlayerDesc)
for _, pd := range pidPlayerDescMap {
slot := rep.InitData.LobbyState.Slots[pd.SlotID]
t.ToonPlayerDescMap[slot.ToonHandle()] = pd
}
}
// isMainBuilding tells if the unit type name denotes a main building, that is
// one of Nexus, Command Center and Hatchery.
func isMainBuilding(unitTypeName string) bool {
return unitTypeName == "Nexus" || unitTypeName == "CommandCenter" || unitTypeName == "Hatchery"
}
// angleToClock converts an angle given in radian to an hour clock value
// in the range of 1..12.
//
// Examples:
// - PI/2 => 12 (o'clock)
// - 0 => 3 (o'clock)
// - PI => 9 (o'clock)
func angleToClock(angle float64) int32 {
// The algorithm below computes clock value in the range of 0..11 where
// 0 corresponds to 12.
// 1 hour is PI/6 angle range
const oneHour = math.Pi / 6
// Shift by 3:30 (0 or 12 o-clock starts at 11:30)
// and invert direction (clockwise):
angle = -angle + oneHour*3.5
// Put in range of 0..2*PI
for angle < 0 {
angle += oneHour * 12
}
for angle >= oneHour*12 {
angle -= oneHour * 12
}
// And convert to a clock value:
hour := int32(angle / oneHour)
if hour == 0 {
return 12
}
return hour
}
// calcSQ calculates the SQ (Spending Quotient).
//
// Algorithm:
// SQ = 35 * ( 0.00137 * I - ln( U ) ) + 240
// Where U is the average unspent resources (Resources Current; including minerals and vespene)
// and I is the average income (Resource Collection Rate; including minerals and vespene);
// and samples are taken up to the loop of the last cmd game event of the user.
//
// Source: Do you macro like a pro? http://www.teamliquid.net/forum/viewmessage.php?topic_id=266019
func calcSQ(unspentResources, income int64) int32 {
return int32(35*(0.00137*float64(income)-math.Log(float64(unspentResources))) + 240 + 0.5)
}