From a3d1d0b7cbb67014ee096c320aaf397f5bf48815 Mon Sep 17 00:00:00 2001 From: Par Winzell Date: Thu, 5 Jul 2007 15:06:49 +0000 Subject: [PATCH] First draft of rating support in Vilya. The algorithm implementation was stolen from Bang and mutated. This implementation assumes that the subclasser can always map Name to an integer user identifier. If this is not the case we'll need to do something tricky with subclassed generic RatingRecords or whatever. git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@342 c613c5cb-e716-0410-b11b-feb51c14d237 --- .../rating/server/RatingManagerDelegate.java | 334 ++++++++++++++++++ .../rating/server/persist/RatingRecord.java | 113 ++++++ .../server/persist/RatingRepository.java | 105 ++++++ 3 files changed, 552 insertions(+) create mode 100644 src/java/com/threerings/parlor/rating/server/RatingManagerDelegate.java create mode 100644 src/java/com/threerings/parlor/rating/server/persist/RatingRecord.java create mode 100644 src/java/com/threerings/parlor/rating/server/persist/RatingRepository.java diff --git a/src/java/com/threerings/parlor/rating/server/RatingManagerDelegate.java b/src/java/com/threerings/parlor/rating/server/RatingManagerDelegate.java new file mode 100644 index 00000000..b192b21c --- /dev/null +++ b/src/java/com/threerings/parlor/rating/server/RatingManagerDelegate.java @@ -0,0 +1,334 @@ +// +// $Id: PuzzleManagerDelegate.java 209 2007-02-24 00:37:33Z mdb $ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2007 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 static com.threerings.parlor.Log.log; + +import java.util.List; +import java.util.logging.Level; + +import com.samskivert.io.PersistenceException; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.IntMap; +import com.samskivert.util.Invoker; + +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.CrowdServer; + +import com.threerings.media.util.MathUtil; +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.server.GameManager; +import com.threerings.parlor.game.server.GameManagerDelegate; + +import com.threerings.parlor.rating.server.persist.RatingRecord; +import com.threerings.parlor.rating.server.persist.RatingRepository; +import com.threerings.util.Name; + +/** + * Rates players after each game and handles persisting the results. + */ +public abstract class RatingManagerDelegate + extends GameManagerDelegate +{ + /** + * The minimum rating value. + */ + public static final int MINIMUM_RATING = 1000; + + /** + * The default rating value. + */ + public static final int DEFAULT_RATING = 1200; + + /** + * The maximum rating value. + */ + public static final int MAXIMUM_RATING = 3000; + + /** + * Constructs a rating manager delegate. + */ + public RatingManagerDelegate (GameManager gmgr) + { + super(gmgr); + _gmgr = gmgr; + + _repo = getRatingRepository(); + } + + @Override + public void didStartup (PlaceObject plobj) + { + super.didStartup(plobj); + + _gobj = (GameObject) plobj; + } + + @Override + public void gameWillStart () + { + super.gameWillStart(); + + // note the time at which we started + _startStamp = (int) (System.currentTimeMillis() / 1000); + + // read ratings from persistent store + CrowdServer.invoker.postUnit(new Invoker.Unit() { + public boolean invoke () { + int gameId = _gmgr.getGameConfig().getGameId(); + + // enumerate our players + Name[] players = new Name[_gmgr.getPlayerCount()]; + for (int ii = 0; ii < players.length; ii ++) { + players[ii] = _gmgr.getPlayerName(ii); + } + + try { + // fetch their previous ratings for this game + List records = _repo.getRatings(gameId, players); + + // and make it easy to look them up by their id + IntMap map = new HashIntMap(); + for (RatingRecord record : records) { + map.put(record.playerId, record); + } + + // now build the array we keep around until the end of the game + _ratings = new Rating[_gmgr.getPlayerCount()]; + for (int ii = 0; ii < _ratings.length; ii ++) { + RatingRecord record = map.get(_repo.mapNameToId(_gmgr.getPlayerName(ii))); + // if the player had no previous record, initiate them at default values + _ratings[ii] = record != null ? + new Rating(record.rating, record.experience) : new Rating(); + } + + return true; + + } catch (PersistenceException pe) { + log.log(Level.WARNING, + "Failed to load ratings [where=" + where() + ", id=" + gameId + "].", pe); + return false; + } + }; + + public void handleResult () { + // nothing to do here? + } + }); + } + + @Override + public void gameDidEnd () + { + super.gameDidEnd(); + + // note the duration of the game (in seconds) + int gameSecs = (int) (System.currentTimeMillis()/1000 - _startStamp); + + // update ratings if the game ran long enough, and the persistence code finished reading + if (_ratings == null || gameSecs < minimumRatedDuration()) { + return; + } + + // compute the updated ratings + int[] nratings = new int[_ratings.length]; + for (int ii = 0; ii < _ratings.length; ii ++) { + nratings[ii] = computeRating(ii); + } + + // and store them + boolean modified = false; + for (int ii = 0; ii < _ratings.length; ii++) { + // skip this rating if we weren't able to compute a value + if (nratings[ii] < 0) { + continue; + } + _ratings[ii].rating = nratings[ii]; + _ratings[ii].experience ++; + modified = true; + } + + if (!modified) { + return; + } + + // finally persist the result if necessary + final int gameId = _gmgr.getGameConfig().getGameId(); + CrowdServer.invoker.postUnit(new Invoker.Unit() { + public boolean invoke () { + try { + for (int ii = 0; ii < _ratings.length; ii ++) { + // for each player, update or create the rating record + // TODO: reorganize things so this can be a single db request? + _repo.setRating( + gameId, _gmgr.getPlayerName(ii), + _ratings[ii].rating, _ratings[ii].experience); + } + return true; + + } catch (PersistenceException pe) { + log.log(Level.WARNING, + "Failed to load ratings [where=" + where() + ", id=" + gameId + "].", pe); + return false; + } + }; + + public void handleResult () { + // let subclasses store away the new ratings if they so desire + for (int ii = 0; ii < _ratings.length; ii ++) { + updateRatingInMemory(gameId, _gmgr.getPlayerName(ii), _ratings[ii]); + } + } + }); + } + + /** + * Return the minimum time (in seconds) a game must've lasted for it to count towards rating. + */ + protected abstract int minimumRatedDuration (); + + /** + * Return a reference to the {@link RatingRepository} instance we should use to persist. + */ + protected abstract RatingRepository getRatingRepository (); + + /** + * Optionally store update ratings in memory e.g. in the user object. + * + * This method is called on the dobj thread. + */ + protected abstract void updateRatingInMemory (int gameId, Name playerName, Rating rating); + + /** + * 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. + * + * This method may be overriden to change the rating algorithm. + * + * @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; + + for (int ii = 0; ii < _ratings.length; ii++) { + // we don't care how we did against ourselves... + if (pidx == ii) { + 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].experience >= 20 && _ratings[ii].experience < 20) { + opprat = Math.min(opprat, DEFAULT_RATING); + } + + // calculate K, the score multiplier constant + int K; + if (_ratings[pidx].experience < 20) { + K = 64; + } else if (_ratings[pidx].rating < 2100) { + K = 32; // experience >= 20 + } else if (_ratings[pidx].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 - _ratings[pidx].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(_ratings[pidx].rating + totDeltaR/opponents); + return MathUtil.bound(MINIMUM_RATING, nrat, MAXIMUM_RATING); + } + + /** + * Simply encapsulates the rating/experience tuple representing a player's rating for a game. + */ + protected static class Rating + { + /** A player's rating for a game. */ + public int rating; + + /** The number of times a player's played a game. */ + public int experience; + + /** + * Sets up a new {@link Rating} object with default values. + */ + public Rating () + { + super(); + this.rating = DEFAULT_RATING; + this.experience = 0; + } + + /** + * Sets up a new {@link Rating} object with the given values. + */ + public Rating (int rating, int experience) + { + super(); + this.rating = rating; + this.experience = experience; + } + } + + /** An appropriately casted reference to our GameManager. */ + protected GameManager _gmgr; + + /** An appropriately casted reference to our GameObject. */ + protected GameObject _gobj; + + /** The RatingRepository that holds our data. */ + protected RatingRepository _repo; + + /** The ratings for each player as they were at the beginning of the game. */ + protected Rating[] _ratings; + + /** A timestamp set at the beginning of the game, used to calculate its duration. */ + protected int _startStamp; +} diff --git a/src/java/com/threerings/parlor/rating/server/persist/RatingRecord.java b/src/java/com/threerings/parlor/rating/server/persist/RatingRecord.java new file mode 100644 index 00000000..47225841 --- /dev/null +++ b/src/java/com/threerings/parlor/rating/server/persist/RatingRecord.java @@ -0,0 +1,113 @@ +// +// $Id: PuzzleManagerDelegate.java 209 2007-02-24 00:37:33Z mdb $ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2007 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.persist; + +import com.samskivert.jdbc.depot.Key; +import com.samskivert.jdbc.depot.PersistentRecord; +import com.samskivert.jdbc.depot.annotation.Entity; +import com.samskivert.jdbc.depot.annotation.Id; +import com.samskivert.jdbc.depot.expression.ColumnExp; + +@Entity +public class RatingRecord extends PersistentRecord +{ + // AUTO-GENERATED: FIELDS START + /** The column identifier for the {@link #gameId} field. */ + public static final String GAME_ID = "gameId"; + + /** The qualified column identifier for the {@link #gameId} field. */ + public static final ColumnExp GAME_ID_C = + new ColumnExp(RatingRecord.class, GAME_ID); + + /** The column identifier for the {@link #playerId} field. */ + public static final String PLAYER_ID = "playerId"; + + /** The qualified column identifier for the {@link #playerId} field. */ + public static final ColumnExp PLAYER_ID_C = + new ColumnExp(RatingRecord.class, PLAYER_ID); + + /** The column identifier for the {@link #rating} field. */ + public static final String RATING = "rating"; + + /** The qualified column identifier for the {@link #rating} field. */ + public static final ColumnExp RATING_C = + new ColumnExp(RatingRecord.class, RATING); + + /** The column identifier for the {@link #experience} field. */ + public static final String EXPERIENCE = "experience"; + + /** The qualified column identifier for the {@link #experience} field. */ + public static final ColumnExp EXPERIENCE_C = + new ColumnExp(RatingRecord.class, EXPERIENCE); + // AUTO-GENERATED: FIELDS END + + public static final int SCHEMA_VERSION = 1; + + /** The identifier of the game we're rating for. */ + @Id + public int gameId; + + /** The identifier of the player we're rating. */ + @Id + public int playerId; + + /** The player's current rating. */ + public int rating; + + /** The number of times the player has played this game. */ + public int experience; + + /** + * An empty constructor for unmarshalling. + */ + public RatingRecord () + { + super(); + } + + /** + * A constructor that populates all our fields. + */ + public RatingRecord (int gameId, int playerId, int rating, int experience) + { + super(); + + this.gameId = gameId; + this.playerId = playerId; + this.rating = rating; + this.experience = experience; + } + + // AUTO-GENERATED: METHODS START + /** + * Create and return a primary {@link Key} to identify a {@link #RatingRecord} + * with the supplied key values. + */ + public static Key getKey (int gameId, int playerId) + { + return new Key( + RatingRecord.class, + new String[] { GAME_ID, PLAYER_ID }, + new Comparable[] { gameId, playerId }); + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/parlor/rating/server/persist/RatingRepository.java b/src/java/com/threerings/parlor/rating/server/persist/RatingRepository.java new file mode 100644 index 00000000..88d983f9 --- /dev/null +++ b/src/java/com/threerings/parlor/rating/server/persist/RatingRepository.java @@ -0,0 +1,105 @@ +// +// $Id: PuzzleManagerDelegate.java 209 2007-02-24 00:37:33Z mdb $ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2007 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.persist; + +import java.util.List; + +import com.samskivert.io.PersistenceException; + +import com.samskivert.jdbc.ConnectionProvider; +import com.samskivert.jdbc.depot.DepotRepository; +import com.samskivert.jdbc.depot.clause.Where; +import com.samskivert.jdbc.depot.operator.Logic.*; +import com.samskivert.jdbc.depot.operator.Conditionals.*; + +import com.threerings.util.Name; + +/** + * Handles the persistent storage of per-user per-game ratings. + */ +public abstract class RatingRepository extends DepotRepository +{ + /** + * Users are indexed by integer. It is the responsibility of the subclasser to implement + * an injective mapping of {@link Name} instance to integer here. + */ + public abstract int mapNameToId (Name player); + + /** + * Loads the rating for the given player for the given game and returns it as a + * {@link RatingRecord} object, or null if the player has no previous rating for the game. + */ + public RatingRecord getRating (int gameId, Name player) + throws PersistenceException + { + return load( + RatingRecord.class, + RatingRecord.GAME_ID, gameId, + RatingRecord.PLAYER_ID, mapNameToId(player)); + } + + /** + * Fetch the ratings registered for any of the given players for the given game and return + * them as a list of {@link RatingRecord} objects. The size of this list is no less than zero + * and no greater than the number of given players. + */ + public List getRatings (int gameId, Name... players) + throws PersistenceException + { + Comparable[] idArr = new Comparable[players.length]; + for (int ii = 0; ii < idArr.length; ii ++) { + idArr[ii] = mapNameToId(players[ii]); + } + return findAll( + RatingRecord.class, + new Where(new And( + new Equals(RatingRecord.GAME_ID, gameId), + new In(RatingRecord.PLAYER_ID, idArr)))); + } + + /** + * Fetch and return all the registered {@link RatingRecord} rows for the given player. + */ + public List getRatings (Name player) + throws PersistenceException + { + return findAll(RatingRecord.class, new Where(RatingRecord.PLAYER_ID, mapNameToId(player))); + } + + /** + * Set the rating and experience for a given player and game. This method will either update + * or create a row. + */ + public void setRating (int gameId, Name player, int rating, int experience) + throws PersistenceException + { + store(new RatingRecord(gameId, mapNameToId(player), rating, experience)); + } + + /** + * Initialize the {@link RatingRepository}. + */ + protected RatingRepository(ConnectionProvider conprov) + { + super(conprov); + } +}