more work on puzzle UI
parent
6ca81f503b
commit
40066caf79
|
@ -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)
|
||||
|
|
|
@ -18,7 +18,7 @@ i18n: @jsI18n()
|
|||
}
|
||||
|
||||
@side = {
|
||||
<div class="side_box puzzle_box"></div>
|
||||
<div class="puzzle_side"></div>
|
||||
}
|
||||
|
||||
@base.layout(
|
||||
|
|
|
@ -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
|
||||
}, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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')
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -117,7 +117,7 @@ module.exports = function(ctrl) {
|
|||
]),
|
||||
m('div.underboard', [
|
||||
m('div.center', [
|
||||
historyView(ctrl.getData())
|
||||
historyView(ctrl)
|
||||
])
|
||||
])
|
||||
];
|
||||
|
|
|
@ -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)
|
||||
];
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue