diff --git a/sources/BaseAPI/API/Commands/Voting/TEST_Voting.uc b/sources/BaseAPI/API/Commands/Voting/TEST_Voting.uc
new file mode 100644
index 0000000..c258303
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Voting/TEST_Voting.uc
@@ -0,0 +1,563 @@
+/**
+ * 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 TEST_Voting extends TestCase
+ abstract
+ dependsOn(VotingModel);
+
+enum ExpectedOutcome {
+ TEST_EO_Continue,
+ TEST_EO_End,
+ TEST_EO_EndDraw,
+};
+
+protected static function VotingModel MakeVotingModel(VotingModel.VotingPolicies policies) {
+ local VotingModel model;
+
+ model = VotingModel(__().memory.Allocate(class'VotingModel'));
+ model.Initialize(policies);
+ return model;
+}
+
+protected static function SetVoters(
+ VotingModel model,
+ optional string voterID0,
+ optional string voterID1,
+ optional string voterID2,
+ optional string voterID3,
+ optional string voterID4,
+ optional string voterID5,
+ optional string voterID6,
+ optional string voterID7,
+ optional string voterID8,
+ optional string voterID9
+) {
+ local UserID nextID;
+ local array voterIDs;
+
+ if (voterID0 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID0));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID1 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID1));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID2 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID2));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID3 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID3));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID4 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID4));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID5 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID5));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID6 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID6));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID7 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID7));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID8 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID8));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ if (voterID9 != "") {
+ nextID = UserID(__().memory.Allocate(class'UserID'));
+ nextID.Initialize(__().text.FromString(voterID9));
+ voterIDs[voterIDs.length] = nextID;
+ }
+ model.UpdatePotentialVoters(voterIDs);
+}
+
+protected static function MakeFaultyYesVote(
+ VotingModel model,
+ string voterID,
+ VotingModel.VotingResult expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Illegal vote had unexpected result.");
+ TEST_ExpectTrue(model.AddVote(id, true) == expected);
+}
+
+protected static function MakeFaultyNoVote(
+ VotingModel model,
+ string voterID,
+ VotingModel.VotingResult expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Illegal vote had unexpected result.");
+ TEST_ExpectTrue(model.AddVote(id, false) == expected);
+}
+
+protected static function VoteYes(VotingModel model, string voterID, ExpectedOutcome expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Failed to add legitimate vote.");
+ TEST_ExpectTrue(model.AddVote(id, true) == VFR_Success);
+ if (expected == TEST_EO_Continue) {
+ Issue("Vote, that shouldn't have ended voting, ended it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ } else if (expected == TEST_EO_End) {
+ Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Success);
+ } else if (expected == TEST_EO_EndDraw) {
+ Issue("Vote, that should've ended voting with a draw, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Draw);
+ }
+}
+
+protected static function VoteNo(VotingModel model, string voterID, ExpectedOutcome expected) {
+ local UserID id;
+
+ id = UserID(__().memory.Allocate(class'UserID'));
+ id.Initialize(__().text.FromString(voterID));
+ Issue("Failed to add legitimate vote.");
+ TEST_ExpectTrue(model.AddVote(id, false) == VFR_Success);
+ if (expected == TEST_EO_Continue) {
+ Issue("Vote, that shouldn't have ended voting, ended it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ } else if (expected == TEST_EO_End) {
+ Issue("Vote, that should've ended voting with one side's victory, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Failure);
+ } else if (expected == TEST_EO_EndDraw) {
+ Issue("Vote, that should've ended voting with a draw, didn't do it.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Draw);
+ }
+}
+
+protected static function TESTS() {
+ Test_RestrictiveVoting();
+ Test_CanLeaveVoting();
+ Test_CanChangeVoting();
+ Test_All();
+}
+
+protected static function Test_RestrictiveVoting() {
+ SubTest_RestrictiveYesVoting();
+ SubTest_RestrictiveNoVoting();
+ SubTest_RestrictiveDrawVoting();
+ SubTest_RestrictiveFaultyVoting();
+ SubTest_RestrictiveDisconnectVoting();
+ SubTest_RestrictiveReconnectVoting();
+}
+
+protected static function SubTest_RestrictiveYesVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"yes\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_End);
+}
+
+protected static function SubTest_RestrictiveNoVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"no\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_End);
+}
+
+protected static function SubTest_RestrictiveDrawVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"draw\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_EndDraw);
+}
+
+protected static function SubTest_RestrictiveFaultyVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"faulty\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ MakeFaultyYesVote(model, "3", VFR_AlreadyVoted);
+ VoteYes(model, "2", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "7", VFR_NotAllowed);
+ VoteNo(model, "5", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "7", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "5", VFR_AlreadyVoted);
+ VoteYes(model, "1", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "3", VFR_CannotChangeVote);
+ VoteYes(model, "6", TEST_EO_End);
+ MakeFaultyYesVote(model, "4", VFR_VotingEnded);
+}
+
+protected static function SubTest_RestrictiveDisconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"disconnect\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "7", TEST_EO_Continue);
+ SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "9", TEST_EO_Continue);
+ // Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
+ // disconnect "2" and "9" for "no" to win
+ SetVoters(model, "4", "5", "6", "8", "10");
+ Issue("Unexpected result after voting users disconnected.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Failure);
+}
+
+protected static function SubTest_RestrictiveReconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing restrictive \"reconnecting\" voting.");
+ model = MakeVotingModel(VP_Restrictive);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 3 "yes" voters
+ SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "7", TEST_EO_Continue);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "9", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "3", VFR_NotAllowed);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteNo(model, "10", TEST_EO_EndDraw);
+}
+/* Testing restrictive "reconnecting" voting.
+ Unexpected result after voting users reconnected. [1] */
+protected static function Test_CanLeaveVoting() {
+ SubTest_CanLeaveYesVoting();
+ SubTest_CanLeaveNoVoting();
+ SubTest_CanLeaveDrawVoting();
+ SubTest_CanLeaveFaultyVoting();
+ SubTest_CanLeaveDisconnectVoting();
+ SubTest_CanLeaveReconnectVoting();
+}
+
+protected static function SubTest_CanLeaveYesVoting() {
+ local VotingModel model;
+
+ Context("Testing \"yes\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ SetVoters(model, "1", "5", "6");
+ Issue("Unexpected result after voting users leaves.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ VoteYes(model, "1", TEST_EO_End);
+}
+
+protected static function SubTest_CanLeaveNoVoting() {
+ local VotingModel model;
+
+ Context("Testing \"no\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ SetVoters(model, "3", "4", "5");
+ Issue("Unexpected result after voting users leaves.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ VoteNo(model, "4", TEST_EO_End);
+}
+
+protected static function SubTest_CanLeaveDrawVoting() {
+ local VotingModel model;
+
+ Context("Testing \"draw\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ SetVoters(model, "4");
+ Issue("Unexpected result after voting users leaves.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ VoteNo(model, "4", TEST_EO_EndDraw);
+}
+
+protected static function SubTest_CanLeaveFaultyVoting() {
+ local VotingModel model;
+
+ Context("Testing \"faulty\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ MakeFaultyYesVote(model, "3", VFR_AlreadyVoted);
+ VoteYes(model, "2", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "7", VFR_NotAllowed);
+ VoteNo(model, "5", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "7", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "5", VFR_AlreadyVoted);
+ VoteYes(model, "1", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "3", VFR_CannotChangeVote);
+ SetVoters(model, "4", "5", "6");
+ Issue("Unexpected result after voting users leaves.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_InProgress);
+ VoteYes(model, "6", TEST_EO_End);
+ MakeFaultyYesVote(model, "4", VFR_VotingEnded);
+}
+
+protected static function SubTest_CanLeaveDisconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing \"leave\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "7", TEST_EO_Continue);
+ SetVoters(model, "2", "4", "5", "6", "8", "9", "10");
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "10", TEST_EO_End);
+}
+
+protected static function SubTest_CanLeaveReconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing \"reconnecting\" voting where users are allowed to leave.");
+ model = MakeVotingModel(VP_CanLeave);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 3 "yes" voters
+ SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "7", TEST_EO_Continue);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteNo(model, "9", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "3", VFR_NotAllowed);
+ VoteYes(model, "10", TEST_EO_EndDraw);
+}
+
+protected static function Test_CanChangeVoting() {
+ SubTest_CanChangeYesVoting();
+ SubTest_CanChangeNoVoting();
+ SubTest_CanChangeDrawVoting();
+ SubTest_CanChangeFaultyVoting();
+ SubTest_CanChangeDisconnectVoting();
+ SubTest_CanChangeReconnectVoting();
+}
+
+protected static function SubTest_CanChangeYesVoting() {
+ local VotingModel model;
+
+ Context("Testing \"yes\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+}
+
+protected static function SubTest_CanChangeNoVoting() {
+ local VotingModel model;
+
+ Context("Testing \"no\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_End);
+}
+
+protected static function SubTest_CanChangeDrawVoting() {
+ local VotingModel model;
+
+ Context("Testing \"draw\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "1", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_EndDraw);
+}
+protected static function SubTest_CanChangeFaultyVoting() {
+ local VotingModel model;
+
+ Context("Testing \"faulty\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6");
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_End);
+ MakeFaultyYesVote(model, "4", VFR_VotingEnded);
+}
+
+protected static function SubTest_CanChangeDisconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing \"disconnect\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteYes(model, "6", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ VoteYes(model, "7", TEST_EO_Continue);
+ SetVoters(model, "2", "4", "5", "6", "8", "9", "10"); // remove 1, 3, 7 - 3 "yes" votes
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "9", TEST_EO_Continue);
+ // Here we're at 3 "no" votes, 3 "yes" votes out of 7 total;
+ // disconnect "6" and "9" for "yes" to win
+ SetVoters(model, "2", "4", "5", "8", "10");
+ Issue("Unexpected result after voting users disconnected.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Success);
+}
+
+protected static function SubTest_CanChangeReconnectVoting() {
+ local VotingModel model;
+
+ Context("Testing \"reconnect\" voting where users are allowed to change their vote.");
+ model = MakeVotingModel(VP_CanChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteNo(model, "2", TEST_EO_Continue);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ VoteYes(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 3 "yes" voters
+ SetVoters(model, "2", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "7", TEST_EO_Continue);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteYes(model, "9", TEST_EO_Continue);
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "3", VFR_NotAllowed);
+ // Restore 3 "yes" voter
+ SetVoters(model, "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteNo(model, "3", TEST_EO_End);
+}
+
+protected static function Test_All() {
+ local VotingModel model;
+
+ Context("Testing permissive voting options.");
+ model = MakeVotingModel(VP_CanLeaveAndChangeVote);
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "1", TEST_EO_Continue);
+ VoteYes(model, "2", TEST_EO_Continue);
+ VoteNo(model, "3", TEST_EO_Continue);
+ VoteYes(model, "4", TEST_EO_Continue);
+ VoteNo(model, "5", TEST_EO_Continue);
+ VoteNo(model, "6", TEST_EO_Continue);
+ // Disconnect 1 and 5 voters
+ SetVoters(model, "2", "3", "4", "6", "7", "8", "9", "10");
+ MakeFaultyNoVote(model, "1", VFR_NotAllowed);
+ MakeFaultyNoVote(model, "5", VFR_NotAllowed);
+ VoteYes(model, "3", TEST_EO_Continue);
+ VoteNo(model, "7", TEST_EO_Continue);
+ VoteNo(model, "8", TEST_EO_Continue);
+ VoteNo(model, "9", TEST_EO_Continue);
+ // Bring back 1, disconnect 3 and 6
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10");
+ VoteYes(model, "8", TEST_EO_Continue);
+ VoteNo(model, "4", TEST_EO_Continue);
+ // Disconnect 10, finishing voting (since now only 9 voters are available)
+ SetVoters(model, "1", "2", "3", "4", "5", "6", "7", "8", "9");
+ Issue("Unexpected result after voting users disconnected.");
+ TEST_ExpectTrue(model.GetStatus() == VPM_Failure);
+}
+
+defaultproperties {
+ caseGroup = "Commands"
+ caseName = "Voting model"
+}
\ No newline at end of file
diff --git a/sources/BaseAPI/API/Commands/Voting/VotingModel.uc b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc
new file mode 100644
index 0000000..0ea065f
--- /dev/null
+++ b/sources/BaseAPI/API/Commands/Voting/VotingModel.uc
@@ -0,0 +1,305 @@
+/**
+ * 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;
+
+enum VotingPolicies {
+ VP_Restrictive,
+ VP_CanLeave,
+ VP_CanChangeVote,
+ VP_CanLeaveAndChangeVote
+};
+
+enum VotingModelStatus {
+ VPM_Uninitialized,
+ VPM_InProgress,
+ VPM_Success,
+ VPM_Failure,
+ VPM_Draw
+};
+
+enum VotingResult {
+ VFR_Success,
+ VFR_NotAllowed,
+ VFR_CannotChangeVote,
+ VFR_AlreadyVoted,
+ VFR_VotingEnded
+};
+
+enum PlayerVoteStatus {
+ PVS_NoVote,
+ PVS_VoteFor,
+ PVS_VoteAgainst
+};
+
+var private VotingModelStatus status;
+
+var private bool policyCanLeave, policyCanChangeVote;
+
+var private array votesFor, votesAgainst;
+var private array storedVotesFor, storedVotesAgainst;
+var private array allowedVoters;
+
+protected function Constructor() {
+ status = VPM_Uninitialized;
+ policyCanLeave = false;
+ policyCanChangeVote = false;
+}
+
+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;
+}
+
+public final function Initialize(VotingPolicies policies) {
+ if (status == VPM_Uninitialized) {
+ policyCanLeave = (policies == VP_CanLeave) || (policies == VP_CanLeaveAndChangeVote);
+ policyCanChangeVote =
+ (policies == VP_CanChangeVote) || (policies == VP_CanLeaveAndChangeVote);
+ }
+ status = VPM_InProgress;
+}
+
+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;
+
+ 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;
+ }
+ }
+}
+
+public final function bool AllowedToVote(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;
+}
+
+public final function PlayerVoteStatus HasVoted(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;
+}
+
+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;
+ }
+ }
+}
+
+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;
+
+ 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);
+ }
+ }
+}
+
+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
diff --git a/sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc b/sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc
index d327b26..9847965 100644
--- a/sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc
+++ b/sources/BaseAPI/API/Math/Tests/TEST_BigInt.uc
@@ -99,7 +99,6 @@ protected static function SubTest_AddingSameSignValues() {
main = __().math.MakeBigInt_S("927641962323462271784269213864");
addition = __().math.MakeBigInt_S("16324234842947239847239239");
main.Add(addition);
- Log("UMBRA" @ main.ToString());
TEST_ExpectTrue(main.ToString() == "927658286558305219024116453103");
main = __().math.MakeBigInt_S("16324234842947239847239239");
addition = __().math.MakeBigInt_S("927641962323462271784269213864");
diff --git a/sources/Manifest.uc b/sources/Manifest.uc
index 4e35b39..17dae9a 100644
--- a/sources/Manifest.uc
+++ b/sources/Manifest.uc
@@ -43,19 +43,20 @@ defaultproperties
testCases(14) = class'TEST_TextTemplate'
testCases(15) = class'TEST_User'
testCases(16) = class'TEST_Memory'
- testCases(17) = class'TEST_ArrayList'
- testCases(18) = class'TEST_HashTable'
- testCases(19) = class'TEST_CollectionsMixed'
- testCases(20) = class'TEST_Iterator'
- testCases(21) = class'TEST_Command'
- testCases(22) = class'TEST_CommandDataBuilder'
- testCases(23) = class'TEST_LogMessage'
- testCases(24) = class'TEST_SchedulerAPI'
- testCases(25) = class'TEST_BigInt'
- testCases(26) = class'TEST_DatabaseCommon'
- testCases(27) = class'TEST_LocalDatabase'
- testCases(28) = class'TEST_DBConnection'
- testCases(29) = class'TEST_AcediaConfig'
- testCases(30) = class'TEST_UTF8EncoderDecoder'
- testCases(31) = class'TEST_AvariceStreamReader'
+ testCases(17) = class'TEST_Voting'
+ testCases(18) = class'TEST_ArrayList'
+ testCases(19) = class'TEST_HashTable'
+ testCases(20) = class'TEST_CollectionsMixed'
+ testCases(21) = class'TEST_Iterator'
+ testCases(22) = class'TEST_Command'
+ testCases(23) = class'TEST_CommandDataBuilder'
+ testCases(24) = class'TEST_LogMessage'
+ testCases(25) = class'TEST_SchedulerAPI'
+ testCases(26) = class'TEST_BigInt'
+ testCases(27) = class'TEST_DatabaseCommon'
+ testCases(28) = class'TEST_LocalDatabase'
+ testCases(29) = class'TEST_DBConnection'
+ testCases(30) = class'TEST_AcediaConfig'
+ testCases(31) = class'TEST_UTF8EncoderDecoder'
+ testCases(32) = class'TEST_AvariceStreamReader'
}
\ No newline at end of file