|
|
@ -21,32 +21,90 @@ |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
class VotingModel extends AcediaObject; |
|
|
|
class VotingModel extends AcediaObject; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//! Class for counting votes according to voting policies it was configured with. |
|
|
|
|
|
|
|
//! |
|
|
|
|
|
|
|
//! Its main purpose is to separate voting logic from the voting interface, simplifying its |
|
|
|
|
|
|
|
//! implementation and making actual logic easily testable. |
|
|
|
|
|
|
|
//! |
|
|
|
|
|
|
|
//! # Usage |
|
|
|
|
|
|
|
//! |
|
|
|
|
|
|
|
//! 1. Allocate instance of [`VotingModel`]; |
|
|
|
|
|
|
|
//! 2. Call [`Initialize()`] to set required policies; |
|
|
|
|
|
|
|
//! 3. Use [`UpdatePotentialVoters()`] to specify which users are allowed to vote. |
|
|
|
|
|
|
|
//! This set can be changed at any point in time later. |
|
|
|
|
|
|
|
//! How votes will be recounted will depend on policies set in previous [`Initialize()`] call. |
|
|
|
|
|
|
|
//! 4. Use [`AddVote()`] to add a vote from users. |
|
|
|
|
|
|
|
//! 5. Check [`GetStatus()`] after either [`UpdatePotentialVoters()`] or [`AddVote()`] calls to |
|
|
|
|
|
|
|
//! check whether voting has concluded. |
|
|
|
|
|
|
|
//! You can release [`VotingModel`]'s reference after you've gotten the result, since once |
|
|
|
|
|
|
|
//! voting has concluded, its result cannot be changed. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Describes how [`VotingModel`] must react to user making potentially illegal reactions. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Illegal here means that either corresponding operation won't be permitted or any vote made |
|
|
|
|
|
|
|
/// would be considered invalid. |
|
|
|
enum VotingPolicies { |
|
|
|
enum VotingPolicies { |
|
|
|
|
|
|
|
/// Consider illegal anything that can be illegal. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Leaving during voting will make a vote invalid. |
|
|
|
|
|
|
|
/// Leaving means losing rights to vote. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Changing vote is forbidden. |
|
|
|
VP_Restrictive, |
|
|
|
VP_Restrictive, |
|
|
|
|
|
|
|
/// Leaving during voting is legal, everything else potentially illegal is still illegal. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Changing vote is forbidden. |
|
|
|
VP_CanLeave, |
|
|
|
VP_CanLeave, |
|
|
|
|
|
|
|
/// Changing one's vote is allowed, everything else potentially illegal is still illegal. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Leaving during voting will make a vote invalid. |
|
|
|
|
|
|
|
/// Leaving means losing rights to vote. |
|
|
|
VP_CanChangeVote, |
|
|
|
VP_CanChangeVote, |
|
|
|
|
|
|
|
/// Leaving during voting and changing a vote is allowed, everything else potentially illegal is |
|
|
|
|
|
|
|
/// still illegal. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Leaving means losing rights to vote. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// Currently this flag means that every option it provides is allowed, but this might change in |
|
|
|
|
|
|
|
/// the future if more options would be added. |
|
|
|
VP_CanLeaveAndChangeVote |
|
|
|
VP_CanLeaveAndChangeVote |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Current state of voting for this model. |
|
|
|
enum VotingModelStatus { |
|
|
|
enum VotingModelStatus { |
|
|
|
|
|
|
|
/// Voting hasn't even started, waiting for [`Initialize()`] call |
|
|
|
VPM_Uninitialized, |
|
|
|
VPM_Uninitialized, |
|
|
|
|
|
|
|
/// Voting is currently in progress |
|
|
|
VPM_InProgress, |
|
|
|
VPM_InProgress, |
|
|
|
|
|
|
|
/// Voting has ended with majority for its success |
|
|
|
VPM_Success, |
|
|
|
VPM_Success, |
|
|
|
|
|
|
|
/// Voting has ended with majority for its failure |
|
|
|
VPM_Failure, |
|
|
|
VPM_Failure, |
|
|
|
|
|
|
|
/// Voting has ended in a draw |
|
|
|
VPM_Draw |
|
|
|
VPM_Draw |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// A result of user trying to make a vote |
|
|
|
enum VotingResult { |
|
|
|
enum VotingResult { |
|
|
|
|
|
|
|
/// Vote accepted |
|
|
|
VFR_Success, |
|
|
|
VFR_Success, |
|
|
|
|
|
|
|
/// Voting is not allowed for this particular user |
|
|
|
VFR_NotAllowed, |
|
|
|
VFR_NotAllowed, |
|
|
|
|
|
|
|
/// User already made a vote and changing votes isn't allowed |
|
|
|
VFR_CannotChangeVote, |
|
|
|
VFR_CannotChangeVote, |
|
|
|
|
|
|
|
/// User has already voted the same way |
|
|
|
VFR_AlreadyVoted, |
|
|
|
VFR_AlreadyVoted, |
|
|
|
|
|
|
|
/// Voting has already ended and doesn't accept new votes |
|
|
|
VFR_VotingEnded |
|
|
|
VFR_VotingEnded |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Checks how given user has voted |
|
|
|
enum PlayerVoteStatus { |
|
|
|
enum PlayerVoteStatus { |
|
|
|
|
|
|
|
/// User hasn't voted yet |
|
|
|
PVS_NoVote, |
|
|
|
PVS_NoVote, |
|
|
|
|
|
|
|
/// User voted for the change |
|
|
|
PVS_VoteFor, |
|
|
|
PVS_VoteFor, |
|
|
|
|
|
|
|
/// User voted against the change |
|
|
|
PVS_VoteAgainst |
|
|
|
PVS_VoteAgainst |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
@ -55,7 +113,10 @@ var private VotingModelStatus status; |
|
|
|
var private bool policyCanLeave, policyCanChangeVote; |
|
|
|
var private bool policyCanLeave, policyCanChangeVote; |
|
|
|
|
|
|
|
|
|
|
|
var private array<UserID> votesFor, votesAgainst; |
|
|
|
var private array<UserID> votesFor, votesAgainst; |
|
|
|
|
|
|
|
/// Votes of people that voted before, but then were forbidden to vote |
|
|
|
|
|
|
|
/// (either because they have left or simply lost the right to vote) |
|
|
|
var private array<UserID> storedVotesFor, storedVotesAgainst; |
|
|
|
var private array<UserID> storedVotesFor, storedVotesAgainst; |
|
|
|
|
|
|
|
/// List of users currently allowed to vote |
|
|
|
var private array<UserID> allowedVoters; |
|
|
|
var private array<UserID> allowedVoters; |
|
|
|
|
|
|
|
|
|
|
|
protected function Constructor() { |
|
|
|
protected function Constructor() { |
|
|
@ -77,6 +138,7 @@ protected function Finalizer() { |
|
|
|
storedVotesAgainst.length = 0; |
|
|
|
storedVotesAgainst.length = 0; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Initializes voting by providing it with a set of policies to follow. |
|
|
|
public final function Initialize(VotingPolicies policies) { |
|
|
|
public final function Initialize(VotingPolicies policies) { |
|
|
|
if (status == VPM_Uninitialized) { |
|
|
|
if (status == VPM_Uninitialized) { |
|
|
|
policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote); |
|
|
|
policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote); |
|
|
@ -86,45 +148,71 @@ public final function Initialize(VotingPolicies policies) { |
|
|
|
status = VPM_InProgress; |
|
|
|
status = VPM_InProgress; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns current status of voting. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// This method should be checked after both [`AddVote()`] and [`UpdatePotentialVoters()`] to check |
|
|
|
|
|
|
|
/// whether either of them was enough to conclude the voting result. |
|
|
|
public final function VotingModelStatus GetStatus() { |
|
|
|
public final function VotingModelStatus GetStatus() { |
|
|
|
return status; |
|
|
|
return status; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private final function RecountVotes() { |
|
|
|
/// Changes set of [`User`]s that are allowed to vote. |
|
|
|
local bool canOverturn, everyoneVoted; |
|
|
|
/// |
|
|
|
local int totalPossibleVotes; |
|
|
|
/// Generally you want to provide this method with a list of current players, optionally filtered |
|
|
|
local int totalVotesFor, totalVotesAgainst; |
|
|
|
/// from spectators, users not in priviledged group or any other relevant criteria. |
|
|
|
local int lowerVoteCount, upperVoteCount, undecidedVoteCount; |
|
|
|
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { |
|
|
|
|
|
|
|
local int i; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_.memory.FreeMany(allowedVoters); |
|
|
|
|
|
|
|
allowedVoters.length = 0; |
|
|
|
|
|
|
|
for (i = 0; i < potentialVoters.length; i += 1) { |
|
|
|
|
|
|
|
potentialVoters[i].NewRef(); |
|
|
|
|
|
|
|
allowedVoters[i] = potentialVoters[i]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
RestoreStoredVoters(potentialVoters); |
|
|
|
|
|
|
|
FilterCurrentVoters(potentialVoters); |
|
|
|
|
|
|
|
RecountVotes(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// 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. |
|
|
|
|
|
|
|
public final function VotingResult AddVote(UserID voter, bool forSuccess) { |
|
|
|
|
|
|
|
local bool votesSameWay; |
|
|
|
|
|
|
|
local PlayerVoteStatus currentVote; |
|
|
|
|
|
|
|
|
|
|
|
if (status != VPM_InProgress) { |
|
|
|
if (status != VPM_InProgress) { |
|
|
|
return; |
|
|
|
return VFR_VotingEnded; |
|
|
|
} |
|
|
|
} |
|
|
|
if (policyCanLeave) { |
|
|
|
if (!AllowedToVote(voter)) { |
|
|
|
totalVotesFor = votesFor.length + storedVotesFor.length; |
|
|
|
return VFR_NotAllowed; |
|
|
|
totalVotesAgainst = votesAgainst.length + storedVotesAgainst.length; |
|
|
|
|
|
|
|
totalPossibleVotes = |
|
|
|
|
|
|
|
allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
totalVotesFor = votesFor.length; |
|
|
|
|
|
|
|
totalVotesAgainst = votesAgainst.length; |
|
|
|
|
|
|
|
totalPossibleVotes = allowedVoters.length; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
lowerVoteCount = Min(totalVotesFor, totalVotesAgainst); |
|
|
|
currentVote = HasVoted(voter); |
|
|
|
upperVoteCount = Max(totalVotesFor, totalVotesAgainst); |
|
|
|
votesSameWay = (forSuccess && currentVote == PVS_VoteFor) |
|
|
|
undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount); |
|
|
|
|| (!forSuccess && currentVote == PVS_VoteAgainst); |
|
|
|
everyoneVoted = (undecidedVoteCount <= 0); |
|
|
|
if (votesSameWay) { |
|
|
|
canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount; |
|
|
|
return VFR_AlreadyVoted; |
|
|
|
if (everyoneVoted || !canOverturn) { |
|
|
|
} |
|
|
|
if (totalVotesFor > totalVotesAgainst) { |
|
|
|
if (!policyCanChangeVote && currentVote != PVS_NoVote) { |
|
|
|
status = VPM_Success; |
|
|
|
return VFR_CannotChangeVote; |
|
|
|
} else if (totalVotesFor < totalVotesAgainst) { |
|
|
|
} |
|
|
|
status = VPM_Failure; |
|
|
|
EraseVote(voter); |
|
|
|
} else { |
|
|
|
voter.NewRef(); |
|
|
|
status = VPM_Draw; |
|
|
|
if (forSuccess) { |
|
|
|
} |
|
|
|
votesFor[votesFor.length] = voter; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
votesAgainst[votesAgainst.length] = voter; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
RecountVotes(); |
|
|
|
|
|
|
|
return VFR_Success; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// Checks if provided user is allowed to vote. |
|
|
|
|
|
|
|
/// |
|
|
|
|
|
|
|
/// The right to vote is decided solely by what was provided into [`UpdatePotentialVoters()`]. |
|
|
|
|
|
|
|
/// Voting can still fail for [`voter`] that is allowed to vote if, for example, they already voted |
|
|
|
|
|
|
|
/// and policies forbid vote change. |
|
|
|
public final function bool AllowedToVote(UserID voter) { |
|
|
|
public final function bool AllowedToVote(UserID voter) { |
|
|
|
local int i; |
|
|
|
local int i; |
|
|
|
|
|
|
|
|
|
|
@ -139,7 +227,9 @@ public final function bool AllowedToVote(UserID voter) { |
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public final function PlayerVoteStatus HasVoted(UserID voter) { |
|
|
|
// 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; |
|
|
|
local int i; |
|
|
|
|
|
|
|
|
|
|
|
if (voter == none) { |
|
|
|
if (voter == none) { |
|
|
@ -158,6 +248,41 @@ public final function PlayerVoteStatus HasVoted(UserID voter) { |
|
|
|
return PVS_NoVote; |
|
|
|
return PVS_NoVote; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final function RecountVotes() { |
|
|
|
|
|
|
|
local bool canOverturn, everyoneVoted; |
|
|
|
|
|
|
|
local int totalPossibleVotes; |
|
|
|
|
|
|
|
local int totalVotesFor, totalVotesAgainst; |
|
|
|
|
|
|
|
local int lowerVoteCount, upperVoteCount, undecidedVoteCount; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (status != VPM_InProgress) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (policyCanLeave) { |
|
|
|
|
|
|
|
totalVotesFor = votesFor.length + storedVotesFor.length; |
|
|
|
|
|
|
|
totalVotesAgainst = votesAgainst.length + storedVotesAgainst.length; |
|
|
|
|
|
|
|
totalPossibleVotes = |
|
|
|
|
|
|
|
allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
totalVotesFor = votesFor.length; |
|
|
|
|
|
|
|
totalVotesAgainst = votesAgainst.length; |
|
|
|
|
|
|
|
totalPossibleVotes = allowedVoters.length; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
status = VPM_Success; |
|
|
|
|
|
|
|
} else if (totalVotesFor < totalVotesAgainst) { |
|
|
|
|
|
|
|
status = VPM_Failure; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
status = VPM_Draw; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private final function EraseVote(UserID voter) { |
|
|
|
private final function EraseVote(UserID voter) { |
|
|
|
local int i; |
|
|
|
local int i; |
|
|
|
|
|
|
|
|
|
|
@ -183,20 +308,6 @@ private final function EraseVote(UserID voter) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { |
|
|
|
|
|
|
|
local int i; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_.memory.FreeMany(allowedVoters); |
|
|
|
|
|
|
|
allowedVoters.length = 0; |
|
|
|
|
|
|
|
for (i = 0; i < potentialVoters.length; i += 1) { |
|
|
|
|
|
|
|
potentialVoters[i].NewRef(); |
|
|
|
|
|
|
|
allowedVoters[i] = potentialVoters[i]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
RestoreStoredVoters(potentialVoters); |
|
|
|
|
|
|
|
FilterCurrentVoters(potentialVoters); |
|
|
|
|
|
|
|
RecountVotes(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final function RestoreStoredVoters(array<UserID> potentialVoters) { |
|
|
|
private final function RestoreStoredVoters(array<UserID> potentialVoters) { |
|
|
|
local int i, j; |
|
|
|
local int i, j; |
|
|
|
local bool isPotentialVoter; |
|
|
|
local bool isPotentialVoter; |
|
|
@ -271,35 +382,5 @@ private final function FilterCurrentVoters(array<UserID> potentialVoters) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public final function VotingResult AddVote(UserID voter, bool forSuccess) { |
|
|
|
|
|
|
|
local bool votesSameWay; |
|
|
|
|
|
|
|
local PlayerVoteStatus currentVote; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (status != VPM_InProgress) { |
|
|
|
|
|
|
|
return VFR_VotingEnded; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!AllowedToVote(voter)) { |
|
|
|
|
|
|
|
return VFR_NotAllowed; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
currentVote = HasVoted(voter); |
|
|
|
|
|
|
|
votesSameWay = (forSuccess && currentVote == PVS_VoteFor) |
|
|
|
|
|
|
|
|| (!forSuccess && currentVote == PVS_VoteAgainst); |
|
|
|
|
|
|
|
if (votesSameWay) { |
|
|
|
|
|
|
|
return VFR_AlreadyVoted; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!policyCanChangeVote && currentVote != PVS_NoVote) { |
|
|
|
|
|
|
|
return VFR_CannotChangeVote; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
EraseVote(voter); |
|
|
|
|
|
|
|
voter.NewRef(); |
|
|
|
|
|
|
|
if (forSuccess) { |
|
|
|
|
|
|
|
votesFor[votesFor.length] = voter; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
votesAgainst[votesAgainst.length] = voter; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
RecountVotes(); |
|
|
|
|
|
|
|
return VFR_Success; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defaultproperties { |
|
|
|
defaultproperties { |
|
|
|
} |
|
|
|
} |