more work on puzzle UI

puzzle-ui
Thibault Duplessis 2016-12-08 00:17:45 +01:00
parent 6ca81f503b
commit 40066caf79
15 changed files with 168 additions and 68 deletions

View File

@ -138,14 +138,15 @@ object Puzzle extends LilaController {
resultInt => ctx.me match { resultInt => ctx.me match {
case Some(me) => case Some(me) =>
lila.mon.puzzle.round.user() lila.mon.puzzle.round.user()
env.finisher(puzzle, me, Result(resultInt == 1)) >> { for {
for { finished <- env.finisher(puzzle, me, Result(resultInt == 1))
me2 <- UserRepo byId me.id map (_ | me) (round, _) = finished
infos <- env userInfos me2 me2 <- UserRepo byId me.id map (_ | me)
} yield Ok(Json.obj( infos <- env userInfos me2
"user" -> lila.puzzle.JsonView.infos(false)(infos) } yield Ok(Json.obj(
)) "user" -> lila.puzzle.JsonView.infos(false)(infos),
} "round" -> lila.puzzle.JsonView.round(round)
))
case None => case None =>
lila.mon.puzzle.round.anon() lila.mon.puzzle.round.anon()
env.finisher.incPuzzleAttempts(puzzle) env.finisher.incPuzzleAttempts(puzzle)

View File

@ -18,7 +18,7 @@ i18n: @jsI18n()
} }
@side = { @side = {
<div class="side_box puzzle_box"></div> <div class="puzzle_side"></div>
} }
@base.layout( @base.layout(

View File

@ -4,19 +4,20 @@ function fullMoveNumber(p) {
return Math.floor(1 + p.history.split(' ').length / 2); return Math.floor(1 + p.history.split(' ').length / 2);
} }
function changeFenMoveNumber(fen, n) { function changeFenMoveNumber(fen) {
parts = fen.split(' '); parts = fen.split(' ');
parts[parts.length - 1] = n; if (parts[1] === 'b') return fen;
parts[5] = parseInt(parts[5]) + 1;
return parts.join(' '); return parts.join(' ');
} }
puzzles.find({ puzzles.find({
// _id: 10107
"_id": { "_id": {
"$lt": 60121 "$lt": 60121
} }
}).forEach(function(p) { }).forEach(function(p) {
var newMoveNumber = fullMoveNumber(p); var newFen = changeFenMoveNumber(p.fen);
var newFen = changeFenMoveNumber(p.fen, newMoveNumber);
puzzles.update({ puzzles.update({
_id: p._id _id: p._id
}, { }, {

View File

@ -50,7 +50,6 @@ object Query {
def user(u: String): Bdoc = F.playerUids $eq u def user(u: String): Bdoc = F.playerUids $eq u
def user(u: User): Bdoc = F.playerUids $eq u.id def user(u: User): Bdoc = F.playerUids $eq u.id
def users(u: Seq[String]) = F.playerUids $in u
val noAi: Bdoc = $doc( val noAi: Bdoc = $doc(
"p0.ai" $exists false, "p0.ai" $exists false,

View File

@ -30,7 +30,6 @@ private final class GameJson(
val anaDests = lastAnaDests(game, tree) val anaDests = lastAnaDests(game, tree)
Json.obj( Json.obj(
"id" -> game.id, "id" -> game.id,
"speed" -> game.speed.key,
"clock" -> game.clock.map(_.show), "clock" -> game.clock.map(_.show),
"perf" -> Json.obj( "perf" -> Json.obj(
"icon" -> perfType.iconChar.toString, "icon" -> perfType.iconChar.toString,
@ -43,11 +42,6 @@ private final class GameJson(
"color" -> p.color.name "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), "treeParts" -> partitionTreeJsonWriter.writes(tree),
"destsCache" -> Json.obj( "destsCache" -> Json.obj(
anaDests.path -> anaDests.dests anaDests.path -> anaDests.dests

View File

@ -58,12 +58,7 @@ final class JsonView(gameJson: GameJson) {
"duration" -> pref.animationFactor * animationDuration.toMillis "duration" -> pref.animationFactor * animationDuration.toMillis
), ),
"mode" -> mode, "mode" -> mode,
"round" -> round.map { a => "round" -> round.map(JsonView.round),
Json.obj(
"ratingDiff" -> a.ratingDiff,
"win" -> a.result.win
)
},
"attempt" -> round.ifTrue(isMobileApi).map { r => "attempt" -> round.ifTrue(isMobileApi).map { r =>
Json.obj( Json.obj(
"userRatingDiff" -> r.ratingDiff, "userRatingDiff" -> r.ratingDiff,
@ -86,9 +81,9 @@ final class JsonView(gameJson: GameJson) {
private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = { private def makeBranch(puzzle: Puzzle): Option[tree.Branch] = {
import chess.format._ 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" Uci.Move(uci) err s"Invalid puzzle solution UCI $uci"
} }.init
val init = chess.Game(none, puzzle.fenAfterInitialMove).withTurns(puzzle.initialPly) val init = chess.Game(none, puzzle.fenAfterInitialMove).withTurns(puzzle.initialPly)
val (_, branchList) = solution.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) { val (_, branchList) = solution.foldLeft[(chess.Game, List[tree.Branch])]((init, Nil)) {
case ((prev, branches), uci) => case ((prev, branches), uci) =>
@ -118,4 +113,9 @@ object JsonView {
Json.arr(r.puzzleId, r.ratingDiff, r.rating) Json.arr(r.puzzleId, r.ratingDiff, r.rating)
} }
).noNull ).noNull
def round(r: Round): JsObject = Json.obj(
"ratingDiff" -> r.ratingDiff,
"win" -> r.result.win
)
} }

View File

@ -20,9 +20,10 @@ case class Puzzle(
attempts: Int, attempts: Int,
mate: Boolean) { mate: Boolean) {
// ply after "initial move" when we start solving
def initialPly: Int = { def initialPly: Int = {
fen.split(' ').lastOption flatMap parseIntOption map { move => fen.split(' ').lastOption flatMap parseIntOption map { move =>
move * 2 - color.fold(2, 1) move * 2 - color.fold(0, 1)
} }
} | 0 } | 0

View File

@ -1593,7 +1593,7 @@ lichess.notifyApp = (function() {
function startPuzzle(cfg) { function startPuzzle(cfg) {
var puzzle; var puzzle;
cfg.element = document.querySelector('#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, { lichess.socket = lichess.StrongSocket('/socket', 0, {
options: { options: {
name: "puzzle" name: "puzzle"

View File

@ -39,6 +39,12 @@
line-height: 64px; line-height: 64px;
text-align: center; text-align: center;
} }
#puzzle .feedback.win .icon {
color: #759900;
}
#puzzle .feedback.fail .icon {
color: #dc322f;
}
#puzzle .feedback .instruction > * { #puzzle .feedback .instruction > * {
display: block; display: block;
} }
@ -60,6 +66,16 @@
opacity: 1; opacity: 1;
} }
#puzzle .after .continue {
display: flex;
height: 50%;
width: 100%;
font-size: 1.3em;
background: #3893E8;
color: #fff;
align-items: center;
}
#puzzle move { #puzzle move {
justify-content: space-between; justify-content: space-between;
} }
@ -119,39 +135,52 @@ body.dark #puzzle .timeline > * {
color: #444; color: #444;
font-size: 1.5em; font-size: 1.5em;
} }
.puzzle_box { .puzzle_side .side_box.metas {
padding: 0 7px; padding: 0 7px;
} }
.puzzle_box .game_infos { .puzzle_side .game_infos {
margin-top: 14px!important; margin-top: 14px!important;
padding-bottom: 14px!important; padding-bottom: 14px!important;
} }
.puzzle_box .header a { .puzzle_side .header a {
color: #3893E8; color: #3893E8;
} }
.puzzle_box .game_infos.puzzle { .puzzle_side .game_infos.puzzle {
padding-left: 56px; padding-left: 56px;
} }
.puzzle_box .game_infos.puzzle::before { .puzzle_side .game_infos.puzzle::before {
font-size: 47px; font-size: 47px;
} }
.puzzle_box .puzzle .header a { .puzzle_side .puzzle .header a {
font-size: 1.2em; font-size: 1.2em;
} }
.puzzle_box .game_infos.game { .puzzle_side .game_infos.game {
border: none; border: none;
padding-bottom: 0!important; padding-bottom: 0!important;
} }
.puzzle_box .players { .puzzle_side .players {
padding-bottom: 14px; padding-bottom: 14px;
} }
.puzzle_box .players .color-icon::before { .puzzle_side .players .color-icon::before {
width: 30px; width: 30px;
display: inline-block; display: inline-block;
text-align: center; 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 { body.dark #puzzle .timeline a.new {
background: #555; background: #555;
color: #ddd; color: #ddd;

View File

@ -30,10 +30,12 @@ module.exports = function(opts, i18n) {
var initiate = function(fromData) { var initiate = function(fromData) {
data = fromData; data = fromData;
console.log(data);
tree = treeBuild(treeOps.reconstruct(data.game.treeParts)); tree = treeBuild(treeOps.reconstruct(data.game.treeParts));
var initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root)); var initialPath = treePath.fromNodeList(treeOps.mainlineNodeList(tree.root));
vm.mode = 'play'; // play | try | view vm.mode = 'play'; // play | try | view
vm.loading = false; vm.loading = false;
vm.round = null;
vm.justPlayed = null; vm.justPlayed = null;
vm.resultSent = false; vm.resultSent = false;
vm.lastFeedback = 'init'; vm.lastFeedback = 'init';
@ -50,13 +52,17 @@ module.exports = function(opts, i18n) {
setTimeout(function() { setTimeout(function() {
vm.canViewSolution = true; vm.canViewSolution = true;
m.redraw(); m.redraw();
}, 5000); // }, 5000);
}, 50);
socket.setDestsCache(data.game.destsCache); socket.setDestsCache(data.game.destsCache);
moveTest = moveTestBuild(vm, data.puzzle); moveTest = moveTestBuild(vm, data.puzzle);
showGround(); showGround();
m.redraw(); m.redraw();
if (window.history.pushState)
window.history.replaceState(null, null, '/training/' + data.puzzle.id);
}; };
var showGround = function() { var showGround = function() {
@ -72,6 +78,7 @@ module.exports = function(opts, i18n) {
}; };
var config = { var config = {
fen: node.fen, fen: node.fen,
orientation: data.puzzle.color,
turnColor: color, turnColor: color,
movable: movable, movable: movable,
premovable: { premovable: {
@ -91,8 +98,6 @@ module.exports = function(opts, i18n) {
config.movable.color = data.puzzle.color; config.movable.color = data.puzzle.color;
config.premovable.enabled = true; config.premovable.enabled = true;
} }
console.log(dests, config);
vm.cgConfig = config;
if (!ground) ground = groundBuild(data, config, userMove); if (!ground) ground = groundBuild(data, config, userMove);
ground.set(config); ground.set(config);
if (!dests) getDests(); if (!dests) getDests();
@ -145,9 +150,7 @@ module.exports = function(opts, i18n) {
ground.playPremove(); ground.playPremove();
var progress = moveTest(); var progress = moveTest();
// console.log(progress, vm.node);
if (progress) applyProgress(progress); if (progress) applyProgress(progress);
// preparePremoving();
m.redraw(); m.redraw();
}; };
@ -193,7 +196,6 @@ module.exports = function(opts, i18n) {
} }
} else if (progress && progress.orig) { } else if (progress && progress.orig) {
vm.lastFeedback = 'good'; vm.lastFeedback = 'good';
// console.log(tree);
setTimeout(function() { setTimeout(function() {
socket.sendAnaMove(progress); socket.sendAnaMove(progress);
}, 500); }, 500);
@ -206,7 +208,9 @@ module.exports = function(opts, i18n) {
vm.loading = true; vm.loading = true;
xhr.round(data.puzzle.id, win).then(function(res) { xhr.round(data.puzzle.id, win).then(function(res) {
data.user = res.user; data.user = res.user;
vm.round = res.round;
vm.loading = false; vm.loading = false;
m.redraw();
}); });
}; };
@ -214,6 +218,7 @@ module.exports = function(opts, i18n) {
vm.loading = true; vm.loading = true;
xhr.nextPuzzle().then(function(d) { xhr.nextPuzzle().then(function(d) {
// pushState(cfg); // pushState(cfg);
vm.round = null;
vm.loading = false; vm.loading = false;
initiate(d); 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); initiate(opts.data);
keyboard.bind({ keyboard.bind({
@ -317,8 +328,6 @@ module.exports = function(opts, i18n) {
userJump: userJump userJump: userJump
}); });
console.log(data);
return { return {
vm: vm, vm: vm,
getData: function() { getData: function() {
@ -331,6 +340,7 @@ module.exports = function(opts, i18n) {
userJump: userJump, userJump: userJump,
viewSolution: viewSolution, viewSolution: viewSolution,
nextPuzzle: nextPuzzle, nextPuzzle: nextPuzzle,
recentHash: recentHash,
trans: lichess.trans(opts.i18n), trans: lichess.trans(opts.i18n),
socketReceive: socket.receive socketReceive: socket.receive
}; };

View File

@ -3,7 +3,7 @@ var treeOps = require('tree').ops;
module.exports = function(tree, initialNode, solution, color) { module.exports = function(tree, initialNode, solution, color) {
tree.ops.updateAll(solution, function(node) { 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); var solutionNode = treeOps.childById(initialNode, solution.id);

View File

@ -24,7 +24,8 @@ var m = require('mithril');
// } // }
module.exports = function(ctrl) { 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', [ // (!ctrl.hasEverVoted.get() && ctrl.data.puzzle.enabled && ctrl.data.voted === null) ? m('div.please_vote', [
// m('p.first', [ // m('p.first', [
// m('strong', ctrl.trans('wasThisPuzzleAnyGood')), // m('strong', ctrl.trans('wasThisPuzzleAnyGood')),
@ -36,8 +37,11 @@ module.exports = function(ctrl) {
// ) // )
// ]) : null, // ]) : null,
// (ctrl.data.puzzle.enabled && ctrl.data.user) ? renderVote(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 onclick: ctrl.nextPuzzle
}, ctrl.trans.noarg('continueTraining')) }, [
m('i[data-icon=G]'),
ctrl.trans.noarg('continueTraining')
])
]); ]);
} }

View File

@ -1,8 +1,13 @@
var m = require('mithril'); var m = require('mithril');
var historySize = 15; 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; if (!data.user) return;
var slots = []; var slots = [];
for (var i = 0; i < historySize; i++) slots[i] = data.user.recent[i] || null; for (var i = 0; i < historySize; i++) slots[i] = data.user.recent[i] || null;

View File

@ -117,7 +117,7 @@ module.exports = function(ctrl) {
]), ]),
m('div.underboard', [ m('div.underboard', [
m('div.center', [ m('div.center', [
historyView(ctrl.getData()) historyView(ctrl)
]) ])
]) ])
]; ];

View File

@ -5,30 +5,37 @@ function strong(txt) {
return '<strong>' + txt + '</strong>'; return '<strong>' + txt + '</strong>';
} }
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; function puzzleInfos(ctrl, puzzle) {
var game = ctrl.getData().game; 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 [ 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="-"]', { m('div.game_infos.game[data-icon="-"]', {
'data-icon': game.perf.icon 'data-icon': game.perf.icon
}, [ }, [
m('div.header', [ m('div.header', [
'From game ', 'From game ',
ctrl.vm.mode === 'view' ? m('a.title', { ctrl.vm.mode === 'view' ? m('a.title', {
href: '/' + game.id href: '/' + game.id + '/' + puzzle.color + '#' + puzzle.initialPly
}, '#' + game.id) : '#' + game.id.slice(0, 5) + '...', }, '#' + game.id) : '#' + game.id.slice(0, 5) + '...',
m('p', [ m('p', [
game.clock, ' • ', 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 += ' <span class="rp ' + klass + '">' + diff + '</span>';
}
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)
];
}; };