puzzle WIP

pull/7680/head
Thibault Duplessis 2020-11-28 12:04:20 +01:00
parent fa6be1c83a
commit cd5f13ab02
13 changed files with 90 additions and 107 deletions

View File

@ -15,23 +15,23 @@ final class Puzzle(
apiC: => Api
) extends LilaController(env) {
private def renderJson(puzzle: Puz, round: Option[PuzzleRound] = None)(implicit
private type ThemeOrAny = Option[PuzzleTheme.Key]
private def renderJson(puzzle: Puz, theme: ThemeOrAny, round: Option[PuzzleRound] = None)(implicit
ctx: Context
): Fu[JsObject] =
env.puzzle.jsonView(
puzzle = puzzle,
user = ctx.me,
round = round
)
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = ctx.me, round = round)
private def renderShow(puzzle: Puz)(implicit ctx: Context) =
private def renderShow(puzzle: Puz, theme: ThemeOrAny)(implicit ctx: Context) =
ctx.me.?? { env.puzzle.api.round.find(_, puzzle) } flatMap {
renderShowWithRound(puzzle, _)
renderShowWithRound(puzzle, theme, _)
}
private def renderShowWithRound(puzzle: Puz, round: Option[PuzzleRound])(implicit ctx: Context) =
private def renderShowWithRound(puzzle: Puz, theme: ThemeOrAny, round: Option[PuzzleRound])(implicit
ctx: Context
) =
ctx.me.?? { env.puzzle.api.round.find(_, puzzle) } flatMap { round =>
renderJson(puzzle, round) map { json =>
renderJson(puzzle, theme, round) map { json =>
EnableSharedArrayBuffer(
Ok(views.html.puzzle.show(puzzle, data = json, pref = env.puzzle.jsonView.pref(ctx.pref)))
)
@ -45,7 +45,7 @@ final class Puzzle(
_.map(_.id) ?? env.puzzle.api.puzzle.find
}) { puzzle =>
negotiate(
html = renderShow(puzzle),
html = renderShow(puzzle, none),
api = _ => renderJson(puzzle, none) map { Ok(_) }
) map NoCache
}
@ -56,7 +56,7 @@ final class Puzzle(
Open { implicit ctx =>
NoBot {
nextPuzzleForMe() flatMap {
renderShowWithRound(_, none)
renderShowWithRound(_, none, none)
}
}
}
@ -64,7 +64,7 @@ final class Puzzle(
def show(id: String) =
Open { implicit ctx =>
NoBot {
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id))(renderShow)
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { renderShow(_, none) }
}
}
@ -78,30 +78,18 @@ final class Puzzle(
}
}
// XHR load next play puzzle
def newPuzzle =
Open { implicit ctx =>
NoBot {
XhrOnly {
nextPuzzleForMe() flatMap { renderJson(_, none) } map { json =>
Ok(json) as JSON
}
}
}
}
private def nextPuzzleForMe(theme: Option[PuzzleTheme.Key] = None)(implicit ctx: Context): Fu[Puz] =
ctx.me match {
case _ => env.puzzle.anon.getOneFor(theme) orFail "Couldn't find a puzzle for anon!"
// case Some(me) => env.puzzle.cursor.nextPuzzleFor(me)
}
def round3(theme: String, id: String) =
def complete(themeStr: String, id: String) =
OpenBody { implicit ctx =>
NoBot {
implicit val req = ctx.body
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { puzzle =>
val theme = PuzzleTheme.find(theme)
val theme = PuzzleTheme.find(themeStr)
lila.mon.puzzle.round.attempt(ctx.isAuth, theme.fold("any")(_.key.value)).increment()
env.puzzle.forms.round
.bindFromRequest()
@ -118,17 +106,18 @@ final class Puzzle(
result = Result(resultInt == 1),
isStudent = isStudent
)
_ = env.puzzle.cursor.onComplete(round, theme.map(_.key))
next <- nextPuzzleForMe(theme.map(_.key))
nextJson <- renderJson(next, none)
} yield Ok(
Json.obj(
"perf" -> Json
.obj("rating" -> perf.intRating)
.add("provisional" -> perf.provisional),
"round" -> Json
.obj(
"win" -> round.win,
"ratingDiff" -> (perf.intRating - me.perfs.puzzle.intRating)
)
.add("vote" -> round.vote)
.add("vote" -> round.vote),
"next" -> nextJson
)
)
case None =>
@ -214,7 +203,7 @@ final class Puzzle(
case None => Redirect(routes.Puzzle.home()).fuccess
case Some(theme) =>
nextPuzzleForMe(theme.key.some) flatMap {
renderShowWithRound(_, none)
renderShowWithRound(_, theme.key.some, none)
}
}
}

View File

@ -78,7 +78,6 @@ POST /training/coordinate/color controllers.Coordinate.color
# Training - Puzzle
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/gif/thumbnail/:id.gif controllers.Export.puzzleThumbnail(id: String)

View File

@ -9,8 +9,7 @@ import lila.base.LilaTimeout
final class DuctSequencer(maxSize: Int, timeout: FiniteDuration, name: String, logging: Boolean = true)(
implicit
system: akka.actor.ActorSystem,
ec: ExecutionContext,
mode: play.api.Mode
ec: ExecutionContext
) {
import DuctSequencer._

View File

@ -18,6 +18,7 @@ final class JsonView(
def apply(
puzzle: Puzzle,
theme: Option[PuzzleTheme.Key],
user: Option[User],
round: Option[PuzzleRound] = None
): Fu[JsObject] = {
@ -30,6 +31,7 @@ final class JsonView(
"game" -> gameJson,
"puzzle" -> puzzleJson(puzzle)
)
.add("theme" -> theme)
.add("user" -> user.map(userJson))
}
}
@ -95,4 +97,6 @@ final class JsonView(
object JsonView {
implicit val puzzleIdWrites: Writes[Puzzle.Id] = stringIsoWriter(Puzzle.idIso)
implicit val puzzleThemeKeyWrites: Writes[PuzzleTheme.Key] = stringIsoWriter(PuzzleTheme.keyIso)
}

View File

@ -14,7 +14,6 @@ object Line {
@scala.annotation.tailrec
def walk(subs: Vector[(Lines, Int)]): Option[Int] =
subs match {
case Vector() => none
case (lines, depth) +: rest =>
lines match {
case Nil => walk(rest)
@ -23,6 +22,7 @@ object Line {
case Node(_, children) :: siblings =>
walk(rest :+ (siblings -> depth) :+ (children -> (depth + 1)))
}
case _ => none
}
(1 + ~walk(Vector(lines -> 1))) / 2
}

View File

@ -38,8 +38,6 @@ final class PuzzleAnon(colls: PuzzleColls, cacheApi: CacheApi, pathApi: PuzzlePa
"min" $gte ratingRange.min,
"max" $lte ratingRange.max
)
println(count)
println(lila.db.BSON.debug(selector))
colls.path {
_.aggregateList(poolSize) { framework =>
import framework._

View File

@ -118,9 +118,9 @@ final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: Us
}
}
def onRound(round: PuzzleRound): Unit =
def onComplete(round: PuzzleRound, theme: Option[PuzzleTheme.Key]): Unit =
cursors.getIfPresent(round.userId) foreach {
_ foreach { cursor =>
_.filter(_.theme == theme) foreach { cursor =>
// yes, even if the completed puzzle was not the current cursor puzzle
// in that case we just skip a puzzle on the path, which doesn't matter
cursors.put(round.userId, fuccess(cursor.next))

View File

@ -7,9 +7,9 @@ import scala.util.chaining._
import lila.common.Bus
import lila.db.AsyncColl
import lila.db.dsl._
import lila.rating.Perf
import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, UserRepo }
import lila.rating.Perf
final private[puzzle] class PuzzleFinisher(
api: PuzzleApi,
@ -24,53 +24,59 @@ final private[puzzle] class PuzzleFinisher(
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
val (round, newPuzzleGlicko, userPerf) = prevRound match {
case Some(prev) =>
(
prev.copy(win = result.win),
none,
user.perfs.puzzle
)
}
.filter(_.sanityCheck)
val userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now)
val round = prevRound
.fold(
PuzzleRound(
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 = 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 round = PuzzleRound(
id = PuzzleRound.Id(user.id, puzzle.id),
date = now,
win = result.win,
vote = none,
weight = none
)
)(_.copy(win = result.win))
val userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now)
(round, newPuzzleGlicko, userPerf)
}
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
}
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) >>-
(userPerf != user.perfs.puzzle).?? { userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) } >>-
Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle"

View File

@ -55,4 +55,6 @@ object PuzzleTheme {
}.toMap
def find(key: String) = byKey get Key(key)
implicit val keyIso = lila.common.Iso.string[Key](Key.apply, _.value)
}

View File

@ -86,8 +86,6 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
});
instanciateCeval();
history.replaceState(null, '', '/training/' + data.puzzle.id);
}
function position(): Chess {
@ -235,11 +233,12 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
if (vm.resultSent) return;
vm.resultSent = true;
nbToVoteCall(Math.max(0, parseInt(nbToVoteCall()) - 1));
xhr.round(data.puzzle.id, win).then((res: PuzzleResult | undefined) => {
if (res && data.user) {
data.user.rating = res.perf.rating;
data.user.provisional = res.perf.provisional;
xhr.complete(data.puzzle.id, data.theme, win).then((res: PuzzleResult | undefined) => {
if (res?.next.user && data.user) {
data.user.rating = res.next.user.rating;
data.user.provisional = res.next.user.provisional;
vm.round = res.round;
vm.next = res.next;
}
if (win) speech.success();
redraw();
@ -247,15 +246,11 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
}
function nextPuzzle(): void {
if (!vm.next) return location.reload();
ceval.stop();
vm.loading = true;
vm.round = undefined;
initiate(vm.next);
redraw();
xhr.nextPuzzle().then((d: PuzzleData) => {
vm.round = undefined;
vm.loading = false;
initiate(d);
redraw();
});
}
function instanciateCeval(): void {

View File

@ -64,6 +64,7 @@ export interface Vm {
mode: 'play' | 'view' | 'try';
loading: boolean;
round?: PuzzleRound;
next?: PuzzleData;
justPlayed?: Key;
resultSent: boolean;
lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry';
@ -99,9 +100,9 @@ export interface PuzzlePrefs {
export interface PuzzleData {
puzzle: Puzzle;
theme?: ThemeKey;
game: PuzzleGame;
user: PuzzleUser | undefined;
voted: boolean | null | undefined;
}
export interface PuzzleGame {
@ -130,11 +131,8 @@ export interface Puzzle {
}
export interface PuzzleResult {
perf: {
rating: number;
provisional?: boolean;
}
round?: PuzzleRound;
next: PuzzleData;
}
export interface PuzzleRound {

View File

@ -25,7 +25,7 @@ function renderVote(ctrl: Controller): MaybeVNode {
export default function(ctrl: Controller): MaybeVNode {
const data = ctrl.getData();
const voteCall = !!data.user && ctrl.callToVote() && data.voted === undefined;
const voteCall = !!data.user && ctrl.callToVote() && ctrl.vm.round?.vote === undefined;
return h('div.puzzle__feedback.after' + (voteCall ? '.call' : ''), [
voteCall ? h('div.vote_call', [
h('strong', ctrl.trans('wasThisPuzzleAnyGood')),

View File

@ -1,9 +1,9 @@
import * as xhr from 'common/xhr';
import { PuzzleData, PuzzleResult, ThemeKey } from './interfaces';
import { PuzzleResult, ThemeKey } from './interfaces';
import {defined} from 'common';
export function complete(puzzleId: string, theme: ThemeKey, win: boolean): Promise<PuzzleResult | undefined> {
return xhr.json(`/training/complete/${theme}/${puzzleId}`, {
export function complete(puzzleId: string, theme: ThemeKey | undefined, win: boolean): Promise<PuzzleResult | undefined> {
return xhr.json(`/training/complete/${theme || "any"}/${puzzleId}`, {
method: 'POST',
body: xhr.form({ win: win ? 1 : 0 })
});
@ -15,10 +15,3 @@ export function vote(puzzleId: string, vote: boolean | undefined): Promise<void>
body: defined(vote) ? xhr.form({ vote }) : undefined
});
}
// do NOT set mobile API headers here
// they trigger a compat layer
export const nextPuzzle = (): Promise<PuzzleData> =>
xhr.json('/training/new', {
headers: { ...xhr.xhrHeader }
});