new puzzle UI WIP

This commit is contained in:
Thibault Duplessis 2016-11-28 15:33:51 +01:00
parent 8eb737fae9
commit 1d838ff770
16 changed files with 341 additions and 176 deletions

View file

@ -2,11 +2,11 @@
@evenMoreJs = {
@embedJs {
LichessPuzzle({
element: document.querySelector('#lichess .round'),
lichess = lichess || {};
lichess.puzzle = {
data: @Html(toJson(data)),
i18n: @jsI18n()
});
};
}
}
@ -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="round cg-512">@miniBoardContent</div>
<div class="puzzle_app cg-512">@miniBoardContent</div>
}

View file

@ -35,7 +35,8 @@ object JsonView {
"vote" -> puzzle.vote.sum
),
"pref" -> Json.obj(
"coords" -> pref.coords
"coords" -> pref.coords,
"rookCastle" -> pref.rookCastle
),
"chessground" -> Json.obj(
"highlight" -> Json.obj(

View file

@ -355,6 +355,7 @@ lichess.notifyApp = (function() {
else if (lichess.user_analysis) startUserAnalysis(document.getElementById('lichess'), lichess.user_analysis);
else if (lichess.study) startStudy(document.getElementById('lichess'), lichess.study);
else if (lichess.lobby) startLobby(document.getElementById('hooks_wrap'), lichess.lobby);
else if (lichess.puzzle) startPuzzle(lichess.puzzle);
else if (lichess.tournament) startTournament(document.getElementById('tournament'), lichess.tournament);
else if (lichess.simul) startSimul(document.getElementById('simul'), lichess.simul);
@ -1300,7 +1301,7 @@ lichess.notifyApp = (function() {
window.history.replaceState(null, null, '/');
};
lichess.socket = lichess.StrongSocket(
'/lobby/socket/v1',
'/lobby/socket/v2',
cfg.data.version, {
receive: function(t, d) {
lobby.socketReceive(t, d);
@ -2002,6 +2003,30 @@ lichess.notifyApp = (function() {
}
}
////////////////
// puzzle.js //
////////////////
function startPuzzle(cfg) {
var puzzle;
cfg.element = document.querySelector('#lichess .puzzle_app');
cfg.sideElement = document.querySelector('#site_header .side_box');
lichess.socket = lichess.StrongSocket('/socket', 0, {
options: {
name: "puzzle"
},
params: {
ran: "--ranph--"
},
receive: function(t, d) {
puzzle.socketReceive(t, d);
}
});
cfg.socketSend = lichess.socket.send;
puzzle = LichessPuzzle(cfg);
topMenuIntent();
}
/////////////// forum.js ////////////////////
$('#lichess_forum').on('click', 'a.delete', function() {

View file

@ -1,4 +1,4 @@
var util = require('../util');
var readDrops = require('chess').readDrops;
module.exports = {
@ -8,7 +8,7 @@ module.exports = {
if (piece.role === 'pawn' && (pos[1] === '1' || pos[1] === '8')) return false;
var drops = util.readDrops(possibleDrops);
var drops = readDrops(possibleDrops);
if (drops === null) return true;

View file

@ -7,6 +7,8 @@ var actionMenu = require('./actionMenu').controller;
var autoplay = require('./autoplay');
var promotion = require('./promotion');
var util = require('./util');
var readDests = require('chess').readDests;
var readDrops = require('chess').readDrops;
var storedProp = require('common').storedProp;
var throttle = require('common').throttle;
var socket = require('./socket');
@ -115,8 +117,8 @@ module.exports = function(opts) {
var showGround = function() {
var node = this.vm.node;
var color = node.ply % 2 === 0 ? 'white' : 'black';
var dests = util.readDests(node.dests);
var drops = util.readDrops(node.drops);
var dests = readDests(node.dests);
var drops = readDrops(node.drops);
var config = {
fen: node.fen,
turnColor: color,
@ -147,8 +149,7 @@ module.exports = function(opts) {
}.bind(this);
var getDests = throttle(800, false, function() {
if (this.vm.node.dests) return;
this.socket.sendAnaDests({
if (!this.vm.node.dests) this.socket.sendAnaDests({
variant: this.data.game.variant.key,
fen: this.vm.node.fen,
path: this.vm.path

View file

@ -1,4 +1,4 @@
var util = require('./util');
var synthetic = require('./util').synthetic;
var initialBoardFen = require('chessground').fen.initial;
module.exports = function(send, ctrl) {
@ -18,7 +18,7 @@ module.exports = function(send, ctrl) {
}
} : {};
if (!util.synthetic(ctrl.data)) setTimeout(function() {
if (!synthetic(ctrl.data)) setTimeout(function() {
send("startWatching", ctrl.data.game.id);
}, 1000);

View file

@ -1,4 +1,3 @@
var piotr2key = require('./piotr');
var fixCrazySan = require('chess').fixCrazySan;
var common = require('common');
var m = require('mithril');
@ -8,20 +7,6 @@ var plyToTurn = function(ply) {
}
module.exports = {
readDests: function(lines) {
if (!common.defined(lines)) return null;
var dests = {};
if (lines) lines.split(' ').forEach(function(line) {
dests[piotr2key[line[0]]] = line.split('').slice(1).map(function(c) {
return piotr2key[c];
});
});
return dests;
},
readDrops: function(line) {
if (!common.defined(line) || line === null) return null;
return line.match(/.{2}/g) || [];
},
synthetic: function(data) {
return data.game.id === 'synthetic';
},

View file

@ -1,3 +1,5 @@
var piotr = require('./piotr');
module.exports = {
initialFen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
@ -12,5 +14,20 @@ module.exports = {
renderEval: function(e) {
e = Math.max(Math.min(Math.round(e / 10) / 10, 99), -99);
return (e > 0 ? '+' : '') + e;
},
readDests: function(lines) {
if (typeof lines === 'undefined') return null;
var dests = {};
if (lines) lines.split(' ').forEach(function(line) {
dests[piotr[line[0]]] = line.split('').slice(1).map(function(c) {
return piotr[c];
});
});
return dests;
},
readDrops: function(line) {
if (typeof line === 'undefined' || line === null) return null;
return line.match(/.{2}/g) || [];
}
};

View file

@ -1,35 +1,152 @@
var m = require('mithril');
var makeTree = require('tree').tree;
var treeBuild = require('tree').build;
var treeOps = require('tree').ops;
var cevalCtrl = require('ceval').ctrl;
var readDests = require('chess').readDests;
var k = Mousetrap;
var chessground = require('chessground');
var partial = chessground.util.partial;
var opposite = chessground.util.opposite;
var groundBuild = require('./ground');
var socketBuild = require('./socket');
var throttle = require('common').throttle;
var xhr = require('./xhr');
var sound = require('./sound');
module.exports = function(data, i18n) {
module.exports = function(opts, i18n) {
var vm = {
loading: false
loading: false,
justPlayed: null,
};
var tree = makeTree(treeOps.reconstruct(data.game.treeParts));
var data = opts.data;
var tree = treeBuild(treeOps.reconstruct(opts.data.game.treeParts));
var ground;
var setPath = function(path) {
vm.path = path;
vm.nodeList = tree.getNodeList(path);
vm.node = treeOps.last(vm.nodeList);
vm.mainline = treeOps.mainlineNodeList(tree.root);
}.bind(this);
};
setPath('');
setPath(initialPath);
var showGround = function() {
var node = vm.node;
var color = node.ply % 2 === 0 ? 'white' : 'black';
var dests = readDests(node.dests);
var config = {
fen: node.fen,
turnColor: color,
movable: {
color: (dests && Object.keys(dests).length > 0) ? color : null,
dests: dests || {}
},
check: node.check,
lastMove: uciToLastMove(node.uci)
};
if (!dests && !node.check) {
// premove while dests are loading from server
// can't use when in check because it highlights the wrong king
config.turnColor = opposite(color);
config.movable.color = color;
}
vm.cgConfig = config;
if (!ground) ground = groundBuild(data, config, userMove);
ground.set(config);
if (!dests) getDests();
};
var userMove = function(orig, dest, capture) {
vm.justPlayed = orig;
sound[capture ? 'capture' : 'move']();
sendMove(orig, dest);
};
var getDests = throttle(800, false, function() {
if (!vm.node.dests) socket.sendAnaDests({
fen: vm.node.fen,
path: vm.path
});
});
var uciToLastMove = function(uci) {
if (!uci) return;
return [uci.substr(0, 2), uci.substr(2, 2)]; // assuming standard chess
};
var addNode = function(node, path) {
var newPath = tree.addNode(node, path);
jump(newPath);
m.redraw();
ground.playPremove();
};
var addDests = function(dests, path, opening) {
tree.addDests(dests, path, opening);
if (path === vm.path) {
showGround();
m.redraw();
if (gameOver()) ceval.stop();
}
ground.playPremove();
};
var gameOver = function() {
if (vm.node.dests !== '') return false;
if (vm.node.check) {
var san = vm.node.san;
var checkmate = san && san[san.length - 1] === '#';
return checkmate;
}
return true;
};
var jump = function(path) {
var pathChanged = path !== vm.path;
setPath(path);
showGround();
if (pathChanged) {
if (!vm.node.uci) sound.move(); // initial position
else if (vm.node.uci.indexOf(vm.justPlayed) !== 0) {
if (vm.node.san.indexOf('x') !== -1) sound.capture();
else sound.move();
}
if (/\+|\#/.test(vm.node.san)) sound.check();
// this.vm.threatMode = false;
// this.ceval.stop();
// this.startCeval();
}
vm.justPlayed = null;
vm.autoScrollRequested = true;
};
var userJump = function(path) {
ground.selectSquare(null);
jump(path);
};
var socket = socketBuild({
send: opts.socketSend,
addNode: addNode,
addDests: addDests,
reset: function() {
showGround();
m.redraw();
}
});
showGround();
console.log(data);
return {
vm: vm,
trans: lichess.trans(i18n)
tree: tree,
ground: ground,
userJump: userJump,
trans: lichess.trans(opts.i18n),
socketReceive: socket.receive
};
}

40
ui/puzzle/src/ground.js Normal file
View file

@ -0,0 +1,40 @@
var chessground = require('chessground');
function makeConfig(data, config, onMove) {
return {
fen: config.fen,
check: config.check,
lastMove: config.lastMove,
orientation: data.orientation,
coordinates: data.pref.coords !== 0,
movable: {
free: false,
color: config.movable.color,
dests: config.movable.dests,
rookCastle: data.pref.rookCastle
},
events: {
move: onMove
},
premovable: {
enabled: config.movable.enabled
},
drawable: {
enabled: true
},
highlight: {
lastMove: true,
check: true,
dragOver: true
},
animation: {
enabled: true,
duration: data.pref.animationDuration
},
disableContextMenu: true
};
}
module.exports = function(data, config, onMove) {
return new chessground.controller(makeConfig(data, config, onMove));
};

View file

@ -4,7 +4,7 @@ var m = require('mithril');
module.exports = function(opts) {
var controller = new ctrl(opts.data, opts.i18n);
var controller = new ctrl(opts);
m.module(opts.element, {
controller: function() {

59
ui/puzzle/src/socket.js Normal file
View file

@ -0,0 +1,59 @@
module.exports = function(opts) {
var anaMoveTimeout;
var anaDestsTimeout;
var anaDestsCache = {};
var handlers = {
stepFailure: function(data) {
clearTimeout(anaMoveTimeout);
opts.reset();
},
dests: function(data) {
anaDestsCache[data.path] = data;
opts.addDests(data.dests, data.path, data.opening);
clearTimeout(anaDestsTimeout);
},
destsFailure: function(data) {
console.log(data);
clearTimeout(anaDestsTimeout);
}
};
var sendAnaMove = function(req) {
clearTimeout(anaMoveTimeout);
opts.send('anaMove', req);
anaMoveTimeout = setTimeout(function() {
sendAnaMove(req);
}, 3000);
};
var sendAnaDests = function(req) {
clearTimeout(anaDestsTimeout);
if (anaDestsCache[req.path]) setTimeout(function() {
handlers.dests(anaDestsCache[req.path]);
}, 300);
else {
opts.send('anaDests', req);
anaDestsTimeout = setTimeout(function() {
sendAnaDests(req);
}, 3000);
}
};
return {
send: opts.send,
receive: function(type, data) {
if (handlers[type]) {
handlers[type](data);
return true;
}
return false;
},
sendAnaMove: sendAnaMove,
sendAnaDests: sendAnaDests
};
}

11
ui/puzzle/src/sound.js Normal file
View file

@ -0,0 +1,11 @@
var throttle = require('common').throttle;
module.exports = $.sound ? {
move: throttle(50, false, $.sound.move),
capture: throttle(50, false, $.sound.capture),
check: throttle(50, false, $.sound.check)
} : {
move: $.noop,
capture: $.noop,
check: $.noop
};

View file

@ -1,13 +1,9 @@
var m = require('mithril');
var raf = require('chessground').util.requestAnimationFrame;
var throttle = require('common').throttle;
var empty = require('common').empty;
var defined = require('common').defined;
var game = require('game').game;
var fixCrazySan = require('chess').fixCrazySan;
var normalizeEval = require('chess').renderEval;
var treePath = require('tree').path;
var commentAuthorText = require('./study/studyComments').authorText;
var autoScroll = throttle(300, false, function(ctrl, el) {
var cont = el.parentNode;
@ -46,48 +42,36 @@ function renderChildrenOf(ctx, node, opts) {
var cs = node.children;
var main = cs[0];
if (!main) return;
var conceal = opts.noConceal ? null : (opts.conceal || ctx.concealOf(true)(opts.parentPath + main.id, main));
if (conceal === 'hide') return;
if (opts.isMainline) {
var isWhite = main.ply % 2 === 1;
var commentTags = renderMainlineCommentsOf(ctx, main, {
conceal: conceal,
withColor: true
}).filter(nonEmpty);
if (!cs[1] && empty(commentTags)) return [
if (!cs[1]) return [
isWhite ? renderIndex(main.ply, false) : null,
renderMoveAndChildrenOf(ctx, main, {
parentPath: opts.parentPath,
isMainline: true,
conceal: conceal
isMainline: true
})
];
var mainChildren = renderChildrenOf(ctx, main, {
parentPath: opts.parentPath + main.id,
isMainline: true,
conceal: conceal
isMainline: true
});
var passOpts = {
parentPath: opts.parentPath,
isMainline: true,
conceal: conceal
isMainline: true
};
return [
isWhite ? renderIndex(main.ply, false) : null,
renderMoveOf(ctx, main, passOpts),
isWhite ? emptyMove(passOpts) : null,
isWhite ? emptyMove() : null,
m('interrupt', [
commentTags,
renderLines(ctx, cs.slice(1), {
parentPath: opts.parentPath,
isMainline: true,
conceal: conceal,
noConceal: !conceal
isMainline: true
})
]),
isWhite && mainChildren ? [
renderIndex(main.ply, false),
emptyMove(passOpts)
emptyMove()
] : null,
mainChildren
];
@ -100,15 +84,13 @@ function renderLines(ctx, nodes, opts) {
return {
tag: 'lines',
attrs: {
class: (nodes[1] ? '' : 'single') // + (opts.conceal ? ' ' + opts.conceal : '')
class: nodes[1] ? '' : 'single'
},
children: nodes.map(function(n) {
return lineTag(renderMoveAndChildrenOf(ctx, n, {
parentPath: opts.parentPath,
isMainline: false,
withIndex: true,
noConceal: opts.noConceal,
truncate: n.comp && !pathContains(ctx, opts.parentPath + n.id) ? 3 : null
withIndex: true
}));
})
};
@ -130,20 +112,14 @@ function renderMainlineMoveOf(ctx, node, opts) {
var attrs = {
p: path
};
var classes = [];
if (path === ctx.ctrl.vm.path) classes.push('active');
if (path === ctx.ctrl.vm.contextMenuPath) classes.push('context_menu');
if (path === ctx.ctrl.vm.initialPath && game.playable(ctx.ctrl.data)) classes.push('current');
if (opts.conceal) classes.push(opts.conceal);
if (classes.length) attrs.class = classes.join(' ');
if (path === ctx.ctrl.vm.path) attrs.class = 'active';
return moveTag(attrs, renderMove(ctx, node));
}
function renderMove(ctx, node) {
var eval = node.eval || node.ceval || {};
return [
fixCrazySan(node.san),
(node.glyphs && ctx.showGlyphs) ? renderGlyphs(node.glyphs) : null,
node.san,
defined(eval.cp) ? renderEval(normalizeEval(eval.cp)) : (
defined(eval.mate) ? renderEval('#' + eval.mate) : null
),
@ -159,29 +135,20 @@ function renderVariationMoveOf(ctx, node, opts) {
var classes = [];
if (path === ctx.ctrl.vm.path) classes.push('active');
else if (pathContains(ctx, path)) classes.push('parent');
if (path === ctx.ctrl.vm.contextMenuPath) classes.push('context_menu');
if (opts.conceal) classes.push(ctx.conceal);
if (classes.length) attrs.class = classes.join(' ');
return moveTag(attrs, [
withIndex ? renderIndex(node.ply, true) : null,
fixCrazySan(node.san),
node.glyphs ? renderGlyphs(node.glyphs) : null
node.san
]);
}
function renderMoveAndChildrenOf(ctx, node, opts) {
var path = opts.parentPath + node.id;
if (opts.truncate === 0) return moveTag({
p: path
}, [m('index', '[...]')]);
return [
renderMoveOf(ctx, node, opts),
renderVariationCommentsOf(ctx, node),
renderChildrenOf(ctx, node, {
parentPath: path,
isMainline: opts.isMainline,
noConceal: opts.noConceal,
truncate: opts.truncate ? opts.truncate - 1 : null
isMainline: opts.isMainline
})
];
}
@ -194,24 +161,12 @@ function moveTag(attrs, content) {
};
}
function emptyMove(opts) {
function emptyMove() {
return moveTag({
class: 'empty' + (opts.conceal ? ' ' + opts.conceal : '')
class: 'empty'
}, '...');
}
function renderGlyphs(glyphs) {
return glyphs.map(function(glyph) {
return {
tag: 'glyph',
attrs: {
title: glyph.name
},
children: [glyph.symbol]
};
});
}
function renderEval(e) {
return {
tag: 'eval',
@ -219,80 +174,18 @@ function renderEval(e) {
};
}
function renderMainlineCommentsOf(ctx, node, opts) {
if (!ctx.ctrl.vm.comments || empty(node.comments)) return [];
var colorClass = opts.withColor ? (node.ply % 2 === 0 ? 'black ' : 'white ') : '';
return node.comments.map(function(comment) {
if (comment.by === 'lichess' && !ctx.showComputer) return;
var klass = '';
if (comment.text.indexOf('Inaccuracy.') === 0) klass = 'inaccuracy';
else if (comment.text.indexOf('Mistake.') === 0) klass = 'mistake';
else if (comment.text.indexOf('Blunder.') === 0) klass = 'blunder';
if (opts.conceal) klass += ' ' + opts.conceal;
return renderMainlineComment(comment, colorClass + klass, node.comments.length > 1, ctx);
});
}
function renderMainlineComment(comment, klass, withAuthor, ctx) {
return {
tag: 'comment',
attrs: {
class: klass
},
children: [
withAuthor ? m('span.by', commentAuthorText(comment.by)) : null,
truncateComment(comment.text, 400, ctx)
]
};
}
function renderVariationCommentsOf(ctx, node) {
if (!ctx.ctrl.vm.comments || empty(node.comments)) return [];
return node.comments.map(function(comment) {
if (comment.by === 'lichess' && !ctx.showComputer) return;
return renderVariationComment(comment, node.comments.length > 1, ctx);
});
}
function renderVariationComment(comment, withAuthor, ctx) {
return {
tag: 'comment',
children: [
withAuthor ? m('span.by', commentAuthorText(comment.by)) : null,
truncateComment(comment.text, 300, ctx)
]
};
}
function truncateComment(text, len, ctx) {
if (ctx.ctrl.embed) return text;
if (text.length <= len) return text;
return text.slice(0, len - 10) + ' [...]';
}
function eventPath(e, ctrl) {
return e.target.getAttribute('p') || e.target.parentNode.getAttribute('p');
}
var noop = function() {};
function emptyConcealOf() {
return noop;
}
module.exports = {
render: function(ctrl, concealOf) {
render: function(ctrl) {
var root = ctrl.tree.root;
var ctx = {
ctrl: ctrl,
concealOf: concealOf || emptyConcealOf,
showComputer: ctrl.vm.showComputer(),
showGlyphs: !!ctrl.study || ctrl.vm.showComputer()
showComputer: false,
showGlyphs: false
};
var commentTags = renderMainlineCommentsOf(ctx, root, {
withColor: false,
conceal: false
});
return m('div.tview2', {
config: function(el, isUpdate) {
if (ctrl.vm.autoScrollRequested || !isUpdate) {
@ -300,15 +193,6 @@ module.exports = {
ctrl.vm.autoScrollRequested = false;
}
if (isUpdate) return;
el.oncontextmenu = function(e) {
var path = eventPath(e, ctrl);
contextMenu.open(e, {
path: path,
root: ctrl
});
m.redraw();
return false;
};
el.addEventListener('mousedown', function(e) {
if (defined(e.button) && e.button !== 0) return; // only touch or left click
var path = eventPath(e, ctrl);
@ -317,10 +201,9 @@ module.exports = {
});
},
}, [
empty(commentTags) ? null : m('interrupt', commentTags),
root.ply % 2 === 1 ? [
renderIndex(root.ply, false),
emptyMove({})
emptyMove()
] : null,
renderChildrenOf(ctx, root, {
parentPath: '',

View file

@ -1,4 +1,6 @@
var m = require('mithril');
var chessground = require('chessground');
var treeView = require('./treeView');
function renderOpeningBox(ctrl) {
var opening = ctrl.tree.getOpening(ctrl.vm.nodeList);
@ -14,11 +16,35 @@ function renderOpeningBox(ctrl) {
function renderAnalyse(ctrl) {
return m('div.areplay', [
renderOpeningBox(ctrl),
treeView.render(ctrl, concealOf),
treeView.render(ctrl),
// renderResult(ctrl)
]);
}
function wheel(ctrl, e) {
if (e.target.tagName !== 'PIECE' && e.target.tagName !== 'SQUARE' && !e.target.classList.contains('cg-board')) return;
if (e.deltaY > 0) control.next(ctrl);
else if (e.deltaY < 0) control.prev(ctrl);
m.redraw();
e.preventDefault();
return false;
}
function visualBoard(ctrl) {
return m('div.lichess_board_wrap', [
m('div.lichess_board', {
config: function(el, isUpdate) {
if (!isUpdate) el.addEventListener('wheel', function(e) {
return wheel(ctrl, e);
});
}
}, [
chessground.view(ctrl.ground)
]),
// cevalView.renderGauge(ctrl)
]);
}
var firstRender = true;
module.exports = function(ctrl) {
@ -39,7 +65,7 @@ module.exports = function(ctrl) {
m('div.lichess_ground', [
// cevalView.renderCeval(ctrl),
// cevalView.renderPvs(ctrl),
renderAnalyse(ctrl, concealOf),
renderAnalyse(ctrl),
// buttons(ctrl)
])
])