From 0746aef7c7e69ffe8413e36c5888eba739d272aa Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 19 Mar 2023 18:23:22 +0700 Subject: [PATCH] Document `VotingModel` class --- .../API/Commands/Voting/VotingModel.uc | 227 ++++++++++++------ 1 file changed, 154 insertions(+), 73 deletions(-) diff --git a/sources/BaseAPI/API/Commands/Voting/VotingModel.uc b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc index 0ea065f..20870f8 100644 --- a/sources/BaseAPI/API/Commands/Voting/VotingModel.uc +++ b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc @@ -21,32 +21,90 @@ */ 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 { + /// 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, + /// Leaving during voting is legal, everything else potentially illegal is still illegal. + /// + /// Changing vote is forbidden. 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, + /// 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 }; +/// Current state of voting for this model. enum VotingModelStatus { + /// Voting hasn't even started, waiting for [`Initialize()`] call VPM_Uninitialized, + /// Voting is currently in progress VPM_InProgress, + /// 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 }; +/// A result of user trying to make a vote enum VotingResult { + /// Vote accepted VFR_Success, + /// Voting is not allowed for this particular user VFR_NotAllowed, + /// User already made a vote and changing votes isn't allowed VFR_CannotChangeVote, + /// User has already voted the same way VFR_AlreadyVoted, + /// Voting has already ended and doesn't accept new votes VFR_VotingEnded }; +/// Checks how given user has voted enum PlayerVoteStatus { + /// User hasn't voted yet PVS_NoVote, + /// User voted for the change PVS_VoteFor, + /// User voted against the change PVS_VoteAgainst }; @@ -55,7 +113,10 @@ var private VotingModelStatus status; var private bool policyCanLeave, policyCanChangeVote; var private array 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 storedVotesFor, storedVotesAgainst; +/// List of users currently allowed to vote var private array allowedVoters; protected function Constructor() { @@ -77,6 +138,7 @@ protected function Finalizer() { storedVotesAgainst.length = 0; } +/// 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); @@ -86,45 +148,71 @@ public final function Initialize(VotingPolicies policies) { 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() { return status; } -private final function RecountVotes() { - local bool canOverturn, everyoneVoted; - local int totalPossibleVotes; - local int totalVotesFor, totalVotesAgainst; - local int lowerVoteCount, upperVoteCount, undecidedVoteCount; +/// 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. +public final function UpdatePotentialVoters(array 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) { - return; + return VFR_VotingEnded; } - 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; + if (!AllowedToVote(voter)) { + return VFR_NotAllowed; } - 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; - } + 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; } +/// 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) { local int i; @@ -139,7 +227,9 @@ public final function bool AllowedToVote(UserID voter) { 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; if (voter == none) { @@ -158,6 +248,41 @@ public final function PlayerVoteStatus HasVoted(UserID voter) { 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) { local int i; @@ -183,20 +308,6 @@ private final function EraseVote(UserID voter) { } } -public final function UpdatePotentialVoters(array 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 potentialVoters) { local int i, j; local bool isPotentialVoter; @@ -271,35 +382,5 @@ private final function FilterCurrentVoters(array 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 { } \ No newline at end of file