Merge branch 'master' into antichess

* master: (116 commits)
  use Game.updatedAt when last move time is not available
  integrate analysis board with correspondence games
  improve analysis integration
  add screenshot
  fix analysis board highlights
  more cache tweaks
  don't show games older than 5 minutes
  improve current game detection
  hr "hrvatski" translation #11283. Author: gus_fring.
  update sl translation
  hu "Magyar" translation #11279. Author: OMMHOA. Couldn't translate perfectly "proceed" so it's "go" instead.
  ca "Català, valencià" translation #11278. Author: pedrolo.
  uk "українська" translation #11276. Author: IvTK.
  nl "Nederlands" translation #11274. Author: rokbe. correspondensie -> correspondentie
  sv "svenska" translation #11273. Author: nuwonga.
  sq "Shqip" translation #11271. Author: xhevati.
  pl "polski" translation #11268. Author: pirouetti.
  tr "Türkçe" translation #11265. Author: mabolek.
  sv "svenska" translation #11264. Author: Weckipecki.
  ca "Català, valencià" translation #11263. Author: Borchess.
  ...

Conflicts:
	modules/chess
	ui/analyse/src/ctrl.js
pull/185/head
Thibault Duplessis 2014-12-24 15:44:34 +01:00
commit b96b982209
143 changed files with 1104 additions and 354 deletions

View File

@ -1,6 +1,8 @@
[lichess.org](http://lichess.org)
---------------------------------
<img src="https://raw.githubusercontent.com/ornicar/lila/master/public/images/homepage_light.1200.png" alt="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),

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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)
// ))
// }
// }
}

View File

@ -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

View File

@ -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</$tag>"""
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</$tag>"""
}
def gameFenNoCtx(game: Game, color: Color, tv: Boolean = false, blank: Boolean = false) = Html {
var isLive = game.isBeingPlayed
val variant = game.variant.key
s"""<a href="%s%s" title="%s" class="mini_board parse_fen is2d %s $variant" data-live="%s" data-color="%s" data-fen="%s" data-lastmove="%s"%s>$miniBoardContent</a>""".format(
s"""<a href="%s%s" title="%s" class="mini_board mini_board_${game.id} parse_fen is2d %s $variant" data-live="%s" data-color="%s" data-fen="%s" data-lastmove="%s"%s>$miniBoardContent</a>""".format(
blank ?? netBaseUrl,
tv.fold(routes.Tv.index, routes.Round.watcher(game.id, color.name)),
gameTitle(game, color),

View File

@ -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)

View File

@ -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)
<!doctype html>
<html lang="@lang.language">
<head>
@ -250,7 +251,7 @@ openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context)
<a href="@routes.Blog.index()">Blog</a> |
<a href="@routes.QaQuestion.index()" title="Questions &amp; Answers">Q&amp;A</a> |
<a href="@routes.Wiki.home" title="@trans.learnMoreAboutLichess()">Wiki</a><br />
<a href="@routes.WorldMap.index" title="Realtime world map of chess moves">Map</a> |
<a href="@routes.WorldMap.index" title="Real time world map of chess moves">Map</a> |
<a href="@routes.Monitor.index">Monitor</a> |
<a href="@routes.Page.helpLichess">Help lichess.org</a>
</div>
@ -271,12 +272,12 @@ openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context)
</div>
}
@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")

View File

@ -30,7 +30,8 @@ LichessEditor(document.getElementById('board_editor'), {
trans.whitePlays,
trans.blackPlays,
trans.playWithTheMachine,
trans.playWithAFriend
trans.playWithAFriend,
trans.analysis
)))
});
}

View File

@ -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) {
<div class="analyse cg-512">@miniBoardContent</div>
@pov.map { p =>
<div class="back_to_game">
<a class="button" href="@routes.Round.watcher(p.gameId, p.color.name)">
<span data-icon="i" class="text">@trans.backToGame()</span>
</a>
</div>
}
}

View File

@ -1,3 +1,3 @@
@(g: lila.game.Game)
@gameFenNoCtx(g, g.firstPlayer.color, tv = true)
@game.vstext(g)
@game.vstext(g)(none)

View File

@ -2,13 +2,13 @@
@game.layout(
title = trans.gamesBeingPlayedRightNow.str(),
menu = sideMenu(listMenu, "realtime").some) {
<div style="padding-bottom: 0" class="content_box current_games_box">
<div class="game_list realtime clearfix">
menu = sideMenu(listMenu, "playing").some) {
<div style="padding-bottom: 0" class="content_box">
<div class="game_list playing">
@games.map { g =>
<div>
@gameFen(g, g.firstPlayer.color)
@game.vstext(g)
@game.vstext(g)(ctx.some)
</div>
}
</div>

View File

@ -1,6 +1,6 @@
@(listMenu: lila.game.ListMenu, active: String)(implicit ctx: Context)
<a class="@active.active("realtime")" href="@routes.Game.realtime()">
<a class="@active.active("playing")" href="@routes.Game.playing()">
@trans.gamesBeingPlayedRightNow()
</a>
<a class="@active.active("search")" href="@routes.Game.search()">
@ -12,6 +12,9 @@
<a class="@active.active("edit")" href="@routes.Editor.index">
@trans.boardEditor()
</a>
<a class="@active.active("userAnalysis")" href="@routes.UserAnalysis.index">
@trans.analysis()
</a>
<a class="@active.active("all")" href="@routes.Game.all()">
@trans.viewAllNbGames(listMenu.nbGames.localize)
</a>

View File

@ -1,4 +1,4 @@
@(g: Game)
@(g: Game)(ctxOption: Option[Context])
<div class="vstext clearfix">
<div class="left">
@playerUsername(g.firstPlayer, withRating = false, withTitle = false)
@ -14,5 +14,13 @@
</div>
@g.clock.map { c =>
<div class="center"><span data-icon="p">&nbsp;@shortClockName(c)</span></div>
}.getOrElse {
@ctxOption.map { ctx =>
@g.daysPerTurn.map { days =>
<div class="center">
<span data-hint="@trans.correspondence()(ctx)" class="hint--top">@{(days == 1).fold(trans.oneDay()(ctx), trans.nbDays(days)(ctx))}</span>
</div>
}
}
}
</div>

View File

@ -23,7 +23,7 @@
}
@games.map { g =>
<div class="game_row paginated_element clearfix">
<div class="game_row paginated_element">
@defining(user flatMap g.player) { fromPlayer =>
@defining(fromPlayer | g.firstPlayer ) { firstPlayer =>
@gameFen(g, firstPlayer.color, ownerLink, withTitle = false)

View File

@ -4,7 +4,7 @@
<div id="featured_game">
@featured.map { g =>
@gameFen(g, g.firstPlayer.color, tv = true)
@game.vstext(g)
@game.vstext(g)(ctx.some)
}
</div>
}

View File

@ -2,7 +2,7 @@
@povs.take(9).map { pov =>
<a href="@routes.Round.player(pov.fullId)" class="@if(pov.isMyTurn){my_turn}">
@gameFen(pov.game, pov.color, withLink = false, withTitle = false)
@gameFen(pov.game, pov.color, withLink = false, withTitle = false, withLive = false)
<span class="meta">
@playerText(pov.opponent, withRating = false)
<span class="indicator">

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,15 @@
@(playing: List[Pov], nextId: Option[String])(implicit ctx: Context)
@nextId.map { id =>
<input type="hidden" class="next_id" value="@id" />
}
<h3>
<button class="move_on button hint--bottom" data-hint="@trans.automaticallyProceedToNextGameAfterMoving()">
<i data-icon="E" class="is-green"></i><span>@trans.autoSwitch()</span>
</button>
@trans.gamesBeingPlayedRightNow()
</h3>
@defining(playing.partition(_.isMyTurn)) {
case (myTurn, otherTurn) => {
@lobby.playing(myTurn ++ otherTurn.take(6 - myTurn.size))
}
}

View File

@ -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) {
<div class="round cg-512">@miniBoardContent</div>
<div class="crosstable" style="display:none">
@cross.map { c =>
@views.html.game.crosstable(ctx.userId.fold(c)(c.fromPov))
}
</div>
@if(otherGames.nonEmpty) {
<div id="now_playing" class="other_games">
<h3>Other correspondence games</h3>
@lobby.playing(otherGames take 6)
@if(playing.nonEmpty) {
<div id="now_playing" class="clearfix other_games" data-reload-url="@routes.Round.others(pov.gameId)">
@others(playing, none)
</div>
}
}

View File

@ -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) {
<div class="round cg-512">@miniBoardContent</div>
<div class="crosstable" style="display:none">
@cross.map { c =>

View File

@ -1,10 +1,10 @@
@(games: List[Game])(implicit ctx: Context)
<div class="game_list realtime">
<div class="game_list playing">
@games.map { g =>
<div>
@gameFen(g, g.firstPlayer.color)
@game.vstext(g)
@game.vstext(g)(ctx.some)
</div>
}
</div>

View File

@ -18,7 +18,7 @@
data-stream-url="@routes.Tv.streamOut">
<div id="featured_game" title="lichess.org TV">
@gameFenNoCtx(g, g.firstPlayer.color, tv = true, blank = true)
@game.vstext(g)
@game.vstext(g)(none)
</div>
<script src="http://code.jquery.com/jquery-2.1.0.min.js"></script>
@jsTag("vendor/chessground.min.js")

View File

@ -21,7 +21,8 @@ side = side(pov, games, streams),
underchat = game.watchers().some,
active = siteMenu.tv.some,
moreJs = moreJs,
moreCss = cssTag("tv.css")) {
moreCss = cssTag("tv.css"),
chessground = false) {
<div class="round cg-512">
@miniBoardContent
</div>

View File

@ -1,8 +1,16 @@
@(u: User, gs: Paginator[Game], filterName: String)(implicit ctx: Context)
<div class="games infinitescroll">
<div class="games infinitescroll @if(filterName == "playing" && gs.nbResults > 2) {game_list playing center}">
@gs.nextPage.map { np =>
<div class="pager none"><a href="@routes.User.showFilter(u.username, filterName, np)">Next</a></div>
}
@if(filterName == "playing" && gs.nbResults > 2) {
@gs.currentPageResults.flatMap{ Pov(_, u) }.map { p =>
<div class="paginated_element">
@gameFen(p.game, p.color)
@game.vstext(p.game)(ctx.some)
</div>
}
} else {
@game.widgets(gs.currentPageResults, user = u.some, ownerLink = ctx is u)
}
</div>

View File

@ -1,4 +1,4 @@
@(u: User, playing: Option[Game], blocked: Boolean, followable: Boolean, rel: Option[lila.relation.Relation])(implicit ctx: Context)
@(u: User, playing: Option[Pov], blocked: Boolean, followable: Boolean, rel: Option[lila.relation.Relation])(implicit ctx: Context)
<div class="title">
<div>
@u.profileOrDefault.countryInfo.map {
@ -22,8 +22,8 @@
</div>
}
</div>
@playing.map { g =>
@gameFen(g, g.player(u).getOrElse(g.firstPlayer).color)
@playing.map { pov =>
@gameFen(pov.game, pov.color)
}
@ctx.userId.map { myId =>
@if(myId != u.id) {

View File

@ -1,8 +1,6 @@
@(u: User, sugs: List[lila.relation.Related])(implicit ctx: Context)
@title = @{ "%s - %s".format(u.username, trans.favoriteOpponents()) }
@user.layout(title = title) {
@user.layout(title = "%s - %s".format(u.username, trans.favoriteOpponents())) {
<div class="content_box no_padding">
<h1>@userLink(u, withOnline = false) @trans.favoriteOpponents()</h1>
@user.relatedTable(u, sugs)

View File

@ -11,7 +11,7 @@ for app in editor puzzle round analyse; do
cd -
done
for file in strongSocket.js tv.js common.js big.js chart2.js user.js coordinate.js; do
for file in socket.js tv.js common.js big.js chart2.js user.js coordinate.js; do
orig=public/javascripts/$file
comp=public/compiled/$file
if [[ ! -f $comp || $orig -nt $comp ]]; then

View File

@ -169,6 +169,7 @@ tournaments=Tournaments
tournamentPoints=Tournament points
viewTournament=View tournament
backToTournament=Return to tournament
backToGame=Return to game
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Free online Chess game. Play Chess now in a clean interface. No registration, no ads, no plugin required. Play Chess with the computer, friends or random opponents.
teams=Teams
nbMembers=%s members
@ -305,3 +306,5 @@ thisPuzzleIsCorrect=This puzzle is correct and interesting
thisPuzzleIsWrong=This puzzle is wrong or boring
youHaveNbSecondsToMakeYourFirstMove=You have %s seconds to make your first move!
nbGamesInPlay=%s games in play
automaticallyProceedToNextGameAfterMoving=Automatically proceed to next game after moving
autoSwitch=Auto switch

View File

@ -47,9 +47,12 @@ computerAnalysisInProgress=Analise de computador em progresso
theComputerAnalysisHasFailed=Die rekenaar analise het misluk
viewTheComputerAnalysis=Sien die rekenaar analise
requestAComputerAnalysis=Versoek 'n rekenaar analise
computerAnalysis=Analisi del computer
analysis=Analisi
blunders=Flaters
mistakes=Foute
inaccuracies=Onakurate
moveTimes=Tempo mossa
flipBoard=Draai bord
threefoldRepetition=Herhaal drie keer
claimADraw=Kies gelykop

View File

@ -81,6 +81,7 @@ players=لاعبي الشطرنج
minutesPerSide=دقائق لكل طرف
variant=النوع
timeControl=التحكم بالوقت
realTime=الوقت الفعلي
correspondence=طويل الأمد
daysPerTurn=يوم لكل نقلة
oneDay=يوم واحد

View File

@ -81,6 +81,7 @@ players=Играчи
minutesPerSide=Минути за страна
variant=Вариант
timeControl=Контрол на времето
realTime=Засечено време
correspondence=Кореспонденция
daysPerTurn=Дни за ход
oneDay=Един ден
@ -92,6 +93,11 @@ password=Парола
haveAnAccount=Имате акаунт?
allYouNeedIsAUsernameAndAPassword=Всичко, което ви е необходимо е потребителско име и парола
changePassword=Смяна на парола
changeEmail=Смяна на имейла
email=Електронна Поща
emailIsOptional=Електронната поща не е задължителна. Lichess ще използва вашия адрес, за да възстанови паролата ви, ако я забравите
passwordReset=Паролата е нулирана
forgotPassword=Забравихте ли си паролаата ?
learnMoreAboutLichess=Научи повече за Lichess
rank=Ранг
gamesPlayed=Играни игри
@ -299,3 +305,5 @@ thisPuzzleIsCorrect=Тази задача е правилна и интерес
thisPuzzleIsWrong=Тази задача е грешна или скучна
youHaveNbSecondsToMakeYourFirstMove=Имате %s секунди да направите първия си ход!
nbGamesInPlay=%s игри в процес
automaticallyProceedToNextGameAfterMoving=Автоматично превключи на следващата игра след ход
autoSwitch=Автоматично превключи

View File

@ -81,6 +81,7 @@ players=Igrači
minutesPerSide=Minuta po igraču
variant=Varijanta
timeControl=Vremenska kontrola
realTime=Stvarno Vrijeme
correspondence=Dopisni sah
daysPerTurn=Dana po potezu
oneDay=Jedan dan
@ -92,6 +93,11 @@ password=Lozinka
haveAnAccount=Već imate račun?
allYouNeedIsAUsernameAndAPassword=Sve što Vam treba je korisničko ime i lozinka.
changePassword=Promijeni lozinku
changeEmail=Promijeni e-Mail
email=e-Mail
emailIsOptional=e-Mail je neoavezan. Lichess ce je koristiti da resetuje vasu Lozinku ako je zaboravite.
passwordReset=resetuj Lozinku
forgotPassword=Zaboravio si Lozinku ?
learnMoreAboutLichess=Naučite više o Lichessu
rank=Poredak
gamesPlayed=Broj odigranih partija

View File

@ -81,6 +81,7 @@ players=Jugadors d'escacs
minutesPerSide=Minuts per jugador
variant=Variant
timeControl=Control de temps
realTime=Temps real
correspondence=Correspondència
daysPerTurn=Dies per torn
oneDay=Un dia
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Aquest trencaclosques és correcte i interessant
thisPuzzleIsWrong=Aquest trencaclosques està malament o és avorrit
youHaveNbSecondsToMakeYourFirstMove=Disposes de %s segons per realitzar el teu primer moviment!
nbGamesInPlay=%s partides en joc
automaticallyProceedToNextGameAfterMoving=Anar automàticament a la següent partida després de cada moviment
autoSwitch=Canvi automàtic

View File

@ -81,6 +81,7 @@ players=Hráči
minutesPerSide=Minut pro každého hráče
variant=Varianta
timeControl=Tempo hry
realTime=Skutečný čas
correspondence=Korespondenčně
daysPerTurn=Dnů na tah
oneDay=Jeden den
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Tato úloha je správná a zajímavá
thisPuzzleIsWrong=Tato úloha je špatná nebo nudná
youHaveNbSecondsToMakeYourFirstMove=Máte %s sekund na provedení prvního tahu!
nbGamesInPlay=%s rozehraných her
automaticallyProceedToNextGameAfterMoving=Automaticky přejdi k další hře po tahu
autoSwitch=Přepni automaticky

View File

@ -81,6 +81,7 @@ players=Skakspillere
minutesPerSide=Minutter per spiller
variant=Variant
timeControl=Tidskontrol
realTime=Real time
correspondence=Korrespondance
daysPerTurn=Dage per træk
oneDay=En dag
@ -92,7 +93,10 @@ password=Password
haveAnAccount=Har du en konto?
allYouNeedIsAUsernameAndAPassword=Det eneste du skal bruge, er et brugernavn og en adgangskode.
changePassword=Skift kodeord
changeEmail=Ændr email
email=Email
emailIsOptional=Email er valgfri. Lichess vil bruge din email til at sende et nyt pasord, hvis du glemmer det eksisterende.
passwordReset=Reset pasord
forgotPassword=Glemt adgangskode?
learnMoreAboutLichess=Få mere at vide om Lichess
rank=Rang

View File

@ -86,7 +86,7 @@ correspondence=Fernschach
daysPerTurn=Tage pro Zug
oneDay=ein Tag
nbDays=%s Tage
nbHours=%s stunden
nbHours=%s Stunden
time=Zeit
username=Benutzername
password=Passwort
@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Dieses Rätsel ist korrekt und interessant.
thisPuzzleIsWrong=Dieses Rätsel ist falsch oder langweilig.
youHaveNbSecondsToMakeYourFirstMove=Du hast %s Sekunden, um deinen ersten Zug zu machen!
nbGamesInPlay=%s laufende Spiele
automaticallyProceedToNextGameAfterMoving=Nach dem Zug automatisch zur nächsten Partie gehen
autoSwitch=Automatischer Wechsel

View File

@ -81,6 +81,7 @@ players=Σκακιστές
minutesPerSide=Λεπτά ανά πλευρά
variant=Εκδοχή
timeControl=Χρονόμετρο
realTime=Πραγματικού χρόνου
correspondence=Αλληλογραφία
daysPerTurn=Μέρες ανά σειρά
oneDay=Μία μέρα

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Este puzzle es correcto e interesante
thisPuzzleIsWrong=Este puzzle es erróneo o aburrido
youHaveNbSecondsToMakeYourFirstMove=¡Dispone de %s segundos para hacer su primer movimiento!
nbGamesInPlay=%s partidas en juego
automaticallyProceedToNextGameAfterMoving=Continuar automáticamente al siguiente juego al mover
autoSwitch=Cambio automático

View File

@ -81,6 +81,7 @@ players=بازیکنان شطرنج
minutesPerSide=دقیقه در هر طرف
variant=شاخه
timeControl=کنترل زمان
realTime=بلادرنگ
correspondence=مکاتبه ای
daysPerTurn=روز برای هر حرکت
oneDay=یک روز
@ -92,6 +93,11 @@ password=رمزعبور
haveAnAccount=شما صاحب حساب کاربری می باشید؟
allYouNeedIsAUsernameAndAPassword=تمام چیزی که شما نیاز دارید نام کاربری و رمزعبور می باشد
changePassword=تغییر کلمه عبور
changeEmail=تغییر ایمیل
email=ایمیل
emailIsOptional=.ایمیل ضروری نیست. زمانیکه پسورد خود را فراموش کنید لایچس از این ایمیل استفاده میکند تا شما پسورد جدیدی دریافت کنید
passwordReset=باز نشانی پسورد
forgotPassword=آیا پسورد را فراموش کرده اید؟
learnMoreAboutLichess=Lichess بیشتر بدانید درباره
rank=رتبه
gamesPlayed=بازی های انجام شده
@ -299,3 +305,5 @@ thisPuzzleIsCorrect=این جدول صحیح و جالب است
thisPuzzleIsWrong=این جدول اشتباه یا خسته کننده است
youHaveNbSecondsToMakeYourFirstMove=! ثانیه فرصت دارید %s شما برای انجام اولین حرکت خود فقط
nbGamesInPlay=بازی در حال انجام است %s
automaticallyProceedToNextGameAfterMoving=حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن
autoSwitch=تعویض خودکار

View File

@ -81,7 +81,7 @@ players=Pelaajat
minutesPerSide=Minuuttia per puoli
variant=Variantti
timeControl=Ajan hallinta
realTime=Oikea aika
realTime=Reaaliaikainen
correspondence=Kirjeshakki
daysPerTurn=Päivää per vuoro
oneDay=Yksi päivä
@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Tämä tehtävä toimii ja on mielenkiintoinen
thisPuzzleIsWrong=Tämä tehtävä on tylsä tai virheellinen
youHaveNbSecondsToMakeYourFirstMove=Sinulla on %s sekuntia aikaa ensimmäisen siirtosi tekemiseen
nbGamesInPlay=%s peliä meneillään
automaticallyProceedToNextGameAfterMoving=Siirry automaattisesti seuraavaan peliin siirron jälkeen
autoSwitch=Automaattinen siirtyminen

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Ce problème est correct et intéressant
thisPuzzleIsWrong=Ce problème est erroné ou ennuyeux
youHaveNbSecondsToMakeYourFirstMove=Vous avez %s secondes pour jouer votre premier coup !
nbGamesInPlay=%s parties en cours
automaticallyProceedToNextGameAfterMoving=Aller automatiquement à la prochaine partie après coup
autoSwitch=Changement automatique

View File

@ -5,3 +5,14 @@ gameOver=રમત પુરી
waitingForOpponent=વિરોધિ માટે રાહ જુએ છે
waiting=રાહ જુએ છે
yourTurn=તમરો વારો
level=પાળવ
chat=વાત ચીત
resign=રાજીનામું
white=સફેદ
black=કાળુ
createAGame=રમત બનાવો
whiteIsVictorious=સફેદ વિજય રહ્યો
blackIsVictorious=કાળો વિજય રહ્યો
playWithTheSameOpponentAgain=આજ પ્રાતીસ્પર્ધી સાથે ફરી રમો
newOpponent=નવો પ્રાતીસ્પર્ધી
joinTheGame=રમત માં જોડવ

View File

@ -81,6 +81,7 @@ players=שחקנים
minutesPerSide=דקות עבור כל צד
variant=סוג משחק
timeControl=כמות זמן
realTime=זמן אמת
correspondence=התכתבות
daysPerTurn=ימים לצעד
oneDay=יום אחד

View File

@ -81,6 +81,7 @@ players=Igrači
minutesPerSide=Minuta po igraču
variant=Varijanta
timeControl=Vremenska kontrola
realTime=Stvarno vrijeme
correspondence=Korespodencija
daysPerTurn=Dana po potezu
oneDay=Jedan dan
@ -92,6 +93,11 @@ password=Zaporka
haveAnAccount=Imate li otvoren račun?
allYouNeedIsAUsernameAndAPassword=Sve što trebate je samo korisničko ime i zaporka.
changePassword=Promijeni zaporku
changeEmail=Promijeni email
email=Email
emailIsOptional=Email je neobavezan. Lichess će koristiti vaš mail kako bi vam povratio šifru ako ju zaboravite.
passwordReset=Resetirajte lozinku
forgotPassword=Zaboravili ste lozinku?
learnMoreAboutLichess=Nauči više o Lichessu
rank=Rang
gamesPlayed=Broj odigranih igara
@ -299,3 +305,5 @@ thisPuzzleIsCorrect=Ovaj problem je točan i zanimljiv
thisPuzzleIsWrong=Ovaj problem je pogresan i dosadan
youHaveNbSecondsToMakeYourFirstMove=Imate %s sekunti da napravite svoj prvi potez!
nbGamesInPlay=%s partije koje se upravo igraju
automaticallyProceedToNextGameAfterMoving=Automatski prebaci na sljedeću partiju nakon odigranog poteza
autoSwitch=Prebaci automatski

View File

@ -81,6 +81,7 @@ players=Játékosok
minutesPerSide=Perc játékosonként
variant=Változat
timeControl=Játék időre
realTime=Valós Idő
correspondence=Levelezés
daysPerTurn=Lépésenkénti napok száma
oneDay=Egy nap
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ez a feladvány helyes és érdekes
thisPuzzleIsWrong=Ez a feladvány hibás vagy unalmas
youHaveNbSecondsToMakeYourFirstMove=%s másodperced van hogy megtedd az első lépést!
nbGamesInPlay=%s meccset játszanak éppen az oldalon
automaticallyProceedToNextGameAfterMoving=Automatikusan menjen a következő játékhoz amiután lépett
autoSwitch=Automatikus váltás

View File

@ -81,6 +81,7 @@ players=Pemain catur
minutesPerSide=Menit untuk tiap sisi
variant=Variasi
timeControl=Kontrol waktu
realTime=Langsung
correspondence=Korespondensi
daysPerTurn=Hari per langkah
oneDay=Satu hari
@ -92,6 +93,11 @@ password=Kata kunci
haveAnAccount=Punya akun?
allYouNeedIsAUsernameAndAPassword=Semua yang anda butuhkan adalah nama pengguna dan kata kunci.
changePassword=Ganti kata kunci
changeEmail=Ubah email
email=Email
emailIsOptional=Email tidak diwajibkan. Lichess akan menggunakan email anda untuk me-reset password anda bila anda lupa.
passwordReset=Reset password
forgotPassword=Lupa password?
learnMoreAboutLichess=Pelajari lebih lanjut tentang Lichess
rank=Pangkat
gamesPlayed=Permainan yang telah dimainkan

View File

@ -81,6 +81,7 @@ players=Skákmenn
minutesPerSide=Mínútur á lið
variant=Afbrigði
timeControl=Tímaskorður
realTime=Rauntími
daysPerTurn=Dagar á leik
oneDay=Einn dagur
nbDays=%s dagar
@ -303,3 +304,5 @@ thisPuzzleIsCorrect=Þessi þraut er rétt og áhugaverð
thisPuzzleIsWrong=Þessi þraut er röng eða leiðinleg
youHaveNbSecondsToMakeYourFirstMove=Þú hefur %s sekúndur til þess að leika fyrsta leik!
nbGamesInPlay=%s leikir í gangi
automaticallyProceedToNextGameAfterMoving=Skipta sjálfkrafa um skák eftir leik
autoSwitch=Skipta sjálfkrafa

View File

@ -70,9 +70,9 @@ viewInFullSize=Visualizza a schermo intero
logOut=Esci
signIn=Entra
newToLichess=Nuovo su Lichess?
youNeedAnAccountToDoThat=Ti servei un account per farlo
youNeedAnAccountToDoThat=Ti serve un account per farlo
signUp=Registrati
computersAreNotAllowedToPlay=Non è permesso giocare a computer o a giocatori che si fanno aiutare dai computer. Mentre giochi, non farti aiutare da programmi di scacchi, da database o da altre persone.
computersAreNotAllowedToPlay=Non è permesso giocare a computer o a giocatori che si fanno aiutare dai computer. Mentre giochi, non farti aiutare da programmi di scacchi, da database o da altre persone. Inoltre si sconsiglia vivamente di creare account multipli, pena la cancellazione dal sito.
games=Partite
forum=Forum
xPostedInForumY=%s ha postato nel forum %s
@ -81,7 +81,7 @@ players=Giocatori
minutesPerSide=Minuti per lato
variant=Variante
timeControl=Controllo del tempo
realTime=Diretta
realTime=Partita a tempo
correspondence=Corrispondenza
daysPerTurn=Giorni per turno
oneDay=Un giorno
@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Questo esercizio è corretto e interessante
thisPuzzleIsWrong=Questo esercizio è sbagliato o noioso
youHaveNbSecondsToMakeYourFirstMove=Hai %s secondi per fare la tua prima mossa
nbGamesInPlay=%s partite in gioco
automaticallyProceedToNextGameAfterMoving=Passa automaticamente alla partita successiva dopo aver mosso
autoSwitch=Passaggio automatico

View File

@ -81,8 +81,10 @@ players=プレイヤー
minutesPerSide=持ち時間
variant=バリアント
timeControl=持ち時間制限
realTime=実時間
correspondence=通信チェス
daysPerTurn=制限日数
oneDay=一日
nbDays=%s日
nbHours=%s時間
time=時間

View File

@ -81,6 +81,7 @@ players=플레이어
minutesPerSide=진영 당 주어진 시간(분)
variant=모드
timeControl=시간 제한
realTime=시간 제한
correspondence=긴 체스
daysPerTurn=한 턴에 걸리는 날짜
oneDay=하루
@ -92,6 +93,11 @@ password=비밀번호
haveAnAccount=계정이 있습니까?
allYouNeedIsAUsernameAndAPassword=사용자 이름과 비밀번호만 입력하시면 됩니다
changePassword=비밀번호 변경
changeEmail=메일 주소 변경
email=메일
emailIsOptional=메일 주소는 선택 항목입니다. 비밀번호를 잊어버렸을 때 초기화하기 위해 메일 주소를 사용합니다.
passwordReset=비밀번호 초기화
forgotPassword=비밀번호를 잊어버리셨나요?
learnMoreAboutLichess=Lichess에 대해 좀 더 알아보기
rank=순위
gamesPlayed=게임
@ -299,3 +305,5 @@ thisPuzzleIsCorrect=이 퍼즐은 정확하고 재밌습니다
thisPuzzleIsWrong=이 퍼즐은 오류가 있거나 지루합니다
youHaveNbSecondsToMakeYourFirstMove=%s초 안에 첫 수를 놓으세요!
nbGamesInPlay=%s개의 게임 플레이 중
automaticallyProceedToNextGameAfterMoving=수를 둔 다음에 자동으로 다음 게임으로 이동
autoSwitch=자동 전환

View File

@ -81,6 +81,7 @@ players=Sjakkspillere
minutesPerSide=Minutter per side
variant=Variant
timeControl=Tidskontroll
realTime=Sanntid
correspondence=Fjernsjakk
daysPerTurn=Dager per trekk
oneDay=Én dag

View File

@ -81,7 +81,8 @@ players=Geregistreerde spelers en spelers die online zijn
minutesPerSide=Minuten per speler
variant=Variant
timeControl=Speelduur
correspondence=Correspondensie
realTime=Live
correspondence=Correspondentie
daysPerTurn=Dagen per zet
oneDay=Eén dag
nbDays=%s dagen
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=De puzzel is correct en interessant
thisPuzzleIsWrong=De puzzel is fout of saai
youHaveNbSecondsToMakeYourFirstMove=Je hebt %s seconden om je eerste zet te doen!
nbGamesInPlay=%s partijen bezig
automaticallyProceedToNextGameAfterMoving=Automatische doorgaan naar de volgende partij na uw zet
autoSwitch=Automatische switch

View File

@ -81,6 +81,7 @@ players=Sjakkspelarar
minutesPerSide=Minutt per side
variant=Variant
timeControl=Tidskontroll
realTime=Sanntid
correspondence=Fjernsjakk
daysPerTurn=Dagar per trekk
oneDay=Ein dag

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=To zadanie jest poprawne i ciekawe
thisPuzzleIsWrong=To zadanie jest niepoprawne lub nudne
youHaveNbSecondsToMakeYourFirstMove=Masz %s sekund na pierwszy ruch!
nbGamesInPlay=%s gier w trakcie
automaticallyProceedToNextGameAfterMoving=Automatycznie przejdź do następnej gry po wykonaniu ruchu
autoSwitch=Auto przełączanie

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=O quebra-cabeças está correcto e é interessante
thisPuzzleIsWrong=O quebra-cabeças está errado ou é chato
youHaveNbSecondsToMakeYourFirstMove=Tens %s segundos para fazeres a primeira jogada!
nbGamesInPlay=%s partidas em andamento
automaticallyProceedToNextGameAfterMoving=Automaticamente passa ao jogo seguinte após seu lance
autoSwitch=Auto ciclo

View File

@ -81,7 +81,7 @@ players=Игроки
minutesPerSide=Минут на партию
variant=Вариант
timeControl=Контроль времени
realTime=Реальное время
realTime=Время на ход
correspondence=По переписке
daysPerTurn=Дней на ход
oneDay=Ежедневно
@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Задача правильная и интересная
thisPuzzleIsWrong=Задача неправильная или неинтересная
youHaveNbSecondsToMakeYourFirstMove=У Вас %s секунд, чтобы сделать свой первый ход!
nbGamesInPlay=%s партий играются
automaticallyProceedToNextGameAfterMoving=Автоматически переходить к следующей игре после хода
autoSwitch=Автосмена

View File

@ -81,6 +81,7 @@ players=Hráči
minutesPerSide=Minút na stranu
variant=Varianta
timeControl=Nastavenie času
realTime=Skutočný čas
correspondence=Korešpondenčný šach
daysPerTurn=Počet dní na ťah
oneDay=Jeden deň
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Tento rébus je správny a pútavý
thisPuzzleIsWrong=Tento rébus je nesprávny alebo nudný
youHaveNbSecondsToMakeYourFirstMove=%s sekúnd na vykonanie prvého ťahu!
nbGamesInPlay=%s hier sa hrá
automaticallyProceedToNextGameAfterMoving=Automaticky prejdi k ďalšej hre po ťahu
autoSwitch=Prepni automaticky

View File

@ -49,7 +49,7 @@ viewTheComputerAnalysis=Poglej računalniško analizo
requestAComputerAnalysis=Zahtevaj računalniško analizo
computerAnalysis=Računalniška analiza
analysis=Analiza
blunders=Hude napake
blunders=Grobe napake
mistakes=Napake
inaccuracies=Nenatančnosti
moveTimes=Čas za potezo
@ -61,7 +61,7 @@ draw=Remi
nbConnectedPlayers=%s igralcev
gamesBeingPlayedRightNow=Število trenutnih iger
viewAllNbGames=%s Iger
viewNbCheckmates=Oglej si mat pozicije %s
viewNbCheckmates=%s mat pozicij
nbBookmarks=%s zaznamkov
nbPopularGames=%s priljubljenih partij
nbAnalysedGames=%s analiziranih Iger
@ -81,6 +81,7 @@ players=Igralci
minutesPerSide=Minut na igralca
variant=Različica
timeControl=Ura
realTime=Standardno
correspondence=Korespondenčno
daysPerTurn=Število dni na potezo
oneDay=En dan
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ta problem je pravilen in zanimiv
thisPuzzleIsWrong=Ta problem je napačen oz. dolgočasen
youHaveNbSecondsToMakeYourFirstMove=Imaš še %s sekund za prvo potezo
nbGamesInPlay=%s igranih partij
automaticallyProceedToNextGameAfterMoving=Po vsaki potezi samodejno preklopi na naslednjo partijo
autoSwitch=Samodejno

View File

@ -81,6 +81,7 @@ players=Lojtarë
minutesPerSide=Minuta për palë
variant=Varianti
timeControl=Kontolla e kohës
realTime=Kha reale
correspondence=korespondenca
daysPerTurn=turne për ditë
oneDay=një ditë
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ky mister është korekt dhe interesant
thisPuzzleIsWrong=Kjo levizje është e gabuar ose e bezëdisëshme
youHaveNbSecondsToMakeYourFirstMove=Ju keni%s sekonda për të bërë lëvizje tuaj të parë
nbGamesInPlay=%s lojëra duke u luajtur
automaticallyProceedToNextGameAfterMoving=Procesimi Automatik i lojës ,më pas duke luajtur potezin
autoSwitch=Mbyllje automatike

View File

@ -81,6 +81,7 @@ players=Schackspelare
minutesPerSide=Minuter per spelare
variant=Variant
timeControl=Tidskontroll
realTime=Realtid
correspondence=Korrespondens
daysPerTurn=Dagar per speltur
oneDay=En dag
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Det här schackproblemet är korrekt och intressant
thisPuzzleIsWrong=Det här schackproblemet är fel eller tråkigt
youHaveNbSecondsToMakeYourFirstMove=du har %s sekunder för att göra första draget
nbGamesInPlay=%s partier spelas
automaticallyProceedToNextGameAfterMoving=Gör att du automatiskt fortsätter till nästa parti efter du gjort ett drag
autoSwitch=Automatiskt byte

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=ปริศนานี้ถูกต้อง และ
thisPuzzleIsWrong=ปริศนานี้ไม่ถูกต้อง หรือน่าเบื่อ
youHaveNbSecondsToMakeYourFirstMove=คุณมีเวลา %s วินาที ในการเริ่มเดิน!
nbGamesInPlay=%s เกมที่กำลังดำเนิน
automaticallyProceedToNextGameAfterMoving=ดำเนินสู่เกมถัดไปอัตโนมัติหลังจากการเดิน
autoSwitch=สลับอัตโนมัติ

View File

@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Bu bulmaca doğru ve ilginç
thisPuzzleIsWrong=Bu bulmaca yanlış veya sıkıcı
youHaveNbSecondsToMakeYourFirstMove=İlk hamleni yapman için %s saniyen kaldı!
nbGamesInPlay=Şu anda %s oyun oynanıyor
automaticallyProceedToNextGameAfterMoving=Hamleden sonra otomatik olarak diğer oyuna devam edecek
autoSwitch=Otomatik Seç

View File

@ -81,6 +81,7 @@ players=Гравці
minutesPerSide=Хвилин на кожного
variant=Варіант
timeControl=Контроль часу
realTime=Швидкі шахи
correspondence=Переписка
daysPerTurn=Днів на хід
oneDay=Один день
@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ця задача є правильною та цікаво
thisPuzzleIsWrong=Ця задача є неправильною чи нудною
youHaveNbSecondsToMakeYourFirstMove=Ви маєте %s секунд, щоб зробити перший хід!
nbGamesInPlay=%s ігор тривають
automaticallyProceedToNextGameAfterMoving=Автоматично перейти до наступної гри після ходу
autoSwitch=Авт. перехід

View File

@ -60,7 +60,7 @@ GET /blog/:id/:slug controllers.Blog.show(id: String, slug: String, ref: O
GET /blog.atom controllers.Blog.atom(ref: Option[String] ?= None)
# Game
GET /games controllers.Game.realtime
GET /games controllers.Game.playing
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
GET /games/bookmark controllers.Game.bookmark(page: Int ?= 1)
@ -89,6 +89,10 @@ GET /training/:id/load controllers.Puzzle.load(id: Int)
POST /training/:id/attempt controllers.Puzzle.attempt(id: Int)
POST /training/:id/vote controllers.Puzzle.vote(id: Int)
# User Analysis
GET /analysis/*urlFen controllers.UserAnalysis.load(urlFen: String)
GET /analysis controllers.UserAnalysis.index
# Round
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")
GET /$gameId<\w{8}>/$color<white|black> controllers.Round.watcher(gameId: String, color: String)
@ -97,9 +101,12 @@ GET /$gameId<\w{8}>/$color<white|black>/socket controllers.Round.websocketWatc
GET /$fullId<\w{12}>/socket/v:apiVersion controllers.Round.websocketPlayer(fullId: String, apiVersion: Int)
GET /$gameId<\w{8}>/$color<white|black>/side controllers.Round.sideWatcher(gameId: String, color: String)
GET /$fullId<\w{12}>/side controllers.Round.sidePlayer(fullId: String)
GET /$gameId<\w{8}>/others controllers.Round.others(gameId: String)
GET /$gameId<\w{8}>/next controllers.Round.next(gameId: String)
GET /$gameId<\w{8}>/continue/:mode controllers.Round.continue(gameId: String, mode: String)
POST /$gameId<\w{8}>/note controllers.Round.writeNote(gameId: String)
GET /$gameId<\w{8}>/edit controllers.Editor.game(gameId: String)
GET /$gameId<\w{8}>/$color<white|black>/analysis controllers.UserAnalysis.game(gameId: String, color: String)
# Round accessibility: text representation
GET /$fullId<\w{12}>/text controllers.Round.playerText(fullId: String)
@ -241,7 +248,7 @@ DELETE /notification/$id<\w{8}> controllers.Notification.remove(id)
GET /paste controllers.Importer.importGame
POST /import controllers.Importer.sendGame
# Progressive import API
# Progressive Import API
POST /api/import/live controllers.Importer.liveCreate
POST /api/import/live/$id<\w{8}>/:move controllers.Importer.liveMove(id: String, move: String)

View File

@ -41,11 +41,11 @@ private[api] final class UserApi(
def one(username: String, token: Option[String]): Fu[Option[JsObject]] = UserRepo named username flatMap {
case None => fuccess(none)
case Some(u) => GameRepo nowPlaying u.id zip
case Some(u) => GameRepo lastPlayed u zip
makeUrl(R User username) zip
(check(token) ?? (knownEnginesSharingIp(u.id) map (_.some))) flatMap {
case ((gameOption, userUrl), knownEngines) => gameOption ?? { g =>
makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) map (_.some)
makeUrl(R.Watcher(g.gameId, g.color.name)) map (_.some)
} map { gameUrlOption =>
jsonView(u, extended = true) ++ Json.obj(
"url" -> userUrl,

@ -1 +1 @@
Subproject commit 2ff1c84d1dd70531ab2fce7fc49691b67d539f33
Subproject commit afa9274da767775f9f228f4d06d2e58ddb0176f6

View File

@ -20,12 +20,12 @@ final class Paginator[A] private[paginator] (
/**
* Returns the previous page.
*/
def previousPage: Option[Int] = (currentPage != 1) option (currentPage - 1)
def previousPage: Option[Int] = (currentPage > 1) option (currentPage - 1)
/**
* Returns the next page.
*/
def nextPage: Option[Int] = (currentPage != nbPages) option (currentPage + 1)
def nextPage: Option[Int] = (currentPage < nbPages) option (currentPage + 1)
/**
* Returns the number of pages.

View File

@ -61,8 +61,25 @@ object BSON {
}
}
}
implicit def MapHandler[V](implicit vr: BSONReader[_ <: BSONValue, V], vw: BSONWriter[V, _ <: BSONValue]): BSONHandler[BSONDocument, Map[String, V]] = new BSONHandler[BSONDocument, Map[String, V]] {
private val reader = MapReader[V]
private val writer = MapWriter[V]
def read(bson: BSONDocument): Map[String, V] = reader read bson
def write(map: Map[String, V]): BSONDocument = writer write map
}
}
// List Handler
final class ListHandler[T](implicit reader: BSONReader[_ <: BSONValue, T], writer: BSONWriter[T, _ <: BSONValue]) extends BSONHandler[BSONArray, List[T]] {
def read(array: BSONArray) = array.stream.filter(_.isSuccess).map { v =>
reader.asInstanceOf[BSONReader[BSONValue, T]].read(v.get)
}.toList
def write(repr: List[T]) =
new BSONArray(repr.map(s => scala.util.Try(writer.write(s))).to[Stream])
}
implicit def bsonArrayToListHandler[T](implicit reader: BSONReader[_ <: BSONValue, T], writer: BSONWriter[T, _ <: BSONValue]): BSONHandler[BSONArray, List[T]] = new ListHandler
final class Reader(val doc: BSONDocument) {
val map = (doc.stream collect { case Success(e) => e }).toMap

View File

@ -164,11 +164,14 @@ object BinaryFormat {
}
def read(ba: ByteArray): PieceMap = {
def splitInts(int: Int) = Array(int >> 4, int & 0x0F)
def splitInts(b: Byte) = {
val int = b.toInt
Array(int >> 4, int & 0x0F)
}
def intPiece(int: Int): Option[Piece] =
intToRole(int & 7) map { role => Piece(Color((int & 8) == 0), role) }
val (aliveInts, deadInts) = ba.value map toInt flatMap splitInts splitAt 64
(Pos.all zip aliveInts flatMap {
val pieceInts = ba.value flatMap splitInts
(Pos.all zip pieceInts flatMap {
case (pos, int) => intPiece(int) map (pos -> _)
}).toMap
}

View File

@ -6,29 +6,51 @@ import org.joda.time.DateTime
import play.api.libs.json.JsObject
import lila.db.api.$count
import lila.memo.{ AsyncCache, ExpireSetMemo, Builder }
import lila.db.BSON._
import lila.memo.{ AsyncCache, MongoCache, ExpireSetMemo, Builder }
import lila.user.{ User, UidNb }
import tube.gameTube
import UidNb.UidNbBSONHandler
final class Cached(ttl: Duration) {
final class Cached(
mongoCache: MongoCache.Builder,
defaultTtl: FiniteDuration) {
def nbGames: Fu[Int] = count(Query.all)
def nbMates: Fu[Int] = count(Query.mate)
def nbImported: Fu[Int] = count(Query.imported)
def nbImportedBy(userId: String): Fu[Int] = count(Query imported userId)
def nbPlaying(userId: String): Fu[Int] = count(Query nowPlaying userId)
def nbPlaying(userId: String): Fu[Int] = countShortTtl(Query nowPlaying userId)
private implicit val userHandler = User.userBSONHandler
private val isPlayingSimulCache = AsyncCache[String, Boolean](
f = userId => GameRepo.countPlayingRealTime(userId) map (1 <),
timeToLive = 10.seconds)
val isPlayingSimul: String => Fu[Boolean] = isPlayingSimulCache.apply _
val rematch960 = new ExpireSetMemo(3.hours)
val activePlayerUidsDay = AsyncCache(
val activePlayerUidsDay = mongoCache[Int, List[UidNb]](
prefix = "player:active:day",
(nb: Int) => GameRepo.activePlayersSince(DateTime.now minusDays 1, nb),
timeToLive = 1 hour)
val activePlayerUidsWeek = AsyncCache(
val activePlayerUidsWeek = mongoCache[Int, List[UidNb]](
prefix = "player:active:week",
(nb: Int) => GameRepo.activePlayersSince(DateTime.now minusWeeks 1, nb),
timeToLive = 6 hours)
private val count = AsyncCache((o: JsObject) => $count(o), timeToLive = ttl)
private val countShortTtl = AsyncCache[JsObject, Int](
f = (o: JsObject) => $count(o),
timeToLive = 5.seconds)
private val count = mongoCache(
prefix = "game:count",
f = (o: JsObject) => $count(o),
timeToLive = defaultTtl)
object Divider {

View File

@ -9,6 +9,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
db: lila.db.Env,
mongoCache: lila.memo.MongoCache.Builder,
system: ActorSystem,
hub: lila.hub.Env,
getLightUser: String => Option[lila.common.LightUser],
@ -41,7 +42,9 @@ final class Env(
lazy val pngExport = PngExport(PngExecPath) _
lazy val cached = new Cached(ttl = CachedNbTtl)
lazy val cached = new Cached(
mongoCache = mongoCache,
defaultTtl = CachedNbTtl)
lazy val paginator = new PaginatorBuilder(
cached = cached,
@ -100,6 +103,7 @@ object Env {
lazy val current = "[boot] game" describes new Env(
config = lila.common.PlayApp loadConfig "game",
db = lila.db.Env.current,
mongoCache = lila.memo.Env.current.mongoCache,
system = lila.common.PlayApp.system,
hub = lila.hub.Env.current,
getLightUser = lila.user.Env.current.lightUser,

View File

@ -86,10 +86,11 @@ case class Game(
// in tenths
private def lastMoveTime: Option[Long] = castleLastMoveTime.lastMoveTime map {
_.toLong + (createdAt.getMillis / 100)
}
} orElse updatedAt.map(_.getMillis / 100)
private def lastMoveTimeDate: Option[DateTime] = castleLastMoveTime.lastMoveTime map { lmt =>
createdAt plusMillis (lmt * 100)
}
} orElse updatedAt
def lastMoveTimeInSeconds: Option[Int] = lastMoveTime.map(x => (x / 10).toInt)

View File

@ -15,7 +15,7 @@ import lila.db.api._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.ByteArray
import lila.db.Implicits._
import lila.user.User
import lila.user.{ User, UidNb }
object GameRepo {
@ -122,6 +122,18 @@ object GameRepo {
_ flatMap { Pov(_, user) } sortBy Pov.priority
}
// gets most urgent game to play
def onePlaying(user: User): Fu[Option[Pov]] = nowPlaying(user) map (_.headOption)
// gets last recently played move game
def lastPlayed(user: User): Fu[Option[Pov]] =
$find.one($query(Query recentlyPlayingWithClock user.id) sort Query.sortUpdatedNoIndex) map {
_ flatMap { Pov(_, user) }
}
def countPlayingRealTime(userId: String): Fu[Int] =
$count(Query.nowPlaying(userId) ++ Query.clock(true))
def setTv(id: ID) {
$update.fieldUnchecked(id, F.tvAt, $date(DateTime.now))
}
@ -179,15 +191,15 @@ object GameRepo {
_ sort Query.sortCreated skip (Random nextInt distribution)
)
def insertDenormalized(game: Game, ratedCheck: Boolean = true): Funit = {
val g2 = if (ratedCheck && game.rated && game.userIds.distinct.size != 2)
game.copy(mode = chess.Mode.Casual)
else game
val userIds = game.userIds.distinct
def insertDenormalized(g: Game, ratedCheck: Boolean = true): Funit = {
val g2 = if (ratedCheck && g.rated && g.userIds.distinct.size != 2)
g.copy(mode = chess.Mode.Casual)
else g
val userIds = g2.userIds.distinct
val bson = (gameTube.handler write g2) ++ BSONDocument(
F.initialFen -> g2.variant.exotic.option(Forsyth >> g2.toChess),
F.checkAt -> (!game.isPgnImport).option(DateTime.now.plusHours(game.hasClock.fold(1, 24))),
F.playingUids -> userIds.nonEmpty.option(userIds)
F.checkAt -> (!g2.isPgnImport).option(DateTime.now.plusHours(g2.hasClock.fold(1, 24))),
F.playingUids -> (g2.started && userIds.nonEmpty).option(userIds)
)
$insert bson bson
}
@ -239,16 +251,6 @@ object GameRepo {
$count(Json.obj(F.createdAt -> ($gte($date(from)) ++ $lt($date(to)))))
}).sequenceFu
def nowPlaying(userId: String): Fu[Option[Game]] =
$find.one(Query.status(Status.Started) ++ Query.user(userId) ++ Json.obj(
F.createdAt -> $gt($date(DateTime.now minusHours 1))
))
def isNowPlaying(userId: String): Fu[Boolean] = nowPlaying(userId) map (_.isDefined)
def lastPlayed(userId: String): Fu[Option[Game]] =
$find($query(Query user userId) sort ($sort desc F.createdAt), 1) map (_.headOption)
def bestOpponents(userId: String, limit: Int): Fu[List[(String, Int)]] = {
import reactivemongo.bson._
import reactivemongo.core.commands._
@ -320,7 +322,7 @@ object GameRepo {
))
}
def activePlayersSince(since: DateTime, max: Int): Fu[List[(String, Int)]] = {
def activePlayersSince(since: DateTime, max: Int): Fu[List[UidNb]] = {
import reactivemongo.bson._
import reactivemongo.core.commands._
import lila.db.BSON.BSONJodaDateTimeHandler
@ -342,7 +344,7 @@ object GameRepo {
(stream.toList map { obj =>
toJSON(obj).asOpt[JsObject] flatMap { o =>
o int "nb" map { nb =>
~(o str "_id") -> nb
UidNb(~(o str "_id"), nb)
}
}
}).flatten

View File

@ -18,9 +18,6 @@ case class Pov(game: Game, color: Color) {
def unary_! = Pov(game, !color)
def isPlayerFullId(fullId: Option[String]): Boolean =
fullId ?? { game.isPlayerFullId(player, _) }
def ref = PovRef(game.id, color)
def withGame(g: Game) = copy(game = g)
@ -32,6 +29,8 @@ case class Pov(game: Game, color: Color) {
game.correspondenceClock.map(_.remainingTime(color).toInt)
}
def hasMoved = game playerHasMoved color
override def toString = ref.toString
}
@ -53,9 +52,11 @@ object Pov {
game player user map { apply(game, _) }
def priority(pov: Pov) =
if (pov.isMyTurn) pov.remainingSeconds.getOrElse(Int.MaxValue - 1)
if (pov.isMyTurn) {
if (pov.hasMoved) pov.remainingSeconds.getOrElse(Int.MaxValue - 1)
else 10 // first move has priority over games with more than 10s left
}
else Int.MaxValue
}
case class PovRef(gameId: String, color: Color) {

View File

@ -50,6 +50,11 @@ object Query {
def nowPlaying(u: String) = Json.obj(F.playingUids -> u)
def recentlyPlayingWithClock(u: String) =
nowPlaying(u) ++ clock(true) ++ Json.obj(
F.updatedAt -> $gt($date(DateTime.now minusMinutes 5))
)
// use the us index
def win(u: String) = user(u) ++ Json.obj(F.winnerId -> u)
@ -65,4 +70,5 @@ object Query {
def checkable = Json.obj(F.checkAt -> $lt($date(DateTime.now)))
val sortCreated = $sort desc F.createdAt
val sortUpdatedNoIndex = $sort desc F.updatedAt
}

View File

@ -15,7 +15,7 @@ object Rewind {
val rewindedHistory = rewindedGame.board.history
val rewindedSituation = rewindedGame.situation
def rewindPlayer(player: Player) = player.copy(isProposingTakeback = false)
Progress(game, game.copy(
val newGame = game.copy(
whitePlayer = rewindPlayer(game.whitePlayer),
blackPlayer = rewindPlayer(game.blackPlayer),
binaryPieces = BinaryFormat.piece write rewindedGame.board.pieces,
@ -29,7 +29,7 @@ object Rewind {
check = if (rewindedSituation.check) rewindedSituation.kingPos else None),
binaryMoveTimes = BinaryFormat.moveTime write (game.moveTimes take rewindedGame.turns),
status = game.status,
clock = game.clock map (_.switch)
))
clock = game.clock map (_.takeback))
Progress(game, newGame, newGame.clock.map(Event.Clock.apply).toList)
}
}

View File

@ -5,6 +5,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
mongoCache: lila.memo.MongoCache.Builder,
db: lila.db.Env) {
private val CachedRatingChartTtl = config duration "cached.rating_chart.ttl"
@ -13,12 +14,16 @@ final class Env(
lazy val api = new HistoryApi(db(Collectionhistory))
lazy val ratingChartApi = new RatingChartApi(api, CachedRatingChartTtl)
lazy val ratingChartApi = new RatingChartApi(
historyApi = api,
mongoCache = mongoCache,
cacheTtl = CachedRatingChartTtl)
}
object Env {
lazy val current = "[boot] history" describes new Env(
config = lila.common.PlayApp loadConfig "history",
mongoCache = lila.memo.Env.current.mongoCache,
db = lila.db.Env.current)
}

View File

@ -9,13 +9,21 @@ import play.api.libs.json._
import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, Perfs }
final class RatingChartApi(historyApi: HistoryApi, cacheTtl: Duration) {
final class RatingChartApi(
historyApi: HistoryApi,
mongoCache: lila.memo.MongoCache.Builder,
cacheTtl: FiniteDuration) {
def apply(user: User): Fu[Option[String]] = cache(user)
def apply(user: User): Fu[Option[String]] = cache(user) map { chart =>
chart.nonEmpty option chart
}
private val cache = lila.memo.AsyncCache(build,
maxCapacity = 50,
timeToLive = cacheTtl)
private val cache = mongoCache[User, String](
prefix = "history:rating",
f = (user: User) => build(user) map (~_),
maxCapacity = 64,
timeToLive = cacheTtl,
keyToString = _.id)
private val columns = Json stringify {
Json.arr(

View File

@ -152,7 +152,8 @@ case class MoveEvent(
gameId: String,
fen: String,
move: String,
ip: String)
ip: String,
opponentUserId: Option[String])
case class NbRounds(nb: Int)
}

File diff suppressed because one or more lines are too long

View File

@ -21,11 +21,13 @@ private[lobby] final class Lobby(
def receive = {
case GetOpen(userOption) =>
case HooksFor(userOption) =>
val replyTo = sender
(userOption.map(_.id) ?? blocking) foreach { blocks =>
val lobbyUser = userOption map { LobbyUser.make(_, blocks) }
replyTo ! HookRepo.list.filter { Biter.canJoin(_, lobbyUser) }
replyTo ! HookRepo.list.filter { hook =>
~(hook.userId |@| lobbyUser.map(_.id)).apply(_ == _) || Biter.canJoin(hook, lobbyUser)
}
}
case msg@AddHook(hook) => {

View File

@ -83,7 +83,7 @@ object Seek {
createdAt = DateTime.now)
import reactivemongo.bson.Macros
import lila.db.BSON.MapValue._
import lila.db.BSON.MapValue.MapHandler
import lila.db.BSON.BSONJodaDateTimeHandler
private[lobby] implicit val lobbyUserBSONHandler = Macros.handler[LobbyUser]
private[lobby] implicit val seekBSONHandler = Macros.handler[Seek]

View File

@ -3,9 +3,11 @@ package lila.lobby
import org.joda.time.DateTime
import reactivemongo.bson.{ BSONDocument, BSONInteger, BSONRegex, BSONArray, BSONBoolean }
import reactivemongo.core.commands._
import scala.concurrent.duration._
import actorApi.LobbyUser
import lila.db.Types.Coll
import lila.memo.AsyncCache
import lila.user.{ User, UserRepo }
final class SeekApi(
@ -14,18 +16,33 @@ final class SeekApi(
maxPerPage: Int,
maxPerUser: Int) {
def forAnon: Fu[List[Seek]] =
private sealed trait CacheKey
private object ForAnon extends CacheKey
private object ForUser extends CacheKey
private def allCursor =
coll.find(BSONDocument())
.sort(BSONDocument("createdAt" -> -1))
.cursor[Seek].collect[List](maxPerPage)
.cursor[Seek]
private val cache = AsyncCache[CacheKey, List[Seek]](
f = {
case ForAnon => allCursor.collect[List](maxPerPage)
case ForUser => allCursor.collect[List]()
},
timeToLive = 5.seconds)
def forAnon = cache(ForAnon)
def forUser(user: User): Fu[List[Seek]] =
blocking(user.id) flatMap { blocking =>
forUser(LobbyUser.make(user, blocking))
}
def forUser(user: LobbyUser): Fu[List[Seek]] = forAnon map {
_ filter { Biter.canJoin(_, user) }
def forUser(user: LobbyUser): Fu[List[Seek]] = cache(ForUser) map {
_ filter { seek =>
seek.user.id == user.id || Biter.canJoin(seek, user)
} take maxPerPage
}
def find(id: String): Fu[Option[Seek]] =
@ -33,19 +50,21 @@ final class SeekApi(
def insert(seek: Seek) = coll.insert(seek) >> findByUser(seek.user.id).flatMap {
case seeks if seeks.size <= maxPerUser => funit
case seeks => seeks.drop(maxPerUser).map(remove).sequenceFu
}
case seeks =>
seeks.drop(maxPerUser).map(remove).sequenceFu
} >> cache.clear
def findByUser(userId: String): Fu[List[Seek]] =
coll.find(BSONDocument("user.id" -> userId))
.sort(BSONDocument("createdAt" -> -1))
.cursor[Seek].collect[List]()
def remove(seek: Seek) = coll.remove(BSONDocument("_id" -> seek.id)).void
def remove(seek: Seek) =
coll.remove(BSONDocument("_id" -> seek.id)).void >> cache.clear
def removeBy(seekId: String, userId: String) =
coll.remove(BSONDocument(
"_id" -> seekId,
"user.id" -> userId
)).void
)).void >> cache.clear
}

View File

@ -64,4 +64,4 @@ private[lobby] case class HookIds(ids: List[String])
case class AddHook(hook: Hook)
case class AddSeek(seek: Seek)
case class GetOpen(user: Option[User])
case class HooksFor(user: Option[User])

View File

@ -0,0 +1,18 @@
package lila.memo
import com.typesafe.config.Config
import lila.db.Types._
final class Env(config: Config, db: lila.db.Env) {
private val CollectionCache = config getString "collection.cache"
lazy val mongoCache: MongoCache.Builder = MongoCache(db(CollectionCache))
}
object Env {
lazy val current = "[boot] memo" describes new Env(
lila.common.PlayApp loadConfig "memo",
lila.db.Env.current)
}

View File

@ -0,0 +1,81 @@
package lila.memo
import org.joda.time.DateTime
import reactivemongo.bson._
import reactivemongo.bson.Macros
import scala.concurrent.duration._
import spray.caching.{ LruCache, Cache }
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.Types._
final class MongoCache[K, V: MongoCache.Handler] private (
prefix: String,
expiresAt: () => DateTime,
cache: Cache[V],
coll: Coll,
f: K => Fu[V],
keyToString: K => String) {
def apply(k: K): Fu[V] = cache(k) {
coll.find(select(k)).one[Entry] flatMap {
case None => f(k) flatMap { v =>
coll.insert(makeEntry(k, v)) inject v
}
case Some(entry) => fuccess(entry.v)
}
}
def remove(k: K): Funit =
coll.remove(select(k)).void >>- (cache remove k)
private case class Entry(_id: String, v: V, e: DateTime)
private implicit val entryBSONHandler = Macros.handler[Entry]
private def makeEntry(k: K, v: V) = Entry(makeKey(k), v, expiresAt())
private def makeKey(k: K) = s"$prefix:${keyToString(k)}"
private def select(k: K) = BSONDocument("_id" -> makeKey(k))
}
object MongoCache {
private type Handler[T] = BSONHandler[_ <: BSONValue, T]
private def expiresAt(ttl: Duration)(): DateTime =
DateTime.now plusSeconds ttl.toSeconds.toInt
final class Builder(coll: Coll) {
def apply[K, V: Handler](
prefix: String,
f: K => Fu[V],
maxCapacity: Int = 512,
initialCapacity: Int = 64,
timeToLive: FiniteDuration,
timeToLiveMongo: Option[FiniteDuration] = None,
keyToString: K => String = (k: K) => k.toString): MongoCache[K, V] = new MongoCache[K, V](
prefix = prefix,
expiresAt = expiresAt(timeToLiveMongo | timeToLive),
cache = LruCache(maxCapacity, initialCapacity, timeToLive),
coll = coll,
f = f,
keyToString = keyToString)
def single[V: Handler](
prefix: String,
f: => Fu[V],
timeToLive: FiniteDuration,
timeToLiveMongo: Option[FiniteDuration] = None) = new MongoCache[Boolean, V](
prefix = prefix,
expiresAt = expiresAt(timeToLiveMongo | timeToLive),
cache = LruCache(timeToLive = timeToLive),
coll = coll,
f = _ => f,
keyToString = _.toString)
}
def apply(coll: Coll) = new Builder(coll)
}

View File

@ -8,6 +8,7 @@ import lila.hub.actorApi.message.LichessThread
final class Env(
config: Config,
db: lila.db.Env,
mongoCache: lila.memo.MongoCache.Builder,
blocks: (String, String) => Fu[Boolean],
system: ActorSystem) {
@ -17,7 +18,7 @@ final class Env(
private[message] lazy val threadColl = db(CollectionThread)
private lazy val unreadCache = new UnreadCache
private lazy val unreadCache = new UnreadCache(mongoCache)
lazy val forms = new DataForm(blocks = blocks)
@ -46,6 +47,7 @@ object Env {
lazy val current = "[boot] message" describes new Env(
config = lila.common.PlayApp loadConfig "message",
db = lila.db.Env.current,
mongoCache = lila.memo.Env.current.mongoCache,
blocks = lila.relation.Env.current.api.blocks,
system = lila.common.PlayApp.system)
}

View File

@ -1,19 +1,24 @@
package lila.message
import spray.caching.{ LruCache, Cache }
import scala.concurrent.duration._
import lila.db.BSON._
import lila.user.User
private[message] final class UnreadCache {
private[message] final class UnreadCache(
mongoCache: lila.memo.MongoCache.Builder) {
// userId => thread IDs
private val cache: Cache[List[String]] = LruCache(maxCapacity = 99999)
private val cache = mongoCache[String, List[String]](
prefix = "message:unread",
f = ThreadRepo.userUnreadIds,
maxCapacity = 4096,
timeToLive = 2.days)
def apply(userId: String): Fu[List[String]] =
cache(userId)(ThreadRepo userUnreadIds userId)
def apply(userId: String): Fu[List[String]] = cache(userId)
def refresh(userId: String): Fu[List[String]] =
(cache remove userId).fold(apply(userId))(_ >> apply(userId))
(cache remove userId) >> apply(userId)
def clear(userId: String) = (cache remove userId).fold(funit)(_.void)
def clear(userId: String) = cache remove userId
}

View File

@ -16,7 +16,7 @@ final class PrefApi(coll: Coll, cacheTtl: Duration) {
private implicit val prefBSONHandler = new BSON[Pref] {
import lila.db.BSON.MapValue._
import lila.db.BSON.MapValue.{ MapReader, MapWriter }
implicit val tagsReader = MapReader[String]
implicit val tagsWriter = MapWriter[String]

View File

@ -8,6 +8,7 @@ final class Env(
config: Config,
hub: lila.hub.Env,
detectLanguage: DetectLanguage,
mongoCache: lila.memo.MongoCache.Builder,
db: lila.db.Env) {
private val CollectionQuestion = config getString "collection.question"
@ -19,6 +20,7 @@ final class Env(
lazy val api = new QaApi(
questionColl = questionColl,
answerColl = db(CollectionAnswer),
mongoCache = mongoCache,
notifier = notifier)
private lazy val notifier = new Notifier(
@ -37,5 +39,6 @@ object Env {
config = lila.common.PlayApp loadConfig "qa",
hub = lila.hub.Env.current,
detectLanguage = DetectLanguage(lila.common.PlayApp loadConfig "detectlanguage"),
mongoCache = lila.memo.Env.current.mongoCache,
db = lila.db.Env.current)
}

View File

@ -9,15 +9,15 @@ import org.joda.time.DateTime
import spray.caching.{ LruCache, Cache }
import lila.common.paginator._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.BSON._
import lila.db.paginator._
import lila.db.Types.Coll
import lila.memo.AsyncCache
import lila.user.{ User, UserRepo }
final class QaApi(
questionColl: Coll,
answerColl: Coll,
mongoCache: lila.memo.MongoCache.Builder,
notifier: Notifier) {
object question {
@ -84,11 +84,12 @@ final class QaApi(
currentPage = page,
maxPerPage = perPage)
private def popularCache = AsyncCache(
(nb: Int) => questionColl.find(BSONDocument())
private def popularCache = mongoCache(
prefix = "qa:popular",
f = (nb: Int) => questionColl.find(BSONDocument())
.sort(BSONDocument("vote.score" -> -1))
.cursor[Question].collect[List](nb),
timeToLive = 1 hour)
timeToLive = 3 hour)
def popular(max: Int): Fu[List[Question]] = popularCache(max)
@ -111,7 +112,7 @@ final class QaApi(
questionColl.update(
BSONDocument("_id" -> q.id),
BSONDocument("$set" -> BSONDocument("vote" -> newVote))
) >> profile.clearCache >> popularCache.clear inject newVote.some
) >> profile.clearCache inject newVote.some
}
}

View File

@ -82,4 +82,5 @@ object PerfType {
def name(key: Perf.Key): Option[String] = apply(key) map (_.name)
val nonPuzzle: List[PerfType] = List(Bullet, Blitz, Classical, Correspondence, Chess960, KingOfTheHill, ThreeCheck)
val leaderboardable: List[PerfType] = List(Bullet, Blitz, Classical, Chess960, KingOfTheHill, ThreeCheck)
}

View File

@ -24,8 +24,8 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
if (by.id == UserRepo.lichessId) reportTube.coll.update(
selectRecent(user, reason),
reportTube.toMongo(report).get - "_id"
) map { res =>
if (!res.updatedExisting) {
) flatMap { res =>
(!res.updatedExisting) ?? {
if (report.isCheat) evaluator ! user
$insert(report)
}

View File

@ -8,7 +8,6 @@ import scala.concurrent.duration._
import actorApi.{ GetSocketStatus, SocketStatus }
import lila.common.PimpedConfig._
import lila.hub.actorApi.map.Ask
import lila.memo.AsyncCache
import lila.socket.actorApi.GetVersion
import makeTimeout.large
@ -29,6 +28,7 @@ final class Env(
prefApi: lila.pref.PrefApi,
chatApi: lila.chat.ChatApi,
historyApi: lila.history.HistoryApi,
isPlayingSimul: String => Fu[Boolean],
scheduler: lila.common.Scheduler) {
private val settings = new {
@ -84,7 +84,8 @@ final class Env(
uidTimeout = UidTimeout,
socketTimeout = SocketTimeout,
disconnectTimeout = PlayerDisconnectTimeout,
ragequitTimeout = PlayerRagequitTimeout)
ragequitTimeout = PlayerRagequitTimeout,
isPlayingSimul = isPlayingSimul)
def receive: Receive = ({
case msg@lila.chat.actorApi.ChatLine(id, line) =>
self ! lila.hub.actorApi.map.Tell(id take 8, msg)
@ -197,5 +198,6 @@ object Env {
prefApi = lila.pref.Env.current.api,
chatApi = lila.chat.Env.current.api,
historyApi = lila.history.Env.current.api,
isPlayingSimul = lila.game.Env.current.cached.isPlayingSimul,
scheduler = lila.common.PlayApp.scheduler)
}

View File

@ -62,9 +62,7 @@ final class JsonView(
"check" -> game.check.map(_.key),
"rematch" -> game.next,
"source" -> game.source.map(sourceJson),
"status" -> Json.obj(
"id" -> game.status.id,
"name" -> game.status.name)),
"status" -> statusJson(game.status)),
"clock" -> game.clock.map(clockJson),
"correspondence" -> game.correspondenceClock.map(correspondenceJson),
"player" -> Json.obj(
@ -166,9 +164,7 @@ final class JsonView(
"size" -> o.size
)
},
"status" -> Json.obj(
"id" -> game.status.id,
"name" -> game.status.name)),
"status" -> statusJson(game.status)),
"clock" -> game.clock.map(clockJson),
"correspondence" -> game.correspondenceClock.map(correspondenceJson),
"player" -> Json.obj(
@ -219,6 +215,32 @@ final class JsonView(
)
}
def userAnalysisJson(pov: Pov, pref: Pref) = {
import pov._
val fen = Forsyth >> game.toChess
Json.obj(
"game" -> Json.obj(
"id" -> gameId,
"variant" -> variantJson(game.variant),
"initialFen" -> fen,
"fen" -> fen,
"player" -> game.turnColor.name,
"status" -> statusJson(game.status)),
"player" -> Json.obj(
"color" -> color.name
),
"opponent" -> Json.obj(
"color" -> opponent.color.name
),
"pref" -> Json.obj(
"animationDuration" -> animationDuration(pov, pref),
"highlight" -> pref.highlight,
"destination" -> pref.destination,
"coords" -> pref.coords
),
"userAnalysis" -> true)
}
private def blurs(game: Game, player: lila.game.Player) = {
val percent = game.playerBlurPercent(player.color)
(percent > 30) option Json.obj(
@ -258,10 +280,13 @@ final class JsonView(
"moretime" -> moretimeSeconds)
private def correspondenceJson(c: CorrespondenceClock) = Json.obj(
"increment" -> c.increment,
"white" -> c.whiteTime,
"black" -> c.blackTime,
"emerg" -> c.emerg)
"increment" -> c.increment,
"white" -> c.whiteTime,
"black" -> c.blackTime,
"emerg" -> c.emerg)
private def statusJson(s: chess.Status) =
Json.obj("id" -> s.id, "name" -> s.name)
private def sourceJson(source: Source) = source.name

View File

@ -31,7 +31,7 @@ private[round] final class Player(
case (progress, move) =>
(GameRepo save progress) >>-
(pov.game.hasAi ! uciMemo.add(pov.game, move)) >>-
notifyProgress(move, progress, ip) >>
notifyMove(move, progress.game, ip) >>
progress.game.finished.fold(
moveFinish(progress.game, color) map { progress.events ::: _ }, {
cheatDetector(progress.game) addEffect {
@ -61,13 +61,13 @@ private[round] final class Player(
fufail(s"[ai play] game ${game.id} turn ${game.turns} not AI turn")
) logFailureErr s"[ai play] game ${game.id} turn ${game.turns}"
private def notifyProgress(move: chess.Move, progress: Progress, ip: String) {
val game = progress.game
private def notifyMove(move: chess.Move, game: Game, ip: String) {
bus.publish(MoveEvent(
ip = ip,
gameId = game.id,
fen = Forsyth exportBoard game.toChess.board,
move = move.keyString
move = move.keyString,
opponentUserId = game.player(!move.color).userId
), 'moveEvent)
}

Some files were not shown because too many files have changed in this diff Show More