sequence puzzle completion

pull/7819/head
Thibault Duplessis 2020-12-29 22:29:47 +01:00
parent e332d70b77
commit eb31cb8167
3 changed files with 145 additions and 130 deletions

View File

@ -74,7 +74,9 @@ final class Puzzle(
def complete(themeStr: String, id: String) =
OpenBody { implicit ctx =>
NoBot {
onComplete(Puz.Id(id), PuzzleTheme findOrAny themeStr, mobileBc = false)
Puz.toId(id) ?? { pid =>
onComplete(pid, PuzzleTheme findOrAny themeStr, mobileBc = false)
}
}
}
@ -87,59 +89,55 @@ final class Puzzle(
private def onComplete[A](id: Puz.Id, theme: PuzzleTheme, mobileBc: Boolean)(implicit
ctx: BodyContext[A]
) =
OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
implicit val req = ctx.body
lila.mon.puzzle.round.attempt(ctx.isAuth, theme.key.value).increment()
env.puzzle.forms.round
.bindFromRequest()
.fold(
jsonFormError,
resultInt =>
{
ctx.me match {
case Some(me) =>
for {
(round, perf) <- env.puzzle.finisher(
puzzle = puzzle,
theme = theme.key,
user = me,
result = Result(resultInt == 1)
)
newUser = me.copy(perfs = me.perfs.copy(puzzle = perf))
_ <- env.puzzle.session.onComplete(round, theme.key)
json <-
if (mobileBc) fuccess {
env.puzzle.jsonView.bc.userJson(perf.intRating) ++ Json.obj(
"round" -> Json.obj(
"ratingDiff" -> 0,
"win" -> (resultInt == 1)
),
"voted" -> round.vote
)
}
else
for {
next <- nextPuzzleForMe(theme.key)
nextJson <- renderJson(next, theme, none, newUser.some)
} yield Json.obj(
"round" -> env.puzzle.jsonView.roundJson(me, round, perf),
"next" -> nextJson
)
} yield json
case None =>
env.puzzle.finisher.incPuzzlePlays(puzzle)
if (mobileBc) fuccess(Json.obj("user" -> false))
else
nextPuzzleForMe(theme.key) flatMap {
renderJson(_, theme)
} map { json =>
Json.obj("next" -> json)
) = {
implicit val req = ctx.body
lila.mon.puzzle.round.attempt(ctx.isAuth, theme.key.value).increment()
env.puzzle.forms.round
.bindFromRequest()
.fold(
jsonFormError,
resultInt =>
{
ctx.me match {
case Some(me) =>
for {
(round, perf) <- env.puzzle
.finisher(id, theme.key, me, Result(resultInt == 1))
.orFail(s"No such puzzle $id")
newUser = me.copy(perfs = me.perfs.copy(puzzle = perf))
_ <- env.puzzle.session.onComplete(round, theme.key)
json <-
if (mobileBc) fuccess {
env.puzzle.jsonView.bc.userJson(perf.intRating) ++ Json.obj(
"round" -> Json.obj(
"ratingDiff" -> 0,
"win" -> (resultInt == 1)
),
"voted" -> round.vote
)
}
}
} dmap { json => Ok(json) as JSON }
)
}
else
for {
next <- nextPuzzleForMe(theme.key)
nextJson <- renderJson(next, theme, none, newUser.some)
} yield Json.obj(
"round" -> env.puzzle.jsonView.roundJson(me, round, perf),
"next" -> nextJson
)
} yield json
case None =>
env.puzzle.finisher.incPuzzlePlays(puzzleId)
if (mobileBc) fuccess(Json.obj("user" -> false))
else
nextPuzzleForMe(theme.key) flatMap {
renderJson(_, theme)
} map { json =>
Json.obj("next" -> json)
}
}
} dmap { json => Ok(json) as JSON }
)
}
def vote(id: String) =
AuthBody { implicit ctx => me =>

View File

@ -37,6 +37,8 @@ object Puzzle {
case class Id(value: String) extends AnyVal with StringValue
def toId(id: String) = id.size == idSize option Id(id)
/* The mobile app requires numerical IDs.
* We convert string ids from and to Longs using base 62
*/

View File

@ -3,6 +3,7 @@ package lila.puzzle
import cats.implicits._
import org.goochjs.glicko2.{ Rating, RatingCalculator, RatingPeriodResults }
import org.joda.time.DateTime
import scala.concurrent.duration._
import scala.util.chaining._
import lila.common.Bus
@ -17,90 +18,104 @@ final private[puzzle] class PuzzleFinisher(
userRepo: UserRepo,
historyApi: lila.history.HistoryApi,
colls: PuzzleColls
)(implicit ec: scala.concurrent.ExecutionContext) {
)(implicit ec: scala.concurrent.ExecutionContext, system: akka.actor.ActorSystem, mode: play.api.Mode) {
import BsonHandlers._
private val sequencer =
new lila.hub.DuctSequencers(
maxSize = 64,
expiration = 5 minutes,
timeout = 5 seconds,
name = "puzzle.finish"
)
def apply(
puzzle: Puzzle,
id: Puzzle.Id,
theme: PuzzleTheme.Key,
user: User,
result: Result
): Fu[(PuzzleRound, Perf)] =
api.round.find(user, puzzle.id) flatMap { prevRound =>
val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating
): Fu[Option[(PuzzleRound, Perf)]] =
api.round.find(user, id) flatMap { prevRound =>
sequencer(id.value) {
api.puzzle.find(id) flatMap {
_ ?? { puzzle =>
val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating
val (round, newPuzzleGlicko, userPerf) = prevRound match {
case Some(prev) =>
(
prev.updateWithWin(result.win),
none,
user.perfs.puzzle
)
case None =>
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 = ponder
.puzzle(
theme,
result,
puzzle.glicko -> Glicko(
rating = puzzleRating.getRating
.atMost(puzzle.glicko.rating + Glicko.maxRatingDelta)
.atLeast(puzzle.glicko.rating - Glicko.maxRatingDelta),
deviation = puzzleRating.getRatingDeviation,
volatility = puzzleRating.getVolatility
).cap,
player = user.perfs.puzzle.glicko
)
.some
.filter(puzzle.glicko !=)
.filter(_.sanityCheck)
val round =
PuzzleRound(
id = PuzzleRound.Id(user.id, puzzle.id),
win = result.win,
fixedAt = none,
date = DateTime.now
)
val userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now) pipe {
p =>
p.copy(glicko =
ponder.player(theme, result, user.perfs.puzzle.glicko -> p.glicko, puzzle.glicko)
val (round, newPuzzleGlicko, userPerf) = prevRound match {
case Some(prev) =>
(
prev.updateWithWin(result.win),
none,
user.perfs.puzzle
)
case None =>
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 = ponder
.puzzle(
theme,
result,
puzzle.glicko -> Glicko(
rating = puzzleRating.getRating
.atMost(puzzle.glicko.rating + Glicko.maxRatingDelta)
.atLeast(puzzle.glicko.rating - Glicko.maxRatingDelta),
deviation = puzzleRating.getRatingDeviation,
volatility = puzzleRating.getVolatility
).cap,
player = user.perfs.puzzle.glicko
)
.some
.filter(puzzle.glicko !=)
.filter(_.sanityCheck)
val round =
PuzzleRound(
id = PuzzleRound.Id(user.id, puzzle.id),
win = result.win,
fixedAt = none,
date = DateTime.now
)
val userPerf =
user.perfs.puzzle
.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now) pipe { p =>
p.copy(glicko =
ponder.player(theme, result, user.perfs.puzzle.glicko -> p.glicko, puzzle.glicko)
)
}
(round, newPuzzleGlicko, userPerf)
}
(round, newPuzzleGlicko, userPerf)
api.round.upsert(round, theme) zip
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
(userPerf != user.perfs.puzzle).?? {
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) zip
historyApi.addPuzzle(user = user, completedAt = now, perf = userPerf) void
} >>- {
if (prevRound.isEmpty)
Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle"
)
} inject (round -> userPerf).some
}
}
}
api.round.upsert(round, theme) zip
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
(userPerf != user.perfs.puzzle).?? {
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) zip
historyApi.addPuzzle(user = user, completedAt = now, perf = userPerf) void
} >>- {
if (prevRound.isEmpty)
Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle"
)
} inject (round -> userPerf)
}
private object ponder {
@ -154,10 +169,10 @@ final private[puzzle] class PuzzleFinisher(
private val VOLATILITY = Glicko.default.volatility
private val TAU = 0.75d
private val system = new RatingCalculator(VOLATILITY, TAU)
private val calculator = new RatingCalculator(VOLATILITY, TAU)
def incPuzzlePlays(puzzle: Puzzle): Funit =
colls.puzzle.map(_.incFieldUnchecked($id(puzzle.id), Puzzle.BSONFields.plays))
def incPuzzlePlays(puzzleId: Puzzle.Id): Funit =
colls.puzzle.map(_.incFieldUnchecked($id(puzzleId), Puzzle.BSONFields.plays))
private def updateRatings(u1: Rating, u2: Rating, result: Glicko.Result): Unit = {
val results = new RatingPeriodResults()
@ -167,7 +182,7 @@ final private[puzzle] class PuzzleFinisher(
case Glicko.Result.Loss => results.addResult(u2, u1)
}
try {
system.updateRatings(results)
calculator.updateRatings(results)
} catch {
case e: Exception => logger.error("finisher", e)
}