more puzzle UI WIP

This commit is contained in:
Thibault Duplessis 2016-11-28 16:17:23 +01:00
parent 1d838ff770
commit ffc73161eb
15 changed files with 425 additions and 249 deletions

View file

@ -1,6 +1,7 @@
@(title: String, evenMoreCss: Option[Html] = None, evenMoreJs: Option[Html] = None, openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
@moreCss = {
@cssTag("analyse.css")
@cssTag("puzzle.css")
@evenMoreCss
}

View file

@ -21,5 +21,5 @@ description = s"Tactic puzzle #${puzzle.id}: " + puzzle.color.fold(
trans.findTheBestMoveForWhite,
trans.findTheBestMoveForBlack
).str() + s" Played by ${puzzle.attempts} players.").some) {
<div class="puzzle_app cg-512">@miniBoardContent</div>
<div id="puzzle" class="cg-512">@miniBoardContent</div>
}

View file

@ -11,26 +11,30 @@ object GameJson {
import lila.game.JsonView._
private val cache = lila.memo.AsyncCache[Game.ID, JsObject](
case class CacheKey(gameId: Game.ID, plies: Int)
private val cache = lila.memo.AsyncCache[CacheKey, JsObject](
name = "puzzle.gameJson",
f = generate,
maxCapacity = 500)
def apply(gameId: Game.ID): Fu[JsObject] = cache(gameId)
def apply(gameId: Game.ID, plies: Int): Fu[JsObject] = cache(CacheKey(gameId, plies))
def generate(gameId: Game.ID): Fu[JsObject] =
(GameRepo game gameId).flatten(s"Missing puzzle game $gameId!") map { game =>
Json.obj(
"id" -> game.id,
"speed" -> game.speed.key,
"perf" -> PerfPicker.key(game),
"rated" -> game.rated,
"winner" -> game.winnerColor.map(_.name),
"turns" -> game.turns,
"status" -> game.status,
"tournamentId" -> game.tournamentId,
"createdAt" -> game.createdAt,
"treeParts" -> partitionTreeJsonWriter.writes(TreeBuilder(game))
).noNull
}
def generate(ck: CacheKey): Fu[JsObject] = ck match {
case CacheKey(gameId, plies) =>
(GameRepo game gameId).flatten(s"Missing puzzle game $gameId!") map { game =>
Json.obj(
"id" -> game.id,
"speed" -> game.speed.key,
"perf" -> PerfPicker.key(game),
"rated" -> game.rated,
"winner" -> game.winnerColor.map(_.name),
"turns" -> game.turns,
"status" -> game.status,
"tournamentId" -> game.tournamentId,
"createdAt" -> game.createdAt,
"treeParts" -> partitionTreeJsonWriter.writes(TreeBuilder(game, plies))
).noNull
}
}
}

View file

@ -18,7 +18,7 @@ object JsonView {
round: Option[Round] = None,
win: Option[Boolean] = None,
voted: Option[Boolean]): Fu[JsObject] =
(!isMobileApi ?? GameJson(puzzle.gameId).map(_.some)) map { gameJson =>
(!isMobileApi ?? GameJson(puzzle.gameId, puzzle.initialPly).map(_.some)) map { gameJson =>
Json.obj(
"game" -> gameJson,
"puzzle" -> Json.obj(

View file

@ -19,9 +19,7 @@ case class Puzzle(
attempts: Int,
mate: Boolean) {
def initialPly: Option[Int] = fen.split(' ').lastOption flatMap parseIntOption map { move =>
move * 2 + color.fold(0, 1)
}
def initialPly: Int = history.size
def withVote(f: AggregateVote => AggregateVote) = copy(vote = f(vote))

View file

@ -9,8 +9,8 @@ object TreeBuilder {
private type Ply = Int
def apply(game: Game): tree.Root = {
chess.Replay.gameMoveWhileValid(game.pgnMoves, Forsyth.initial, game.variant) match {
def apply(game: Game, plies: Int): tree.Root = {
chess.Replay.gameMoveWhileValid(game.pgnMoves take plies, Forsyth.initial, game.variant) match {
case (init, games, error) =>
error foreach logChessError(game.id)
val fen = Forsyth >> init

View file

@ -2009,7 +2009,7 @@ lichess.notifyApp = (function() {
function startPuzzle(cfg) {
var puzzle;
cfg.element = document.querySelector('#lichess .puzzle_app');
cfg.element = document.querySelector('#puzzle');
cfg.sideElement = document.querySelector('#site_header .side_box');
lichess.socket = lichess.StrongSocket('/socket', 0, {
options: {

View file

@ -1,16 +1,3 @@
div.board_wrap {
float: left;
width: 514px;
min-height: 558px;
}
div.moves_wrap {
margin-left: 532px;
position: relative;
}
p.rematch_wrap {
margin-top: 31px;
text-align: center;
}
div.analysis_menu {
text-align: center;
border-top: 1px solid #c0c0c0;

View file

@ -0,0 +1,251 @@
#puzzle {
position: relative;
min-height: 600px;
}
#puzzle .board_and_ground {
display: flex;
}
#puzzle div.box {
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 7px;
}
#puzzle > .side {
position: absolute;
top: 64px;
left: -222px;
width: 212px;
}
#puzzle > .side h1 {
text-align: center;
font-size: 1.4em;
font-weight: bold;
margin-bottom: 10px;
}
#puzzle > .side div.box {
margin-bottom: 20px;
}
#puzzle div.box > p, div.training div.box > div {
line-height: 1.8em;
}
#puzzle > .side .register {
margin-top: 1em;
}
#puzzle > .side .register .signup {
margin: 1em auto;
text-align: center;
}
#puzzle > .side .user_chart {
margin: 0 0 -10px -9px;
}
#puzzle > .side .chart_container {
margin-top: 30px;
}
#puzzle > .side .chart_container p {
margin-bottom: 3px;
}
#puzzle div.comment {
padding: 10px 5px;
text-align: center;
margin-bottom: 1em;
}
#puzzle div.comment h3 {
font-size: inherit;
margin: 4px;
display: block;
}
#puzzle .comment.great,
#puzzle .comment.win {
background: #DFF0D8;
color: #3c763d;
}
#puzzle .comment.loss,
#puzzle .comment.fail {
color: #a94442;
background: #ebccd1;
}
#puzzle .comment .rating {
font-size: 2em;
vertical-align: -3px;
padding-left: 5px;
}
#puzzle > .center {
width: 512px;
}
#puzzle .right {
display: flex;
flex-flow: column nowrap;
justify-content: center;
padding-left: 15px;
width: 242px;
}
#puzzle .right .spinner {
width: 90px;
height: 90px;
}
#puzzle .right .box input {
width: 214px;
}
#puzzle .right h2 {
font-size: 1.4em;
margin-bottom: 7px;
}
#puzzle .please_vote {
background: #DFF0D8;
color: #3c763d;
margin-bottom: 1em;
text-align: center;
padding: 10px 5px;
}
#puzzle .please_vote .then,
#puzzle .please_vote.thanks .first {
display: none;
}
#puzzle .please_vote.thanks .then {
display: block;
}
#puzzle .right .player {
display: flex;
align-items: center;
margin-bottom: 15px;
}
#puzzle .right .no-square {
width: 64px;
height: 64px;
}
.is3d #puzzle .right div.no-square {
height: 82px;
}
#puzzle .right piece {
position: inherit;
display: block;
width: 100%;
height: 100%;
}
#puzzle .right .instruction > * {
display: block;
}
#puzzle .right .instruction strong {
font-size: 1.5em;
}
#puzzle .right .control {
text-align: center;
font-size: 1.3em;
margin-bottom: 10px;
}
#puzzle .right .control .giveup {
visibility: hidden;
font-weight: normal;
transition: 1s;
opacity: 0;
}
#puzzle .right .control .giveup.revealed {
opacity: 1;
visibility: visible;
}
#puzzle .right .continue_wrap {
width: 100%;
text-align: center;
}
#puzzle .right .retry {
display: inline-block;
margin-top: 20px;
}
#puzzle .right .continue {
display: inline-block;
white-space: nowrap;
font-size: 1.3em;
font-weight: bold;
}
#puzzle .game_control {
text-align: center;
margin-top: 15px;
}
#puzzle .game_control a {
font-size: 16px;
height: 22px;
display:inline-block;
}
#puzzle #GameButtons {
display: inline-block;
margin-left: 10px;
}
#puzzle #GameButtons a {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
margin-left: -2px;
margin-right: -3px;
}
#puzzle #GameButtons a.disabled::before {
opacity: 0.5;
text-shadow: none !important;
}
#puzzle div.upvote {
float: right;
text-align: center;
display: flex;
flex-flow: column;
}
#puzzle div.upvote a {
cursor: default;
}
#puzzle div.upvote.enabled a {
cursor: pointer;
}
#puzzle div.upvote a {
font-size: 24px;
}
#puzzle div.upvote.enabled a:hover,
#puzzle div.upvote a.active {
color: #d85000;
}
#puzzle div.upvote span.count {
font-size: 24px;
line-height: 1;
}
#puzzle .undertable td {
text-align: left;
padding-left: 10px;
}
#puzzle .timeline {
display: flex;
margin-top: 10px;
}
#puzzle .timeline > * {
flex: 1 0;
background: #ccc;
color: #fff;
margin-left: 1px;
box-sizing: border-box;
opacity: 0.8;
height: 30px;
line-height: 30px;
}
body.dark #puzzle .timeline > * {
background: #444;
}
#puzzle .timeline a:first-child {
margin-left: 0;
}
#puzzle .timeline a:hover {
opacity: 1;
}
#puzzle .timeline a.current {
background: #3893E8;
}
#puzzle .timeline a.win {
background: #759900;
}
#puzzle .timeline a.loss {
background: #dc322f;
}
#puzzle .timeline a.new {
background: #fff;
color: #444;
font-size: 1.5em;
}
body.dark #puzzle .timeline a.new {
background: #555;
color: #ddd;
}

View file

@ -1,213 +1,7 @@
#puzzle {
position: relative;
min-height: 600px;
}
#puzzle .board_and_ground {
display: flex;
}
#puzzle div.box {
margin-bottom: 1em;
border: 1px solid #ccc;
padding: 7px;
}
#puzzle > .side {
position: absolute;
top: 64px;
left: -222px;
width: 212px;
}
#puzzle > .side h1 {
text-align: center;
font-size: 1.4em;
font-weight: bold;
margin-bottom: 10px;
}
#puzzle > .side div.box {
margin-bottom: 20px;
}
#puzzle div.box > p, div.training div.box > div {
line-height: 1.8em;
}
#puzzle > .side .register {
margin-top: 1em;
}
#puzzle > .side .register .signup {
margin: 1em auto;
text-align: center;
}
#puzzle > .side .user_chart {
margin: 0 0 -10px -9px;
}
#puzzle > .side .chart_container {
margin-top: 30px;
}
#puzzle > .side .chart_container p {
margin-bottom: 3px;
}
#puzzle div.comment {
padding: 10px 5px;
text-align: center;
margin-bottom: 1em;
}
#puzzle div.comment h3 {
font-size: inherit;
margin: 4px;
display: block;
}
#puzzle .comment.great,
#puzzle .comment.win {
background: #DFF0D8;
color: #3c763d;
}
#puzzle .comment.loss,
#puzzle .comment.fail {
color: #a94442;
background: #ebccd1;
}
#puzzle .comment .rating {
font-size: 2em;
vertical-align: -3px;
padding-left: 5px;
}
#puzzle > .center {
width: 512px;
}
#puzzle .right {
display: flex;
flex-flow: column nowrap;
justify-content: center;
padding-left: 15px;
width: 242px;
}
#puzzle .right .spinner {
width: 90px;
height: 90px;
}
#puzzle .right .box input {
width: 214px;
}
#puzzle .right h2 {
font-size: 1.4em;
margin-bottom: 7px;
}
#puzzle .please_vote {
background: #DFF0D8;
color: #3c763d;
margin-bottom: 1em;
text-align: center;
padding: 10px 5px;
}
#puzzle .please_vote .then,
#puzzle .please_vote.thanks .first {
display: none;
}
#puzzle .please_vote.thanks .then {
display: block;
}
#puzzle .right .player {
display: flex;
align-items: center;
margin-bottom: 15px;
}
#puzzle .right .no-square {
width: 64px;
height: 64px;
}
.is3d #puzzle .right div.no-square {
height: 82px;
}
#puzzle .right piece {
position: inherit;
display: block;
width: 100%;
height: 100%;
}
#puzzle .right .instruction > * {
display: block;
}
#puzzle .right .instruction strong {
font-size: 1.5em;
}
#puzzle .right .control {
text-align: center;
font-size: 1.3em;
margin-bottom: 10px;
}
#puzzle .right .control .giveup {
visibility: hidden;
font-weight: normal;
transition: 1s;
opacity: 0;
}
#puzzle .right .control .giveup.revealed {
opacity: 1;
visibility: visible;
}
#puzzle .right .continue_wrap {
width: 100%;
text-align: center;
}
#puzzle .right .retry {
display: inline-block;
margin-top: 20px;
}
#puzzle .right .continue {
display: inline-block;
white-space: nowrap;
font-size: 1.3em;
font-weight: bold;
}
#puzzle .game_control {
text-align: center;
margin-top: 15px;
}
#puzzle .game_control a {
font-size: 16px;
height: 22px;
display:inline-block;
}
#puzzle #GameButtons {
display: inline-block;
margin-left: 10px;
}
#puzzle #GameButtons a {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
margin-left: -2px;
margin-right: -3px;
}
#puzzle #GameButtons a.disabled::before {
opacity: 0.5;
text-shadow: none !important;
}
#puzzle div.upvote {
float: right;
text-align: center;
display: flex;
flex-flow: column;
}
#puzzle div.upvote a {
cursor: default;
}
#puzzle div.upvote.enabled a {
cursor: pointer;
}
#puzzle div.upvote a {
font-size: 24px;
}
#puzzle div.upvote.enabled a:hover,
#puzzle div.upvote a.active {
color: #d85000;
}
#puzzle div.upvote span.count {
font-size: 24px;
line-height: 1;
}
#puzzle .undertable td {
text-align: left;
padding-left: 10px;
justify-content: center;
}
#puzzle .timeline {
display: flex;
margin-top: 10px;

26
ui/puzzle/src/control.js Normal file
View file

@ -0,0 +1,26 @@
var treePath = require('tree').path;
module.exports = {
canGoForward: function(ctrl) {
return ctrl.vm.node.children.length > 0;
},
next: function(ctrl) {
var child = ctrl.vm.node.children[0];
if (!child) return;
ctrl.userJump(ctrl.vm.path + child.id);
},
prev: function(ctrl) {
ctrl.userJump(treePath.init(ctrl.vm.path));
},
last: function(ctrl) {
ctrl.userJump(treePath.fromNodeList(ctrl.vm.mainline));
},
first: function(ctrl) {
ctrl.userJump(treePath.root);
}
};

View file

@ -1,10 +1,12 @@
var m = require('mithril');
var treeBuild = require('tree').build;
var treeOps = require('tree').ops;
var treePath = require('tree').path;
var cevalCtrl = require('ceval').ctrl;
var readDests = require('chess').readDests;
var k = Mousetrap;
var chessground = require('chessground');
var keyboard = require('./keyboard');
var opposite = chessground.util.opposite;
var groundBuild = require('./ground');
var socketBuild = require('./socket');
@ -22,6 +24,7 @@ module.exports = function(opts, i18n) {
var data = opts.data;
var tree = treeBuild(treeOps.reconstruct(opts.data.game.treeParts));
var ground;
var ceval;
var setPath = function(path) {
vm.path = path;
@ -30,7 +33,7 @@ module.exports = function(opts, i18n) {
vm.mainline = treeOps.mainlineNodeList(tree.root);
};
setPath('');
setPath(treePath.fromNodeList(treeOps.mainlineNodeList(tree.root)));
var showGround = function() {
var node = vm.node;
@ -93,6 +96,34 @@ module.exports = function(opts, i18n) {
ground.playPremove();
};
var instanciateCeval = function(failsafe) {
ceval = cevalCtrl({
variant: data.game.variant,
possible: true,
emit: function(res) {
tree.updateAt(res.work.path, function(node) {
if (node.ceval && node.ceval.depth >= res.eval.depth) return;
node.ceval = res.eval;
// if (res.work.path === vm.path) {
// setAutoShapes();
// m.redraw();
// }
});
},
setAutoShapes: $.noop,
failsafe: failsafe,
onCrash: function(e) {
console.log('Local eval failed!', e);
if (ceval.pnaclSupported) {
console.log('Retrying in failsafe mode');
instanciateCeval(true);
startCeval();
}
}
});
};
instanciateCeval();
var gameOver = function() {
if (vm.node.dests !== '') return false;
if (vm.node.check) {
@ -139,6 +170,11 @@ module.exports = function(opts, i18n) {
showGround();
keyboard.bind({
vm: vm,
userJump: userJump
});
console.log(data);
return {

View file

@ -29,7 +29,7 @@ function makeConfig(data, config, onMove) {
},
animation: {
enabled: true,
duration: data.pref.animationDuration
duration: data.animation.duration
},
disableContextMenu: true
};

37
ui/puzzle/src/keyboard.js Normal file
View file

@ -0,0 +1,37 @@
var control = require('./control');
var m = require('mithril');
function preventing(f) {
return function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
// internet explorer
e.returnValue = false;
}
f();
};
}
module.exports = {
bind: function(ctrl) {
if (!window.Mousetrap) return;
var kbd = window.Mousetrap;
kbd.bind(['left', 'k'], preventing(function() {
control.prev(ctrl);
m.redraw();
}));
kbd.bind(['right', 'j'], preventing(function() {
control.next(ctrl);
m.redraw();
}));
kbd.bind(['up', '0'], preventing(function() {
control.first(ctrl);
m.redraw();
}));
kbd.bind(['down', '$'], preventing(function() {
control.last(ctrl);
m.redraw();
}));
}
};

View file

@ -1,6 +1,8 @@
var m = require('mithril');
var chessground = require('chessground');
var bindOnce = require('common').bindOnce;
var treeView = require('./treeView');
var control = require('./control');
function renderOpeningBox(ctrl) {
var opening = ctrl.tree.getOpening(ctrl.vm.nodeList);
@ -45,6 +47,46 @@ function visualBoard(ctrl) {
]);
}
function dataAct(e) {
return e.target.getAttribute('data-act') ||
e.target.parentNode.getAttribute('data-act');
}
function jumpButton(icon, effect) {
return {
tag: 'button',
attrs: {
'data-act': effect,
'data-icon': icon
}
};
}
var cachedButtons = (function() {
return m('div.jumps', [
jumpButton('W', 'first'),
jumpButton('Y', 'prev'),
jumpButton('X', 'next'),
jumpButton('V', 'last')
])
})();
function buttons(ctrl) {
return m('div.game_control', {
config: bindOnce('mousedown', function(e) {
var action = dataAct(e);
if (action === 'prev') control.prev(ctrl);
else if (action === 'next') control.next(ctrl);
else if (action === 'first') control.first(ctrl);
else if (action === 'last') control.last(ctrl);
// else if (action === 'explorer') ctrl.explorer.toggle();
// else if (action === 'menu') ctrl.actionMenu.toggle();
})
}, [
cachedButtons
]);
}
var firstRender = true;
module.exports = function(ctrl) {
@ -66,7 +108,7 @@ module.exports = function(ctrl) {
// cevalView.renderCeval(ctrl),
// cevalView.renderPvs(ctrl),
renderAnalyse(ctrl),
// buttons(ctrl)
buttons(ctrl)
])
])
]),