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 * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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. //! This class counts votes according to the configured voting policies.
//! //!
//! Its main purpose is to separate the voting logic from the voting interface, making //! Its main purpose is to separate the voting logic from the voting interface,
//! the implementation simpler and the logic easier to test. //! making the implementation simpler and the logic easier to test.
//! //!
//! # Usage //! # Usage
//! //!
//! 1. Allocate an instance of the [`VotingModel`] class. //! 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. //! 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 //! You can change this set at any time before the voting has concluded.
//! the policies set during the previous [`Initialize()`] call. //! 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. //! 4. Use [`CastVote()`] to add a vote from a user.
//! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`], check [`GetStatus()`] to //! 5. After calling either [`UpdatePotentialVoters()`] or [`CastVote()`],
//! see if the voting has concluded. //! check [`GetStatus()`] to see if the voting has concluded.
//! Once voting has concluded, the result cannot be changed, so you can release the reference //! Once voting has concluded, the result cannot be changed, so you can
//! to the [`VotingModel`] object. //! release the reference to the [`VotingModel`] object.
//! 6. Alternatively, before voting has concluded naturally, you can use
/// Describes how [`VotingModel`] should react when a user performs potentially illegal actions. //! [`ForceEnding()`] method to immediately end voting with result being
/// //! determined by provided [`ForceEndingType`] argument.
/// 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
};
/// Current state of voting for this model. /// Current state of voting for this model.
enum VotingModelStatus { enum VotingModelStatus {
@ -73,9 +53,7 @@ enum VotingModelStatus {
/// Voting has ended with majority for its success /// Voting has ended with majority for its success
VPM_Success, VPM_Success,
/// Voting has ended with majority for its failure /// Voting has ended with majority for its failure
VPM_Failure, VPM_Failure
/// Voting has ended in a draw
VPM_Draw
}; };
/// A result of user trying to make a vote /// A result of user trying to make a vote
@ -102,9 +80,20 @@ enum PlayerVoteStatus {
PVS_VoteAgainst 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 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; var private array<UserID> votesFor, votesAgainst;
/// Votes of people that voted before, but then were forbidden to vote /// Votes of people that voted before, but then were forbidden to vote
@ -115,8 +104,6 @@ var private array<UserID> allowedVoters;
protected function Constructor() { protected function Constructor() {
status = VPM_Uninitialized; status = VPM_Uninitialized;
policyCanLeave = false;
policyCanChangeVote = false;
} }
protected function Finalizer() { protected function Finalizer() {
@ -133,35 +120,42 @@ protected function Finalizer() {
} }
/// Initializes voting by providing it with a set of policies to follow. /// Initializes voting by providing it with a set of policies to follow.
public final function Initialize(VotingPolicies policies) { ///
if (status == VPM_Uninitialized) { /// The only available policy is configuring whether draw means victory or loss
policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote); /// in voting.
policyCanChangeVote = ///
(policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote); /// 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; status = VPM_InProgress;
} }
/// Returns whether voting has already concluded. /// Returns whether voting has already concluded.
/// ///
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check /// This method should be checked after both [`CastVote()`] and
/// whether either of them was enough to conclude the voting result. /// [`UpdatePotentialVoters()`] to check whether either of them was enough to
/// conclude the voting result.
public final function bool HasEnded() { public final function bool HasEnded() {
return (status != VPM_Uninitialized && status != VPM_InProgress); return (status != VPM_Uninitialized && status != VPM_InProgress);
} }
/// Returns current status of voting. /// Returns current status of voting.
/// ///
/// This method should be checked after both [`CastVote()`] and [`UpdatePotentialVoters()`] to check /// This method should be checked after both [`CastVote()`] and
/// whether either of them was enough to conclude the voting result. /// [`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;
} }
/// Changes set of [`User`]s that are allowed to vote. /// 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 /// Generally you want to provide this method with a list of current players,
/// from spectators, users not in priviledged group or any other relevant criteria. /// optionally filtered from spectators, users not in priviledged group or any
/// other relevant criteria.
public final function UpdatePotentialVoters(array<UserID> potentialVoters) { public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
local int i; local int i;
@ -178,8 +172,7 @@ public final function UpdatePotentialVoters(array<UserID> potentialVoters) {
/// Attempts to add a vote from specified user. /// 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 /// Adding a vote can fail if [`voter`] isn't allowed to vote.
/// forbid changing that vote.
public final function VotingResult CastVote(UserID voter, bool voteForSuccess) { public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
local bool votesSameWay; local bool votesSameWay;
local PlayerVoteStatus currentVote; local PlayerVoteStatus currentVote;
@ -190,15 +183,12 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
if (!IsVotingAllowedFor(voter)) { if (!IsVotingAllowedFor(voter)) {
return VFR_NotAllowed; return VFR_NotAllowed;
} }
currentVote = HasVoted(voter); currentVote = GetVote(voter);
votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor)
|| (!voteForSuccess && currentVote == PVS_VoteAgainst); || (!voteForSuccess && currentVote == PVS_VoteAgainst);
if (votesSameWay) { if (votesSameWay) {
return VFR_AlreadyVoted; return VFR_AlreadyVoted;
} }
if (!policyCanChangeVote && currentVote != PVS_NoVote) {
return VFR_CannotChangeVote;
}
EraseVote(voter); EraseVote(voter);
voter.NewRef(); voter.NewRef();
if (voteForSuccess) { if (voteForSuccess) {
@ -210,12 +200,11 @@ public final function VotingResult CastVote(UserID voter, bool voteForSuccess) {
return VFR_Success; 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 /// The right to vote is decided solely by the list of potential voters set
/// [`UpdatePotentialVoters()`]. /// 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.
/// ///
/// Returns true if the user is allowed to vote, false otherwise. /// Returns true if the user is allowed to vote, false otherwise.
public final function bool IsVotingAllowedFor(UserID voter) { 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. /// 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 /// If the voter was previously allowed to vote, voted, and had their right to
/// vote will only count if policies allow voters to leave mid-vote. /// vote revoked, their vote won't count.
/// Otherwise, the method will return [`PVS_NoVote`].
public final function PlayerVoteStatus GetVote(UserID voter) { public final function PlayerVoteStatus GetVote(UserID voter) {
local int i; local int i;
@ -253,94 +241,99 @@ public final function PlayerVoteStatus GetVote(UserID voter) {
return PVS_VoteAgainst; 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; return PVS_NoVote;
} }
/// Returns amount of current valid votes for the success of this voting. /// Returns amount of current valid votes for the success of this voting.
public final function int GetVotesFor() { public final function int GetVotesFor() {
if (policyCanLeave) {
return votesFor.length + storedVotesFor.length;
} else {
return votesFor.length; return votesFor.length;
} }
}
/// Returns amount of current valid votes against the success of this voting. /// Returns amount of current valid votes against the success of this voting.
public final function int GetVotesAgainst() { public final function int GetVotesAgainst() {
if (policyCanLeave) {
return votesAgainst.length + storedVotesAgainst.length;
} else {
return votesAgainst.length; return votesAgainst.length;
} }
}
/// Returns amount of users that are currently allowed to vote in this voting. /// Returns amount of users that are currently allowed to vote in this voting.
public final function int GetTotalPossibleVotes() { public final function int GetTotalPossibleVotes() {
if (policyCanLeave) {
return allowedVoters.length + storedVotesFor.length + storedVotesAgainst.length;
} else {
return allowedVoters.length; return allowedVoters.length;
} }
}
// Checks if provided user has already voted. /// Checks whether, if stopped now, voting will win.
// Only checks among users that are currently allowed to vote, even if their past vote still counts. public final function bool IsVotingWinning() {
private final function PlayerVoteStatus HasVoted(UserID voter) { if (status == VPM_Success) return true;
local int i; if (status == VPM_Failure) return false;
if (GetVotesFor() > GetVotesAgainst()) return true;
if (GetVotesFor() < GetVotesAgainst()) return false;
if (voter == none) { return policyDrawWinsVoting;
return PVS_NoVote;
}
for (i = 0; i < votesFor.length; i += 1) {
if (voter.IsEqual(votesFor[i])) {
return PVS_VoteFor;
} }
/// 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) { switch (type) {
if (voter.IsEqual(votesAgainst[i])) { case FET_CurrentLeader:
return PVS_VoteAgainst; 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() { private final function RecountVotes() {
local bool canOverturn, everyoneVoted; local MathApi.IntegerDivisionResult divisionResult;
local int winningScore, losingScore;
local int totalPossibleVotes; local int totalPossibleVotes;
local int totalVotesFor, totalVotesAgainst;
local int lowerVoteCount, upperVoteCount, undecidedVoteCount;
if (status != VPM_InProgress) { if (status != VPM_InProgress) {
return; return;
} }
totalVotesFor = GetVotesFor();
totalVotesAgainst = GetVotesAgainst();
totalPossibleVotes = GetTotalPossibleVotes(); totalPossibleVotes = GetTotalPossibleVotes();
lowerVoteCount = Min(totalVotesFor, totalVotesAgainst); divisionResult = _.math.IntegerDivision(totalPossibleVotes, 2);
upperVoteCount = Max(totalVotesFor, totalVotesAgainst); if (divisionResult.remainder == 1) {
undecidedVoteCount = totalPossibleVotes - (lowerVoteCount + upperVoteCount); // For odd amount of voters winning is simply majority
everyoneVoted = (undecidedVoteCount <= 0); winningScore = divisionResult.quotient + 1;
canOverturn = lowerVoteCount + undecidedVoteCount >= upperVoteCount; } else {
if (everyoneVoted || !canOverturn) { if (policyDrawWinsVoting) {
if (totalVotesFor > totalVotesAgainst) { // 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; status = VPM_Success;
} else if (totalVotesFor < totalVotesAgainst) { } else if (GetVotesAgainst() >= losingScore) {
status = VPM_Failure; status = VPM_Failure;
} else {
status = VPM_Draw;
}
} }
} }

Loading…
Cancel
Save