diff --git a/app/controllers/Puzzle.scala b/app/controllers/Puzzle.scala index ee4be8c3b5..edc06d63e8 100644 --- a/app/controllers/Puzzle.scala +++ b/app/controllers/Puzzle.scala @@ -138,14 +138,15 @@ object Puzzle extends LilaController { resultInt => ctx.me match { case Some(me) => lila.mon.puzzle.round.user() - env.finisher(puzzle, me, Result(resultInt == 1)) >> { - for { - me2 <- UserRepo byId me.id map (_ | me) - infos <- env userInfos me2 - } yield Ok(Json.obj( - "user" -> lila.puzzle.JsonView.infos(false)(infos) - )) - } + for { + finished <- env.finisher(puzzle, me, Result(resultInt == 1)) + (round, _) = finished + me2 <- UserRepo byId me.id map (_ | me) + infos <- env userInfos me2 + } yield Ok(Json.obj( + "user" -> lila.puzzle.JsonView.infos(false)(infos), + "round" -> lila.puzzle.JsonView.round(round) + )) case None => lila.mon.puzzle.round.anon() env.finisher.incPuzzleAttempts(puzzle) diff --git a/app/views/puzzle/show.scala.html b/app/views/puzzle/show.scala.html index 7bb7671634..750d3a8fa2 100644 --- a/app/views/puzzle/show.scala.html +++ b/app/views/puzzle/show.scala.html @@ -18,7 +18,7 @@ i18n: @jsI18n() } @side = { - +
} @base.layout( diff --git a/bin/mongodb/puzzle-fen-turn.js b/bin/mongodb/puzzle-fen-turn.js index 2ced6439a0..0f929ba405 100644 --- a/bin/mongodb/puzzle-fen-turn.js +++ b/bin/mongodb/puzzle-fen-turn.js @@ -4,19 +4,20 @@ function fullMoveNumber(p) { return Math.floor(1 + p.history.split(' ').length / 2); } -function changeFenMoveNumber(fen, n) { +function changeFenMoveNumber(fen) { parts = fen.split(' '); - parts[parts.length - 1] = n; + if (parts[1] === 'b') return fen; + parts[5] = parseInt(parts[5]) + 1; return parts.join(' '); } puzzles.find({ + // _id: 10107 "_id": { "$lt": 60121 } }).forEach(function(p) { - var newMoveNumber = fullMoveNumber(p); - var newFen = changeFenMoveNumber(p.fen, newMoveNumber); + var newFen = changeFenMoveNumber(p.fen); puzzles.update({ _id: p._id }, { diff --git a/modules/game/src/main/Query.scala b/modules/game/src/main/Query.scala index da64cc711f..ea2f9ff879 100644 --- a/modules/game/src/main/Query.scala +++ b/modules/game/src/main/Query.scala @@ -50,7 +50,6 @@ object Query { def user(u: String): Bdoc = F.playerUids $eq u def user(u: User): Bdoc = F.playerUids $eq u.id - def users(u: Seq[String]) = F.playerUids $in u val noAi: Bdoc = $doc( "p0.ai" $exists false, diff --git a/modules/puzzle/src/main/GameJson.scala b/modules/puzzle/src/main/GameJson.scala index 6a1cbc518b..9df5530bb6 100644 --- a/modules/puzzle/src/main/GameJson.scala +++ b/modules/puzzle/src/main/GameJson.scala @@ -30,7 +30,6 @@ private final class GameJson( val anaDests = lastAnaDests(game, tree) Json.obj( "id" -> game.id, - "speed" -> game.speed.key, "clock" -> game.clock.map(_.show), "perf" -> Json.obj( "icon" -> perfType.iconChar.toString, @@ -43,11 +42,6 @@ private final class GameJson( "color" -> p.color.name ) }), - "winner" -> game.winnerColor.map(_.name), - "turns" -> game.turns, - "status" -> game.status, - "tournamentId" -> game.tournamentId, - "createdAt" -> game.createdAt, "treeParts" -> partitionTreeJsonWriter.writes(tree), "destsCache" -> Json.obj( anaDests.path -> anaDests.dests diff --git a/modules/puzzle/src/main/JsonView.scala b/modules/puzzle/src/main/JsonView.scala index 2a8e89aa1d..f00f0cbb96 100644 --- a/modules/puzzle/src/main/JsonView.scala +++ b/modules/puzzle/src/main/JsonView.scala @@ -58,12 +58,7 @@ final class JsonView(gameJson: GameJson) { "duration" -> pref.animationFactor * animationDuration.toMillis ), "mode" -> mode, - "round" -> round.map { a => - Json.obj( - "ratingDiff" -> a.ratingDiff, - "win" -> a.result.win - ) - }, + "round" -> round.map(JsonView.round), "attempt" -> round.ifTrue(isMobileApi).map { r => Json.obj( "userRatingDiff" -> r.ratingDiff, @@ -86,9 +81,9 @@ final class JsonView(gameJson: GameJson) { private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = { import chess.format._ - val solution: List[Uci.Move] = Line solution puzzle.lines map { uci => + val solution: List[Uci.Move] = (Line solution puzzle.lines).map { uci => Uci.Move(uci) err s"Invalid puzzle solution UCI $uci" - } + }.init val init = chess.Game(none, puzzle.fenAfterInitialMove).withTurns(puzzle.initialPly) val (_, branchList) = solution.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) { case ((prev, branches), uci) => @@ -118,4 +113,9 @@ object JsonView { Json.arr(r.puzzleId, r.ratingDiff, r.rating) } ).noNull + + def round(r: Round): JsObject = Json.obj( + "ratingDiff" -> r.ratingDiff, + "win" -> r.result.win + ) } diff --git a/modules/puzzle/src/main/Puzzle.scala b/modules/puzzle/src/main/Puzzle.scala index 9f1c7f935d..d736acc4b1 100644 --- a/modules/puzzle/src/main/Puzzle.scala +++ b/modules/puzzle/src/main/Puzzle.scala @@ -20,9 +20,10 @@ case class Puzzle( attempts: Int, mate: Boolean) { + // ply after "initial move" when we start solving def initialPly: Int = { fen.split(' ').lastOption flatMap parseIntOption map { move => - move * 2 - color.fold(2, 1) + move * 2 - color.fold(0, 1) } } | 0 diff --git a/public/javascripts/main.js b/public/javascripts/main.js index ff9c27ebe1..8ef000a48f 100644 --- a/public/javascripts/main.js +++ b/public/javascripts/main.js @@ -1593,7 +1593,7 @@ lichess.notifyApp = (function() { function startPuzzle(cfg) { var puzzle; cfg.element = document.querySelector('#puzzle'); - cfg.sideElement = document.querySelector('#site_header .puzzle_box'); + cfg.sideElement = document.querySelector('#site_header .puzzle_side'); lichess.socket = lichess.StrongSocket('/socket', 0, { options: { name: "puzzle" diff --git a/public/stylesheets/puzzle.css b/public/stylesheets/puzzle.css index 0966ee66cc..d622d850b0 100644 --- a/public/stylesheets/puzzle.css +++ b/public/stylesheets/puzzle.css @@ -39,6 +39,12 @@ line-height: 64px; text-align: center; } +#puzzle .feedback.win .icon { + color: #759900; +} +#puzzle .feedback.fail .icon { + color: #dc322f; +} #puzzle .feedback .instruction > * { display: block; } @@ -60,6 +66,16 @@ opacity: 1; } +#puzzle .after .continue { + display: flex; + height: 50%; + width: 100%; + font-size: 1.3em; + background: #3893E8; + color: #fff; + align-items: center; +} + #puzzle move { justify-content: space-between; } @@ -119,39 +135,52 @@ body.dark #puzzle .timeline > * { color: #444; font-size: 1.5em; } -.puzzle_box { +.puzzle_side .side_box.metas { padding: 0 7px; } -.puzzle_box .game_infos { +.puzzle_side .game_infos { margin-top: 14px!important; padding-bottom: 14px!important; } -.puzzle_box .header a { +.puzzle_side .header a { color: #3893E8; } -.puzzle_box .game_infos.puzzle { +.puzzle_side .game_infos.puzzle { padding-left: 56px; } -.puzzle_box .game_infos.puzzle::before { +.puzzle_side .game_infos.puzzle::before { font-size: 47px; } -.puzzle_box .puzzle .header a { +.puzzle_side .puzzle .header a { font-size: 1.2em; } -.puzzle_box .game_infos.game { +.puzzle_side .game_infos.game { border: none; padding-bottom: 0!important; } -.puzzle_box .players { +.puzzle_side .players { padding-bottom: 14px; } -.puzzle_box .players .color-icon::before { +.puzzle_side .players .color-icon::before { width: 30px; display: inline-block; text-align: center; } +.puzzle_side .rating h2 { + padding: 7px; +} +.puzzle_side .rating span.rp { + /* font-size: 1.2em; */ +} +.puzzle_side .rating span.rp.up { + color: #759900; +} +.puzzle_side .rating span.rp.down { + color: #ac524f; +} + body.dark #puzzle .timeline a.new { background: #555; color: #ddd; diff --git a/ui/puzzle/src/ctrl.js b/ui/puzzle/src/ctrl.js index b4e683897d..87ab73a95b 100644 --- a/ui/puzzle/src/ctrl.js +++ b/ui/puzzle/src/ctrl.js @@ -30,10 +30,12 @@ module.exports = function(opts, i18n) { var initiate = function(fromData) { data = fromData; + console.log(data); tree = treeBuild(treeOps.reconstruct(data.game.treeParts)); var initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root)); vm.mode = 'play'; // play | try | view vm.loading = false; + vm.round = null; vm.justPlayed = null; vm.resultSent = false; vm.lastFeedback = 'init'; @@ -50,13 +52,17 @@ module.exports = function(opts, i18n) { setTimeout(function() { vm.canViewSolution = true; m.redraw(); - }, 5000); + // }, 5000); + }, 50); socket.setDestsCache(data.game.destsCache); moveTest = moveTestBuild(vm, data.puzzle); showGround(); m.redraw(); + + if (window.history.pushState) + window.history.replaceState(null, null, '/training/' + data.puzzle.id); }; var showGround = function() { @@ -72,6 +78,7 @@ module.exports = function(opts, i18n) { }; var config = { fen: node.fen, + orientation: data.puzzle.color, turnColor: color, movable: movable, premovable: { @@ -91,8 +98,6 @@ module.exports = function(opts, i18n) { config.movable.color = data.puzzle.color; config.premovable.enabled = true; } - console.log(dests, config); - vm.cgConfig = config; if (!ground) ground = groundBuild(data, config, userMove); ground.set(config); if (!dests) getDests(); @@ -145,9 +150,7 @@ module.exports = function(opts, i18n) { ground.playPremove(); var progress = moveTest(); - // console.log(progress, vm.node); if (progress) applyProgress(progress); - // preparePremoving(); m.redraw(); }; @@ -193,7 +196,6 @@ module.exports = function(opts, i18n) { } } else if (progress && progress.orig) { vm.lastFeedback = 'good'; - // console.log(tree); setTimeout(function() { socket.sendAnaMove(progress); }, 500); @@ -206,7 +208,9 @@ module.exports = function(opts, i18n) { vm.loading = true; xhr.round(data.puzzle.id, win).then(function(res) { data.user = res.user; + vm.round = res.round; vm.loading = false; + m.redraw(); }); }; @@ -214,6 +218,7 @@ module.exports = function(opts, i18n) { vm.loading = true; xhr.nextPuzzle().then(function(d) { // pushState(cfg); + vm.round = null; vm.loading = false; initiate(d); }); @@ -310,6 +315,12 @@ module.exports = function(opts, i18n) { } }); + var recentHash = function() { + return data.user ? data.user.recent.reduce(function(h, r) { + return h + r[0]; + }, '') : ''; + }; + initiate(opts.data); keyboard.bind({ @@ -317,8 +328,6 @@ module.exports = function(opts, i18n) { userJump: userJump }); - console.log(data); - return { vm: vm, getData: function() { @@ -331,6 +340,7 @@ module.exports = function(opts, i18n) { userJump: userJump, viewSolution: viewSolution, nextPuzzle: nextPuzzle, + recentHash: recentHash, trans: lichess.trans(opts.i18n), socketReceive: socket.receive }; diff --git a/ui/puzzle/src/solution.js b/ui/puzzle/src/solution.js index 2f57cb11ee..25140a3dec 100644 --- a/ui/puzzle/src/solution.js +++ b/ui/puzzle/src/solution.js @@ -3,7 +3,7 @@ var treeOps = require('tree').ops; module.exports = function(tree, initialNode, solution, color) { tree.ops.updateAll(solution, function(node) { - if ((color === 'white') === (node.ply % 2 === 0)) node.puzzle = 'good'; + if ((color === 'white') === (node.ply % 2 === 1)) node.puzzle = 'good'; }); var solutionNode = treeOps.childById(initialNode, solution.id); diff --git a/ui/puzzle/src/view/after.js b/ui/puzzle/src/view/after.js index af1cfe541e..b61eaafaf5 100644 --- a/ui/puzzle/src/view/after.js +++ b/ui/puzzle/src/view/after.js @@ -24,7 +24,8 @@ var m = require('mithril'); // } module.exports = function(ctrl) { - return m('div.feedback.view', [ + var data = ctrl.getData(); + return m('div.feedback.after', [ // (!ctrl.hasEverVoted.get() && ctrl.data.puzzle.enabled && ctrl.data.voted === null) ? m('div.please_vote', [ // m('p.first', [ // m('strong', ctrl.trans('wasThisPuzzleAnyGood')), @@ -36,8 +37,11 @@ module.exports = function(ctrl) { // ) // ]) : null, // (ctrl.data.puzzle.enabled && ctrl.data.user) ? renderVote(ctrl) : null, - m('a.continue.button.text[data-icon=G]', { + m('a.continue', { onclick: ctrl.nextPuzzle - }, ctrl.trans.noarg('continueTraining')) + }, [ + m('i[data-icon=G]'), + ctrl.trans.noarg('continueTraining') + ]) ]); } diff --git a/ui/puzzle/src/view/history.js b/ui/puzzle/src/view/history.js index 5fd4676b35..352c81bab1 100644 --- a/ui/puzzle/src/view/history.js +++ b/ui/puzzle/src/view/history.js @@ -1,8 +1,13 @@ var m = require('mithril'); var historySize = 15; +var recentHash = ''; -module.exports = function(data) { +module.exports = function(ctrl) { + var hash = ctrl.recentHash(); + if (hash === recentHash) return {subtree: 'retain'}; + recentHash = hash; + var data = ctrl.getData(); if (!data.user) return; var slots = []; for (var i = 0; i < historySize; i++) slots[i] = data.user.recent[i] || null; diff --git a/ui/puzzle/src/view/main.js b/ui/puzzle/src/view/main.js index ec9b1f9762..ac6a9b2ab9 100644 --- a/ui/puzzle/src/view/main.js +++ b/ui/puzzle/src/view/main.js @@ -117,7 +117,7 @@ module.exports = function(ctrl) { ]), m('div.underboard', [ m('div.center', [ - historyView(ctrl.getData()) + historyView(ctrl) ]) ]) ]; diff --git a/ui/puzzle/src/view/side.js b/ui/puzzle/src/view/side.js index a5c720cbbf..fd36af1109 100644 --- a/ui/puzzle/src/view/side.js +++ b/ui/puzzle/src/view/side.js @@ -5,30 +5,37 @@ function strong(txt) { return '' + txt + ''; } -module.exports = function(ctrl) { +function puzzleBox(ctrl) { + var data = ctrl.getData(); + return m('div.side_box.metas', [ + puzzleInfos(ctrl, data.puzzle), + gameInfos(ctrl, data.game, data.puzzle) + ]) +} - var puzzle = ctrl.getData().puzzle; - var game = ctrl.getData().game; +function puzzleInfos(ctrl, puzzle) { + return m('div.game_infos.puzzle[data-icon="-"]', [ + m('div.header', [ + m('a.title', { + href: '/training/' + puzzle.id + }, ctrl.trans('puzzleId', puzzle.id)), + m('p', m.trust(ctrl.trans('ratingX', + strong(ctrl.vm.mode === 'view' ? puzzle.rating : '?')))), + m('p', m.trust(ctrl.trans('playedXTimes', + strong(lichess.numberFormat(puzzle.attempts))))) + ]) + ]); +} +function gameInfos(ctrl, game, puzzle) { return [ - m('div.game_infos.puzzle[data-icon="-"]', [ - m('div.header', [ - m('a.title', { - href: '/training/' + puzzle.id - }, ctrl.trans('puzzleId', puzzle.id)), - m('p', m.trust(ctrl.trans('ratingX', - strong(ctrl.vm.mode === 'view' ? puzzle.rating : '?')))), - m('p', m.trust(ctrl.trans('playedXTimes', - strong(lichess.numberFormat(puzzle.attempts))))) - ]) - ]), m('div.game_infos.game[data-icon="-"]', { 'data-icon': game.perf.icon }, [ m('div.header', [ 'From game ', ctrl.vm.mode === 'view' ? m('a.title', { - href: '/' + game.id + href: '/' + game.id + '/' + puzzle.color + '#' + puzzle.initialPly }, '#' + game.id) : '#' + game.id.slice(0, 5) + '...', m('p', [ game.clock, ' • ', @@ -45,4 +52,53 @@ module.exports = function(ctrl) { ); })) ]; +} + +function userBox(ctrl) { + var data = ctrl.getData(); + if (!data.user) return; + var ratingHtml = data.user.rating; + if (ctrl.vm.round) { + var diff = ctrl.vm.round.ratingDiff, + klass = ''; + if (diff >= 0) { + diff = '+' + diff; + klass = 'up'; + } else if (diff === 0) diff = '+0'; + else klass = 'down'; + ratingHtml += ' ' + diff + ''; + } + return m('div.side_box.rating', [ + m('h2', m.trust(ctrl.trans('yourPuzzleRatingX', strong(ratingHtml)))), + m('div', ratingChart(ctrl)) + ]); +} + +function ratingChart(ctrl) { + return m('div.rating_chart', { + config: function(el, isUpdate, ctx) { + var hash = ctrl.recentHash(); + if (hash == ctx.hash) return; + ctx.hash = hash; + var dark = document.body.classList.contains('dark'); + var points = ctrl.getData().user.recent.map(function(r) { + return r[2] + r[1]; + }); + jQuery(el).sparkline(points, { + type: 'line', + width: '213px', + height: '80px', + lineColor: dark ? '#4444ff' : '#0000ff', + fillColor: dark ? '#222255' : '#ccccff' + }); + } + }); +} + +module.exports = function(ctrl) { + + return [ + puzzleBox(ctrl), + userBox(ctrl) + ]; };