new puzzles WIP
parent
5d5c607dfa
commit
245b4560fe
|
@ -9,6 +9,7 @@ import scala.concurrent.duration._
|
|||
import lila.app._
|
||||
import lila.common.HTTPRequest
|
||||
import lila.game.Pov
|
||||
import lila.puzzle.Puzzle.Id
|
||||
|
||||
final class Export(env: Env) extends LilaController(env) {
|
||||
|
||||
|
@ -53,18 +54,13 @@ final class Export(env: Env) extends LilaController(env) {
|
|||
}(rateLimitedFu)
|
||||
}
|
||||
|
||||
def legacyPuzzleThumbnail(id: Int) =
|
||||
Action {
|
||||
MovedPermanently(routes.Export.puzzleThumbnail(id).url)
|
||||
}
|
||||
|
||||
def puzzleThumbnail(id: Int) =
|
||||
def puzzleThumbnail(id: String) =
|
||||
Open { implicit ctx =>
|
||||
ExportImageRateLimitGlobal("-", msg = HTTPRequest.ipAddress(ctx.req).value) {
|
||||
OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
|
||||
OptionFuResult(env.puzzle.api.puzzle find Id(id)) { puzzle =>
|
||||
env.game.gifExport.thumbnail(
|
||||
fen = puzzle.fenAfterInitialMove | puzzle.fen,
|
||||
lastMove = puzzle.initialMove.uci.some,
|
||||
fen = puzzle.fenAfterInitialMove,
|
||||
lastMove = puzzle.line.head.uci.some,
|
||||
orientation = puzzle.color
|
||||
) map stream("image/gif") map { res =>
|
||||
res.withHeaders(CACHE_CONTROL -> "max-age=86400")
|
||||
|
|
|
@ -7,7 +7,7 @@ import views._
|
|||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.config.MaxPerSecond
|
||||
import lila.puzzle.{ PuzzleId, Result, Puzzle => PuzzleModel, UserInfos }
|
||||
import lila.puzzle.{ Result, Puzzle => Puz }
|
||||
|
||||
final class Puzzle(
|
||||
env: Env,
|
||||
|
@ -15,24 +15,19 @@ final class Puzzle(
|
|||
) extends LilaController(env) {
|
||||
|
||||
private def renderJson(
|
||||
puzzle: PuzzleModel,
|
||||
userInfos: Option[UserInfos],
|
||||
mode: String,
|
||||
voted: Option[Boolean],
|
||||
puzzle: Puz,
|
||||
round: Option[lila.puzzle.Round] = None
|
||||
)(implicit ctx: Context): Fu[JsObject] =
|
||||
env.puzzle.jsonView(
|
||||
puzzle = puzzle,
|
||||
userInfos = userInfos,
|
||||
user = ctx.me,
|
||||
round = round,
|
||||
mode = mode,
|
||||
mobileApi = ctx.mobileApiVersion,
|
||||
voted = voted
|
||||
mobileApi = ctx.mobileApiVersion
|
||||
)
|
||||
|
||||
private def renderShow(puzzle: PuzzleModel, mode: String)(implicit ctx: Context) =
|
||||
env.puzzle userInfos ctx.me flatMap { infos =>
|
||||
renderJson(puzzle = puzzle, userInfos = infos, mode = mode, voted = none) map { json =>
|
||||
private def renderShow(puzzle: Puz)(implicit ctx: Context) =
|
||||
ctx.me.?? { env.puzzle.api.round.find(_, puzzle) } flatMap { round =>
|
||||
renderJson(puzzle, round) map { json =>
|
||||
EnableSharedArrayBuffer(
|
||||
Ok(views.html.puzzle.show(puzzle, data = json, pref = env.puzzle.jsonView.pref(ctx.pref)))
|
||||
)
|
||||
|
@ -46,7 +41,7 @@ final class Puzzle(
|
|||
_.map(_.id) ?? env.puzzle.api.puzzle.find
|
||||
}) { puzzle =>
|
||||
negotiate(
|
||||
html = renderShow(puzzle, "play"),
|
||||
html = renderShow(puzzle),
|
||||
api = _ => puzzleJson(puzzle) map { Ok(_) }
|
||||
) map NoCache
|
||||
}
|
||||
|
@ -56,24 +51,21 @@ final class Puzzle(
|
|||
def home =
|
||||
Open { implicit ctx =>
|
||||
NoBot {
|
||||
env.puzzle.selector(ctx.me) flatMap { puzzle =>
|
||||
renderShow(puzzle, if (ctx.isAuth) "play" else "try")
|
||||
}
|
||||
???
|
||||
// env.puzzle.selector(ctx.me) flatMap { puzzle =>
|
||||
// renderShow(puzzle, if (ctx.isAuth) "play" else "try")
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
def show(id: PuzzleId) =
|
||||
def show(id: Puz.Id) =
|
||||
Open { implicit ctx =>
|
||||
if (id < env.puzzle.idMin) notFound
|
||||
else
|
||||
NoBot {
|
||||
OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
|
||||
renderShow(puzzle, "play")
|
||||
}
|
||||
}
|
||||
NoBot {
|
||||
OptionFuResult(env.puzzle.api.puzzle find id)(renderShow)
|
||||
}
|
||||
}
|
||||
|
||||
def load(id: PuzzleId) =
|
||||
def load(id: Puz.Id) =
|
||||
Open { implicit ctx =>
|
||||
NoBot {
|
||||
XhrOnly {
|
||||
|
@ -82,120 +74,83 @@ final class Puzzle(
|
|||
}
|
||||
}
|
||||
|
||||
private def puzzleJson(puzzle: PuzzleModel)(implicit ctx: Context) =
|
||||
env.puzzle userInfos ctx.me flatMap { infos =>
|
||||
renderJson(puzzle, infos, if (ctx.isAuth) "play" else "try", voted = none)
|
||||
}
|
||||
private def puzzleJson(puzzle: Puz)(implicit ctx: Context) =
|
||||
renderJson(puzzle, round = none)
|
||||
|
||||
// XHR load next play puzzle
|
||||
def newPuzzle =
|
||||
Open { implicit ctx =>
|
||||
NoBot {
|
||||
XhrOnly {
|
||||
env.puzzle.selector(ctx.me) flatMap puzzleJson map { json =>
|
||||
Ok(json) as JSON
|
||||
}
|
||||
???
|
||||
// env.puzzle.selector(ctx.me) flatMap puzzleJson map { json =>
|
||||
// Ok(json) as JSON
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mobile app BC
|
||||
def round(id: PuzzleId) =
|
||||
OpenBody { implicit ctx =>
|
||||
implicit val req = ctx.body
|
||||
OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
|
||||
lila.mon.puzzle.round.attempt(puzzle.mate, ctx.isAuth, "old")
|
||||
env.puzzle.forms.round
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
jsonFormError,
|
||||
resultInt => {
|
||||
val result = Result(resultInt == 1)
|
||||
ctx.me match {
|
||||
case Some(me) =>
|
||||
for {
|
||||
isStudent <- env.clas.api.student.isStudent(me.id)
|
||||
(round, mode) <-
|
||||
env.puzzle.finisher(puzzle, me, result, mobile = true, isStudent = isStudent)
|
||||
me2 <- if (mode.rated) env.user.repo byId me.id map (_ | me) else fuccess(me)
|
||||
infos <- env.puzzle userInfos me2
|
||||
voted <- ctx.me.?? { env.puzzle.api.vote.value(puzzle.id, _) }
|
||||
data <- renderJson(puzzle, infos.some, "view", voted = voted, round = round.some)
|
||||
} yield {
|
||||
val d2 = if (mode.rated) data else data ++ Json.obj("win" -> result.win)
|
||||
Ok(d2)
|
||||
}
|
||||
case None =>
|
||||
env.puzzle.finisher.incPuzzleAttempts(puzzle)
|
||||
renderJson(puzzle, none, "view", voted = none) map { data =>
|
||||
val d2 = data ++ Json.obj("win" -> result.win)
|
||||
Ok(d2)
|
||||
}
|
||||
}
|
||||
}
|
||||
) map (_ as JSON)
|
||||
}
|
||||
}
|
||||
|
||||
// new API
|
||||
def round2(id: PuzzleId) =
|
||||
def round2(id: Puz.Id) =
|
||||
OpenBody { implicit ctx =>
|
||||
NoBot {
|
||||
implicit val req = ctx.body
|
||||
OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
|
||||
lila.mon.puzzle.round.attempt(puzzle.mate, ctx.isAuth, "new")
|
||||
env.puzzle.forms.round
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
jsonFormError,
|
||||
resultInt =>
|
||||
ctx.me match {
|
||||
case Some(me) =>
|
||||
for {
|
||||
isStudent <- env.clas.api.student.isStudent(me.id)
|
||||
(round, mode) <- env.puzzle.finisher(
|
||||
puzzle = puzzle,
|
||||
user = me,
|
||||
result = Result(resultInt == 1),
|
||||
mobile = lila.api.Mobile.Api.requested(ctx.req),
|
||||
isStudent = isStudent
|
||||
)
|
||||
me2 <- if (mode.rated) env.user.repo byId me.id map (_ | me) else fuccess(me)
|
||||
infos <- env.puzzle userInfos me2
|
||||
voted <- ctx.me.?? { env.puzzle.api.vote.value(puzzle.id, _) }
|
||||
} yield Ok(
|
||||
Json.obj(
|
||||
"user" -> lila.puzzle.JsonView.infos(isOldMobile = false)(infos),
|
||||
"round" -> lila.puzzle.JsonView.round(round),
|
||||
"voted" -> voted
|
||||
)
|
||||
)
|
||||
case None =>
|
||||
env.puzzle.finisher.incPuzzleAttempts(puzzle)
|
||||
Ok(Json.obj("user" -> false)).fuccess
|
||||
}
|
||||
) map (_ as JSON)
|
||||
}
|
||||
???
|
||||
// implicit val req = ctx.body
|
||||
// OptionFuResult(env.puzzle.api.puzzle find id) { puzzle =>
|
||||
// lila.mon.puzzle.round.attempt(puzzle.mate, ctx.isAuth, "new")
|
||||
// env.puzzle.forms.round
|
||||
// .bindFromRequest()
|
||||
// .fold(
|
||||
// jsonFormError,
|
||||
// resultInt =>
|
||||
// ctx.me match {
|
||||
// case Some(me) =>
|
||||
// for {
|
||||
// isStudent <- env.clas.api.student.isStudent(me.id)
|
||||
// (round, mode) <- env.puzzle.finisher(
|
||||
// puzzle = puzzle,
|
||||
// user = me,
|
||||
// result = Result(resultInt == 1),
|
||||
// mobile = lila.api.Mobile.Api.requested(ctx.req),
|
||||
// isStudent = isStudent
|
||||
// )
|
||||
// me2 <- if (mode.rated) env.user.repo byId me.id map (_ | me) else fuccess(me)
|
||||
// infos <- env.puzzle userInfos me2
|
||||
// voted <- ctx.me.?? { env.puzzle.api.vote.value(puzzle.id, _) }
|
||||
// } yield Ok(
|
||||
// Json.obj(
|
||||
// "user" -> lila.puzzle.JsonView.infos(isOldMobile = false)(infos),
|
||||
// "round" -> lila.puzzle.JsonView.round(round),
|
||||
// "voted" -> voted
|
||||
// )
|
||||
// )
|
||||
// case None =>
|
||||
// env.puzzle.finisher.incPuzzleAttempts(puzzle)
|
||||
// Ok(Json.obj("user" -> false)).fuccess
|
||||
// }
|
||||
// ) map (_ as JSON)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
def vote(id: PuzzleId) =
|
||||
def vote(id: Puz.Id) =
|
||||
AuthBody { implicit ctx => me =>
|
||||
NoBot {
|
||||
implicit val req = ctx.body
|
||||
env.puzzle.forms.vote
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
jsonFormError,
|
||||
vote =>
|
||||
env.puzzle.api.vote.find(id, me) flatMap { v =>
|
||||
env.puzzle.api.vote.update(id, me, v, vote == 1)
|
||||
} map { case (p, a) =>
|
||||
if (vote == 1) lila.mon.puzzle.vote.up.increment()
|
||||
else lila.mon.puzzle.vote.down.increment()
|
||||
Ok(Json.arr(a.value, p.vote.sum))
|
||||
}
|
||||
) map (_ as JSON)
|
||||
???
|
||||
// implicit val req = ctx.body
|
||||
// env.puzzle.forms.vote
|
||||
// .bindFromRequest()
|
||||
// .fold(
|
||||
// jsonFormError,
|
||||
// vote =>
|
||||
// env.puzzle.api.vote.find(id, me) flatMap { v =>
|
||||
// env.puzzle.api.vote.update(id, me, v, vote == 1)
|
||||
// } map { case (p, a) =>
|
||||
// if (vote == 1) lila.mon.puzzle.vote.up.increment()
|
||||
// else lila.mon.puzzle.vote.down.increment()
|
||||
// Ok(Json.arr(a.value, p.vote.sum))
|
||||
// }
|
||||
// ) map (_ as JSON)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,42 +159,43 @@ final class Puzzle(
|
|||
Auth { implicit ctx => me =>
|
||||
negotiate(
|
||||
html = notFound,
|
||||
api = _ =>
|
||||
for {
|
||||
puzzles <- env.puzzle.batch.select(
|
||||
me,
|
||||
nb = getInt("nb") getOrElse 50 atLeast 1 atMost 100,
|
||||
after = getInt("after")
|
||||
)
|
||||
userInfo <- env.puzzle userInfos me
|
||||
json <- env.puzzle.jsonView.batch(puzzles, userInfo)
|
||||
} yield Ok(json) as JSON
|
||||
api = _ => ???
|
||||
// for {
|
||||
// puzzles <- env.puzzle.batch.select(
|
||||
// me,
|
||||
// nb = getInt("nb") getOrElse 50 atLeast 1 atMost 100,
|
||||
// after = getInt("after")
|
||||
// )
|
||||
// userInfo <- env.puzzle userInfos me
|
||||
// json <- env.puzzle.jsonView.batch(puzzles, userInfo)
|
||||
// } yield Ok(json) as JSON
|
||||
)
|
||||
}
|
||||
|
||||
/* Mobile API: tell the server about puzzles solved while offline */
|
||||
def batchSolve =
|
||||
AuthBody(parse.json) { implicit ctx => me =>
|
||||
import lila.puzzle.PuzzleBatch._
|
||||
ctx.body.body
|
||||
.validate[SolveData]
|
||||
.fold(
|
||||
err => BadRequest(err.toString).fuccess,
|
||||
data =>
|
||||
negotiate(
|
||||
html = notFound,
|
||||
api = _ =>
|
||||
for {
|
||||
_ <- env.puzzle.batch.solve(me, data)
|
||||
me2 <- env.user.repo byId me.id map (_ | me)
|
||||
infos <- env.puzzle userInfos me2
|
||||
} yield Ok(
|
||||
Json.obj(
|
||||
"user" -> lila.puzzle.JsonView.infos(isOldMobile = false)(infos)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
???
|
||||
// import lila.puzzle.PuzzleBatch._
|
||||
// ctx.body.body
|
||||
// .validate[SolveData]
|
||||
// .fold(
|
||||
// err => BadRequest(err.toString).fuccess,
|
||||
// data =>
|
||||
// negotiate(
|
||||
// html = notFound,
|
||||
// api = _ =>
|
||||
// for {
|
||||
// _ <- env.puzzle.batch.solve(me, data)
|
||||
// me2 <- env.user.repo byId me.id map (_ | me)
|
||||
// infos <- env.puzzle userInfos me2
|
||||
// } yield Ok(
|
||||
// Json.obj(
|
||||
// "user" -> lila.puzzle.JsonView.infos(isOldMobile = false)(infos)
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
}
|
||||
|
||||
def frame =
|
||||
|
|
|
@ -29,15 +29,15 @@ object show {
|
|||
chessground = false,
|
||||
openGraph = lila.app.ui
|
||||
.OpenGraph(
|
||||
image = cdnUrl(routes.Export.puzzleThumbnail(puzzle.id).url).some,
|
||||
image = cdnUrl(routes.Export.puzzleThumbnail(puzzle.id.value).url).some,
|
||||
title = s"Chess tactic #${puzzle.id} - ${puzzle.color.name.capitalize} to play",
|
||||
url = s"$netBaseUrl${routes.Puzzle.show(puzzle.id).url}",
|
||||
url = s"$netBaseUrl${routes.Puzzle.show(puzzle.id.value).url}",
|
||||
description = s"Lichess tactic trainer: " + puzzle.color
|
||||
.fold(
|
||||
trans.findTheBestMoveForWhite,
|
||||
trans.findTheBestMoveForBlack
|
||||
)
|
||||
.txt() + s" Played by ${puzzle.attempts} players."
|
||||
.txt() + s" Played by ${puzzle.plays} players."
|
||||
)
|
||||
.some,
|
||||
zoomable = true
|
||||
|
|
|
@ -130,9 +130,6 @@ puzzle {
|
|||
head = puzzle_head
|
||||
}
|
||||
api.token = ${api.token}
|
||||
selector {
|
||||
puzzle_id_min = 61053 # puzzle 61052 is bad
|
||||
}
|
||||
animation.duration = ${chessground.animation.duration}
|
||||
}
|
||||
coordinate {
|
||||
|
|
11
conf/routes
11
conf/routes
|
@ -81,14 +81,13 @@ GET /training controllers.Puzzle.home
|
|||
GET /training/new controllers.Puzzle.newPuzzle
|
||||
GET /training/daily controllers.Puzzle.daily
|
||||
GET /training/frame controllers.Puzzle.frame
|
||||
GET /training/export/png/:id.png controllers.Export.legacyPuzzleThumbnail(id: Int)
|
||||
GET /training/export/gif/thumbnail/:id.gif controllers.Export.puzzleThumbnail(id: Int)
|
||||
GET /training/export/gif/thumbnail/:id.gif controllers.Export.puzzleThumbnail(id: String)
|
||||
GET /training/batch controllers.Puzzle.batchSelect
|
||||
POST /training/batch controllers.Puzzle.batchSolve
|
||||
GET /training/:id controllers.Puzzle.show(id: Int)
|
||||
GET /training/:id/load controllers.Puzzle.load(id: Int)
|
||||
POST /training/:id/vote controllers.Puzzle.vote(id: Int)
|
||||
POST /training/:id/round controllers.Puzzle.round(id: Int)
|
||||
GET /training/:id controllers.Puzzle.show(id: String)
|
||||
GET /training/:id/load controllers.Puzzle.load(id: String)
|
||||
POST /training/:id/vote controllers.Puzzle.vote(id: String)
|
||||
POST /training/:id/round controllers.Puzzle.round(id: String)
|
||||
# new UI
|
||||
POST /training/:id/round2 controllers.Puzzle.round2(id: Int)
|
||||
# mobile app BC
|
||||
|
|
|
@ -3,6 +3,9 @@ package lila.puzzle
|
|||
import chess.format.{ FEN, Uci }
|
||||
import reactivemongo.api.bson._
|
||||
|
||||
import scala.util.Success
|
||||
|
||||
import lila.db.BSON
|
||||
import lila.db.dsl._
|
||||
import lila.game.Game
|
||||
import lila.rating.Glicko
|
||||
|
@ -33,4 +36,16 @@ private[puzzle] object BsonHandlers {
|
|||
plays = plays
|
||||
)
|
||||
}
|
||||
|
||||
implicit val RoundIdHandler = tryHandler[Round.Id](
|
||||
{ case BSONString(v) =>
|
||||
v split Round.idSep match {
|
||||
case Array(userId, puzzleId) => Success(Round.Id(userId, Puzzle.Id(puzzleId)))
|
||||
case _ => handlerBadValue(s"Invalid puzzle round id $v")
|
||||
}
|
||||
},
|
||||
id => BSONString(id.toString)
|
||||
)
|
||||
|
||||
implicit val RoundBSONHandler = Macros.handler[Round]
|
||||
}
|
||||
|
|
|
@ -16,8 +16,7 @@ private class PuzzleConfig(
|
|||
@ConfigName("collection.vote") val voteColl: CollName,
|
||||
@ConfigName("collection.head") val headColl: CollName,
|
||||
@ConfigName("api.token") val apiToken: Secret,
|
||||
@ConfigName("animation.duration") val animationDuration: FiniteDuration,
|
||||
@ConfigName("selector.puzzle_id_min") val puzzleIdMin: Int
|
||||
@ConfigName("animation.duration") val animationDuration: FiniteDuration
|
||||
)
|
||||
|
||||
case class RoundRepo(coll: lila.db.AsyncColl)
|
||||
|
@ -45,9 +44,7 @@ final class Env(
|
|||
private def voteColl = db(config.voteColl)
|
||||
private def headColl = db(config.headColl)
|
||||
|
||||
private lazy val gameJson = wire[GameJson]
|
||||
|
||||
val idMin = config.puzzleIdMin
|
||||
private lazy val gameJson: GameJson = wire[GameJson]
|
||||
|
||||
lazy val jsonView = wire[JsonView]
|
||||
|
||||
|
@ -68,24 +65,6 @@ final class Env(
|
|||
puzzleColl = puzzleColl
|
||||
)
|
||||
|
||||
lazy val selector = new Selector(
|
||||
puzzleColl = puzzleColl,
|
||||
api = api,
|
||||
puzzleIdMin = config.puzzleIdMin
|
||||
)
|
||||
|
||||
lazy val batch = new PuzzleBatch(
|
||||
puzzleColl = puzzleColl,
|
||||
api = api,
|
||||
finisher = finisher,
|
||||
puzzleIdMin = config.puzzleIdMin
|
||||
)
|
||||
|
||||
lazy val userInfos = new UserInfosApi(
|
||||
roundColl = roundColl,
|
||||
currentPuzzleId = api.head.currentPuzzleId
|
||||
)
|
||||
|
||||
lazy val forms = PuzzleForm
|
||||
|
||||
lazy val daily = new Daily(
|
||||
|
@ -101,10 +80,8 @@ final class Env(
|
|||
|
||||
def cli =
|
||||
new lila.common.Cli {
|
||||
def process = { case "puzzle" :: "disable" :: id :: Nil =>
|
||||
id.toIntOption ?? { id =>
|
||||
api.puzzle disable id inject "Done"
|
||||
}
|
||||
def process = { case "puzzle" :: "delete" :: id :: Nil =>
|
||||
api.puzzle delete Puzzle.Id(id) inject "Done"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,93 +23,62 @@ final private[puzzle] class Finisher(
|
|||
result: Result,
|
||||
mobile: Boolean,
|
||||
isStudent: Boolean
|
||||
): Fu[(Round, Mode)] = {
|
||||
val formerUserRating = user.perfs.puzzle.intRating
|
||||
api.head.find(user) flatMap {
|
||||
case Some(PuzzleHead(_, Some(c), _)) if c == puzzle.id || mobile =>
|
||||
api.head.solved(user, puzzle.id) >> {
|
||||
val userRating = user.perfs.puzzle.toRating
|
||||
val puzzleRating = puzzle.perf.toRating
|
||||
updateRatings(userRating, puzzleRating, result.glicko)
|
||||
val date = DateTime.now
|
||||
val puzzlePerf =
|
||||
puzzle.perf.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id} user")(puzzleRating)
|
||||
val userPerf =
|
||||
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, date)
|
||||
val round = new Round(
|
||||
id = Round.Id(user.id, puzzle.id),
|
||||
date = date,
|
||||
result = result,
|
||||
rating = formerUserRating,
|
||||
ratingDiff = userPerf.intRating - formerUserRating
|
||||
)
|
||||
historyApi.addPuzzle(user = user, completedAt = date, perf = userPerf)
|
||||
(api.round upsert round) >> {
|
||||
isStudent ?? api.round.addDenormalizedUser(round, user)
|
||||
} >> {
|
||||
puzzleColl {
|
||||
_.update.one(
|
||||
$id(puzzle.id),
|
||||
$inc(Puzzle.BSONFields.attempts -> $int(1)) ++
|
||||
$set(Puzzle.BSONFields.perf -> PuzzlePerf.puzzlePerfBSONHandler.write(puzzlePerf))
|
||||
)
|
||||
} zip userRepo.setPerf(user.id, PerfType.Puzzle, userPerf)
|
||||
} inject {
|
||||
Bus.publish(
|
||||
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
|
||||
"finishPuzzle"
|
||||
)
|
||||
round -> Mode.Rated
|
||||
}
|
||||
}
|
||||
case _ =>
|
||||
incPuzzleAttempts(puzzle) inject new Round(
|
||||
id = Round.Id(user.id, puzzle.id),
|
||||
date = DateTime.now,
|
||||
result = result,
|
||||
rating = formerUserRating,
|
||||
ratingDiff = 0
|
||||
) -> Mode.Casual
|
||||
}
|
||||
}
|
||||
|
||||
/* offline solving from the mobile API
|
||||
* avoid exploits by not updating the puzzle rating,
|
||||
* only the user rating (we don't care about that one).
|
||||
* Returns the user with updated puzzle rating */
|
||||
def ratedUntrusted(puzzle: Puzzle, user: User, result: Result): Fu[User] = {
|
||||
val formerUserRating = user.perfs.puzzle.intRating
|
||||
val userRating = user.perfs.puzzle.toRating
|
||||
val puzzleRating = puzzle.perf.toRating
|
||||
updateRatings(userRating, puzzleRating, result.glicko)
|
||||
val date = DateTime.now
|
||||
val userPerf =
|
||||
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, date)
|
||||
val a = new Round(
|
||||
id = Round.Id(user.id, puzzle.id),
|
||||
date = date,
|
||||
result = result,
|
||||
rating = formerUserRating,
|
||||
ratingDiff = userPerf.intRating - formerUserRating
|
||||
)
|
||||
(api.round add a) >>
|
||||
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) >>-
|
||||
Bus.publish(
|
||||
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
|
||||
"finishPuzzle"
|
||||
) inject
|
||||
user.copy(perfs = user.perfs.copy(puzzle = userPerf))
|
||||
} recover lila.db.recoverDuplicateKey { _ =>
|
||||
// logger.info(s"ratedUntrusted ${user.id} ${puzzle.id} duplicate round")
|
||||
user // has already been solved!
|
||||
}
|
||||
): Fu[(Round, Mode)] = ???
|
||||
// val formerUserRating = user.perfs.puzzle.intRating
|
||||
// api.head.find(user) flatMap {
|
||||
// case Some(PuzzleHead(_, Some(c), _)) if c == puzzle.id || mobile =>
|
||||
// api.head.solved(user, puzzle.id) >> {
|
||||
// val userRating = user.perfs.puzzle.toRating
|
||||
// val puzzleRating = puzzle.perf.toRating
|
||||
// updateRatings(userRating, puzzleRating, result.glicko)
|
||||
// val date = DateTime.now
|
||||
// val puzzlePerf =
|
||||
// puzzle.perf.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id} user")(puzzleRating)
|
||||
// val userPerf =
|
||||
// user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, date)
|
||||
// val round = new Round(
|
||||
// id = Round.Id(user.id, puzzle.id),
|
||||
// date = date,
|
||||
// result = result,
|
||||
// rating = formerUserRating,
|
||||
// ratingDiff = userPerf.intRating - formerUserRating
|
||||
// )
|
||||
// historyApi.addPuzzle(user = user, completedAt = date, perf = userPerf)
|
||||
// (api.round upsert round) >> {
|
||||
// isStudent ?? api.round.addDenormalizedUser(round, user)
|
||||
// } >> {
|
||||
// puzzleColl {
|
||||
// _.update.one(
|
||||
// $id(puzzle.id),
|
||||
// $inc(Puzzle.BSONFields.attempts -> $int(1)) ++
|
||||
// $set(Puzzle.BSONFields.perf -> PuzzlePerf.puzzlePerfBSONHandler.write(puzzlePerf))
|
||||
// )
|
||||
// } zip userRepo.setPerf(user.id, PerfType.Puzzle, userPerf)
|
||||
// } inject {
|
||||
// Bus.publish(
|
||||
// Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
|
||||
// "finishPuzzle"
|
||||
// )
|
||||
// round -> Mode.Rated
|
||||
// }
|
||||
// }
|
||||
// case _ =>
|
||||
// incPuzzleAttempts(puzzle) inject new Round(
|
||||
// id = Round.Id(user.id, puzzle.id),
|
||||
// date = DateTime.now,
|
||||
// result = result,
|
||||
// rating = formerUserRating,
|
||||
// ratingDiff = 0
|
||||
// ) -> Mode.Casual
|
||||
// }
|
||||
// }
|
||||
|
||||
private val VOLATILITY = Glicko.default.volatility
|
||||
private val TAU = 0.75d
|
||||
private val system = new RatingCalculator(VOLATILITY, TAU)
|
||||
|
||||
def incPuzzleAttempts(puzzle: Puzzle): Funit =
|
||||
puzzleColl.map(_.incFieldUnchecked($id(puzzle.id), Puzzle.BSONFields.attempts))
|
||||
def incPuzzlePlays(puzzle: Puzzle): Funit =
|
||||
puzzleColl.map(_.incFieldUnchecked($id(puzzle.id.value), Puzzle.BSONFields.plays))
|
||||
|
||||
private def updateRatings(u1: Rating, u2: Rating, result: Glicko.Result): Unit = {
|
||||
val results = new RatingPeriodResults()
|
||||
|
|
|
@ -6,6 +6,7 @@ import lila.common.Json._
|
|||
import lila.game.GameRepo
|
||||
import lila.tree
|
||||
import lila.tree.Node.defaultNodeJsonWriter
|
||||
import lila.user.User
|
||||
|
||||
final class JsonView(
|
||||
gameJson: GameJson,
|
||||
|
@ -13,45 +14,27 @@ final class JsonView(
|
|||
animationDuration: scala.concurrent.duration.Duration
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
import JsonView._
|
||||
|
||||
def apply(
|
||||
puzzle: Puzzle,
|
||||
userInfos: Option[UserInfos],
|
||||
mode: String,
|
||||
user: Option[User],
|
||||
mobileApi: Option[lila.common.ApiVersion],
|
||||
round: Option[Round] = None,
|
||||
voted: Option[Boolean]
|
||||
round: Option[Round] = None
|
||||
): Fu[JsObject] = {
|
||||
val isOldMobile = mobileApi.exists(_.value < 3)
|
||||
val isMobile = mobileApi.isDefined
|
||||
(!isOldMobile ?? gameJson(
|
||||
gameJson(
|
||||
gameId = puzzle.gameId,
|
||||
plies = puzzle.initialPly,
|
||||
onlyLast = isMobile
|
||||
) dmap some) map { gameJson =>
|
||||
onlyLast = mobileApi.isDefined
|
||||
) map { gameJson =>
|
||||
Json
|
||||
.obj(
|
||||
"game" -> gameJson,
|
||||
"puzzle" -> puzzleJson(puzzle, isOldMobile),
|
||||
"mode" -> mode,
|
||||
"attempt" -> round.ifTrue(isOldMobile).map { r =>
|
||||
Json.obj(
|
||||
"userRatingDiff" -> r.ratingDiff,
|
||||
"win" -> r.result.win,
|
||||
"seconds" -> "a few" // lol we don't have the value anymore
|
||||
)
|
||||
},
|
||||
"voted" -> voted,
|
||||
"user" -> userInfos.map(JsonView.infos(isOldMobile)),
|
||||
"difficulty" -> isOldMobile.option {
|
||||
Json.obj(
|
||||
"choices" -> Json.arr(
|
||||
Json.arr(2, "Normal")
|
||||
),
|
||||
"current" -> 2
|
||||
)
|
||||
}
|
||||
"puzzle" -> puzzleJson(puzzle)
|
||||
)
|
||||
.noNull
|
||||
.add("user" -> user.map { u =>
|
||||
Json.obj("rating" -> u.perfs.puzzle.intRating)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,88 +53,22 @@ final class JsonView(
|
|||
"is3d" -> p.is3d
|
||||
)
|
||||
|
||||
def batch(puzzles: List[Puzzle], userInfos: UserInfos): Fu[JsObject] =
|
||||
for {
|
||||
games <- gameRepo.gameOptionsFromSecondary(puzzles.map(_.gameId))
|
||||
jsons <- (puzzles zip games).collect { case (puzzle, Some(game)) =>
|
||||
gameJson.noCache(game, puzzle.initialPly, onlyLast = true) map { gameJson =>
|
||||
Json.obj(
|
||||
"game" -> gameJson,
|
||||
"puzzle" -> puzzleJson(puzzle, isOldMobile = false)
|
||||
)
|
||||
}
|
||||
}.sequenceFu
|
||||
} yield Json.obj(
|
||||
"user" -> JsonView.infos(isOldMobile = false)(userInfos),
|
||||
"puzzles" -> jsons
|
||||
)
|
||||
|
||||
private def puzzleJson(puzzle: Puzzle, isOldMobile: Boolean): JsObject =
|
||||
private def puzzleJson(puzzle: Puzzle): JsObject =
|
||||
Json
|
||||
.obj(
|
||||
"id" -> puzzle.id,
|
||||
"rating" -> puzzle.perf.intRating,
|
||||
"attempts" -> puzzle.attempts,
|
||||
"rating" -> puzzle.glicko.intRating,
|
||||
"plays" -> puzzle.plays,
|
||||
"fen" -> puzzle.fen,
|
||||
"color" -> puzzle.color.name,
|
||||
"initialPly" -> puzzle.initialPly,
|
||||
"gameId" -> puzzle.gameId,
|
||||
"lines" -> lila.puzzle.Line.toJson(puzzle.lines),
|
||||
"vote" -> puzzle.vote.sum
|
||||
"line" -> puzzle.line.toList.map(_.uci).mkString(" "),
|
||||
"vote" -> puzzle.vote
|
||||
)
|
||||
.add("initialMove" -> isOldMobile.option(puzzle.initialMove.uci))
|
||||
.add("branch" -> (!isOldMobile).??(makeBranch(puzzle)).map(defaultNodeJsonWriter.writes))
|
||||
.add("enabled" -> puzzle.enabled)
|
||||
|
||||
private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = {
|
||||
import chess.format._
|
||||
val fullSolution: List[Uci.Move] = (Line solution puzzle.lines).map { uci =>
|
||||
Uci.Move(uci) err s"Invalid puzzle solution UCI $uci"
|
||||
}
|
||||
val solution =
|
||||
if (fullSolution.isEmpty) {
|
||||
logger.warn(s"Puzzle ${puzzle.id} has an empty solution from ${puzzle.lines}")
|
||||
fullSolution
|
||||
} else if (fullSolution.size % 2 == 0) fullSolution.init
|
||||
else fullSolution
|
||||
val init = chess.Game(none, puzzle.fenAfterInitialMove).withTurns(puzzle.initialPly)
|
||||
val (_, branchList) = solution.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) {
|
||||
case ((prev, branches), uci) =>
|
||||
val (game, move) =
|
||||
prev(uci.orig, uci.dest, uci.promotion).fold(err => sys error s"puzzle ${puzzle.id} $err", identity)
|
||||
val branch = tree.Branch(
|
||||
id = UciCharPair(move.toUci),
|
||||
ply = game.turns,
|
||||
move = Uci.WithSan(move.toUci, game.pgnMoves.last),
|
||||
fen = chess.format.Forsyth >> game,
|
||||
check = game.situation.check,
|
||||
crazyData = none
|
||||
)
|
||||
(game, branch :: branches)
|
||||
}
|
||||
branchList.foldLeft[Option[tree.Branch]](None) {
|
||||
case (None, branch) => branch.some
|
||||
case (Some(child), branch) => Some(branch addChild child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object JsonView {
|
||||
|
||||
def infos(isOldMobile: Boolean)(i: UserInfos): JsObject =
|
||||
Json
|
||||
.obj(
|
||||
"rating" -> i.user.perfs.puzzle.intRating,
|
||||
"history" -> isOldMobile.option(i.history.map(_.rating)), // for mobile BC
|
||||
"recent" -> i.history.map { r =>
|
||||
Json.arr(r.id.puzzleId, r.ratingDiff, r.rating)
|
||||
}
|
||||
)
|
||||
.noNull
|
||||
|
||||
def round(r: Round): JsObject =
|
||||
Json.obj(
|
||||
"ratingDiff" -> r.ratingDiff,
|
||||
"win" -> r.result.win
|
||||
)
|
||||
implicit val puzzleIdWrites: Writes[Puzzle.Id] = stringIsoWriter(Puzzle.idIso)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package lila.puzzle
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import chess.Color
|
||||
import chess.format.{ FEN, Forsyth, Uci }
|
||||
import scala.util.{ Success, Try }
|
||||
|
||||
import lila.rating.Glicko
|
||||
|
||||
|
|
|
@ -21,12 +21,13 @@ final class PuzzleActivity(
|
|||
) {
|
||||
|
||||
import PuzzleActivity._
|
||||
import Round.RoundBSONHandler
|
||||
import BsonHandlers._
|
||||
import JsonView._
|
||||
|
||||
def stream(config: Config): Source[String, _] =
|
||||
Source futureSource {
|
||||
roundColl.map {
|
||||
_.find($doc("_id" $startsWith config.user.id))
|
||||
_.find($doc("_id" $startsWith s"${config.user.id}${Round.idSep}"))
|
||||
.sort($sort desc "_id")
|
||||
.batchSize(config.perSecond.value)
|
||||
.cursor[Round](ReadPreference.secondaryPreferred)
|
||||
|
@ -44,7 +45,7 @@ final class PuzzleActivity(
|
|||
|
||||
private def enrich(rounds: Seq[Round]): Fu[Seq[JsObject]] =
|
||||
puzzleColl {
|
||||
_.primitiveMap[Int, Double](
|
||||
_.primitiveMap[Puzzle.Id, Double](
|
||||
ids = rounds.map(_.id.puzzleId),
|
||||
field = "perf.gl.r",
|
||||
fieldExtractor = obj =>
|
||||
|
@ -59,8 +60,7 @@ final class PuzzleActivity(
|
|||
Json.obj(
|
||||
"id" -> round.id.puzzleId,
|
||||
"date" -> round.date,
|
||||
"rating" -> round.rating,
|
||||
"ratingDiff" -> round.ratingDiff,
|
||||
"win" -> round.win,
|
||||
"puzzleRating" -> puzzleRating.toInt
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,121 +15,101 @@ final private[puzzle] class PuzzleApi(
|
|||
cacheApi: lila.memo.CacheApi
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
import Puzzle.puzzleBSONHandler
|
||||
import Puzzle.BSONFields._
|
||||
import BsonHandlers._
|
||||
|
||||
object puzzle {
|
||||
|
||||
def find(id: PuzzleId): Fu[Option[Puzzle]] =
|
||||
puzzleColl(_.find($doc(F.id -> id)).one[Puzzle])
|
||||
def find(id: Puzzle.Id): Fu[Option[Puzzle]] =
|
||||
puzzleColl(_.byId[Puzzle](id.value))
|
||||
|
||||
def findMany(ids: List[PuzzleId]): Fu[List[Option[Puzzle]]] =
|
||||
puzzleColl(_.optionsByOrderedIds[Puzzle, PuzzleId](ids)(_.id))
|
||||
|
||||
def latest(nb: Int): Fu[List[Puzzle]] =
|
||||
puzzleColl {
|
||||
_.find($empty)
|
||||
.sort($doc(F.date -> -1))
|
||||
.cursor[Puzzle]()
|
||||
.list(nb)
|
||||
}
|
||||
|
||||
val cachedLastId = cacheApi.unit[Int] {
|
||||
_.refreshAfterWrite(1 day)
|
||||
.buildAsyncFuture { _ =>
|
||||
puzzleColl(lila.db.Util.findNextId) dmap (_ - 1)
|
||||
}
|
||||
}
|
||||
|
||||
def disable(id: PuzzleId): Funit =
|
||||
puzzleColl {
|
||||
_.update
|
||||
.one(
|
||||
$id(id),
|
||||
$doc("$set" -> $doc(F.vote -> AggregateVote.disable))
|
||||
)
|
||||
.void
|
||||
}
|
||||
def delete(id: Puzzle.Id): Funit =
|
||||
puzzleColl(_.delete.one($id(id.value))).void
|
||||
}
|
||||
|
||||
object round {
|
||||
|
||||
def add(a: Round) = roundColl(_.insert.one(a))
|
||||
|
||||
def upsert(a: Round) = roundColl(_.update.one($id(a.id), a, upsert = true))
|
||||
|
||||
def addDenormalizedUser(a: Round, user: User) = roundColl(_.updateField($id(a.id), "u", user.id).void)
|
||||
|
||||
def reset(user: User) =
|
||||
roundColl {
|
||||
_.delete.one(
|
||||
$doc(
|
||||
Round.BSONFields.id $startsWith s"${user.id}:"
|
||||
)
|
||||
)
|
||||
}
|
||||
def find(user: User, puzzle: Puzzle): Fu[Option[Round]] =
|
||||
roundColl(_.byId[Round](Round.Id(user.id, puzzle.id).toString))
|
||||
}
|
||||
|
||||
object vote {
|
||||
// def add(a: Round) = roundColl(_.insert.one(a))
|
||||
|
||||
def value(id: PuzzleId, user: User): Fu[Option[Boolean]] =
|
||||
voteColl {
|
||||
_.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v")
|
||||
}
|
||||
// def upsert(a: Round) = roundColl(_.update.one($id(a.id), a, upsert = true))
|
||||
|
||||
def find(id: PuzzleId, user: User): Fu[Option[Vote]] =
|
||||
voteColl {
|
||||
_.byId[Vote](Vote.makeId(id, user.id))
|
||||
}
|
||||
// def addDenormalizedUser(a: Round, user: User) = roundColl(_.updateField($id(a.id), "u", user.id).void)
|
||||
|
||||
def update(id: PuzzleId, user: User, v1: Option[Vote], v: Boolean): Fu[(Puzzle, Vote)] =
|
||||
puzzle find id orFail s"Can't vote for non existing puzzle $id" flatMap { p1 =>
|
||||
val (p2, v2) = v1 match {
|
||||
case Some(from) =>
|
||||
(
|
||||
(p1 withVote (_.change(from.value, v))),
|
||||
from.copy(v = v)
|
||||
)
|
||||
case None =>
|
||||
(
|
||||
(p1 withVote (_ add v)),
|
||||
Vote(Vote.makeId(id, user.id), v)
|
||||
)
|
||||
}
|
||||
voteColl {
|
||||
_.update
|
||||
.one(
|
||||
$id(v2.id),
|
||||
$set("v" -> v),
|
||||
upsert = true
|
||||
)
|
||||
.void
|
||||
.recover(lila.db.recoverDuplicateKey { _ => () })
|
||||
} zip
|
||||
puzzleColl {
|
||||
_.update
|
||||
.one($id(p2.id), $set(F.vote -> p2.vote))
|
||||
} inject (p2 -> v2)
|
||||
}
|
||||
}
|
||||
// def reset(user: User) =
|
||||
// roundColl {
|
||||
// _.delete.one(
|
||||
// $doc(
|
||||
// Round.BSONFields.id $startsWith s"${user.id}:"
|
||||
// )
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
object head {
|
||||
// object vote {
|
||||
|
||||
def find(user: User): Fu[Option[PuzzleHead]] = headColl(_.byId[PuzzleHead](user.id))
|
||||
// def value(id: PuzzleId, user: User): Fu[Option[Boolean]] =
|
||||
// voteColl {
|
||||
// _.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v")
|
||||
// }
|
||||
|
||||
def set(h: PuzzleHead) = headColl(_.update.one($id(h.id), h, upsert = true).void)
|
||||
// def find(id: PuzzleId, user: User): Fu[Option[Vote]] =
|
||||
// voteColl {
|
||||
// _.byId[Vote](Vote.makeId(id, user.id))
|
||||
// }
|
||||
|
||||
def addNew(user: User, puzzleId: PuzzleId) = set(PuzzleHead(user.id, puzzleId.some, puzzleId))
|
||||
// def update(id: PuzzleId, user: User, v1: Option[Vote], v: Boolean): Fu[(Puzzle, Vote)] =
|
||||
// puzzle find id orFail s"Can't vote for non existing puzzle $id" flatMap { p1 =>
|
||||
// val (p2, v2) = v1 match {
|
||||
// case Some(from) =>
|
||||
// (
|
||||
// (p1 withVote (_.change(from.value, v))),
|
||||
// from.copy(v = v)
|
||||
// )
|
||||
// case None =>
|
||||
// (
|
||||
// (p1 withVote (_ add v)),
|
||||
// Vote(Vote.makeId(id, user.id), v)
|
||||
// )
|
||||
// }
|
||||
// voteColl {
|
||||
// _.update
|
||||
// .one(
|
||||
// $id(v2.id),
|
||||
// $set("v" -> v),
|
||||
// upsert = true
|
||||
// )
|
||||
// .void
|
||||
// .recover(lila.db.recoverDuplicateKey { _ => () })
|
||||
// } zip
|
||||
// puzzleColl {
|
||||
// _.update
|
||||
// .one($id(p2.id), $set(F.vote -> p2.vote))
|
||||
// } inject (p2 -> v2)
|
||||
// }
|
||||
// }
|
||||
|
||||
def currentPuzzleId(user: User): Fu[Option[PuzzleId]] =
|
||||
find(user) dmap2 { (h: PuzzleHead) =>
|
||||
h.current | h.last
|
||||
}
|
||||
// object head {
|
||||
|
||||
private[puzzle] def solved(user: User, id: PuzzleId): Funit =
|
||||
head find user flatMap { headOption =>
|
||||
set {
|
||||
PuzzleHead(user.id, none, headOption.fold(id)(head => id atLeast head.last))
|
||||
}
|
||||
}
|
||||
}
|
||||
// def find(user: User): Fu[Option[PuzzleHead]] = headColl(_.byId[PuzzleHead](user.id))
|
||||
|
||||
// def set(h: PuzzleHead) = headColl(_.update.one($id(h.id), h, upsert = true).void)
|
||||
|
||||
// def addNew(user: User, puzzleId: PuzzleId) = set(PuzzleHead(user.id, puzzleId.some, puzzleId))
|
||||
|
||||
// def currentPuzzleId(user: User): Fu[Option[PuzzleId]] =
|
||||
// find(user) dmap2 { (h: PuzzleHead) =>
|
||||
// h.current | h.last
|
||||
// }
|
||||
|
||||
// private[puzzle] def solved(user: User, id: PuzzleId): Funit =
|
||||
// head find user flatMap { headOption =>
|
||||
// set {
|
||||
// PuzzleHead(user.id, none, headOption.fold(id)(head => id atLeast head.last))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
package lila.puzzle
|
||||
|
||||
import lila.db.AsyncColl
|
||||
import lila.db.dsl._
|
||||
import lila.user.User
|
||||
import lila.memo.CacheApi._
|
||||
|
||||
final private[puzzle] class PuzzleBatch(
|
||||
puzzleColl: AsyncColl,
|
||||
api: PuzzleApi,
|
||||
finisher: Finisher,
|
||||
puzzleIdMin: PuzzleId
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
def solve(originalUser: User, data: PuzzleBatch.SolveData): Funit =
|
||||
for {
|
||||
puzzles <- api.puzzle findMany data.solutions.map(_.id)
|
||||
user <- lila.common.Future.fold(data.solutions zip puzzles)(originalUser) {
|
||||
case (user, (solution, Some(puzzle))) => finisher.ratedUntrusted(puzzle, user, solution.result)
|
||||
case (user, _) => fuccess(user)
|
||||
}
|
||||
_ <- data.solutions.lastOption ?? { lastSolution =>
|
||||
api.head.solved(user, lastSolution.id).void
|
||||
}
|
||||
} yield for {
|
||||
first <- puzzles.headOption.flatten
|
||||
last <- puzzles.lastOption.flatten
|
||||
} {
|
||||
if (puzzles.sizeIs > 1) logger.info(s"Batch solve ${user.id} ${puzzles.size} ${first.id}->${last.id}")
|
||||
}
|
||||
|
||||
object select {
|
||||
|
||||
import Selector._
|
||||
|
||||
def apply(user: User, nb: Int, after: Option[PuzzleId]): Fu[List[Puzzle]] = {
|
||||
api.head.find(user) flatMap {
|
||||
newPuzzlesForUser(user, _, nb, after)
|
||||
} addEffect { puzzles =>
|
||||
lila.mon.puzzle.batch.selector.count.increment(puzzles.size).unit
|
||||
}
|
||||
}.mon(_.puzzle.batch.selector.time)
|
||||
|
||||
private def newPuzzlesForUser(
|
||||
user: User,
|
||||
headOption: Option[PuzzleHead],
|
||||
nb: Int,
|
||||
after: Option[PuzzleId]
|
||||
): Fu[List[Puzzle]] = {
|
||||
val rating = user.perfs.puzzle.intRating min 2300 max 900
|
||||
val step = toleranceStepFor(rating, user.perfs.puzzle.nb)
|
||||
api.puzzle.cachedLastId.getUnit flatMap { maxId =>
|
||||
val fromId = headOption match {
|
||||
case Some(PuzzleHead(_, _, l)) if l < maxId - 500 => after.fold(l)(_ atLeast l)
|
||||
case _ => puzzleIdMin
|
||||
}
|
||||
puzzleColl { coll =>
|
||||
tryRange(
|
||||
coll = coll,
|
||||
rating = rating,
|
||||
tolerance = step,
|
||||
step = step,
|
||||
idRange = Range(fromId, fromId + nb * 50),
|
||||
nb = nb
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private def tryRange(
|
||||
coll: Coll,
|
||||
rating: Int,
|
||||
tolerance: Int,
|
||||
step: Int,
|
||||
idRange: Range,
|
||||
nb: Int
|
||||
): Fu[List[Puzzle]] =
|
||||
coll
|
||||
.find(
|
||||
rangeSelector(
|
||||
rating = rating,
|
||||
tolerance = tolerance,
|
||||
idRange = idRange
|
||||
)
|
||||
)
|
||||
.cursor[Puzzle]()
|
||||
.list(nb) flatMap {
|
||||
case res if res.sizeIs < nb && (tolerance + step) <= toleranceMax =>
|
||||
tryRange(
|
||||
coll = coll,
|
||||
rating = rating,
|
||||
tolerance = tolerance + step,
|
||||
step = step,
|
||||
idRange = Range(idRange.min, idRange.max + 100),
|
||||
nb = nb
|
||||
)
|
||||
case res => fuccess(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PuzzleBatch {
|
||||
|
||||
case class Solution(id: PuzzleId, win: Boolean) {
|
||||
def result = Result(win)
|
||||
}
|
||||
case class SolveData(solutions: List[Solution])
|
||||
|
||||
import play.api.libs.json._
|
||||
implicit private val SolutionReads = Json.reads[Solution]
|
||||
implicit val SolveDataReads = Json.reads[SolveData]
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package lila.puzzle
|
||||
|
||||
case class PuzzleHead(
|
||||
_id: lila.user.User.ID,
|
||||
current: Option[PuzzleId], // current puzzle assigned to user (rated)
|
||||
last: PuzzleId // last puzzle assigned to user
|
||||
) {
|
||||
|
||||
def id = _id
|
||||
}
|
||||
|
||||
object PuzzleHead {
|
||||
|
||||
object BSONFields {
|
||||
val id = "_id"
|
||||
val current = "current"
|
||||
val last = "last"
|
||||
}
|
||||
|
||||
import reactivemongo.api.bson._
|
||||
|
||||
implicit private[puzzle] val puzzleHeadBSONHandler = Macros.handler[PuzzleHead]
|
||||
}
|
|
@ -7,85 +7,26 @@ import lila.user.User
|
|||
case class Round(
|
||||
id: Round.Id,
|
||||
date: DateTime,
|
||||
result: Result,
|
||||
rating: Int,
|
||||
ratingDiff: Int
|
||||
) {
|
||||
|
||||
def userPostRating = rating + ratingDiff
|
||||
}
|
||||
win: Boolean,
|
||||
vote: Option[Boolean],
|
||||
// tags: List[RoundTag],
|
||||
weight: Option[Int]
|
||||
) {}
|
||||
|
||||
object Round {
|
||||
|
||||
case class Id(userId: User.ID, puzzleId: PuzzleId)
|
||||
val idSep = ':'
|
||||
|
||||
case class Id(userId: User.ID, puzzleId: Puzzle.Id) {
|
||||
|
||||
override def toString = s"${userId}$idSep${puzzleId}"
|
||||
}
|
||||
|
||||
object BSONFields {
|
||||
val id = "_id"
|
||||
val date = "a"
|
||||
val magic = "m"
|
||||
}
|
||||
|
||||
import reactivemongo.api.bson._
|
||||
import lila.db.BSON
|
||||
import lila.db.dsl._
|
||||
import scala.util.Success
|
||||
import BSON.BSONJodaDateTimeHandler
|
||||
|
||||
/* We shift the puzzle ID by -60000
|
||||
* Because the initial puzzle is 60000 and something.
|
||||
* This way the lowest puzzle ID is 00000
|
||||
* and it allows us to sort rounds by ID: userId:puzzleId
|
||||
* because all puzzle IDs have the same length */
|
||||
private val shiftValue = -60000
|
||||
def encode(puzzleId: PuzzleId) = puzzleId + shiftValue
|
||||
def decode(puzzleId: PuzzleId) = puzzleId - shiftValue
|
||||
|
||||
private val idSep = ':'
|
||||
implicit val roundIdHandler = tryHandler[Id](
|
||||
{ case BSONString(v) =>
|
||||
v split idSep match {
|
||||
case Array(userId, puzzleId) => Success(Id(userId, decode(Integer parseInt puzzleId)))
|
||||
case _ => handlerBadValue(s"Invalid puzzle round id $v")
|
||||
}
|
||||
},
|
||||
id => {
|
||||
val puzzleId = "%05d".format(encode(id.puzzleId))
|
||||
BSONString(s"${id.userId}$idSep$puzzleId")
|
||||
}
|
||||
)
|
||||
|
||||
implicit val RoundBSONHandler = new BSON[Round] {
|
||||
|
||||
import BSONFields._
|
||||
|
||||
/* `magic` field stores;
|
||||
* - win: boolean | 1 bit
|
||||
* - ratingDiff: int | 15 bits
|
||||
* - rating: int | 16 bits
|
||||
*/
|
||||
|
||||
def reads(r: BSON.Reader): Round = {
|
||||
val m = r int magic
|
||||
val win = m >>> 31 != 0
|
||||
val ratingDiff = (m << 1) >>> 17
|
||||
Round(
|
||||
id = r.get[Id](id),
|
||||
date = r.get[DateTime](date),
|
||||
result = Result(win),
|
||||
rating = m & (-1 >>> 16),
|
||||
ratingDiff = if (win) ratingDiff else -ratingDiff
|
||||
)
|
||||
}
|
||||
|
||||
def writes(w: BSON.Writer, o: Round) =
|
||||
BSONDocument(
|
||||
id -> o.id,
|
||||
date -> o.date,
|
||||
magic -> {
|
||||
(o.result.win ?? (1 << 31)) |
|
||||
(Math.abs(o.ratingDiff) << 16) |
|
||||
o.rating
|
||||
}
|
||||
)
|
||||
val id = "_id"
|
||||
val date = "d"
|
||||
val win = "w"
|
||||
val vote = "v"
|
||||
val weight = "w"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
package lila.puzzle
|
||||
|
||||
import Puzzle.{ BSONFields => F }
|
||||
import reactivemongo.api.ReadPreference
|
||||
|
||||
import lila.common.ThreadLocalRandom
|
||||
import lila.db.AsyncColl
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi._
|
||||
import lila.user.User
|
||||
|
||||
final private[puzzle] class Selector(
|
||||
puzzleColl: AsyncColl,
|
||||
api: PuzzleApi,
|
||||
puzzleIdMin: Int
|
||||
)(implicit ec: scala.concurrent.ExecutionContext) {
|
||||
|
||||
import Selector._
|
||||
|
||||
private lazy val anonIdsCache: Fu[Vector[Int]] =
|
||||
puzzleColl { // this query precisely matches a mongodb partial index
|
||||
_.find($doc(F.voteNb $gte 100), $id(true).some)
|
||||
.sort($sort desc F.voteRatio)
|
||||
.cursor[Bdoc](ReadPreference.secondaryPreferred)
|
||||
.vector(anonPuzzles)
|
||||
.map(_.flatMap(_ int "_id"))
|
||||
}
|
||||
|
||||
def apply(me: Option[User]): Fu[Puzzle] = {
|
||||
me match {
|
||||
// anon
|
||||
case None =>
|
||||
anonIdsCache flatMap { ids =>
|
||||
puzzleColl {
|
||||
_.byId[Puzzle, Int](ids(ThreadLocalRandom nextInt ids.size))
|
||||
}
|
||||
}
|
||||
// user
|
||||
case Some(user) =>
|
||||
api.head find user flatMap {
|
||||
// new player
|
||||
case None =>
|
||||
api.puzzle find puzzleIdMin flatMap { puzzleOption =>
|
||||
puzzleOption ?? { p =>
|
||||
api.head.addNew(user, p.id)
|
||||
} inject puzzleOption
|
||||
}
|
||||
// current puzzle
|
||||
case Some(PuzzleHead(_, Some(current), _)) => api.puzzle find current
|
||||
// find new based on last
|
||||
case Some(PuzzleHead(_, _, last)) =>
|
||||
newPuzzleForUser(user, last) flatMap {
|
||||
// user played all puzzles. Reset rounds and start anew.
|
||||
case None =>
|
||||
api.puzzle.cachedLastId.getUnit flatMap { maxId =>
|
||||
(last > maxId - 1000) ?? {
|
||||
api.round.reset(user) >> api.puzzle.find(puzzleIdMin)
|
||||
}
|
||||
}
|
||||
case Some(found) => fuccess(found.some)
|
||||
} flatMap { puzzleOption =>
|
||||
puzzleOption ?? { p =>
|
||||
api.head.addNew(user, p.id)
|
||||
} inject puzzleOption
|
||||
}
|
||||
}
|
||||
}
|
||||
}.mon(_.puzzle.selector.time) orFailWith NoPuzzlesAvailableException addEffect { puzzle =>
|
||||
if (puzzle.vote.sum < -1000)
|
||||
logger.info(s"Select #${puzzle.id} vote.sum: ${puzzle.vote.sum} for ${me.fold("Anon")(_.username)} (${me
|
||||
.fold("?")(_.perfs.puzzle.intRating.toString)})")
|
||||
else
|
||||
lila.mon.puzzle.selector.vote.record(1000 + puzzle.vote.sum).unit
|
||||
}
|
||||
|
||||
private def newPuzzleForUser(user: User, lastPlayed: PuzzleId): Fu[Option[Puzzle]] = {
|
||||
val rating = user.perfs.puzzle.intRating atMost 2300 atLeast 900
|
||||
val step = toleranceStepFor(rating, user.perfs.puzzle.nb)
|
||||
puzzleColl { coll =>
|
||||
tryRange(
|
||||
coll = coll,
|
||||
rating = rating,
|
||||
tolerance = step,
|
||||
step = step,
|
||||
idRange = Range(lastPlayed, lastPlayed + 200)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private def tryRange(
|
||||
coll: Coll,
|
||||
rating: Int,
|
||||
tolerance: Int,
|
||||
step: Int,
|
||||
idRange: Range
|
||||
): Fu[Option[Puzzle]] =
|
||||
coll
|
||||
.find(
|
||||
rangeSelector(
|
||||
rating = rating,
|
||||
tolerance = tolerance,
|
||||
idRange = idRange
|
||||
)
|
||||
)
|
||||
.sort($sort asc F.id)
|
||||
.one[Puzzle] flatMap {
|
||||
case None if (tolerance + step) <= toleranceMax =>
|
||||
tryRange(coll, rating, tolerance + step, step, Range(idRange.min, idRange.max + 100))
|
||||
case res => fuccess(res)
|
||||
}
|
||||
}
|
||||
|
||||
final private object Selector {
|
||||
|
||||
case object NoPuzzlesAvailableException extends lila.base.LilaException {
|
||||
val message = "No puzzles available"
|
||||
}
|
||||
|
||||
val anonPuzzles = 8192
|
||||
|
||||
val toleranceMax = 1000
|
||||
|
||||
def toleranceStepFor(rating: Int, nbPuzzles: Int) = {
|
||||
math.abs(1500 - rating) match {
|
||||
case d if d >= 500 => 300
|
||||
case d if d >= 300 => 250
|
||||
case _ => 200
|
||||
}
|
||||
} * {
|
||||
// increase rating tolerance for puzzle blitzers,
|
||||
// so they get more puzzles to play
|
||||
if (nbPuzzles > 10000) 2
|
||||
else if (nbPuzzles > 5000) 3 / 2
|
||||
else 1
|
||||
}
|
||||
|
||||
def rangeSelector(rating: Int, tolerance: Int, idRange: Range) =
|
||||
$doc(
|
||||
F.id $gt idRange.min $lt idRange.max,
|
||||
F.rating $gt (rating - tolerance) $lt (rating + tolerance),
|
||||
$or(
|
||||
F.voteRatio $gt AggregateVote.minRatio,
|
||||
F.voteNb $lt AggregateVote.minVotes
|
||||
)
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package lila.puzzle
|
||||
|
||||
import reactivemongo.api.bson._
|
||||
|
||||
import lila.db.AsyncColl
|
||||
import lila.db.dsl._
|
||||
import lila.user.User
|
||||
|
||||
case class UserInfos(user: User, history: List[Round])
|
||||
|
||||
final class UserInfosApi(roundColl: AsyncColl, currentPuzzleId: User => Fu[Option[PuzzleId]])(implicit
|
||||
ec: scala.concurrent.ExecutionContext
|
||||
) {
|
||||
|
||||
private val historySize = 15
|
||||
private val chartSize = 15
|
||||
|
||||
def apply(user: Option[User]): Fu[Option[UserInfos]] = user ?? { apply(_) dmap (_.some) }
|
||||
|
||||
def apply(user: User): Fu[UserInfos] =
|
||||
for {
|
||||
current <- currentPuzzleId(user)
|
||||
rounds <- fetchRounds(user.id, current)
|
||||
} yield UserInfos(user, rounds)
|
||||
|
||||
private def fetchRounds(userId: User.ID, currentPuzzleId: Option[PuzzleId]): Fu[List[Round]] = {
|
||||
val idSelector = $doc("$regex" -> BSONRegex(s"^$userId:", "")) ++
|
||||
currentPuzzleId.?? { id =>
|
||||
$doc("$lte" -> s"$userId:${Round encode id}")
|
||||
}
|
||||
roundColl {
|
||||
_.find($doc(Round.BSONFields.id -> idSelector))
|
||||
.sort($sort desc Round.BSONFields.id)
|
||||
.cursor[Round]()
|
||||
.list(historySize atLeast chartSize)
|
||||
.dmap(_.reverse)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package lila.puzzle
|
||||
|
||||
case class Vote(
|
||||
_id: String, // puzzleId/userId
|
||||
v: Boolean
|
||||
) {
|
||||
|
||||
def id = _id
|
||||
|
||||
def value = v
|
||||
}
|
||||
|
||||
object Vote {
|
||||
|
||||
def makeId(puzzleId: PuzzleId, userId: String) = s"$puzzleId/$userId"
|
||||
|
||||
implicit val voteBSONHandler = reactivemongo.api.bson.Macros.handler[Vote]
|
||||
}
|
|
@ -87,8 +87,8 @@ object BuildSettings {
|
|||
"-Wdead-code",
|
||||
"-Wextra-implicit",
|
||||
// "-Wnumeric-widen",
|
||||
"-Wunused:imports",
|
||||
"-Wunused:locals",
|
||||
// "-Wunused:imports",
|
||||
// "-Wunused:locals",
|
||||
"-Wunused:patvars",
|
||||
// "-Wunused:privates", // unfortunately doesn't work with macros
|
||||
// "-Wunused:implicits",
|
||||
|
|
Loading…
Reference in New Issue