mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-04-04 08:25:42 -05:00
429 lines
12 KiB
PHP
Executable File
429 lines
12 KiB
PHP
Executable File
<?php
|
|
|
|
error_reporting(E_ALL);
|
|
|
|
// An implementation of the Glicko2 rating system.
|
|
|
|
include_once dirname(__FILE__).'/../../pokemonshowdown.com/lib/ntbb-database.lib.php';
|
|
|
|
// connect to the ladder database (if we aren't already connected)
|
|
if (empty($ladderdb)) {
|
|
$ladderdb = new NTBBDatabase($config['ladder_server'],
|
|
$config['ladder_username'],
|
|
$config['ladder_password'],
|
|
$config['ladder_database'],
|
|
$config['ladder_prefix']);
|
|
}
|
|
|
|
class Glicko2Player {
|
|
public $rating;
|
|
public $rd;
|
|
public $sigma;
|
|
|
|
public $mu;
|
|
public $phi;
|
|
public $tau;
|
|
|
|
private $pi2 = 9.8696044;
|
|
|
|
var $M = array();
|
|
|
|
function __construct($rating = 1500, $rd = 350, $volatility = 0.06, $mu = null, $phi = null, $sigma = null, $systemconstant = 0.75) {
|
|
// Step 1
|
|
$this->rating = $rating;
|
|
$this->rd = $rd;
|
|
// volatility
|
|
if (is_null($sigma)) {
|
|
$this->sigma = $volatility;
|
|
} else {
|
|
$this->sigma = $sigma;
|
|
}
|
|
// System Constant
|
|
$this->tau = $systemconstant;
|
|
|
|
// Step 2
|
|
// Rating
|
|
if (is_null($mu)) {
|
|
$this->mu = ( $this->rating - 1500 ) / 173.7178;
|
|
} else {
|
|
$this->mu = $mu;
|
|
}
|
|
// Rating Deviation
|
|
if (is_null($phi)) {
|
|
$this->phi = $this->rd / 173.7178;
|
|
} else {
|
|
$this->phi = $phi;
|
|
}
|
|
}
|
|
|
|
function AddWin($OtherPlayer) {
|
|
$this->M[] = $OtherPlayer->MatchElement(1);
|
|
}
|
|
|
|
function AddLoss($OtherPlayer) {
|
|
$this->M[] = $OtherPlayer->MatchElement(0);
|
|
}
|
|
|
|
function AddDraw($OtherPlayer) {
|
|
$this->M[] = $OtherPlayer->MatchElement(0.5);
|
|
}
|
|
|
|
function Update() {
|
|
$Results = $this->AddMatches($this->M);
|
|
$this->rating = $Results['r'];
|
|
$this->rd = $Results['RD'];
|
|
$this->mu = $Results['mu'];
|
|
$this->phi = $Results['phi'];
|
|
$this->sigma = $Results['sigma'];
|
|
$this->M = array();
|
|
}
|
|
|
|
function MatchElement($score) {
|
|
return array( 'mu' => $this->mu, 'phi' => $this->phi, 'score' => $score );
|
|
}
|
|
|
|
function AddMatches($M) {
|
|
// This is where the Glicko2 rating calculation actually happens
|
|
|
|
// Follow along the steps using: http://www.glicko.net/glicko/glicko2.pdf
|
|
|
|
if (count($M) == 0) {
|
|
$phi_p = sqrt( ( $this->phi * $this->phi ) + ( $this->sigma * $this->sigma ) );
|
|
return array( 'r' => $this->rating, 'RD' => 173.7178 * $phi_p, 'mu' => $this->mu, 'phi' => $phi_p, 'sigma' => $this->sigma ) ;
|
|
}
|
|
|
|
// summation parts of Step 3 & 4 & 7
|
|
$v_sum = 0;
|
|
$delta_sum = 0;
|
|
$mu_p_sum = 0;
|
|
for ($j = 0; $j < count($M); $j++) {
|
|
$E = $this->E( $this->mu, $M[$j]['mu'], $M[$j]['phi'] );
|
|
$g = $this->g( $M[$j]['phi'] );
|
|
$v_sum += ( $g * $g * $E * ( 1 - $E ) );
|
|
|
|
$delta_sum += $g * ( $M[$j]['score'] - $E );
|
|
|
|
$mu_p_sum += $g * ( $M[$j]['score'] - $E );
|
|
}
|
|
|
|
// Step 3
|
|
// Estimated variance
|
|
$v = 1.0 / $v_sum;
|
|
|
|
// Step 4
|
|
// Estimated improvment in rating
|
|
$delta = $v * $delta_sum;
|
|
|
|
// Step 5
|
|
$a = log( $this->sigma * $this->sigma );
|
|
$x_prev = $a;
|
|
$x = $x_prev;
|
|
$tausq = $this->tau * $this->tau;
|
|
$phisq = $this->phi * $this->phi;
|
|
$deltasq = $delta * $delta;
|
|
do {
|
|
$exp_xp = exp( $x_prev );
|
|
$d = $this->phi * $this->phi + $v + $exp_xp;
|
|
$deltadsq = $deltasq / ($d * $d);
|
|
$h1 = -( $x_prev - $a ) / ( $tausq ) - ( 0.5 * $exp_xp / $d ) + ( 0.5 * $exp_xp * $deltadsq );
|
|
$h2 = ( -1.0 / $tausq ) - ( ( 0.5 * $exp_xp ) * ( $phisq + $v ) / ( $d * $d ) ) + ( 0.5 * $deltasq * $exp_xp * ( $phisq + $v - $exp_xp ) / ( $d * $d * $d ) );
|
|
$tmp_x = $x;
|
|
$x = $x_prev - ( $h1 / $h2 );
|
|
$x_prev = $tmp_x;
|
|
} while (abs($x - $x_prev) > 0.1);
|
|
|
|
$sigma_p = exp( $x / 2 );
|
|
|
|
// Step 6
|
|
$phi_star = sqrt( $phisq + ( $sigma_p * $sigma_p ) );
|
|
|
|
// Step 7
|
|
$phi_p = 1.0 / ( sqrt( ( 1.0 / ( $phi_star * $phi_star ) ) + ( 1.0 / $v ) ) );
|
|
// New mu
|
|
$mu_p = $this->mu + $phi_p * $phi_p * $mu_p_sum;
|
|
|
|
return array( 'r' => ( 173.7178 * $mu_p ) + 1500, 'RD' => 173.7178 * $phi_p, 'mu' => $mu_p, 'phi' => $phi_p, 'sigma' => $sigma_p );
|
|
}
|
|
|
|
function g($phi) {
|
|
return 1.0 / ( sqrt( 1.0 + ( 3.0 * $phi * $phi) / ( $this->pi2 ) ) );
|
|
}
|
|
|
|
function E($mu, $mu_j, $phi_j) {
|
|
return 1.0 / ( 1.0 + exp( -$this->g($phi_j) * ( $mu - $mu_j ) ) );
|
|
}
|
|
}
|
|
|
|
class NTBBLadder {
|
|
var $serverid;
|
|
var $formatid;
|
|
var $rplen;
|
|
|
|
function __construct($serverid, $formatid) {
|
|
$this->formatid = preg_replace('/[^a-z0-9]+/', '', strtolower($formatid));
|
|
$this->serverid = $serverid;
|
|
$this->rplen = 3*24*60*60;
|
|
}
|
|
|
|
function getrp() {
|
|
$rpnum = intval(time() / $this->rplen);
|
|
return $rpnum * $this->rplen;
|
|
}
|
|
function nextrp($rp) {
|
|
$rpnum = intval($rp / $this->rplen);
|
|
return ($rpnum+1) * $this->rplen;
|
|
}
|
|
|
|
function getRating(&$user, $create=false) {
|
|
global $ladderdb;
|
|
if (!@$user['rating']) {
|
|
$res = $ladderdb->query("SELECT * FROM `{$ladderdb->prefix}ladder` WHERE `serverid` = '{$this->serverid}' AND `formatid` = '{$this->formatid}' AND `userid` = '".$ladderdb->escape($user['userid'])."' LIMIT 1");
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
$data = $ladderdb->fetch_assoc($res);
|
|
|
|
if (!$data) {
|
|
if (!$create) {
|
|
return false;
|
|
}
|
|
//echo "INSERT INTO `{$ladderdb->prefix}ladder` (`formatid`,`userid`,`username`) VALUES ('{$this->formatid}','".$ladderdb->escape($user['userid'])."','".$ladderdb->escape($user['username'])."')";
|
|
$rp = $this->getrp();
|
|
$ladderdb->query("INSERT INTO `{$ladderdb->prefix}ladder` (`formatid`,`serverid`,`userid`,`username`,`rptime`) VALUES ('{$this->formatid}','{$this->serverid}','".$ladderdb->escape($user['userid'])."','".$ladderdb->escape($user['username'])."',".$rp.")");
|
|
$user['rating'] = array(
|
|
'entryid' => $ladderdb->insert_id(),
|
|
'formatid' => $this->formatid,
|
|
'userid' => $user['userid'],
|
|
'username' => $user['username'],
|
|
'r' => 1500,
|
|
'rd' => 350,
|
|
'sigma' => 0.06,
|
|
'rpr' => 1500,
|
|
'rprd' => 350,
|
|
'rpsigma' => 0.06,
|
|
'gxe' => 0,
|
|
'rptime' => $rp,
|
|
'rpdata' => '',
|
|
'w' => 0,
|
|
'l' => 0,
|
|
't' => 0,
|
|
'gxe' => 50,
|
|
'acre' => 1000,
|
|
'lacre' => -4000,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
$user['rating'] = $data;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
function getAllRatings(&$user) {
|
|
global $ladderdb;
|
|
if (!@$user['ratings']) {
|
|
$res = $ladderdb->query("SELECT * FROM `{$ladderdb->prefix}ladder` WHERE `serverid` = '{$this->serverid}' AND `userid` = '".$ladderdb->escape($user['userid'])."'");
|
|
if (!$res) {
|
|
return false;
|
|
}
|
|
$user['ratings'] = array();
|
|
while ($row = $ladderdb->fetch_assoc($res)) {
|
|
unset($row['rpdata']);
|
|
$user['ratings'][] = $row;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getTop() {
|
|
global $ladderdb;
|
|
$needUpdate = true;
|
|
$top = array();
|
|
|
|
$i = 0;
|
|
while ($needUpdate) {
|
|
$i++;
|
|
if ($i > 2) break;
|
|
|
|
$needUpdate = false;
|
|
$top = array();
|
|
|
|
$res = $ladderdb->query("SELECT * FROM `{$ladderdb->prefix}ladder` WHERE `formatid` = '{$this->formatid}' AND `serverid` = '{$this->serverid}' ORDER BY `lacre` DESC LIMIT 100");
|
|
|
|
while ($row = $ladderdb->fetch_assoc($res)) {
|
|
$user = array(
|
|
'username' => $row['username'],
|
|
'userid' => $row['userid'],
|
|
'rating' => $row
|
|
);
|
|
|
|
if ($this->update($user)) {
|
|
$this->saveRating($user);
|
|
$needUpdate = true;
|
|
}
|
|
|
|
unset($row['rpdata']);
|
|
$top[] = $row;
|
|
}
|
|
}
|
|
|
|
return $top;
|
|
}
|
|
|
|
function saveRating($user) {
|
|
global $ladderdb;
|
|
if (!$user['rating']) return false;
|
|
|
|
return !!$ladderdb->query("UPDATE `{$ladderdb->prefix}ladder` SET `w`={$user['rating']['w']}, `l`={$user['rating']['l']}, `t`={$user['rating']['t']}, `r`={$user['rating']['r']}, `rd`={$user['rating']['rd']}, `sigma`={$user['rating']['sigma']}, `rptime`={$user['rating']['rptime']}, `rpr`={$user['rating']['rpr']}, `rprd`={$user['rating']['rprd']}, `rpsigma`={$user['rating']['rpsigma']}, `rpdata`='".$ladderdb->escape($user['rating']['rpdata'])."', `gxe`={$user['rating']['gxe']}, `acre`={$user['rating']['acre']}, `lacre`={$user['rating']['lacre']} WHERE `entryid` = {$user['rating']['entryid']} LIMIT 1");
|
|
}
|
|
|
|
function getAcre($rating) {
|
|
return $rating->rating - $rating->rd*500/355.13567109546;
|
|
}
|
|
|
|
function update(&$user, $newM = false, $force = false) {
|
|
$offset = 0;
|
|
|
|
$rp = $this->getrp();
|
|
if ($rp <= $user['rating']['rptime'] && !$newM && !$force) {
|
|
return false;
|
|
}
|
|
|
|
$rating = new Glicko2Player($user['rating']['r'], $user['rating']['rd'], $user['rating']['sigma']);
|
|
if ($user['rating']['rpdata']) {
|
|
$rpdata = explode('##',$user['rating']['rpdata']);
|
|
if (count($rpdata) > 1) $offset = floatval($rpdata[1]);
|
|
$rating->M = json_decode($rpdata[0], true);
|
|
//var_export($rating->M);
|
|
}
|
|
|
|
if ($rp > $user['rating']['rptime'] || count($rating->M) >= 14) {
|
|
$i=0;
|
|
while ($rp > $user['rating']['rptime'] || count($rating->M) >= 14) {
|
|
$i++;
|
|
if ($i > 1000) break;
|
|
|
|
$rating->Update();
|
|
if ($offset) {
|
|
$rating->r += $offset;
|
|
$rating->mu += $offset/173.7178;
|
|
$offset = 0;
|
|
}
|
|
$user['rating']['rptime'] = $this->nextrp($user['rating']['rptime']);
|
|
}
|
|
$user['rating']['r'] = $rating->rating;
|
|
$user['rating']['rd'] = $rating->rd;
|
|
$user['rating']['sigma'] = $rating->sigma;
|
|
$user['rating']['gxe'] = 0;
|
|
if ($user['rating']['rd'] < 100) {
|
|
$user['rating']['gxe'] = 1; //round(100 / (1 + pow(10,((1500 - $user['rating']['r']) * pi() / sqrt(3 * log(10)*log(10) * $user['rating']['rd']*$user['rating']['rd'] + 2500 * (64 * pi()*pi() + 147 * log(10)*log(10)))))));
|
|
}
|
|
}
|
|
|
|
if ($newM) {
|
|
// grab oldacre
|
|
{
|
|
$oldM = $rating->M;
|
|
$oldR = $rating->rating;
|
|
$oldRd = $rating->rd;
|
|
$oldSigma = $rating->sigma;
|
|
$rating->Update();
|
|
|
|
$user['rating']['oldacre'] = $this->getAcre($rating) + $offset;
|
|
$newOldRd = $rating->rd;
|
|
|
|
$rating = new Glicko2Player($oldR, $oldRd, $oldSigma);
|
|
$rating->M = $oldM;
|
|
}
|
|
|
|
$rating->M[] = $newM;
|
|
if ($newM['score'] > 0.99) {
|
|
$user['rating']['w']++;
|
|
} else if ($newM['score'] < 0.01) {
|
|
$user['rating']['l']++;
|
|
} else {
|
|
$user['rating']['t']++;
|
|
}
|
|
}
|
|
|
|
if (count($rating->M)) {
|
|
$user['rating']['rpdata'] = json_encode($rating->M);
|
|
} else {
|
|
$user['rating']['rpdata'] = '';
|
|
}
|
|
|
|
$rating->Update();
|
|
|
|
// grab oldrdacre
|
|
if ($newM) {
|
|
$newRd = $rating->rd;
|
|
$rating->rd = $newOldRd;
|
|
$user['rating']['oldrdacre'] = $this->getAcre($rating) + $offset;
|
|
$rating->rd = $newRd;
|
|
}
|
|
|
|
$oldrpr = $user['rating']['rpr'];
|
|
|
|
$user['rating']['rpr'] = $rating->rating;
|
|
$user['rating']['rprd'] = $rating->rd;
|
|
$user['rating']['rpsigma'] = $rating->sigma;
|
|
|
|
$user['rating']['gxe'] = round(100 / (1 + pow(10,((1500 - $rating->rating) * pi() / sqrt(3 * log(10)*log(10) * $rating->rd*$rating->rd + 2500 * (64 * pi()*pi() + 147 * log(10)*log(10)))))), 1);
|
|
$user['rating']['acre'] = $this->getAcre($rating) + $offset;
|
|
|
|
if ($newM) {
|
|
// compensate for Glicko2 bug: don't lose rating on win, don't gain rating on lose
|
|
if ($newM['score'] > .9 && $rating->rating < $oldrpr) {
|
|
$delta = $oldrpr - $rating->rating;
|
|
$offset += $delta;
|
|
$user['rating']['acre'] += $delta;
|
|
$user['rating']['rpr'] += $delta;
|
|
}
|
|
if ($newM['score'] < .1 && $rating->rating > $oldrpr) {
|
|
$delta = $oldrpr - $rating->rating;
|
|
$offset += $delta;
|
|
$user['rating']['acre'] += $delta;
|
|
$user['rating']['rpr'] += $delta;
|
|
}
|
|
|
|
// minimum +1 ACRE on win, minimum -1 ACRE on loss
|
|
if ($newM['score'] > .9 && $user['rating']['acre'] < $user['rating']['oldacre'] + 1) {
|
|
$user['rating']['acre'] = $user['rating']['oldacre'] + 1;
|
|
}
|
|
if ($newM['score'] < .1 && $user['rating']['acre'] > $user['rating']['oldacre'] - 1) {
|
|
$user['rating']['acre'] = $user['rating']['oldacre'] - 1;
|
|
}
|
|
}
|
|
if ($offset) {
|
|
$user['rating']['rpdata'] .= '##'.$offset;
|
|
}
|
|
|
|
$user['rating']['lacre'] = $user['rating']['acre'];
|
|
if ($user['rating']['rprd'] > 100) $user['rating']['lacre'] -= 5000;
|
|
|
|
return true;
|
|
}
|
|
function updateRating(&$p1, &$p2, $p1score, $p1M=false, $p2M=false) {
|
|
if (!@$p1['rating']) $this->getRating($p1, true);
|
|
if (!@$p2['rating']) $this->getRating($p2, true);
|
|
|
|
if (!$p1M) {
|
|
$p2rating = new Glicko2Player($p2['rating']['rpr'], $p2['rating']['rprd'], $p2['rating']['rpsigma']);
|
|
$p1M = $p2rating->MatchElement($p1score);
|
|
}
|
|
if (!$p2M) {
|
|
$p1rating = new Glicko2Player($p1['rating']['rpr'], $p1['rating']['rprd'], $p1['rating']['rpsigma']);
|
|
$p2M = $p1rating->MatchElement(1 - $p1score);
|
|
}
|
|
$p1M['score'] = $p1score;
|
|
$p2M['score'] = 1 - $p1score;
|
|
|
|
$this->update($p1, $p1M);
|
|
$this->update($p2, $p2M);
|
|
|
|
$this->saveRating($p1);
|
|
$this->saveRating($p2);
|
|
}
|
|
}
|