mirror of
https://github.com/AleziaKurdis/Overte-community-apps.git
synced 2025-04-05 13:37:41 +02:00
Refactored voting engine.
Should be more simple to follow.
This commit is contained in:
parent
2243de9cc9
commit
5ef98c7c5b
3 changed files with 118 additions and 79 deletions
|
@ -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
|
||||
|
|
|
@ -626,7 +626,6 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Style: Not centered. Remake.
|
||||
// Client actions
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
|
98
applications/voting/vote_engine.js
Normal file
98
applications/voting/vote_engine.js
Normal 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 };
|
Loading…
Reference in a new issue