forked from goonstation/goonstation
-
Notifications
You must be signed in to change notification settings - Fork 0
/
antag_weighting.dm
339 lines (268 loc) · 10.3 KB
/
antag_weighting.dm
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/*
* A system for picking players to be antags based on their previous picks
* Designed to give those who never get picked for antag a greater chance
*/
var/global/datum/antagWeighter/antagWeighter
/datum/antagWeighter
var/debug = 0 //print a shit load of debug messages or not
var/variance = 100 //percentage probability *per choice* to ignore weighting for a single antag role (instead picking some random dude)
var/minPlayed = 5 //minimum amount of rounds participated in required for the antag weighter to consider a person a valid choice
New(debugMode)
..()
src.debug = debugMode ? debugMode : 0
proc/debugLog(msg)
out(world, msg)
//logTheThing("debug", null, null, "<b>AntagWeighter</b> [msg]")
/**
* Queries the goonhub API for hisorical antag rounds for a single target
* NOTE: Currently unused
*
* @param string role Name of the antag role we're looking up (e.g. traitor, spy_thief)
* @param string ckey Ckey of the person we're looking up
* @return list List of history details
*/
proc/history(role = "", ckey = "")
if (!role || !ckey)
throw EXCEPTION("Incorrect parameters given")
var/list/response = apiHandler.queryAPI("antags/history", list(
"role" = role,
"players" = ckey,
"amount" = 1
), 1)
if (response["error"])
throw EXCEPTION(response["error"])
return response["history"]
/**
* Get the entire antag selection history for a player (all roles, all modes)
*
* @param string ckey Ckey of the person we're looking up
* @return list List of history details
*/
proc/completeHistory(ckey = "")
if (!ckey)
throw EXCEPTION("No ckey given")
if (!config.goonhub_api_token)
throw EXCEPTION("You must have the goonhub API token to use this command!")
var/list/response
try
response = apiHandler.queryAPI("antags/completeHistory", list(
"player" = ckey,
), 1)
catch ()
throw EXCEPTION("API is currently having issues, try again later")
if (response["error"])
throw EXCEPTION(response["error"])
if (length(response["history"]) < 1)
throw EXCEPTION("No history for that player")
return response["history"]
/**
* Simulates a history response from the API, so local development doesn't fuck up
*
* @param string role Name of the antag role we're looking up (e.g. traitor, spy_thief)
* @param list ckeyMinds List of minds keyed by ckeys
* @return list Simulated response
*/
proc/simulateHistory(role = "", list/ckeyMinds = list())
var/list/response = list(
"role" = role,
"history" = list()
)
for (var/ckey in ckeyMinds)
response["history"][ckey] = list(
"selected" = 1,
"seen" = 1
)
return response
/**
* Queries the goonhub API for hisorical antag rounds for the pool of minds given
*
* @param string role Name of the antag role we're picking for (e.g. traitor, spy_thief)
* @param list history List of historical antag data returned by the goonhub API
* @return list List ckeys sorted by weight (highest weight first)
*/
proc/calculateWeightings(role = "", list/history = list())
if (!role)
throw EXCEPTION("No role given")
if (!history.len)
throw EXCEPTION("Empty history given")
var/poolSize = history.len
var/targetPlayRate = config.play_antag_rates[role]
var/list/weightings = list()
//calculate our weightings
for (var/ckey in history)
var/list/details = history[ckey]
var/selected = text2num(details["selected"]) //amount of times selected for antag role in given round type
var/seen = text2num(details["seen"]) //amount of times seen in given round type
var/weight
//players never selected and above min played get highest weightings
if (!selected && seen >= minPlayed)
if (src.debug)
src.debugLog("(Weighting Calc) [ckey] has no selections and [seen] participations. Applying max weight.")
weight = INFINITY
//new players below the min played requirement get lowest weightings
else if (seen < minPlayed)
if (src.debug)
src.debugLog("(Weighting Calc) [ckey] has too few participations ([seen]). Applying min weight.")
weight = 0
//the lower % of rounds played as antag role in given round type, the higher weighting given
else
var/percentSelected = (selected / seen) * 100
weight = (targetPlayRate * poolSize) / percentSelected
if (src.debug)
src.debugLog("(Weighting Calc) [ckey] has [selected] selections and [seen] participations. Calculated weight as [weight] (poolSize: [poolSize]).")
//insert the weighted entry in the right place
var/inserted = 0
for (var/wCkey in weightings)
if (weight >= weightings[wCkey]["weight"]) //highest weights first
var/existingIndex = weightings.Find(wCkey)
weightings.Insert(existingIndex, ckey)
weightings[ckey] = list("weight" = weight, "seen" = seen)
inserted = 1
break
//couldn't find a place for this entry, shove it on the end
if (!inserted)
weightings.Insert(0, ckey)
weightings[ckey] = list("weight" = weight, "seen" = seen)
return weightings
/**
* Queries the goonhub API for hisorical antag rounds for the pool of minds given
* Returns a list of minds that haevn't played up to the percentage of antag rounds defined in config
*
* @param list pool List of minds under consideration for antag picking
* @param string role Name of the antag role we're picking for (e.g. traitor, spy_thief)
* @param int amount Max amount of players to choose for this role
* @param boolean recordChosen When true, triggers a src.recordMultiple() for the chosen players
* @return list List of minds chosen
*/
proc/choose(list/pool = list(), role = "", amount = 0, recordChosen = 0)
if (!pool.len || !role || !amount)
throw EXCEPTION("Incorrect parameters given")
if (src.debug)
src.debugLog("---------- Starting antagWeighter.choose with role: [role] and amount: [amount] ----------")
var/list/apiPayload = list(
"role" = role,
"mode" = ticker.mode.name
)
//Build a couple lists for sending to the API and for easy lookup after
var/pCount = 0
var/list/ckeyMinds = list()
for (var/datum/mind/M in pool)
if (M.ckey)
apiPayload["players\[[pCount]]"] = M.ckey
ckeyMinds[M.ckey] = M
pCount++
if (!ckeyMinds.len)
throw EXCEPTION("No minds with valid ckeys were given")
logTheThing("debug", null, null, "<b>AntagWeighter</b> Selecting [amount] out of [ckeyMinds.len] candidates for [role].")
if (src.debug)
src.debugLog("Sending payload: [json_encode(apiPayload)]")
var/list/response
if (config.goonhub_api_token && apiHandler.enabled)
//YO API WADDUP
try
response = apiHandler.queryAPI("antags/history", apiPayload, 1)
catch ()
//If the API is in the process of failing, we need to gracefully fail so that SOME antags can be picked
response = src.simulateHistory(role, ckeyMinds)
else
//Fallback for no API set, for local dev (or API is unavailable)
response = src.simulateHistory(role, ckeyMinds)
if (response && response["error"])
throw EXCEPTION(response["error"])
var/list/history = response["history"]
if (src.debug)
src.debugLog("History returned: [json_encode(history)]")
history = src.calculateWeightings(role, history)
//Set up segmented list for variance
var/list/historyLookup = list()
historyLookup = history.Copy()
//Build our final list of chosen people, to the max of "amount"
var/cCount = 0
var/list/chosen = list()
for (var/ckey in history)
cCount++
var/cckey
var/weight
var/seen
//Variance triggered, go pick a random player
if (historyLookup.len && prob(src.variance))
cckey = pick(historyLookup)
weight = historyLookup[cckey]["weight"]
seen = historyLookup[cckey]["seen"]
historyLookup -= cckey
if (src.debug)
src.debugLog("Variance triggered, overriding pick with: [cckey]")
//Normal weighted pick
else
cckey = ckey
weight = history[ckey]["weight"]
seen = history[ckey]["seen"]
chosen[ckeyMinds[cckey]] = list("weight" = weight, "seen" = seen)
if (cCount >= amount)
break
if (src.debug)
src.debugLog("Final chosen list: [json_encode(chosen)]")
src.debugLog("---------- Ending antagWeighter.choose ----------")
//Shortcut to record selection for players chosen
if (recordChosen)
var/list/record = list()
for (var/datum/mind/M in chosen)
record[M.ckey] = role
logTheThing("debug", null, null, "<b>AntagWeighter</b> Selected [M.ckey] for [role]. (Weight: [chosen[M]["weight"]], Seen: [chosen[M]["seen"]])")
for (var/datum/mind/M in pool)
if(!M.ckey)
continue
if(M in chosen)
continue
logTheThing("debug", null, null, "<b>AntagWeighter</b> Did <b>not</b> select [M.ckey] for [role]. (Weight: [history[M.ckey]["weight"]], Seen: [history[M.ckey]["seen"]])")
src.recordMultiple(players = record)
return chosen
/**
* Records an antag selection for a single player
*
* @param string role Name of the antag role we're recording a selection for
* @param string ckey Ckey of the player
* @param boolean latejoin Whether this record is a latejoin antag selection
* @return null
*/
proc/record(role = "", ckey = "", latejoin = 0)
if (!role || !ckey)
throw EXCEPTION("Incorrect parameters given")
if (src.debug)
src.debugLog("Recording selection of role: [role] for ckey: [ckey]. latejoin: [latejoin]")
//Fire and forget
apiHandler.queryAPI("antags/record", list(
"role" = role,
"players" = ckey,
"latejoin" = latejoin
))
/**
* Records multiple antag selections at once, reduces API usage
*
* @param list players Specially formatted list of players to record selection for. e.g.
* players = list(
* "ckeyforadude1" = "traitor",
* "ckeyforadude2" = "wraith"
* )
* @return null
*/
proc/recordMultiple(list/players = list())
if (!players.len)
throw EXCEPTION("Incorrect parameters given")
if (src.debug)
src.debugLog("Recording multiple selections for: [json_encode(players)]")
//Build an API-friendly list of players
var/list/apiPlayers = list()
var/count = 0
for (var/ckey in players)
apiPlayers["players\[[count]]\[role]"] = players[ckey]
apiPlayers["players\[[count]]\[ckey]"] = ckey
count++
if (src.debug)
src.debugLog("Players list sending to API: [json_encode(apiPlayers)]")
//Fire and forget
apiHandler.queryAPI("antags/record", apiPlayers)
world/New()
. = ..()
antagWeighter = new()
//antagWeighter = new(1) //Enables debug mode