lila/ui/analyse/src/ctrl.js

505 lines
15 KiB
JavaScript

var chessground = require('chessground');
var opposite = chessground.util.opposite;
var tree = require('./tree/tree');
var treePath = require('./tree/path');
var treeOps = require('./tree/ops');
var ground = require('./ground');
var keyboard = require('./keyboard');
var actionMenu = require('./actionMenu').controller;
var autoplay = require('./autoplay');
var control = require('./control');
var promotion = require('./promotion');
var util = require('./util');
var throttle = require('./util').throttle;
var socket = require('./socket');
var forecastCtrl = require('./forecast/forecastCtrl');
var cevalCtrl = require('./ceval/cevalCtrl');
var explorerCtrl = require('./explorer/explorerCtrl');
var router = require('game').router;
var game = require('game').game;
var crazyValid = require('./crazy/crazyValid');
var crazyView = require('./crazy/crazyView');
var studyCtrl = require('./study/studyCtrl');
var makeFork = require('./fork').ctrl;
var m = require('mithril');
module.exports = function(opts) {
this.userId = opts.userId;
var initialize = function(data) {
this.data = data;
if (!data.game.moveTimes) this.data.game.moveTimes = [];
this.ongoing = !util.synthetic(this.data) && game.playable(this.data);
this.tree = tree(treeOps.reconstruct(this.data.treeParts));
this.actionMenu = new actionMenu();
this.autoplay = new autoplay(this);
this.socket = new socket(opts.socketSend, this);
this.explorer = explorerCtrl(this, opts.explorer, this.explorer ? this.explorer.allowed() : true);
}.bind(this);
initialize(opts.data);
var initialPath = treePath.root;
if (opts.initialPly) {
var plyStr = opts.initialPly === 'url' ? (location.hash ? location.hash.replace(/#/, '') : treePath.root) : opts.initialPly;
var mainline = treeOps.mainlineNodeList(this.tree.root);
if (plyStr === 'last') initialPath = treePath.fromNodeList(mainline);
else {
var ply = parseInt(plyStr);
if (ply) initialPath = treeOps.takePathWhile(mainline, function(n) {
return n.ply <= ply;
});
}
}
this.vm = {
initialPath: initialPath,
cgConfig: null,
comments: true,
flip: false,
showAutoShapes: util.storedProp('show-auto-shapes', true),
showGauge: util.storedProp('show-gauge', true),
autoScrollRequested: false,
element: opts.element,
redirecting: false,
contextMenuPath: null,
justPlayed: null,
justDropped: null,
keyboardHelp: location.hash === '#keyboard'
};
this.setPath = function(path) {
this.vm.path = path;
this.vm.nodeList = this.tree.getNodeList(path);
this.vm.node = treeOps.last(this.vm.nodeList);
this.vm.mainline = treeOps.mainlineNodeList(this.tree.root);
}.bind(this);
this.setPath(initialPath);
this.flip = function() {
this.vm.flip = !this.vm.flip;
this.chessground.set({
orientation: this.bottomColor()
});
m.redraw();
}.bind(this);
this.topColor = function() {
return this.data[this.vm.flip ? 'player' : 'opponent'].color;
}.bind(this);
this.bottomColor = function() {
return opposite(this.topColor());
}.bind(this);
this.togglePlay = function(delay) {
this.autoplay.toggle(delay);
this.actionMenu.open = false;
}.bind(this);
var uciToLastMove = function(uci) {
if (!uci) return;
if (uci[1] === '@') return [uci.substr(2, 2), uci.substr(2, 2)];
return [uci.substr(0, 2), uci.substr(2, 2)];
};
this.fork = makeFork(this);
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 config = {
fen: node.fen,
turnColor: color,
movable: {
color: (dests && Object.keys(dests).length > 0) || drops === null || drops.length ? 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;
}
this.vm.cgConfig = config;
if (!this.chessground)
this.chessground = ground.make(this.data, config, userMove, userNewPiece, !!opts.study);
this.chessground.set(config);
onChange();
if (!dests) getDests();
this.setAutoShapes();
if (node.shapes) this.chessground.setShapes(node.shapes);
}.bind(this);
var getDests = throttle(800, false, function() {
if (this.vm.node.dests) return;
this.socket.sendAnaDests({
variant: this.data.game.variant.key,
fen: this.vm.node.fen,
path: this.vm.path
});
}.bind(this));
var sound = {
move: throttle(50, false, $.sound.move),
capture: throttle(50, false, $.sound.capture),
check: throttle(50, false, $.sound.check)
};
var onChange = opts.onChange ? throttle(300, false, function() {
var mainlinePly = this.tree.pathIsMainline(this.vm.path) ? this.vm.node.ply : false;
opts.onChange(this.vm.node.fen, this.vm.path, mainlinePly);
}.bind(this)) : $.noop;
var updateHref = (!opts.study && window.history.replaceState) ? throttle(750, false, function() {
window.history.replaceState(null, null, '#' + this.vm.node.ply);
}.bind(this), false) : $.noop;
this.autoScroll = function() {
this.vm.autoScrollRequested = true;
}.bind(this);
this.jump = function(path) {
var pathChanged = path !== this.vm.path;
this.setPath(path);
showGround();
if (pathChanged) {
if (this.study) this.study.setPath(path, this.vm.node);
if (!this.vm.node.uci) sound.move(); // initial position
else if (this.vm.node.uci.indexOf(this.vm.justPlayed) !== 0) {
if (this.vm.node.san.indexOf('x') !== -1) sound.capture();
else sound.move();
}
if (/\+|\#/.test(this.vm.node.san)) sound.check();
this.ceval.stop();
this.startCeval();
}
this.vm.justPlayed = null;
this.vm.justDropped = null;
this.explorer.setNode();
updateHref();
this.autoScroll();
promotion.cancel(this);
if (this.music) this.music.jump(this.vm.node);
}.bind(this);
this.userJump = function(path) {
this.autoplay.stop();
this.chessground.selectSquare(null);
this.jump(path);
}.bind(this);
var canJumpTo = function(path) {
return this.study ? this.study.canJumpTo(path) : true;
}.bind(this);
this.userJumpIfCan = function(path) {
if (canJumpTo(path)) this.userJump(path);
}.bind(this);
this.mainlinePathToPly = function(ply) {
return treeOps.takePathWhile(this.vm.mainline, function(n) {
return n.ply <= ply;
});
}.bind(this);
this.jumpToMain = function(ply) {
this.userJump(this.mainlinePathToPly(ply));
}.bind(this);
this.jumpToIndex = function(index) {
this.jumpToMain(index + 1 + this.data.game.startedAtTurn);
}.bind(this);
this.jumpToGlyphSymbol = function(color, symbol) {
var ply = this.tree.plyOfNextGlyphSymbol(color, symbol, this.vm.mainline, this.vm.node.ply);
if (ply) this.jumpToMain(ply);
m.redraw();
}.bind(this);
this.reloadData = function(data) {
initialize(data);
this.vm.redirecting = false;
this.setPath(treePath.root);
}.bind(this);
this.changePgn = function(pgn) {
this.vm.redirecting = true;
$.ajax({
url: '/analysis/pgn',
method: 'post',
data: {
pgn: pgn
},
success: function(data) {
this.reloadData(data);
this.userJump(this.mainlinePathToPly(this.tree.lastPly()));
}.bind(this),
error: function(error) {
console.log(error);
this.vm.redirecting = false;
m.redraw();
}.bind(this)
});
}.bind(this);
this.changeFen = function(fen) {
this.vm.redirecting = true;
window.location = makeUrl(this.data.game.variant.key, fen);
}.bind(this);
var makeUrl = function(variantKey, fen) {
return '/analysis/' + variantKey + '/' + encodeURIComponent(fen).replace(/%20/g, '_').replace(/%2F/g, '/');
}
var roleToSan = {
pawn: 'P',
knight: 'N',
bishop: 'B',
rook: 'R',
queen: 'Q'
};
var sanToRole = {
P: 'pawn',
N: 'knight',
B: 'bishop',
R: 'rook',
Q: 'queen'
};
var userNewPiece = function(piece, pos) {
if (crazyValid.drop(this.chessground, this.vm.node.drops, piece, pos)) {
this.vm.justPlayed = roleToSan[piece.role] + '@' + pos;
this.vm.justDropped = {
ply: this.vm.node.ply,
role: piece.role
};
sound.move();
var drop = {
role: piece.role,
pos: pos,
variant: this.data.game.variant.key,
fen: this.vm.node.fen,
path: this.vm.path
};
this.socket.sendAnaDrop(drop);
preparePremoving();
m.redraw();
} else this.jump(this.vm.path);
}.bind(this);
var userMove = function(orig, dest, capture) {
this.vm.justPlayed = orig;
this.vm.justDropped = null;
sound[capture ? 'capture' : 'move']();
if (!promotion.start(this, orig, dest, sendMove)) sendMove(orig, dest);
}.bind(this);
var sendMove = function(orig, dest, prom) {
var move = {
orig: orig,
dest: dest,
variant: this.data.game.variant.key,
fen: this.vm.node.fen,
path: this.vm.path
};
if (prom) move.promotion = prom;
this.socket.sendAnaMove(move);
preparePremoving();
}.bind(this);
var preparePremoving = function() {
this.chessground.set({
turnColor: this.chessground.data.movable.color,
movable: {
color: opposite(this.chessground.data.movable.color)
}
});
}.bind(this);
this.addNode = function(node, path) {
var newPath = this.tree.addNode(node, path);
this.jump(newPath);
m.redraw();
this.chessground.playPremove();
}.bind(this);
this.addDests = function(dests, path, opening) {
this.tree.addDests(dests, path, opening);
if (path === this.vm.path) {
showGround();
m.redraw();
if (dests === '') this.ceval.stop();
}
this.chessground.playPremove();
}.bind(this);
this.deleteNode = function(path) {
var node = this.tree.nodeAtPath(path);
if (!node) return;
var count = treeOps.countChildrenAndComments(node);
if ((count.nodes >= 10 || count.comments > 0) && !confirm(
'Delete ' + util.plural('move', count.nodes) + (count.comments ? ' and ' + util.plural('comment', count.comments) : '') + '?'
)) return;
this.tree.deleteNodeAt(path);
if (treePath.contains(this.vm.path, path)) this.userJump(treePath.init(path));
else this.jump(this.vm.path);
this.study && this.study.deleteNode(path);
}.bind(this);
this.promoteNode = function(path) {
this.tree.promoteNodeAt(path);
this.jump(this.vm.path);
this.study && this.study.promoteNode(path);
}.bind(this);
this.reset = function() {
showGround();
m.redraw();
}.bind(this);
this.encodeNodeFen = function() {
return this.vm.node.fen.replace(/\s/g, '_');
}.bind(this);
this.currentEvals = function() {
var node = this.vm.node;
return node && (node.eval || node.ceval) ? {
server: node.eval,
client: node.ceval,
fav: node.eval || node.ceval
} : null;
}.bind(this);
this.forecast = opts.data.forecast ? forecastCtrl(
opts.data.forecast,
router.forecasts(this.data)) : null;
this.nextNodeBest = function() {
return this.tree.ops.withMainlineChild(this.vm.node, function(n) {
return n.eval ? n.eval.best : null;
});
}.bind(this);
var cevalVariants = ['standard', 'fromPosition', 'chess960', 'kingOfTheHill', 'threeCheck', 'horde', 'racingKings', 'atomic', 'crazyhouse'];
var cevalPossible = function() {
return (util.synthetic(this.data) || !game.playable(this.data)) &&
cevalVariants.indexOf(this.data.game.variant.key) !== -1;
}.bind(this);
this.ceval = cevalCtrl(cevalPossible, this.data.game.variant, function(res) {
this.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 === this.vm.path) {
this.setAutoShapes();
m.redraw();
}
}.bind(this));
}.bind(this));
var canUseCeval = function() {
return this.vm.node.dests !== '' && (!this.vm.node.eval || !this.nextNodeBest());
}.bind(this);
this.startCeval = throttle(800, false, function() {
if (this.ceval.enabled() && canUseCeval())
this.ceval.start(this.vm.path, this.vm.nodeList);
}.bind(this));
this.toggleCeval = function() {
this.ceval.toggle();
this.setAutoShapes();
this.startCeval();
}.bind(this);
this.showEvalGauge = function() {
return this.hasAnyComputerAnalysis() && this.vm.showGauge() && this.vm.node.dests !== '';
}.bind(this);
this.hasAnyComputerAnalysis = function() {
return this.data.analysis || this.ceval.enabled();
}
this.toggleAutoShapes = function(v) {
if (this.vm.showAutoShapes(v)) this.setAutoShapes();
else this.chessground.setAutoShapes([]);
}.bind(this);
this.toggleGauge = function(v) {
this.vm.showGauge(!this.vm.showGauge());
}.bind(this);
this.setAutoShapes = function() {
var n = this.vm.node,
shapes = [],
explorerUci = this.explorer.hoveringUci();
if (explorerUci) shapes.push(makeAutoShapeFromUci(explorerUci, 'paleBlue'));
if (this.vm.showAutoShapes()) {
if (n.eval && n.eval.best) shapes.push(makeAutoShapeFromUci(n.eval.best, 'paleGreen'));
if (!explorerUci) {
var nextNodeBest = this.nextNodeBest();
if (nextNodeBest) shapes.push(makeAutoShapeFromUci(nextNodeBest, 'paleBlue'));
else if (this.ceval.enabled() && n.ceval && n.ceval.best) shapes.push(makeAutoShapeFromUci(n.ceval.best, 'paleBlue'));
}
}
this.chessground.setAutoShapes(shapes);
}.bind(this);
var decomposeUci = function(uci) {
return [uci.slice(0, 2), uci.slice(2, 4), uci.slice(4, 5)];
};
var makeAutoShapeFromUci = function(uci, brush) {
var move = decomposeUci(uci);
return uci[1] === '@' ? {
orig: move[1],
brush: brush
} : {
orig: move[0],
dest: move[1],
brush: brush
};
};
this.explorerMove = function(uci) {
var move = decomposeUci(uci);
if (uci[1] === '@') this.chessground.apiNewPiece({
color: this.chessground.data.movable.color,
role: sanToRole[uci[0]]
},
move[1])
else if (!move[2]) sendMove(move[0], move[1])
else sendMove(move[0], move[1], sanToRole[move[2].toUpperCase()]);
this.explorer.loading(true);
}.bind(this);
this.socketReceive = function(type, data) {
this.socket.receive(type, data);
}.bind(this);
this.trans = lichess.trans(opts.i18n);
showGround();
this.startCeval();
this.explorer.setNode();
this.study = opts.study ? studyCtrl.init(opts.study, this) : null;
keyboard.bind(this);
this.music = null;
lichess.pubsub.on('sound_set', function(set) {
if (!this.music && set === 'music')
lichess.loadScript('/assets/javascripts/music/replay.js').then(function() {
this.music = lichessReplayMusic();
}.bind(this));
if (this.music && set !== 'music') this.music = null;
}.bind(this));
};