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) ++ - """
@player.color.white.fold(trans.yourTurn(), trans.waiting())
+@player.color.black.fold(trans.yourTurn(), trans.waiting())
+