arpad
Version:
An implementation of the ELO Rating System
283 lines (211 loc) • 7.27 kB
JavaScript
;
const test = require('tape');
const Elo = require('../index.js');
test("sanity check", (t) => {
t.ok(approximate(1, 1, 0));
t.ok(approximate(1, 1));
t.ok(approximate(9999.99999999, 9999.999999998));
t.notOk(approximate(0.12345, 0.12347));
t.notOk(approximate(0.12347, 0.12345));
t.ok(approximate(0.12345, 0.12346));
t.ok(approximate(0.12346, 0.12345));
t.end();
});
test("should have expected defaults", (t) => {
let myElo = new Elo();
t.equal(myElo.getKFactor(), 32);
t.equal(myElo.getMin(), -Infinity);
t.equal(myElo.getMax(), Infinity);
t.end();
});
test("should allow passing in defaults via the constructor", (t) => {
let fide = new Elo(20, 2251, 2825);
t.equal(fide.getKFactor(), 20);
t.equal(fide.getMin(), 2251);
t.equal(fide.getMax(), 2825);
t.end();
});
test("should allow updating of parameters after instantiation", (t) => {
let uscf = new Elo(31, 0, Infinity);
t.equal(uscf.getKFactor(), 31);
t.equal(uscf.getMin(), 0);
t.equal(uscf.getMax(), Infinity);
uscf.setMin(100).setMax(2500).setKFactor(27);
t.equal(uscf.getMin(), 100);
t.equal(uscf.getMax(), 2500);
t.equal(uscf.getKFactor(), 27);
t.end();
});
test("prevents scores leaving boundaries", (t) => {
let elo = new Elo(32, 100, 2800);
let alice = 2000;
for (let i = 0; i < 100; i++) {
alice = elo.newRating(1, 0, alice);
}
t.equal(alice, 100);
let bob = 200;
for (let j = 0; j < 100; j++) {
bob = elo.newRating(0, 1, bob);
}
t.equal(bob, 2800);
t.end();
});
test("should determine the probability that someone will win or lose", (t) => {
let myElo = new Elo(32, 0, 2500);
let alice = 1400;
let bob = 1200;
let odds_alice_wins = myElo.expectedScore(alice, bob);
let odds_bob_wins = myElo.expectedScore(bob, alice);
t.ok(odds_alice_wins <= 1);
t.ok(odds_alice_wins >= 0);
t.ok(odds_bob_wins <= 1);
t.ok(odds_bob_wins >= 0);
t.ok(odds_alice_wins > odds_bob_wins);
t.ok(approximate(odds_alice_wins + odds_bob_wins, 1));
let both_odds = myElo.bothExpectedScores(alice, bob);
t.equal(odds_alice_wins, both_odds[0]);
t.equal(odds_bob_wins, both_odds[1]);
t.end();
});
test("should calculate same scores as WikiPedia", (t) => {
let elo = new Elo(32);
let rating = 1613;
let opponents = [
{rating: 1609, score: 0.506},
{rating: 1477, score: 0.686},
{rating: 1388, score: 0.785},
{rating: 1586, score: 0.539},
{rating: 1720, score: 0.351}
];
opponents.forEach((pair) => {
let actual = elo.expectedScore(rating, pair.rating);
t.ok(approximate(actual, pair.score, 0.001));
});
t.end();
});
test("should calculate same ratings as WikiPedia", (t) => {
let elo = new Elo(32);
let current_rating = 1613;
let actual_score = 3;
let expected_score = 2.867;
t.equal(elo.newRating(expected_score, actual_score, current_rating), 1617);
t.end();
});
test("should do some end-to-end examples", (t) => {
let elo = new Elo(24, 200, 3000);
let alice_rating = 1600;
let bob_rating = 1300;
let expected_alice_score = elo.expectedScore(alice_rating, bob_rating);
let expected_bob_score = elo.expectedScore(bob_rating, alice_rating);
t.ok(approximate(expected_alice_score, 0.849, 0.001));
t.ok(approximate(expected_bob_score, 0.151, 0.001));
// Assuming Alice wins (which is expected)
let alice_new_rating = elo.newRating(expected_alice_score, 1, alice_rating);
let bob_new_rating = elo.newRating(expected_bob_score, 0, bob_rating);
t.equal(alice_new_rating, 1604);
t.equal(bob_new_rating, 1296);
// Assuming Bobb wins (which is unexpected)
alice_new_rating = elo.newRating(expected_alice_score, 0, alice_rating);
bob_new_rating = elo.newRating(expected_bob_score, 1, bob_rating);
t.equal(alice_new_rating, 1580);
t.equal(bob_new_rating, 1320);
t.end();
});
test("should get same results when using convenience methods", (t) => {
let elo = new Elo(32);
let alice_rating = 1275;
let bob_rating = 1362;
let expected_alice_score = elo.expectedScore(alice_rating, bob_rating);
let alice_new_rating = elo.newRating(expected_alice_score, 1, alice_rating);
let alice_new_rating_convenient = elo.newRatingIfWon(alice_rating, bob_rating);
t.equal(alice_new_rating, alice_new_rating_convenient);
expected_alice_score = elo.expectedScore(alice_rating, bob_rating);
alice_new_rating = elo.newRating(expected_alice_score, 0, alice_rating);
alice_new_rating_convenient = elo.newRatingIfLost(alice_rating, bob_rating);
t.equal(alice_new_rating, alice_new_rating_convenient);
expected_alice_score = elo.expectedScore(alice_rating, bob_rating);
alice_new_rating = elo.newRating(expected_alice_score, 0.5, alice_rating);
alice_new_rating_convenient = elo.newRatingIfTied(alice_rating, bob_rating);
t.equal(alice_new_rating, alice_new_rating_convenient);
t.end();
});
test("should do valid K-factor lookups with no numeric K-Factor provided", (t) => {
let elo = new Elo();
t.equal(elo.getKFactor(), 32);
t.equal(elo.getKFactor(-Infinity), 32);
t.equal(elo.getKFactor(Infinity), 32);
t.equal(elo.getKFactor(0), 32);
t.equal(elo.getKFactor(1), 32);
t.end();
});
test("should do valid K-factor lookups with a numeric K-Factor provided", (t) => {
let elo = new Elo(42);
t.equal(elo.getKFactor(), 42);
t.equal(elo.getKFactor(-Infinity), 42);
t.equal(elo.getKFactor(Infinity), 42);
t.equal(elo.getKFactor(0), 42);
t.equal(elo.getKFactor(1), 42);
t.end();
});
test("table: should perform valid K-factor lookups", (t) => {
let uscf_table = {
0: 32,
2100: 24,
2400: 16
};
let elo = new Elo(uscf_table, 0, Infinity);
t.equal(elo.getKFactor(), 32);
t.equal(elo.getKFactor(0), 32);
t.equal(elo.getKFactor(null), 32);
t.equal(elo.getKFactor(100), 32);
t.equal(elo.getKFactor(-Infinity), null); // Or should this throw an error?
t.equal(elo.getKFactor(-1), null); // Or should this throw an error?
t.equal(elo.getKFactor(2099), 32);
t.equal(elo.getKFactor(2100), 24);
t.equal(elo.getKFactor(Infinity), 16);
t.end();
});
test("table: should allow default for lower bounds lookups", (t) => {
let table = {
0: 32,
default: 48
};
let elo = new Elo(table);
t.equal(elo.getKFactor(), 32);
t.equal(elo.getKFactor(0), 32);
t.equal(elo.getKFactor(1), 32);
t.equal(elo.getKFactor(-1), 48);
t.equal(elo.getKFactor(-Infinity), 48);
t.equal(elo.getKFactor(Infinity), 32);
t.end();
});
test("table: should use proper table entries when calculating ratings", (t) => {
let table = {
default: 100,
0: 50,
1000: 25
};
let elo = new Elo(table, -2000, 2000);
let alice = 500;
let bob = 2000;
let cathy = -500;
t.equal(elo.newRatingIfWon(alice, bob), 550);
t.equal(elo.newRatingIfWon(cathy, bob), -400);
let derik = 950;
derik = elo.newRatingIfWon(derik, bob);
t.equal(derik, 1000); // +50
derik = elo.newRatingIfWon(derik, bob);
t.equal(derik, 1025); // +25
t.end();
});
/**
* is 0.99999999999999 === 0.99999999999999 ?
*/
function approximate(actual, anticipated, threshold) {
if (!threshold) {
threshold = 0.00001;
}
let difference = Math.abs(actual - anticipated);
return difference < threshold;
}