Merge branch 'master' into pools

* master:
  fix positioning of high rated graph seeks
  translate coordinate trainer, use icons for color buttons
  zh "中文" translation #9053. Author: squares-64. I just added everything that was missing, except where the percentages were because I didn't know what to do...
  puzzle: add missing chess.js extern - fixes underpromotion
  puzzle: somehow this fixes the production build
  fix flickering of board editor pieces
  upgrade chess module to support more PGN formats
  fix blinking pieces (was a redirect due to bad asset domain)
  give 15 seconds on deploy
  no captcha for blind dudes
  accessibility: buttons rather than <a> without href
  fix anon blind mode
  unify game API and analysis API
  use Game.analysed denormalization in search indexer
  denormalize presence of game analysis, expose filter in API
  improve game textual representation
  add game by id API entry point
  de "Deutsch" translation #9052. Author: schachfl. removed "elo-rage"
  sk "slovenčina" translation #9050. Author: MoKy.
  upgrade chess module to support ++ PGN mate notation
pull/87/head
Thibault Duplessis 2014-06-07 09:57:04 +02:00
commit 07f3c8c165
43 changed files with 304 additions and 241 deletions

109
README.md
View File

@ -130,7 +130,9 @@ name | type | default | description
--- | --- | --- | ---
**username** | string | - | filter games by user
**rated** | 1 or 0 | - | filter rated or casual games
**analysed** | 1 or 0 | - | filter only analysed (or not analysed) games
**nb** | int | 10 | maximum number of games to return
**with_analysis** | 1 or 0 | 0 | include deep analysis data in the result
**token** | string | - | security token (unlocks secret game data)
```
@ -143,7 +145,7 @@ name | type | default | description
{
"id": "x2kpaixn",
"rated": false,
"status": "mate",
"status": "mate", // (1)
"clock":{ // all clock values are expressed in seconds
"limit": 300,
"increment": 8,
@ -165,36 +167,7 @@ name | type | default | description
},
"black": ... // other player
}
},
{
... // other game
}
]
}
```
(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L25
### `GET /api/analysis` fetch many analysis
This API requires a secret token to work.
Analysis are returned by descendant chronological order.
All parameters are optional.
name | type | default | description
--- | --- | --- | ---
**token** | string | - | security token
**nb** | int | 10 | maximum number of analysis to return
```
> curl http://en.lichess.org/api/analysis?nb=10
```
```javascript
{
"list": [
{
"analysis": [
"analysis": [ // only if the with_analysis flag is set
{
"eval": -26, // board evaluation in centipawns
"move": "e4",
@ -214,15 +187,81 @@ name | type | default | description
},
// ... more moves
],
"game": {
// similar to the game API format, see above
},
"uci": "e2e4 e7e5 d2d4 e5d4 g1f3 g8f6" // UCI compatible game moves
},
{
... // other game
}
]
}
```
(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L25
### `GET /api/game/{id}` fetch one game by ID
A single game is returned.
All parameters are optional.
name | type | default | description
--- | --- | --- | ---
**with_analysis** | 1 or 0 | 0 | include deep analysis data in the result
**token** | string | - | security token (unlocks secret game data)
```
> curl http://en.lichess.org/api/game/x2kpaixn
```
```javascript
{
"id": "x2kpaixn",
"rated": false,
"status": "mate", // (1)
"clock":{ // all clock values are expressed in seconds
"limit": 300,
"increment": 8,
"totalTime": 540 // evaluation of the game duration = limit + 30 * increment
},
"timestamp": 1389100907239,
"turns": 44,
"url": "http://lichess.org/x2kpaixn",
"winner": "black",
"players": {
"white": {
"userId": "thibault"
"rating": 1642,
"analysis": {
"blunder": 1,
"inaccuracy": 0,
"mistake": 2
}
},
"black": ... // other player
},
"analysis": [ // only if the with_analysis flag is set
{
"eval": -26, // board evaluation in centipawns
"move": "e4",
"ply": 1
},
{
"eval": -8,
"move": "b5",
"ply": 2
},
{
"comment": "(-0.08 → -0.66) Inaccuracy. The best move was c4.",
"eval": -66,
"move": "Nfe3",
"ply": 3,
"variation": "c4 bxc4 Nfe3 c5 Qf1 f6 Rxc4 Bb7 b4 Ba6"
},
// ... more moves
]
}
```
(1) All game statuses: https://github.com/ornicar/scalachess/blob/master/src/main/scala/Status.scala#L16-L25
### `GET /api/puzzle/<id>` fetch one puzzle
```

View File

@ -9,7 +9,6 @@ object Api extends LilaController {
private val userApi = Env.api.userApi
private val gameApi = Env.api.gameApi
private val analysisApi = Env.api.analysisApi
private val puzzleApi = Env.api.puzzleApi
def user(username: String) = ApiResult { req =>
@ -21,7 +20,7 @@ object Api extends LilaController {
def users = ApiResult { req =>
userApi.list(
team = get("team", req),
engine = get("engine", req) map ("1"==),
engine = getBoolOpt("engine", req),
token = get("token", req),
nb = getInt("nb", req)
) map (_.some)
@ -30,17 +29,19 @@ object Api extends LilaController {
def games = ApiResult { req =>
gameApi.list(
username = get("username", req),
rated = get("rated", req) map ("1"==),
rated = getBoolOpt("rated", req),
analysed = getBoolOpt("analysed", req),
withAnalysis = getBool("with_analysis", req),
token = get("token", req),
nb = getInt("nb", req)
) map (_.some)
}
def analysis = ApiResult { req =>
analysisApi.list(
nb = getInt("nb", req),
token = get("token", req)
) map (_.some)
def game(id: String) = ApiResult { req =>
gameApi.one(
id = id take lila.game.Game.gameIdSize,
withAnalysis = getBool("with_analysis", req),
token = get("token", req))
}
def puzzle(id: String) = ApiResult { req =>

View File

@ -64,7 +64,7 @@ object Auth extends LilaController {
BadRequest(html.auth.signup(err, captcha))
},
data => Firewall {
UserRepo.create(data.username, data.password) flatMap { userOption =>
UserRepo.create(data.username, data.password, ctx.blindMode) flatMap { userOption =>
val user = userOption err "No user could be created for %s".format(data.username)
HistoryRepo.create(user) >> gotoSignupSucceeded(user.username)
}

View File

@ -188,7 +188,7 @@ private[controllers] trait LilaController
}
private def pageDataBuilder(ctx: lila.user.UserContext): Fu[PageData] =
ctx.me.fold(fuccess(PageData.default)) { me =>
ctx.me.fold(fuccess(PageData anon blindMode(ctx.req))) { me =>
val isPage = HTTPRequest.isSynchronousHttp(ctx.req)
(Env.pref.api getPref me) zip {
isPage ?? {
@ -203,11 +203,13 @@ private[controllers] trait LilaController
}
} map {
case (pref, ((friends, teamNbRequests), messageIds)) =>
val blindMode = ctx.req.cookies.get(Env.api.accessibilityBlindCookieName).map(_.value) == "1".some
PageData(friends, teamNbRequests, messageIds.size, pref, blindMode)
PageData(friends, teamNbRequests, messageIds.size, pref, blindMode(ctx.req))
}
}
private def blindMode(req: RequestHeader) =
req.cookies.get(Env.api.accessibilityBlindCookieName).map(_.value) == "1".some
private def restoreUser(req: RequestHeader): Fu[Option[UserModel]] =
Env.security.api restoreUser req addEffect {
_ ifTrue (HTTPRequest isSynchronousHttp req) foreach { user =>

View File

@ -23,4 +23,7 @@ trait RequestGetter {
protected def getBool(name: String, req: RequestHeader) =
getInt(name, req) exists (1==)
protected def getBoolOpt(name: String, req: RequestHeader) =
getInt(name, req) map (1==)
}

View File

@ -10,9 +10,11 @@ trait AssetHelper {
def isProd: Boolean
private val domain = lila.api.Env.current.Net.AssetDomain
val assetDomain = lila.api.Env.current.Net.AssetDomain
def staticUrl(path: String) = s"http://$domain${routes.Assets.at(path)}"
val assetBaseUrl = s"http://$assetDomain"
def staticUrl(path: String) = s"$assetBaseUrl${routes.Assets.at(path)}"
def cssTag(name: String, staticDomain: Boolean = true) = cssAt("stylesheets/" + name, staticDomain)

View File

@ -1,5 +1,9 @@
@(move: Field, gameId: Field, captcha: lila.common.Captcha)(implicit ctx: Context)
<input type="hidden" name="@gameId.name" id="@gameId.id" value="@captcha.gameId" />
@if(ctx.blindMode) {
<input type="hidden" name="@move.name" id="@move.id" value="@captcha.solutions.head" />
} else {
<div class="checkmateCaptcha clearfix" data-check-url="@routes.Main.captchaCheck(captcha.gameId)">
<div class="checkmateFen">
<div
@ -23,3 +27,4 @@
<input type="hidden" name="@move.name" id="@move.id" value="" />
</div>
</div>
}

View File

@ -50,10 +50,10 @@ openGraph: Map[Symbol, String] = Map.empty)(body: Html)(implicit ctx: Context)
<div id="site_description">@trans.freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents()</div>
<div id="top" class="clearfix">
@if(!zen) {
<a data-hint="@trans.toggleSound()" id="sound_state" class="available hint--bottom-left">
<button data-hint="@trans.toggleSound()" id="sound_state" class="a available hint--bottom-left">
<span class="is2 on" data-icon="n"></span>
<span class="is2 off" data-icon="o"></span>
</a>
</button>
}
@if(!zen) {
@siteMenu.all(ctx.me).map { elem =>

View File

@ -16,7 +16,7 @@ title = trans.boardEditor.str(),
menu = game.sideMenu(listMenu, "edit").some,
moreJs = moreJs,
moreCss = moreCss) {
<div id="board_editor" class="content_box">
<div id="board_editor" class="content_box" data-asset-url="@assetBaseUrl">
<div id="chessboard_side">
<div>
<a class="button start">@trans.startPosition()</a>

View File

@ -1,5 +1,7 @@
@(scoreOption: Option[lila.coordinate.Score])(implicit ctx: Context)
@import lila.pref.Pref.Color
@moreCss = {
@cssTag("training.css")
@cssTag("coordinate.css")
@ -50,11 +52,11 @@ active = siteMenu.puzzle.some) {
}
</div>
<form class="color buttons" action="@routes.Coordinate.color" method="POST">
@lila.pref.Pref.Color.choices.map {
@List(Color.BLACK -> trans.black(), Color.RANDOM -> trans.randomColor(), Color.WHITE -> trans.white()).map {
case (id, name) => {
<input type="radio" id="coord_color_@id" name="coord_color" value="@id"
@(if(id == ctx.pref.coordColor) "checked" else "") />
<label for="coord_color_@id">@name</label>
<label for="coord_color_@id" data-hint="@name" class="color color_@id hint--bottom"></label>
}
}
</form>

View File

@ -6,7 +6,8 @@
data-color="@puzzle.color"
data-move="@puzzle.initialMove"
data-lines="@lila.puzzle.Line.toJsonString(puzzle.lines)"
data-post-url="@routes.Puzzle.attempt(puzzle.id)">
data-post-url="@routes.Puzzle.attempt(puzzle.id)"
data-asset-url="@assetBaseUrl">
<div class="side">
@trainingBox(puzzle, userInfos, true)

View File

@ -6,7 +6,8 @@
data-color="@puzzle.color"
data-move="@puzzle.initialMove"
data-lines="@lila.puzzle.Line.toJsonString(puzzle.lines)"
data-new-url="@routes.Puzzle.newPuzzle">
data-new-url="@routes.Puzzle.newPuzzle"
data-asset-url="@assetBaseUrl">
<div class="side">
@trainingBox(puzzle, userInfos, false)

View File

@ -14,23 +14,23 @@
</div>
<div class="lichess_control icons">
@if(game.abortable) {
<a class="abort button socket-link hint--bottom" data-msg="abort" data-hint="@trans.abortGame()">
<button class="abort button socket-link hint--bottom" data-msg="abort" data-hint="@trans.abortGame()">
<span data-icon="L"></span>
</a>
</button>
} else {
@if(takebackable && game.playerCanProposeTakeback(color)) {
<a class="takeback button socket-link hint--bottom" data-msg="takeback-yes" data-hint="@trans.proposeATakeback()">
<button class="takeback button socket-link hint--bottom" data-msg="takeback-yes" data-hint="@trans.proposeATakeback()">
<span data-icon="i"></span>
</a>
</button>
}
@if(game.playerCanOfferDraw(color)) {
<a class="draw button socket-link hint--bottom" data-msg="draw-yes" data-hint="@trans.offerDraw()">
<button class="draw button socket-link hint--bottom" data-msg="draw-yes" data-hint="@trans.offerDraw()">
<span data-icon="2"></span>
</a>
</button>
}
<a class="resign button socket-link hint--bottom" data-msg="resign" data-hint="@trans.resign()">
<button class="resign button socket-link hint--bottom" data-msg="resign" data-hint="@trans.resign()">
<span data-icon="b"></span>
</a>
</button>
}
</div>
@if(game.resignable && !game.hasAi) {

View File

@ -5,7 +5,7 @@
<dt>Turn</dt>
<dd>@pov.game.turns</dd>
<dt>PGN</dt>
<dd>@pov.game.pgnMoves.mkString(" ")</dd>
<dd>@Html(pov.game.pgnMoves.mkString("<br />"))</dd>
<dt>FEN</dt>
<dd>@{chess.format.Forsyth.>>(pov.game.toChess)}</dd>
<dt>Your color</dt>
@ -25,8 +25,8 @@
<label>
Your move in UCI format (e.g. e2e4 or d7d8Q)
<input name="move" class="move" type="text" value="" />
<button type="submit">Send move</button>
</label>
<button type="submit">Send move</button>
</form>
}
</dl>

View File

@ -0,0 +1,4 @@
var games = db.game5;
db.analysis2.find({},{_id:true}).forEach(function(analysis) {
games.update({_id: analysis._id},{$set:{an:true}});
});

View File

@ -5,7 +5,8 @@ var Chess = {
load: function(fen) {},
fen: function() {},
turn: function() {},
in_check: function() {}
in_check: function() {},
undo: function() {}
};
jQuery.prototype.sparkline = function(points, options) {};
jQuery.displayBoardMarks = function(board, white) {};

View File

@ -22,13 +22,12 @@
(jq/add-class ($ squares $puzzle) :last)
(when $check (jq/add-class $check :check)))))
(defn make-chessboard [config]
(let [static-domain (str "http://" (clojure.string/replace (.-domain js/document) #"^\w+" "static"))
piece-set (jq/data ($ :body) :piece-set)]
(defn make-chessboard [config asset-url]
(let [piece-set (jq/data ($ :body) :piece-set)]
(new js/ChessBoard "chessboard"
(clj->js (merge {:sparePieces false
:showNotation false
:pieceTheme (str static-domain "/assets/piece/" piece-set "/{piece}.svg")}
:pieceTheme (str asset-url "/assets/piece/" piece-set "/{piece}.svg")}
config)))))
(defn board-marks! [$puzzle]

View File

@ -13,13 +13,14 @@
(defn on-drop! [orig, dest]
(if (core/apply-move chess orig dest) (put! drop-chan (str orig dest)) "snapback"))
(defn make-chessboard [$puzzle fen]
(defn make-chessboard [$puzzle asset-url fen]
(core/make-chessboard {:orientation (jq/data $puzzle :color)
:position fen
:moveSpeed animation-delay
:draggable true
:dropOffBoard "snapback"
:onDrop on-drop!}))
:onDrop on-drop!}
asset-url))
(defn show-turn! [$puzzle]
(let [color (if (= (.turn chess) "w") "white" "black")
@ -106,9 +107,10 @@
(defn run! []
(let [$puzzle ($ :#puzzle)
assets (jq/data $puzzle :asset-url)
lines (js->clj (jq/data $puzzle :lines))
initial-fen (jq/data $puzzle :fen)
chessboard (make-chessboard $puzzle initial-fen)
chessboard (make-chessboard $puzzle assets initial-fen)
started-at (new js/Date)]
(core/center-right! ($ :.right $puzzle))
(core/board-marks! $puzzle)

View File

@ -9,10 +9,11 @@
(def continue-chan (async/chan))
(def animation-delay 200)
(defn make-chessboard [$puzzle]
(defn make-chessboard [$puzzle asset-url]
(core/make-chessboard {:orientation (jq/data $puzzle :color)
:moveSpeed animation-delay
:draggable false}))
:draggable false}
asset-url))
(defn bind-vote! [$vote]
(jq/on $vote :click ".enabled a:not(.active)"
@ -72,6 +73,7 @@
(defn run! [progress]
(let [$puzzle ($ :#puzzle)
assets (jq/data $puzzle :asset-url)
$browse ($ :#GameButtons $puzzle)
$first ($ :.first $browse)
$prev ($ :.prev $browse)
@ -80,7 +82,7 @@
lines (js->clj (jq/data $puzzle :lines))
line (find-best-line-from-progress lines progress)
history (vec (make-history (jq/data $puzzle :fen) (conj (seq line) (jq/data $puzzle :move))))
chessboard (make-chessboard $puzzle)]
chessboard (make-chessboard $puzzle assets)]
(core/center-right! ($ :.right $puzzle))
(core/board-marks! $puzzle)
(core/buttons! $puzzle)

View File

@ -28,7 +28,7 @@ yourOpponentWantsToPlayANewGameWithYou=Dein Gegner möchte eine neue Partie mit
joinTheGame=Der Partie beitreten
whitePlays=Weiß am Zug
blackPlays=Schwarz am Zug
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=Dein Gegner hat die Partie verlassen. Du kannst den Sieg reklamieren oder auf deinen Gegner warten.
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=Dein Gegner hat die Partie verlassen. Du kannst den Sieg reklamieren, dass Spiel remis erklären oder auf deinen Gegner warten.
makeYourOpponentResign=Bringe deinen Gegner zur Aufgabe
forceResignation=Sieg reklamieren
forceDraw=Remis erzwingen
@ -135,7 +135,7 @@ nbWins=%s Siege
nbLosses=%s Niederlagen
nbDraws=%s Remis
exportGames=Spiele exportieren
ratingRange=Elo-Bereich
ratingRange=Rating-Bereich
giveNbSeconds=%s Sekunden geben
premoveEnabledClickAnywhereToCancel=Vorauszug aktiviert - Zum Abbrechen irgendwo klicken
thisPlayerUsesChessComputerAssistance=Dieser Spieler verwendet einen Schachcomputer als Hilfe

View File

@ -50,7 +50,7 @@ theComputerAnalysisHasFailed=Počítačová analýza zlyhala
viewTheComputerAnalysis=Zobraziť počítačovú analýzu
requestAComputerAnalysis=Požiadať o počítačovú analýzu
computerAnalysis=Počítačová anylýza
blunders=Hrubyé chyby
blunders=Hrubé chyby
mistakes=Chyby
inaccuracies=Nepresnosti
moveTimes=Trvanie ťahu
@ -65,7 +65,7 @@ viewAllNbGames=Zobraziť všetkých %s hier
viewNbCheckmates=Zobraziť %s šach-maty
nbBookmarks=%s Záložky
nbPopularGames=%s Populárne hry
nbAnalysedGames=%s Analyzovaných Hier
nbAnalysedGames=%s Analyzovaných hier
bookmarkedByNbPlayers=Záložku uložilo %s hráčov
viewInFullSize=Zobraziť v plnej veľkosti
logOut=Odhlásiť sa
@ -73,7 +73,7 @@ signIn=Prihlásiť sa
newToLichess=Nováčik na Lichess?
youNeedAnAccountToDoThat=Pre túto požiadavku musíš byť prihlásený
signUp=Zaregistrovať sa
computersAreNotAllowedToPlay=I programmi scacchistici o l'ausilio di programmi scacchistici nono sono autorizzati,per cortesia evitate l'uso di programmi,databases,o i suggerimenti di altri giocatori durante una partita
computersAreNotAllowedToPlay=Hráči využívajúci pomoc počítačov nie sú vítaní. Počas partie nepoužívajte šachové programy, databázy ani rady iných hráčov.
games=Hry
forum=Forum
xPostedInForumY=%s príspevkov na fóre %s
@ -124,7 +124,7 @@ inbox=Správy
chatRoom=Chatovacia miestnosť
spectatorRoom=Divácka miestnosť
composeMessage=Napísať správu
noNewMessages=Žiadne správy momentálne
noNewMessages=Žiadne nové správy
subject=Predmet
recipient=Príjemca
send=Poslať
@ -156,7 +156,7 @@ tournamentPoints=Turnajové body
viewTournament=Zobraz turnaj
backToTournament=Návrat do turnaja
xTournament=%s turnaj
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Šach online zadarmo. Hrajte šach teraz v novom rozhraní. Bez registrácie, reklám a zásuvných modulov. Hraj šach s počítačom, priateľmi alebo náhodnými protivníkmi.
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Šach online zadarmo. Hrajte šach teraz v novom rozhraní. Bez registrácie, reklám a zásuvných modulov. Hrajte šach s počítačom, priateľmi alebo náhodnými protivníkmi.
teams=Tímy
nbMembers=%s členovia
allTeams=Všetky tímy
@ -170,7 +170,7 @@ aConfirmationIsRequiredToJoin=Potvrdenie je potrebné na vstup
joiningPolicy=Pravidlá vstupu
teamLeader=Vecúci tímu
teamBestPlayers=Najlepší hráči
teamRecentMembers=Nový členovia tímu
teamRecentMembers=Noví členovia tímu
xJoinedTeamY=%s sa pripojil/la k tímu %s
xCreatedTeamY=%s vytvoril/la tím %s
averageElo=Priemerné Elo
@ -181,15 +181,15 @@ reset=Reset
apply=Použiť
leaderboard=Rebríček
pasteTheFenStringHere=Vlož sem hodnotu FEN
pasteThePgnStringHere=Vlož sem PGN hodnotu
pasteThePgnStringHere=Vlož sem PGN text
fromPosition=Od pozície
continueFromHere=Pokračuj odtiaľto
importGame=Import hry
nbImportedGames=%s importovaných hier
thisIsAChessCaptcha=Šachová CAPTCHA
clickOnTheBoardToMakeYourMove=Kliknite na šachovnicu a spravte ťah, aby ste dokázali, že ste človek.
notACheckmate=Nie je šach mat
colorPlaysCheckmateInOne=%s hrá; šach mat na jeden ťah
notACheckmate=Nie je to mat
colorPlaysCheckmateInOne=%s na ťahu; mat prvým ťahom
retry=Znova
reconnecting=Pripájanie
onlineFriends=Online pritelia
@ -221,14 +221,14 @@ required=Vyžaduje sa
addToChrome=Pridať do Chrome
openTournaments=Otvorené turnaje
duration=Dĺžka
winner=Výťaz
winner=Víťaz
standing=Pozícia
createANewTournament=Vytvor nový turnaj
join=Pripojiť
withdraw=Odstúpiť
points=Bodov
wins=Výťazi
losses=Porazený
wins=Víťazi
losses=Prehry
winStreak=Séria výhier
createdBy=Vytvorené
waitingForNbPlayers=Čakanie na %s hráčov
@ -250,7 +250,7 @@ biography=Životopis
country=Krajina
preferences=Nastavenia
watchLichessTV=Sleduj Lichess TV
previouslyOnLichessTV=Bolo na Liechess TV
previouslyOnLichessTV=Bolo na Lichess TV
todaysLeaders=Dnešní lídri
onlinePlayers=Pripojený hráči
progressToday=Dnešný pokrok
@ -267,10 +267,10 @@ bestSlowPlayers=Najlepší hráči pomalých hier
bewareTheGameIsRatedButHasNoClock=Pozor, hra je bodovaná, ale bez času!
training=Tréning
yourPuzzleRatingX=Hodnotenie z : %s
findTheBestMoveForWhite=Trova la miglior mossa del bianco
findTheBestMoveForBlack=Trova la miglior mossa del nero
findTheBestMoveForWhite=Nájdite za bieleho najlepší ťah.
findTheBestMoveForBlack=Nájdite za čierneho najlepší ťah.
toTrackYourProgress=Sledovať svoj pokrok:
trainingSignupExplanation=Lichess ponúka šachové rébusy, ktoré zlepšujú vašu šikovnosť pre ďalšie šachové partie .
trainingSignupExplanation=Lichess ponúka šachové rébusy, ktoré zlepšujú vašu šikovnosť pre ďalšie šachové partie.
recentlyPlayedPuzzles=Nedávno hrané rébusy
puzzleId=Rébus %s
puzzleOfTheDay=Rébus dňa
@ -280,16 +280,16 @@ butYouCanDoBetter=Ale môžete to spraviť lepšie.
bestMove=Najlepší ťah!
keepGoing=Pokračujte...
puzzleFailed=Nesprávne vyriešený rébus
butYouCanKeepTrying=Napriek tomu môžte pokračovať.
butYouCanKeepTrying=Napriek tomu môžete pokračovať.
victory=Výhra!
giveUp=Vzdať sa
puzzleSolvedInXSeconds=Rébus vyriešený za %s sekúnd.
wasThisPuzzleAnyGood=Bol rébus správny?
wasThisPuzzleAnyGood=Bol tento rébus dobrý?
pleaseVotePuzzle=Pomôžte zlepšiť lichess hlasovaním, kliknutím na šípku hore alebo dole:
thankYou=Ďakujem!
ratingX=Hodnotenie: %s
playedXTimes=Hraný %s krát
fromGameLink=Pre hru %s
fromGameLink=Z hry %s
startTraining=Začať tréning
continueTraining=Pokračovanie v tréningu
retryThisPuzzle=Opakovať tento rébus

View File

@ -73,6 +73,7 @@ signIn=登录
newToLichess=新来的吗
youNeedAnAccountToDoThat=你需要先登入帐号
signUp=注册
computersAreNotAllowedToPlay=电脑及电脑辅助队员不准玩。请不要从国际象棋引擎,数据库,或者从其他玩家边玩得到帮助
games=棋
forum=论坛
xPostedInForumY=%s 张贴在论坛 %s
@ -266,12 +267,27 @@ bestSlowPlayers=最佳慢棋棋手
bewareTheGameIsRatedButHasNoClock=注意,这局是评分的但不计时
training=训练
yourPuzzleRatingX=您的难题评级:%s
findTheBestMoveForWhite=找到白色的最好的移动
findTheBestMoveForBlack=找到黑色的最好的移动
toTrackYourProgress=跟踪你的进度:
trainingSignupExplanation=Lichess将提供符合您的能力从而为更好地训练难题。
recentlyPlayedPuzzles=最近在玩拼图
puzzleId=谜 %s
puzzleOfTheDay=一天之谜
clickToSolve=点击解决
goodMove=好着
butYouCanDoBetter=你可以做的更好
bestMove=最理想的做法
keepGoing=继续下去...
puzzleFailed=失败之谜
butYouCanKeepTrying=但是你可以不断尝试
victory=胜利!
giveUp=放弃
wasThisPuzzleAnyGood=为这个难题有什么好处?
pleaseVotePuzzle=通过投票帮助lichess提高使用向上键或向下键
thankYou=谢谢!
startTraining=开始训练
continueTraining=继续训练
retryThisPuzzle=重试这个难题
thisPuzzleIsCorrect=这个难题是正确的,有趣的
thisPuzzleIsWrong=这个难题是错误的或无聊

View File

@ -245,7 +245,7 @@ POST /report/:id/process controllers.Report.process(id: String)
GET /api/user controllers.Api.users
GET /api/user/:id controllers.Api.user(id: String)
GET /api/game controllers.Api.games
GET /api/analysis controllers.Api.analysis
GET /api/game/:id controllers.Api.game(id: String)
GET /api/puzzle/:id controllers.Api.puzzle(id: String)
POST /api/puzzle controllers.Puzzle.importBatch

View File

@ -31,11 +31,6 @@ final class Analyser(
if (a.stalled) (AnalysisRepo remove a.id) inject none[Analysis] else fuccess(a.some)
}
def hasDone(id: String): Fu[Boolean] = getDone(id) map (_.isDefined)
def hasMany(ids: Seq[String]): Fu[Set[String]] =
$primitive[Analysis, String]($select byIds ids, "_id")(_.asOpt[String]) map (_.toSet)
def getOrGenerate(
id: String,
userId: String,
@ -84,7 +79,7 @@ final class Analyser(
game.userIds foreach { userId =>
evaluator ! lila.hub.actorApi.evaluation.Refresh(userId)
}
} inject analysis
} >>- GameRepo.setAnalysed(game.id) inject analysis
}
else fufail(s"[analysis] invalid $id")
})

View File

@ -1,71 +0,0 @@
package lila.api
import chess.format.pgn.Pgn
import chess.format.UciDump
import chess.Replay
import play.api.libs.json._
import lila.analyse.{ Analysis, AnalysisRepo }
import lila.common.PimpedJson._
import lila.db.api._
import lila.db.Implicits._
import lila.game.{ Game, GameRepo, PgnDump }
import lila.hub.actorApi.{ router => R }
private[api] final class AnalysisApi(
apiToken: String,
makeUrl: Any => Fu[String],
nbAnalysis: () => Fu[Int],
pgnDump: PgnDump) {
private def makeNb(nb: Option[Int]) = math.min(100, nb | 10)
private def makeSkip = nbAnalysis() map { nb => scala.util.Random.nextInt(nb) }
def list(nb: Option[Int], token: Option[String]): Fu[JsObject] =
if (~token != apiToken) fuccess(Json.obj("oh" -> "bummer"))
else makeSkip flatMap { skip =>
AnalysisRepo.skipping(skip, makeNb(nb)) flatMap { as =>
GameRepo games as.map(_.id) flatMap { games =>
games.map { g =>
as find (_.id == g.id) map { _ -> g }
}.flatten.map {
case (a, g) => GameRepo initialFen g.id flatMap { initialFen =>
pgnDump(g) zip makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) map {
case (pgn, url) => (g, a, url, pgn, initialFen)
}
}
}.sequenceFu map { tuples =>
Json.obj(
"list" -> JsArray(tuples map {
case (game, analysis, url, pgn, fen) => Json.obj(
"game" -> (GameApi.gameToJson(game, url, analysis.some) ++ {
fen ?? { f => Json.obj("initialFen" -> f) }
}),
"analysis" -> AnalysisApi.analysisToJson(analysis, pgn),
"uci" -> uciMovesOf(game, fen).map(_.mkString(" "))
).noNull
})
)
}
}
}
}
private def uciMovesOf(game: Game, initialFen: Option[String]): Option[List[String]] =
Replay(game.pgnMoves mkString " ", initialFen, game.variant).toOption map UciDump.apply
}
private[api] object AnalysisApi {
def analysisToJson(analysis: Analysis, pgn: Pgn) = JsArray(analysis.infoAdvices zip pgn.moves map {
case ((info, adviceOption), move) => Json.obj(
"ply" -> info.ply,
"move" -> move.san,
"eval" -> info.score.map(_.centipawns),
"mate" -> info.mate,
"variation" -> info.variation.isEmpty.fold(JsNull, info.variation mkString " "),
"comment" -> adviceOption.map(_.makeComment(true, true))
).noNull
})
}

View File

@ -14,7 +14,10 @@ case class PageData(
blindMode: Boolean)
object PageData {
val default = PageData(Nil, 0, 0, Pref.default, false)
def anon(blindMode: Boolean) = default.copy(blindMode = blindMode)
}
sealed trait Context extends lila.user.UserContextWrapper {

View File

@ -44,12 +44,6 @@ final class Env(
val gameApi = new GameApi(
makeUrl = apiUrl,
apiToken = apiToken,
isOnline = userEnv.isOnline)
val analysisApi = new AnalysisApi(
apiToken = apiToken,
makeUrl = apiUrl,
nbAnalysis = () => analyseEnv.cached.nbAnalysis,
pgnDump = pgnDump)
val puzzleApi = new PuzzleApi(

View File

@ -2,58 +2,70 @@ package lila.api
import play.api.libs.json._
import chess.format.pgn.Pgn
import lila.analyse.{ AnalysisRepo, Analysis }
import lila.common.PimpedJson._
import lila.db.api._
import lila.db.Implicits._
import lila.game.Game
import lila.game.Game.{ BSONFields => G }
import lila.game.tube.gameTube
import lila.game.{ Game, PgnDump }
import lila.hub.actorApi.{ router => R }
import makeTimeout.short
private[api] final class GameApi(
makeUrl: Any => Fu[String],
apiToken: String,
isOnline: String => Boolean) {
pgnDump: PgnDump) {
private def makeNb(nb: Option[Int]) = math.min(100, nb | 10)
def list(
username: Option[String],
rated: Option[Boolean],
analysed: Option[Boolean],
withAnalysis: Boolean,
token: Option[String],
nb: Option[Int]): Fu[JsObject] = $find($query(Json.obj(
G.status -> $gte(chess.Status.Mate.id),
G.playerUids -> username,
G.rated -> rated.map(_.fold(JsBoolean(true), $exists(false)))
).noNull) sort lila.game.Query.sortCreated, makeNb(nb)) flatMap { games =>
G.rated -> rated.map(_.fold(JsBoolean(true), $exists(false))),
G.analysed -> analysed.map(_.fold(JsBoolean(true), $exists(false)))
).noNull) sort lila.game.Query.sortCreated, makeNb(nb)) flatMap
gamesJson(withAnalysis, token) map { games =>
Json.obj("list" -> games)
}
def one(
id: String,
withAnalysis: Boolean,
token: Option[String]): Fu[Option[JsObject]] =
$find byId id map (_.toList) flatMap gamesJson(withAnalysis, token) map (_.headOption)
private def gamesJson(withAnalysis: Boolean, token: Option[String])(games: List[Game]): Fu[List[JsObject]] =
AnalysisRepo doneByIds games.map(_.id) flatMap { analysisOptions =>
(games map { g => makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) }).sequenceFu map { urls =>
val validToken = check(token)
Json.obj(
"list" -> JsArray(
games zip urls zip analysisOptions map {
case ((g, url), analysisOption) =>
GameApi.gameToJson(g, url, analysisOption,
withBlurs = validToken,
withMoveTimes = validToken)
}
)
)
(games map { g => withAnalysis ?? (pgnDump(g) map (_.some)) }).sequenceFu flatMap { pgns =>
(games map { g => makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) }).sequenceFu map { urls =>
val validToken = check(token)
games zip urls zip analysisOptions zip pgns map {
case (((g, url), analysisOption), pgnOption) =>
gameToJson(g, url, analysisOption, pgnOption,
withAnalysis = withAnalysis,
withBlurs = validToken,
withMoveTimes = validToken)
}
}
}
}
}
private def check(token: Option[String]) = token ?? (apiToken==)
}
private[api] object GameApi {
def gameToJson(
private def gameToJson(
g: Game,
url: String,
analysisOption: Option[Analysis],
pgnOption: Option[Pgn],
withAnalysis: Boolean,
withBlurs: Boolean = false,
withMoveTimes: Boolean = false) = Json.obj(
"id" -> g.id,
@ -84,6 +96,18 @@ private[api] object GameApi {
)
).noNull
}),
"analysis" -> analysisOption.ifTrue(withAnalysis).|@|(pgnOption).apply {
case (analysis, pgn) => JsArray(analysis.infoAdvices zip pgn.moves map {
case ((info, adviceOption), move) => Json.obj(
"ply" -> info.ply,
"move" -> move.san,
"eval" -> info.score.map(_.centipawns),
"mate" -> info.mate,
"variation" -> info.variation.isEmpty.fold(JsNull, info.variation mkString " "),
"comment" -> adviceOption.map(_.makeComment(true, true))
).noNull
})
},
"winner" -> g.winnerColor.map(_.name),
"url" -> url
).noNull

@ -1 +1 @@
Subproject commit 87a28a41a6a9087a3af7af597cf9fedbe24c81ce
Subproject commit 7b015665cd7d97c61c38800e10202a33d1d0beb0

View File

@ -423,6 +423,7 @@ object Game {
val castleLastMoveTime = "cl"
val moveTimes = "mt"
val rated = "ra"
val analysed = "an"
val variant = "v"
val next = "ne"
val bookmarks = "bm"

View File

@ -109,6 +109,18 @@ trait GameRepo {
def onTv(nb: Int): Fu[List[Game]] = $find($query(Json.obj(F.tvAt -> $exists(true))) sort $sort.desc(F.tvAt), nb)
def setAnalysed(id: ID) {
$update.fieldUnchecked(id, F.analysed, true)
}
private def selectAnalysed = Json.obj(F.analysed -> true)
def isAnalysed(id: ID): Fu[Boolean] =
$count.exists($select(id) ++ selectAnalysed)
def filterAnalysed(ids: Seq[String]): Fu[Set[String]] =
$primitive(($select byIds ids) ++ selectAnalysed, "_id")(_.asOpt[String]) map (_.toSet)
def incBookmarks(id: ID, value: Int) =
$update($select(id), $incBson("bm" -> value))

View File

@ -10,8 +10,7 @@ import lila.game.tube.gameTube
final class Env(
config: Config,
system: ActorSystem,
client: ElasticClient,
analyser: lila.analyse.Analyser) {
client: ElasticClient) {
private val IndexName = config getString "index"
private val TypeName = config getString "type"
@ -21,8 +20,7 @@ final class Env(
private val indexer: ActorRef = system.actorOf(Props(new Indexer(
client = client,
indexName = IndexName,
typeName = TypeName,
analyser = analyser
typeName = TypeName
)), name = IndexerName)
lazy val paginator = new lila.search.PaginatorBuilder(
@ -49,6 +47,5 @@ object Env {
lazy val current = "[boot] gameSearch" describes new Env(
config = lila.common.PlayApp loadConfig "gameSearch",
system = lila.common.PlayApp.system,
client = lila.search.Env.current.client,
analyser = lila.analyse.Env.current.analyser)
client = lila.search.Env.current.client)
}

View File

@ -6,6 +6,7 @@ import com.sksamuel.elastic4s.ElasticClient
import com.sksamuel.elastic4s.ElasticDsl._
import com.sksamuel.elastic4s.mapping.FieldType._
import lila.game.GameRepo
import lila.game.actorApi.{ InsertGame, FinishGame }
import lila.search.actorApi._
import lila.search.ElasticSearch
@ -13,8 +14,7 @@ import lila.search.ElasticSearch
private[gameSearch] final class Indexer(
client: ElasticClient,
indexName: String,
typeName: String,
analyser: lila.analyse.Analyser) extends Actor {
typeName: String) extends Actor {
context.system.lilaBus.subscribe(self, 'finishGame)
@ -28,7 +28,7 @@ private[gameSearch] final class Indexer(
case FinishGame(game, _, _) => self ! InsertGame(game)
case InsertGame(game) => if (storable(game)) {
analyser hasDone game.id foreach { analysed =>
GameRepo isAnalysed game.id foreach { analysed =>
client execute store(game, analysed)
}
}
@ -68,7 +68,7 @@ private[gameSearch] final class Indexer(
$enumerate.bulk[Option[lila.game.Game]]($query.all, batchSize) { gameOptions =>
val games = gameOptions.flatten filter storable
val nbGames = games.size
(analyser hasMany games.map(_.id).toSeq flatMap { analysedIds =>
(GameRepo filterAnalysed games.map(_.id).toSeq flatMap { analysedIds =>
client bulk {
games.map { g => store(g, analysedIds(g.id)) }: _*
}

View File

@ -28,7 +28,7 @@ case class Modlog(
case a => a
}
override def toString = s"$mod $showAction $user"
override def toString = s"$mod $showAction ${~user}"
}
object Modlog {

View File

@ -128,14 +128,14 @@ private[round] final class Round(
case Deploy(RemindDeployPost, _) => handle { game =>
game.clock.filter(_ => game.playable) ?? { clock =>
import chess.Color
val freeSeconds = 12
val freeSeconds = 15
val newClock = clock.giveTime(Color.White, freeSeconds).giveTime(Color.Black, freeSeconds)
val progress = (game withClock newClock) + Event.Clock(newClock)
messenger.system(game, (_.untranslated("Deploy in progress")))
messenger.system(game, (_.untranslated("Sorry for the inconvenience!")))
Color.all.foreach { c =>
messenger.system(game, (_.untranslated(s"$c + $freeSeconds seconds")))
}
messenger.system(game, (_.untranslated("Sorry for the inconvenience!")))
GameRepo save progress inject progress.events
}
}

View File

@ -180,10 +180,10 @@ trait UserRepo {
_ ?? (data => data.enabled && data.compare(password))
}
def create(username: String, password: String): Fu[Option[User]] =
def create(username: String, password: String, blind: Boolean): Fu[Option[User]] =
!nameExists(username) flatMap {
_ ?? {
$insert.bson(newUser(username, password)) >> named(normalize(username))
$insert.bson(newUser(username, password, blind)) >> named(normalize(username))
}
}
@ -276,7 +276,7 @@ trait UserRepo {
def filterByEngine(userIds: List[String]): Fu[List[String]] =
$primitive(Json.obj("_id" -> $in(userIds)) ++ engineSelect(true), F.username)(_.asOpt[String])
private def newUser(username: String, password: String) = {
private def newUser(username: String, password: String, blind: Boolean) = {
val salt = ornicar.scalalib.Random nextStringUppercase 32
val perfs = Perfs.default
@ -295,7 +295,9 @@ trait UserRepo {
F.count -> Count.default,
F.enabled -> true,
F.createdAt -> DateTime.now,
F.seenAt -> DateTime.now)
F.seenAt -> DateTime.now) ++ {
if (blind) BSONDocument("blind" -> true) else BSONDocument()
}
}
def artificialSetPassword(id: String, password: String) =

View File

@ -118,7 +118,7 @@ object ApplicationBuild extends Build {
play.api, RM, PRM)
)
lazy val gameSearch = project("gameSearch", Seq(common, hub, chess, search, game, analyse)).settings(
lazy val gameSearch = project("gameSearch", Seq(common, hub, chess, search, game)).settings(
libraryDependencies ++= provided(
play.api, RM, PRM, elastic4s)
)

View File

@ -506,7 +506,7 @@ var storage = {
});
}
$('#lichess').on('click', 'a.socket-link:not(.disabled)', function() {
$('#lichess').on('click', '.socket-link:not(.disabled)', function() {
lichess.socket.send($(this).data('msg'), $(this).data('data'));
});
@ -2517,7 +2517,7 @@ var storage = {
return $plot.data('hook', hook).powerTip({
fadeInTime: 0,
fadeOutTime: 0,
placement: 'ne',
placement: hook.rating > 2200 ? 'se' : 'ne',
mouseOnToPopup: true,
closeDelay: 200,
intentPollInterval: 50,

View File

@ -11,6 +11,7 @@ $(function() {
bq: 'q'
};
$wrap.find('.castling input').on('change', onChange);
var assetUrl = $wrap.data('asset-url');
function getRich() {
return toRich(board.fen());
@ -38,7 +39,7 @@ $(function() {
});
}
var pieceTheme = 'http://' + document.domain.replace(/^\w+/, 'static') + '/assets/piece/' + $('body').data('piece-set') + '/{piece}.svg';
var pieceTheme = assetUrl + '/assets/piece/' + $('body').data('piece-set') + '/{piece}.svg';
board = new ChessBoard('chessboard', {
position: toBase($('#chessboard').data('fen')) || 'start',
draggable: true,

View File

@ -513,7 +513,7 @@ div.lichess_table div.username.connected span.status:before {
div.lichess_table div.username a:hover {
text-decoration: none;
}
div.lichess_table a.button.strong {
div.lichess_table .button.strong {
padding-top: 1em;
padding-bottom: 1em;
font-weight: bold;
@ -622,23 +622,26 @@ div.lichess_control div.rematch_alert .button {
div.lichess_control div.rematch_wait {
margin-bottom: 1em;
}
div.lichess_control.icons a.button {
div.lichess_control.icons .button {
font-size: 16px;
height: 22px;
height: 34px;
padding: 0 10px;
margin: 0 3px;
}
div.lichess_control.icons a.takeback span:before {
vertical-align: -7px;
}
div.lichess_control.icons a.draw span:before {
div.lichess_control.icons .takeback span:before {
display: inline-block;
transform: rotate(-90deg);
transform: translateY(1px);
}
div.lichess_control.icons .draw span:before {
display: inline-block;
transform: translateY(-1px) rotate(-90deg);
-webkit-transform: rotate(-90deg);
vertical-align: -4px;
}
div.lichess_control.icons a.resign span:before {
vertical-align: -6px;
div.lichess_control.icons .resign span:before {
display: inline-block;
transform: translateY(1px);
}
div.lichess_control.buttons a.button {
div.lichess_control.buttons .button {
display: block;
margin-bottom: 0.7em;
text-align: center;

View File

@ -513,6 +513,10 @@ a.revert-underline {
a.revert-underline:hover {
text-decoration: underline;
}
button.a {
border: none;
background: none;
}
strong {
font-weight: bold;
}
@ -841,7 +845,7 @@ body.tight div.side_menu a {
#reconnecting,
a.goto_nav,
#top a.toggle,
a#sound_state {
#sound_state {
text-decoration: none;
font-size: 13px;
height: 24px;
@ -877,7 +881,7 @@ body.offline #nb_connected_players {
}
#top a.goto_nav:hover,
#top a.toggle:hover,
a#sound_state:hover {
#sound_state:hover {
color: #303030;
border-color: #808080;
}
@ -1723,7 +1727,7 @@ div.game_config div.color_submits button.random span {
}
#hooks_wrap table.list thead th {
cursor: pointer;
padding: 13px 12px;
padding: 12.5px 12px;
font-weight: lighter;
border-bottom: 1px solid #ccc;
}

View File

@ -1,3 +1,21 @@
form.color .color span {
display: block;
width: 30px;
height: 30px;
background-size: 30px 30px;
background-repeat: no-repeat;
padding: 0;
margin: 10px 15px;
}
form.color .color_1 span {
background-image: url(../piece/cburnett/wK.svg);
}
form.color .color_2 span {
background-image: url(../piece/cburnett/wbK.svg);
}
form.color .color_3 span {
background-image: url(../piece/cburnett/bK.svg);
}
#trainer .lichess_board {
display: none;
border: 1px solid #c0c0c0;

View File

@ -140,7 +140,7 @@ body.dark #top a.toggle:hover,
body.dark #top .dropdown,
body.dark #top a.goto_nav.current,
body.dark #top a.goto_nav:hover,
body.dark a#sound_state:hover {
body.dark #sound_state:hover {
color: #c0c0c0;
}
body.dark #site_title,