Merge branch 'master' into ScalaEvaluator

* master: (23 commits)
  add missing font
  realtime replay
  autoplay fast and slow
  analysis autoplay
  analysis action menu
  fix analysis board from FEN
  fix swiss tournament UI
  upgrade chess module
  lv "latviešu valoda" translation #12512. Author: krauzand. fixed translation errors
  send a reload message on failed force resign
  link to actual game initial position
  show previous moves in correspondence analysis screen - resolves #274
  fix integer overflow in correspondence games - fixes #278
  don't cancel draw offer on move - resolves #277
  don't batch single messages
  cancel premove on takeback - fixes #276
  he "עִבְרִית" translation #12508. Author: _PurelySmart.
  lv "latviešu valoda" translation #12507. Author: krauzand.
  hu "Magyar" translation #12506. Author: WMage.
  fa "فارسی" translation #12502. Author: gambi.
  ...
This commit is contained in:
Thibault Duplessis 2015-01-30 01:20:35 +01:00
commit 4819f24869
38 changed files with 353 additions and 128 deletions

View file

@ -8,10 +8,10 @@ import play.api.mvc._
import play.twirl.api.Html
import lila.analyse.{ Analysis, TimeChart, AdvantageChart, Accuracy }
import lila.evaluation.GameResults
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.evaluation.GameResults
import lila.game.{ Pov, Game => GameModel, GameRepo, PgnDump }
import lila.hub.actorApi.map.Tell
import lila.round.actorApi.AnalysisAvailable
@ -60,17 +60,21 @@ object Analyse extends LilaController {
GameRepo initialFen pov.game.id flatMap { initialFen =>
(env.analyser get pov.game.id) zip
(pov.game.tournamentId ?? lila.tournament.TournamentRepo.byId) zip
Env.game.crosstableApi(pov.game) flatMap {
case ((analysis, tour), crosstable) =>
val division =
if (HTTPRequest.isBot(ctx.req)) divider.empty
else divider(pov.game, initialFen)
val pgn = Env.game.pgnDump(pov.game, initialFen)
val assessResults = if (isGranted(_.MarkEngine)) Env.mod.assessApi.getResultsByGameId(pov.game.id)
else fuccess(GameResults(None, None))
assessResults flatMap {
results =>
Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = none, analysis.map(pgn -> _), initialFen = initialFen.some) map { data => {
Env.game.crosstableApi(pov.game) flatMap {
case ((analysis, tour), crosstable) =>
val division =
if (HTTPRequest.isBot(ctx.req)) divider.empty
else divider(pov.game, initialFen)
val pgn = Env.game.pgnDump(pov.game, initialFen)
val assessResults = if (isGranted(_.MarkEngine)) Env.mod.assessApi.getResultsByGameId(pov.game.id)
else fuccess(GameResults(None, None))
assessResults flatMap {
results =>
Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion,
tv = none,
analysis.map(pgn -> _),
initialFen = initialFen.some,
withMoveTimes = true) map { data =>
Ok(html.analyse.replay(
pov,
data,
@ -83,8 +87,8 @@ object Analyse extends LilaController {
userTv,
division,
results))
} }
}
}
}
}
}
}
}

View file

@ -18,9 +18,9 @@ object UserAnalysis extends LilaController with BaseGame {
val decodedFen = fenStr.map { java.net.URLDecoder.decode(_, "UTF-8").trim }.filter(_.nonEmpty)
val situation = (decodedFen flatMap Forsyth.<<<) | SituationPlus(Situation(chess.variant.Standard), 1)
val pov = makePov(situation)
val data = Env.round.jsonView.userAnalysisJson(pov, ctx.pref)
makeListMenu map { listMenu =>
Ok(html.board.userAnalysis(listMenu, data, none))
Env.round.jsonView.userAnalysisJson(pov, ctx.pref) zip makeListMenu map {
case (data, listMenu) =>
Ok(html.board.userAnalysis(listMenu, data, none))
}
}
@ -42,9 +42,9 @@ object UserAnalysis extends LilaController with BaseGame {
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)
Env.round.jsonView.userAnalysisJson(pov, ctx.pref) zip makeListMenu map {
case (data, listMenu) =>
html.board.userAnalysis(listMenu, data, pov.some)
}
}
}

View file

@ -14,7 +14,10 @@ data: @Html(play.api.libs.json.Json.stringify(data)),
routes: roundRoutes.controllers,
i18n: @round.jsI18n()
};
lichess.user_analysis.data.inGame = @pov.isDefined;
@pov.map { p =>
lichess.user_analysis.data.inGame = true;
lichess.user_analysis.data.path = "@p.game.turns";
}
}
}

View file

@ -20,7 +20,7 @@
}
} •
@if(game.variant.exotic) {
@variantLink(game.variant, (if (game.variant == chess.variant.KingOfTheHill) game.variant.shortName else game.variant.name).toUpperCase, cssClass = "hint--top")
@variantLink(game.variant, (if (game.variant == chess.variant.KingOfTheHill) game.variant.shortName else game.variant.name).toUpperCase, cssClass = "hint--top", initialFen = initialFen)
} else {
@game.perfType.map { pt =>
<span class="hint--top" data-hint="@pt.title">@pt.name.toUpperCase</span>

View file

@ -1,4 +1,4 @@
@(variant: chess.variant.Variant, name: String, hintAsTitle: Boolean = false, cssClass: String = "hint--bottom")
@(variant: chess.variant.Variant, name: String, hintAsTitle: Boolean = false, cssClass: String = "hint--bottom", initialFen: Option[String] = None)
@url = {
@variant match {
case chess.variant.Standard => {https://en.wikipedia.org/wiki/Chess}
@ -6,7 +6,7 @@ case chess.variant.Chess960 => {https://en.wikipedia.org/wiki/Chess960}
case chess.variant.KingOfTheHill => {@routes.Page.kingOfTheHill}
case chess.variant.ThreeCheck => {http://en.wikipedia.org/wiki/Three-check_chess}
case chess.variant.Antichess => {http://en.wikipedia.org/wiki/Losing_chess}
case chess.variant.FromPosition => {@routes.Editor.index}
case chess.variant.FromPosition => {@routes.Editor.index?fen=@initialFen}
case chess.variant.Atomic => {http://www.freechess.org/Help/HelpFiles/atomic.html}
case _ => {}
}

View file

@ -382,7 +382,7 @@ gameBehavior=Chování hry
premovesPlayingDuringOpponentTurn=Předtahy (hraní během protivníkova tahu)
takebacksWithOpponentApproval=Vrácení tahu (s protivníkovým souhlasem)
promoteToQueenAutomatically=Povýšit na královnu automaticky
claimDrawOnThreefoldRepetitionAutomatically=Vyžádat remízu při %trojím opakování pozic%s automaticky
claimDrawOnThreefoldRepetitionAutomatically=Vyžádat remízu při %strojím opakování pozic%s automaticky
privacy=Soukromí
letOtherPlayersFollowYou=Umožnit ostatním hráčům následovat
letOtherPlayersChallengeYou=Umožnit ostatním hráčům vyzvat

View file

@ -347,3 +347,23 @@ blog=Blog
questionsAndAnswers=Spørgsmål & svar
notes=Noter
materialDifference=Materialeforskel
chessClock=Ur
tenthsOfSeconds=Tiendedele sekunder
never=Aldrig
soundWhenTimeGetsCritical=Lyd, når tiden bliver kritisk
gameBehavior=Spil adfærd
letOtherPlayersFollowYou=Lad andre spillere følge dig
letOtherPlayersChallengeYou=Lad andre spillere udfordre dig
sound=Lys
yourPreferencesHaveBeenSaved=Dine præferencer er blevet gemt.
none=Ingen
fast=Hurtigt
normal=Normalt
slow=Langsomt
insideTheBoard=På brættet
outsideTheBoard=Udenfor brættet
always=Altid
difficultyEasy=Nemt
difficultyNormal=Normalt
difficultyHard=Svært
timeline=Tidslinje

View file

@ -1,7 +1,7 @@
playWithAFriend=بازی دوستانه
playWithTheMachine=بازی با رایانه
toInviteSomeoneToPlayGiveThisUrl=فراخواندن برای بازی با استفاده از این لینک
gameOver=بازنده شدید
toInviteSomeoneToPlayGiveThisUrl=%برای دعوت کردن حریف این پیوند را برای او بفرستید%
gameOver=%پایان بازی%
waitingForOpponent=منتظر حریف
waiting=انتظار
yourTurn=نوبت شماست
@ -361,6 +361,7 @@ materialDifference=تفاوت ماده
closeAccount=بستن اکانت
closeYourAccount=بستن اکانت شما
changedMindDoNotCloseAccount=نظرم را عوض کردم اکانتم را نمی بندم
closeAccountExplanation=در صورت بستن حساب کاربری دیگر قادر نخواهید بود آنرا بازگزدانید. و صفحه کاربری از دسترس خارج خواهد شد . آیا اطمینان دارید ؟؟
thisAccountIsClosed=این اکانت بسته شده است
invalidUsernameOrPassword=نام کاربری و رمز عبور نادرست است
emailMeALink=یک لینک به من ایمیل کنید
@ -385,6 +386,7 @@ privacy=خلوت
letOtherPlayersFollowYou=بقیه بازیکنان شما را دنبال کنند
letOtherPlayersChallengeYou=بقیه بازیکنان با شما بازی کنند
sound=صدا
soundControlInTheTopBarOfEveryPage=%کنترل صدا در همه صفحات بالا سمت راست قرار دارد%
yourPreferencesHaveBeenSaved=تغییرات شما ذخیره شده است
none=هیچ کدام
fast=سریع

View file

@ -310,20 +310,38 @@ nbGamesInPlay=%s משחקים בתהליך
automaticallyProceedToNextGameAfterMoving=המשך אוטומטית למשחק הבא אחרי מהלך
autoSwitch=החלפה אוטומטית
openingId=פתיחה%s
findNbStrongMoves=מצא %s מהלכים חזקים
thisMoveGivesYourOpponentTheAdvantage=מהלך זה נותן ליריבך את היתרון
openingFailed=פתיחה נכשלה
openingSolved=פתיחה נפתרה
recentlyPlayedOpenings=פתיחות אחרונות ששוחקו
puzzles=פאזלים
coordinates=קואורדינטות
openings=פתיחות
latestUpdates=עדכונים אחרונים
tournamentWinners=מנצחי הטורנירים
name=שם
description=תיאור
no=לא
yes=כן
help=:עזרה
createANewTopic=צור נושא חדש
topics=נושאים
posts=אשכולות
lastPost=אשכול אחרון
views=צפיות
replies=תגובות
replyToThisTopic=הגב לאשכול זה
reply=הגב
message=הודעה
createTheTopic=צור אשכול
reportAUser=דווח על משתמש
user=משתמש
reason=סיבה
whatIsIheMatter=מה הבעיה?
cheat=רמאות
insult=העלבה
troll=טרול
other=אחר
donate=תרום
blog=בלוג

View file

@ -338,18 +338,20 @@ message=Üzenet
createTheTopic=Topik létrehozása
reportAUser=Jelenteni egy felhasználót
user=Felhasználó
reason=Indoklás
whatIsIheMatter=Mi újság
cheat=Csevegő
reason=Ok
whatIsIheMatter=Probléma meghatározása
cheat=Csalás
insult=Sértegetés
troll=Trollkodás
other=Más
other=Egyéb
reportDescriptionHelp=Játék(ok) linkje és a játékos viselkedésében tapasztalt probléma kifejtése
by=%s által
thisTopicIsNowClosed=Ez a téma jelenleg lezárt
theming=Kinézet
donate=Támogatás
blog=Blog
map=Térkép
realTimeWorldMapOfChessMoves=Valós idejő földtérkép a sakk-lépésekről
questionsAndAnswers=Kérdések & Válaszok
notes=Jegyzetek
typePrivateNotesHere=Ide írd a saját jegyzeteidet
@ -359,6 +361,7 @@ materialDifference=Különböző anyagok
closeAccount=Fiók zárolása
closeYourAccount=Fiók zárolása
changedMindDoNotCloseAccount=Meggondoltam magam, mégsem zárolom a fiókomat
closeAccountExplanation=Biztos hogy zárolod a fiókodat? A zárolás végleges. Nem leszel képes bejelentkezni, és a profilod sem lesz elérhető.
thisAccountIsClosed=Ez a fiók zárolva van
invalidUsernameOrPassword=Érvénytelen felhasználónév vagy jelszó
emailMeALink=Link küldése E-mailban
@ -373,9 +376,12 @@ tenthsOfSeconds=Tizedmásodpercek
never=Soha
whenTimeRemainingLessThanTenSeconds=Amikor a hátralevő idő < 10 másodperc
horizontalGreenProgressBars=Vízszintes zöld folyamatjelző
soundWhenTimeGetsCritical=Figyelmeztető hang, mikor a hátralevő idő kritikus alá csökken
gameBehavior=Játék működése
premovesPlayingDuringOpponentTurn=Előre meghatározott lépések (Lépés meghatározása, míg az ellenfél van soron)
takebacksWithOpponentApproval=Visszalépés (Ellenfél beleegyezésével)
promoteToQueenAutomatically=Bejutott paraszt automatikus királynőre cseréje
privacy=Személyes
letOtherPlayersFollowYou=Követésed engedélyezése a többi játékosnak
letOtherPlayersChallengeYou=Kihívásod engedélyezése a többi játékosnak
sound=Hangok

View file

@ -233,7 +233,7 @@ xToYMinutes=%s līdz %s minūtes
textIsTooShort=Teksts ir par īsu.
textIsTooLong=Teksts ir par garu.
required=Obligāts.
openTournaments=Atvērt turnīrus
openTournaments=Atvērtie turnīri
duration=Ilgums
winner=Uzvarētājs
standing=Pozīcija
@ -339,23 +339,23 @@ createTheTopic=Izveidot jaunu tematu
reportAUser=Ziņot lietotājam
user=Lietotājs
reason=Cēlonis
whatIsIheMatter=Kāds ir Jūsu jautājums?
cheat=Apkrāpšana
insult=Aizvainišana
troll=Trollis
whatIsIheMatter=Kas par lietu?
cheat=Krāpšanās
insult=Apvainošana
troll=Troļļošana
other=Cits
reportDescriptionHelp=Ielikt atsauci spēlē un izskaidrot kas nav kārtībā lietotāja uzvedībā.
reportDescriptionHelp=Iekopē saiti uz spēli un paskaidro, kas nav kārtībā ar lietotāja uzvedību.
by=No %s
thisTopicIsNowClosed=Šis temats tagad ir slēgts!
thisTopicIsNowClosed=Šis temats tagad ir slēgts.
theming=Tēmas
donate=Ziedot
blog=Blogs
map=Karte
realTimeWorldMapOfChessMoves=Šahveida gaitu karte reālā laikā
realTimeWorldMapOfChessMoves=Šaha gājienu reāllaika pasaules karte
questionsAndAnswers=Jautājumi un Atbildes
notes=Piezīmes
typePrivateNotesHere=Uzrakstīt privāto piezīmi šeit
gameDisplay=Spēļu displejs
typePrivateNotesHere=Šeit vari rakstīt savas slepenās piezīmes
gameDisplay=Spēļu ekrāns
pieceAnimation=Figūru animācija
materialDifference=Materiāla starpība
closeAccount=Slēgt kontu
@ -415,3 +415,5 @@ allInformationIsPublicAndOptional=Visa informācija ir publiska un nav obligāta
yourCityRegionOrDepartment=Tava pilsēta, novads vai nodaļa.
biographyDescription=Pastāsti par sevi, kāpēc tev patīk šahs, savas mīļākās atklātes, spēles, šahistus ...
maximumNbCharacters=Maksimums: %s simboli.
blocks=%s bloķētie
listBlockedPlayers=Parādīt bloķēto spēlētāju sarakstu

View file

@ -31,9 +31,12 @@ private[api] final class RoundApi(
def watcher(pov: Pov, apiVersion: Int, tv: Option[Boolean],
analysis: Option[(Pgn, Analysis)] = None,
initialFen: Option[Option[String]] = None)(implicit ctx: Context): Fu[JsObject] =
initialFen: Option[Option[String]] = None,
withMoveTimes: Boolean = false)(implicit ctx: Context): Fu[JsObject] =
jsonView.watcherJson(pov, ctx.pref, apiVersion, ctx.me, tv,
withBlurs = ctx.me ?? Granter(_.ViewBlurs), initialFen = initialFen) zip
withBlurs = ctx.me ?? Granter(_.ViewBlurs),
initialFen = initialFen,
withMoveTimes = withMoveTimes) zip
(pov.game.tournamentId ?? TournamentRepo.byId) zip
(ctx.me ?? (me => noteApi.get(pov.gameId, me.id))) map {
case ((json, tourOption), note) => (

@ -1 +1 @@
Subproject commit acbe2c7a81c82ef078566eddb6782fc4cdfce738
Subproject commit a4c01151936ffdd12faeccfd259f03bdc8cafcc6

View file

@ -90,7 +90,7 @@ case class Game(
} orElse updatedAt.map(_.getMillis / 100)
private def lastMoveTimeDate: Option[DateTime] = castleLastMoveTime.lastMoveTime map { lmt =>
createdAt plusMillis (lmt * 100)
createdAt plus (lmt * 100l)
} orElse updatedAt
def updatedAtOrCreatedAt = updatedAt | createdAt

View file

@ -138,7 +138,8 @@ final class JsonView(
user: Option[User],
tv: Option[Boolean],
withBlurs: Boolean,
initialFen: Option[Option[String]] = None) =
initialFen: Option[Option[String]] = None,
withMoveTimes: Boolean) =
initialFen.fold(GameRepo initialFen pov.game)(fuccess) zip
getSocketStatus(pov.game.id) zip
getWatcherChat(pov.game, user) zip
@ -163,6 +164,7 @@ final class JsonView(
"rematch" -> game.next,
"source" -> game.source.map(sourceJson),
"moves" -> game.pgnMoves.mkString(" "),
"moveTimes" -> withMoveTimes.option(game.moveTimes),
"opening" -> game.opening.map { o =>
Json.obj(
"code" -> o.code,
@ -224,31 +226,37 @@ final class JsonView(
).noNull
}
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)
}
def userAnalysisJson(pov: Pov, pref: Pref) =
(pov.game.pgnMoves.nonEmpty ?? GameRepo.initialFen(pov.game)) map { initialFen =>
import pov._
val fen = Forsyth >> game.toChess
Json.obj(
"game" -> Json.obj(
"id" -> gameId,
"variant" -> variantJson(game.variant),
"initialFen" -> {
if (pov.game.pgnMoves.isEmpty) fen
else (initialFen | chess.format.Forsyth.initial)
},
"fen" -> fen,
"moves" -> game.pgnMoves.mkString(" "),
"turns" -> game.turns,
"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)

View file

@ -38,8 +38,8 @@ private[round] final class Player(
case Some(color) => round ! Cheat(color)
case None =>
if (progress.game.playableByAi) round ! AiPlay
if (game.player.isOfferingDraw) round ! DrawNo(game.player.id)
if (game.player.isProposingTakeback) round ! TakebackNo(game.player.id)
if (pov.opponent.isOfferingDraw) round ! DrawNo(pov.player.id)
if (pov.player.isProposingTakeback) round ! TakebackNo(pov.player.id)
} inject progress.events
})
}

View file

@ -75,7 +75,7 @@ private[round] final class Round(
(pov.game.resignable && !pov.game.hasAi && pov.game.hasClock) ?? {
socketHub ? Ask(pov.gameId, IsGone(!pov.color)) flatMap {
case true => finisher(pov.game, _.Timeout, Some(pov.color))
case _ => fufail("[round] cannot force resign of " + pov)
case _ => fuccess(List(Event.Reload))
}
}
}
@ -88,7 +88,7 @@ private[round] final class Round(
(pov.game.drawable && !pov.game.hasAi && pov.game.hasClock) ?? {
socketHub ? Ask(pov.gameId, IsGone(!pov.color)) flatMap {
case true => finisher(pov.game, _.Timeout, None)
case _ => fufail("[round] cannot force draw of " + pov)
case _ => fuccess(List(Event.Reload))
}
}
}

View file

@ -188,8 +188,10 @@ private[round] final class Socket(
}
def batch(member: Member, vevents: List[VersionedEvent]) {
if (vevents.nonEmpty) {
member push makeMessage("b", vevents map (_ jsFor member))
vevents match {
case Nil =>
case List(one) => member push one.jsFor(member)
case many => member push makeMessage("b", many map (_ jsFor member))
}
}

Binary file not shown.

View file

@ -98,4 +98,5 @@
<glyph unicode="&#96;" d="M228 359c5 0 9 1 14 2 2 0 4 1 5 3 1 2 2 4 2 6l-24 135c-1 5-5 8-10 7-42-7-70-47-62-89 6-37 38-64 75-64z m-18 134l20-116-2-1c-29 0-53 21-58 50-6 29 12 58 40 67z m154-132c5-1 9-2 14-2 37 0 69 27 75 64 8 42-20 82-62 89-2 0-4 0-6-2-2-1-3-3-4-5l-23-135c-1-4 2-9 6-9z m32 132c28-9 46-38 41-67-6-29-31-51-61-49z m99-374l-94 0 6 33c5-3 10-6 16-7 19-4 37 9 40 28l14 90c3 15-1 31-10 44-9 13-23 22-38 25l-67 12c-3 0-5 0-7-2-2-1-3-3-3-5l-31-177c-1-4 2-9 6-9 5-1 8-6 7-10l-3-22-57 0-2 22c-1 4 2 9 7 10 2 0 4 1 5 3 2 2 2 4 2 6l-31 177c-1 5-6 8-10 7l-68-12c-32-6-54-37-48-69l15-89c2-9 7-17 14-22 8-6 17-8 26-6 6 1 11 4 15 7l6-34-43 0 0 9c0 5-4 9-8 9-5 0-9-4-9-9l0-9-128 0c-13 0-15-18-15-34 0-15 2-34 15-34l128 0 0-25c0-5 4-9 9-9 4 0 8 4 8 9l0 25 56 0 4-23c3-17 18-28 34-28 2 0 4 0 6 0 18 4 31 21 28 40l-2 11 31 0-2-11c-2-9 0-18 5-26 5-7 13-12 22-14 2 0 4 0 6 0 17 0 31 12 34 28l4 23 73 0c25 0 43 39 43 60 0 5-4 8-9 8z m-284 65l0 1-10 58c-1 5-5 8-10 7-4 0-8-5-7-9l10-58 0 0 0-1c2-9-4-18-13-19-5-1-9 0-13 3-4 2-6 6-7 11l-15 89c-4 23 11 45 34 49l59 11 29-162c-10-5-15-15-13-26l2-19-35 0z m-117-82l17 0 0-34-17 0z m-17-34l-17 0 0 34 17 0z m-57 34l23 0 0-34-23 0c-2 7-2 27 0 34z m108-34l0 34 17 0 0-34z m145-31c1-9-5-18-14-20-9-2-18 5-20 14l-3 20 34 0z m-111 31l0 34 166 0-6-34z m205-37c-2-9-10-16-20-14-4 1-8 3-11 7-2 4-3 8-3 13l18 101c2 11-3 21-12 26l28 162 59-11c11-2 21-8 27-17 7-10 9-21 7-32l-14-91c-2-9-10-15-20-13-4 0-8 3-11 7-2 3-3 8-3 12l0 1 11 59c0 4-3 9-7 10-5 1-9-3-10-7l-11-61 0-1z m94 37l-70 0 7 34 87 0c-3-14-14-34-24-34z"/>
<glyph unicode="&#64;" d="M223 464c-63-3-97-16-105-38-1-3-2-7-2-17-1-20-3-35-7-51-6-22-10-31-39-72-15-22-22-42-22-59 0-21 7-40 22-55 12-12 29-21 46-25 10-1 26-1 35 1 16 3 31 10 45 21 3 1 5 3 5 3 0 0 0-4 0-8 0-16 2-28 7-37 2-4 4-7 10-13 4-4 10-9 14-12l6-4 0-9 0-8-14-8-13-9-1-12 0-13 13 0 13 0 4-12 4-13 23 0 5 13 4 12 13 0 12 0 0 13 0 12-14 9-13 8 0 8 0 9 5 4c8 6 21 18 24 23 4 7 8 23 8 37 1 9 1 11 2 9 1-1 14-10 21-13 15-8 28-12 45-12 20 0 36 5 50 15 7 4 19 17 23 24 13 20 16 44 8 66-5 14-12 27-27 48-13 18-14 20-19 29-11 22-16 46-18 80-1 19-2 22-9 29-8 9-20 15-40 19-27 7-79 10-124 8z m-9-56l42-4 42 4c40 3 46 3 52 2 8-2 10-10 3-15-11-8-49-12-105-11-46 1-77 4-89 11-4 2-5 4-5 7 1 5 4 7 11 8 4 1 6 1 49-2z m-20-78c12-1 40-5 53-8l8-1 13 2c27 5 50 7 68 8 14 1 28-1 37-5 6-3 24-21 36-36 10-15 21-35 26-47 2-8 2-8 2-18 0-11-1-19-6-27-9-18-28-27-53-28-4 0-10 1-13 1-19 4-38 15-57 34-10 10-16 18-22 30-8 16-12 36-12 55 0 10-4 13-13 15-8 1-17-1-21-4-2-3-3-5-3-12 0-23-8-49-20-68-14-20-37-39-59-47-14-5-36-5-50 0-12 4-22 12-28 23-3 7-5 11-6 22-1 11-1 16 3 26 7 20 26 48 47 70 10 9 13 12 23 14 10 3 26 3 47 1z m-37-18c-8-1-10-2-20-10-14-10-32-32-41-50-5-10-6-15-7-24 0-12 2-20 10-28 5-5 11-8 21-11 8-2 23-2 31 0 15 5 27 12 40 25 17 19 27 42 29 73 1 10 1 11 0 14-3 5-9 9-20 11-7 1-32 1-43 0z m156 0c-13-2-19-6-22-12-1-3-1-4-1-12 2-21 6-34 13-49 5-10 11-18 21-27 12-12 23-19 36-23 5-1 7-1 16-1 13 0 18 1 27 5 5 3 12 9 14 14 3 5 5 16 5 22 0 19-17 46-46 70-12 10-15 12-24 13-10 1-32 1-39 0z"/>
<glyph unicode="&#94;" d="M256 480c-141 0-256-115-256-256 0 0 0-96 0-128 0-32 32-64 64-64 32 0 352 0 384 0 32 0 64 32 64 64 0 32 0 128 0 128 0 141-114 256-256 256z m48-384l-96 0c-9 0-16 7-16 16 0 9 7 16 16 16l96 0c9 0 16-7 16-16 0-9-7-16-16-16z m144 64c0-16-16-32-32-32-16 0-64 0-64 0 0 16-16 32-32 32-16 0-112 0-128 0-16 0-32-16-32-32 0 0-48 0-64 0-16 0-32 16-32 32 0 16 0 180 0 180 39 64 110 108 192 108 82 0 153-44 192-108 0 0 0-164 0-180z m-64 192c-16 0-240 0-256 0-16 0-32-16-32-32 0-16 0-48 0-64 0-16 16-32 32-32 16 0 240 0 256 0 16 0 32 16 32 32 0 16 0 48 0 64 0 16-16 32-32 32z m0-64l-32-32-64 0-32 32-32-32-64 0-32 32 0 32 32 0 32-32 32 32 64 0 32-32 32 32 32 0z"/>
<glyph unicode="&#91;" d="M484 396c0-19-16-35-36-35l-384 0c-20 0-36 16-36 35 0 19 16 35 36 35l384 0c20 0 36-16 36-35z m0-140c0-19-16-35-36-35l-384 0c-20 0-36 16-36 35 0 19 16 35 36 35l384 0c20 0 36-16 36-35z m0-140c0-19-16-35-36-35l-384 0c-20 0-36 16-36 35 0 19 16 35 36 35l384 0c20 0 36-16 36-35z"/>
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

View file

@ -409,6 +409,10 @@ h2{font-size:18px;padding:0 0 21px 5px;margin:45px 0 0 0;text-transform:uppercas
<div class="icon icon-hubot"></div>
<input type="text" readonly="readonly" value="hubot">
</li>
<li>
<div class="icon icon-reorder"></div>
<input type="text" readonly="readonly" value="reorder">
</li>
</ul>
<h2>Character mapping</h2>
<ul class="glyphs character-mapping">
@ -776,6 +780,10 @@ h2{font-size:18px;padding:0 0 21px 5px;margin:45px 0 0 0;text-transform:uppercas
<div data-icon="^" class="icon"></div>
<input type="text" readonly="readonly" value="^">
</li>
<li>
<div data-icon="[" class="icon"></div>
<input type="text" readonly="readonly" value="[">
</li>
</ul>
</div><script type="text/javascript">
(function() {

View file

@ -311,3 +311,6 @@
.icon-hubot:before {
content: "^";
}
.icon-reorder:before {
content: "[";
}

View file

@ -50,7 +50,7 @@ lichess.StrongSocket.defaults = {
pingDelay: 1000, // time between pong and ping
autoReconnectDelay: 1000,
lagTag: false, // jQuery object showing ping lag
ignoreUnknownMessages: false,
ignoreUnknownMessages: true,
baseUrls: ['socket.' + document.domain].concat(
($('body').data('ports') + '').split(',').map(function(port) {
return 'socket.' + document.domain + ':' + port;

View file

@ -100,12 +100,13 @@ div.fen_pgn input.fen {
div.game_control {
margin-top: 10px;
text-align: center;
white-space: nowrap
width: 244px;
white-space: nowrap;
}
div.game_control a {
font-size: 12px;
width: 25px;
height: 17px;
width: 12px;
display: inline-block;
-webkit-user-select: none;
-moz-user-select: none;
@ -113,14 +114,32 @@ div.game_control a {
margin: 0 -1px;
}
div.game_control .jumps {
display: inline-block;
margin-right: 10px;
}
.lichess_ground .replay {
.lichess_ground .replay,
.lichess_ground .action_menu {
height: calc(100% - 40px);
overflow: auto;
overflow-x: hidden;
border: 1px solid #ccc;
}
.lichess_ground .action_menu {
border-color: transparent;
}
.lichess_ground .action_menu .inner {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.lichess_ground .action_menu .button {
display: block;
margin: 15px 0;
padding: 10px;
}
.lichess_ground .action_menu .button::before {
font-size: 2em;
}
.lichess_ground .replay .turn > .index {
display: inline-block;
width: 26px;

View file

@ -70,8 +70,8 @@ time {
}
@font-face {
font-family: "lichess";
src: url("../font31/fonts/lichess.eot");
src: url("../font31/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font31/fonts/lichess.woff") format("woff"), url("../font31/fonts/lichess.ttf") format("truetype"), url("../font31/fonts/lichess.svg#lichess") format("svg");
src: url("../font32/fonts/lichess.eot");
src: url("../font32/fonts/lichess.eot?#iefix") format("embedded-opentype"), url("../font32/fonts/lichess.woff") format("woff"), url("../font32/fonts/lichess.ttf") format("truetype"), url("../font32/fonts/lichess.svg#lichess") format("svg");
font-weight: normal;
font-style: normal;
}

View file

@ -0,0 +1,57 @@
var partial = require('chessground').util.partial;
var m = require('mithril');
module.exports = {
controller: function() {
this.open = false;
this.toggle = function() {
this.open = !this.open
}.bind(this);
},
view: function(ctrl) {
var flipAttrs = {};
if (ctrl.data.userAnalysis) flipAttrs.onclick = ctrl.flip;
else flipAttrs.href = ctrl.router.Round.watcher(ctrl.data.game.id, ctrl.data.opponent.color).url;
return m('div.action_menu',
m('div.inner', [
m('a.button.text[data-icon=B]', flipAttrs, ctrl.trans('flipBoard')),
m('a.button.text[data-icon=m]', {
href: ctrl.data.userAnalysis ? '/editor?fen=' + ctrl.vm.situation.fen : '/' + ctrl.data.game.id + '/edit?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, ctrl.trans('boardEditor')),
m('a.button.text[data-icon=U]', {
onclick: function() {
$.modal($('.continue_with.' + ctrl.data.game.id));
}
}, ctrl.trans('continueFromHere')), [{
name: 'fast',
delay: 1000
}, {
name: 'slow',
delay: 5000
}, {
name: 'realtime',
delay: true
}].map(function(speed) {
return m('a.button[data-icon=G]', {
class: 'text' + (ctrl.autoplay.active(speed.delay) ? ' active' : ''),
onclick: partial(ctrl.togglePlay, speed.delay)
}, 'Auto play ' + speed.name);
}),
m('div.continue_with.' + ctrl.data.game.id, [
m('a.button', {
href: ctrl.data.userAnalysis ? '/?fen=' + ctrl.vm.situation.fen + '#ai' : ctrl.router.Round.continue(ctrl.data.game.id, 'ai').url + '?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, ctrl.trans('playWithTheMachine')),
m('br'),
m('a.button', {
href: ctrl.data.userAnalysis ? '/?fen=' + ctrl.vm.situation.fen + '#friend' : ctrl.router.Round.continue(ctrl.data.game.id, 'friend').url + '?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, ctrl.trans('playWithAFriend'))
])
]));
}
};

View file

@ -0,0 +1,63 @@
var control = require('./control');
var partial = require('chessground').util.partial;
var m = require('mithril');
module.exports = function(ctrl) {
var timeout;
this.delay = null;
var move = function() {
if (control.canGoForward(ctrl)) {
var p = ctrl.vm.path;
p[p.length - 1].ply++;
ctrl.jump(p);
m.redraw();
return true;
}
this.stop();
m.redraw();
return false;
}.bind(this);
var nextDelay = function() {
if (this.delay === true) {
// in a variation
if (ctrl.vm.path.length > 1) return 2000;
return (ctrl.data.game.moveTimes[ctrl.vm.path[0].ply] * 100) || 2000;
}
return this.delay;
}.bind(this);
var schedule = function() {
timeout = setTimeout(function() {
if (move()) schedule();
}, nextDelay());
}.bind(this);
var start = function(delay) {
this.delay = delay;
this.stop();
schedule();
}.bind(this);
this.stop = function() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
}.bind(this);
this.toggle = function(delay) {
if (this.active(delay)) this.stop();
else {
if (!this.active()) move();
start(delay);
}
}.bind(this);
this.active = function(delay) {
return (!delay || delay === this.delay) && !!timeout;
}.bind(this);
};

View file

@ -15,11 +15,13 @@ function canGoForward(ctrl) {
module.exports = {
canGoForward: canGoForward,
next: function(ctrl) {
if (!canGoForward(ctrl)) return;
var p = ctrl.vm.path;
p[p.length - 1].ply++;
ctrl.jump(p);
ctrl.userJump(p);
},
prev: function(ctrl) {
@ -36,18 +38,18 @@ module.exports = {
if (p[len - 2].ply > 1) p[len - 2].ply--;
}
}
ctrl.jump(p);
ctrl.userJump(p);
},
last: function(ctrl) {
ctrl.jump([{
ctrl.userJump([{
ply: ctrl.analyse.tree[ctrl.analyse.tree.length - 1].ply,
variation: null
}]);
},
first: function(ctrl) {
ctrl.jump([{
ctrl.userJump([{
ply: 0,
variation: null
}]);

View file

@ -5,12 +5,17 @@ var analyse = require('./analyse');
var ground = require('./ground');
var keyboard = require('./keyboard');
var treePath = require('./path');
var actionMenu = require('./actionMenu').controller;
var autoplay = require('./autoplay');
var control = require('./control');
var m = require('mithril');
module.exports = function(cfg, router, i18n, onChange) {
this.data = data({}, cfg);
this.analyse = new analyse(this.data.game, this.data.analysis);
this.actionMenu = new actionMenu();
this.autoplay = new autoplay(this);
var initialPath = cfg.path ? treePath.read(cfg.path) : treePath.default();
@ -29,6 +34,11 @@ module.exports = function(cfg, router, i18n, onChange) {
});
}.bind(this);
this.togglePlay = function(delay) {
this.autoplay.toggle(delay);
this.actionMenu.open = false;
}.bind(this);
var gameVariantChessId = function() {
switch (this.data.game.variant.key) {
case 'chess960':
@ -132,8 +142,13 @@ module.exports = function(cfg, router, i18n, onChange) {
showGround();
}.bind(this);
this.userJump = function(path) {
this.autoplay.stop();
this.jump(path);
}.bind(this);
this.jumpToMain = function(ply) {
this.jump([{
this.userJump([{
ply: ply,
variation: null
}]);
@ -148,7 +163,7 @@ module.exports = function(cfg, router, i18n, onChange) {
to: dest,
promotion: (dest[1] == 1 || dest[1] == 8) ? 'q' : null
});
if (move) this.jump(this.analyse.explore(this.vm.path, move.san));
if (move) this.userJump(this.analyse.explore(this.vm.path, move.san));
else this.chessground.set(this.vm.situation);
m.redraw();
}.bind(this);

View file

@ -36,7 +36,7 @@ module.exports = function(ctrl) {
if (!ctrl.vm.comments && ctrl.vm.path.length > 1) {
path = [ctrl.vm.path[0]];
path[0].variation = null;
ctrl.jump(path);
ctrl.userJump(path);
}
m.redraw();
}));

View file

@ -7,6 +7,7 @@ var renderStatus = require('game').view.status;
var mod = require('game').view.mod;
var treePath = require('./path');
var control = require('./control');
var actionMenu = require('./actionMenu').view;
function renderEval(e) {
e = Math.round(e / 10) / 10;
@ -224,7 +225,7 @@ function renderAnalyse(ctrl) {
var path = e.target.getAttribute('data-path') || e.target.parentNode.getAttribute('data-path');
if (path) {
e.preventDefault();
ctrl.jump(treePath.read(path));
ctrl.userJump(treePath.read(path));
}
},
onclick: function(e) {
@ -266,17 +267,9 @@ function blindBoard(ctrl) {
}
function buttons(ctrl) {
var nbMoves = ctrl.data.game.moves.length;
var flipAttrs = {
'data-hint': ctrl.trans('flipBoard'),
};
if (ctrl.data.userAnalysis) flipAttrs.onclick = ctrl.flip;
else flipAttrs.href = ctrl.router.Round.watcher(ctrl.data.game.id, ctrl.data.opponent.color).url;
return [
m('div.game_control', [
m('div.jumps.hint--bottom', {
'data-hint': 'Tip: use your keyboard arrow keys!'
}, [
m('div.jumps.hint--bottom', [
['first', 'W', control.first, ],
['prev', 'Y', control.prev],
['next', 'X', control.next],
@ -294,29 +287,12 @@ function buttons(ctrl) {
}
};
})),
m('a.button.hint--bottom', flipAttrs, m('span[data-icon=B]')),
ctrl.data.inGame ? null : m('a.button.hint--bottom', {
'data-hint': ctrl.trans('boardEditor'),
href: ctrl.data.userAnalysis ? '/editor?fen=' + ctrl.vm.situation.fen : '/' + ctrl.data.game.id + '/edit?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, m('span[data-icon=m]')),
ctrl.data.inGame ? null : m('a.button.hint--bottom', {
'data-hint': ctrl.trans('continueFromHere'),
onclick: function() {
$.modal($('.continue_with.' + ctrl.data.game.id));
}
}, m('span[data-icon=U]'))
]),
m('div.continue_with.' + ctrl.data.game.id, [
m('a.button', {
href: ctrl.data.userAnalysis ? '/?fen=' + ctrl.vm.situation.fen + '#ai' : ctrl.router.Round.continue(ctrl.data.game.id, 'ai').url + '?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, ctrl.trans('playWithTheMachine')),
m('br'),
m('a.button', {
href: ctrl.data.userAnalysis ? '/?fen=' + ctrl.vm.situation.fen + '#friend' : ctrl.router.Round.continue(ctrl.data.game.id, 'friend').url + '?fen=' + ctrl.vm.situation.fen,
rel: 'nofollow'
}, ctrl.trans('playWithAFriend'))
ctrl.data.inGame ? null : m('a.button', {
onclick: ctrl.actionMenu.toggle,
class: ctrl.actionMenu.open ? 'active' : ''
}, m('span', {
'data-icon': '['
}))
])
];
}
@ -332,7 +308,7 @@ module.exports = function(ctrl) {
}, [
ctrl.data.blind ? blindBoard(ctrl) : visualBoard(ctrl),
m('div.lichess_ground',
m('div.replay', {
ctrl.actionMenu.open ? actionMenu(ctrl) : m('div.replay', {
config: function(el, isUpdate) {
autoScroll(el);
if (!isUpdate) setTimeout(partial(autoScroll, el), 100);

View file

@ -128,6 +128,11 @@ module.exports = function(opts) {
}.bind(this);
setQuietMode();
this.takebackYes = function() {
this.socket.send('takeback-yes');
this.chessground.cancelPremove();
}.bind(this);
this.moveOn = new moveOn(this, 'lichess.move_on');
this.replay = new replayCtrl(this);

View file

@ -8,7 +8,7 @@ var m = require('mithril');
module.exports = {
standard: function(ctrl, condition, icon, hint, socketMsg) {
return condition(ctrl.data) ? m('button', {
class: 'button hint--bottom',
class: 'button hint--bottom ' + socketMsg,
'data-hint': ctrl.trans(hint),
onclick: partial(ctrl.socket.send, socketMsg, null)
}, m('span', {
@ -71,7 +71,7 @@ module.exports = {
ctrl.trans('yourOpponentProposesATakeback'),
m('br'),
m('a.button.text[data-icon=E]', {
onclick: partial(ctrl.socket.send, 'takeback-yes', null)
onclick: partial(ctrl.takebackYes),
}, ctrl.trans('accept')),
m.trust('&nbsp;'),
m('a.button.text[data-icon=L]', {

View file

@ -85,7 +85,11 @@ function renderTablePlay(ctrl) {
renderReplay(ctrl.replay),
m('div.control.icons', [
button.standard(ctrl, game.abortable, 'L', 'abortGame', 'abort'),
button.standard(ctrl, game.takebackable, 'i', 'proposeATakeback', 'takeback-yes'),
game.takebackable ? m('button', {
class: 'button hint--bottom takeback-yes',
'data-hint': ctrl.trans('proposeATakeback'),
onclick: partial(ctrl.takebackYes)
}, m('span[data-icon=i]')) : null,
button.standard(ctrl, game.drawable, '2', 'offerDraw', 'draw-yes'),
button.standard(ctrl, game.resignable, 'b', 'resign', 'resign')
]),

View file

@ -2,6 +2,7 @@ var m = require('mithril');
var partial = require('chessground').util.partial;
var tournament = require('../tournament');
var util = require('./util');
var button = require('./button');
var legend = m('th.legend', [
m('span.streakstarter', 'Streak starter'),
@ -45,6 +46,9 @@ function playerTrs(ctrl, maxScore, player) {
]
}, {
tag: 'tr',
attrs: {
key: player.id + '.bar'
},
children: [
m('td', {
class: 'around-bar',