Functional election engine.

This commit is contained in:
armored-dragon 2024-09-10 03:34:29 -05:00
parent fb47e24a65
commit 24816cd49b
No known key found for this signature in database
GPG key ID: C7207ACC3382AD8B
2 changed files with 155 additions and 16 deletions

View file

@ -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));
}
}
}

View file

@ -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]);