102 lines
No EOL
3.4 KiB
TypeScript
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;
|
|
} |