ElectionsBot/src/predictor.ts

102 lines
No EOL
3.4 KiB
TypeScript

import { createSVGWindow } from 'svgdom'
import { SVG, registerWindow } from '@svgdotjs/svg.js'
import sharp from "sharp";
import crypto from "crypto";
import Election from "./election.js";
import VoteType from './VoteType.js';
export function MakePrediction(election : Election) : Promise<Prediction> {
return new Promise(async res => {
const window = createSVGWindow();
registerWindow(window, window.document);
var draw = SVG(window.document.documentElement);
draw.svg(election.svg);
var pred = {} as any;
pred.candidates = [];
for (var candidate of election.candidates) {
pred.candidates.push({
name: candidate.name,
party: candidate.party,
votes: 0,
electoralVotes: 0,
});
}
for (const state of Object.keys(election.states)) {
if (election.states[state].population === undefined) election.states[state].population = 1000;
if (Object.keys(election.states[state].odds).every(p => election.states[state].odds[p] <= 0))
for (const candidate of election.candidates)
election.states[state].odds[candidate.party] = 1;
var votes = await getVotes(election.states[state].population!, election.states[state].odds);
var winner = Object.keys(votes).sort((a, b) => votes[b] - votes[a])[0];
if (election.voteType === VoteType.Electoral) {
pred.candidates.find((c : any) => c.party === winner).electoralVotes += election.states[state].electoralVotes;
}
for (const candidate of election.candidates) {
pred.candidates.find((c : any) => c.party === candidate.party).votes += votes[candidate.party];
}
// @ts-ignore
draw.find(`#${state}`).fill(election.candidates.find((c : any) => c.party === winner).color);
}
var s = sharp(Buffer.from(draw.svg()));
var png = await s.png().toBuffer();
if (election.voteType === VoteType.Electoral)
pred.winner = pred.candidates.sort((a : any, b : any) => b.electoralVotes - a.electoralVotes)[0].name;
else if (election.voteType === VoteType.Popular)
pred.winner = pred.candidates.sort((a : any, b : any) => b.votes - a.votes)[0].name;
pred.totalVotes = pred.candidates.reduce((a : any, b : any) => a + b.votes, 0);
pred.svg = draw.svg();
pred.png = png;
res(pred);
});
}
export interface Prediction {
candidates : {
name : string,
party : string,
electoralVotes : number,
votes : number,
}[],
winner: string,
totalVotes: number,
svg: string,
png: Buffer,
}
export function betterRandom(min : number, max : number) : Promise<number> {
return new Promise(res => {
crypto.randomBytes(4, (err, buf) => {
if (err) throw err;
const random = buf.readUInt32LE(0) / 0x100000000;
res(Math.floor(min + random * (max - min)));
});
});
}
// https://stackoverflow.com/a/8435261
export async function weightedRand(spec : { [key : string] : number }) : Promise<string> {
var i, j, table = [];
for (i in spec) {
if (spec[i] <= 0) continue;
for (j = 0; j < spec[i] * 10; j++) {
table.push(i);
}
}
return table[await betterRandom(0, table.length)];
}
export async function getVotes(population: number, spec: { [key: string]: number }) : Promise<{ [key: string]: number }>{
// split the population into 100 chunks
var chunk = population / 100;
var votes = {} as { [key: string]: number };
for (var c in spec) votes[c] = 0;
for (var i = 0; i < 100; i++) {
var winner = await weightedRand(spec);
votes[winner] += chunk;
}
for (var c in votes) votes[c] = Math.round(votes[c]);
return votes;
}