|
|
|
@ -19,50 +19,30 @@
|
|
|
|
|
* You should have received a copy of the GNU General Public License |
|
|
|
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
|
|
|
*/ |
|
|
|
|
class VotingModel extends AcediaObject; |
|
|
|
|
class VotingModel extends AcediaObject |
|
|
|
|
dependsOn(MathApi); |
|
|
|
|
|
|
|
|
|
//! This class counts votes according to the configured voting policies. |
|
|
|
|
//! |
|
|
|
|
//! Its main purpose is to separate the voting logic from the voting interface, making |
|
|
|
|
//! the implementation simpler and the logic easier to test. |
|
|
|
|
//! Its main purpose is to separate the voting logic from the voting interface, |
|
|
|
|
//! making the implementation simpler and the logic easier to test. |
|
|
|
|
//! |
|
|
|
|
//! # Usage |
|
|
|
|
//! |
|
|
|
|
//! 1. Allocate an instance of the [`VotingModel`] class. |
|
|
|
|
//! 2. Call [`Initialize()`] to set the required policies. |
|
|
|
|
//! 2. Call [`Start()`] to start voting with required policies. |
|
|
|
|
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote. |
|
|
|
|
//! You can change this set at any time. The method used to recount the votes will depend on |
|
|
|
|
//! the policies set during the previous [`Initialize()`] call. |
|
|
|
|
//! You can change this set at any time before the voting has concluded. |
|
|
|
|
//! The method used to recount the votes will depend on the policies set |
|
|
|
|
//! during the previous [`Initialize()`] call. |
|
|
|
|
//! 4. Use [`CastVote()`] to add a vote from a user. |
|
|
|
|
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], check [`GetStatus()`] to |
|
|
|
|
//! see if the voting has concluded. |
|
|
|
|
//! Once voting has concluded, the result cannot be changed, so you can release the reference |
|
|
|
|
//! to the [`VotingModel`] object. |
|
|
|
|
|
|
|
|
|
/// Describes how [`VotingModel`] should react when a user performs potentially illegal actions. |
|
|
|
|
/// |
|
|
|
|
/// Illegal here means that either corresponding operation won't be permitted or any vote made |
|
|
|
|
/// would be considered invalid. |
|
|
|
|
/// |
|
|
|
|
/// Leaving means simply no longer being in a potential pool of voters, which includes actually |
|
|
|
|
/// leaving the game and simply losing rights to vote. |
|
|
|
|
enum VotingPolicies { |
|
|
|
|
/// Anything that can be considered illegal actions is prohibited. |
|
|
|
|
/// |
|
|
|
|
/// Leaving (or losing rights to vote) during voting will make a vote invalid. |
|
|
|
|
/// |
|
|
|
|
/// Changing vote is forbidden. |
|
|
|
|
VP_Restrictive, |
|
|
|
|
/// Leaving during voting is allowed. Changing a vote is not allowed. |
|
|
|
|
VP_CanLeave, |
|
|
|
|
/// Changing one's vote is allowed. If a user leaves during voting, their vote will be invalid. |
|
|
|
|
VP_CanChangeVote, |
|
|
|
|
/// Leaving during voting and changing a vote is allowed. Leaving means losing rights to vote. |
|
|
|
|
/// |
|
|
|
|
/// Currently, this policy allows all available options, but this may change in the future if |
|
|
|
|
/// more options are added. |
|
|
|
|
VP_CanLeaveAndChangeVote |
|
|
|
|
}; |
|
|
|
|
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], |
|
|
|
|
//! check [`GetStatus()`] to see if the voting has concluded. |
|
|
|
|
//! Once voting has concluded, the result cannot be changed, so you can |
|
|
|
|
//! release the reference to the [`VotingModel`] object. |
|
|
|
|
//! 6. Alternatively, before voting has concluded naturally, you can use |
|
|
|
|
//! [`ForceEnding()`] method to immediately end voting with result being |
|
|
|
|
//! determined by provided [`ForceEndingType`] argument. |
|
|
|
|
|
|
|
|
|
/// Current state of voting for this model. |
|
|
|
|
enum VotingModelStatus { |
|
|
|
@ -73,9 +53,7 @@ enum VotingModelStatus {
|
|
|
|
|
/// Voting has ended with majority for its success |
|
|
|
|
VPM_Success, |
|
|
|
|
/// Voting has ended with majority for its failure |
|
|
|
|
VPM_Failure, |
|
|
|
|
/// Voting has ended in a draw |
|
|
|
|
VPM_Draw |
|
|
|
|
VPM_Failure |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
/// A result of user trying to make a vote |
|
|
|
@ -102,9 +80,20 @@ enum PlayerVoteStatus {
|
|
|
|
|
PVS_VoteAgainst |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
/// Types of possible outcomes when forcing a voting to end |
|
|
|
|
enum ForceEndingType { |
|
|
|
|
/// Result will be decided by the votes that already have been cast |
|
|
|
|
FET_CurrentLeader, |
|
|
|
|
/// Voting will end in success |
|
|
|
|
FET_Success, |
|
|
|
|
/// Voting will end in failure |
|
|
|
|
FET_Failure |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
var private VotingModelStatus status; |
|
|
|
|
|
|
|
|
|
var private bool policyCanLeave, policyCanChangeVote; |
|
|
|
|
/// Specifies whether draw would count as a victory for corresponding voting. |
|
|
|
|
var private bool policyDrawWinsVoting; |
|
|
|
|
|
|
|
|
|
var private array<UserID> votesFor, votesAgainst; |
|
|
|
|
/// Votes of people that voted before, but then were forbidden to vote |
|
|
|
@ -115,8 +104,6 @@ var private array<UserID> allowedVoters;
|
|
|
|
|
|
|
|
|
|
protected function Constructor() { |
|
|
|
|
status = VPM_Uninitialized; |
|
|
|
|
policyCanLeave = false; |
|
|
|
|
policyCanChangeVote = false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
protected function Finalizer() { |
|
|
|
@ -133,35 +120,42 @@ protected function Finalizer() {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Initializes voting by providing it with a set of policies to follow. |
|
|
|
|
public final function Initialize(VotingPolicies policies) { |
|
|
|
|
if (status == VPM_Uninitialized) { |
|
|
|
|
policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote); |
|
|
|
|
policyCanChangeVote = |
|
|
|
|
(policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote); |
|
|
|
|
/// |
|
|
|
|
/// The only available policy is configuring whether draw means victory or loss |
|
|
|
|
/// in voting. |
|
|
|
|
/// |
|
|
|
|
/// Can only be called once, after that will do nothing. |
|
|
|
|
public final function Start(bool drawWinsVoting) { |
|
|
|
|
if (status != VPM_Uninitialized) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
policyDrawWinsVoting = drawWinsVoting; |
|
|
|
|
status = VPM_InProgress; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns whether voting has already concluded. |
|
|
|
|
/// |
|
|
|
|
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check |
|
|
|
|
/// whether either of them was enough to conclude the voting result. |
|
|
|
|
/// This method should be checked after both [`CastVote()`] and |
|
|
|
|
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to |
|
|
|
|
/// conclude the voting result. |
|
|
|
|
public final function bool HasEnded() { |
|
|
|
|
return (status != VPM_Uninitialized && status != VPM_InProgress); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns current status of voting. |
|
|
|
|
/// |
|
|
|
|
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check |
|
|
|
|
/// whether either of them was enough to conclude the voting result. |
|
|
|
|
/// This method should be checked after both [`CastVote()`] and |
|
|
|
|
/// [`UpdatePotentialVoters()`] to check whether either of them was enough to |
|
|
|
|
/// conclude the voting result. |
|
|
|
|
public final function VotingModelStatus GetStatus() { |
|
|
|
|
return status; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Changes set of [`User`]s that are allowed to vote. |
|
|
|
|
/// |
|
|
|
|
/// Generally you want to provide this method with a list of current players, optionally filtered |
|
|
|
|
/// from spectators, users not in priviledged group or any other relevant criteria. |
|
|
|
|
/// Generally you want to provide this method with a list of current players, |
|
|
|
|
/// optionally filtered from spectators, users not in priviledged group or any |
|
|
|
|
/// other relevant criteria. |
|
|
|
|
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { |
|
|
|
|
local int i; |
|
|
|
|
|
|
|
|
@ -178,8 +172,7 @@ public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
|
|
|
|
|
|
|
|
|
|
/// Attempts to add a vote from specified user. |
|
|
|
|
/// |
|
|
|
|
/// Adding a vote can fail if [`voter`] isn't allowed to vote or has already voted and policies |
|
|
|
|
/// forbid changing that vote. |
|
|
|
|
/// Adding a vote can fail if [`voter`] isn't allowed to vote. |
|
|
|
|
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) { |
|
|
|
|
local bool votesSameWay; |
|
|
|
|
local PlayerVoteStatus currentVote; |
|
|
|
@ -190,15 +183,12 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
|
|
|
|
|
if (!IsVotingAllowedFor(voter)) { |
|
|
|
|
return VFR_NotAllowed; |
|
|
|
|
} |
|
|
|
|
currentVote = HasVoted(voter); |
|
|
|
|
currentVote = GetVote(voter); |
|
|
|
|
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) |
|
|
|
|
|| (!voteForSuccess && currentVote == PVS_VoteAgainst); |
|
|
|
|
if (votesSameWay) { |
|
|
|
|
return VFR_AlreadyVoted; |
|
|
|
|
} |
|
|
|
|
if (!policyCanChangeVote && currentVote != PVS_NoVote) { |
|
|
|
|
return VFR_CannotChangeVote; |
|
|
|
|
} |
|
|
|
|
EraseVote(voter); |
|
|
|
|
voter.NewRef(); |
|
|
|
|
if (voteForSuccess) { |
|
|
|
@ -210,12 +200,11 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
|
|
|
|
|
return VFR_Success; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Checks if the provided user is allowed to vote based on the current list of potential voters. |
|
|
|
|
/// Checks if the provided user is allowed to vote based on the current list of |
|
|
|
|
/// potential voters. |
|
|
|
|
/// |
|
|
|
|
/// The right to vote is decided solely by the list of potential voters set using |
|
|
|
|
/// [`UpdatePotentialVoters()`]. |
|
|
|
|
/// However, even if a user is on the list of potential voters, they may not be allowed to vote if |
|
|
|
|
/// they have already cast a vote and the voting policies do not allow vote changes. |
|
|
|
|
/// The right to vote is decided solely by the list of potential voters set |
|
|
|
|
/// using [`UpdatePotentialVoters()`]. |
|
|
|
|
/// |
|
|
|
|
/// Returns true if the user is allowed to vote, false otherwise. |
|
|
|
|
public final function bool IsVotingAllowedFor(UserID voter) { |
|
|
|
@ -234,9 +223,8 @@ public final function bool IsVotingAllowedFor(UserID voter) {
|
|
|
|
|
|
|
|
|
|
/// Returns the current vote status for the given voter. |
|
|
|
|
/// |
|
|
|
|
/// If the voter was previously allowed to vote, voted, and had their right to vote revoked, their |
|
|
|
|
/// vote will only count if policies allow voters to leave mid-vote. |
|
|
|
|
/// Otherwise, the method will return [`PVS_NoVote`]. |
|
|
|
|
/// If the voter was previously allowed to vote, voted, and had their right to |
|
|
|
|
/// vote revoked, their vote won't count. |
|
|
|
|
public final function PlayerVoteStatus GetVote(UserID voter) { |
|
|
|
|
local int i; |
|
|
|
|
|
|
|
|
@ -253,94 +241,99 @@ public final function PlayerVoteStatus GetVote(UserID voter) {
|
|
|
|
|
return PVS_VoteAgainst; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (policyCanLeave) { |
|
|
|
|
for (i = 0; i < storedVotesFor.length; i += 1) { |
|
|
|
|
if (voter.IsEqual(storedVotesFor[i])) { |
|
|
|
|
return PVS_VoteFor; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
for (i = 0; i < storedVotesAgainst.length; i += 1) { |
|
|
|
|
if (voter.IsEqual(storedVotesAgainst[i])) { |
|
|
|
|
return PVS_VoteAgainst; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return PVS_NoVote; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns amount of current valid votes for the success of this voting. |
|
|
|
|
public final function int GetVotesFor() { |
|
|
|
|
if (policyCanLeave) { |
|
|
|
|
return votesFor.length + storedVotesFor.length; |
|
|
|
|
} else { |
|
|
|
|
return votesFor.length; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns amount of current valid votes against the success of this voting. |
|
|
|
|
public final function int GetVotesAgainst() { |
|
|
|
|
if (policyCanLeave) { |
|
|
|
|
return votesAgainst.length + storedVotesAgainst.length; |
|
|
|
|
} else { |
|
|
|
|
return votesAgainst.length; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Returns amount of users that are currently allowed to vote in this voting. |
|
|
|
|
public final function int GetTotalPossibleVotes() { |
|
|
|
|
if (policyCanLeave) { |
|
|
|
|
return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length; |
|
|
|
|
} else { |
|
|
|
|
return allowedVoters.length; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Checks if provided user has already voted. |
|
|
|
|
// Only checks among users that are currently allowed to vote, even if their past vote still counts. |
|
|
|
|
private final function PlayerVoteStatus HasVoted(UserID voter) { |
|
|
|
|
local int i; |
|
|
|
|
/// Checks whether, if stopped now, voting will win. |
|
|
|
|
public final function bool IsVotingWinning() { |
|
|
|
|
if (status == VPM_Success) return true; |
|
|
|
|
if (status == VPM_Failure) return false; |
|
|
|
|
if (GetVotesFor() > GetVotesAgainst()) return true; |
|
|
|
|
if (GetVotesFor() < GetVotesAgainst()) return false; |
|
|
|
|
|
|
|
|
|
if (voter == none) { |
|
|
|
|
return PVS_NoVote; |
|
|
|
|
} |
|
|
|
|
for (i = 0; i < votesFor.length; i += 1) { |
|
|
|
|
if (voter.IsEqual(votesFor[i])) { |
|
|
|
|
return PVS_VoteFor; |
|
|
|
|
return policyDrawWinsVoting; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Forcibly ends the voting, deciding winner depending on the argument. |
|
|
|
|
/// |
|
|
|
|
/// Only does anything if voting is currently in progress |
|
|
|
|
/// (in `VPM_InProgress` state). |
|
|
|
|
/// |
|
|
|
|
/// By default decides result by the votes that already have been cast. |
|
|
|
|
/// |
|
|
|
|
/// Returns `true` only if voting was actually ended with this call. |
|
|
|
|
public final function bool ForceEnding(optional ForceEndingType type) { |
|
|
|
|
if (status != VPM_InProgress) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
for (i = 0; i < votesAgainst.length; i += 1) { |
|
|
|
|
if (voter.IsEqual(votesAgainst[i])) { |
|
|
|
|
return PVS_VoteAgainst; |
|
|
|
|
switch (type) { |
|
|
|
|
case FET_CurrentLeader: |
|
|
|
|
if (IsVotingWinning()) { |
|
|
|
|
status = VPM_Success; |
|
|
|
|
} else { |
|
|
|
|
status = VPM_Failure; |
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
case FET_Success: |
|
|
|
|
status = VPM_Success; |
|
|
|
|
break; |
|
|
|
|
case FET_Failure: |
|
|
|
|
default: |
|
|
|
|
status = VPM_Failure; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
return PVS_NoVote; |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private final function RecountVotes() { |
|
|
|
|
local bool canOverturn, everyoneVoted; |
|
|
|
|
local MathApi.IntegerDivisionResult divisionResult; |
|
|
|
|
local int winningScore, losingScore; |
|
|
|
|
local int totalPossibleVotes; |
|
|
|
|
local int totalVotesFor, totalVotesAgainst; |
|
|
|
|
local int lowerVoteCount, upperVoteCount, undecidedVoteCount; |
|
|
|
|
|
|
|
|
|
if (status != VPM_InProgress) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
totalVotesFor = GetVotesFor(); |
|
|
|
|
totalVotesAgainst = GetVotesAgainst(); |
|
|
|
|
totalPossibleVotes = GetTotalPossibleVotes(); |
|
|
|
|
lowerVoteCount = Min(totalVotesFor, totalVotesAgainst); |
|
|
|
|
upperVoteCount = Max(totalVotesFor, totalVotesAgainst); |
|
|
|
|
undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount); |
|
|
|
|
everyoneVoted = (undecidedVoteCount <= 0); |
|
|
|
|
canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount; |
|
|
|
|
if (everyoneVoted || !canOverturn) { |
|
|
|
|
if (totalVotesFor > totalVotesAgainst) { |
|
|
|
|
divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2); |
|
|
|
|
if (divisionResult.remainder == 1) { |
|
|
|
|
// For odd amount of voters winning is simply majority |
|
|
|
|
winningScore = divisionResult.quotient + 1; |
|
|
|
|
} else { |
|
|
|
|
if (policyDrawWinsVoting) { |
|
|
|
|
// For even amount of voters, exactly half is enough if draw means victory |
|
|
|
|
winningScore = divisionResult.quotient; |
|
|
|
|
} else { |
|
|
|
|
// Otherwise - majority |
|
|
|
|
winningScore = divisionResult.quotient + 1; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// The `winningScore` represents the number of votes required for a mean victory. |
|
|
|
|
// If the number of votes against the mean is less than or equal to |
|
|
|
|
// `totalPossibleVotes - winningScore`, then victory is still possible. |
|
|
|
|
// However, if there is even one additional vote against, then victory is no longer achievable |
|
|
|
|
// and a loss is inevitable. |
|
|
|
|
losingScore = (totalPossibleVotes - winningScore) + 1; |
|
|
|
|
// `totalPossibleVotes < losingScore + winningScore`, so only one of these inequalities |
|
|
|
|
// can be satisfied at a time |
|
|
|
|
if (GetVotesFor() >= winningScore) { |
|
|
|
|
status = VPM_Success; |
|
|
|
|
} else if (totalVotesFor < totalVotesAgainst) { |
|
|
|
|
} else if (GetVotesAgainst() >= losingScore) { |
|
|
|
|
status = VPM_Failure; |
|
|
|
|
} else { |
|
|
|
|
status = VPM_Draw; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|