diff --git a/README.md b/README.md index 62dc1b446e..a6d79b75fb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [lichess.org](http://lichess.org) --------------------------------- +lichess.org + It's a free online chess game focused on [realtime](http://lichess.org/games) and ease of use It has a [search engine](http://lichess.org/games/search), diff --git a/app/Env.scala b/app/Env.scala index 28e04c1ca5..a4618d88b9 100644 --- a/app/Env.scala +++ b/app/Env.scala @@ -20,7 +20,7 @@ final class Env( lobby = Env.lobby.lobby, lobbyVersion = () => Env.lobby.history.version, featured = Env.tv.featured, - leaderboard = Env.user.cached.topToday.apply, + leaderboard = Env.user.cached.topToday, tourneyWinners = Env.tournament.winners.scheduled, timelineEntries = Env.timeline.getter.userEntries _, dailyPuzzle = Env.puzzle.daily, diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala index 19f9766281..ae4403d3a4 100644 --- a/app/controllers/Game.scala +++ b/app/controllers/Game.scala @@ -33,10 +33,10 @@ object Game extends LilaController with BaseGame { } } - def realtime = Open { implicit ctx => + def playing = Open { implicit ctx => GameRepo.featuredCandidates map lila.tv.Featured.sort map (_ take 9) zip makeListMenu map { - case (games, menu) => html.game.realtime(games, menu) + case (games, menu) => html.game.playing(games, menu) } } diff --git a/app/controllers/Round.scala b/app/controllers/Round.scala index 00ab1f7462..c0b5aceb67 100644 --- a/app/controllers/Round.scala +++ b/app/controllers/Round.scala @@ -54,10 +54,10 @@ object Round extends LilaController with TheftPrevention { PreventTheft(pov) { (pov.game.tournamentId ?? TournamentRepo.byId) zip Env.game.crosstableApi(pov.game) zip - otherGames(pov.game) flatMap { - case ((tour, crosstable), otherGames) => + otherPovs(pov.gameId) flatMap { + case ((tour, crosstable), playing) => Env.api.roundApi.player(pov, Env.api.version) map { data => - Ok(html.round.player(pov, data, tour = tour, cross = crosstable, otherGames = otherGames)) + Ok(html.round.player(pov, data, tour = tour, cross = crosstable, playing = playing)) } } }, @@ -69,11 +69,30 @@ object Round extends LilaController with TheftPrevention { } } - private def otherGames(g: GameModel)(implicit ctx: Context) = ctx.me.ifFalse(g.hasClock) ?? { user => + private def otherPovs(gameId: String)(implicit ctx: Context) = ctx.me ?? { user => GameRepo nowPlaying user map { - _ filter { pov => - pov.isMyTurn && pov.game.id != g.id - } sortBy Pov.priority + _ filter { _.game.id != gameId } + } + } + + private def getNext(currentGame: GameModel)(povs: List[Pov])(implicit ctx: Context) = + povs find { pov => + pov.isMyTurn && (pov.game.hasClock || !currentGame.hasClock) + } map (_.fullId) + + def others(gameId: String) = Open { implicit ctx => + OptionFuResult(GameRepo game gameId) { currentGame => + otherPovs(gameId) map { povs => + Ok(html.round.others(povs, nextId = getNext(currentGame)(povs))) + } + } + } + + def next(gameId: String) = Open { implicit ctx => + OptionFuResult(GameRepo game gameId) { currentGame => + otherPovs(gameId) map getNext(currentGame) map { nextId => + Ok(Json.obj("next" -> nextId)) + } } } diff --git a/app/controllers/User.scala b/app/controllers/User.scala index 7cc6dcea34..13b9ee2ffa 100644 --- a/app/controllers/User.scala +++ b/app/controllers/User.scala @@ -23,9 +23,9 @@ object User extends LilaController { def tv(username: String) = Open { implicit ctx => OptionFuResult(UserRepo named username) { user => - (GameRepo nowPlaying user.id) orElse - (GameRepo lastPlayed user.id) flatMap { - _.flatMap { Pov(_, user) }.fold(fuccess(Redirect(routes.User.show(username)))) { pov => + (GameRepo lastPlayed user) orElse + (GameRepo lastPlayed user) flatMap { + _.fold(fuccess(Redirect(routes.User.show(username)))) { pov => Round.watch(pov, userTv = user.some) } } @@ -38,12 +38,12 @@ object User extends LilaController { def showMini(username: String) = Open { implicit ctx => OptionFuResult(UserRepo named username) { user => - GameRepo nowPlaying user.id zip + GameRepo lastPlayed user zip (ctx.userId ?? { relationApi.blocks(user.id, _) }) zip (ctx.isAuth ?? { Env.pref.api.followable(user.id) }) zip (ctx.userId ?? { relationApi.relation(_, user.id) }) map { - case (((game, blocked), followable), relation) => - Ok(html.user.mini(user, game, blocked, followable, relation)) + case (((pov, blocked), followable), relation) => + Ok(html.user.mini(user, pov, blocked, followable, relation)) .withHeaders(CACHE_CONTROL -> "max-age=5") } } @@ -119,7 +119,7 @@ object User extends LilaController { user -> user.count.game } nbWeek ← Env.game.cached activePlayerUidsWeek nb flatMap { pairs => - UserRepo.byOrderedIds(pairs.map(_._1)) map (_ zip pairs.map(_._2)) + UserRepo.byOrderedIds(pairs.map(_.userId)) map (_ zip pairs.map(_.nb)) } tourneyWinners ← Env.tournament.winners scheduled nb online ← env.cached topOnline 30 diff --git a/app/controllers/UserAnalysis.scala b/app/controllers/UserAnalysis.scala new file mode 100644 index 0000000000..ca85968712 --- /dev/null +++ b/app/controllers/UserAnalysis.scala @@ -0,0 +1,55 @@ +package controllers + +import chess.format.Forsyth +import chess.{ Situation, Variant } +import play.api.libs.json.Json + +import lila.app._ +import lila.game.{ GameRepo, Pov } +import views._ + +object UserAnalysis extends LilaController with BaseGame { + + def index = load("") + + def load(urlFen: String) = Open { implicit ctx => + val fenStr = Some(urlFen.trim.replace("_", " ")).filter(_.nonEmpty) orElse get("fen") + val decodedFen = fenStr.map { java.net.URLDecoder.decode(_, "UTF-8").trim }.filter(_.nonEmpty) + val situation = (decodedFen flatMap Forsyth.<<< map (_.situation)) | Situation(Variant.Standard) + val pov = makePov(situation) + val data = Env.round.jsonView.userAnalysisJson(pov, ctx.pref) + makeListMenu map { listMenu => + Ok(html.board.userAnalysis(listMenu, data, none)) + } + } + + private def makePov(situation: Situation) = lila.game.Pov( + lila.game.Game.make( + game = chess.Game(situation.board, situation.color), + whitePlayer = lila.game.Player.white, + blackPlayer = lila.game.Player.black, + mode = chess.Mode.Casual, + variant = chess.Variant.Standard, + source = lila.game.Source.Api, + pgnImport = None, + castles = situation.board.history.castles), + situation.color) + + def game(id: String, color: String) = Open { implicit ctx => + OptionFuOk(GameRepo game id) { game => + val pov = Pov(game, chess.Color(color == "white")) + val data = Env.round.jsonView.userAnalysisJson(pov, ctx.pref) + makeListMenu map { listMenu => + html.board.userAnalysis(listMenu, data, pov.some) + } + } + } + + // def game(id: String) = Open { implicit ctx => + // OptionResult(GameRepo game id) { game => + // Redirect(routes.Editor.load( + // get("fen") | (chess.format.Forsyth >> game.toChess) + // )) + // } + // } +} diff --git a/app/mashup/Preload.scala b/app/mashup/Preload.scala index 40232f65ed..2b4429f4c4 100644 --- a/app/mashup/Preload.scala +++ b/app/mashup/Preload.scala @@ -9,7 +9,7 @@ import controllers.routes import lila.api.Context import lila.forum.MiniForumPost import lila.game.{ Game, GameRepo, Pov } -import lila.lobby.actorApi.GetOpen +import lila.lobby.actorApi.HooksFor import lila.lobby.{ Hook, HookRepo, Seek, SeekApi } import lila.rating.PerfType import lila.setup.FilterConfig @@ -38,7 +38,7 @@ final class Preload( posts: Fu[List[MiniForumPost]], tours: Fu[List[Enterable]], filter: Fu[FilterConfig])(implicit ctx: Context): Fu[Response] = - (lobby ? GetOpen(ctx.me)).mapTo[List[Hook]] zip + (lobby ? HooksFor(ctx.me)).mapTo[List[Hook]] zip posts zip tours zip featured.one zip diff --git a/app/templating/GameHelper.scala b/app/templating/GameHelper.scala index 0fa5a4bc6a..8d7c24da0e 100644 --- a/app/templating/GameHelper.scala +++ b/app/templating/GameHelper.scala @@ -178,8 +178,9 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel ownerLink: Boolean = false, tv: Boolean = false, withTitle: Boolean = true, - withLink: Boolean = true)(implicit ctx: UserContext) = Html { - var isLive = game.isBeingPlayed + withLink: Boolean = true, + withLive: Boolean = true)(implicit ctx: UserContext) = Html { + var isLive = withLive && game.isBeingPlayed val href = withLink ?? { val owner = ownerLink.fold(ctx.me flatMap game.player, none) val url = tv.fold(routes.Tv.index, owner.fold(routes.Round.watcher(game.id, color.name)) { o => @@ -194,13 +195,13 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel val lastMove = ~game.castleLastMoveTime.lastMoveString val variant = game.variant.key val tag = if (withLink) "a" else "span" - s"""<$tag $href $title class="mini_board parse_fen is2d $cssClass $variant" data-live="$live" data-color="${color.name}" data-fen="$fen" data-lastmove="$lastMove">$miniBoardContent""" + s"""<$tag $href $title class="mini_board mini_board_${game.id} parse_fen is2d $cssClass $variant" data-live="$live" data-color="${color.name}" data-fen="$fen" data-lastmove="$lastMove">$miniBoardContent""" } def gameFenNoCtx(game: Game, color: Color, tv: Boolean = false, blank: Boolean = false) = Html { var isLive = game.isBeingPlayed val variant = game.variant.key - s"""$miniBoardContent""".format( + s"""$miniBoardContent""".format( blank ?? netBaseUrl, tv.fold(routes.Tv.index, routes.Round.watcher(game.id, color.name)), gameTitle(game, color), diff --git a/app/ui/SiteMenu.scala b/app/ui/SiteMenu.scala index 338d8fed19..0fb2f6063c 100644 --- a/app/ui/SiteMenu.scala +++ b/app/ui/SiteMenu.scala @@ -12,7 +12,7 @@ final class SiteMenu(trans: I18nKeys) { import SiteMenu._ val play = new Elem("play", routes.Lobby.home, trans.play) - val game = new Elem("game", routes.Game.realtime, trans.games) + val game = new Elem("game", routes.Game.playing, trans.games) val puzzle = new Elem("puzzle", routes.Puzzle.home, trans.training) val tournament = new Elem("tournament", routes.Tournament.home, trans.tournaments) val user = new Elem("user", routes.User.list, trans.players) diff --git a/app/views/base/layout.scala.html b/app/views/base/layout.scala.html index b15d5b08eb..b0d9b8e5ff 100644 --- a/app/views/base/layout.scala.html +++ b/app/views/base/layout.scala.html @@ -11,7 +11,8 @@ robots: Boolean = true, moreCss: Html = Html(""), moreJs: Html = Html(""), zen: Boolean = false, -openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context) +openGraph: Map[Symbol, String] = Map.empty, +chessground: Boolean = true)(body: Html)(implicit ctx: Context) @@ -250,7 +251,7 @@ openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context) Blog | Q&A | Wiki
- Map | + Map | Monitor | Help lichess.org @@ -271,12 +272,12 @@ openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context) } @jQueryTag - @jsTag("vendor/chessground.min.js") + @if(chessground) {@jsTag("vendor/chessground.min.js")} @jsTag("deps.min.js") @momentjsTag @powertipTag @jsTagCompiled("common.js") - @jsTagCompiled("strongSocket.js") + @jsTagCompiled("socket.js") @jsTagCompiled("big.js") @moreJs @jsAt(s"trans/${lang.language}.js") diff --git a/app/views/board/editor.scala.html b/app/views/board/editor.scala.html index 7174115267..282fa3df82 100644 --- a/app/views/board/editor.scala.html +++ b/app/views/board/editor.scala.html @@ -30,7 +30,8 @@ LichessEditor(document.getElementById('board_editor'), { trans.whitePlays, trans.blackPlays, trans.playWithTheMachine, - trans.playWithAFriend + trans.playWithAFriend, + trans.analysis ))) }); } diff --git a/app/views/board/userAnalysis.scala.html b/app/views/board/userAnalysis.scala.html new file mode 100644 index 0000000000..408d38e7ea --- /dev/null +++ b/app/views/board/userAnalysis.scala.html @@ -0,0 +1,33 @@ +@(listMenu: lila.game.ListMenu, data: play.api.libs.json.JsObject, pov: Option[Pov])(implicit ctx: Context) + +@moreCss = { +@cssTag("analyse.css") +} + +@moreJs = { +@jsAt(s"compiled/lichess.analyse${isProd??(".min")}.js") +@round.jsRoutes() +@embedJs { +lichess = lichess || {}; +lichess.user_analysis = { +data: @Html(play.api.libs.json.Json.stringify(data)), +routes: roundRoutes.controllers, +i18n: @round.jsI18n() +}; +} +} + +@game.layout( +title = trans.analysis.str(), +moreCss = moreCss, +moreJs = moreJs, +menu = game.sideMenu(listMenu, "userAnalysis").some) { +
@miniBoardContent
+@pov.map { p => +
+ + @trans.backToGame() + +
+} +} diff --git a/app/views/game/featuredJs.scala.html b/app/views/game/featuredJs.scala.html index 44b8756c81..3df6341f0e 100644 --- a/app/views/game/featuredJs.scala.html +++ b/app/views/game/featuredJs.scala.html @@ -1,3 +1,3 @@ @(g: lila.game.Game) @gameFenNoCtx(g, g.firstPlayer.color, tv = true) -@game.vstext(g) +@game.vstext(g)(none) diff --git a/app/views/game/realtime.scala.html b/app/views/game/playing.scala.html similarity index 56% rename from app/views/game/realtime.scala.html rename to app/views/game/playing.scala.html index f2ed7735e9..0e8869d564 100644 --- a/app/views/game/realtime.scala.html +++ b/app/views/game/playing.scala.html @@ -2,13 +2,13 @@ @game.layout( title = trans.gamesBeingPlayedRightNow.str(), -menu = sideMenu(listMenu, "realtime").some) { -
-
+menu = sideMenu(listMenu, "playing").some) { +
+
@games.map { g =>
@gameFen(g, g.firstPlayer.color) - @game.vstext(g) + @game.vstext(g)(ctx.some)
}
diff --git a/app/views/game/sideMenu.scala.html b/app/views/game/sideMenu.scala.html index 3cb4530567..6ef226138e 100644 --- a/app/views/game/sideMenu.scala.html +++ b/app/views/game/sideMenu.scala.html @@ -1,6 +1,6 @@ @(listMenu: lila.game.ListMenu, active: String)(implicit ctx: Context) - + @trans.gamesBeingPlayedRightNow() @@ -12,6 +12,9 @@ @trans.boardEditor() + + @trans.analysis() + @trans.viewAllNbGames(listMenu.nbGames.localize) diff --git a/app/views/game/vstext.scala.html b/app/views/game/vstext.scala.html index 7f720767a0..d965fb9f3e 100644 --- a/app/views/game/vstext.scala.html +++ b/app/views/game/vstext.scala.html @@ -1,4 +1,4 @@ -@(g: Game) +@(g: Game)(ctxOption: Option[Context])
@playerUsername(g.firstPlayer, withRating = false, withTitle = false) @@ -14,5 +14,13 @@
@g.clock.map { c =>
 @shortClockName(c)
+}.getOrElse { +@ctxOption.map { ctx => +@g.daysPerTurn.map { days => +
+@{(days == 1).fold(trans.oneDay()(ctx), trans.nbDays(days)(ctx))} +
+} +} }
diff --git a/app/views/game/widgets.scala.html b/app/views/game/widgets.scala.html index 27e042880c..4e43313ada 100644 --- a/app/views/game/widgets.scala.html +++ b/app/views/game/widgets.scala.html @@ -23,7 +23,7 @@ } @games.map { g => -
+
@defining(user flatMap g.player) { fromPlayer => @defining(fromPlayer | g.firstPlayer ) { firstPlayer => @gameFen(g, firstPlayer.color, ownerLink, withTitle = false) diff --git a/app/views/lobby/home.scala.html b/app/views/lobby/home.scala.html index da7e0f5f45..17689c22a5 100644 --- a/app/views/lobby/home.scala.html +++ b/app/views/lobby/home.scala.html @@ -4,7 +4,7 @@ } diff --git a/app/views/lobby/playing.scala.html b/app/views/lobby/playing.scala.html index f35fed90e9..0f8dffa88b 100644 --- a/app/views/lobby/playing.scala.html +++ b/app/views/lobby/playing.scala.html @@ -2,7 +2,7 @@ @povs.take(9).map { pov => - @gameFen(pov.game, pov.color, withLink = false, withTitle = false) + @gameFen(pov.game, pov.color, withLink = false, withTitle = false, withLive = false) @playerText(pov.opponent, withRating = false) diff --git a/app/views/round/jsRoutes.scala.html b/app/views/round/jsRoutes.scala.html index 218ff97d7b..c23b23ad83 100644 --- a/app/views/round/jsRoutes.scala.html +++ b/app/views/round/jsRoutes.scala.html @@ -9,7 +9,9 @@ routes.javascript.Round.playerText, routes.javascript.Round.watcherText, routes.javascript.Round.sideWatcher, routes.javascript.Round.continue, +routes.javascript.Round.next, /* routes.javascript.Tv.index */ -routes.javascript.Tv.side +routes.javascript.Tv.side, +routes.javascript.UserAnalysis.game /* routes.javascript.Editor.game */ )(ctx.req) diff --git a/app/views/round/layout.scala.html b/app/views/round/layout.scala.html index f3c13deadc..8d6edea09b 100644 --- a/app/views/round/layout.scala.html +++ b/app/views/round/layout.scala.html @@ -1,4 +1,4 @@ -@(title: String, side: Html, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true, moreJs: Html = Html(""), active: Option[lila.app.ui.SiteMenu.Elem] = None, openGraph: Map[Symbol, String] = Map.empty, moreCss: Html = Html(""))(body: Html)(implicit ctx: Context) +@(title: String, side: Html, chat: Option[Html] = None, underchat: Option[Html] = None, robots: Boolean = true, moreJs: Html = Html(""), active: Option[lila.app.ui.SiteMenu.Elem] = None, openGraph: Map[Symbol, String] = Map.empty, moreCss: Html = Html(""), chessground: Boolean = true)(body: Html)(implicit ctx: Context) @base.layout( title = title, @@ -9,4 +9,5 @@ underchat = underchat, robots = robots, openGraph = openGraph, moreJs = moreJs, -moreCss = moreCss)(body) +moreCss = moreCss, +chessground = chessground)(body) diff --git a/app/views/round/others.scala.html b/app/views/round/others.scala.html new file mode 100644 index 0000000000..b23d176911 --- /dev/null +++ b/app/views/round/others.scala.html @@ -0,0 +1,15 @@ +@(playing: List[Pov], nextId: Option[String])(implicit ctx: Context) +@nextId.map { id => + +} +

+ + @trans.gamesBeingPlayedRightNow() +

+@defining(playing.partition(_.isMyTurn)) { +case (myTurn, otherTurn) => { +@lobby.playing(myTurn ++ otherTurn.take(6 - myTurn.size)) +} +} diff --git a/app/views/round/player.scala.html b/app/views/round/player.scala.html index b192f650de..41cacebf9d 100644 --- a/app/views/round/player.scala.html +++ b/app/views/round/player.scala.html @@ -1,4 +1,4 @@ -@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable], otherGames: List[Pov])(implicit ctx: Context) +@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable], playing: List[Pov])(implicit ctx: Context) @import pov._ @@ -23,17 +23,17 @@ side = views.html.game.side(pov, tour, withTourStanding = true), chat = pov.game.hasChat.option(base.chatDom(trans.chatRoom.str(), ctx.isAuth)), underchat = views.html.game.watchers().some, moreJs = moreJs, -openGraph = povOpenGraph(pov)) { +openGraph = povOpenGraph(pov), +chessground = false) {
@miniBoardContent
-@if(otherGames.nonEmpty) { -
-

Other correspondence games

- @lobby.playing(otherGames take 6) +@if(playing.nonEmpty) { +
+ @others(playing, none)
} } diff --git a/app/views/round/watcher.scala.html b/app/views/round/watcher.scala.html index ece9cf24a7..0b056f64ac 100644 --- a/app/views/round/watcher.scala.html +++ b/app/views/round/watcher.scala.html @@ -1,8 +1,6 @@ @(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable], userTv: Option[User] = None)(implicit ctx: Context) -@import pov._ - -@title = @{ s"${playerText(pov.player)} vs ${playerText(pov.opponent)} in $gameId" } +@title = @{ s"${playerText(pov.player)} vs ${playerText(pov.opponent)} in ${pov.gameId}" } @moreJs = { @jsAt(s"compiled/lichess.round${isProd??(".min")}.js") @@ -23,7 +21,8 @@ side = views.html.game.side(pov, tour, withTourStanding = false, userTv = userTv chat = base.chatDom(trans.spectatorRoom.str()).some, underchat = views.html.game.watchers().some, moreJs = moreJs, -openGraph = povOpenGraph(pov)) { +openGraph = povOpenGraph(pov), +chessground = false) {
@miniBoardContent