mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-18 13:43:27 +00:00
* Vote System: Approval Voting (#73749) ## About The Pull Request Approval Voting is a system in which voters can select as many maps as they want, instead of selecting only one. Final tallies show how many votes each map received, and the winner is the map with the most support. ## Changes since https://github.com/tgstation/tgstation/pull/73413 - Custom votes can now be started using either system - Icon during AV votes indicating your selections - Map population filter counts active players and participating ghosts https://user-images.githubusercontent.com/83487515/222580901-61506cc3-dc42-4435-9775-1e6291a3f734.mp4 ## Why It's Good For The Game First-past-the-post (our current voting system) has flaws such as creating a bunch of wasted votes, in that a large number of selections ultimately have no impact and for example, a map can win a 3 way race 11/10/10, even though 2/3 of the votes were not for that map. This leads to people having to vote strategically, and perhaps not what their true choice is. Approval Voting solves this by instead allowing the player to select all the maps they would like to play, so they can vote for their true preferred choice, as well as alternates. For example, a player that wants Metastation, is okay with Icebox, and doesn't want Delta may feel pressured to vote Icebox if it's in a 2 way race with Delta. AV lets them vote for Meta, and Icebox or as many others as they want as their alternates and creates a more fair outcome of a map vote. Map population filter removing AFK/lobby screen dwellers gives a better number of active players so as to not trip the map filter's population cap earlier than it should. tl;dr: Less of this  ## Changelog 🆑 LT3 rscadd: Added new multi-vote system balance: Map votes are now calculated using multi-vote instead of the old single-vote system admin: Admins can now use either multi-vote or single-vote for custom votes code: Map choice filtering uses active player count, not all connected clients /🆑 * Vote System: Approval Voting --------- Co-authored-by: lessthanthree <83487515+lessthnthree@users.noreply.github.com>
212 lines
6.3 KiB
Plaintext
212 lines
6.3 KiB
Plaintext
|
|
/**
|
|
* # Vote Singleton
|
|
*
|
|
* A singleton datum that represents a type of vote for the voting subsystem.
|
|
*/
|
|
/datum/vote
|
|
/// The name of the vote.
|
|
var/name
|
|
/// If supplied, an override question will be displayed instead of the name of the vote.
|
|
var/override_question
|
|
/// The sound effect played to everyone when this vote is initiated.
|
|
var/vote_sound = 'sound/misc/bloop.ogg'
|
|
/// A list of default choices we have for this vote.
|
|
var/list/default_choices
|
|
/// Does the name of this vote contain the word "vote"?
|
|
var/contains_vote_in_name = FALSE
|
|
/// What message do we want to pass to the player-side vote panel as a tooltip?
|
|
var/message = "Click to initiate a vote."
|
|
|
|
// Internal values used when tracking ongoing votes.
|
|
// Don't mess with these, change the above values / override procs for subtypes.
|
|
/// An assoc list of [all choices] to [number of votes in the current running vote].
|
|
var/list/choices = list()
|
|
/// A assoc list of [ckey] to [what they voted for in the current running vote].
|
|
var/list/choices_by_ckey = list()
|
|
/// The world time this vote was started.
|
|
var/started_time
|
|
/// The time remaining in this vote's run.
|
|
var/time_remaining
|
|
/// The counting method we use for votes.
|
|
var/count_method = VOTE_COUNT_METHOD_SINGLE
|
|
|
|
/**
|
|
* Used to determine if this vote is a possible
|
|
* vote type for the vote subsystem.
|
|
*
|
|
* If FALSE is returned, this vote singleton
|
|
* will not be created when the vote subsystem initializes,
|
|
* meaning no one will be able to hold this vote.
|
|
*/
|
|
/datum/vote/proc/is_accessible_vote()
|
|
return !!length(default_choices)
|
|
|
|
/**
|
|
* Resets our vote to its default state.
|
|
*/
|
|
/datum/vote/proc/reset()
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
choices.Cut()
|
|
choices_by_ckey.Cut()
|
|
started_time = null
|
|
time_remaining = null
|
|
|
|
/**
|
|
* If this vote has a config associated, toggles it between enabled and disabled.
|
|
* Returns TRUE on a successful toggle, FALSE otherwise
|
|
*/
|
|
/datum/vote/proc/toggle_votable(mob/toggler)
|
|
return FALSE
|
|
|
|
/**
|
|
* If this vote has a config associated, returns its value (True or False, usually).
|
|
* If it has no config, returns -1.
|
|
*/
|
|
/datum/vote/proc/is_config_enabled()
|
|
return -1
|
|
|
|
/**
|
|
* Checks if the passed mob can initiate this vote.
|
|
*
|
|
* Return TRUE if the mob can begin the vote, allowing anyone to actually vote on it.
|
|
* Return FALSE if the mob cannot initiate the vote.
|
|
*/
|
|
/datum/vote/proc/can_be_initiated(mob/by_who, forced = FALSE)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
if(started_time)
|
|
var/next_allowed_time = (started_time + CONFIG_GET(number/vote_delay))
|
|
if(next_allowed_time > world.time && !forced)
|
|
message = "A vote was initiated recently. You must wait [DisplayTimeText(next_allowed_time - world.time)] before a new vote can be started!"
|
|
return FALSE
|
|
|
|
message = initial(message)
|
|
return TRUE
|
|
|
|
/**
|
|
* Called prior to the vote being initiated.
|
|
*
|
|
* Return FALSE to prevent the vote from being initiated.
|
|
*/
|
|
/datum/vote/proc/create_vote(mob/vote_creator)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
for(var/key in default_choices)
|
|
choices[key] = 0
|
|
|
|
return TRUE
|
|
|
|
/**
|
|
* Called when this vote is actually initiated.
|
|
*
|
|
* Return a string - the text displayed to the world when the vote is initiated.
|
|
*/
|
|
/datum/vote/proc/initiate_vote(initiator, duration)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
started_time = world.time
|
|
time_remaining = round(duration / 10)
|
|
|
|
return "[contains_vote_in_name ? "[capitalize(name)]" : "[capitalize(name)] vote"] started by [initiator || "Central Command"]."
|
|
|
|
/**
|
|
* Gets the result of the vote.
|
|
*
|
|
* non_voters - a list of all ckeys who didn't vote in the vote.
|
|
*
|
|
* Returns a list of all options that won.
|
|
* If there were no votes at all, the list will be length = 0, non-null.
|
|
* If only one option one, the list will be length = 1.
|
|
* If there was a tie, the list will be length > 1.
|
|
*/
|
|
/datum/vote/proc/get_vote_result(list/non_voters)
|
|
RETURN_TYPE(/list)
|
|
|
|
var/list/winners = list()
|
|
var/highest_vote = 0
|
|
|
|
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
|
|
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
|
|
|
|
// If we're equal to the highest vote, we tie for winner
|
|
else if(vote_count == highest_vote)
|
|
winners += option
|
|
|
|
return winners
|
|
|
|
/**
|
|
* Gets the resulting text displayed when the vote is completed.
|
|
*
|
|
* all_winners - list of all options that won. Can be multiple, in the event of ties.
|
|
* real_winner - the option that actually won.
|
|
* non_voters - a list of all ckeys who didn't vote in the vote.
|
|
*
|
|
* 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")
|
|
|
|
for(var/option in choices)
|
|
returned_text += "\n[span_bold(option)]: [choices[option]]"
|
|
|
|
returned_text += "\n"
|
|
returned_text += get_winner_text(all_winners, real_winner, non_voters)
|
|
|
|
return returned_text
|
|
|
|
/**
|
|
* Gets the text that displays the winning options within the result text.
|
|
*
|
|
* all_winners - list of all options that won. Can be multiple, in the event of ties.
|
|
* real_winner - the option that actually won.
|
|
* non_voters - a list of all ckeys who didn't vote in the vote.
|
|
*
|
|
* Return a formatted string of text to be displayed to everyone.
|
|
*/
|
|
/datum/vote/proc/get_winner_text(list/all_winners, real_winner, list/non_voters)
|
|
var/returned_text = ""
|
|
if(length(all_winners) > 1)
|
|
returned_text += "\n[span_bold("Vote Tied Between:")]"
|
|
for(var/a_winner in all_winners)
|
|
returned_text += "\n\t[a_winner]"
|
|
|
|
returned_text += span_bold("\nVote Result: [real_winner]")
|
|
return returned_text
|
|
|
|
/**
|
|
* How this vote handles a tiebreaker between multiple winners.
|
|
*/
|
|
/datum/vote/proc/tiebreaker(list/winners)
|
|
return pick(winners)
|
|
|
|
/**
|
|
* Called when a vote is actually all said and done.
|
|
* Apply actual vote effects here.
|
|
*/
|
|
/datum/vote/proc/finalize_vote(winning_option)
|
|
return
|