Merge branch 'master' into swiss

* master: (21 commits)
  show class menu to all titled players - for #6524
  let everyone create 3 teams per week - for #6524
  let class teachers create more teams - for #6524
  make it clearer that a player can join up to 100 teams - closes #6517
  index perf stats from secondary
  assign colors in open challenges - closes #6525
  fix socket disconnect
  tweak crosstable style
  remove dead code
  {master} tweak crosstable style
  {master} close WS on reload
  {master} FIDE can create up to 48 tournaments per day
  Move space outside link
  scalafmt
  Add a space between two sentences
  Fix translation source
  More translations for the teams
  fix typo
  Add `gameplay` string and remove some trailing spaces
  Remove LM string
  ...
swiss
Thibault Duplessis 2020-05-01 13:13:59 -06:00
commit 00eca9b56f
26 changed files with 183 additions and 120 deletions

View File

@ -27,14 +27,19 @@ final class Challenge(
}
}
def show(id: String) = Open { implicit ctx =>
def show(id: String, _color: Option[String]) = Open { implicit ctx =>
showId(id)
}
protected[controllers] def showId(id: String)(implicit ctx: Context): Fu[Result] =
protected[controllers] def showId(id: String)(
implicit ctx: Context
): Fu[Result] =
OptionFuResult(api byId id)(showChallenge(_))
protected[controllers] def showChallenge(c: ChallengeModel, error: Option[String] = None)(
protected[controllers] def showChallenge(
c: ChallengeModel,
error: Option[String] = None
)(
implicit ctx: Context
): Fu[Result] =
env.challenge version c.id flatMap { version =>
@ -54,7 +59,7 @@ final class Challenge(
}
} else
(c.challengerUserId ?? env.user.repo.named) map { user =>
Ok(html.challenge.theirs(c, json, user))
Ok(html.challenge.theirs(c, json, user, get("color") flatMap chess.Color.apply))
},
api = _ => Ok(json).fuccess
) flatMap withChallengeAnonCookie(mine && c.challengerIsAnon, c, true)
@ -69,20 +74,23 @@ final class Challenge(
private def isForMe(challenge: ChallengeModel)(implicit ctx: Context) =
challenge.destUserId.fold(true)(ctx.userId.contains)
def accept(id: String) = Open { implicit ctx =>
def accept(id: String, color: Option[String]) = Open { implicit ctx =>
OptionFuResult(api byId id) { c =>
isForMe(c) ?? api.accept(c, ctx.me, HTTPRequest sid ctx.req).flatMap {
case Some(pov) =>
negotiate(
html = Redirect(routes.Round.watcher(pov.gameId, "white")).fuccess,
api = apiVersion => env.api.roundApi.player(pov, none, apiVersion) map { Ok(_) }
) flatMap withChallengeAnonCookie(ctx.isAnon, c, false)
case None =>
negotiate(
html = Redirect(routes.Round.watcher(c.id, "white")).fuccess,
api = _ => notFoundJson("Someone else accepted the challenge")
)
}
val cc = color flatMap chess.Color.apply
isForMe(c) ?? api
.accept(c, ctx.me, HTTPRequest sid ctx.req, cc)
.flatMap {
case Some(pov) =>
negotiate(
html = Redirect(routes.Round.watcher(pov.gameId, cc.fold("white")(_.name))).fuccess,
api = apiVersion => env.api.roundApi.player(pov, none, apiVersion) map { Ok(_) }
) flatMap withChallengeAnonCookie(ctx.isAnon, c, false)
case None =>
negotiate(
html = Redirect(routes.Round.watcher(c.id, cc.fold("white")(_.name))).fuccess,
api = _ => notFoundJson("Someone else accepted the challenge")
)
}
}
}
def apiAccept(id: String) = Scoped(_.Challenge.Write, _.Bot.Play, _.Board.Play) { _ => me =>
@ -285,8 +293,10 @@ final class Challenge(
(env.challenge.api create challenge) map {
case true =>
JsonOk(
env.challenge.jsonView
.show(challenge, SocketVersion(0), none)
env.challenge.jsonView.show(challenge, SocketVersion(0), none) ++ Json.obj(
"urlWhite" -> s"${env.net.baseUrl}/${challenge.id}?color=white",
"urlBlack" -> s"${env.net.baseUrl}/${challenge.id}?color=black"
)
)
case false =>
BadRequest(jsonError("Challenge not created"))

View File

@ -198,22 +198,30 @@ final class Team(
def join(id: String) = AuthOrScoped(_.Team.Write)(
auth = implicit ctx =>
me =>
negotiate(
html = api.join(id, me) flatMap {
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
case _ => notFound(ctx)
},
api = _ =>
api.join(id, me) flatMap {
case Some(Joined(_)) => jsonOkResult.fuccess
case Some(Motivate(_)) =>
BadRequest(
jsonError("This team requires confirmation.")
).fuccess
case _ => notFoundJson("Team not found")
}
),
api countTeamsOf me flatMap { nb =>
if (nb >= TeamModel.maxJoin)
negotiate(
html = BadRequest(views.html.site.message.teamJoinLimit).fuccess,
api = _ => BadRequest(jsonError("You have joined too many teams")).fuccess
)
else
negotiate(
html = api.join(id, me) flatMap {
case Some(Joined(team)) => Redirect(routes.Team.show(team.id)).flashSuccess.fuccess
case Some(Motivate(team)) => Redirect(routes.Team.requestForm(team.id)).flashSuccess.fuccess
case _ => notFound(ctx)
},
api = _ =>
api.join(id, me) flatMap {
case Some(Joined(_)) => jsonOkResult.fuccess
case Some(Motivate(_)) =>
BadRequest(
jsonError("This team requires confirmation.")
).fuccess
case _ => notFoundJson("Team not found")
}
)
},
scoped = req =>
me =>
env.oAuth.server.fetchAppAuthor(req) flatMap {
@ -401,8 +409,9 @@ You received this message because you are part of the team lichess.org${routes.T
)
private def OnePerWeek[A <: Result](me: UserModel)(a: => Fu[A])(implicit ctx: Context): Fu[Result] =
api.hasCreatedRecently(me) flatMap { did =>
if (did && !Granter(_.ManageTeam)(me)) Forbidden(views.html.site.message.teamCreateLimit).fuccess
api.countCreatedRecently(me) flatMap { count =>
if (count > 10 || (count > 3 && !Granter(_.Teacher)(me) && !Granter(_.ManageTeam)(me)))
Forbidden(views.html.site.message.teamCreateLimit).fuccess
else a
}

View File

@ -12,7 +12,7 @@ object topnav {
if (ctx.blind) h3(name) else a(href := url)(name)
private def canSeeClasMenu(implicit ctx: Context) =
ctx.hasClas || ctx.me.exists(_.roles contains "ROLE_COACH")
ctx.hasClas || ctx.me.exists(u => u.hasTitle || u.roles.contains("ROLE_COACH"))
def apply()(implicit ctx: Context) = st.nav(id := "topnav", cls := "hover")(
st.section(

View File

@ -12,13 +12,15 @@ import controllers.routes
object bits {
def js(c: Challenge, json: play.api.libs.json.JsObject, owner: Boolean)(implicit ctx: Context) =
def js(c: Challenge, json: play.api.libs.json.JsObject, owner: Boolean, color: Option[chess.Color] = None)(
implicit ctx: Context
) =
frag(
jsTag("challenge.js", defer = true),
embedJsUnsafe(s"""lichess=window.lichess||{};customWs=true;lichess_challenge = ${safeJsonValue(
Json.obj(
"socketUrl" -> s"/challenge/${c.id}/socket/v$apiVersion",
"xhrUrl" -> routes.Challenge.show(c.id).url,
"xhrUrl" -> routes.Challenge.show(c.id, color.map(_.name)).url,
"owner" -> owner,
"data" -> json
)

View File

@ -3,6 +3,7 @@ package views.html.challenge
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.challenge.Challenge
import lila.challenge.Challenge.Status
import controllers.routes
@ -10,14 +11,15 @@ import controllers.routes
object theirs {
def apply(
c: lila.challenge.Challenge,
c: Challenge,
json: play.api.libs.json.JsObject,
user: Option[lila.user.User]
user: Option[lila.user.User],
color: Option[chess.Color]
)(implicit ctx: Context) =
views.html.base.layout(
title = challengeTitle(c),
openGraph = challengeOpenGraph(c).some,
moreJs = bits.js(c, json, false),
moreJs = bits.js(c, json, false, color),
moreCss = cssTag("challenge.page")
) {
main(cls := "page-small challenge-page challenge-theirs box box-pad")(
@ -40,15 +42,20 @@ object theirs {
c.notableInitialFen.map { fen =>
div(cls := "board-preview", views.html.game.bits.miniBoard(fen, color = !c.finalColor))
},
if (!c.mode.rated || ctx.isAuth)
if (color.map(Challenge.ColorChoice.apply).has(c.colorChoice))
badTag(
// very rare message, don't translate
s"You have the wrong color link for this open challenge. The ${color.??(_.name)} player has already joined."
)
else if (!c.mode.rated || ctx.isAuth) {
frag(
(c.mode.rated && c.unlimited) option
badTag(trans.bewareTheGameIsRatedButHasNoClock()),
postForm(cls := "accept", action := routes.Challenge.accept(c.id))(
postForm(cls := "accept", action := routes.Challenge.accept(c.id, color.map(_.name)))(
submitButton(cls := "text button button-fat", dataIcon := "G")(trans.joinTheGame())
)
)
else
} else
frag(
hr,
badTag(

View File

@ -35,6 +35,7 @@ object faq {
whyIsLichessCalledLichess.txt(),
p(
lichessCombinationLiveLightLibrePronounced(em(leechess())),
" ",
a(href := "https://www.youtube.com/watch?v=KRpPqcrdE-o")(hearItPronouncedBySpecialist())
),
p(
@ -111,7 +112,7 @@ object faq {
youCanUseOpeningBookNoEngine()
)
),
h2("Gameplay"),
h2(gameplay()),
question(
"time-controls",
howBulletBlitzEtcDecided.txt(),
@ -206,7 +207,7 @@ object faq {
p(
showYourTitle(
a(href := routes.Main.verifyTitle())(verificationForm()),
a(href := "#lm")(lichessMasterLM())
a(href := "#lm")("Lichess master (LM)")
)
)
),

View File

@ -84,6 +84,10 @@ object message {
"You have already created a team this week."
}
def teamJoinLimit(implicit ctx: Context) = apply("Cannot join the team") {
"You have already joined too many teams."
}
def authFailed(implicit ctx: Context) = apply("403 - Access denied!") {
"You tried to visit a page you're not authorized to access."
}

View File

@ -23,7 +23,7 @@ object admin {
div(cls := "page-menu__content box box-pad")(
h1(title),
postForm(cls := "leaders", action := routes.Team.leaders(t.id))(
form3.group(form("leaders"), frag("Users who can manage this team"))(
form3.group(form("leaders"), frag(usersWhoCanManageThisTeam()))(
form3.textarea(_)(rows := 2)
),
form3.actions(
@ -64,7 +64,7 @@ object admin {
implicit ctx: Context
) = {
val title = s"${t.name}message all members"
val title = s"${t.name}${messageAllMembers.txt()}"
views.html.base.layout(
title = title,
@ -81,14 +81,10 @@ object admin {
div(cls := "page-menu__content box box-pad")(
h1(title),
p(
"Send a private message to ALL members of the team.",
br,
"You can use this to call players to join a tournament or a team battle.",
br,
"Players who don't like receiving your messages might leave the team."
),
messageAllMembersLongDescription()
),
tours.nonEmpty option div(cls := "tournaments")(
p("You may want to link one of these upcoming tournaments?"),
p(youWayWantToLinkOneOfTheseTournaments()),
p(
ul(
tours.map { t =>

View File

@ -44,7 +44,7 @@ object list {
main(cls := "team-list page-menu")(
bits.menu("leader".some),
div(cls := "page-menu__content box")(
h1("Teams I lead"),
h1(teamsIlead()),
standardFlash(),
table(cls := "slist slist-pad")(
if (teams.size > 0) tbody(teams.map(bits.teamTr(_)))

View File

@ -45,7 +45,7 @@ object show {
"socketVersion" -> v.value,
"chat" -> views.html.chat.json(
chat.chat,
name = if (t.isChatFor(_.LEADERS)) "Leaders chat" else trans.chatRoom.txt(),
name = if (t.isChatFor(_.LEADERS)) leadersChat.txt() else trans.chatRoom.txt(),
timeout = chat.timeout,
public = true,
resourceId = lila.chat.Chat.ResourceId(s"team/${chat.chat.id}"),
@ -68,7 +68,7 @@ object show {
(info.mine || t.enabled) option div(cls := "team-show__content")(
div(cls := "team-show__content__col1")(
st.section(cls := "team-show__meta")(
p(teamLeaders(), ": ", fragList(t.leaders.toList.map { l =>
p(teamLeaders.pluralSame(t.leaders.size), ": ", fragList(t.leaders.toList.map { l =>
userIdLink(l.some)
}))
),

View File

@ -336,8 +336,8 @@ GET /setup/validate-fen controllers.Setup.validateFen
# Challenge
GET /challenge controllers.Challenge.all
GET /challenge/$id<\w{8}> controllers.Challenge.show(id: String)
POST /challenge/$id<\w{8}>/accept controllers.Challenge.accept(id: String)
GET /challenge/$id<\w{8}> controllers.Challenge.show(id: String, color: Option[String] ?= None)
POST /challenge/$id<\w{8}>/accept controllers.Challenge.accept(id: String, color: Option[String] ?= None)
POST /challenge/$id<\w{8}>/decline controllers.Challenge.decline(id: String)
POST /challenge/$id<\w{8}>/cancel controllers.Challenge.cancel(id: String)
POST /challenge/$id<\w{8}>/to-friend controllers.Challenge.toFriend(id: String)

View File

@ -2,7 +2,7 @@ package lila.challenge
import chess.format.FEN
import chess.variant.{ FromPosition, Horde, RacingKings, Variant }
import chess.{ Mode, Speed }
import chess.{ Color, Mode, Speed }
import org.joda.time.DateTime
import lila.game.PerfPicker
@ -135,6 +135,7 @@ object Challenge {
case object Random extends ColorChoice
case object White extends ColorChoice
case object Black extends ColorChoice
def apply(c: Color) = c.fold[ColorChoice](White, Black)
}
private def speedOf(timeControl: TimeControl) = timeControl match {

View File

@ -66,16 +66,22 @@ final class ChallengeApi(
private val acceptQueue = new WorkQueue(buffer = 64, timeout = 5 seconds, "challengeAccept")
def accept(c: Challenge, user: Option[User], sid: Option[String]): Fu[Option[Pov]] = acceptQueue {
if (c.challengerIsOpen) repo.setChallenger(c.setChallenger(user, sid)) inject none
def accept(
c: Challenge,
user: Option[User],
sid: Option[String],
color: Option[chess.Color] = None
): Fu[Option[Pov]] = acceptQueue {
if (c.challengerIsOpen)
repo.setChallenger(c.setChallenger(user, sid), color) inject none
else
joiner(c, user).flatMap {
case None => fuccess(None)
case Some(pov) =>
joiner(c, user, color).flatMap {
_ ?? { pov =>
(repo accept c) >>- {
uncacheAndNotify(c)
Bus.publish(Event.Accept(c, user.map(_.id)), "challenge")
} inject pov.some
}
}
}
@ -95,7 +101,7 @@ final class ChallengeApi(
}
def oauthAccept(dest: User, challenge: Challenge): Fu[Option[Game]] =
joiner(challenge, dest.some).map2(_.game)
joiner(challenge, dest.some, none).map2(_.game)
private def isLimitedByMaxPlaying(c: Challenge) =
if (c.hasClock) fuFalse

View File

@ -42,8 +42,15 @@ final private class ChallengeRepo(coll: Coll, maxPerUser: Max)(
.sort($doc("createdAt" -> 1))
.list[Challenge]()
def setChallenger(c: Challenge) =
coll.update.one($id(c.id), $set("challenger" -> c.challenger)).void
def setChallenger(c: Challenge, color: Option[chess.Color]) =
coll.update
.one(
$id(c.id),
$set($doc("challenger" -> c.challenger) ++ color.?? { c =>
$doc("colorChoice" -> Challenge.ColorChoice(c), "finalColor" -> c)
})
)
.void
private[challenge] def allWithUserId(userId: String): Fu[List[Challenge]] =
createdByChallengerId(userId) zip createdByDestId(userId) dmap {

View File

@ -4,7 +4,7 @@ import scala.util.chaining._
import chess.format.Forsyth
import chess.format.Forsyth.SituationPlus
import chess.{ Mode, Situation }
import chess.{ Color, Mode, Situation }
import lila.game.{ Game, Player, Pov, Source }
import lila.user.User
@ -14,10 +14,11 @@ final private class Joiner(
onStart: lila.round.OnStart
)(implicit ec: scala.concurrent.ExecutionContext) {
def apply(c: Challenge, destUser: Option[User]): Fu[Option[Pov]] =
def apply(c: Challenge, destUser: Option[User], color: Option[Color]): Fu[Option[Pov]] =
gameRepo exists c.id flatMap {
case true => fuccess(None)
case false =>
case true => fuccess(None)
case _ if color.map(Challenge.ColorChoice.apply).has(c.colorChoice) => fuccess(None)
case _ =>
c.challengerUserId.??(userRepo.byId) flatMap { challengerUser =>
def makeChess(variant: chess.variant.Variant): chess.Game =
chess.Game(situation = Situation(variant), clock = c.clock.map(_.config.toClock))

View File

@ -1533,7 +1533,6 @@ val `quitTeam` = new I18nKey("team:quitTeam")
val `anyoneCanJoin` = new I18nKey("team:anyoneCanJoin")
val `aConfirmationIsRequiredToJoin` = new I18nKey("team:aConfirmationIsRequiredToJoin")
val `joiningPolicy` = new I18nKey("team:joiningPolicy")
val `teamLeaders` = new I18nKey("team:teamLeaders")
val `teamBestPlayers` = new I18nKey("team:teamBestPlayers")
val `teamRecentMembers` = new I18nKey("team:teamRecentMembers")
val `kickSomeone` = new I18nKey("team:kickSomeone")
@ -1546,7 +1545,13 @@ val `teamTournament` = new I18nKey("team:teamTournament")
val `teamTournamentOverview` = new I18nKey("team:teamTournamentOverview")
val `messageAllMembers` = new I18nKey("team:messageAllMembers")
val `messageAllMembersOverview` = new I18nKey("team:messageAllMembersOverview")
val `messageAllMembersLongDescription` = new I18nKey("team:messageAllMembersLongDescription")
val `teamsIlead` = new I18nKey("team:teamsIlead")
val `youWayWantToLinkOneOfTheseTournaments` = new I18nKey("team:youWayWantToLinkOneOfTheseTournaments")
val `usersWhoCanManageThisTeam` = new I18nKey("team:usersWhoCanManageThisTeam")
val `leadersChat` = new I18nKey("team:leadersChat")
val `nbMembers` = new I18nKey("team:nbMembers")
val `teamLeaders` = new I18nKey("team:teamLeaders")
val `xJoinRequests` = new I18nKey("team:xJoinRequests")
}
@ -1680,6 +1685,7 @@ val `howCanIBecomeModerator` = new I18nKey("faq:howCanIBecomeModerator")
val `youCannotApply` = new I18nKey("faq:youCannotApply")
val `isCorrespondenceDifferent` = new I18nKey("faq:isCorrespondenceDifferent")
val `youCanUseOpeningBookNoEngine` = new I18nKey("faq:youCanUseOpeningBookNoEngine")
val `gameplay` = new I18nKey("faq:gameplay")
val `howBulletBlitzEtcDecided` = new I18nKey("faq:howBulletBlitzEtcDecided")
val `basedOnGameDuration` = new I18nKey("faq:basedOnGameDuration")
val `durationFormula` = new I18nKey("faq:durationFormula")
@ -1714,7 +1720,6 @@ val `lichessRecognizeAllOTBtitles` = new I18nKey("faq:lichessRecognizeAllOTBtitl
val `showYourTitle` = new I18nKey("faq:showYourTitle")
val `asWellAsManyNMtitles` = new I18nKey("faq:asWellAsManyNMtitles")
val `verificationForm` = new I18nKey("faq:verificationForm")
val `lichessMasterLM` = new I18nKey("faq:lichessMasterLM")
val `canIbecomeLM` = new I18nKey("faq:canIbecomeLM")
val `noUpperCaseDot` = new I18nKey("faq:noUpperCaseDot")
val `lMtitleComesToYouDoNotRequestIt` = new I18nKey("faq:lMtitleComesToYouDoNotRequestIt")

View File

@ -1,6 +1,7 @@
package lila.perfStat
import scala.concurrent.duration._
import reactivemongo.api.ReadPreference
import lila.game.{ Game, GameRepo, Pov, Query }
import lila.rating.PerfType
@ -12,7 +13,8 @@ final class PerfStatIndexer(
storage: PerfStatStorage
)(implicit ec: scala.concurrent.ExecutionContext, mat: akka.stream.Materializer) {
private val workQueue = new WorkQueue(buffer = 64, timeout = 1 minute, "perfStatIndexer")
private val workQueue =
new WorkQueue(buffer = 64, timeout = 10 seconds, name = "perfStatIndexer")
private[perfStat] def userPerf(user: User, perfType: PerfType): Fu[PerfStat] = workQueue {
storage.find(user.id, perfType) getOrElse gameRepo
@ -21,7 +23,8 @@ final class PerfStatIndexer(
Query.finished ++
Query.turnsGt(2) ++
Query.variant(PerfType variantOf perfType),
Query.sortChronological
Query.sortChronological,
readPreference = ReadPreference.secondaryPreferred
)
.fold(PerfStat.init(user.id, perfType)) {
case (perfStat, game) if game.perfType.contains(perfType) =>

View File

@ -39,11 +39,6 @@ case class AnaDrop(
)
}
}
// def json(b: Branch): JsObject = Json.obj(
// "node" -> b,
// "path" -> path
// ).add("ch" -> chapterId)
}
object AnaDrop {

View File

@ -34,6 +34,8 @@ case class Team(
object Team {
val maxJoin = 100
type ID = String
type ChatFor = Int

View File

@ -30,8 +30,6 @@ final class TeamApi(
import BSONHandlers._
val creationPeriod = Period weeks 1
def team(id: Team.ID) = teamRepo.coll.byId[Team](id)
def light(id: Team.ID) = teamRepo.coll.byId[LightTeam](id, $doc("name" -> true))
@ -76,10 +74,13 @@ final class TeamApi(
def mine(me: User): Fu[List[Team]] =
cached teamIdsList me.id flatMap teamRepo.byIdsSortPopular
def countTeamsOf(me: User) =
cached teamIdsList me.id dmap (_.size)
def hasTeams(me: User): Fu[Boolean] = cached.teamIds(me.id).map(_.value.nonEmpty)
def hasCreatedRecently(me: User): Fu[Boolean] =
teamRepo.userHasCreatedSince(me.id, creationPeriod)
def countCreatedRecently(me: User): Fu[Int] =
teamRepo.countCreatedSince(me.id, Period weeks 1)
def requestsWithUsers(team: Team): Fu[List[RequestWithUser]] =
for {

View File

@ -53,8 +53,8 @@ final class TeamRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionCont
def name(id: String): Fu[Option[String]] =
coll.primitiveOne[String]($id(id), "name")
def userHasCreatedSince(userId: String, duration: Period): Fu[Boolean] =
coll.exists(
private[team] def countCreatedSince(userId: String, duration: Period): Fu[Int] =
coll.countSel(
$doc(
"createdAt" $gt DateTime.now.minus(duration),
"createdBy" -> userId

View File

@ -27,46 +27,46 @@
<string name="youCannotApply">Its not possible to apply to become a moderator. If we see someone who we think would be good as a moderator, we will contact them directly.</string>
<string name="isCorrespondenceDifferent">Is correspondence different from normal chess?</string>
<string name="youCanUseOpeningBookNoEngine">On Lichess, the main difference in rules for correspondence chess is that an opening book is allowed. The use of engines is still prohibited and will result in being flagged for engine assistance. Although ICCF allows engine use in correspondence, Lichess does not.</string>
<string name="gameplay">Gameplay</string>
<string name="howBulletBlitzEtcDecided">How are Bullet, Blitz and other time controls decided?</string>
<string name="basedOnGameDuration">Lichess time controls are based on estimated game duration = %1$s
For instance, the estimated duration of a 5+3 game is 5 * 60 + 40 * 3 = 420 seconds.</string>
<string name="durationFormula">(clock initial time) + 40 * (clock increment)</string>
For instance, the estimated duration of a 5+3 game is 5 × 60 + 40 × 3 = 420 seconds.</string>
<string name="durationFormula">(clock initial time) + 40 × (clock increment)</string>
<string name="inferiorThanXsEqualYtimeControl">≤ %1$ss = %2$s</string>
<string name="superiorThanXsEqualYtimeControl">≥ %1$ss = %2$s</string>
<string name="whatVariantsCanIplay">What variants can I play on Lichess?</string>
<string name="lichessSupportChessAnd">Lichess supports standard chess and %1$s.</string>
<string name="eightVariants">8 chess variants</string>
<string name="whatIsACPL">What is "average centipawn loss"/ACPL?</string>
<string name="whatIsACPL">What is the average centipawn loss (ACPL)?</string>
<string name="acplExplanation">The centipawn is the unit of measure used in chess as representation of the advantage. A centipawn is equal to 1/100th of a pawn. Therefore 100 centipawns = 1 pawn. These values play no formal role in the game but are useful to players, and essential in computer chess, for evaluating positions.
The top computer move will lose zero centipawns, but lesser moves will result in a deterioration of the position, measured in centipawns.
The top computer move will lose zero centipawns, but lesser moves will result in a deterioration of the position, measured in centipawns.
This value can be used as an indicator of the quality of play. The fewer centipawns one loses per move, the stronger the play.
This value can be used as an indicator of the quality of play. The fewer centipawns one loses per move, the stronger the play.
The computer analysis on Lichess is powered by Stockfish.</string>
The computer analysis on Lichess is powered by Stockfish.</string>
<string name="insufficientMaterial">Losing on time, drawing and insufficient material</string>
<string name="lichessFollowFIDErules">In the event of one player running out of time, that player will usually lose the game. However, the game is drawn if the position is such that the opponent cannot checkmate the player's king by any possible series of legal moves (%1$s).
In rare cases this can be difficult to decide automatically (forced lines, fortresses). By default we always side with the player who did not run out of time.
In rare cases this can be difficult to decide automatically (forced lines, fortresses). By default we always side with the player who did not run out of time.
Note that it can be possible to mate with a single knight or bishop if the opponent has a piece that could block the king.</string>
<string name="linkToFIDErules">FIDE handbook §6.9, pdf</string>
Note that it can be possible to mate with a single knight or bishop if the opponent has a piece that could block the king.</string>
<string name="linkToFIDErules">FIDE handbook §6.9, PDF</string>
<string name="discoveringEnPassant">Why can a pawn capture another pawn when it is already passed? (en passant)</string>
<string name="explainingEnPassant">This is a legal move known as "en passant". The Wikipedia article gives a %1$s.
It is described in section 3.7 (d) of the %2$s:
It is described in section 3.7 (d) of the %2$s:
"A pawn occupying a square on the same rank as and on an adjacent file to an opponents pawn which has just advanced two squares in one move from its original square may capture this opponents pawn as though the latter had been moved only one square. This capture is only legal on the move following this advance and is called an en passant capture."
"A pawn occupying a square on the same rank as and on an adjacent file to an opponents pawn which has just advanced two squares in one move from its original square may capture this opponents pawn as though the latter had been moved only one square. This capture is only legal on the move following this advance and is called an en passant capture."
See the %3$s on this move for some practice with it.
</string>
See the %3$s on this move for some practice with it.</string>
<string name="goodIntroduction">good introduction</string>
<string name="officialRulesPDF">official rules (pdf)</string>
<string name="officialRulesPDF">official rules (PDF)</string>
<string name="lichessTraining">Lichess training</string>
<string name="threefoldRepetition">Threefold repetition</string>
<string name="threefoldRepetitionExplanation">If a position occurs three times, players can claim a draw by %1$s. Lichess implements the official FIDE rules, as described in Article 9.2 (d) of the %2$s.</string>
<string name="threefoldRepetitionLowerCase">threefold repetition</string>
<string name="handBookPDF">handbook (pdf)</string>
<string name="handBookPDF">handbook (PDF)</string>
<string name="notRepeatedMoves">We did not repeat moves. Why was the game still drawn by repetition?</string>
<string name="repeatedPositionsThatMatters">Threefold repetition is about repeated %1$s, not moves. Repetition does not have to occur consecutively.</string>
<string name="positions">positions</string>
@ -78,19 +78,18 @@
<string name="lichessRecognizeAllOTBtitles">Lichess recognises all FIDE titles gained from OTB (over the board) play, as well as %1$s. Here is a list of FIDE titles:</string>
<string name="showYourTitle">If you have an OTB title, you can apply to have this displayed on your account by completing the %1$s, including a clear image of an identifying document/card and a selfie of you holding the document/card.
Verifying as a titled player on Lichess gives access to play in the Titled Arena events.
Verifying as a titled player on Lichess gives access to play in the Titled Arena events.
Finally there is an honorary %2$s title.</string>
Finally there is an honorary %2$s title.</string>
<string name="asWellAsManyNMtitles">many national master titles</string>
<string name="verificationForm">verification form</string>
<string name="lichessMasterLM">Lichess Master (LM)</string>
<string name="canIbecomeLM">Can I get the Lichess Master (LM) title?</string>
<string name="noUpperCaseDot">No.</string>
<string name="lMtitleComesToYouDoNotRequestIt">This honorific title is unofficial and only exists on Lichess.
We rarely award it to highly notable players who are good citizens of Lichess, at our discretion. You don't get the LM title, the LM title gets to you. If you qualify, you will get a message from us regarding it and the choice to accept or decline.
We rarely award it to highly notable players who are good citizens of Lichess, at our discretion. You don't get the LM title, the LM title gets to you. If you qualify, you will get a message from us regarding it and the choice to accept or decline.
Do not request to get the LM title.</string>
Do not ask for the LM title.</string>
<string name="whatUsernameCanIchoose">What can my username be?</string>
<string name="usernamesNotOffensive">In general, usernames should not be: offensive, impersonating someone else, or advertising. You can read more about the %1$s.</string>
<string name="guidelines">guidelines</string>
@ -103,11 +102,11 @@
<string name="whichRatingSystemUsedByLichess">What rating system does Lichess use?</string>
<string name="ratingSystemUsedByLichess">Ratings are calculated using the Glicko-2 rating method developed by Mark Glickman. This is a very popular rating method, and is used by a significant number of chess organisations (FIDE being a notable counter-example, as they still use the dated Elo rating system).
Fundamentally, Glicko ratings use "confidence intervals" when calculating and representing your rating. When you first start using the site, your rating starts at 1500 ± 700. The 1500 represents your rating, and the 700 represents the confidence interval.
Fundamentally, Glicko ratings use "confidence intervals" when calculating and representing your rating. When you first start using the site, your rating starts at 1500 ± 700. The 1500 represents your rating, and the 700 represents the confidence interval.
Basically, the system is 90% sure that your rating is somewhere between 800 and 2200. It is incredibly uncertain. Because of this, when a player is just starting out, their rating will change very dramatically, potentially several hundred points at a time. But after some games against established players the confidence interval will narrow, and the amount of points gained/lost after each game will decrease.
Basically, the system is 90% sure that your rating is somewhere between 800 and 2200. It is incredibly uncertain. Because of this, when a player is just starting out, their rating will change very dramatically, potentially several hundred points at a time. But after some games against established players the confidence interval will narrow, and the amount of points gained/lost after each game will decrease.
Another point to note is that, as time passes, the confidence interval will increase. This allows you to gain/lose points points more rapidly to match any changes in your skill level over that time.</string>
Another point to note is that, as time passes, the confidence interval will increase. This allows you to gain/lose points points more rapidly to match any changes in your skill level over that time.</string>
<string name="whatIsProvisionalRating">Why is there a question mark (?) next to a rating?</string>
<string name="provisionalRatingExplanation">The question mark means the rating is provisional. Reasons include:</string>
<string name="notPlayedEnoughRatedGamesAgainstX">The player has not yet finished enough rated games against %1$s in the rating category.</string>
@ -116,7 +115,7 @@
<string name="ratingDeviationMorethanOneHundredTen">Concretely, it means that the Glicko-2 deviation is greater than 110. The deviation is the level of confidence the system has in the rating. The lower the deviation, the more stable is a rating.</string>
<string name="howDoLeaderoardsWork">How do ranks and leaderboards work?</string>
<string name="inOrderToAppearsYouMust">In order to get on the %1$s you must:</string>
<string name="ratingLeaderboards">rating leaderboards:</string>
<string name="ratingLeaderboards">rating leaderboard</string>
<string name="havePlayedMoreThanThirtyGamesInThatRating">have played at least 30 rated games in a given rating,</string>
<string name="havePlayedARatedGameAtLeastOneWeekAgo">have played a rated game within the last week for this rating,</string>
<string name="ratingDeviationLowerThanXinChessYinVariants">have a rating deviation lower than %1$s, in standard chess, and lower than %2$s in variants,</string>
@ -125,7 +124,7 @@
<string name="whyAreRatingHigher">Why are ratings higher compared to other sites and organisations such as FIDE, USCF and the ICC?</string>
<string name="whyAreRatingHigherExplanation">It is best not to think of ratings as absolute numbers, or compare them against other organisations. Different organisations have different levels of players, different rating systems (Elo, Glicko, Glicko-2, or a modified version of the aforementioned). These factors can drastically affect the absolute numbers (ratings).
It's best to think of ratings as "relative" figures (as opposed to "absolute" figures): Within a pool of players, their relative differences in ratings will help you estimate who will win/draw/lose, and how often. Saying "I have X rating" means nothing unless there are other players to compare that rating to.</string>
It's best to think of ratings as "relative" figures (as opposed to "absolute" figures): Within a pool of players, their relative differences in ratings will help you estimate who will win/draw/lose, and how often. Saying "I have X rating" means nothing unless there are other players to compare that rating to.</string>
<string name="howToHideRatingWhilePlaying">How to hide ratings while playing?</string>
<string name="enableZenMode">Enable Zen-mode in the %1$s, or by pressins %2$s during a game.</string>
<string name="displayPreferences">display preferences</string>
@ -136,7 +135,7 @@
<string name="viewSiteInformationPopUp">View site information popup</string>
<string name="lichessCanOptionnalySendPopUps">Lichess can optionally send popup notifications, for example when it is your turn or you received a private message.
Click the lock icon next to the lichess.org address in the URL bar of your browser.
Click the lock icon next to the lichess.org address in the URL bar of your browser.
Then select whether to allow or block notifications from Lichess.</string>
Then select whether to allow or block notifications from Lichess.</string>
</resources>

View File

@ -859,7 +859,7 @@ computer analysis, game chat and shareable URL.</string>
<string name="toRequestSupport">To request support, %1$s</string>
<string name="tryTheContactPage">try the contact page</string>
<string name="thisTopicIsArchived">This topic has been archived and can no longer be replied to.</string>
<string name="joinTheTeamXToPost">Join the %$1s, to post in this forum</string>
<string name="joinTheTeamXToPost">Join the %1$s, to post in this forum</string>
<string name="teamNamedX">%1$s team</string>
<string name="youCannotPostYetPlaySomeGames">You can't post in the forums yet. Play some games!</string>
<string name="subscribe">Subscribe</string>

View File

@ -15,7 +15,10 @@
<string name="anyoneCanJoin">Free for all</string>
<string name="aConfirmationIsRequiredToJoin">A confirmation is required to join</string>
<string name="joiningPolicy">Joining policy</string>
<string name="teamLeaders">Team leaders</string>
<plurals name="teamLeaders">
<item quantity="one">Team leader</item>
<item quantity="other">Team leaders</item>
</plurals>
<string name="teamBestPlayers">Best players</string>
<string name="teamRecentMembers">Recent members</string>
<string name="kickSomeone">Kick someone out of the team</string>
@ -32,4 +35,11 @@
<string name="teamTournamentOverview">An Arena tournament that only members of your team can join</string>
<string name="messageAllMembers">Message all members</string>
<string name="messageAllMembersOverview">Send a private message to every member of the team</string>
<string name="messageAllMembersLongDescription">Send a private message to ALL members of the team.
You can use this to call players to join a tournament or a team battle.
Players who don't like receiving your messages might leave the team.</string>
<string name="teamsIlead">Teams I lead</string>
<string name="youWayWantToLinkOneOfTheseTournaments">You may want to link one of these upcoming tournaments?</string>
<string name="usersWhoCanManageThisTeam">Users who can manage this team</string>
<string name="leadersChat">Leaders chat</string>
</resources>

View File

@ -53,9 +53,13 @@
opacity: 1!important;
}
}
a.loss {
opacity: .2;
}
&.current a {
background: mix($c-accent, $c-bg-box, 70%);
color: #fff;
opacity: 1;
}
&.new {
border: $c-border;

View File

@ -360,7 +360,7 @@ lichess.redirect = function(obj) {
lichess.reload = function() {
if (lichess.redirectInProgress) return;
lichess.hasToReload = true;
lichess.socket.destroy();
lichess.socket.disconnect();
if (location.hash) location.reload();
else location.href = location.href;
};