diff --git a/src/java/com/threerings/parlor/rating/server/Rating.java b/src/java/com/threerings/parlor/rating/server/Rating.java new file mode 100644 index 00000000..0845729b --- /dev/null +++ b/src/java/com/threerings/parlor/rating/server/Rating.java @@ -0,0 +1,165 @@ +// +// $Id$ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/vilya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.rating.server; + +import com.samskivert.util.StringUtil; + +import com.threerings.media.util.MathUtil; + +import com.threerings.parlor.rating.data.RatingCodes; + +/** + * Encapsulates the rating/experience tuple representing a player's rating for a game, and logic + * for calculating rating changes. + */ +public class Rating + implements RatingCodes +{ + /** The player's rating for our game. */ + public int rating; + + /** The number of times the player's played our game. */ + public int experience; + + public Rating () + { + this.rating = DEFAULT_RATING; + this.experience = 0; + } + + /** + * Computes a player's updated rating using a modified version of the + * FIDE/ELO system. The rating adjustment is computed for the player versus + * each opponent individually and these adjustments are summed and scaled + * by one over the number of opponents to create the final rating + * adjustment, which is then added to the player's previous rating and + * bounded to the rating range. Note: provisional players (those + * with experience of less than 20) will be treated as having, at most, the + * default rating when used as an opponent in calculatons for a + * non-provisional player. + * + * @param ratings the pre-match ratings of each of the opponents. + * @param pidx the index of the player whose rating is to be calculated. + * @param W the win value for the player whose rating is to be calculated, + * (1.0 means the player won, 0.5 means they drew, 0 means they lost). + * + * @return the player's updated rating or -1 if none of the opponents could + * be applicably rated against this player due to provisional/ + * non-provisional mismatch or lack of participation. + */ + public static int computeRating (Rating[] ratings, int pidx, float W) + { + float dR = 0; // the total delta rating + int opponents = 0; + + for (int ii = 0; ii < ratings.length; ii++) { + Rating orating = ratings[ii]; + // we don't care how we did against ourselves, or against guests... + if (pidx == ii || orating == null) { + continue; + } + + // if we are non-provisional, and the opponent is provisional, we + // max the opponent out at the default rating to avoid potentially + // inflating a real rating with one that has a lot of uncertainty + int opprat = ratings[ii].rating; + if (!ratings[pidx].isProvisional() && ratings[ii].isProvisional()) { + opprat = Math.min(opprat, DEFAULT_RATING); + } + dR += computeAdjustment(W, opprat, ratings[pidx]); + opponents++; + } + + // if we have no valid opponents, we cannot compute a rating + if (opponents == 0) { + return -1; + } + + int nrat = Math.round(ratings[pidx].rating + dR/opponents); + return MathUtil.bound(MINIMUM_RATING, nrat, MAXIMUM_RATING); + } + + /** + * Computes a ratings adjustment for the given player, using a modified + * version of the FIDE Chess rating system as: + * + *
+ * adjustment = K(W - We) + * + * where: + * + * K = if (experience < 20) then 64 + * else if (rating < 2100 and experience >= 20) then 32 + * else if (rating >= 2100 and rating < 2400 and experience >= 20) + * then 24 + * else 16 + * W = score for the game just completed, as 1.0, 0.5, and 0.0 for a + * win, draw, or loss, respectively. + * dR = opponent's rating minus player's rating. + * We = expected score (win expectancy) as determined by: + * + * We = 1 / (10^(dR/400) + 1) + *+ * + * @param W the win value the game in question (1.0 means the player won, + * 0.5 means they drew, 0 means they lost). + * @param opprat the opponent's current rating. + * @param rating the player's current rating. + * + * @return the adjustment to the player's rating. + */ + public static float computeAdjustment (float W, int opprat, Rating rating) + { + // calculate We, the win expectancy + float dR = opprat - rating.rating; + float We = 1.0f / (float)(Math.pow(10.0f, (dR / 400.0f)) + 1); + + // calculate K, the score multiplier constant + int K; + if (rating.experience < 20) { + K = 64; + } else if (rating.rating < 2100) { + K = 32; // experience >= 20 + } else if (rating.rating < 2400) { + K = 24; // experience >= 20 && rating >= 2100 + } else { + K = 16; // experience >= 20 && rating >= 2400 + } + + // compute and return the ratings adjustment + return K * (W - We); + } + + /** + * Returns true if this rating is provisional (experience < 20). + */ + public boolean isProvisional () + { + return (experience < 20); + } + + @Override + public String toString () + { + return StringUtil.fieldsToString(this); + } +} diff --git a/src/java/com/threerings/parlor/rating/server/RatingDelegate.java b/src/java/com/threerings/parlor/rating/server/RatingDelegate.java index 4a166984..7bf1f8f7 100644 --- a/src/java/com/threerings/parlor/rating/server/RatingDelegate.java +++ b/src/java/com/threerings/parlor/rating/server/RatingDelegate.java @@ -83,7 +83,7 @@ public abstract class RatingDelegate extends GameManagerDelegate // if the game is in play and this is a player, load their ratings BodyObject occupant = (BodyObject)_omgr.getObject(bodyOid); if (shouldRateGame() && _gobj.isInPlay() && isPlayer(occupant)) { - Rating rating = maybeCreateRating(occupant); + PlayerRating rating = maybeCreateRating(occupant); if (rating != null) { loadRatings(Collections.singleton(rating)); } @@ -96,7 +96,7 @@ public abstract class RatingDelegate extends GameManagerDelegate super.bodyLeft(bodyOid); // flush this player's rating if its modified - for (Rating rating : _ratings.values()) { + for (PlayerRating rating : _ratings.values()) { if (rating.playerOid == bodyOid && rating.modified) { saveRatings(Collections.singleton(rating.cloneForSave())); break; @@ -122,14 +122,14 @@ public abstract class RatingDelegate extends GameManagerDelegate // load up the ratings for all players in this game; also make a note of the persistent // player id of each player position for seated table games - List
The rating adjustment is computed for the player versus each opponent individually and
- * these adjustments are summed and scaled by one over the number of opponents to create the
- * final rating adjustment, which is then added to the player's previous rating and bounded to
- * the rating range. Note: provisional players (those with experience of less than 20)
- * will be treated as having, at most, the default rating when used as an opponent in
- * calculations for a non-provisional player.
- *
- * @return the player's updated rating or -1 if none of the opponents could be applicably rated
- * against this player.
- */
- protected int computeRating (int pidx)
- {
- float totDeltaR = 0; // the total delta rating
- int opponents = 0;
-
- Rating prating = _ratings.get(_playerIds[pidx]);
- if (prating == null) {
- // be robust
- log.warning("Computing ratings for unknown player [where=" + where() +
- ", pidx=" + pidx + "].");
- return -1;
- }
-
- for (int ii = 0; ii < _playerIds.length; ii++) {
- Rating orating = _ratings.get(_playerIds[ii]);
- // we don't care how we did against ourselves, or against guests...
- if (pidx == ii || orating == null) {
- continue;
- }
-
- // if we are non-provisional, and the opponent is provisional, we max the opponent out
- // at the default rating to avoid potentially inflating a real rating with one that has
- // a lot of uncertainty
- int opprat = orating.rating;
- if (prating.experience >= 20 && orating.experience < 20) {
- opprat = Math.min(opprat, DEFAULT_RATING);
- }
-
- // calculate K, the score multiplier constant
- int K;
- if (prating.experience < 20) {
- K = 64;
- } else if (prating.rating < 2100) {
- K = 32; // experience >= 20
- } else if (prating.rating < 2400) {
- K = 24; // experience >= 20 && rating >= 2100
- } else {
- K = 16; // experience >= 20 && rating >= 2400
- }
-
- // calculate W, the win value representing the actual result of the game
- float W = _gobj.isDraw() ? 0.5f : _gobj.isWinner(pidx) ? 1f : 0f;
- // calculate We, the expected win value given the players' respective ratings
- float dR = opprat - prating.rating;
- float We = 1.0f / (float)(Math.pow(10.0f, (dR / 400.0f)) + 1);
-
- // update the total rating adjustment with the value for this opponent
- totDeltaR += K * (W - We);
- opponents++;
- }
-
- // if we have no valid opponents, we cannot compute a rating;
- if (opponents == 0) {
- return -1;
- }
-
- // return the updated and clamped rating
- int nrat = Math.round(prating.rating + totDeltaR/opponents);
- return MathUtil.bound(MINIMUM_RATING, nrat, MAXIMUM_RATING);
- }
-
/**
* Returns the game id to use when reading and writing ratings.
*/
@@ -372,12 +304,12 @@ public abstract class RatingDelegate extends GameManagerDelegate
*
* This method is called on the dobj thread.
*/
- protected abstract void updateRatingInMemory (int gameId, Rating rating);
+ protected abstract void updateRatingInMemory (int gameId, PlayerRating rating);
/**
* Encapsulates the rating/experience tuple representing a player's rating for a game.
*/
- protected static class Rating
+ protected static class PlayerRating extends Rating
implements Cloneable
{
/** The oid of the rated player. */
@@ -389,19 +321,13 @@ public abstract class RatingDelegate extends GameManagerDelegate
/** The id of the rated player. */
public int playerId;
- /** The player's rating for our game. */
- public int rating;
-
- /** The number of times the player's played our game. */
- public int experience;
-
/** Whether or not this rating needs to be written back to the database. */
public boolean modified;
/**
- * Sets up a new {@link Rating} object with default values.
+ * Sets up a new {@link PlayerRating} object with default values.
*/
- public Rating (BodyObject player, int playerId)
+ public PlayerRating (BodyObject player, int playerId)
{
this.playerOid = player.getOid();
this.playerName = player.getVisibleName();
@@ -413,22 +339,16 @@ public abstract class RatingDelegate extends GameManagerDelegate
/**
* Duplicates this rating and clears its modified status.
*/
- public Rating cloneForSave ()
+ public PlayerRating cloneForSave ()
{
try {
- Rating rating = (Rating)this.clone();
+ PlayerRating rating = (PlayerRating)this.clone();
modified = false;
return rating;
} catch (CloneNotSupportedException cnse) {
throw new AssertionError(cnse);
}
}
-
- @Override
- public String toString ()
- {
- return StringUtil.fieldsToString(this);
- }
}
/** An appropriately casted reference to our GameManager. */
@@ -441,7 +361,7 @@ public abstract class RatingDelegate extends GameManagerDelegate
protected int[] _playerIds;
/** The ratings for each player as they were at the beginning of the game. */
- protected IntMap