640 lines
24 KiB
JavaScript
640 lines
24 KiB
JavaScript
$.widget("lichess.game", {
|
|
|
|
_init: function() {
|
|
var self = this;
|
|
self.$board = self.element.find("div.lichess_board");
|
|
self.$table = self.element.find("div.lichess_table_wrap");
|
|
self.$tableInner = self.$table.find("div.table_inner");
|
|
self.$chat = $("div.lichess_chat");
|
|
self.$chatMsgs = self.$chat.find('.lichess_messages');
|
|
self.$nbViewers = $('.nb_viewers');
|
|
self.initialTitle = document.title;
|
|
self.hasMovedOnce = false;
|
|
self.premove = null;
|
|
self.options.tableUrl = self.element.data('table-url');
|
|
self.options.playersUrl = self.element.data('players-url');
|
|
|
|
if (self.options.game.started) {
|
|
self.indicateTurn();
|
|
self.initSquaresAndPieces();
|
|
self.initChat();
|
|
self.initTable();
|
|
self.initClocks();
|
|
if (self.isMyTurn() && self.options.game.turns == 0) {
|
|
self.element.one('lichess.audio_ready', function() {
|
|
$.playSound();
|
|
});
|
|
}
|
|
if (!self.options.game.finished && ! self.options.player.spectator) {
|
|
self.blur = 0;
|
|
$(window).blur(function() {
|
|
self.blur = 1;
|
|
});
|
|
}
|
|
self.unloaded = false;
|
|
$(window).unload(function() {
|
|
self.unloaded = true;
|
|
});
|
|
if (self.options.game.last_move) {
|
|
self.highlightLastMove(self.options.game.last_move);
|
|
}
|
|
}
|
|
|
|
if (!self.options.opponent.ai && !self.options.player.spectator) {
|
|
// update document title to show playing state
|
|
setTimeout(self.updateTitle = function() {
|
|
document.title = (self.isMyTurn() && ! self.options.game.finished) ? document.title = document.title.indexOf('/\\/') == 0 ? '\\/\\ ' + document.title.replace(/\/\\\/ /, '') : '/\\/ ' + document.title.replace(/\\\/\\ /, '') : document.title;
|
|
setTimeout(self.updateTitle, 400);
|
|
},
|
|
400);
|
|
}
|
|
|
|
lichess.socket = new $.websocket(
|
|
lichess.socketUrl + "/socket/" + self.options.game.id + "/" + self.options.player.color, self.options.player.version,
|
|
$.extend(true, lichess.socketDefaults, {
|
|
events: {
|
|
message: function(event) {
|
|
self.element.queue(function() {
|
|
self.appendToChat(event);
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
possible_moves: function(event) {
|
|
self.element.queue(function() {
|
|
self.options.possible_moves = event;
|
|
self.indicateTurn();
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
move: function(event) {
|
|
self.element.queue(function() {
|
|
// if a draw was claimable, remove the zone
|
|
$('div.lichess_claim_draw_zone').remove();
|
|
self.$board.find("div.lcs.check").removeClass("check");
|
|
self.$board.find("div.droppable-hover").removeClass("droppable-hover");
|
|
// If I made the move, the piece is already moved on the board
|
|
if (self.hasMovedOnce && event.color == self.options.player.color) {
|
|
self.element.dequeue();
|
|
} else {
|
|
self.movePiece(event.from, event.to, function() {
|
|
self.element.dequeue();
|
|
}, false);
|
|
}
|
|
});
|
|
},
|
|
castling: function(event) {
|
|
self.element.queue(function() {
|
|
$("div#" + event.rook[1], self.$board).append($("div#" + event.rook[0] + " div.lichess_piece.rook", self.$board));
|
|
// if the king is beeing animated, stop it now
|
|
if ($king = $('body > div.king').orNot()) $king.stop(true, true);
|
|
$("div#" + event.king[1], self.$board).append($("div.lichess_piece.king."+event.color, self.$board));
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
promotion: function(event) {
|
|
self.element.queue(function() {
|
|
$("div#" + event.key + " div.lichess_piece").addClass(event.pieceClass).removeClass("pawn");
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
check: function(event) {
|
|
self.element.queue(function() {
|
|
$("div#" + event, self.$board).addClass("check");
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
enpassant: function(event) {
|
|
self.element.queue(function() {
|
|
self.killPiece($("div#" + event + " div.lichess_piece", self.$board));
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
redirect: function(event) {
|
|
// stop queue propagation here
|
|
self.element.queue(function() {
|
|
setTimeout(function() {
|
|
location.href = event;
|
|
}, 400);
|
|
});
|
|
},
|
|
reload: function(event) {
|
|
// stop queue propagation here
|
|
self.element.queue(function() {
|
|
setTimeout(function() {
|
|
location.reload();
|
|
}, 400);
|
|
});
|
|
},
|
|
threefold_repetition: function(event) {
|
|
self.element.queue(function() {
|
|
self.reloadTable(function() {
|
|
self.element.dequeue();
|
|
});
|
|
});
|
|
},
|
|
gone: function(event) {
|
|
self.$table.find("div.force_resign_zone").toggle(event);
|
|
self.centerTable();
|
|
},
|
|
end: function(event) {
|
|
// Game end must be applied firt: no queue
|
|
self.options.game.finished = true;
|
|
self.$table
|
|
.find("div.lichess_table").addClass("finished").end()
|
|
.find(".moretime").remove().end()
|
|
.find('div.clock').clock('stop');
|
|
self.element.find("div.ui-draggable").draggable("destroy");
|
|
// But enqueue the visible changes
|
|
self.element.queue(function() {
|
|
self.changeTitle($.trans('Game over'));
|
|
self.element.removeClass("my_turn");
|
|
self.reloadTable(function() {
|
|
self.reloadPlayers(function() {
|
|
self.element.dequeue();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
reload_table: function(event) {
|
|
self.element.queue(function() {
|
|
self.reloadTable(function() {
|
|
self.element.dequeue();
|
|
});
|
|
});
|
|
},
|
|
clock: function(event) {
|
|
self.element.queue(function() {
|
|
self.updateClocks(event);
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
premove: function(event) {
|
|
self.element.queue(function() {
|
|
self.applyPremove();
|
|
self.element.dequeue();
|
|
});
|
|
},
|
|
crowd: function(event) {
|
|
if (self.$nbViewers.length) {
|
|
self.$nbViewers.html(self.$nbViewers.html().replace(/(\d+|-)/, event.watchers)).toggle(event.watchers > 0);
|
|
$(["white", "black"]).each(function() {
|
|
self.$table.find("div.username." + this).toggleClass("connected", event[this]).toggleClass("offline", !event[this]);
|
|
});
|
|
}
|
|
},
|
|
state: function(event) {
|
|
self.element.queue(function() {
|
|
self.options.game.player = event.color;
|
|
self.options.game.turns = event.turns;
|
|
self.element.dequeue();
|
|
});
|
|
}
|
|
},
|
|
params: {
|
|
playerId: self.options.player.id
|
|
},
|
|
options: {
|
|
name: "game"
|
|
}
|
|
}));
|
|
},
|
|
isMyTurn: function() {
|
|
return this.options.possible_moves != null;
|
|
},
|
|
changeTitle: function(text) {
|
|
if (this.options.player.spectator) return;
|
|
document.title = text + " - " + this.initialTitle;
|
|
},
|
|
indicateTurn: function() {
|
|
var self = this;
|
|
if (self.options.game.finished) {
|
|
self.changeTitle($.trans('Game over'));
|
|
}
|
|
else if (self.isMyTurn()) {
|
|
self.element.addClass("my_turn");
|
|
self.changeTitle($.trans('Your turn'));
|
|
}
|
|
else {
|
|
self.element.removeClass("my_turn");
|
|
self.changeTitle($.trans('Waiting for opponent'));
|
|
}
|
|
|
|
if (!self.$table.find('>div').hasClass('finished')) {
|
|
self.$tableInner.find("div.lichess_current_player div.lichess_player." + (self.isMyTurn() ? self.options.opponent.color: self.options.player.color)).hide();
|
|
self.$tableInner.find("div.lichess_current_player div.lichess_player." + (self.isMyTurn() ? self.options.player.color: self.options.opponent.color)).show();
|
|
}
|
|
},
|
|
movePiece: function(from, to, callback, mine) {
|
|
var self = this,
|
|
$piece = self.$board.find("div#" + from + " div.lichess_piece"),
|
|
$from = $("div#" + from, self.$board),
|
|
$to = $("div#" + to, self.$board);
|
|
|
|
// already moved
|
|
if (!$piece.length) {
|
|
self.onError(from + " " + to+' empty from square!!', true);
|
|
return;
|
|
}
|
|
|
|
self.highlightLastMove(from + " " + to);
|
|
if (!self.isPlayerColor(self.getPieceColor($piece))) {
|
|
$.playSound();
|
|
}
|
|
|
|
var afterMove = function() {
|
|
var $killed = $to.find("div.lichess_piece");
|
|
if ($killed.length && self.getPieceColor($piece) != self.getPieceColor($killed)) {
|
|
self.killPiece($killed);
|
|
}
|
|
$piece.css({top: 0, left: 0});
|
|
$to.append($piece);
|
|
$.isFunction(callback || null) && callback();
|
|
};
|
|
|
|
var animD = mine ? 0 : self.options.animation_delay;
|
|
|
|
$('body > div.lichess_piece').stop(true, true);
|
|
if (animD < 100) {
|
|
afterMove();
|
|
}
|
|
else {
|
|
$("body").append($piece.css({ top: $from.offset().top, left: $from.offset().left }));
|
|
$piece.animate({ top: $to.offset().top, left: $to.offset().left }, animD, afterMove);
|
|
}
|
|
},
|
|
highlightLastMove: function(notation) {
|
|
var self = this;
|
|
var squareIds = notation.split(" ");
|
|
$("div.lcs.moved", self.$board).removeClass("moved");
|
|
$("#" + squareIds[0] + ",#" + squareIds[1], self.$board).addClass("moved");
|
|
|
|
},
|
|
killPiece: function($piece) {
|
|
if ($.data($piece, 'draggable')) $piece.draggable("destroy");
|
|
var self = this,
|
|
$deads = self.element.find("div.lichess_cemetery." + self.getPieceColor($piece)),
|
|
$square = $piece.parent();
|
|
$deads.append($("<div>").addClass('lichess_tomb'));
|
|
var $tomb = $("div.lichess_tomb:last", $deads),
|
|
tomb_offset = $tomb.offset();
|
|
$('body').append($piece.css($square.offset()));
|
|
$piece.css("opacity", 0.5).animate({
|
|
top: tomb_offset.top,
|
|
left: tomb_offset.left
|
|
},
|
|
self.options.animation_delay * 2, function() {
|
|
$tomb.append($piece.css({
|
|
position: "relative",
|
|
top: 0,
|
|
left: 0
|
|
}));
|
|
});
|
|
},
|
|
possibleMovesContain: function(from, to) {
|
|
return this.options.possible_moves != null
|
|
&& typeof this.options.possible_moves[from] !== 'undefined'
|
|
&& this.options.possible_moves[from].indexOf(to) != -1;
|
|
},
|
|
applyPremove: function() {
|
|
var self = this;
|
|
if (self.premove && self.isMyTurn()) {
|
|
var move = self.premove;
|
|
self.unsetPremove();
|
|
if (self.possibleMovesContain(move.from, move.to)) {
|
|
var $fromSquare = $("#"+move.from).orNot();
|
|
var $toSquare = $("#"+move.to).orNot();
|
|
var $piece = $fromSquare.find(".lichess_piece").orNot();
|
|
if ($fromSquare && $toSquare && $piece) {
|
|
self.dropPiece($piece, $fromSquare, $toSquare, true);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
setPremove: function(move) {
|
|
var self = this;
|
|
if (self.isMyTurn()) return;
|
|
self.unsetPremove();
|
|
if (move.from == move.to) return;
|
|
self.premove = move;
|
|
$("#"+move.from+",#"+move.to).addClass("premoved");
|
|
self.unselect();
|
|
$("#premove").show();
|
|
},
|
|
unsetPremove: function() {
|
|
var self = this;
|
|
self.premove = null;
|
|
self.$board.find('div.lcs.premoved').removeClass('premoved');
|
|
$("#premove").hide();
|
|
},
|
|
unselect: function() {
|
|
this.$board.find('> div.selected').removeClass('selected');
|
|
},
|
|
dropPiece: function($piece, $oldSquare, $newSquare, isPremove) {
|
|
var self = this,
|
|
isPremove = isPremove || false;
|
|
squareId = $newSquare.attr('id'),
|
|
moveData = {
|
|
from: $oldSquare.attr("id"),
|
|
to: squareId,
|
|
b: self.blur
|
|
};
|
|
|
|
if (!self.isMyTurn()) {
|
|
return self.setPremove({ from: moveData.from, to: moveData.to });
|
|
}
|
|
|
|
self.unselect();
|
|
self.hasMovedOnce = true;
|
|
self.blur = 0;
|
|
self.options.possible_moves = null;
|
|
self.movePiece($oldSquare.attr("id"), squareId, null, true);
|
|
|
|
// TODO send moveData here
|
|
function sendMoveRequest(moveData) {
|
|
lichess.socket.send("move", moveData);
|
|
}
|
|
|
|
var color = self.options.player.color;
|
|
// promotion
|
|
if ($piece.hasClass('pawn') && ((color == "white" && squareId[1] == 8) || (color == "black" && squareId[1] == 1))) {
|
|
if (isPremove) {
|
|
moveData.promotion = "queen";
|
|
sendMoveRequest(moveData);
|
|
} else {
|
|
var $choices = $('<div class="lichess_promotion_choice">').appendTo(self.$board).html('\
|
|
<div data-piece="queen" class="lichess_piece queen ' + color + '"></div>\
|
|
<div data-piece="knight" class="lichess_piece knight ' + color + '"></div>\
|
|
<div data-piece="rook" class="lichess_piece rook ' + color + '"></div>\
|
|
<div data-piece="bishop" class="lichess_piece bishop ' + color + '"></div>').fadeIn(self.options.animation_delay).find('div.lichess_piece').click(function() {
|
|
moveData.promotion = $(this).attr('data-piece');
|
|
sendMoveRequest(moveData);
|
|
$choices.fadeOut(self.options.animation_delay, function() {
|
|
$choices.remove();
|
|
});
|
|
}).end();
|
|
}
|
|
}
|
|
else {
|
|
sendMoveRequest(moveData);
|
|
}
|
|
},
|
|
initSquaresAndPieces: function() {
|
|
var self = this;
|
|
if (self.options.game.finished || self.options.player.spectator) {
|
|
return;
|
|
}
|
|
var draggingKey = null;
|
|
var dropped = false;
|
|
// init squares
|
|
self.$board.find("div.lcs").each(function() {
|
|
var squareId = $(this).attr('id');
|
|
$(this).droppable({
|
|
accept: function(draggable) {
|
|
if (!self.isMyTurn()) {
|
|
return draggingKey != squareId;
|
|
} else {
|
|
return draggingKey && self.possibleMovesContain(draggingKey, squareId);
|
|
}
|
|
},
|
|
drop: function(ev, ui) {
|
|
self.dropPiece(ui.draggable, ui.draggable.parent(), $(this));
|
|
dropped = true;
|
|
},
|
|
hoverClass: 'droppable-hover'
|
|
});
|
|
});
|
|
|
|
// init pieces
|
|
self.$board.find("div.lichess_piece." + self.options.player.color).each(function() {
|
|
var $this = $(this);
|
|
$this.draggable({
|
|
containment: self.$board,
|
|
helper: function() { return $('<div>').attr('class', $this.attr('class')).appendTo(self.$board); },
|
|
start: function() {
|
|
draggingKey = $this.hide().parent().attr('id');
|
|
dropped = false;
|
|
self.unselect();
|
|
},
|
|
stop: function(e, ui) {
|
|
draggingKey = null;
|
|
var dist = Math.sqrt(Math.pow(ui.originalPosition.top - ui.position.top, 2) + Math.pow(ui.originalPosition.left - ui.position.left, 2));
|
|
if (!dropped && dist <= 32) $this.trigger('click');
|
|
$this.show();
|
|
},
|
|
scroll: false
|
|
});
|
|
});
|
|
|
|
/*
|
|
* Code for touch screens like android or iphone
|
|
*/
|
|
|
|
self.$board.find("div.lichess_piece." + self.options.player.color).each(function() {
|
|
$(this).click(function() {
|
|
self.unsetPremove();
|
|
var $square = $(this).parent();
|
|
if ($square.hasClass('selectable')) return;
|
|
var isSelected = $square.hasClass('selected');
|
|
self.unselect();
|
|
if (isSelected) return;
|
|
$square.addClass('selected');
|
|
});
|
|
});
|
|
|
|
self.$board.find("div.lcs").each(function() {
|
|
var $this = $(this);
|
|
$this.hover(function() {
|
|
if($selected = self.$board.find('div.lcs.selected').orNot()) {
|
|
if (!self.isMyTurn() || self.possibleMovesContain($selected.attr('id'), $this.attr('id'))) {
|
|
$this.addClass('selectable');
|
|
}
|
|
}
|
|
},
|
|
function() {
|
|
$this.removeClass('selectable');
|
|
}).click(function() {
|
|
self.unsetPremove();
|
|
var $from = self.$board.find('div.lcs.selected').orNot();
|
|
var $to = $this;
|
|
if (!$from || $from == $to) return;
|
|
var $piece = $from.find('div.lichess_piece');
|
|
if (!self.isMyTurn() && $from) {
|
|
self.dropPiece($piece, $from, $to);
|
|
} else {
|
|
if (!self.possibleMovesContain($from.attr('id'), $this.attr('id'))) return;
|
|
if (!$to.hasClass('selectable')) return;
|
|
$to.removeClass('selectable');
|
|
self.dropPiece($piece, $from, $this);
|
|
}
|
|
});
|
|
});
|
|
|
|
/*
|
|
* End of code for touch screens
|
|
*/
|
|
},
|
|
initChat: function() {
|
|
var self = this;
|
|
if (self.options.player.spectator) {
|
|
return;
|
|
}
|
|
if (self.$chat.length) {
|
|
self.$chatMsgs.find('>li').each(function() { $(this).html(urlToLink($(this).html())); });
|
|
self.$chatMsgs.scrollable();
|
|
var $form = self.$chat.find('form');
|
|
self.$chatMsgs[0].scrollTop = 9999999;
|
|
var $input = self.$chat.find('input.lichess_say').one("focus", function() {
|
|
$input.val('').removeClass('lichess_hint');
|
|
});
|
|
|
|
// send a message
|
|
$form.submit(function() {
|
|
var text = $.trim($input.val());
|
|
if (!text) return false;
|
|
if (text.length > 140) {
|
|
alert('Max length: 140 chars. ' + text.length + ' chars used.');
|
|
return false;
|
|
}
|
|
$input.val('');
|
|
lichess.socket.send('talk', text);
|
|
return false;
|
|
});
|
|
|
|
self.$chat.find('a.send').click(function() {
|
|
$input.trigger('click');
|
|
$form.submit();
|
|
});
|
|
|
|
// toggle the chat
|
|
self.$chat.find('input.toggle_chat').change(function() {
|
|
self.$chat.toggleClass('hidden', ! $(this).attr('checked'));
|
|
}).trigger('change');
|
|
}
|
|
},
|
|
appendToChat: function(msg) {
|
|
if (this.$chat.length) this.$chatMsgs.append(urlToLink(msg))[0].scrollTop = 9999999;
|
|
},
|
|
reloadTable: function(callback) {
|
|
var self = this;
|
|
self.get(self.options.tableUrl, {
|
|
success: function(html) {
|
|
$('body > div.tipsy').remove();
|
|
self.$tableInner.html(html);
|
|
self.initTable();
|
|
$.isFunction(callback) && callback();
|
|
$('body').trigger('lichess.content_loaded');
|
|
}
|
|
}, false);
|
|
},
|
|
reloadPlayers: function(callback) {
|
|
var self = this;
|
|
$.getJSON(self.options.playersUrl, function(data) {
|
|
$(['white', 'black']).each(function() {
|
|
if (data[this]) self.$table.find('div.username.' + this).html(data[this]);
|
|
});
|
|
if (data.me) $('#user_tag span').text(data.me);
|
|
$('body').trigger('lichess.content_loaded');
|
|
$.isFunction(callback) && callback();
|
|
});
|
|
},
|
|
initTable: function() {
|
|
var self = this;
|
|
self.centerTable();
|
|
self.$table.find('a, input, label').tipsy({
|
|
fade: true
|
|
});
|
|
self.$table.find('a.lichess_play_again_decline').one('click', function() {
|
|
$(this).parent().remove();
|
|
});
|
|
self.$table.find('a.lichess_rematch, a.lichess_rematch_cancel, a.lichess_rematch_decline').click(function() {
|
|
self.post($(this).attr('href'), {}, true);
|
|
if ($(this).is('.lichess_play_again_join a')) {
|
|
$(this).parent().remove();
|
|
}
|
|
return false;
|
|
});
|
|
self.$table.find('a.moretime').click(function() {
|
|
lichess.socket.send("moretime");
|
|
return false;
|
|
});
|
|
},
|
|
centerTable: function() {
|
|
this.$table.css('top', (256 - this.$table.height() / 2) + 'px');
|
|
},
|
|
initClocks: function() {
|
|
var self = this;
|
|
if (!self.canRunClock()) return;
|
|
self.$table.find('div.clock').each(function() {
|
|
$(this).clock({
|
|
time: $(this).attr('data-time'),
|
|
buzzer: function() {
|
|
if (!self.options.game.finished && ! self.options.player.spectator) {
|
|
lichess.socket.send("outoftime");
|
|
}
|
|
}
|
|
});
|
|
});
|
|
self.updateClocks();
|
|
},
|
|
updateClocks: function(times) {
|
|
var self = this;
|
|
if (!self.canRunClock()) return;
|
|
if (times || false) {
|
|
for (color in times) {
|
|
self.$table.find('div.clock_' + color).clock('setTime', times[color]);
|
|
}
|
|
}
|
|
self.$table.find('div.clock').clock('stop');
|
|
if (self.options.game.turns > 0) {
|
|
self.$table.find('div.clock_' + self.options.game.player).clock('start');
|
|
}
|
|
},
|
|
canRunClock: function() {
|
|
return this.options.game.clock && this.options.game.started && ! this.options.game.finished;
|
|
},
|
|
getPieceColor: function($piece) {
|
|
return $piece.hasClass('white') ? 'white': 'black';
|
|
},
|
|
isPlayerColor: function(color) {
|
|
return !this.options.player.spectator && this.options.player.color == color;
|
|
},
|
|
get: function(url, options, reloadIfFail) {
|
|
var self = this;
|
|
options = $.extend({
|
|
type: 'GET',
|
|
timeout: 8000,
|
|
cache: false
|
|
},
|
|
options || {});
|
|
$.ajax(url, options).complete(function(x, s) {
|
|
self.onXhrComplete(x, s, null, reloadIfFail);
|
|
});
|
|
},
|
|
post: function(url, options, reloadIfFail) {
|
|
var self = this;
|
|
options = $.extend({
|
|
type: 'POST',
|
|
timeout: 8000
|
|
},
|
|
options || {});
|
|
$.ajax(url, options).complete(function(x, s) {
|
|
self.onXhrComplete(x, s, 'ok', reloadIfFail);
|
|
});
|
|
},
|
|
onXhrComplete: function(xhr, status, expectation, reloadIfFail) {
|
|
if (status != 'success') {
|
|
this.onError('status is not success: '+status, reloadIfFail);
|
|
}
|
|
if ((expectation || false) && expectation != xhr.responseText) {
|
|
this.onError('expectation failed: '+xhr.responseText, reloadIfFail);
|
|
}
|
|
},
|
|
onError: function(error, reloadIfFail) {
|
|
var self = this;
|
|
if (reloadIfFail) {
|
|
location.reload();
|
|
//console.debug(error)
|
|
}
|
|
}
|
|
});
|