lila/modules/puzzle/src/main/Finisher.scala

107 lines
3.7 KiB
Scala

package lila.puzzle
import org.goochjs.glicko2.{ Rating, RatingCalculator, RatingPeriodResults }
import org.joda.time.DateTime
import scala.util.chaining._
import lila.common.Bus
import lila.db.AsyncColl
import lila.db.dsl._
import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, UserRepo }
import lila.rating.Perf
final private[puzzle] class Finisher(
api: PuzzleApi,
userRepo: UserRepo,
historyApi: lila.history.HistoryApi,
colls: PuzzleColls
)(implicit ec: scala.concurrent.ExecutionContext) {
import BsonHandlers._
def apply(puzzle: Puzzle, user: User, result: Result, isStudent: Boolean): Fu[(PuzzleRound, Perf)] =
api.round.find(user, puzzle) flatMap { prevRound =>
val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating
val userRating = user.perfs.puzzle.toRating
val puzzleRating = new Rating(
puzzle.glicko.rating atLeast Glicko.minRating,
puzzle.glicko.deviation,
puzzle.glicko.volatility,
puzzle.plays,
null
)
updateRatings(userRating, puzzleRating, result.glicko)
val newPuzzleGlicko = user.perfs.puzzle.established
.option {
Glicko(
rating = puzzleRating.getRating
.atMost(puzzle.glicko.rating + Glicko.maxRatingDelta)
.atLeast(puzzle.glicko.rating - Glicko.maxRatingDelta),
deviation = puzzleRating.getRatingDeviation,
volatility = puzzleRating.getVolatility
)
}
.filter(_.sanityCheck)
val userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now)
val round = prevRound
.fold(
PuzzleRound(
id = PuzzleRound.Id(user.id, puzzle.id),
date = now,
win = result.win,
vote = none,
weight = none
)
)(_.copy(win = result.win))
api.round.upsert(round) zip
isStudent.??(api.round.addDenormalizedUser(round, user)) zip
prevRound.isEmpty.?? {
colls.puzzle {
_.update
.one(
$id(puzzle.id),
$inc(Puzzle.BSONFields.plays -> $int(1)) ++ newPuzzleGlicko ?? { glicko =>
$set(Puzzle.BSONFields.glicko -> Glicko.glickoBSONHandler.write(glicko))
}
)
.void
}
} zip
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) >>-
Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle"
) inject (round -> userPerf)
}
private val VOLATILITY = Glicko.default.volatility
private val TAU = 0.75d
private val system = new RatingCalculator(VOLATILITY, TAU)
def incPuzzlePlays(puzzle: Puzzle): Funit =
colls.puzzle.map(_.incFieldUnchecked($id(puzzle.id), Puzzle.BSONFields.plays))
private def updateRatings(u1: Rating, u2: Rating, result: Glicko.Result): Unit = {
val results = new RatingPeriodResults()
result match {
case Glicko.Result.Draw => results.addDraw(u1, u2)
case Glicko.Result.Win => results.addResult(u1, u2)
case Glicko.Result.Loss => results.addResult(u2, u1)
}
try {
val (r1, r2) = (u1.getRating, u2.getRating)
system.updateRatings(results)
// never take away more than 30 rating points - it just causes upsets
List(r1 -> u1, r2 -> u2).foreach {
case (prev, next) if next.getRating - prev < -30 => next.setRating(prev - 30)
case _ =>
}
} catch {
case e: Exception => logger.error("finisher", e)
}
}
}