From 8c3705a7d1ac91f6cf39c04498351c923103a0e3 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 18 May 2012 12:16:17 +0200 Subject: [PATCH] Better round sockets --- app/controllers/App.scala | 12 - app/controllers/Round.scala | 21 + app/round/RoundEnv.scala | 3 +- app/round/Socket.scala | 79 +- app/views/round/player.scala.html | 1 + app/views/round/watcher.scala.html | 1 + conf/routes | 3 +- public/javascripts/game.js | 1243 ++++++++++++++-------------- 8 files changed, 694 insertions(+), 669 deletions(-) diff --git a/app/controllers/App.scala b/app/controllers/App.scala index beab6eedc4..b041d942a0 100644 --- a/app/controllers/App.scala +++ b/app/controllers/App.scala @@ -28,18 +28,6 @@ object App extends LilaController { username = get("username")) } - def gameSocket(gameId: String, color: String) = - WebSocket.async[JsValue] { implicit req ⇒ - implicit val ctx = Context(req, None) - env.round.socket.join( - uidOption = get("uid"), - username = get("username"), - gameId = gameId, - colorName = color, - versionOption = getInt("version"), - playerId = get("playerId")).unsafePerformIO - } - def abort(fullId: String) = performAndRedirect(fullId, hand.abort) def resign(fullId: String) = performAndRedirect(fullId, hand.resign) diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index 610586a746..e497d7b876 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -2,14 +2,35 @@ package controllers import lila._ import views._ +import http.Context +import game.Pov +import socket.Util.connectionFail import play.api.mvc._ +import play.api.libs.json._ +import play.api.libs.iteratee._ object Round extends LilaController { val gameRepo = env.game.gameRepo val socket = env.round.socket + def websocketWatcher(gameId: String, color: String) = + WebSocket.async[JsValue] { req ⇒ + implicit val ctx = reqToCtx(req) + socket.joinWatcher( + gameId, color, getInt("version"), get("uid"), get("username") + ).unsafePerformIO + } + + def websocketPlayer(fullId: String) = + WebSocket.async[JsValue] { req ⇒ + implicit val ctx = reqToCtx(req) + socket.joinPlayer( + fullId, getInt("version"), get("uid"), get("username") + ).unsafePerformIO + } + def watcher(gameId: String, color: String) = Open { implicit ctx ⇒ IOption(gameRepo.pov(gameId, color)) { pov ⇒ html.round.watcher(pov, version(pov.gameId)) diff --git a/app/round/RoundEnv.scala b/app/round/RoundEnv.scala index 44aa70ba43..f370d80459 100644 --- a/app/round/RoundEnv.scala +++ b/app/round/RoundEnv.scala @@ -35,7 +35,8 @@ final class RoundEnv( )), name = ActorGameHubMaster) lazy val socket = new Socket( - getGame = gameRepo.game, + getWatcherPov = gameRepo.pov, + getPlayerPov = gameRepo.pov, hand = hand, hubMaster = hubMaster, messenger = messenger) diff --git a/app/round/Socket.scala b/app/round/Socket.scala index 0e4286102a..3f14f461b9 100644 --- a/app/round/Socket.scala +++ b/app/round/Socket.scala @@ -14,13 +14,15 @@ import play.api.Play.current import scalaz.effects._ -import game.{ DbGame, PovRef } +import game.{ Pov, PovRef } import chess.Color -import socket.{ Util, Ping, Quit } +import socket.{ Ping, Quit } +import socket.Util.connectionFail import implicits.RichJs._ final class Socket( - getGame: String ⇒ IO[Option[DbGame]], + getWatcherPov: (String, String) ⇒ IO[Option[Pov]], + getPlayerPov: String ⇒ IO[Option[Pov]], hand: Hand, val hubMaster: ActorRef, messenger: Messenger) { @@ -77,37 +79,48 @@ final class Socket( case _ ⇒ } - def join( - uidOption: Option[String], - username: Option[String], + def joinWatcher( gameId: String, colorName: String, + version: Option[Int], + uid: Option[String], + username: Option[String]): IO[SocketPromise] = getWatcherPov(gameId, colorName) map { + join(_, false, version, uid, username) + } + + def joinPlayer( + fullId: String, + version: Option[Int], + uid: Option[String], + username: Option[String]): IO[SocketPromise] = getPlayerPov(fullId) map { + join(_, true, version, uid, username) + } + + private def join( + povOption: Option[Pov], + owner: Boolean, versionOption: Option[Int], - playerId: Option[String]): IO[SocketPromise] = - getGame(gameId) map { gameOption ⇒ - val promise: Option[SocketPromise] = for { - game ← gameOption - color ← Color(colorName) - version ← versionOption - uid ← uidOption - } yield (for { - hub ← hubMaster ? GetHub(gameId) mapTo manifest[ActorRef] - socket ← hub ? Join( - uid = uid, - username = username, - version = version, - color = color, - owner = (playerId flatMap game.player).isDefined - ) map { - case Connected(member) ⇒ ( - Iteratee.foreach[JsValue]( - controller(hub, uid, member, PovRef(gameId, member.color)) - ) mapDone { _ ⇒ - hub ! Quit(uid) - }, - member.channel) - } - } yield socket).asPromise - promise | Util.connectionFail - } + uidOption: Option[String], + username: Option[String]): SocketPromise = + ((povOption |@| uidOption |@| versionOption) apply { + (pov: Pov, uid: String, version: Int) ⇒ + (for { + hub ← hubMaster ? GetHub(pov.gameId) mapTo manifest[ActorRef] + socket ← hub ? Join( + uid = uid, + username = username, + version = version, + color = pov.color, + owner = owner + ) map { + case Connected(member) ⇒ ( + Iteratee.foreach[JsValue]( + controller(hub, uid, member, PovRef(pov.gameId, member.color)) + ) mapDone { _ ⇒ + hub ! Quit(uid) + }, + member.channel) + } + } yield socket).asPromise: SocketPromise + }) | connectionFail } diff --git a/app/views/round/player.scala.html b/app/views/round/player.scala.html index 3d27e3b19e..92ae9b917d 100644 --- a/app/views/round/player.scala.html +++ b/app/views/round/player.scala.html @@ -19,6 +19,7 @@ @round.layout(title = title, goodies = goodies) {
diff --git a/app/views/round/watcher.scala.html b/app/views/round/watcher.scala.html index b87e44b570..8512327a80 100644 --- a/app/views/round/watcher.scala.html +++ b/app/views/round/watcher.scala.html @@ -45,6 +45,7 @@ @round.layout(title = title, goodies = goodies) {
diff --git a/conf/routes b/conf/routes index 13d642acf1..230cd90a9d 100644 --- a/conf/routes +++ b/conf/routes @@ -5,6 +5,8 @@ GET /games/all controllers.Game.all(page: Int ?= 1) GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1) # Round +GET /socket/$gameId<[\w\-]{8}>/$color controllers.Round.websocketWatcher(gameId: String, color: String) +GET /socket/$fullId<[\w\-]{12}> controllers.Round.websocketPlayer(fullId: String) GET /$gameId<[\w\-]{8}> controllers.Round.watcher(gameId: String, color: String = "white") GET /$gameId<[\w\-]{8}>/$color controllers.Round.watcher(gameId: String, color: String) GET /$fullId<[\w\-]{12}> controllers.Round.player(fullId: String) @@ -58,7 +60,6 @@ GET /wiki controllers.Wiki.home # App Public API GET /socket controllers.App.socket -GET /socket/:gameId/:color controllers.App.gameSocket(gameId: String, color: String) GET /ai controllers.Ai.run diff --git a/public/javascripts/game.js b/public/javascripts/game.js index 256cac5fc5..ef401d314f 100644 --- a/public/javascripts/game.js +++ b/public/javascripts/game.js @@ -1,639 +1,638 @@ $.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'); + _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'); + self.options.socketUrl = self.element.data('socket-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.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); - } + 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 - }, + lichess.socket = new $.websocket( + lichess.socketUrl + self.options.socketUrl, + self.options.player.version, + $.extend(true, lichess.socketDefaults, { 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($("
").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 = $('
').appendTo(self.$board).html('\ -
\ -
\ -
\ -
').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' + }, + events: { + message: function(event) { + self.element.queue(function() { + self.appendToChat(event); + self.element.dequeue(); }); - }); - - // 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 $('
').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]); + }, + possible_moves: function(event) { + self.element.queue(function() { + self.options.possible_moves = event; + self.indicateTurn(); + self.element.dequeue(); }); - 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(); + }, + 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); } - 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"); - } - } + }); + }, + 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(); }); - }); - 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 + gone: function(event) { + self.$table.find("div.force_resign_zone").toggle(event); + self.centerTable(); }, - 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); + 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(); + }); } - 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) } + })); + }, + 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($("
").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 = $('
').appendTo(self.$board).html('\ +
\ +
\ +
\ +
').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 $('
').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) + } + } });