diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index 854b366bc6..d4e00ebc9b 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -8,8 +8,27 @@ import play.api.mvc._ object Round extends LilaController { val gameRepo = env.game.gameRepo + val socket = env.round.socket def player(id: String) = Open { implicit ctx ⇒ - IOption(gameRepo pov id) { html.round.player(_) } + IOption(gameRepo pov id) { pov ⇒ + html.round.player(pov, socket blockingVersion pov.gameId) + } } + + def abort(fullId: String) = TODO + def resign(fullId: String) = TODO + def resignForce(fullId: String) = TODO + def drawClaim(fullId: String) = TODO + def drawAccept(fullId: String) = TODO + def drawOffer(fullId: String) = TODO + def drawCancel(fullId: String) = TODO + def drawDecline(fullId: String) = TODO + def takebackAccept(fullId: String) = TODO + def takebackOffer(fullId: String) = TODO + def takebackCancel(fullId: String) = TODO + def takebackDecline(fullId: String) = TODO + + def table(gameId: String, color: String, fullId: String) = TODO + def players(gameId: String) = TODO } diff --git a/app/controllers/Setup.scala b/app/controllers/Setup.scala index 9a5211ea4a..d1416631dd 100644 --- a/app/controllers/Setup.scala +++ b/app/controllers/Setup.scala @@ -19,7 +19,7 @@ object Setup extends LilaController { _ ⇒ Redirect(routes.Lobby.home), config ⇒ IORedirect( processor ai config map { pov ⇒ - routes.Round.player(pov.playerFullId) + routes.Round.player(pov.fullId) } ) ) diff --git a/app/core/Settings.scala b/app/core/Settings.scala index ba7c3ef787..163ab49f49 100644 --- a/app/core/Settings.scala +++ b/app/core/Settings.scala @@ -13,6 +13,7 @@ final class Settings(config: Config) { val GameUidTimeout = millis("game.uid.timeout") val GameHubTimeout = millis("game.hub.timeout") val GamePlayerTimeout = millis("game.player.timeout") + val GameAnimationDelay = millis("game.animation.delay") val LobbyEntryMax = getInt("lobby.entry.max") val LobbyMessageMax = getInt("lobby.message.max") diff --git a/app/game/DbGame.scala b/app/game/DbGame.scala index ac7f747a0a..5be2dbcc54 100644 --- a/app/game/DbGame.scala +++ b/app/game/DbGame.scala @@ -49,7 +49,11 @@ case class DbGame( def opponent(p: DbPlayer): DbPlayer = player(!(p.color)) - def player: DbPlayer = player(if (0 == turns % 2) White else Black) + def player: DbPlayer = player(turnColor) + + def turnColor = Color(0 == turns % 2) + + def turnOf(p: DbPlayer) = p == player def fullIdOf(player: DbPlayer): Option[String] = (players contains player) option id + player.id @@ -87,7 +91,7 @@ case class DbGame( ) } - def toChessHistory = ChessHistory( + lazy val toChessHistory = ChessHistory( lastMove = lastMove, castles = castles, positionHashes = positionHashes) @@ -189,13 +193,20 @@ case class DbGame( blackPlayer = f(blackPlayer) ) + def start = started.fold(this, copy( + status = Status.Started, + isRated = isRated && (players forall (_.hasUser)) + )) + def recordMoveTimes = !hasAi def hasMoveTimes = players forall (_.hasMoveTimes) + def started = status >= Status.Started + def playable = status < Status.Aborted - def playableBy(p: DbPlayer) = playable && p == player + def playableBy(p: DbPlayer) = playable && turnOf(p) def aiLevel: Option[Int] = players find (_.isAi) flatMap (_.aiLevel) @@ -207,8 +218,7 @@ case class DbGame( ) def playerCanOfferDraw(color: Color) = - status >= Status.Started && - status < Status.Aborted && + started && playable && turns >= 2 && !player(color).isOfferingDraw && !(player(!color).isAi) && @@ -217,6 +227,11 @@ case class DbGame( def playerHasOfferedDraw(color: Color) = player(color).lastDrawOffer some (_ >= turns - 1) none false + def playerCanProposeTakeback(color: Color) = + started && playable && + turns >= 2 && + !player(color).isProposingTakeback + def abortable = status == Status.Started && turns < 2 def resignable = playable && !abortable @@ -248,6 +263,11 @@ case class DbGame( def withClock(c: Clock) = Progress(this, copy(clock = Some(c))) + def estimateTotalTime = clock.fold( + c ⇒ c.limit + 30 * c.increment, + 1200 // default to 20 minutes + ) + def creator = player(creatorColor) def invited = player(!creatorColor) @@ -266,7 +286,7 @@ object DbGame { def takeGameId(fullId: String) = fullId take gameIdSize def apply( - game: Game, + game: Game, whitePlayer: DbPlayer, blackPlayer: DbPlayer, ai: Option[(Color, Int)], @@ -289,5 +309,5 @@ object DbGame { isRated = isRated, variant = variant, lastMoveTime = None, - createdAt = createdAt.some) + createdAt = createdAt.some) } diff --git a/app/game/DbPlayer.scala b/app/game/DbPlayer.scala index f68b81b2d8..2be3e8cb8d 100644 --- a/app/game/DbPlayer.scala +++ b/app/game/DbPlayer.scala @@ -42,6 +42,8 @@ case class DbPlayer( def userId: Option[String] = user map (_.getId.toString) + def hasUser = user.isDefined + def wins = isWinner getOrElse false def hasMoveTimes = moveTimes.size > 10 diff --git a/app/game/Pov.scala b/app/game/Pov.scala index 0c30f3515b..7293ea8798 100644 --- a/app/game/Pov.scala +++ b/app/game/Pov.scala @@ -9,7 +9,7 @@ case class Pov(game: DbGame, color: Color) { def playerId = player.id - def playerFullId = game fullIdOf color + def fullId = game fullIdOf color def gameId = game.id diff --git a/app/round/HubMaster.scala b/app/round/HubMaster.scala index ba3c845b21..3f82466f39 100644 --- a/app/round/HubMaster.scala +++ b/app/round/HubMaster.scala @@ -9,7 +9,6 @@ import akka.util.duration._ import akka.util.Timeout import akka.pattern.{ ask, pipe } import akka.dispatch.{ Future, Promise } -import akka.event.Logging import play.api.libs.json._ import play.api.libs.concurrent._ import play.api.Play.current @@ -21,7 +20,6 @@ final class HubMaster( playerTimeout: Int) extends Actor { implicit val timeout = Timeout(1 second) - val log = Logging(context.system, this) implicit val executor = Akka.system.dispatcher var hubs = Map.empty[String, ActorRef] diff --git a/app/round/RoundHelper.scala b/app/round/RoundHelper.scala new file mode 100644 index 0000000000..5e96ba80de --- /dev/null +++ b/app/round/RoundHelper.scala @@ -0,0 +1,51 @@ +package lila +package round + +import http.Context +import game.Pov +import templating.ConfigHelper + +import com.codahale.jerkson.Json +import scala.math.{ min, max, round } + +trait RoundHelper { self: ConfigHelper ⇒ + + def roundJsData(pov: Pov, version: Int) = Json generate { + + import pov._ + + Map( + "game" -> Map( + "id" -> gameId, + "started" -> game.started, + "finished" -> game.finished, + "clock" -> game.hasClock, + "player" -> game.turnColor.name, + "turns" -> game.turns, + "lastMove" -> game.lastMove + ), + "player" -> Map( + "id" -> player.id, + "color" -> player.color.name, + "version" -> version, + "spectator" -> false + ), + "opponent" -> Map( + "color" -> opponent.color.name, + "ai" -> opponent.isAi + ), + "possible_moves" -> possibleMoves(pov), + "animation_delay" -> animationDelay(pov) + ) + } + + private def possibleMoves(pov: Pov) = (pov.game playableBy pov.player) option { + pov.game.toChess.situation.destinations map { + case (from, dests) ⇒ from.key -> (dests.mkString) + } toMap + } + + private def animationDelay(pov: Pov) = round { + gameAnimationDelay * max(0, min(1.2, ((pov.game.estimateTotalTime - 60) / 60) * 0.2)) + } +} diff --git a/app/round/Socket.scala b/app/round/Socket.scala index 6e06979d0a..0e4286102a 100644 --- a/app/round/Socket.scala +++ b/app/round/Socket.scala @@ -5,6 +5,7 @@ import akka.actor._ import akka.pattern.ask import akka.util.duration._ import akka.util.Timeout +import akka.dispatch.Await import play.api.libs.json._ import play.api.libs.iteratee._ @@ -24,7 +25,12 @@ final class Socket( val hubMaster: ActorRef, messenger: Messenger) { - implicit val timeout = Timeout(1 second) + private val timeoutDuration = 1 second + implicit private val timeout = Timeout(timeoutDuration) + + def blockingVersion(gameId: String): Int = Await.result( + hubMaster ? GetGameVersion(gameId) mapTo manifest[Int], + timeoutDuration) def send(progress: Progress): IO[Unit] = send(progress.game.id, progress.events) @@ -33,7 +39,7 @@ final class Socket( hubMaster ! GameEvents(gameId, events) } - def controller( + private def controller( hub: ActorRef, uid: String, member: Member, diff --git a/app/setup/AiConfig.scala b/app/setup/AiConfig.scala index 5261d4b09f..e4c15b7b26 100644 --- a/app/setup/AiConfig.scala +++ b/app/setup/AiConfig.scala @@ -23,7 +23,7 @@ case class AiConfig(variant: Variant, level: Int, color: Color) extends Config { creatorColor = creatorColor, isRated = false, variant = variant, - createdAt = DateTime.now) + createdAt = DateTime.now).start } object AiConfig extends BaseConfig { diff --git a/app/templating/ConfigHelper.scala b/app/templating/ConfigHelper.scala index a741207176..d875c8a4fa 100644 --- a/app/templating/ConfigHelper.scala +++ b/app/templating/ConfigHelper.scala @@ -8,4 +8,6 @@ trait ConfigHelper { protected def env: CoreEnv def moretimeSeconds = env.settings.MoretimeSeconds + + def gameAnimationDelay = env.settings.GameAnimationDelay } diff --git a/app/templating/Environment.scala b/app/templating/Environment.scala index 9cd3e2957c..596d26d6bc 100644 --- a/app/templating/Environment.scala +++ b/app/templating/Environment.scala @@ -2,11 +2,14 @@ package lila package templating import core.Global.{ env ⇒ coreEnv } // OMG +import round.RoundHelper import http.{ HttpEnvironment, Setting } object Environment extends HttpEnvironment with scalaz.Identitys + with scalaz.Options + with scalaz.Booleans with StringHelper with AssetHelper with I18nHelper @@ -14,7 +17,8 @@ object Environment with RequestHelper with SettingHelper with UserHelper - with ConfigHelper { + with ConfigHelper + with RoundHelper { protected def env = coreEnv } diff --git a/app/ui/Board.scala b/app/ui/Board.scala index 3b0842f5a1..90a65ca4eb 100644 --- a/app/ui/Board.scala +++ b/app/ui/Board.scala @@ -17,13 +17,13 @@ object Board { s.pos.key, s.top, s.left) ++ - """
""" ++ { + """
""" ++ { board(s.pos) map { piece ⇒ """
""".format( piece.role.name, piece.color.name) } getOrElse "" } ++ - "
" + "" } mkString } diff --git a/app/views/round/player.scala.html b/app/views/round/player.scala.html index 533f046944..394d370d5b 100644 --- a/app/views/round/player.scala.html +++ b/app/views/round/player.scala.html @@ -1,4 +1,4 @@ -@(pov: Pov)(implicit ctx: Context) +@(pov: Pov, version: Int)(implicit ctx: Context) @import pov._ @@ -18,7 +18,9 @@ } @round.layout(title = title, goodies = goodies) { -
+
@widget.connection()
@Html(ui.Board.render(pov))
@@ -55,4 +57,5 @@ @round.cemetery(pov, "bottom")
+ } diff --git a/app/views/round/table.scala.html b/app/views/round/table.scala.html index cfbad1d871..95ff2580bf 100644 --- a/app/views/round/table.scala.html +++ b/app/views/round/table.scala.html @@ -1,3 +1,69 @@ @(pov: Pov)(implicit ctx: Context) -table +@import pov._ + +
+
+
+

@player.color.white.fold(trans.yourTurn(), trans.waiting())

+
+
+
+

@player.color.black.fold(trans.yourTurn(), trans.waiting())

+
+
+
+ @if(game.abortable) { + @trans.abortGame() + } else { + @trans.resign() + @if(game.playerCanOfferDraw(color)) { + @trans.offerDraw() + } + @if(game.playerCanProposeTakeback(color)) { + @trans.takeback() + } + } +
+@if(game.resignable && !game.hasAi) { +
+ @trans.theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim()
+ @trans.forceResignation() +
+} +@if(game.turnOf(player) && game.toChessHistory.threefoldRepetition) { +
+ @trans.threefoldRepetition().  + @trans.claimADraw() +
+} else { +@if(player.isOfferingDraw) { +
+ @trans.drawOfferSent().  + @trans.cancel() +
+} else { +@if(opponent.isOfferingDraw) { +
+ @trans.yourOpponentOffersADraw().
+ @trans.accept()  + @trans.decline() +
+} else { +@if(player.isProposingTakeback) { +
+ @trans.takebackPropositionSent().  + @trans.cancel() +
+} else { +@if(opponent.isProposingTakeback) { +
+ @trans.yourOpponentProposesATakeback().
+ @trans.accept()  + @trans.decline() +
+} +} +} +} +} diff --git a/app/views/round/username.scala.html b/app/views/round/username.scala.html index 60351ce90c..01b9425950 100644 --- a/app/views/round/username.scala.html +++ b/app/views/round/username.scala.html @@ -1,11 +1,15 @@ @(player: DbPlayer)(implicit ctx: Context) -@if(player.isAi) { +@ai(level: Int) = {
- @trans.aiNameLevelAiLevel("Crafty A.I.", player.aiLevel) + @trans.aiNameLevelAiLevel("Crafty A.I.", level)
-} else { +} + +@human = {
@playerLink(player, "blank_if_play")
} + +@player.aiLevel.fold(ai, human) diff --git a/conf/application.conf b/conf/application.conf index 510f3fe919..08861b1cd2 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -28,6 +28,7 @@ game { uid.timeout = 10 seconds hub.timeout = 2 minutes player.timeout = 1 minute + animation.delay = 200 ms } site { uid.timeout = 10 seconds diff --git a/conf/routes b/conf/routes index e353ead0e7..746fb2e8bb 100644 --- a/conf/routes +++ b/conf/routes @@ -2,12 +2,27 @@ GET /games controllers.Game.list # Round -GET /$id<[\w\-]{12}> controllers.Round.player(id: String) +GET /$fullId<[\w\-]{12}> controllers.Round.player(fullId: String) +GET /abort/$fullId<[\w\-]{12}> controllers.Round.abort(fullId: String) +GET /resign/$fullId<[\w\-]{12}> controllers.Round.resign(fullId: String) +GET /resign-force/$fullId<[\w\-]{12}> controllers.Round.resignForce(fullId: String) +GET /draw-claim/$fullId<[\w\-]{12}> controllers.Round.drawClaim(fullId: String) +GET /draw-accept/$fullId<[\w\-]{12}> controllers.Round.drawAccept(fullId: String) +GET /draw-offer/$fullId<[\w\-]{12}> controllers.Round.drawOffer(fullId: String) +GET /draw-cancel/$fullId<[\w\-]{12}> controllers.Round.drawCancel(fullId: String) +GET /draw-decline/$fullId<[\w\-]{12}> controllers.Round.drawDecline(fullId: String) +GET /takeback-accept/$fullId<[\w\-]{12}> controllers.Round.takebackAccept(fullId: String) +GET /takeback-offer/$fullId<[\w\-]{12}> controllers.Round.takebackOffer(fullId: String) +GET /takeback-cancel/$fullId<[\w\-]{12}> controllers.Round.takebackCancel(fullId: String) +GET /takeback-decline/$fullId<[\w\-]{12}> controllers.Round.takebackDecline(fullId: String) +GET /table/$gameId<[\w\-]{8}>/$color<[white|black]> controllers.Round.table(gameId: String, color: String, fullId: String = "") +GET /table/$gameId<[\w\-]{8}>/$color<[white|black]>/$fullId<[\w\-]{12}> controllers.Round.table(gameId: String, color: String, fullId: String) +GET /players/$gameId<[\w\-]{8}> controllers.Round.players(gameId: String) # Analyse -GET /analyse/$id<[\w\-]{8}> controllers.Analyse.replay(id: String, color: String = "white") -GET /analyse/$id<[\w\-]{8}>/$color<[white|black]> controllers.Analyse.replay(id: String, color: String) -GET /$id<[\w\-]{8}>/stats controllers.Analyse.stats(id: String) +GET /analyse/$gameId<[\w\-]{8}> controllers.Analyse.replay(gameId: String, color: String = "white") +GET /analyse/$gameId<[\w\-]{8}>/$color<[white|black]> controllers.Analyse.replay(gameId: String, color: String) +GET /$gameId<[\w\-]{8}>/stats controllers.Analyse.stats(gameId: String) # Setting POST /setting/color controllers.Setting.color @@ -39,25 +54,12 @@ 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 /abort/:fullId controllers.App.abort(fullId: String) -GET /resign/:fullId controllers.App.resign(fullId: String) -GET /resign-force/:fullId controllers.App.resignForce(fullId: String) -GET /draw-claim/:fullId controllers.App.drawClaim(fullId: String) -GET /draw-accept/:fullId controllers.App.drawAccept(fullId: String) -GET /draw-offer/:fullId controllers.App.drawOffer(fullId: String) -GET /draw-cancel/:fullId controllers.App.drawCancel(fullId: String) -GET /draw-decline/:fullId controllers.App.drawDecline(fullId: String) -GET /takeback-accept/:fullId controllers.App.takebackAccept(fullId: String) -GET /takeback-offer/:fullId controllers.App.takebackOffer(fullId: String) -GET /takeback-cancel/:fullId controllers.App.takebackCancel(fullId: String) -GET /takeback-decline/:fullId controllers.App.takebackDecline(fullId: String) GET /ai controllers.Ai.run # App Private API -GET /api/show/:fullId controllers.AppApi.show(fullId: String) POST /api/start/:gameId controllers.AppApi.start(gameId: String) -POST /api/join/:fullId controllers.AppApi.join(fullId: String) +POST /api/join/$fullId<[\w\-]{12}> controllers.AppApi.join(fullId: String) POST /api/reload-table/:gameId controllers.AppApi.reloadTable(gameId: String) POST /api/adjust/:username controllers.AppApi.adjust(username: String) GET /api/activity/:gameId/:color controllers.AppApi.activity(gameId: String, color: String) diff --git a/public/javascripts/game.js b/public/javascripts/game.js index 5b86f57ac0..256cac5fc5 100644 --- a/public/javascripts/game.js +++ b/public/javascripts/game.js @@ -11,6 +11,8 @@ $.widget("lichess.game", { 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(); @@ -514,7 +516,7 @@ $.widget("lichess.game", { }, reloadTable: function(callback) { var self = this; - self.get(self.options.url.table, { + self.get(self.options.tableUrl, { success: function(html) { $('body > div.tipsy').remove(); self.$tableInner.html(html); @@ -526,7 +528,7 @@ $.widget("lichess.game", { }, reloadPlayers: function(callback) { var self = this; - $.getJSON(self.options.url.players, function(data) { + $.getJSON(self.options.playersUrl, function(data) { $(['white', 'black']).each(function() { if (data[this]) self.$table.find('div.username.' + this).html(data[this]); }); diff --git a/public/javascripts/socket.js b/public/javascripts/socket.js index 00ef916691..aeea7f7b9a 100644 --- a/public/javascripts/socket.js +++ b/public/javascripts/socket.js @@ -56,10 +56,11 @@ $.websocket.prototype = { }) .bind('message', function(e){ var m = JSON.parse(e.originalEvent.data); - self._debug(m); if (m.t == "n") { self.keepAlive(); - } + } else { + self._debug(m); + } if (m.t == "batch") { $(m.d || []).each(function() { self._handle(this); }); } else {