mirror of
https://github.com/AleziaKurdis/Overte-community-apps.git
synced 2025-04-05 13:37:41 +02:00
Functional election engine.
This commit is contained in:
parent
fb47e24a65
commit
24816cd49b
2 changed files with 155 additions and 16 deletions
|
@ -12,17 +12,27 @@
|
|||
/* global Script Tablet Messages MyAvatar Uuid*/
|
||||
|
||||
// TODO: Documentation
|
||||
// TODO: Save questions and answers locally
|
||||
// TODO: Allow more than 9 candidates
|
||||
// TODO: Allow host voting
|
||||
// TODO: Sound for new vote
|
||||
// TODO: Clear poll host view on creating new poll
|
||||
// TODO: Confirm before closing poll
|
||||
// TODO: Debug mode?
|
||||
// FIXME: Handle ties
|
||||
// FIXME: Joining poll resets everyones vote
|
||||
// FIXME: Running election without votes causes max stack error
|
||||
|
||||
(() => {
|
||||
"use strict";
|
||||
var tablet;
|
||||
var appButton;
|
||||
var active = false;
|
||||
var poll = {id: '', title: '', description: '', host: '', question: '', options: []};
|
||||
var responses = {};
|
||||
const debug = false;
|
||||
|
||||
var poll = {id: '', title: '', description: '', host: '', question: '', options: []}; // The current poll
|
||||
var responses = {}; // All ballots received and to be used by the election function.
|
||||
let electionIterations = 0; // How many times the election function has been called to narrow down a candidate.
|
||||
|
||||
const url = Script.resolvePath("./vote.qml");
|
||||
const myUuid = generateUUID(MyAvatar.sessionUUID);
|
||||
Messages.messageReceived.connect(receivedMessage);
|
||||
|
@ -100,6 +110,7 @@
|
|||
poll.title = pollInformation.title;
|
||||
poll.description = pollInformation.description;
|
||||
console.log(`Active poll set as:\nid:${poll.id}\ntitle:${poll.title}\ndescription:${poll.description}`);
|
||||
responses = {}; // Clear any lingering responses
|
||||
|
||||
// Send message to all clients
|
||||
Messages.sendMessage("ga-polls", JSON.stringify({type: "active_poll", poll: poll}));
|
||||
|
@ -110,6 +121,13 @@
|
|||
|
||||
// Update the UI screen
|
||||
_emitEvent({type: "create_poll"});
|
||||
|
||||
// Debug: Create a lot of fake ballots
|
||||
if (!debug) return;
|
||||
|
||||
for (let i = 0; i < 25; ++i) {
|
||||
_debugDummyBallot();
|
||||
}
|
||||
}
|
||||
|
||||
// Closes the poll and return to the main menu
|
||||
|
@ -180,16 +198,78 @@
|
|||
function emitPrompt(){
|
||||
if (poll.host != myUuid) return; // We are not the host of this poll
|
||||
|
||||
console.log(`Clearing responses`)
|
||||
responses = {}
|
||||
|
||||
console.log(`Emitting prompt`);
|
||||
Messages.sendMessage(poll.id, JSON.stringify({type: "poll_prompt", prompt: {question: poll.question, options: poll.options}}));
|
||||
}
|
||||
|
||||
// Take the gathered responses and preform the election
|
||||
// TODO: Simplify logging of critical information
|
||||
// FIXME: Recursive function call
|
||||
function preformElection(){
|
||||
// Get the array of responses in a list
|
||||
let voteList = Object.values(responses);
|
||||
let firstVotes = [];
|
||||
let voteObject = {};
|
||||
|
||||
console.log(voteList)
|
||||
// TODO: Debug total votes at beginning of election vs ending
|
||||
|
||||
Object.keys(responses).forEach((key) => {
|
||||
let uuid = key;
|
||||
let vote = responses[uuid];
|
||||
|
||||
// Assign first vote to new array
|
||||
firstVotes.push(vote[0]);
|
||||
});
|
||||
|
||||
for (let i = 0; i < firstVotes.length; i++) {
|
||||
// Check if firstVotes index exists
|
||||
if (!firstVotes[i]) firstVotes[i] = -1; // FIXME: We need a special case for "Non-vote" or "Vacant?"
|
||||
|
||||
// Create voteObject index if it does not exist
|
||||
if (!voteObject[firstVotes[i]]) voteObject[firstVotes[i]] = 0;
|
||||
|
||||
// Increment value for each vote
|
||||
voteObject[firstVotes[i]]++
|
||||
}
|
||||
|
||||
console.log(`Votes: ${JSON.stringify(voteObject, null, 4)}`);
|
||||
|
||||
// Check to see if there is a majority vote
|
||||
let totalVotes = Object.keys(responses).length; // TODO: Check to make sure this value never changes.
|
||||
let majority = Math.floor(totalVotes / 2);
|
||||
|
||||
// Sort the voteObject by value in descending order
|
||||
const sortedArray = Object.entries(voteObject).sort(([, a], [, b]) => b - a); // FIXME: This works but looks ugly
|
||||
const sortedObject = Object.fromEntries(sortedArray);
|
||||
|
||||
// Check the most voted for option to see if it makes up over 50% of votes
|
||||
// NOTE: Has to be *over* 50%.
|
||||
if (sortedObject[Object.keys(sortedObject)[0]] > majority) {
|
||||
// Show dialog of election statistics
|
||||
console.log(`\nWinner: ${Object.keys(sortedObject)[0]}\nElection rounds: ${electionIterations}\nVotes counted: ${totalVotes}`);
|
||||
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 = Object.keys(sortedObject).length - 1;
|
||||
let leastPopular = Object.keys(sortedObject)[leastPopularIndex];
|
||||
|
||||
console.log(`Removing least popular: ${JSON.stringify(leastPopular, null, 4)}`);
|
||||
|
||||
// Go into each vote and delete the selected least popular candidate
|
||||
Object.keys(responses).forEach((key) => {
|
||||
let uuid = key;
|
||||
// Remove the least popular candidate from each vote.
|
||||
responses[uuid].splice(responses[uuid].indexOf(leastPopular), 1);
|
||||
console.log(responses[uuid]);
|
||||
});
|
||||
|
||||
// Update statistics
|
||||
electionIterations++;
|
||||
|
||||
// Run again
|
||||
preformElection();
|
||||
}
|
||||
|
||||
// Create a UUID or turn an existing UUID into a string
|
||||
|
@ -200,6 +280,21 @@
|
|||
return existingUuid.replace(/[{}]/g, ''); // Remove '{' and '}' from UUID string >:(
|
||||
}
|
||||
|
||||
function _debugDummyBallot() {
|
||||
if (!debug) return; // Just incase...
|
||||
let ballot = getRandomOrder('C1', 'C2', 'C3', 'C4', 'C5', 'C6');
|
||||
|
||||
responses[Object.keys(responses).length.toString()] = ballot;
|
||||
|
||||
function getRandomOrder(...words) {
|
||||
for (let i = words.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[words[i], words[j]] = [words[j], words[i]];
|
||||
}
|
||||
return words;
|
||||
}
|
||||
}
|
||||
|
||||
// Communication
|
||||
function fromQML(event) {
|
||||
console.log(`New QML event:\n${JSON.stringify(event)}`);
|
||||
|
@ -222,6 +317,9 @@
|
|||
poll.options = event.prompt.options;
|
||||
emitPrompt();
|
||||
break;
|
||||
case "run_election":
|
||||
preformElection();
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -285,6 +383,20 @@
|
|||
_emitEvent({type: "poll_prompt", prompt: message.prompt});
|
||||
}
|
||||
|
||||
// Received a ballot
|
||||
if (message.type == "vote") {
|
||||
// Check if we are the host
|
||||
if (poll.host != myUuid) return;
|
||||
|
||||
// Record the ballot
|
||||
responses[message.uuid] = message.ballot;
|
||||
|
||||
// Emit a echo so the voter knows we have received it
|
||||
// TODO:
|
||||
|
||||
// console.log(JSON.stringify(responses));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -234,23 +234,21 @@ Rectangle {
|
|||
ListModel {
|
||||
id: poll_option_model_host
|
||||
|
||||
ListElement {
|
||||
option: "Yes"
|
||||
}
|
||||
|
||||
ListElement {
|
||||
option: "No"
|
||||
}
|
||||
// ListElement {
|
||||
// option: "Prefill"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// Add Option Button
|
||||
Item {
|
||||
ColumnLayout {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 150
|
||||
|
@ -322,7 +320,30 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 150
|
||||
height: 40
|
||||
color: "#1c71d8"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text:"Run Election"
|
||||
color: "white"
|
||||
font.pointSize:18
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
toScript({type: "run_election"})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,10 +457,16 @@ Rectangle {
|
|||
votes[option.option] = option.rank
|
||||
}
|
||||
|
||||
// TODO: This is painful to look at.
|
||||
// FIXME: This is painful to look at.
|
||||
// Sort the object from lowest to heighest
|
||||
var entries = Object.entries(votes);
|
||||
entries.sort((a, b) => a[1] - b[1]);
|
||||
|
||||
// Remove entries that have a numerical value of 0
|
||||
// FIXME: Inconsistant with how we are handling non-votes in the script side?
|
||||
// This is our "leave seat empty" or "non-vote"
|
||||
entries = entries.filter((entry) => entry[1]!== 0);
|
||||
|
||||
// Get names instead of numbers
|
||||
var onlyNames = entries.map((entry) => entry[0]);
|
||||
|
||||
|
|
Loading…
Reference in a new issue