440 lines
14 KiB
Ucode
440 lines
14 KiB
Ucode
/**
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
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<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;
|
|
/// List of users currently allowed to vote
|
|
var private array<UserID> 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<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.
|
|
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<UserID> 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<UserID> 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 {
|
|
} |