Skip to content

Instantly share code, notes, and snippets.

@JLaferri
Created March 8, 2022 21:16
Show Gist options
  • Save JLaferri/b4cc280e6f376ab22a55f3475e95a89f to your computer and use it in GitHub Desktop.
Save JLaferri/b4cc280e6f376ab22a55f3475e95a89f to your computer and use it in GitHub Desktop.
Script to test openskill by generating random players and simulating a bunch of matches
const util = require('util');
const { range, random, min, max, mean, orderBy } = require('lodash');
const { rating, rate } = require('openskill');
const PLAYER_COUNT = 100;
const START_MU = 25;
const START_SIGMA = 25.0 / 3.0;
const MATCH_COUNT = PLAYER_COUNT * 1000; // Creates ~200 games per player
// const TAU = 25.0 / 300.0;
const TAU = 0.3;
const ORDINAL_SCALING = 25.0;
const ORDINAL_OFFSET = 1100.0;
const slippiOrdinal = (rating) => {
// The constants used get a range fairly close to glicko's range
// https://docs.google.com/spreadsheets/d/1rDPjiYX8JaoGebTlIK_2OYeRzEVm2fcRTJOsMYKY-1Q/edit#gid=0
return ORDINAL_SCALING * (rating.mu - 3 * rating.sigma) + ORDINAL_OFFSET
// return rating.mu - 3 * rating.sigma;
};
// https://stackoverflow.com/a/49434653/1249024
const randn_bm = () => {
let u = 0, v = 0;
while(u === 0) u = Math.random(); //Converting [0,1) to (0,1)
while(v === 0) v = Math.random();
let num = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
num = num / 10.0 + 0.5; // Translate to 0 -> 1
if (num > 1 || num < 0) return randn_bm() // resample between 0 and 1
return num
}
const playMatches = (players, count, tau = 0) => {
const games = [];
const changes = [];
const skillSortedPlayers = orderBy(players, 'skill');
const playerToFollow = skillSortedPlayers[PLAYER_COUNT / 2];
const getRating = r => ({
mu: r.mu,
sigma: r.sigma,
ordinal: slippiOrdinal(r)
});
range(0, count, 1).forEach(() => {
const p1Idx = random(PLAYER_COUNT - 1);
let p2Idx = random(PLAYER_COUNT - 1);
while (p2Idx === p1Idx) {
p2Idx = random(PLAYER_COUNT - 1);
}
const p1 = players[p1Idx];
const p2 = players[p2Idx];
p1.gamesPlayed += 1;
p2.gamesPlayed += 1;
// This is probably a shit way to decide who wins
const lossProbability = 0.5 - 0.4 * (p1.skill - p2.skill);
const gameResultRoll = Math.random();
const result = gameResultRoll > lossProbability ? [1, 2] : [2, 1];
const game = {
result: result,
player1: {
change: {
previous: getRating(p1.rating),
},
change2: {
previous: getRating(p1.rating2),
},
},
player2: {
change: {
previous: getRating(p2.rating),
},
change2: {
previous: getRating(p2.rating2),
},
},
};
const [[r1], [r2]] = rate([[p1.rating], [p2.rating]], {
rank: result,
});
p1.rating = r1;
p2.rating = r2;
p1.rating2.sigma = Math.sqrt(p1.rating2.sigma * p1.rating2.sigma + tau * tau);
p2.rating2.sigma = Math.sqrt(p2.rating2.sigma * p2.rating2.sigma + tau * tau);
const [[r1_2], [r2_2]] = rate([[p1.rating2], [p2.rating2]], {
rank: result,
});
p1.rating2 = r1_2;
p2.rating2 = r2_2;
game.player1.change.new = getRating(p1.rating);
game.player1.change2.new = getRating(p1.rating2);
game.player1.change.amount = game.player1.change.new.ordinal - game.player1.change.previous.ordinal;
game.player1.change2.amount = game.player1.change2.new.ordinal - game.player1.change2.previous.ordinal;
game.player2.change.new = getRating(p2.rating);
game.player2.change2.new = getRating(p2.rating2);
game.player2.change.amount = game.player2.change.new.ordinal - game.player2.change.previous.ordinal;
game.player2.change2.amount = game.player2.change2.new.ordinal - game.player2.change2.previous.ordinal;
games.push(game);
if (p1 === playerToFollow) {
changes.push({
gameNum: p1.gamesPlayed,
ratingChange: Math.abs(game.player1.change.amount),
ratingChangeWithTau: Math.abs(game.player1.change2.amount),
sigma: p1.rating.sigma,
sigmaWithTau: p1.rating2.sigma,
});
} else if (p2 === playerToFollow) {
changes.push({
gameNum: p2.gamesPlayed,
ratingChange: Math.abs(game.player2.change.amount),
ratingChangeWithTau: Math.abs(game.player2.change2.amount),
sigma: p2.rating.sigma,
sigmaWithTau: p2.rating2.sigma,
});
}
});
return {
games: games,
trackedPlayerChanges: changes,
};
};
const printPlayers = (players) => {
console.log("Index, Skill, GamesPlayed, Rating, Mu, Sigma, Rating2, Mu2, Sigma2");
players.forEach((p) => {
console.log(`${p.index}, ${p.skill}, ${p.gamesPlayed}, ${slippiOrdinal(p.rating)}, ${p.rating.mu}, ${p.rating.sigma}, ${slippiOrdinal(p.rating2)}, ${p.rating2.mu}, ${p.rating2.sigma}`);
});
};
const printAggregates = (players) => {
const ratings = players.map(p => slippiOrdinal(p.rating));
console.log({
minRating: min(ratings),
maxRating: max(ratings),
averageRating: mean(ratings),
});
};
const printChanges = (changes) => {
console.log("GameNum, RatingChange, RatingChangeWithTau, Sigma, SigmaWithTau");
changes.forEach(c => {
console.log(`${c.gameNum}, ${c.ratingChange}, ${c.ratingChangeWithTau}, ${c.sigma}, ${c.sigmaWithTau}`)
});
}
// Sheet with testing data:
// https://docs.google.com/spreadsheets/d/1KDnA_w_RwGDzS_5ErhrIwxQHiChAM3JBdMqCXhJ6qUs/edit#gid=1174040382
(async () => {
console.log("Initializing Players...");
const players = range(0, PLAYER_COUNT, 1).map((idx) => {
const skill = randn_bm() * 10;
return {
skill: skill,
index: idx,
rating: rating({ mu: START_MU, sigma: START_SIGMA }),
rating2: rating({ mu: START_MU, sigma: START_SIGMA }),
gamesPlayed: 0,
};
});
// Run 10000 matches
// TODO: Perhaps add matchmaking requirements?
console.log(`Playing ${MATCH_COUNT} matches...`);
const results = playMatches(players, MATCH_COUNT, TAU);
console.log("Printing players...");
printPlayers(players);
console.log("Printing aggregates...");
printAggregates(players);
console.log("Printing rating changes...");
printChanges(results.trackedPlayerChanges);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment