Browse Source

Document `VotingModel` class

pull/12/head
Anton Tarasenko 2 years ago
parent
commit
0746aef7c7
  1. 227
      sources/BaseAPI/API/Commands/Voting/VotingModel.uc

227
sources/BaseAPI/API/Commands/Voting/VotingModel.uc

@ -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 {
} }
Loading…
Cancel
Save