puzzle themes WIP
parent
b1bd0a327b
commit
96d860efab
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
})
|
||||
])
|
||||
])
|
||||
))
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue