From ad2078240ac67199d197728ad06684bd4cc30ea6 Mon Sep 17 00:00:00 2001 From: Doubleumc Date: Wed, 28 Aug 2024 14:41:54 -0400 Subject: [PATCH] Weighted Slotting (#366) --- code/__DEFINES/stats.dm | 4 ++ code/__HELPERS/_lists.dm | 37 +++++++++++++++++++ code/game/jobs/role_authority.dm | 59 +++++++++++++++++++++--------- code/modules/client/preferences.dm | 12 ++++++ 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/code/__DEFINES/stats.dm b/code/__DEFINES/stats.dm index 1810d01e1d..bc7cbfc4a4 100644 --- a/code/__DEFINES/stats.dm +++ b/code/__DEFINES/stats.dm @@ -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" diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index fe15e6d84c..ff63e33beb 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -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 diff --git a/code/game/jobs/role_authority.dm b/code/game/jobs/role_authority.dm index 42ffc22570..de2fb256ef 100644 --- a/code/game/jobs/role_authority.dm +++ b/code/game/jobs/role_authority.dm @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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 /** diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 09d606c502..26bdaa8941 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -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