Files
Bubberstation/code/datums/votes/_vote_datum.dm
SkyratBot 03854f9abe [MIRROR] Vote System: Approval Voting [MDB IGNORE] (#19666)
* 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

![image](https://user-images.githubusercontent.com/83487515/222860681-210f2d7e-2368-4d42-84d5-6de838995e50.png)

## 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>
2023-03-04 17:49:21 -08:00

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