Refactored voting engine.

Should be more simple to follow.
This commit is contained in:
armored-dragon 2024-09-18 05:10:23 -05:00
parent 2243de9cc9
commit 5ef98c7c5b
No known key found for this signature in database
GPG key ID: C7207ACC3382AD8B
3 changed files with 118 additions and 79 deletions

View file

@ -14,6 +14,7 @@
// TODO: Documentation
// FIXME: Handle ties: kill both of tied results
// FIXME: Handle ties: Last two standing are tied.
// FIXME: Make the candidate name have reserved value "-1", or change to uncommon name.
(() => {
"use strict";
@ -28,6 +29,8 @@
let activePolls = []; // All active polls.
let selectedPage = ""; // Selected page the vote screen is on. Used when the host closes the window.
let voteEngine = Script.require("./vote_engine.js");
const url = Script.resolvePath("./vote.qml");
const myUuid = generateUUID(MyAvatar.sessionUUID);
Messages.messageReceived.connect(receivedMessage);
@ -220,88 +223,27 @@
// Take the gathered responses and preform the election
// FIXME: Recursive function call
function preformElection(){
let firstVotes = []; // List of first choices from every ballot
let voteResults = {}; // Object that stores the total amount of votes each candidate gets
// Don't run election if we don't have any votes.
if (Object.keys(pollStats.responses).length == 0) return;
// Go though each vote received and get the most preferred candidate per ballot.
Object.keys(pollStats.responses).forEach((key) => {
let uuid = key;
let vote = pollStats.responses[uuid];
// Assign first vote to new array
firstVotes.push(vote[0]);
// Format the votes into a format that the election engine can understand
let votesFormatted = [];
const allVoters = Object.keys(pollStats.responses);
allVoters.forEach((voterId) => {
votesFormatted.push(pollStats.responses[voterId]);
});
// Go through each first choice and increment the total amount of votes per candidate.
for (let i = 0; i < firstVotes.length; i++) {
let candidate = firstVotes[i];
// Check if firstVotes index exists
if (!candidate) candidate = -1; // If we have received a "no-vote", just assign -1
// Create voteResults index if it does not exist
if (!voteResults[candidate]) voteResults[candidate] = 0;
// Increment value for each vote
voteResults[candidate]++;
}
const totalVotes = Object.keys(pollStats.responses).length; // Total votes to expect to be counted.
const majority = Math.floor(totalVotes / 2); // Minimum value to be considered a majority
const sortedArray = Object.entries(voteResults).sort((a, b) => b[1] - a[1]);
let sortedObject = [];
for (const [key, value] of sortedArray) {
sortedObject.push({ [key]: value });
}
console.log(`Iteration Votes: ${JSON.stringify(sortedObject, null, 2)}`);
// Check the most voted for option to see if it makes up over 50% of votes
// NOTE: Has to be *over* 50%.
if (sortedObject[0][Object.keys(sortedObject[0])[0]] > majority) {
let winnerName = Object.keys(sortedObject[0])[0];
if (winnerName == '-1') winnerName = "No vote";
const winner = voteEngine.preformVote(votesFormatted);
console.log(`Winner: ${winner.name}`);
pollStats.winnerName = winnerName;
pollStats.votesCounted = totalVotes;
pollStats.winnerSelected = true;
// Update the stats
pollStats.winnerName = winner.name;
pollStats.winnerSelected = true;
pollStats.votesCounted = Object.keys(pollStats.responses).length;
pollStats.iterations = winner.iterations;
// _emitEvent({type: "poll_sync", poll: poll, pollStats: pollStats});
Messages.sendMessage(poll.id, JSON.stringify({type: "poll_winner", pollStats: pollStats}));
console.log(`\nWinner: ${winnerName}\nElection rounds: ${pollStats.iterations}\nVotes counted: ${totalVotes}`);
pollStats.responses = {};
return; // Winner was selected. We are done!
};
// If there is not a majority vote, remove the least popular candidate and call preformElection() again
let leastPopularIndex = sortedObject.length - 1;
let leastPopular = Object.keys(sortedObject[leastPopularIndex])[0];
// Check to see if least popular is "-1"/"no-vote"
if (leastPopular === "-1") {
leastPopularIndex--;
leastPopular = Object.keys(sortedObject[leastPopularIndex])[0]; // Get the real leastPopular candidate
}
console.log(`Removing least popular: ${leastPopular}`);
// Go into each vote and delete the selected least popular candidate
Object.keys(pollStats.responses).forEach((uuid) => {
// Remove the least popular candidate from each vote.
if (pollStats.responses[uuid].indexOf(leastPopular) != -1) pollStats.responses[uuid].splice(pollStats.responses[uuid].indexOf(leastPopular), 1);
console.log(pollStats.responses[uuid]);
});
// Update statistics
pollStats.iterations++;
// Run again
preformElection();
// Synchronize the winner with all clients
Messages.sendMessage(poll.id, JSON.stringify({type: "poll_winner", pollStats: pollStats}));
pollStats.responses = {};
return;
}
// Create a UUID or turn an existing UUID into a string

View file

@ -626,7 +626,6 @@ Rectangle {
}
}
// TODO: Style: Not centered. Remake.
// Client actions
RowLayout {
width: parent.width

View file

@ -0,0 +1,98 @@
/*
[
["<NAME>", "<NAME>", "<NAME>", "<NAME>", "<NAME>"], // Each entry in the array is an array of strings.
["<NAME>", "<NAME>", "<NAME>", "<NAME>", "<NAME>"], // Each entry is a separate vote received from a participant.
["<NAME>", "<NAME>", "<NAME>", "<NAME>" ], // There may be entries missing as a form of "non-vote"
[ ] // There may just be an empty array.
]
*/
let ballotsStorage = [];
let iterations = 0;
function preformVote(arrayOfBallots) {
if (ballotsStorage.length === 0) {
ballotsStorage = arrayOfBallots
print(JSON.stringify(ballotsStorage, null, 4));
};
const totalAmountOfVotes = ballotsStorage.length;
let firstChoices = {};
if (totalAmountOfVotes === 0) return; // No votes, no results.
iterations++;
// Go though each ballot and count the first choice for each
for (let ballotIndex = 0; ballotIndex < totalAmountOfVotes; ballotIndex++) {
const firstChoice = ballotsStorage[ballotIndex][0];
// Convert "undefined" to "-1" as a non vote.
if (!firstChoice) {
if (!firstChoices["-1"]) firstChoices["-1"] = 0;
firstChoices["-1"]++;
continue;
}
// Keep track of the most preferred candidate.
if (!firstChoices[firstChoice]) firstChoices[firstChoice] = 0;
firstChoices[firstChoice]++;
}
// At this point we have a map of the first choices.
// Now we need to find the candidates that have the lowest amount of votes.
// Exclude candidates that have a name of "-1". This is considered a non vote.
// We look for the lowest voted for candidate, take their amount of votes, and look for other candidates that share this value.
let lowestVoteAmount = Infinity;
let highestVoteAmount = -Infinity;
let highestVoteCandidate = "";
// let highestVoteCandidateTied = false; // If there are multiple candidates with the same amount of votes. TODO
// Find the lowest amount of votes for a candidate
Object.keys(firstChoices).forEach((candidate) => {
if (firstChoices[candidate] > highestVoteAmount) {
highestVoteAmount = firstChoices[candidate];
highestVoteCandidate = candidate;
};
if (candidate === "-1") return; // Never eliminate -1
if (firstChoices[candidate] < lowestVoteAmount) lowestVoteAmount = firstChoices[candidate];
});
// Check to see if we have a winner.
// A winner is chosen when they have a total vote amount that is more than half of the total votes.
// print(JSON.stringify(firstChoices, null, 4));
// print(`Is ${highestVoteAmount} > ${Math.floor(ballotsStorage.length / 2)}`);
// TODO: Check for ties
if (highestVoteAmount > Math.floor(ballotsStorage.length / 2)) {
const returnValue = {name: highestVoteCandidate, iterations: iterations};
iterations = 0; // Reset iterations.
ballotsStorage = []; // Reset the ballots array.
return returnValue;
}
// Make a list of candidates that share the lowest vote
// These will be the candidates that will be removed before the next "round/iteration" of elimination
let candidatesWithSameLowestVote = [];
Object.keys(firstChoices).forEach((candidate) => {
if (candidate === "-1") return;
if (firstChoices[candidate] == lowestVoteAmount) candidatesWithSameLowestVote.push(candidate);
});
print(`Lowest amount of votes: ${lowestVoteAmount}`);
print(`Removing candidates: ${candidatesWithSameLowestVote}`);
// Remove all candidates with the lowest vote amount from the first choices
for (let ballotIndex = 0; ballotIndex < totalAmountOfVotes; ballotIndex++) {
const firstChoice = ballotsStorage[ballotIndex][0];
if (candidatesWithSameLowestVote.includes(firstChoice)) {
ballotsStorage[ballotIndex].shift(); // Remove the first choice from this ballot
}
}
return preformVote(ballotsStorage);
}
module.exports = { preformVote };