Skip to content

Instantly share code, notes, and snippets.

@benib
Created May 18, 2016 15:41
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benib/5cffac28f7fc0b44e60c5f3276c71190 to your computer and use it in GitHub Desktop.
Save benib/5cffac28f7fc0b44e60c5f3276c71190 to your computer and use it in GitHub Desktop.
NZZ Euro 2016 Orakel Algorithmus
/*
NZZ Euro 2016 Orakel
http://nzz.ch/-ld.17757
The algorithm
this works like this:
first we calculate the total points per team using getTotalPoints function which calculates the
sum of weight points * parameter for all 5 parameters. (and some additional points if special flags set).
this gives us something like this
points team a (20) points team b (40)
|--------------------/----------------------------------------|
^ 0 ^ threshold (20) ^ total points team a + team b (60)
In the end we pick a random number within the total, if its below the threshold, a scores, otherwise b.
but before we shift the threshold twice (not always):
underdog bonus:
take the spread (diff between a and b), cut it in half (to never cross the 50% of total points),
divide it by 5 and multiply this with the underdog bonus points (0, 1 or 2).
then shift the threshold by the result of this calculation towards the center (50% of total points).
let underdogThresholdShift = spread / 2 / 5 * flags.underdogBonus;
let's say we have an underdog bonus of 1 for this game, we shift the threshold by 40 / 2 / 5 * 1 = 4 points to the center.
unclear weight points:
take all the weight points, if we do not have a diff between min and max > 2, then we assume that the user is not
sure about what parameter is important, so we add more randomness to the system by shifting the threshold again
and thus making the underdog stronger.
for this we take the take the new spread, cut it in half and divide it by 25 (the maximum possible weight point sum).
we multiply this by the difference of 25 and the total provided weight points (5 times a number between 1 and 5).
then we shift the threshold again towards the center by this amount.
if everything is pretty important (high totalWeightPoints), the shift is smaller than if everything is unimportant.
let weightThresholdShift = (spread / 2 / maxWeightPoints) * (maxWeightPoints - totalWeightPoints);
ko round:
if the game is part of the ko round, we think the underdogs chance to win the game are lower, as the pressure on favorit
is pretty high, this gives them a boost, so in ko round we shift the threshold back away from the center
by 2/5 of half the initial spread.
*/
// wm '14 and em '12
const numberOfGamesWithGoals = [
7,19,17,26,12,7,5,1,1
];
let goalDistribution = [];
for (let i = 0; i < numberOfGamesWithGoals.length; i++) {
for (let ii = 0; ii < numberOfGamesWithGoals[i]; ii++) {
goalDistribution.push(i);
}
}
const maxWeightPoints = 25;
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function getWinnerFromThreshold(threshold, totalPoints) {
let random = getRandom(0, totalPoints);
if (random < threshold) { // random is on the left side of the threshold
return 'a';
} else if (random > threshold) {
return 'b';
} else {
return false;
}
}
// returns the total points for a team based on param weights and some special flags
function getTotalPoints(team, weights, flags) {
let paramNames = Object.keys(weights);
let points = paramNames
.reduce(function(totalPoints, currentParam) {
return totalPoints + parseInt(weights[currentParam]) * parseFloat(team[currentParam]);
}, 0);
if (flags.homeFieldAdvantage && team.kurz === 'FRA') {
points = points * 1.1;
}
if (flags.hopSwitzerland && team.kurz === 'SUI') {
points = points * 1.1;
}
if (flags.linekerAxiom && team.kurz === 'DEU') {
points = points * 1.1;
}
return points;
}
function getScorer(threshold, totalPoints) {
let scorer = false;
while (scorer === false) {
scorer = getWinnerFromThreshold(threshold, totalPoints);
}
return scorer;
}
/*
getResult()
params:
a the home team data
b the away team data
weights weights per parameter
isKoRound boolean
see comment on top of this file for an explanation
*/
function getResult(a, b, weights, flags, isKoRound) {
let pointsA = getTotalPoints(a, weights, flags);
let pointsB = getTotalPoints(b, weights, flags);
let totalPoints = pointsA + pointsB;
let spread = Math.abs(pointsA - pointsB);
let initialSpread = spread;
// set the threshold at the points team a provides to the total
let threshold = pointsA;
// shift the threshold towards the center if we have and underdog (spread > 0)
if (spread > 0) {
// underdog bonus
let underdogThresholdShift = spread / 2 / 5 * flags.underdogBonus;
if (pointsA > pointsB) {
threshold = threshold - underdogThresholdShift;
} else if (pointsA < pointsB) {
threshold = threshold + underdogThresholdShift;
}
// update the spread after threshold shift
spread = Math.abs((totalPoints - threshold) - threshold);
}
// shift the threshold towards the center if we do not have clear weight points
let weightPoints = Object.keys(weights)
.map(function(paramName) {
return parseInt(weights[paramName]);
});
let maxWeightPoint = Math.max.apply(null, weightPoints);
let minWeightPoint = Math.min.apply(null, weightPoints);
if (spread > 0 && maxWeightPoint - minWeightPoint < 2) {
let totalWeightPoints = weightPoints
.reduce(function(currentTotal, weightPoints) {
return currentTotal + weightPoints;
}, 0);
let weightThresholdShift = (spread / 2 / maxWeightPoints) * (maxWeightPoints - totalWeightPoints);
if (pointsA > pointsB) {
threshold = threshold - weightThresholdShift;
} else if (pointsA < pointsB) {
threshold = threshold + weightThresholdShift;
}
}
// if we are in ko round, we shift threshold away from the center by 1/5 of half the initial spread
if (isKoRound) {
let koRoundThresholdShift = initialSpread / 2 / 5;
if (pointsA > pointsB) {
threshold = threshold + koRoundThresholdShift;
} else if (pointsA < pointsB) {
threshold = threshold - koRoundThresholdShift;
}
}
// run every goal against the computed strength
let numberOfGoals = goalDistribution[getRandom(0,goalDistribution.length)];
let goals = {a: 0, b: 0};
for (let i = 0; i < numberOfGoals; i++) {
let scorer = getScorer(threshold, totalPoints);
goals[scorer] = goals[scorer] + 1;
}
let winner = false;
if (goals.a > goals.b) {
winner = 'a';
} else if (goals.b > goals.a) {
winner = 'b';
}
let winnerAfterTieBreak = false;
// if this is ko round, we need a winner. if there is none, we run the algorithm for one more goal to get a winner.
if (isKoRound === true && winner === false) {
let scorer = getScorer(threshold, totalPoints);
goals[scorer] = goals[scorer] + 1;
winner = scorer;
winnerAfterTieBreak = true;
}
return {
winner: winner,
winnerAfterTieBreak: winnerAfterTieBreak,
goals: goals,
pointsA: pointsA,
pointsB: pointsB
};
}
export default {
getResult,
getTotalPoints
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment