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

View File

@ -2,12 +2,14 @@ package lila.puzzle
import org.goochjs.glicko2.{ Rating, RatingCalculator, RatingPeriodResults }
import org.joda.time.DateTime
import scala.util.chaining._
import lila.common.Bus
import lila.db.AsyncColl
import lila.db.dsl._
import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, UserRepo }
import lila.rating.Perf
final private[puzzle] class Finisher(
api: PuzzleApi,
@ -16,15 +18,32 @@ final private[puzzle] class Finisher(
colls: PuzzleColls
)(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 =>
val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating
val userRating = user.perfs.puzzle.toRating
// val puzzleRating = puzzle.perf.toRating
// updateRatings(userRating, puzzleRating, result.glicko)
// val puzzlePerf =
// puzzle.perf.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id} user")(puzzleRating)
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 userPerf =
user.perfs.puzzle.addOrReset(_.puzzle.crazyGlicko, s"puzzle ${puzzle.id}")(userRating, now)
val round = prevRound
@ -37,24 +56,25 @@ final private[puzzle] class Finisher(
weight = none
)
)(_.copy(win = result.win))
// 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 {
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
}
} zip
userRepo.setPerf(user.id, PerfType.Puzzle, userPerf) >>-
Bus.publish(
Puzzle.UserResult(puzzle.id, user.id, result, formerUserRating -> userPerf.intRating),
"finishPuzzle"
)
round
}
) inject (round -> userPerf)
}
private val VOLATILITY = Glicko.default.volatility
@ -62,7 +82,7 @@ final private[puzzle] class Finisher(
private val system = new RatingCalculator(VOLATILITY, TAU)
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 = {
val results = new RatingPeriodResults()

View File

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

View File

@ -1,6 +1,6 @@
package lila.puzzle
import Puzzle.{ BSONFields => F }
import cats.implicits._
import scala.concurrent.duration._
import lila.db.AsyncColl
@ -12,7 +12,7 @@ final private[puzzle] class PuzzleApi(
colls: PuzzleColls
)(implicit ec: scala.concurrent.ExecutionContext) {
import Puzzle.BSONFields._
import Puzzle.{ BSONFields => F }
import BsonHandlers._
object puzzle {
@ -36,46 +36,61 @@ final private[puzzle] class PuzzleApi(
)
}
// object vote {
object vote {
// def value(id: PuzzleId, user: User): Fu[Option[Boolean]] =
// voteColl {
// _.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v")
// }
// def value(id: Puzzle.Id, user: User): Fu[Option[Boolean]] =
// colls.round {
// _.primitiveOne[Boolean]($id(Vote.makeId(id, user.id)), "v")
// }
// def find(id: PuzzleId, user: User): Fu[Option[Vote]] =
// voteColl {
// _.byId[Vote](Vote.makeId(id, user.id))
// }
// def find(id: PuzzleId, user: User): Fu[Option[Vote]] =
// voteColl {
// _.byId[Vote](Vote.makeId(id, user.id))
// }
// 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 update(id: Puzzle.Id, user: User, v: Boolean): Funit =
colls.round {
_.ext
.findAndUpdate[PuzzleRound](
$id(PuzzleRound.Id(user.id, id)),
$set($doc(PuzzleRound.BSONFields.vote -> v))
)
} flatMap {
case Some(prevRound) if prevRound.vote.fold(true)(v !=) =>
val dir = if (v) 1 else -1
val inc = dir * (if (prevRound.vote.isDefined) 2 else 1)
colls.puzzle {
_.incField($id(id), F.vote, inc).void
}
case _ => funit
}
// 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)
// }
}
}

View File

@ -102,7 +102,7 @@ final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: Us
)
}.map { docOpt =>
import NextPuzzleResult._
println(docOpt map lila.db.BSON.debug)
// println(docOpt map lila.db.BSON.debug)
docOpt.fold[NextPuzzleResult](PathMissing) { doc =>
doc.getAsOpt[Puzzle.Id]("puzzleId").fold[NextPuzzleResult](PathEnded) { puzzleId =>
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="findTheBestMoveForBlack">Find the best move for black.</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="clickToSolve">Click to solve</string>
<string name="goodMove">Good move</string>

View File

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

View File

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

View File

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

View File

@ -16,9 +16,6 @@ function puzzleInfos(ctrl: Controller, puzzle: Puzzle): VNode {
return h('div.infos.puzzle', {
attrs: dataIcon('-')
}, [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('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 {
const data = ctrl.getData();
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', [
h('h2', ctrl.trans.vdom('yourPuzzleRatingX', h('strong', [
data.user.rating,
...(diff >= 0 ? [' ', h('good.rp', '+' + diff)] : []),
...(diff < 0 ? [' ', h('bad.rp', '' + (-diff))] : [])
...(diff && diff > 0 ? [' ', h('good.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';
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`, {
method: 'POST',
body: xhr.form({ win: win ? 1 : 0 })