diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index 837b88ad7a7fc..d98c2ac89bd59 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -199,7 +199,7 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): case Some((puzzle, replay)) => renderJson(puzzle, angle, replay.some) map { nextJson => Json.obj( - "round" -> env.puzzle.jsonView.roundJson(me, round, perf), + "round" -> env.puzzle.jsonView.roundJson.web(me, round, perf), "next" -> nextJson ) } @@ -210,7 +210,7 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): next <- nextPuzzleForMe(angle, none) nextJson <- next.?? { renderJson(_, angle, none, newUser.some) dmap some } yield Json.obj( - "round" -> env.puzzle.jsonView.roundJson(me, round, perf), + "round" -> env.puzzle.jsonView.roundJson.web(me, round, perf), "next" -> nextJson ) } @@ -449,15 +449,13 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): } def apiBatchSelect(angleStr: String) = AnonOrScoped(_.Puzzle.Read) { implicit req => me => - batchSelect(me, PuzzleAngle findOrMix angleStr, reqDifficulty, getInt("nb", req) | 15) + batchSelect(me, PuzzleAngle findOrMix angleStr, reqDifficulty, getInt("nb", req) | 15).dmap(Ok.apply) } private def reqDifficulty(using req: RequestHeader) = PuzzleDifficulty.orDefault(~get("difficulty", req)) - private def batchSelect(me: Option[UserModel], angle: PuzzleAngle, difficulty: PuzzleDifficulty, nb: Int)( - using req: RequestHeader - ): Fu[Result] = - env.puzzle.batch.nextFor(me, angle, difficulty, nb atLeast 1 atMost 50) flatMap - env.puzzle.jsonView.batch dmap { Ok(_) } + private def batchSelect(me: Option[UserModel], angle: PuzzleAngle, difficulty: PuzzleDifficulty, nb: Int) = + env.puzzle.batch.nextFor(me, angle, difficulty, nb atMost 50) flatMap + env.puzzle.jsonView.batch(me) def apiBatchSolve(angleStr: String) = AnonOrScopedBody(parse.json)(_.Puzzle.Write) { implicit req => me => req.body @@ -466,19 +464,18 @@ final class Puzzle(env: Env, apiC: => Api) extends LilaController(env): err => BadRequest(err.toString).toFuccess, data => { val angle = PuzzleAngle findOrMix angleStr - { - me match { + for + rounds <- me match case Some(me) => - lila.common.LilaFuture - .applySequentially(data.solutions) { sol => - env.puzzle - .finisher(sol.id, angle, me, sol.win, sol.mode) - .void - } + env.puzzle.finisher.batch(me, angle, data.solutions).map { + _.map { (round, rDiff) => env.puzzle.jsonView.roundJson.api(round, rDiff) } + } case None => - data.solutions.map { sol => env.puzzle.finisher.incPuzzlePlays(sol.id) }.parallel - } - } >> getInt("nb", req).fold(fuccess(NoContent))(batchSelect(me, angle, reqDifficulty, _)(using req)) + data.solutions.map { sol => env.puzzle.finisher.incPuzzlePlays(sol.id) }.parallel inject Nil + newMe <- me.??(env.user.repo.byId) + nextPuzzles <- batchSelect(newMe, angle, reqDifficulty, ~getInt("nb", req)) + result = Json.obj("next" -> nextPuzzles, "rounds" -> rounds) + yield Ok(result) } ) } diff --git a/modules/common/src/main/LilaFuture.scala b/modules/common/src/main/LilaFuture.scala index 76fee8bf18df4..cb3db2382fcc1 100644 --- a/modules/common/src/main/LilaFuture.scala +++ b/modules/common/src/main/LilaFuture.scala @@ -8,28 +8,22 @@ import lila.Lila.Fu object LilaFuture: - def fold[T, R]( - list: List[T] - )(zero: R)(op: (R, T) => Fu[R])(using Executor): Fu[R] = + def fold[T, R](list: List[T])(acc: R)(op: (R, T) => Fu[R])(using Executor): Fu[R] = list match case head :: rest => - op(zero, head) flatMap { res => + op(acc, head) flatMap { res => fold(rest)(res)(op) } - case Nil => fuccess(zero) + case Nil => fuccess(acc) - def lazyFold[T, R]( - futures: LazyList[Fu[T]] - )(zero: R)(op: (R, T) => R)(using Executor): Fu[R] = - LazyList.cons.unapply(futures).fold(fuccess(zero)) { case (future, rest) => + def lazyFold[T, R](futures: LazyList[Fu[T]])(acc: R)(op: (R, T) => R)(using Executor): Fu[R] = + LazyList.cons.unapply(futures).fold(fuccess(acc)) { (future, rest) => future flatMap { f => - lazyFold(rest)(op(zero, f))(op) + lazyFold(rest)(op(acc, f))(op) } } - def filter[A]( - list: List[A] - )(f: A => Fu[Boolean])(using Executor): Fu[List[A]] = + def filter[A](list: List[A])(f: A => Fu[Boolean])(using Executor): Fu[List[A]] = ScalaFu .sequence { list.map { element => @@ -45,7 +39,7 @@ object LilaFuture: def linear[A, B, M[B] <: Iterable[B]]( in: M[A] - )(f: A => Fu[B])(implicit cbf: BuildFrom[M[A], B, M[B]], ec: Executor): Fu[M[B]] = + )(f: A => Fu[B])(using cbf: BuildFrom[M[A], B, M[B]], ec: Executor): Fu[M[B]] = in.foldLeft(fuccess(cbf.newBuilder(in))) { (fr, a) => fr flatMap { r => f(a).dmap(r += _) diff --git a/modules/perfStat/src/main/JsonView.scala b/modules/perfStat/src/main/JsonView.scala index d2f97d540e4dd..f63dea5d6d7c7 100644 --- a/modules/perfStat/src/main/JsonView.scala +++ b/modules/perfStat/src/main/JsonView.scala @@ -44,7 +44,7 @@ final class JsonView(getLightUser: LightUser.GetterSync): object JsonView: - private def round(v: Double, depth: Int = 2) = lila.common.Maths.roundDownAt(v, depth) + import lila.rating.Glicko.given private val isoFormatter = ISODateTimeFormat.dateTime private given Writes[DateTime] = Writes { d => @@ -53,18 +53,11 @@ object JsonView: given OWrites[User] = OWrites { u => Json.obj("name" -> u.username) } - given OWrites[Glicko] = OWrites { p => - Json.obj( - "rating" -> round(p.rating), - "deviation" -> round(p.deviation), - "provisional" -> p.provisional - ) - } given OWrites[Perf] = OWrites { p => Json.obj("glicko" -> p.glicko, "nb" -> p.nb, "progress" -> p.progress) } private given Writes[Avg] = Writes { a => - JsNumber(round(a.avg)) + JsNumber(lila.common.Maths.roundDownAt(a.avg, 2)) } given (using lang: Lang): OWrites[PerfType] = OWrites { pt => Json.obj( diff --git a/modules/puzzle/src/main/JsonView.scala b/modules/puzzle/src/main/JsonView.scala index efaec2eaa6d82..8ff70ac2c2e7a 100644 --- a/modules/puzzle/src/main/JsonView.scala +++ b/modules/puzzle/src/main/JsonView.scala @@ -62,25 +62,28 @@ final class JsonView( "id" -> u.id, "rating" -> u.perfs.puzzle.intRating ) - .add( - "provisional" -> u.perfs.puzzle.provisional - ) + .add("provisional" -> u.perfs.puzzle.provisional) private def replayJson(r: PuzzleReplay) = Json.obj("days" -> r.days, "i" -> r.i, "of" -> r.nb) - def roundJson(u: User, round: PuzzleRound, perf: Perf) = - Json - .obj( - "win" -> round.win, - "ratingDiff" -> (perf.intRating.value - u.perfs.puzzle.intRating.value) - ) - .add("vote" -> round.vote) - .add("themes" -> round.nonEmptyThemes.map { rt => - JsObject(rt.map { t => - t.theme.value -> JsBoolean(t.vote) + object roundJson { + def web(u: User, round: PuzzleRound, perf: Perf) = + base(round, IntRatingDiff(perf.intRating.value - u.perfs.puzzle.intRating.value)) + .add("vote" -> round.vote) + .add("themes" -> round.nonEmptyThemes.map { rt => + JsObject(rt.map { t => + t.theme.value -> JsBoolean(t.vote) + }) }) - }) + + def api = base _ + private def base(round: PuzzleRound, ratingDiff: IntRatingDiff) = Json.obj( + "id" -> round.id.puzzleId, + "win" -> round.win, + "ratingDiff" -> ratingDiff + ) + } def pref(p: lila.pref.Pref) = Json.obj( @@ -114,7 +117,7 @@ final class JsonView( "performance" -> res.performance ) - def batch(puzzles: Seq[Puzzle]): Fu[JsObject] = for { + def batch(user: Option[User])(puzzles: Seq[Puzzle]): Fu[JsObject] = for games <- gameRepo.gameOptionsFromSecondary(puzzles.map(_.gameId)) jsons <- (puzzles zip games).collect { case (puzzle, Some(game)) => gameJson.noCache(game, puzzle.initialPly) map { gameJson => @@ -124,7 +127,9 @@ final class JsonView( ) } }.parallel - } yield Json.obj("puzzles" -> jsons) + yield + import lila.rating.Glicko.given + Json.obj("puzzles" -> jsons).add("glicko" -> user.map(_.perfs.puzzle.glicko)) object bc: diff --git a/modules/puzzle/src/main/PuzzleFinisher.scala b/modules/puzzle/src/main/PuzzleFinisher.scala index b7ab2a62143c9..f189d6d8f752e 100644 --- a/modules/puzzle/src/main/PuzzleFinisher.scala +++ b/modules/puzzle/src/main/PuzzleFinisher.scala @@ -10,6 +10,7 @@ import lila.rating.{ Glicko, Perf, PerfType } import lila.user.{ User, UserRepo } import chess.Mode import lila.common.config.Max +import lila.puzzle.PuzzleForm.batch.Solution final private[puzzle] class PuzzleFinisher( api: PuzzleApi, @@ -27,6 +28,19 @@ final private[puzzle] class PuzzleFinisher( name = "puzzle.finish" ) + def batch(me: User, angle: PuzzleAngle, solutions: List[Solution]): Fu[List[(PuzzleRound, IntRatingDiff)]] = + lila.common.LilaFuture + .fold(solutions)((me.perfs.puzzle, List.empty[(PuzzleRound, IntRatingDiff)])) { + case ((perf, rounds), sol) => + apply(sol.id, angle, me, sol.win, sol.mode) map { + case Some((round, newPerf)) => + val rDiff = IntRatingDiff(newPerf.intRating.value - perf.intRating.value) + (newPerf, (round, rDiff) :: rounds) + case None => (perf, rounds) + } + } + .map { (_, rounds) => rounds.reverse } + def apply( id: PuzzleId, angle: PuzzleAngle, diff --git a/modules/rating/src/main/Glicko.scala b/modules/rating/src/main/Glicko.scala index 283d7e3a3050d..7b06b6209e458 100644 --- a/modules/rating/src/main/Glicko.scala +++ b/modules/rating/src/main/Glicko.scala @@ -113,6 +113,19 @@ case object Glicko: "v" -> w.double(o.volatility) ) + import play.api.libs.json.{ OWrites, Json } + given OWrites[Glicko] = + import lila.common.Maths.roundDownAt + import lila.common.Json.given + OWrites { p => + Json + .obj( + "rating" -> roundDownAt(p.rating, 2), + "deviation" -> roundDownAt(p.deviation, 2) + ) + .add("provisional" -> p.provisional) + } + sealed abstract class Result: def negate: Result object Result: diff --git a/package.json b/package.json index 4c751d65b2db1..c0adcebc7e1e8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "check-format": "prettier --check .", "lint": "eslint . --ext .ts", "lila-journal": "journalctl --user -fu lila -o cat", - "metals-log": "tail -F .metals/metals.log | rg -v '(Unmatched cancel notification|handleCancellation)' | cut -c 21-", + "metals-log": "tail -F .metals/metals.log | stdbuf -oL cut -c 21- | rg -v '(notification for request|handleCancellation)'", "serverlog": "pnpm lila-journal & pnpm metals-log", "multilog": "pnpm serverlog & ui/build -w" }