From cf3f27f259abeb97b3409c7baddd873db4458bbc Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Tue, 22 Jan 2019 18:41:50 +0800 Subject: [PATCH] prevent bot farming - closes #4839 --- modules/round/src/main/BotFarming.scala | 33 +++++++++++++++++++++++ modules/round/src/main/Env.scala | 4 ++- modules/round/src/main/PerfsUpdater.scala | 9 ++++--- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 modules/round/src/main/BotFarming.scala diff --git a/modules/round/src/main/BotFarming.scala b/modules/round/src/main/BotFarming.scala new file mode 100644 index 000000000000..5df6e338fbaa --- /dev/null +++ b/modules/round/src/main/BotFarming.scala @@ -0,0 +1,33 @@ +package lila.round + +import lila.common.LightUser.IsBotSync +import lila.game.{ Game, GameRepo, CrosstableApi } + +private final class BotFarming( + crosstableApi: CrosstableApi, + isBotSync: IsBotSync +) { + + val SAME_PLIES = 20 + val PREV_GAMES = 2 + + /* true if + * - at least one bot + * - rated + * - recent game in same matchup has same first SAME_PLIES and same winner + */ + def apply(g: Game): Fu[Boolean] = g.userIds.distinct match { + case List(u1, u2) if g.finished && g.rated && g.userIds.exists(isBotSync) => + crosstableApi(u1, u2) flatMap { + _ ?? { ct => + GameRepo.gamesFromSecondary(ct.results.reverse.take(PREV_GAMES).map(_.gameId)) map { + _ exists { prev => + g.winnerUserId == prev.winnerUserId && + g.pgnMoves.take(SAME_PLIES) == prev.pgnMoves.take(SAME_PLIES) + } + } + } + } + case _ => fuccess(false) + } +} diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala index a82c0cafec18..150a1b16472f 100644 --- a/modules/round/src/main/Env.scala +++ b/modules/round/src/main/Env.scala @@ -152,7 +152,9 @@ final class Env( isRecentTv = recentTvGames get _ ) - lazy val perfsUpdater = new PerfsUpdater(historyApi, rankingApi) + private lazy val botFarming = new BotFarming(crosstableApi, isBotSync) + + lazy val perfsUpdater = new PerfsUpdater(historyApi, rankingApi, botFarming) lazy val forecastApi: ForecastApi = new ForecastApi( coll = db(CollectionForecast), diff --git a/modules/round/src/main/PerfsUpdater.scala b/modules/round/src/main/PerfsUpdater.scala index 0a284ecef70b..e63937df4477 100644 --- a/modules/round/src/main/PerfsUpdater.scala +++ b/modules/round/src/main/PerfsUpdater.scala @@ -10,7 +10,8 @@ import lila.user.{ UserRepo, User, Perfs, RankingApi } final class PerfsUpdater( historyApi: HistoryApi, - rankingApi: RankingApi + rankingApi: RankingApi, + botFarming: BotFarming ) { private val VOLATILITY = Glicko.default.volatility @@ -18,8 +19,9 @@ final class PerfsUpdater( private val system = new RatingCalculator(VOLATILITY, TAU) // returns rating diffs - def save(game: Game, white: User, black: User): Fu[Option[RatingDiffs]] = - PerfPicker.main(game) ?? { mainPerf => + def save(game: Game, white: User, black: User): Fu[Option[RatingDiffs]] = botFarming(game) flatMap { + case true => fuccess(none) + case _ => PerfPicker.main(game) ?? { mainPerf => (game.rated && game.finished && game.accountable && !white.lame && !black.lame) ?? { val ratingsW = mkRatings(white.perfs) val ratingsB = mkRatings(black.perfs) @@ -73,6 +75,7 @@ final class PerfsUpdater( rankingApi.save(black, game.perfType, perfsB) inject ratingDiffs.some } } + } private final case class Ratings( chess960: Rating,