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