puzzle themes WIP

pull/7680/head
Thibault Duplessis 2020-12-04 13:28:23 +01:00
parent b1bd0a327b
commit 96d860efab
12 changed files with 104 additions and 55 deletions

View File

@ -15,25 +15,14 @@ final class Puzzle(
apiC: => Api
) extends LilaController(env) {
private def renderJson(puzzle: Puz, theme: PuzzleTheme, round: Option[PuzzleRound] = None)(implicit
ctx: Context
): Fu[JsObject] =
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = ctx.me, round = round)
private def renderJson(puzzle: Puz, theme: PuzzleTheme)(implicit ctx: Context): Fu[JsObject] =
env.puzzle.jsonView(puzzle = puzzle, theme = theme, user = ctx.me)
private def renderShow(puzzle: Puz, theme: PuzzleTheme)(implicit ctx: Context) =
ctx.me.?? { env.puzzle.api.round.find(_, puzzle) } flatMap {
renderShowWithRound(puzzle, theme, _)
}
private def renderShowWithRound(puzzle: Puz, theme: PuzzleTheme, round: Option[PuzzleRound])(implicit
ctx: Context
) =
ctx.me.?? { env.puzzle.api.round.find(_, puzzle) } flatMap { round =>
renderJson(puzzle, theme, round) map { json =>
EnableSharedArrayBuffer(
Ok(views.html.puzzle.show(puzzle, data = json, pref = env.puzzle.jsonView.pref(ctx.pref)))
)
}
renderJson(puzzle, theme) map { json =>
EnableSharedArrayBuffer(
Ok(views.html.puzzle.show(puzzle, data = json, pref = env.puzzle.jsonView.pref(ctx.pref)))
)
}
def daily =
@ -55,7 +44,7 @@ final class Puzzle(
NoBot {
val theme = PuzzleTheme.any
nextPuzzleForMe(theme.key) flatMap {
renderShowWithRound(_, theme, none)
renderShow(_, theme)
}
}
}
@ -102,17 +91,12 @@ final class Puzzle(
_ = env.puzzle.session.onComplete(round, theme.key)
next <- nextPuzzleForMe(theme.key)
nextJson <- renderJson(next, theme)
} yield Ok(
} yield Ok {
Json.obj(
"round" -> Json
.obj(
"win" -> round.win,
"ratingDiff" -> (perf.intRating - me.perfs.puzzle.intRating)
)
.add("vote" -> round.vote),
"next" -> nextJson
"round" -> env.puzzle.jsonView.roundJson(me, round, perf),
"next" -> nextJson
)
)
}
case None =>
env.puzzle.finisher.incPuzzlePlays(puzzle)
fuccess(NoContent)
@ -150,7 +134,7 @@ final class Puzzle(
jsonFormError,
vote => {
vote foreach { v => lila.mon.puzzle.voteTheme(theme.key.value, v).increment() }
env.puzzle.api.theme.vote(Puz.Id(id), me, theme, vote) inject jsonOkResult
env.puzzle.api.theme.vote(me, Puz.Id(id), theme.key, vote) inject jsonOkResult
}
)
}
@ -215,7 +199,7 @@ final class Puzzle(
case None => Redirect(routes.Puzzle.home()).fuccess
case Some(theme) =>
nextPuzzleForMe(theme.key) flatMap {
renderShowWithRound(_, theme, none)
renderShow(_, theme)
}
}
}

View File

@ -47,7 +47,7 @@ playColl.find({ dirty: true }, { themes: true }).forEach(p => {
oldThemes.find(t => !newThemes.has(t))
) {
const arr = Array.from(newThemes);
print(`Update ${p._id} themes: ${oldThemes.join(', ')} -> ${arr.join(', ')}`);
// print(`Update ${p._id} themes: ${oldThemes.join(', ')} -> ${arr.join(', ')}`);
update['$set'] = {themes:arr};
}
playColl.update({_id:p._id},update);

View File

@ -67,7 +67,7 @@ private[puzzle] object BsonHandlers {
date = r.date(date),
win = r.bool(win),
vote = r.boolO(vote),
themes = r.get[List[PuzzleRound.Theme]](themes),
themes = r.getsD[PuzzleRound.Theme](themes),
weight = r.intO(weight)
)
def writes(w: BSON.Writer, r: PuzzleRound) =
@ -76,6 +76,7 @@ private[puzzle] object BsonHandlers {
date -> r.date,
win -> r.win,
vote -> r.vote,
themes -> w.listO(r.themes),
weight -> r.weight
)
}

View File

@ -1,13 +1,14 @@
package lila.puzzle
import play.api.i18n.Lang
import play.api.libs.json._
import lila.common.Json._
import lila.game.GameRepo
import lila.rating.Perf
import lila.tree
import lila.tree.Node.defaultNodeJsonWriter
import lila.user.User
import play.api.i18n.Lang
final class JsonView(
gameJson: GameJson,
@ -17,12 +18,7 @@ final class JsonView(
import JsonView._
def apply(
puzzle: Puzzle,
theme: PuzzleTheme,
user: Option[User],
round: Option[PuzzleRound] = None
)(implicit lang: Lang): Fu[JsObject] = {
def apply(puzzle: Puzzle, theme: PuzzleTheme, user: Option[User])(implicit lang: Lang): Fu[JsObject] = {
gameJson(
gameId = puzzle.gameId,
plies = puzzle.initialPly
@ -51,6 +47,19 @@ final class JsonView(
"provisional" -> u.perfs.puzzle.provisional
)
def roundJson(u: User, round: PuzzleRound, perf: Perf) =
Json
.obj(
"win" -> round.win,
"ratingDiff" -> (perf.intRating - u.perfs.puzzle.intRating)
)
.add("vote" -> round.vote)
.add("themes" -> round.nonEmptyThemes.map { rt =>
JsObject(rt.map { t =>
t.theme.value -> JsBoolean(t.vote)
})
})
def pref(p: lila.pref.Pref) =
Json.obj(
"blindfold" -> p.blindfold,

View File

@ -55,6 +55,7 @@ object Puzzle {
val plays = "plays"
val themes = "themes"
val day = "day"
val dirty = "dirty" // themes need to be denormalized
}
implicit val idIso = lila.common.Iso.string[Id](Id.apply, _.value)

View File

@ -69,17 +69,18 @@ final private[puzzle] class PuzzleApi(
}
}
def vote(user: User, id: Puzzle.Id, theme: PuzzleTheme, vote: Option[Boolean]): Funit =
round.find(user, id) flatMap {
def vote(user: User, id: Puzzle.Id, theme: PuzzleTheme.Key, vote: Option[Boolean]): Funit =
round.find(user, id).thenPp flatMap {
_ ?? { round =>
???
round.themeVote(theme, vote.pp("vote")).pp ?? { newThemes =>
import PuzzleRound.{ BSONFields => F }
val update =
if (newThemes.isEmpty) $unset(F.themes, F.puzzle)
else $set(F.themes -> newThemes, F.puzzle -> id)
colls.round(_.update.one($id(round.id), update)) zip
colls.puzzle(_.updateField($id(round.id.puzzleId), Puzzle.BSONFields.dirty, true)) void
}
}
}
// colls.round {
// _.byId[
// .findAndUpdate[PuzzleRound](
// $id(PuzzleRound.Id(user.id, id)),
// $set($doc(PuzzleRound.BSONFields.vote -> vote))
}
}

View File

@ -28,7 +28,7 @@ final private[puzzle] class PuzzleFinisher(
result: Result,
isStudent: Boolean
): Fu[(PuzzleRound, Perf)] =
api.round.find(user, puzzle) flatMap { prevRound =>
api.round.find(user, puzzle.id) flatMap { prevRound =>
val now = DateTime.now
val formerUserRating = user.perfs.puzzle.intRating

View File

@ -15,8 +15,25 @@ case class PuzzleRound(
def userId = id.userId
def themeVote(theme: PuzzleTheme.Key, vote: Option[Boolean]): Option[Boolean] =
themes.find(_.theme == theme)
def themeVote(theme: PuzzleTheme.Key, vote: Option[Boolean]): Option[List[PuzzleRound.Theme]] =
themes.find(_.theme == theme) match {
case None =>
vote map { v =>
PuzzleRound.Theme(theme, v) :: themes
}
case Some(prev) =>
vote match {
case None => themes.filter(_.theme != theme).some
case Some(v) if v == prev.vote => none
case Some(v) =>
themes.map {
case t if t.theme == theme => t.copy(vote = v)
case t => t
}.some
}
}
def nonEmptyThemes = themes.nonEmpty option themes
}
object PuzzleRound {
@ -36,6 +53,7 @@ object PuzzleRound {
val win = "w"
val vote = "v"
val themes = "t"
val puzzle = "p" // only if themes is set!
val weight = "w"
val user = "u" // student denormalization
}

View File

@ -394,6 +394,14 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
nextPuzzle();
});
const voteTheme = throttle(500, (theme, v) => {
if (vm.round) {
vm.round.themes = vm.round.themes || {};
vm.round.themes[theme] = v;
xhr.voteTheme(data.puzzle.id, theme, v);
}
});
initiate(opts.data);
const promotion = makePromotion(vm, ground, redraw);
@ -436,6 +444,7 @@ export default function(opts: PuzzleOpts, redraw: Redraw): Controller {
viewSolution,
nextPuzzle,
vote,
voteTheme,
getCeval,
pref: opts.pref,
trans: lichess.trans(opts.i18n),

View File

@ -47,6 +47,7 @@ export interface Controller extends KeyboardController {
viewSolution(): void;
nextPuzzle(): void;
vote(v: boolean): void;
voteTheme(theme: ThemeKey, v: boolean): void;
pref: PuzzlePrefs;
userMove(orig: Key, dest: Key): void;
promotion: any;
@ -143,10 +144,14 @@ export interface PuzzleResult {
next: PuzzleData;
}
export interface RoundThemes {
[key: string]: boolean;
}
export interface PuzzleRound {
win: boolean;
ratingDiff: number;
vote?: boolean;
themes?: RoundThemes;
}
export interface Promotion {

View File

@ -1,6 +1,7 @@
import { Controller } from '../interfaces';
import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import { bind } from '../util';
const staticThemes = new Set([
"enPassant",
@ -25,9 +26,16 @@ export default function theme(ctrl: Controller): VNode {
const editor = (ctrl: Controller): VNode => {
const data = ctrl.getData(),
user = data.user;
user = data.user,
themes = ctrl.vm.round?.themes || {};
return h('div.puzzle__themes', [
h('div.puzzle__themes_list', data.puzzle.themes.map(key =>
h('div.puzzle__themes_list', {
hook: bind('click', e => {
const target = e.target as HTMLElement;
const theme = target.getAttribute('data-theme');
if (theme) ctrl.voteTheme(theme, target.classList.contains('vote-up'));
}, ctrl.redraw)
}, data.puzzle.themes.map(key =>
h('div.puzzle__themes__list__entry', [
h('a', {
attrs: {
@ -36,8 +44,14 @@ const editor = (ctrl: Controller): VNode => {
}
}, ctrl.trans.noarg(key)),
!user || staticThemes.has(key) ? null : h('div.puzzle__themes__votes', [
h('span.puzzle__themes__vote.vote-up'),
h('span.puzzle__themes__vote.vote-down')
h('span.puzzle__themes__vote.vote-up', {
class: { active: themes[key] },
attrs: { 'data-theme': key }
}),
h('span.puzzle__themes__vote.vote-down', {
class: { active: themes[key] === false },
attrs: { 'data-theme': key }
})
])
])
))

View File

@ -15,3 +15,10 @@ export function vote(puzzleId: string, vote: boolean | undefined): Promise<void>
body: defined(vote) ? xhr.form({ vote }) : undefined
});
}
export function voteTheme(puzzleId: string, theme: ThemeKey, vote: boolean | undefined): Promise<void> {
return xhr.json(`/training/${puzzleId}/vote/${theme}`, {
method: 'POST',
body: defined(vote) ? xhr.form({ vote }) : undefined
});
}