Skip to content

Commit

Permalink
Weighted Slotting (#366)
Browse files Browse the repository at this point in the history
  • Loading branch information
Doubleumc committed Aug 28, 2024
1 parent bf50f97 commit ad20782
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 18 deletions.
4 changes: 4 additions & 0 deletions code/__DEFINES/stats.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
#define FACEHUG_TIER_3 100
#define FACEHUG_TIER_4 1000

/// Consecutive rounds this player has readied up and failed to get a slot.
#define PLAYER_STAT_UNASSIGNED_ROUND_STREAK "unassigned_round_streak"

// Stat Categories
#define STAT_CATEGORY_MARINE "marine"
#define STAT_CATEGORY_XENO "xeno"
#define STAT_CATEGORY_YAUTJA "yautja"
#define STAT_CATEGORY_MISC "misc"
37 changes: 37 additions & 0 deletions code/__HELPERS/_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,43 @@

return null

/**
* Shuffles a provided list based on the weight of each element.
*
* Higher weight elements have a higher probability of being picked and tend to appear earlier in the list.
* Unweighted elements are never picked and are discarded.
*
* Arguments:
* * list_to_pick - assoc list in the form of: element = weight
*
* Returns list of shuffled weighted elements
*/
/proc/shuffle_weight(list/list_to_pick)
list_to_pick = list_to_pick.Copy() //not inplace

var/total_weight = 0
for(var/item in list_to_pick)
if(list_to_pick[item])
total_weight += list_to_pick[item]
else
list_to_pick -= item //discard unweighted

var/list_to_return = list()

while(length(list_to_pick))
var/target_weight = rand(1, total_weight)
for(var/item in list_to_pick)
var/item_weight = list_to_pick[item]
target_weight -= item_weight

if(target_weight <= 0)
list_to_return += item
list_to_pick -= item
total_weight -= item_weight
break

return list_to_return

/**
* Removes any null entries from the list
* Returns TRUE if the list had nulls, FALSE otherwise
Expand Down
59 changes: 41 additions & 18 deletions code/game/jobs/role_authority.dm
Original file line number Diff line number Diff line change
Expand Up @@ -206,18 +206,46 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
//PART II: Setting up our player variables and lists, to see if we have anyone to destribute.

unassigned_players = list()
for(var/mob/new_player/M in GLOB.player_list) //Get all players who are ready.
if(!M.ready || M.job)
for(var/mob/new_player/player as anything in GLOB.new_player_list)
if(!player.ready || player.job) //get only players who are ready and unassigned
continue

unassigned_players += M
var/datum/preferences/prefs = player.client?.prefs
if(!prefs) //either no client to play, or no preferences
continue

if(prefs.alternate_option == RETURN_TO_LOBBY && !prefs.has_job_priorities()) //only try to assign players that could possibly be assigned
continue

unassigned_players += player

if(!length(unassigned_players)) //If we don't have any players, the round can't start.
unassigned_players = null
return

unassigned_players = shuffle(unassigned_players, 1) //Shuffle the players.
var/list/player_weights = list()
var/debug_total_weight = 0
for(var/mob/new_player/cycled_unassigned as anything in unassigned_players)
var/base_weight = 1 //baseline weighting

var/new_bonus = 0
switch(cycled_unassigned.client.get_total_human_playtime()) //+1 for new players, +2 for really new players
if(0 to 2 HOURS)
new_bonus = 2
if(2 HOURS to 5 HOURS)
new_bonus = 1

var/streak_bonus = max(get_client_stat(cycled_unassigned.client, PLAYER_STAT_UNASSIGNED_ROUND_STREAK) - 2, 0) //+1 per missed round after 2

player_weights[cycled_unassigned] = base_weight + new_bonus + streak_bonus
debug_total_weight += player_weights[cycled_unassigned]
log_debug("ASSIGNMENT: player_weights generated with [length(player_weights)] players and [debug_total_weight] total weight.")

unassigned_players = shuffle_weight(player_weights)
var/list/debug_weight_order = list()
for(var/mob/new_player/cycled_unassigned as anything in unassigned_players)
debug_weight_order += player_weights[cycled_unassigned]
log_debug("ASSIGNMENT: unassigned_players by entry weight: ([debug_weight_order.Join(", ")])")

// How many positions do we open based on total pop
for(var/i in roles_by_name)
Expand Down Expand Up @@ -247,11 +275,11 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
return

log_debug("ASSIGNMENT: Starting prime priority assignments.")
for(var/mob/new_player/cycled_unassigned in shuffle(unassigned_players))
for(var/mob/new_player/cycled_unassigned in unassigned_players)
assign_role_to_player_by_priority(cycled_unassigned, roles_to_assign, unassigned_players, PRIME_PRIORITY)

log_debug("ASSIGNMENT: Starting regular priority assignments.")
for(var/mob/new_player/cycled_unassigned in shuffle(unassigned_players))
for(var/mob/new_player/cycled_unassigned in unassigned_players)
var/player_assigned_job = FALSE

for(var/priority in HIGH_PRIORITY to LOW_PRIORITY)
Expand All @@ -264,20 +292,17 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
break

if(!player_assigned_job)
log_debug("ASSIGNMENT: [cycled_unassigned] was unable to be assigned a job based on preferences and roles to assign. Attempting alternate options.")

switch(cycled_unassigned.client.prefs.alternate_option)
if(GET_RANDOM_JOB)
log_debug("ASSIGNMENT: [cycled_unassigned] has opted for random job alternate option. Finding random job.")
var/iterator = 0
while((cycled_unassigned in unassigned_players) || iterator >= 5)
iterator++
var/random_job_name = pick(roles_to_assign)
var/datum/job/random_job = roles_to_assign[random_job_name]
log_debug("ASSIGNMENT: [cycled_unassigned] is attempting to be assigned to [random_job_name].")

if(assign_role(cycled_unassigned, random_job))
log_debug("ASSIGNMENT: We have randomly assigned [random_job_name] to [cycled_unassigned]")
cycled_unassigned.client.player_data.adjust_stat(PLAYER_STAT_UNASSIGNED_ROUND_STREAK, STAT_CATEGORY_MISC, 0, TRUE)
unassigned_players -= cycled_unassigned

if(random_job.spawn_positions != -1 && random_job.current_positions >= random_job.spawn_positions)
Expand All @@ -288,10 +313,10 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
log_debug("ASSIGNMENT: [cycled_unassigned] was unable to be randomly assigned a job. Something has gone wrong.")

if(BE_MARINE)
log_debug("ASSIGNMENT: [cycled_unassigned] has opted for marine alternate option. Checking if slot is available.")
var/datum/job/marine_job = GET_MAPPED_ROLE(JOB_SQUAD_MARINE)
if(assign_role(cycled_unassigned, marine_job))
log_debug("ASSIGNMENT: We have assigned [marine_job.title] to [cycled_unassigned] via alternate option.")
cycled_unassigned.client.player_data.adjust_stat(PLAYER_STAT_UNASSIGNED_ROUND_STREAK, STAT_CATEGORY_MISC, 0, TRUE)
unassigned_players -= cycled_unassigned

if(marine_job.spawn_positions != -1 && marine_job.current_positions >= marine_job.spawn_positions)
Expand All @@ -305,22 +330,22 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
cycled_unassigned.ready = 0

log_debug("ASSIGNMENT: Assignment complete. Players unassigned: [length(unassigned_players)] Jobs unassigned: [length(roles_to_assign)]")
for(var/mob/new_player/cycled_unassigned in unassigned_players)
cycled_unassigned.client.player_data.adjust_stat(PLAYER_STAT_UNASSIGNED_ROUND_STREAK, STAT_CATEGORY_MISC, 1)

return roles_to_assign

/datum/authority/branch/role/proc/assign_role_to_player_by_priority(mob/new_player/cycled_unassigned, list/roles_to_assign, list/unassigned_players, priority)
log_debug("ASSIGNMENT: We have started cycled through priority [priority] for [cycled_unassigned].")
var/wanted_jobs_by_name = shuffle(cycled_unassigned.client?.prefs?.get_jobs_by_priority(priority))
var/wanted_jobs_by_name = shuffle(cycled_unassigned.client.prefs.get_jobs_by_priority(priority))
var/player_assigned_job = FALSE

for(var/job_name in wanted_jobs_by_name)
log_debug("ASSIGNMENT: We are cycling through wanted jobs and are at [job_name] for [cycled_unassigned].")
if(job_name in roles_to_assign)
log_debug("ASSIGNMENT: We have found [job_name] in roles to assign for [cycled_unassigned].")
var/datum/job/actual_job = roles_to_assign[job_name]

if(assign_role(cycled_unassigned, actual_job))
log_debug("ASSIGNMENT: We have assigned [job_name] to [cycled_unassigned].")
log_debug("ASSIGNMENT: We have assigned [job_name] to [cycled_unassigned] at priority [priority].")
cycled_unassigned.client.player_data?.adjust_stat(PLAYER_STAT_UNASSIGNED_ROUND_STREAK, STAT_CATEGORY_MISC, 0, TRUE)
unassigned_players -= cycled_unassigned

if(actual_job.spawn_positions != -1 && actual_job.current_positions >= actual_job.spawn_positions)
Expand All @@ -331,10 +356,8 @@ I hope it's easier to tell what the heck this proc is even doing, unlike previou
break

if(player_assigned_job)
log_debug("ASSIGNMENT: [cycled_unassigned] has been assigned a job.")
return player_assigned_job

log_debug("ASSIGNMENT: [cycled_unassigned] did not get a job at priority [priority].")
return player_assigned_job

/**
Expand Down
12 changes: 12 additions & 0 deletions code/modules/client/preferences.dm
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,18 @@ var/const/MAX_SAVE_SLOTS = 10

return jobs_to_return

/// Returns TRUE if any job has a priority other than NEVER, FALSE otherwise.
/datum/preferences/proc/has_job_priorities()
if(!length(job_preference_list))
ResetJobs()
return FALSE

for(var/job in job_preference_list)
if(job_preference_list[job] != NEVER_PRIORITY)
return TRUE

return FALSE

/datum/preferences/proc/SetJobDepartment(datum/job/J, priority)
if(!J || priority < 0 || priority > 4)
return FALSE
Expand Down

0 comments on commit ad20782

Please sign in to comment.