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 {
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)

View File

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

View File

@ -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
}, {

View File

@ -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,

View File

@ -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

View File

@ -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
)
}

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -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
};

View File

@ -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);

View File

@ -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')
])
]);
}

View File

@ -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;

View File

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

View File

@ -5,30 +5,37 @@ function strong(txt) {
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;
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 += ' <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)
];
};