puzzle WIP
parent
fa6be1c83a
commit
cd5f13ab02
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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._
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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._
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue