Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Weighted Slotting #366

Merged
merged 12 commits into from
Aug 28, 2024
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 @@ -931,6 +931,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
Loading