Browse Source

Change Voting classes to work with new API

develop
Anton Tarasenko 1 year ago
parent
commit
80cecd1d20
  1. 826
      sources/BaseAPI/API/Commands/Voting/Voting.uc
  2. 233
      sources/BaseAPI/API/Commands/Voting/VotingModel.uc

826
sources/BaseAPI/API/Commands/Voting/Voting.uc

File diff suppressed because it is too large Load Diff

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

@ -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;
}
}
}

Loading…
Cancel
Save