From b7cca4c15e702d394253b8f234975a278f4a703f Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:47:04 +0100 Subject: [PATCH] [MIRROR] Votes now show the percentage of total votes each choice got | Adds weighted random vote type | Map votes are now weighted random [MDB IGNORE] (#25702) * Votes now show the percentage of total votes each choice got | Adds weighted random vote type | Map votes are now weighted random (#80362) ## About The Pull Request See the title. ## Why It's Good For The Game Mothblocks wanted map votes to not be simple winner take all and wanted it to be randomly chosen based on votes. ![image](https://github.com/tgstation/tgstation/assets/12817816/7435b444-dcd7-4aa5-a747-4bafe434ebdb) ## Changelog :cl: add: Map Votes are now weighted random. add: Custom Votes can now take advantage of Weighted Random winner selection del: Removed Herobrine from the game /:cl: --------- Co-authored-by: Mothblocks <35135081+Mothblocks@ users.noreply.github.com> * Votes now show the percentage of total votes each choice got | Adds weighted random vote type | Map votes are now weighted random --------- Co-authored-by: Zephyr <12817816+ZephyrTFA@users.noreply.github.com> Co-authored-by: Mothblocks <35135081+Mothblocks@ users.noreply.github.com> --- code/__DEFINES/maths.dm | 4 ++ code/__DEFINES/subsystems.dm | 7 +++ code/controllers/subsystem/vote.dm | 24 ++++++++- code/datums/votes/_vote_datum.dm | 79 ++++++++++++++++++++++-------- code/datums/votes/custom_vote.dm | 22 +++++++-- code/datums/votes/map_vote.dm | 1 + 6 files changed, 111 insertions(+), 26 deletions(-) diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index 1260e3daf03..ad35588f736 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -43,6 +43,10 @@ #define ROUND_UP(x) ( -round(-(x))) +/// Returns the number of digits in a number. Only works on whole numbers. +/// This is marginally faster than string interpolation -> length +#define DIGITS(x) (ROUND_UP(log(10, x))) + // round() acts like floor(x, 1) by default but can't handle other values #define FLOOR(x, y) ( round((x) / (y)) * (y) ) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index c6f412f15c0..d227744ddd1 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -339,3 +339,10 @@ #define VOTE_COUNT_METHOD_SINGLE 1 /// Approval voting. Any number of selections per person, and the selection with the most votes wins. #define VOTE_COUNT_METHOD_MULTI 2 + +/// The choice with the most votes wins. Ties are broken by the first choice to reach that number of votes. +#define VOTE_WINNER_METHOD_SIMPLE "Simple" +/// The winning choice is selected randomly based on the number of votes each choice has. +#define VOTE_WINNER_METHOD_WEIGHTED_RANDOM "Weighted Random" +/// There is no winner for this vote. +#define VOTE_WINNER_METHOD_NONE "None" diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index e68ee9ee1f2..2d134eacb66 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -86,11 +86,31 @@ SUBSYSTEM_DEF(vote) // Announce the results of the vote to the world. var/to_display = current_vote.get_result_text(winners, final_winner, non_voters) - log_vote(to_display) + var/total_votes = 0 + var/list/vote_choice_data = list() + for(var/choice in current_vote.choices) + var/choice_votes = current_vote.choices[choice] + vote_choice_data["[choice]"] = choice_votes + + // stringify the winners to prevent potential unimplemented serialization errors. + // Perhaps this can be removed in the future and we assert that vote choices must implement serialization. + var/final_winner_string = final_winner && "[final_winner]" + var/list/winners_string = list() + for(var/winner in winners) + winners_string += "[winner]" + + var/list/vote_log_data = list( + "choices" = vote_choice_data, + "total" = total_votes, + "winners" = winners_string, + "final_winner" = final_winner_string, + ) + var/log_string = replacetext(to_display, "\n", "\\n") // 'keep' the newlines, but dont actually print them as newlines + log_vote(log_string, vote_log_data) to_chat(world, span_infoplain(vote_font("\n[to_display]"))) // Finally, doing any effects on vote completion - if (final_winner) // if no one voted final_winner will be null + if (final_winner) // if no one voted, or the vote cannot be won, final_winner will be null current_vote.finalize_vote(final_winner) /** diff --git a/code/datums/votes/_vote_datum.dm b/code/datums/votes/_vote_datum.dm index b1560bf8ecd..a882511b511 100644 --- a/code/datums/votes/_vote_datum.dm +++ b/code/datums/votes/_vote_datum.dm @@ -30,6 +30,8 @@ var/time_remaining /// The counting method we use for votes. var/count_method = VOTE_COUNT_METHOD_SINGLE + /// The method for selecting a winner. + var/winner_method = VOTE_WINNER_METHOD_SIMPLE /** * Used to determine if this vote is a possible @@ -123,33 +125,41 @@ */ /datum/vote/proc/get_vote_result(list/non_voters) RETURN_TYPE(/list) + SHOULD_CALL_PARENT(TRUE) + + switch(winner_method) + if(VOTE_WINNER_METHOD_NONE) + return list() + if(VOTE_WINNER_METHOD_SIMPLE) + return get_simple_winner() + if(VOTE_WINNER_METHOD_WEIGHTED_RANDOM) + return get_random_winner() + + stack_trace("invalid select winner method: [winner_method]. Defaulting to simple.") + return get_simple_winner() - var/list/winners = list() +/// Gets the winner of the vote, selecting the choice with the most votes. +/datum/vote/proc/get_simple_winner() var/highest_vote = 0 + var/list/current_winners = list() for(var/option in choices) - var/vote_count = choices[option] - // If we currently have no winners... - if(!length(winners)) - // And the current option has any votes, it's the new highest. - if(vote_count > 0) - winners += option - highest_vote = vote_count + if(vote_count < highest_vote) continue - // If we're greater than, and NOT equal to, the highest vote, - // we are the new supreme winner - clear all others if(vote_count > highest_vote) - winners.Cut() - winners += option highest_vote = vote_count + current_winners = list(option) + continue + current_winners += option - // If we're equal to the highest vote, we tie for winner - else if(vote_count == highest_vote) - winners += option + return length(current_winners) ? current_winners : list() - return winners +/// Gets the winner of the vote, selecting a random choice from all choices based on their vote count. +/datum/vote/proc/get_random_winner() + var/winner = pick_weight(choices) + return winner ? list(winner) : list() /** * Gets the resulting text displayed when the vote is completed. @@ -161,17 +171,46 @@ * Return a formatted string of text to be displayed to everyone. */ /datum/vote/proc/get_result_text(list/all_winners, real_winner, list/non_voters) - if(length(all_winners) <= 0 || !real_winner) - return span_bold("Vote Result: Inconclusive - No Votes!") - var/returned_text = "" if(override_question) returned_text += span_bold(override_question) else returned_text += span_bold("[capitalize(name)] Vote") + returned_text += "\nWinner Selection: " + switch(winner_method) + if(VOTE_WINNER_METHOD_NONE) + returned_text += "None" + if(VOTE_WINNER_METHOD_WEIGHTED_RANDOM) + returned_text += "Weighted Random" + else + returned_text += "Simple" + + var/total_votes = 0 // for determining percentage of votes + for(var/option in choices) + total_votes += choices[option] + + if(total_votes <= 0) + return span_bold("Vote Result: Inconclusive - No Votes!") + + returned_text += "\nResults:" for(var/option in choices) - returned_text += "\n[span_bold(option)]: [choices[option]]" + returned_text += "\n" + var/votes = choices[option] + var/percentage_text = "" + if(votes > 0) + var/actual_percentage = round((votes / total_votes) * 100, 0.1) + var/text = "[actual_percentage]" + var/spaces_needed = 5 - length(text) + for(var/_ in 1 to spaces_needed) + returned_text += " " + percentage_text += "[text]%" + else + percentage_text = " 0%" + returned_text += "[percentage_text] | [span_bold(option)]: [choices[option]]" + + if(!real_winner) // vote has no winner or cannot be won, but still had votes + return returned_text returned_text += "\n" returned_text += get_winner_text(all_winners, real_winner, non_voters) diff --git a/code/datums/votes/custom_vote.dm b/code/datums/votes/custom_vote.dm index 534377b3d69..5960e7dff0e 100644 --- a/code/datums/votes/custom_vote.dm +++ b/code/datums/votes/custom_vote.dm @@ -29,6 +29,24 @@ return forced /datum/vote/custom_vote/create_vote(mob/vote_creator) + var/custom_win_method = tgui_input_list( + vote_creator, + "How should the vote winner be determined?", + "Winner Method", + list("Simple", "Weighted Random", "No Winner"), + default = "Simple", + ) + switch(custom_win_method) + if("Simple") + winner_method = VOTE_WINNER_METHOD_SIMPLE + if("Weighted Random") + winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM + if("No Winner") + winner_method = VOTE_WINNER_METHOD_NONE + else + to_chat(vote_creator, span_boldwarning("Unknown winner method. Contact a coder.")) + return FALSE + override_question = tgui_input_text(vote_creator, "What is the vote for?", "Custom Vote") if(!override_question) return FALSE @@ -52,8 +70,4 @@ . = ..() . += "\n[override_question]" -// There are no winners or losers for custom votes -/datum/vote/custom_vote/get_winner_text(list/all_winners, real_winner, list/non_voters) - return "[span_bold("Did not vote:")] [length(non_voters)]" - #undef MAX_CUSTOM_VOTE_OPTIONS diff --git a/code/datums/votes/map_vote.dm b/code/datums/votes/map_vote.dm index 323ee29ccc2..f65b06a1d8c 100644 --- a/code/datums/votes/map_vote.dm +++ b/code/datums/votes/map_vote.dm @@ -2,6 +2,7 @@ name = "Map" message = "Vote for next round's map!" count_method = VOTE_COUNT_METHOD_MULTI + winner_method = VOTE_WINNER_METHOD_WEIGHTED_RANDOM /datum/vote/map_vote/New() . = ..()