Created
March 8, 2022 21:16
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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