puzzle WIP

pull/7680/head
Thibault Duplessis 2020-11-22 19:34:10 +01:00
parent 400b8d23c5
commit 11e3bde24e
11 changed files with 166 additions and 131 deletions

View File

@ -97,7 +97,6 @@ final class Puzzle(
def round3(id: String) = def round3(id: String) =
OpenBody { implicit ctx => OpenBody { implicit ctx =>
NoBot { NoBot {
fuccess(Ok(Json.obj()))
implicit val req = ctx.body implicit val req = ctx.body
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { puzzle => OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { puzzle =>
lila.mon.puzzle.round.attempt(ctx.isAuth).increment() lila.mon.puzzle.round.attempt(ctx.isAuth).increment()
@ -110,24 +109,28 @@ final class Puzzle(
case Some(me) => case Some(me) =>
for { for {
isStudent <- env.clas.api.student.isStudent(me.id) isStudent <- env.clas.api.student.isStudent(me.id)
round <- env.puzzle.finisher( (round, perf) <- env.puzzle.finisher(
puzzle = puzzle, puzzle = puzzle,
user = me, user = me,
result = Result(resultInt == 1), result = Result(resultInt == 1),
isStudent = isStudent isStudent = isStudent
) )
_ = env.puzzle.cursor.onRound(round)
me2 <- env.user.repo.byId(me.id).dmap(_ | me)
// infos <- env.puzzle userInfos me2
} yield Ok( } yield Ok(
Json.obj( Json.obj(
"user" -> env.puzzle.jsonView.userJson(me) "perf" -> Json
// "round" -> lila.puzzle.JsonView.round(round) .obj("rating" -> perf.intRating)
.add("provisional" -> perf.provisional),
"round" -> Json
.obj(
"win" -> round.win,
"ratingDiff" -> (me.perfs.puzzle.intRating - perf.intRating)
)
.add("vote" -> round.vote)
) )
) )
case None => case None =>
env.puzzle.finisher.incPuzzlePlays(puzzle) env.puzzle.finisher.incPuzzlePlays(puzzle)
Ok(Json.obj("user" -> false)).fuccess fuccess(NoContent)
} }
) )
.dmap(_ as JSON) .dmap(_ as JSON)
@ -138,21 +141,18 @@ final class Puzzle(
def vote(id: String) = def vote(id: String) =
AuthBody { implicit ctx => me => AuthBody { implicit ctx => me =>
NoBot { NoBot {
??? implicit val req = ctx.body
// implicit val req = ctx.body env.puzzle.forms.vote
// env.puzzle.forms.vote .bindFromRequest()
// .bindFromRequest() .fold(
// .fold( jsonFormError,
// jsonFormError, vote =>
// vote => env.puzzle.api.vote.update(Puz.Id(id), me, vote == 1) map { newVote =>
// env.puzzle.api.vote.find(id, me) flatMap { v => if (vote == 1) lila.mon.puzzle.vote.up.increment()
// env.puzzle.api.vote.update(id, me, v, vote == 1) else lila.mon.puzzle.vote.down.increment()
// } map { case (p, a) => jsonOkResult
// 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)
} }
} }

View File

@ -2,12 +2,14 @@ package lila.puzzle
import org.goochjs.glicko2.{ Rating, RatingCalculator, RatingPeriodResults } import org.goochjs.glicko2.{ Rating, RatingCalculator, RatingPeriodResults }
import org.joda.time.DateTime import org.joda.time.DateTime
import scala.util.chaining._
import lila.common.Bus import lila.common.Bus
import lila.db.AsyncColl import lila.db.AsyncColl
import lila.db.dsl._ import lila.db.dsl._
import lila.rating.{ Glicko, PerfType } import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, UserRepo } import lila.user.{ User, UserRepo }
import lila.rating.Perf
final private[puzzle] class Finisher( final private[puzzle] class Finisher(
api: PuzzleApi, api: PuzzleApi,
@ -16,15 +18,32 @@ final private[puzzle] class Finisher(
colls: PuzzleColls colls: PuzzleColls
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
def apply(puzzle: Puzzle, user: User, result: Result, isStudent: Boolean): Fu[PuzzleRound] = import BsonHandlers._
def apply(puzzle: Puzzle, user: User, result: Result, isStudent: Boolean): Fu[(PuzzleRound, Perf)] =
api.round.find(user, puzzle) flatMap { prevRound => api.round.find(user, puzzle) flatMap { prevRound =>
val now = DateTime.now val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating val formerUserRating = user.perfs.puzzle.intRating
val userRating = user.perfs.puzzle.toRating val userRating = user.perfs.puzzle.toRating
// val puzzleRating = puzzle.perf.toRating val puzzleRating = new Rating(
// updateRatings(userRating, puzzleRating, result.glicko) puzzle.glicko.rating atLeast Glicko.minRating,
// val puzzlePerf = puzzle.glicko.deviation,
// puzzle.perf.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id} user")(puzzleRating) 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 userPerf = val userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now) user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now)
val round = prevRound val round = prevRound
@ -37,24 +56,25 @@ final private[puzzle] class Finisher(
weight = none weight = none
) )
)(_.copy(win = result.win)) )(_.copy(win = result.win))
// historyApi.addPuzzle(user = user, completedAt = date, perf = userPerf) api.round.upsert(round) zip
api.round.upsert(round) >> { isStudent.??(api.round.addDenormalizedUser(round, user)) zip
isStudent ?? api.round.addDenormalizedUser(round, user) prevRound.isEmpty.?? {
// } >> { colls.puzzle {
// puzzleColl { _.update
// _.update.one( .one(
// $id(puzzle.id), $id(puzzle.id),
// $inc(Puzzle.BSONFields.attempts -> $int(1)) ++ $inc(Puzzle.BSONFields.plays -> $int(1)) ++ newPuzzleGlicko ?? { glicko =>
// $set(Puzzle.BSONFields.perf -> PuzzlePerf.puzzlePerfBSONHandler.write(puzzlePerf)) $set(Puzzle.BSONFields.glicko -> Glicko.glickoBSONHandler.write(glicko))
// ) }
// } zip userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) )
} inject { .void
}
} zip
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) >>-
Bus.publish( Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating), Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle" "finishPuzzle"
) ) inject (round -> userPerf)
round
}
} }
private val VOLATILITY = Glicko.default.volatility private val VOLATILITY = Glicko.default.volatility
@ -62,7 +82,7 @@ final private[puzzle] class Finisher(
private val system = new RatingCalculator(VOLATILITY, TAU) private val system = new RatingCalculator(VOLATILITY, TAU)
def incPuzzlePlays(puzzle: Puzzle): Funit = def incPuzzlePlays(puzzle: Puzzle): Funit =
colls.puzzle.map(_.incFieldUnchecked($id(puzzle.id.value), Puzzle.BSONFields.plays)) colls.puzzle.map(_.incFieldUnchecked($id(puzzle.id), Puzzle.BSONFields.plays))
private def updateRatings(u1: Rating, u2: Rating, result: Glicko.Result): Unit = { private def updateRatings(u1: Rating, u2: Rating, result: Glicko.Result): Unit = {
val results = new RatingPeriodResults() val results = new RatingPeriodResults()

View File

@ -35,10 +35,14 @@ final class JsonView(
} }
def userJson(u: User) = def userJson(u: User) =
Json.obj( Json
"rating" -> u.perfs.puzzle.intRating, .obj(
"recent" -> JsArray() "rating" -> u.perfs.puzzle.intRating,
) "recent" -> JsArray()
)
.add(
"provisional" -> u.perfs.puzzle.provisional
)
def pref(p: lila.pref.Pref) = def pref(p: lila.pref.Pref) =
Json.obj( Json.obj(
@ -55,16 +59,14 @@ final class JsonView(
"is3d" -> p.is3d "is3d" -> p.is3d
) )
private def puzzleJson(puzzle: Puzzle): JsObject = private def puzzleJson(puzzle: Puzzle): JsObject = Json.obj(
Json "id" -> puzzle.id,
.obj( "rating" -> puzzle.glicko.intRating,
"id" -> puzzle.id, "plays" -> puzzle.plays,
"rating" -> puzzle.glicko.intRating, "initialPly" -> puzzle.initialPly,
"plays" -> puzzle.plays, "solution" -> puzzle.line.tail.map(_.uci),
"initialPly" -> puzzle.initialPly, "vote" -> puzzle.vote
"solution" -> puzzle.line.tail.map(_.uci), )
"vote" -> puzzle.vote
)
private def makeSolution(puzzle: Puzzle): Option[tree.Branch] = { private def makeSolution(puzzle: Puzzle): Option[tree.Branch] = {
import chess.format._ import chess.format._

View File

@ -1,6 +1,6 @@
package lila.puzzle package lila.puzzle
import Puzzle.{ BSONFields => F } import cats.implicits._
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.db.AsyncColl import lila.db.AsyncColl
@ -12,7 +12,7 @@ final private[puzzle] class PuzzleApi(
colls: PuzzleColls colls: PuzzleColls
)(implicit ec: scala.concurrent.ExecutionContext) { )(implicit ec: scala.concurrent.ExecutionContext) {
import Puzzle.BSONFields._ import Puzzle.{ BSONFields => F }
import BsonHandlers._ import BsonHandlers._
object puzzle { object puzzle {
@ -36,46 +36,61 @@ final private[puzzle] class PuzzleApi(
) )
} }
// object vote { object vote {
// def value(id: PuzzleId, user: User): Fu[Option[Boolean]] = // def value(id: Puzzle.Id, user: User): Fu[Option[Boolean]] =
// voteColl { // colls.round {
// _.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v") // _.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v")
// } // }
// def find(id: PuzzleId, user: User): Fu[Option[Vote]] = // def find(id: PuzzleId, user: User): Fu[Option[Vote]] =
// voteColl { // voteColl {
// _.byId[Vote](Vote.makeId(id, user.id)) // _.byId[Vote](Vote.makeId(id, user.id))
// } // }
// def update(id: PuzzleId, user: User, v1: Option[Vote], v: Boolean): Fu[(Puzzle, Vote)] = def update(id: Puzzle.Id, user: User, v: Boolean): Funit =
// puzzle find id orFail s"Can't vote for non existing puzzle $id" flatMap { p1 => colls.round {
// val (p2, v2) = v1 match { _.ext
// case Some(from) => .findAndUpdate[PuzzleRound](
// ( $id(PuzzleRound.Id(user.id, id)),
// (p1 withVote (_.change(from.value, v))), $set($doc(PuzzleRound.BSONFields.vote -> v))
// from.copy(v = v) )
// ) } flatMap {
// case None => case Some(prevRound) if prevRound.vote.fold(true)(v !=) =>
// ( val dir = if (v) 1 else -1
// (p1 withVote (_ add v)), val inc = dir * (if (prevRound.vote.isDefined) 2 else 1)
// Vote(Vote.makeId(id, user.id), v) colls.puzzle {
// ) _.incField($id(id), F.vote, inc).void
// } }
// voteColl { case _ => funit
// _.update }
// .one( // puzzle find id orFail s"Can't vote for non existing puzzle $id" flatMap { p1 =>
// $id(v2.id), // val (p2, v2) = v1 match {
// $set("v" -> v), // case Some(from) =>
// upsert = true // (
// ) // (p1 withVote (_.change(from.value, v))),
// .void // from.copy(v = v)
// .recover(lila.db.recoverDuplicateKey { _ => () }) // )
// } zip // case None =>
// puzzleColl { // (
// _.update // (p1 withVote (_ add v)),
// .one($id(p2.id), $set(F.vote -> p2.vote)) // Vote(Vote.makeId(id, user.id), v)
// } inject (p2 -> v2) // )
// } // }
// } // 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)
// }
}
} }

View File

@ -102,7 +102,7 @@ final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: Us
) )
}.map { docOpt => }.map { docOpt =>
import NextPuzzleResult._ import NextPuzzleResult._
println(docOpt map lila.db.BSON.debug) // println(docOpt map lila.db.BSON.debug)
docOpt.fold[NextPuzzleResult](PathMissing) { doc => docOpt.fold[NextPuzzleResult](PathMissing) { doc =>
doc.getAsOpt[Puzzle.Id]("puzzleId").fold[NextPuzzleResult](PathEnded) { puzzleId => doc.getAsOpt[Puzzle.Id]("puzzleId").fold[NextPuzzleResult](PathEnded) { puzzleId =>
doc doc

View File

@ -460,7 +460,6 @@ computer analysis, game chat and shareable URL.</string>
<string name="findTheBestMoveForWhite">Find the best move for white.</string> <string name="findTheBestMoveForWhite">Find the best move for white.</string>
<string name="findTheBestMoveForBlack">Find the best move for black.</string> <string name="findTheBestMoveForBlack">Find the best move for black.</string>
<string name="toTrackYourProgress">To track your progress:</string> <string name="toTrackYourProgress">To track your progress:</string>
<string name="puzzleId">Puzzle %s</string>
<string name="puzzleOfTheDay">Puzzle of the day</string> <string name="puzzleOfTheDay">Puzzle of the day</string>
<string name="clickToSolve">Click to solve</string> <string name="clickToSolve">Click to solve</string>
<string name="goodMove">Good move</string> <string name="goodMove">Good move</string>

View File

@ -30,10 +30,6 @@
border-bottom: $border; border-bottom: $border;
margin-bottom: 2vh; margin-bottom: 2vh;
a {
font-size: 1.2em;
}
.hidden { .hidden {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -16,7 +16,7 @@ import { moveTestBuild, MoveTestFn } from './moveTest';
import { parseFen, makeFen } from 'chessops/fen'; import { parseFen, makeFen } from 'chessops/fen';
import { parseSquare, parseUci, makeSquare, makeUci } from 'chessops/util'; import { parseSquare, parseUci, makeSquare, makeUci } from 'chessops/util';
import { pgnToTree, mergeSolution } from './moveTree'; import { pgnToTree, mergeSolution } from './moveTree';
import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleRound, MoveTest } from './interfaces'; import { Redraw, Vm, Controller, PuzzleOpts, PuzzleData, PuzzleResult, MoveTest } from './interfaces';
import { Role, Move, Outcome } from 'chessops/types'; import { Role, Move, Outcome } from 'chessops/types';
import { storedProp } from 'common/storage'; import { storedProp } from 'common/storage';
@ -57,7 +57,6 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
vm.mode = 'play'; vm.mode = 'play';
vm.loading = false; vm.loading = false;
vm.round = undefined; vm.round = undefined;
vm.voted = undefined;
vm.justPlayed = undefined; vm.justPlayed = undefined;
vm.resultSent = false; vm.resultSent = false;
vm.lastFeedback = 'init'; vm.lastFeedback = 'init';
@ -193,7 +192,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
if (p == 'good' || p == 'win') return -1; if (p == 'good' || p == 'win') return -1;
return 0; return 0;
}); });
if (recursive) node.children.forEach(child => if (recursive) node.children.forEach(child =>
reorderChildren(path + child.id, true) reorderChildren(path + child.id, true)
); );
} }
@ -236,12 +235,14 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
if (vm.resultSent) return; if (vm.resultSent) return;
vm.resultSent = true; vm.resultSent = true;
nbToVoteCall(Math.max(0, parseInt(nbToVoteCall()) - 1)); nbToVoteCall(Math.max(0, parseInt(nbToVoteCall()) - 1));
xhr.round(data.puzzle.id, win).then((res: PuzzleRound) => { xhr.round(data.puzzle.id, win).then((res: PuzzleResult | undefined) => {
data.user = res.user; if (res && data.user) {
vm.round = res.round; data.user.rating = res.perf.rating;
vm.voted = res.voted; data.user.provisional = res.perf.provisional;
redraw(); vm.round = res.round;
}
if (win) speech.success(); if (win) speech.success();
redraw();
}); });
} }
@ -250,7 +251,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
vm.loading = true; vm.loading = true;
redraw(); redraw();
xhr.nextPuzzle().then((d: PuzzleData) => { xhr.nextPuzzle().then((d: PuzzleData) => {
vm.round = null; vm.round = undefined;
vm.loading = false; vm.loading = false;
initiate(d); initiate(d);
redraw(); redraw();
@ -402,7 +403,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
const vote = throttle(1000, function(v) { const vote = throttle(1000, function(v) {
if (callToVote()) thanksUntil = Date.now() + 2000; if (callToVote()) thanksUntil = Date.now() + 2000;
nbToVoteCall(5); nbToVoteCall(5);
vm.voted = v; vm.round!.vote = v;
xhr.vote(data.puzzle.id, v); xhr.vote(data.puzzle.id, v);
redraw(); redraw();
}); });

View File

@ -61,8 +61,7 @@ export interface Vm {
pov: Color; pov: Color;
mode: 'play' | 'view' | 'try'; mode: 'play' | 'view' | 'try';
loading: boolean; loading: boolean;
round: any; round?: PuzzleRound;
voted?: boolean | null;
justPlayed?: Key; justPlayed?: Key;
resultSent: boolean; resultSent: boolean;
lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry'; lastFeedback: 'init' | 'fail' | 'win' | 'good' | 'retry';
@ -117,6 +116,7 @@ export interface PuzzleGame {
export interface PuzzleUser { export interface PuzzleUser {
rating: number; rating: number;
provisional?: boolean;
} }
export interface Puzzle { export interface Puzzle {
@ -127,13 +127,18 @@ export interface Puzzle {
initialPly: number; initialPly: number;
} }
export interface PuzzleResult {
perf: {
rating: number;
provisional?: boolean;
}
round?: PuzzleRound;
}
export interface PuzzleRound { export interface PuzzleRound {
user: PuzzleUser; win: boolean;
round?: { ratingDiff: number;
ratingDiff: number; vote?: boolean;
win: boolean;
};
voted?: null | true | false;
} }
export interface Promotion { export interface Promotion {

View File

@ -16,9 +16,6 @@ function puzzleInfos(ctrl: Controller, puzzle: Puzzle): VNode {
return h('div.infos.puzzle', { return h('div.infos.puzzle', {
attrs: dataIcon('-') attrs: dataIcon('-')
}, [h('div', [ }, [h('div', [
h('a.title', {
attrs: { href: '/training/' + puzzle.id }
}, ctrl.trans('puzzleId', puzzle.id)),
h('p', ctrl.trans.vdom('ratingX', ctrl.vm.mode === 'play' ? h('span.hidden', ctrl.trans.noarg('hidden')) : h('strong', puzzle.rating))), h('p', ctrl.trans.vdom('ratingX', ctrl.vm.mode === 'play' ? h('span.hidden', ctrl.trans.noarg('hidden')) : h('strong', puzzle.rating))),
h('p', ctrl.trans.vdom('playedXTimes', h('strong', numberFormat(puzzle.plays)))) h('p', ctrl.trans.vdom('playedXTimes', h('strong', numberFormat(puzzle.plays))))
])]); ])]);
@ -49,12 +46,12 @@ function gameInfos(ctrl: Controller, game: PuzzleGame, puzzle: Puzzle): VNode {
export function userBox(ctrl: Controller): MaybeVNode { export function userBox(ctrl: Controller): MaybeVNode {
const data = ctrl.getData(); const data = ctrl.getData();
if (!data.user) return; if (!data.user) return;
const diff = ctrl.vm.round && ctrl.vm.round.ratingDiff; const diff = ctrl.vm.round?.ratingDiff;
return h('div.puzzle__side__user', [ return h('div.puzzle__side__user', [
h('h2', ctrl.trans.vdom('yourPuzzleRatingX', h('strong', [ h('h2', ctrl.trans.vdom('yourPuzzleRatingX', h('strong', [
data.user.rating, data.user.rating,
...(diff >= 0 ? [' ', h('good.rp', '+' + diff)] : []), ...(diff && diff > 0 ? [' ', h('good.rp', '+' + diff)] : []),
...(diff < 0 ? [' ', h('bad.rp', '' + (-diff))] : []) ...(diff && diff < 0 ? [' ', h('bad.rp', '' + (-diff))] : [])
]))) ])))
]); ]);
} }

View File

@ -1,7 +1,7 @@
import { PuzzleRound, PuzzleData } from './interfaces'; import { PuzzleData, PuzzleResult } from './interfaces';
import * as xhr from 'common/xhr'; import * as xhr from 'common/xhr';
export function round(puzzleId: string, win: boolean): Promise<PuzzleRound> { export function round(puzzleId: string, win: boolean): Promise<PuzzleResult | undefined> {
return xhr.json(`/training/${puzzleId}/round3`, { return xhr.json(`/training/${puzzleId}/round3`, {
method: 'POST', method: 'POST',
body: xhr.form({ win: win ? 1 : 0 }) body: xhr.form({ win: win ? 1 : 0 })