mirror of
https://github.com/correl/mage.git
synced 2024-12-24 03:00:14 +00:00
Add user rating using Glicko rating system (#1498)
This commit is contained in:
parent
11158d5fa4
commit
972d59aa37
10 changed files with 701 additions and 9 deletions
|
@ -48,6 +48,8 @@ import mage.players.net.UserData;
|
|||
import mage.server.draft.DraftSession;
|
||||
import mage.server.game.GameManager;
|
||||
import mage.server.game.GameSessionPlayer;
|
||||
import mage.server.rating.GlickoRating;
|
||||
import mage.server.rating.GlickoRatingSystem;
|
||||
import mage.server.record.UserStats;
|
||||
import mage.server.record.UserStatsRepository;
|
||||
import mage.server.tournament.TournamentController;
|
||||
|
@ -108,7 +110,6 @@ public class User {
|
|||
this.watchedGames = new ArrayList<>();
|
||||
this.tablesToDelete = new ArrayList<>();
|
||||
this.sessionId = "";
|
||||
this.userStats = null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
|
@ -535,15 +536,23 @@ public class User {
|
|||
}
|
||||
userStats = UserStatsRepository.instance.getUser(this.userName);
|
||||
if (userStats != null) {
|
||||
userData.setMatchHistory(userStatsToMatchHistory(userStats.getProto()));
|
||||
userData.setMatchQuitRatio(userStatsToMatchQuitRatio(userStats.getProto()));
|
||||
userData.setTourneyHistory(userStatsToTourneyHistory(userStats.getProto()));
|
||||
userData.setTourneyQuitRatio(userStatsToTourneyQuitRatio(userStats.getProto()));
|
||||
ResultProtos.UserStatsProto userStatsProto = userStats.getProto();
|
||||
|
||||
userData.setMatchHistory(userStatsToMatchHistory(userStatsProto));
|
||||
userData.setMatchQuitRatio(userStatsToMatchQuitRatio(userStatsProto));
|
||||
userData.setTourneyHistory(userStatsToTourneyHistory(userStatsProto));
|
||||
userData.setTourneyQuitRatio(userStatsToTourneyQuitRatio(userStatsProto));
|
||||
userData.setGeneralRating(userStatsToGeneralRating(userStatsProto));
|
||||
userData.setConstructedRating(userStatsToConstructedRating(userStatsProto));
|
||||
userData.setLimitedRating(userStatsToLimitedRating(userStatsProto));
|
||||
} else {
|
||||
userData.setMatchHistory("0");
|
||||
userData.setMatchQuitRatio(0);
|
||||
userData.setTourneyHistory("0");
|
||||
userData.setTourneyQuitRatio(0);
|
||||
userData.setGeneralRating(GlickoRatingSystem.getDefaultDisplayedRating());
|
||||
userData.setConstructedRating(GlickoRatingSystem.getDefaultDisplayedRating());
|
||||
userData.setLimitedRating(GlickoRatingSystem.getDefaultDisplayedRating());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -644,6 +653,48 @@ public class User {
|
|||
return 100 * quits / tourneys;
|
||||
}
|
||||
|
||||
private static int userStatsToGeneralRating(ResultProtos.UserStatsProto proto) {
|
||||
GlickoRating glickoRating;
|
||||
if (proto.hasGeneralGlickoRating()) {
|
||||
ResultProtos.GlickoRatingProto glickoRatingProto = proto.getGeneralGlickoRating();
|
||||
glickoRating = new GlickoRating(
|
||||
glickoRatingProto.getRating(),
|
||||
glickoRatingProto.getRatingDeviation(),
|
||||
glickoRatingProto.getLastGameTimeMs());
|
||||
} else {
|
||||
glickoRating = GlickoRatingSystem.getInitialRating();
|
||||
}
|
||||
return GlickoRatingSystem.getDisplayedRating(glickoRating);
|
||||
}
|
||||
|
||||
private static int userStatsToConstructedRating(ResultProtos.UserStatsProto proto) {
|
||||
GlickoRating glickoRating;
|
||||
if (proto.hasConstructedGlickoRating()) {
|
||||
ResultProtos.GlickoRatingProto glickoRatingProto = proto.getConstructedGlickoRating();
|
||||
glickoRating = new GlickoRating(
|
||||
glickoRatingProto.getRating(),
|
||||
glickoRatingProto.getRatingDeviation(),
|
||||
glickoRatingProto.getLastGameTimeMs());
|
||||
} else {
|
||||
glickoRating = GlickoRatingSystem.getInitialRating();
|
||||
}
|
||||
return GlickoRatingSystem.getDisplayedRating(glickoRating);
|
||||
}
|
||||
|
||||
private static int userStatsToLimitedRating(ResultProtos.UserStatsProto proto) {
|
||||
GlickoRating glickoRating;
|
||||
if (proto.hasLimitedGlickoRating()) {
|
||||
ResultProtos.GlickoRatingProto glickoRatingProto = proto.getLimitedGlickoRating();
|
||||
glickoRating = new GlickoRating(
|
||||
glickoRatingProto.getRating(),
|
||||
glickoRatingProto.getRatingDeviation(),
|
||||
glickoRatingProto.getLastGameTimeMs());
|
||||
} else {
|
||||
glickoRating = GlickoRatingSystem.getInitialRating();
|
||||
}
|
||||
return GlickoRatingSystem.getDisplayedRating(glickoRating);
|
||||
}
|
||||
|
||||
private static void joinStrings(StringBuilder joined, List<String> strings, String separator) {
|
||||
for (int i = 0; i < strings.size(); ++i) {
|
||||
if (i > 0) {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2011 BetaSteward_at_googlemail.com. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are
|
||||
* permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
* conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
* of conditions and the following disclaimer in the documentation and/or other materials
|
||||
* provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those of the
|
||||
* authors and should not be interpreted as representing official policies, either expressed
|
||||
* or implied, of BetaSteward_at_googlemail.com.
|
||||
*/
|
||||
package mage.server.rating;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Quercitron
|
||||
*/
|
||||
public class GlickoRating {
|
||||
|
||||
private double rating;
|
||||
|
||||
private double ratingDeviation;
|
||||
|
||||
private long lastGameTimeMs;
|
||||
|
||||
public GlickoRating(double rating, double ratingDeviation) {
|
||||
this(rating, ratingDeviation, 0);
|
||||
}
|
||||
|
||||
public GlickoRating(double rating, double ratingDeviation, long lastGameTimeMs) {
|
||||
this.rating = rating;
|
||||
this.ratingDeviation = ratingDeviation;
|
||||
this.lastGameTimeMs = lastGameTimeMs;
|
||||
}
|
||||
|
||||
public double getRating() {
|
||||
return rating;
|
||||
}
|
||||
|
||||
public void setRating(double rating) {
|
||||
this.rating = rating;
|
||||
}
|
||||
|
||||
public double getRatingDeviation() {
|
||||
return ratingDeviation;
|
||||
}
|
||||
|
||||
public void setRatingDeviation(double ratingDeviation) {
|
||||
this.ratingDeviation = ratingDeviation;
|
||||
}
|
||||
|
||||
public long getLastGameTimeMs() {
|
||||
return lastGameTimeMs;
|
||||
}
|
||||
|
||||
public void setLastGameTimeMs(long lastGameTimeMs) {
|
||||
this.lastGameTimeMs = lastGameTimeMs;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright 2011 BetaSteward_at_googlemail.com. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are
|
||||
* permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
* conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
* of conditions and the following disclaimer in the documentation and/or other materials
|
||||
* provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those of the
|
||||
* authors and should not be interpreted as representing official policies, either expressed
|
||||
* or implied, of BetaSteward_at_googlemail.com.
|
||||
*/
|
||||
package mage.server.rating;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Quercitron
|
||||
*/
|
||||
public class GlickoRatingSystem {
|
||||
|
||||
// rating deviation will grow back from 50 to max 350 in 2 years
|
||||
public static final double C = 0.00137934314767061324980397708525;
|
||||
|
||||
public static final double BaseRating = 1500;
|
||||
public static final double BaseRD = 350;
|
||||
public static final double MinRD = 30;
|
||||
|
||||
private static final double Q = Math.log(10) / 400;
|
||||
|
||||
public static GlickoRating getInitialRating() {
|
||||
return new GlickoRating(GlickoRatingSystem.BaseRating, GlickoRatingSystem.BaseRD, 0);
|
||||
}
|
||||
|
||||
public static int getDisplayedRating(GlickoRating rating) {
|
||||
long currentTime = new Date().getTime();
|
||||
double updatedRatingDeviation = getUpdatedRD(rating, currentTime);
|
||||
return (int) Math.max(rating.getRating() - 2 * updatedRatingDeviation, 0);
|
||||
}
|
||||
|
||||
public static int getDefaultDisplayedRating() {
|
||||
return getDisplayedRating(getInitialRating());
|
||||
}
|
||||
|
||||
public void updateRating(GlickoRating playerRating, GlickoRating opponentRating, double outcome, long gameTimeMs) {
|
||||
playerRating.setRatingDeviation(getUpdatedRD(playerRating, gameTimeMs));
|
||||
opponentRating.setRatingDeviation(getUpdatedRD(opponentRating, gameTimeMs));
|
||||
|
||||
GlickoRating newPlayerRating = getNewRating(playerRating, opponentRating, outcome);
|
||||
GlickoRating newOpponentRating = getNewRating(opponentRating, playerRating, 1 - outcome);
|
||||
|
||||
playerRating.setRating(newPlayerRating.getRating());
|
||||
playerRating.setRatingDeviation(newPlayerRating.getRatingDeviation());
|
||||
playerRating.setLastGameTimeMs(gameTimeMs);
|
||||
|
||||
opponentRating.setRating(newOpponentRating.getRating());
|
||||
opponentRating.setRatingDeviation(newOpponentRating.getRatingDeviation());
|
||||
opponentRating.setLastGameTimeMs(gameTimeMs);
|
||||
}
|
||||
|
||||
private static double getUpdatedRD(GlickoRating rating, long gameTimeMs) {
|
||||
double newRatingDeviation;
|
||||
if (rating.getLastGameTimeMs() != 0)
|
||||
{
|
||||
double newRD = Math.sqrt(
|
||||
rating.getRatingDeviation() * rating.getRatingDeviation()
|
||||
+ C * C * Math.max(gameTimeMs - rating.getLastGameTimeMs(), 0));
|
||||
newRatingDeviation = Math.max(Math.min(BaseRD, newRD), MinRD);
|
||||
}
|
||||
else
|
||||
{
|
||||
newRatingDeviation = BaseRD;
|
||||
}
|
||||
return newRatingDeviation;
|
||||
}
|
||||
|
||||
private GlickoRating getNewRating(GlickoRating playerRating, GlickoRating opponentRating, double outcome) {
|
||||
double RD = playerRating.getRatingDeviation();
|
||||
|
||||
double g = gFunc(opponentRating.getRatingDeviation());
|
||||
double p = -g * (playerRating.getRating() - opponentRating.getRating()) / 400;
|
||||
double e = 1 / (1 + Math.pow(10, p));
|
||||
double d2 = 1 / (Q * Q * g * g * e * (1 - e));
|
||||
|
||||
// todo: set minimum K?
|
||||
double newRating = playerRating.getRating() + Q / (1 / RD / RD + 1 / d2) * g * (outcome - e);
|
||||
double newRD = Math.sqrt(1 / (1 / RD / RD + 1 / d2));
|
||||
|
||||
return new GlickoRating(newRating, newRD);
|
||||
}
|
||||
|
||||
private double gFunc(double rd) {
|
||||
return 1 / Math.sqrt(1 + 3 * Q * Q * rd * rd / Math.PI / Math.PI);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,8 @@ import java.util.HashSet;
|
|||
import java.util.List;
|
||||
import mage.cards.repository.RepositoryUtil;
|
||||
import mage.game.result.ResultProtos;
|
||||
import mage.server.rating.GlickoRating;
|
||||
import mage.server.rating.GlickoRatingSystem;
|
||||
import org.apache.log4j.Logger;
|
||||
|
||||
public enum UserStatsRepository {
|
||||
|
@ -120,7 +122,9 @@ public enum UserStatsRepository {
|
|||
ResultProtos.MatchProto match = table.getMatch();
|
||||
for (ResultProtos.MatchPlayerProto player : match.getPlayersList()) {
|
||||
UserStats userStats = this.getUser(player.getName());
|
||||
ResultProtos.UserStatsProto proto = userStats != null ? userStats.getProto()
|
||||
ResultProtos.UserStatsProto proto =
|
||||
userStats != null
|
||||
? userStats.getProto()
|
||||
: ResultProtos.UserStatsProto.newBuilder().setName(player.getName()).build();
|
||||
ResultProtos.UserStatsProto.Builder builder = ResultProtos.UserStatsProto.newBuilder(proto)
|
||||
.setMatches(proto.getMatches() + 1);
|
||||
|
@ -142,6 +146,7 @@ public enum UserStatsRepository {
|
|||
}
|
||||
updatedUsers.add(player.getName());
|
||||
}
|
||||
updateRating(match, table.getEndTimeMs());
|
||||
} else if (table.hasTourney()) {
|
||||
ResultProtos.TourneyProto tourney = table.getTourney();
|
||||
for (ResultProtos.TourneyPlayerProto player : tourney.getPlayersList()) {
|
||||
|
@ -168,12 +173,196 @@ public enum UserStatsRepository {
|
|||
}
|
||||
updatedUsers.add(player.getName());
|
||||
}
|
||||
|
||||
for (ResultProtos.TourneyRoundProto round : tourney.getRoundsList()) {
|
||||
for (ResultProtos.MatchProto match : round.getMatchesList()) {
|
||||
updateRating(match, table.getEndTimeMs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(updatedUsers);
|
||||
}
|
||||
|
||||
private void updateRating(ResultProtos.MatchProto match, long tableEndTimeMs) {
|
||||
long matchEndTimeMs;
|
||||
if (match.hasEndTimeMs()) {
|
||||
matchEndTimeMs = match.getEndTimeMs();
|
||||
} else {
|
||||
matchEndTimeMs = tableEndTimeMs;
|
||||
}
|
||||
|
||||
// process only match with options
|
||||
if (!match.hasMatchOptions()) {
|
||||
return;
|
||||
}
|
||||
ResultProtos.MatchOptionsProto matchOptions = match.getMatchOptions();
|
||||
|
||||
// process only rated matches
|
||||
if (!matchOptions.getRated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// rating only for duels
|
||||
if (match.getPlayersCount() != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResultProtos.MatchPlayerProto player1 = match.getPlayers(0);
|
||||
ResultProtos.MatchPlayerProto player2 = match.getPlayers(1);
|
||||
|
||||
// rate only games between human players
|
||||
if (!player1.getHuman() || !player2.getHuman()) {
|
||||
return;
|
||||
}
|
||||
|
||||
double outcome;
|
||||
if ((player1.getQuit() == ResultProtos.MatchQuitStatus.NO_MATCH_QUIT && player1.getWins() > player2.getWins())
|
||||
|| player2.getQuit() != ResultProtos.MatchQuitStatus.NO_MATCH_QUIT) {
|
||||
// player1 won
|
||||
outcome = 1;
|
||||
} else if ((player2.getQuit() == ResultProtos.MatchQuitStatus.NO_MATCH_QUIT && player1.getWins() < player2.getWins())
|
||||
|| player1.getQuit() != ResultProtos.MatchQuitStatus.NO_MATCH_QUIT) {
|
||||
// player2 won
|
||||
outcome = 0;
|
||||
} else {
|
||||
// draw
|
||||
outcome = 0.5;
|
||||
}
|
||||
|
||||
ResultProtos.UserStatsProto player1StatsProto = getUserStatsProto(player1.getName(), matchEndTimeMs);
|
||||
ResultProtos.UserStatsProto player2StatsProto = getUserStatsProto(player2.getName(), matchEndTimeMs);
|
||||
|
||||
ResultProtos.UserStatsProto.Builder player1StatsBuilder =
|
||||
ResultProtos.UserStatsProto.newBuilder(player1StatsProto);
|
||||
ResultProtos.UserStatsProto.Builder player2StatsBuilder =
|
||||
ResultProtos.UserStatsProto.newBuilder(player2StatsProto);
|
||||
|
||||
// update general rating
|
||||
ResultProtos.GlickoRatingProto player1GeneralRatingProto = null;
|
||||
if (player1StatsProto.hasGeneralGlickoRating()) {
|
||||
player1GeneralRatingProto = player1StatsProto.getGeneralGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto player2GeneralRatingProto = null;
|
||||
if (player2StatsProto.hasGeneralGlickoRating()) {
|
||||
player2GeneralRatingProto = player2StatsProto.getGeneralGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player1GeneralGlickoRatingBuilder =
|
||||
player1StatsBuilder.getGeneralGlickoRatingBuilder();
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player2GeneralGlickoRatingBuilder =
|
||||
player2StatsBuilder.getGeneralGlickoRatingBuilder();
|
||||
|
||||
updateRating(player1GeneralRatingProto, player2GeneralRatingProto, outcome, matchEndTimeMs,
|
||||
player1GeneralGlickoRatingBuilder, player2GeneralGlickoRatingBuilder);
|
||||
|
||||
if (matchOptions.hasLimited()) {
|
||||
if (matchOptions.getLimited()) {
|
||||
// update limited rating
|
||||
ResultProtos.GlickoRatingProto player1LimitedRatingProto = null;
|
||||
if (player1StatsProto.hasLimitedGlickoRating()) {
|
||||
player1LimitedRatingProto = player1StatsProto.getLimitedGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto player2LimitedRatingProto = null;
|
||||
if (player2StatsProto.hasLimitedGlickoRating()) {
|
||||
player2LimitedRatingProto = player2StatsProto.getLimitedGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player1LimitedGlickoRatingBuilder =
|
||||
player1StatsBuilder.getLimitedGlickoRatingBuilder();
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player2LimitedGlickoRatingBuilder =
|
||||
player2StatsBuilder.getLimitedGlickoRatingBuilder();
|
||||
|
||||
updateRating(player1LimitedRatingProto, player2LimitedRatingProto, outcome, matchEndTimeMs,
|
||||
player1LimitedGlickoRatingBuilder, player2LimitedGlickoRatingBuilder);
|
||||
} else {
|
||||
// update constructed rating
|
||||
|
||||
ResultProtos.GlickoRatingProto player1ConstructedRatingProto = null;
|
||||
if (player1StatsProto.hasConstructedGlickoRating()) {
|
||||
player1ConstructedRatingProto = player1StatsProto.getConstructedGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto player2ConstructedRatingProto = null;
|
||||
if (player2StatsProto.hasConstructedGlickoRating()) {
|
||||
player2ConstructedRatingProto = player2StatsProto.getConstructedGlickoRating();
|
||||
}
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player1ConstructedGlickoRatingBuilder =
|
||||
player1StatsBuilder.getConstructedGlickoRatingBuilder();
|
||||
|
||||
ResultProtos.GlickoRatingProto.Builder player2ConstructedGlickoRatingBuilder =
|
||||
player2StatsBuilder.getConstructedGlickoRatingBuilder();
|
||||
|
||||
updateRating(player1ConstructedRatingProto, player2ConstructedRatingProto, outcome, matchEndTimeMs,
|
||||
player1ConstructedGlickoRatingBuilder, player2ConstructedGlickoRatingBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.update(new UserStats(player1StatsBuilder.build(), matchEndTimeMs));
|
||||
this.update(new UserStats(player2StatsBuilder.build(), matchEndTimeMs));
|
||||
}
|
||||
|
||||
private void updateRating(
|
||||
ResultProtos.GlickoRatingProto player1RatingProto,
|
||||
ResultProtos.GlickoRatingProto player2RatingProto,
|
||||
double outcome,
|
||||
long tableEndTimeMs,
|
||||
ResultProtos.GlickoRatingProto.Builder player1GlickoRatingBuilder,
|
||||
ResultProtos.GlickoRatingProto.Builder player2GlickoRatingBuilder) {
|
||||
|
||||
GlickoRating player1GlickoRating;
|
||||
if (player1RatingProto != null) {
|
||||
player1GlickoRating = new GlickoRating(
|
||||
player1RatingProto.getRating(),
|
||||
player1RatingProto.getRatingDeviation(),
|
||||
player1RatingProto.getLastGameTimeMs());
|
||||
} else {
|
||||
player1GlickoRating = GlickoRatingSystem.getInitialRating();
|
||||
}
|
||||
|
||||
GlickoRating player2GlickoRating;
|
||||
if (player2RatingProto != null) {
|
||||
player2GlickoRating = new GlickoRating(
|
||||
player2RatingProto.getRating(),
|
||||
player2RatingProto.getRatingDeviation(),
|
||||
player2RatingProto.getLastGameTimeMs());
|
||||
} else {
|
||||
player2GlickoRating = GlickoRatingSystem.getInitialRating();
|
||||
}
|
||||
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
glickoRatingSystem.updateRating(player1GlickoRating, player2GlickoRating, outcome, tableEndTimeMs);
|
||||
|
||||
player1GlickoRatingBuilder
|
||||
.setRating(player1GlickoRating.getRating())
|
||||
.setRatingDeviation(player1GlickoRating.getRatingDeviation())
|
||||
.setLastGameTimeMs(tableEndTimeMs);
|
||||
|
||||
player2GlickoRatingBuilder
|
||||
.setRating(player2GlickoRating.getRating())
|
||||
.setRatingDeviation(player2GlickoRating.getRatingDeviation())
|
||||
.setLastGameTimeMs(tableEndTimeMs);
|
||||
}
|
||||
|
||||
private ResultProtos.UserStatsProto getUserStatsProto(String playerName, long endTimeMs) {
|
||||
UserStats player1Stats = this.getUser(playerName);
|
||||
ResultProtos.UserStatsProto player1StatsProto;
|
||||
if (player1Stats != null) {
|
||||
player1StatsProto = player1Stats.getProto();
|
||||
} else {
|
||||
player1StatsProto = ResultProtos.UserStatsProto.newBuilder().setName(playerName).build();
|
||||
this.add(new UserStats(player1StatsProto, endTimeMs));
|
||||
}
|
||||
return player1StatsProto;
|
||||
}
|
||||
|
||||
public void closeDB() {
|
||||
try {
|
||||
if (dao != null && dao.getConnectionSource() != null) {
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright 2011 BetaSteward_at_googlemail.com. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are
|
||||
* permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
* conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
* of conditions and the following disclaimer in the documentation and/or other materials
|
||||
* provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* The views and conclusions contained in the software and documentation are those of the
|
||||
* authors and should not be interpreted as representing official policies, either expressed
|
||||
* or implied, of BetaSteward_at_googlemail.com.
|
||||
*/
|
||||
package org.mage.test.serverside.rating;
|
||||
|
||||
import org.junit.Assert;
|
||||
import mage.server.rating.GlickoRating;
|
||||
import mage.server.rating.GlickoRatingSystem;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Quercitron
|
||||
*/
|
||||
public class GlickoRatingSystemTest {
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
@Test
|
||||
public void testRatingsAreEqualAfterDraws() {
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
int count = 1000;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double startRating = random.nextDouble() * 2500 + 500;
|
||||
double startRatingDeviation = Math.min(random.nextDouble() * 300 + 100, GlickoRatingSystem.BaseRD);
|
||||
GlickoRating player1 = new GlickoRating(startRating, startRatingDeviation, 1);
|
||||
GlickoRating player2 = new GlickoRating(startRating, startRatingDeviation, 1);
|
||||
|
||||
int gamesCount = random.nextInt(50) + 1;
|
||||
|
||||
for (int j = 0; j < gamesCount; j++) {
|
||||
glickoRatingSystem.updateRating(player1, player2, 0.5, j + 2);
|
||||
Assert.assertEquals(player1.getRating(), player2.getRating(), 1e-5);
|
||||
Assert.assertEquals(player1.getRatingDeviation(), player2.getRatingDeviation(), 1e-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRatingChangesAreSymmetric() {
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
int count = 1000;
|
||||
for (int i = 0; i < count; i++) {
|
||||
double startRating1 = random.nextDouble() * 2500 + 500;
|
||||
double startRating2 = random.nextDouble() * 2500 + 500;
|
||||
double startRatingDeviation = Math.min(random.nextDouble() * 300 + 100, GlickoRatingSystem.BaseRD);
|
||||
GlickoRating player1 = new GlickoRating(startRating1, startRatingDeviation, 1);
|
||||
GlickoRating player2 = new GlickoRating(startRating2, startRatingDeviation, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, random.nextDouble(), 1);
|
||||
Assert.assertEquals(player1.getRating() - startRating1, startRating2 - player2.getRating(), 1e-5);
|
||||
Assert.assertEquals(player1.getRatingDeviation(), player2.getRatingDeviation(), 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactResult1()
|
||||
{
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
GlickoRating player1 = new GlickoRating(1500, 350, 1);
|
||||
GlickoRating player2 = new GlickoRating(1500, 350, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, 1, 1);
|
||||
|
||||
Assert.assertEquals(1662, player1.getRating(), 1);
|
||||
Assert.assertEquals(290.2, player1.getRatingDeviation(), 0.1);
|
||||
|
||||
Assert.assertEquals(1338, player2.getRating(), 1);
|
||||
Assert.assertEquals(290.2, player2.getRatingDeviation(), 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactResult2()
|
||||
{
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
GlickoRating player1 = new GlickoRating(1500, 350, 1);
|
||||
GlickoRating player2 = new GlickoRating(1200, 50, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, 1, 1);
|
||||
|
||||
Assert.assertEquals(1571, player1.getRating(), 1);
|
||||
Assert.assertEquals(284.3, player1.getRatingDeviation(), 0.1);
|
||||
|
||||
Assert.assertEquals(1198, player2.getRating(), 1);
|
||||
Assert.assertEquals(49.8, player2.getRatingDeviation(), 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactResult3()
|
||||
{
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
GlickoRating player1 = new GlickoRating(1500, 350, 1);
|
||||
GlickoRating player2 = new GlickoRating(1200, 50, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, 0, 1);
|
||||
|
||||
Assert.assertEquals(1111, player1.getRating(), 1);
|
||||
Assert.assertEquals(284.3, player1.getRatingDeviation(), 0.1);
|
||||
|
||||
Assert.assertEquals(1207, player2.getRating(), 1);
|
||||
Assert.assertEquals(49.8, player2.getRatingDeviation(), 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactResult4()
|
||||
{
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
GlickoRating player1 = new GlickoRating(1500, 250, 1);
|
||||
GlickoRating player2 = new GlickoRating(2000, 100, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, 0.5, 1);
|
||||
|
||||
Assert.assertEquals(1636, player1.getRating(), 1);
|
||||
Assert.assertEquals(237.6, player1.getRatingDeviation(), 0.1);
|
||||
|
||||
Assert.assertEquals(1982, player2.getRating(), 1);
|
||||
Assert.assertEquals(99.1, player2.getRatingDeviation(), 0.1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactResult5()
|
||||
{
|
||||
GlickoRatingSystem glickoRatingSystem = new GlickoRatingSystem();
|
||||
|
||||
GlickoRating player1 = new GlickoRating(1500, 100, 1);
|
||||
GlickoRating player2 = new GlickoRating(2000, 100, 1);
|
||||
|
||||
glickoRatingSystem.updateRating(player1, player2, 1, 1);
|
||||
|
||||
Assert.assertEquals(1551, player1.getRating(), 1);
|
||||
Assert.assertEquals(99.2, player1.getRatingDeviation(), 0.1);
|
||||
|
||||
Assert.assertEquals(1949, player2.getRating(), 1);
|
||||
Assert.assertEquals(99.2, player2.getRatingDeviation(), 0.1);
|
||||
}
|
||||
|
||||
}
|
|
@ -497,7 +497,9 @@ public abstract class MatchImpl implements Match {
|
|||
.setGameType(this.getOptions().getGameType())
|
||||
.setDeckType(this.getOptions().getDeckType())
|
||||
.setGames(this.getNumGames())
|
||||
.setDraws(this.getDraws());
|
||||
.setDraws(this.getDraws())
|
||||
.setMatchOptions(this.getOptions().toProto())
|
||||
.setEndTimeMs((this.getEndTime() != null ? this.getEndTime() : new Date()).getTime());
|
||||
for (MatchPlayer matchPlayer : this.getPlayers()) {
|
||||
MatchQuitStatus status = !matchPlayer.hasQuit() ? MatchQuitStatus.NO_MATCH_QUIT :
|
||||
matchPlayer.getPlayer().hasTimerTimeout() ? MatchQuitStatus.TIMER_TIMEOUT :
|
||||
|
@ -505,6 +507,7 @@ public abstract class MatchImpl implements Match {
|
|||
MatchQuitStatus.QUIT;
|
||||
builder.addPlayersBuilder()
|
||||
.setName(matchPlayer.getName())
|
||||
.setHuman(matchPlayer.getPlayer().isHuman())
|
||||
.setQuit(status)
|
||||
.setWins(matchPlayer.getWins());
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import mage.constants.MatchTimeLimit;
|
|||
import mage.constants.MultiplayerAttackOption;
|
||||
import mage.constants.RangeOfInfluence;
|
||||
import mage.constants.SkillLevel;
|
||||
import mage.game.result.ResultProtos;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -55,6 +56,7 @@ public class MatchOptions implements Serializable {
|
|||
protected SkillLevel skillLevel;
|
||||
protected boolean rollbackTurnsAllowed;
|
||||
protected int quitRatio;
|
||||
protected boolean rated;
|
||||
|
||||
/**
|
||||
* Time each player has during the game to play using his\her priority.
|
||||
|
@ -177,4 +179,36 @@ public class MatchOptions implements Serializable {
|
|||
public void setQuitRatio(int quitRatio) {
|
||||
this.quitRatio = quitRatio;
|
||||
}
|
||||
|
||||
public boolean isRated() {
|
||||
return rated;
|
||||
}
|
||||
|
||||
public void setRated(boolean rated) {
|
||||
this.rated = rated;
|
||||
}
|
||||
|
||||
public ResultProtos.MatchOptionsProto toProto() {
|
||||
ResultProtos.MatchOptionsProto.Builder builder = ResultProtos.MatchOptionsProto.newBuilder()
|
||||
.setName(this.getName())
|
||||
.setLimited(this.isLimited())
|
||||
.setRated(this.isRated())
|
||||
.setWinsNeeded(this.getWinsNeeded());
|
||||
|
||||
ResultProtos.SkillLevel skillLevel = ResultProtos.SkillLevel.BEGINNER;
|
||||
switch (this.getSkillLevel()) {
|
||||
case BEGINNER:
|
||||
skillLevel = ResultProtos.SkillLevel.BEGINNER;
|
||||
break;
|
||||
case CASUAL:
|
||||
skillLevel = ResultProtos.SkillLevel.CASUAL;
|
||||
break;
|
||||
case SERIOUS:
|
||||
skillLevel = ResultProtos.SkillLevel.SERIOUS;
|
||||
break;
|
||||
}
|
||||
builder.setSkillLevel(skillLevel);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -584,7 +584,9 @@ public abstract class TournamentImpl implements Tournament {
|
|||
.setGames(match.getNumGames())
|
||||
.setDraws(match.getDraws())
|
||||
.addPlayers(matchToProto(match, pair.getPlayer1()))
|
||||
.addPlayers(matchToProto(match, pair.getPlayer2()));
|
||||
.addPlayers(matchToProto(match, pair.getPlayer2()))
|
||||
.setMatchOptions(match.getOptions().toProto())
|
||||
.setEndTimeMs((match.getEndTime() != null ? match.getEndTime() : new Date()).getTime());
|
||||
}
|
||||
}
|
||||
for (TournamentPlayer tp : round.getPlayerByes()) {
|
||||
|
@ -602,6 +604,7 @@ public abstract class TournamentImpl implements Tournament {
|
|||
MatchQuitStatus.QUIT;
|
||||
return MatchPlayerProto.newBuilder()
|
||||
.setName(player.getPlayer().getName())
|
||||
.setHuman(player.getPlayer().isHuman())
|
||||
.setWins(matchPlayer.getWins())
|
||||
.setQuit(quit)
|
||||
.build();
|
||||
|
|
|
@ -29,6 +29,10 @@ public class UserData implements Serializable {
|
|||
protected String tourneyHistory;
|
||||
protected int tourneyQuitRatio;
|
||||
|
||||
private int generalRating;
|
||||
private int constructedRating;
|
||||
private int limitedRating;
|
||||
|
||||
public UserData(UserGroup userGroup, int avatarId, boolean showAbilityPickerForced,
|
||||
boolean allowRequestShowHandCards, boolean confirmEmptyManaPool, UserSkipPrioritySteps userSkipPrioritySteps,
|
||||
String flagName, boolean askMoveToGraveOrder, boolean manaPoolAutomatic, boolean manaPoolAutomaticRestricted,
|
||||
|
@ -68,6 +72,7 @@ public class UserData implements Serializable {
|
|||
this.passPriorityActivation = userData.passPriorityActivation;
|
||||
this.autoOrderTrigger = userData.autoOrderTrigger;
|
||||
this.useFirstManaAbility = userData.useFirstManaAbility;
|
||||
// todo: why we don't copy user stats here?
|
||||
}
|
||||
|
||||
public static UserData getDefaultUserDataView() {
|
||||
|
@ -225,8 +230,31 @@ public class UserData implements Serializable {
|
|||
return tourneyQuitRatio;
|
||||
}
|
||||
|
||||
public int getGeneralRating() {
|
||||
return generalRating;
|
||||
}
|
||||
|
||||
public void setGeneralRating(int generalRating) {
|
||||
this.generalRating = generalRating;
|
||||
}
|
||||
|
||||
public int getConstructedRating() {
|
||||
return constructedRating;
|
||||
}
|
||||
|
||||
public void setConstructedRating(int constructedRating) {
|
||||
this.constructedRating = constructedRating;
|
||||
}
|
||||
|
||||
public int getLimitedRating() {
|
||||
return limitedRating;
|
||||
}
|
||||
|
||||
public void setLimitedRating(int limitedRating) {
|
||||
this.limitedRating = limitedRating;
|
||||
}
|
||||
|
||||
public static String getDefaultFlagName() {
|
||||
return "world.png";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,6 +20,22 @@ message MatchProto {
|
|||
optional int32 games = 4;
|
||||
optional int32 draws = 5;
|
||||
repeated MatchPlayerProto players = 6;
|
||||
optional MatchOptionsProto match_options = 7;
|
||||
optional int64 end_time_ms = 8;
|
||||
}
|
||||
|
||||
message MatchOptionsProto {
|
||||
optional string name = 1;
|
||||
optional bool limited = 2;
|
||||
optional bool rated = 3;
|
||||
optional SkillLevel skill_level = 4;
|
||||
optional int32 wins_needed = 5;
|
||||
}
|
||||
|
||||
enum SkillLevel {
|
||||
BEGINNER = 0;
|
||||
CASUAL = 1;
|
||||
SERIOUS = 2;
|
||||
}
|
||||
|
||||
message MatchPlayerProto {
|
||||
|
@ -27,6 +43,7 @@ message MatchPlayerProto {
|
|||
optional int32 wins = 2;
|
||||
optional MatchQuitStatus quit = 3;
|
||||
optional bool bye = 4;
|
||||
optional bool human = 5;
|
||||
}
|
||||
|
||||
enum MatchQuitStatus {
|
||||
|
@ -74,4 +91,14 @@ message UserStatsProto {
|
|||
optional int32 matches_idle_timeout = 7;
|
||||
optional int32 matches_timer_timeout = 8;
|
||||
optional int32 matches_quit = 9;
|
||||
|
||||
optional GlickoRatingProto general_glicko_rating = 10;
|
||||
optional GlickoRatingProto constructed_glicko_rating = 11;
|
||||
optional GlickoRatingProto limited_glicko_rating = 12;
|
||||
}
|
||||
|
||||
message GlickoRatingProto {
|
||||
required double rating = 1;
|
||||
required double rating_deviation = 2;
|
||||
optional int64 last_game_time_ms = 3;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue