/*
* JOGRE (Java Online Gaming Real-time Engine) - API
* Copyright (C) 2004 Bob Marks (marksie531@yahoo.com)
* http://jogre.sourceforge.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package net.olemartin.tools.rating;
import net.olemartin.domain.PlayerResult;
import java.util.HashMap;
import java.util.StringTokenizer;
/**
* JOGRE's implementation of the ELO rating system. The following is an
* example of how to use the Elo Rating System.
* <code>
* EloRatingSystem elo = new EloRatingSystem();
* int userRating = 1600;
* int opponentRating = 1650;
* int newUserRating = elo.getNewRating(userRating, opponentRating, WIN);
* int newOpponentRating = elo.getNewRating(opponentRating, userRating, LOSS);
* </code>
*
* @author Garrett Lehman (gman)
*/
public class EloRatingSystem {
public KFactor[] kFactors = {};
// List of singletons are stored in this HashMap
private static HashMap<String, EloRatingSystem> ratingSystems = null;
private EloRatingSystem() {
// Read k factor in from server properties
String kFactorStr = "0-4000=64";
// Split each of the kFactor ranges up (kfactor1,factor2, etc)
StringTokenizer st1 = new StringTokenizer(kFactorStr, ",");
kFactors = new KFactor[st1.countTokens()];
int index = 0;
while (st1.hasMoreTokens()) {
String kfr = st1.nextToken();
// Split the range from the value (range=value)
StringTokenizer st2 = new StringTokenizer(kfr, "=");
String range = st2.nextToken();
// Retrieve value
double value = Double.parseDouble(st2.nextToken());
// Retrieve start end index from the range
st2 = new StringTokenizer(range, "-");
int startIndex = Integer.parseInt(st2.nextToken());
int endIndex = Integer.parseInt(st2.nextToken());
// Add kFactor to range
kFactors[index++] = new KFactor(startIndex, endIndex, value);
}
}
/**
* Return instance of an ELO rating system.
*
* @param game Game to key of.
* @return ELO rating system for specified game.
*/
public static synchronized EloRatingSystem getInstance(String game) {
if (ratingSystems == null) {
ratingSystems = new HashMap<>();
}
// Retrieve rating system
EloRatingSystem ratingSystem = ratingSystems.get(game);
// If null then create new one and add to hash keying off the game
if (ratingSystem == null) {
ratingSystem = new EloRatingSystem();
ratingSystems.put(game, ratingSystem);
return ratingSystem;
} else {
return ratingSystem;
}
}
public int getNewRating(int rating, int opponentRating, PlayerResult result) {
switch (result) {
case WIN:
return getNewRating(rating, opponentRating, 1.0);
case LOSS:
return getNewRating(rating, opponentRating, 0.0);
case DRAW:
return getNewRating(rating, opponentRating, 0.5);
}
return -1; // no score this time.
}
/**
* Get new rating.
*
* @param rating Rating of either the current player or the average of the
* current team.
* @param opponentRating Rating of either the opponent player or the average of the
* opponent team or teams.
* @param score Score: 0=Loss 0.5=Draw 1.0=Win
* @return the new rating
*/
private int getNewRating(int rating, int opponentRating, double score) {
double kFactor = getKFactor(rating);
double expectedScore = getExpectedScore(rating, opponentRating);
return calculateNewRating(rating, score, expectedScore, kFactor);
}
/**
* Calculate the new rating based on the ELO standard formula.
* newRating = oldRating + constant * (score - expectedScore)
*
* @param oldRating Old Rating
* @param score Score
* @param expectedScore Expected Score
* @return the new rating of the player
*/
private int calculateNewRating(int oldRating, double score, double expectedScore, double kFactor) {
return oldRating + (int) (kFactor * (score - expectedScore));
}
/**
* This is the standard chess constant. This constant can differ
* based on different games. The higher the constant the faster
* the rating will grow. That is why for this standard chess method,
* the constant is higher for weaker players and lower for stronger
* players.
*
* @param rating Rating
* @return Constant
*/
private double getKFactor(int rating) {
// Return the correct k factor.
for (KFactor kFactor : kFactors) {
if (rating >= kFactor.getStartIndex() && rating <= kFactor.getEndIndex()) {
return kFactor.value;
}
}
return 32;
}
/**
* Get expected score based on two players. If more than two players
* are competing, then opponentRating will be the average of all other
* opponent's ratings. If there is two teams against each other, rating
* and opponentRating will be the average of those players.
*
* @param rating Rating
* @param opponentRating Opponent(s) rating
* @return the expected score
*/
private double getExpectedScore(int rating, int opponentRating) {
return 1.0 / (1.0 + Math.pow(10.0, ((double) (opponentRating - rating) / 400.0)));
}
/**
* Small inner class data structure to describe a KFactor range.
*/
public class KFactor {
private int startIndex, endIndex;
private double value;
public KFactor(int startIndex, int endIndex, double value) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.value = value;
}
public int getStartIndex() {
return startIndex;
}
public int getEndIndex() {
return endIndex;
}
public double getValue() {
return value;
}
public String toString() {
return "kfactor: " + startIndex + " " + endIndex + " " + value;
}
}
}