/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ 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. //! //! # Usage //! //! 1. Allocate an instance of the [`VotingModel`] class. //! 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 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. //! 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 { /// 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 }; /// 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 }; /// 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; /// Specifies whether draw would count as a victory for corresponding voting. var private bool policyDrawWinsVoting; 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() { status = VPM_Uninitialized; } protected function Finalizer() { _.memory.FreeMany(allowedVoters); _.memory.FreeMany(votesFor); _.memory.FreeMany(votesAgainst); _.memory.FreeMany(storedVotesFor); _.memory.FreeMany(storedVotesAgainst); allowedVoters.length = 0; votesFor.length = 0; votesAgainst.length = 0; storedVotesFor.length = 0; storedVotesAgainst.length = 0; } /// Initializes voting by providing it with a set of policies to follow. /// /// 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. 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. 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. 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. public final function VotingResult CastVote(UserID voter, bool voteForSuccess) { local bool votesSameWay; local PlayerVoteStatus currentVote; if (status != VPM_InProgress) { return VFR_VotingEnded; } if (!IsVotingAllowedFor(voter)) { return VFR_NotAllowed; } currentVote = GetVote(voter); votesSameWay = (voteForSuccess && currentVote == PVS_VoteFor) || (!voteForSuccess && currentVote == PVS_VoteAgainst); if (votesSameWay) { return VFR_AlreadyVoted; } EraseVote(voter); voter.NewRef(); if (voteForSuccess) { votesFor[votesFor.length] = voter; } else { votesAgainst[votesAgainst.length] = voter; } RecountVotes(); return VFR_Success; } /// 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()`]. /// /// Returns true if the user is allowed to vote, false otherwise. public final function bool IsVotingAllowedFor(UserID voter) { local int i; if (voter == none) { return false; } for (i = 0; i < allowedVoters.length; i += 1) { if (voter.IsEqual(allowedVoters[i])) { return true; } } return false; } /// 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 won't count. public final function PlayerVoteStatus GetVote(UserID voter) { local int i; if (voter == none) { return PVS_NoVote; } for (i = 0; i < votesFor.length; i += 1) { if (voter.IsEqual(votesFor[i])) { return PVS_VoteFor; } } for (i = 0; i < votesAgainst.length; i += 1) { if (voter.IsEqual(votesAgainst[i])) { return PVS_VoteAgainst; } } return PVS_NoVote; } /// Returns amount of current valid votes for the success of this voting. public final function int GetVotesFor() { return votesFor.length; } /// Returns amount of current valid votes against the success of this voting. public final function int GetVotesAgainst() { return votesAgainst.length; } /// Returns amount of users that are currently allowed to vote in this voting. public final function int GetTotalPossibleVotes() { return allowedVoters.length; } /// 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; 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; } 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 true; } private final function RecountVotes() { local MathApi.IntegerDivisionResult divisionResult; local int winningScore, losingScore; local int totalPossibleVotes; if (status != VPM_InProgress) { return; } totalPossibleVotes = GetTotalPossibleVotes(); 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 (GetVotesAgainst() >= losingScore) { status = VPM_Failure; } } private final function EraseVote(UserID voter) { local int i; if (voter == none) { return; } while (i < votesFor.length) { if (voter.IsEqual(votesFor[i])) { _.memory.Free(votesFor[i]); votesFor.Remove(i, 1); } else { i += 1; } } i = 0; while (i < votesAgainst.length) { if (voter.IsEqual(votesAgainst[i])) { _.memory.Free(votesAgainst[i]); votesAgainst.Remove(i, 1); } else { i += 1; } } } private final function RestoreStoredVoters(array potentialVoters) { local int i, j; local bool isPotentialVoter; while (i < storedVotesFor.length) { isPotentialVoter = false; for (j = 0; j < potentialVoters.length; j += 1) { if (storedVotesFor[i].IsEqual(potentialVoters[j])) { isPotentialVoter = true; break; } } if (isPotentialVoter) { votesFor[votesFor.length] = storedVotesFor[i]; storedVotesFor.Remove(i, 1); } else { i += 1; } } i = 0; while (i < storedVotesAgainst.length) { isPotentialVoter = false; for (j = 0; j < potentialVoters.length; j += 1) { if (storedVotesAgainst[i].IsEqual(potentialVoters[j])) { isPotentialVoter = true; break; } } if (isPotentialVoter) { votesAgainst[votesAgainst.length] = storedVotesAgainst[i]; storedVotesAgainst.Remove(i, 1); } else { i += 1; } } } private final function FilterCurrentVoters(array potentialVoters) { local int i, j; local bool isPotentialVoter; while (i < votesFor.length) { isPotentialVoter = false; for (j = 0; j < potentialVoters.length; j += 1) { if (votesFor[i].IsEqual(potentialVoters[j])) { isPotentialVoter = true; break; } } if (isPotentialVoter) { i += 1; } else { storedVotesFor[storedVotesFor.length] = votesFor[i]; votesFor.Remove(i, 1); } } i = 0; while (i < votesAgainst.length) { isPotentialVoter = false; for (j = 0; j < potentialVoters.length; j += 1) { if (votesAgainst[i].IsEqual(potentialVoters[j])) { isPotentialVoter = true; break; } } if (isPotentialVoter) { i += 1; } else { storedVotesAgainst[storedVotesAgainst.length] = votesAgainst[i]; votesAgainst.Remove(i, 1); } } } defaultproperties { }