Merge branch 'master' into tourneyScheduleUI

* master: (24 commits)
  hi "हिन्दी, हिंदी" translation #14525. Author: AdityaPrakash5000.
  lt "lietuvių kalba" translation #14524. Author: mast3r. "Refactoring" and review of translation, 50/444
  pl "polski" translation #14522. Author: rzenaikrzys. Perfect translation for Polish by Captain Kristo
  smarter mobile app puzzle export
  remove body background in embedded iframes
  export puzzles in mobile app format
  puzzle UI: get rid of lodash/merge and save 4K minified
  export puzzles JSON for mobile app
  move tournament WaitingUsers to a new file
  drop arena pairing left over logging
  raise puzzle selector max rating
  more arena pairing refinements
  drop user who waiting for less time if arena players count is odd
  try arena pairings every 3 seconds
  reorganize tournament waiting users code - no functional change
  tweak arena pairing system
  add player provisional rating to game JSON API
  link APK download on /mobile (content from prismic.io)
  add message when password recovery fails
  fix experimental marathon title display
  ...
pull/599/head
Thibault Duplessis 2015-06-19 16:05:52 +02:00
commit aa77c41243
33 changed files with 231 additions and 137 deletions

View File

@ -71,8 +71,8 @@ object Main extends LilaController {
}
def mobile = Open { implicit ctx =>
fuccess {
html.site.mobile()
OptionOk(Prismic oneShotBookmark "mobile-apk") {
case (doc, resolver) => html.site.mobile(doc, resolver)
}
}
}

View File

@ -94,7 +94,7 @@ object Simul extends LilaController {
}
}
def join(id: String, variant: String) = AuthBody { implicit ctx =>
def join(id: String, variant: String) = Auth { implicit ctx =>
implicit me =>
NoEngine {
fuccess {

View File

@ -5,6 +5,7 @@ import java.util.Locale
import scala.collection.mutable
import org.joda.time.format._
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ Period, PeriodType, DurationFieldType, DateTime }
import play.twirl.api.Html
@ -68,4 +69,9 @@ trait DateHelper { self: I18nHelper =>
}
def secondsFromNow(seconds: Int) = momentFromNow(DateTime.now plusSeconds seconds)
private val atomDateFormatter = ISODateTimeFormat.dateTime
def atomDate(date: DateTime): String = atomDateFormatter print date
def atomDate(field: String)(doc: io.prismic.Document): Option[String] =
doc getDate field map (_.value.toDateTimeAtStartOfDay) map atomDate
}

View File

@ -12,6 +12,12 @@ zen = true) {
}
@trans.passwordReset()
</h1>
@if(ok.contains(false)) {
<p>
Are you sure you even registered your email on lichess?<br />
It was not required for your registration.
</p>
}
<form action="@routes.Auth.passwordResetApply" method="POST">
<ul>
<li class="email">
@ -29,7 +35,7 @@ zen = true) {
</li>
@errMsg(form)
<li>
<button type="submit" class="submit button" data-icon="F"> @trans.emailMeALink()</button>
<button type="submit" class="submit button text" data-icon="F">@trans.emailMeALink()</button>
</li>
</ul>
</form>

View File

@ -5,12 +5,12 @@
<link rel="alternate" type="text/html" href="@routes.Blog.index().absoluteURL()"/>
<link rel="self" type="application/atom+xml" href="@routes.Blog.atom().absoluteURL()"/>
<title>lichess.org blog</title>
<updated>@(docs.headOption.flatMap(_.getDate("blog.date", "yyyy-MM-dd'T'HH:mm:ss'Z'")).??(_.toString))</updated>
<updated>@docs.headOption.flatMap(atomDate("blog.date"))</updated>
@docs.map { doc =>
<entry>
<id>@routes.Blog.show(doc.id, doc.slug).absoluteURL()</id>
<published>@(doc.getDate("blog.date", "yyyy-MM-dd'T'HH:mm:ss'Z'").??(_.toString))</published>
<updated>@(doc.getDate("blog.date", "yyyy-MM-dd'T'HH:mm:ss'Z'").??(_.toString))</updated>
<published>@atomDate("blog.date")(doc)</published>
<updated>@atomDate("blog.date")(doc)</updated>
<link rel="alternate" type="text/html" href="@routes.Blog.show(doc.id, doc.slug).absoluteURL()"/>
<title>@doc.getText("blog.title")</title>
<category>

View File

@ -13,7 +13,7 @@
<meta charset="utf-8">
</head>
<body
class="base highlight @bg"
class="base highlight @bg no-bg"
style="width: 224px; height: 264px; overflow: hidden;">
<div id="daily_puzzle" class="is2d highlight @bg @theme merida" title="@trans.clickToSolve()">
@daily.html

View File

@ -4,16 +4,10 @@
@helper.javascriptRouter("puzzleRoutes")(
routes.javascript.Puzzle.attempt,
routes.javascript.Puzzle.show,
/* routes.javascript.Puzzle.history, */
/* routes.javascript.Puzzle.difficulty, */
/* routes.javascript.Puzzle.home, */
/* routes.javascript.Puzzle.newPuzzle, */
routes.javascript.Puzzle.load,
routes.javascript.Puzzle.vote,
/* routes.javascript.Coordinate.home, */
routes.javascript.Editor.load,
routes.javascript.Round.watcher
/* routes.javascript.Auth.signup */
)(ctx.req)
@embedJs {
LichessPuzzle(

View File

@ -1,4 +1,4 @@
@()(implicit ctx: Context)
@(apkDoc: io.prismic.Document, resolver: io.prismic.DocumentLinkResolver)(implicit ctx: Context)
@base.layout(
title = "Mobile",
@ -22,6 +22,9 @@ moreCss = cssTag("mobile.css")) {
width="172"
src="https://devimages.apple.com.edgekey.net/app-store/marketing/guidelines/images/badge-download-on-the-app-store.svg" />
</a>
<div class="apk" style="margin-top: 20px">
@Html(~apkDoc.getHtml("doc.content", resolver))
</div>
<h2>lichess on the go</h2>
<ul class="block">
<li>Challenge online players</li>
@ -36,11 +39,13 @@ moreCss = cssTag("mobile.css")) {
<li>Zero advertisement</li>
<li>Entirely <a href="https://github.com/veloce/lichobile">Open Source</a> as usual</li>
</ul>
<h2>Three modes included</h2>
<h2>Full featured</h2>
<ul class="block">
<li>Real time with increment</li>
<li>Correspondence games</li>
<li>Over The Board, offline</li>
<li>VS computer, offline</li>
<li>Watch Lichess TV</li>
</ul>
<p class="block">
More of the features you love from the website will be added in the near future.<br />

View File

@ -13,7 +13,7 @@
<meta charset="utf-8">
</head>
<body
class="base highlight @bg"
class="base highlight @bg no-bg"
style="width: 224px; height: 264px; overflow: hidden;"
data-stream-url="@routes.Tv.streamOut">
<div id="featured_game" class="is2d highlight @bg @theme merida" title="lichess.org TV">

View File

@ -43,7 +43,7 @@ whiteResigned=सफेद ने हार मानली
blackResigned=काले ने हार मानली
whiteLeftTheGame=सफेद दूम दबाकर भाग गया
blackLeftTheGame=काला दूम दबाकर भाग गया
shareThisUrlToLetSpectatorsSeeTheGame=यह URL को SHARE करिएँ अपने दोस्तों के साथ
shareThisUrlToLetSpectatorsSeeTheGame=इस URL को बाँटें ताकी सभा इसे देख सके।
replayAndAnalyse=विश्लेषण
computerAnalysisInProgress=कम्प्यूटर विश्लेषण प्रगति में
theComputerAnalysisHasFailed=कम्प्यूटर विश्लेषण असफल हुआ

View File

@ -1,13 +1,13 @@
playWithAFriend=Žaisti prieš draugą
playWithTheMachine=Žaisti prieš kompiuterį
toInviteSomeoneToPlayGiveThisUrl=Norint pakviesti ką nors žaidimui, nusiųsk šią nuorodą
playWithAFriend=Žaisti su draugu
playWithTheMachine=Žaisti su kompiuteriu
toInviteSomeoneToPlayGiveThisUrl=Norėdami pakviesti varžovą, pasidalinkite šiuo adresu
gameOver=Žaidimas baigtas
waitingForOpponent=Laukiama priešininko
waitingForOpponent=Laukiama varžovo
waiting=Laukiama
yourTurn=Tavo ėjimas
aiNameLevelAiLevel=%s lygis %s
yourTurn=Jūsų ėjimas
aiNameLevelAiLevel=%s lygis Nr. %s
level=Lygis
toggleTheChat=Įgalinti pokalbį
toggleTheChat=Įjungti/išjungti pokalbius
toggleSound=Įjungti/išjungti garsus
chat=Pokalbis
resign=Pasiduoti
@ -19,20 +19,23 @@ randomColor=Atsitiktinė spalva
createAGame=Sukurti žaidimą
whiteIsVictorious=Baltieji laimėjo
blackIsVictorious=Juodieji laimėjo
playWithTheSameOpponentAgain=Žaisti su tuo pačiu priešininku dar kartą
newOpponent=Naujas priešininkas
playWithAnotherOpponent=Žaisti su kitu priešininku
yourOpponentWantsToPlayANewGameWithYou=Tavo priešininkas nori žaisti su tavimi iš naujo
kingInTheCenter=Karalius centre
threeChecks=Trys šachai
variantEnding=Variantinė pabaiga
playWithTheSameOpponentAgain=Žaisti su tuo pačiu varžovu dar kartą
newOpponent=Naujas varžovas
playWithAnotherOpponent=Žaisti su kitu varžovu
yourOpponentWantsToPlayANewGameWithYou=Jūsų varžovas norėtų sužaisti dar kartą
joinTheGame=Prisijungti prie žaidimo
whitePlays=Baltųjų eilė
blackPlays=Juodųjų eilė
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=Priešininkas paliko žaidimą. Galite pareikšti pergalę, lygiąsias arba palaukti.
makeYourOpponentResign=Pareikšti savo pergalę
forceResignation=Priverstinis pralaimėjimas
forceDraw=Priverstinės lygiosios
whitePlays=Baltųjų ėjimas
blackPlays=Juodųjų ėjimas
theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim=Panašu, kad varžovas paliko žaidimą. Galite pasiimti pergalę, lygiąsias, arba palaukti.
makeYourOpponentResign=Priversti priešininką pasiduoti
forceResignation=Pasiimti pergalę
forceDraw=Užskaityti lygiąsias
talkInChat=Kalbėti
theFirstPersonToComeOnThisUrlWillPlayWithYou=Pirmasis žmogus prisijungęs šiuo adresu žais su tavimi.
whiteCreatesTheGame=Baltieji sukūrė žaidimą
theFirstPersonToComeOnThisUrlWillPlayWithYou=Pirmasis šiuo adresu atėjęs žmogus taps jūsų varžovu.
whiteCreatesTheGame=Baltieji sukūrė partiją
blackCreatesTheGame=Juodieji sukūrė partiją
whiteJoinsTheGame=Baltieji prisijungė
blackJoinsTheGame=Juodieji prisijungė
@ -40,11 +43,11 @@ whiteResigned=Baltieji pasidavė
blackResigned=Juodieji pasidavė
whiteLeftTheGame=Baltieji paliko žaidimą
blackLeftTheGame=Juodieji paliko žaidimą
shareThisUrlToLetSpectatorsSeeTheGame=Pasidalinkite šiuo adresu norintiems stebėti šį žaidimą
replayAndAnalyse=Pakartoti ir analizuoti
shareThisUrlToLetSpectatorsSeeTheGame=Pasidalinę šiuo adresu leisite žiūrovams stebėti partiją
replayAndAnalyse=Peržiūrėti ir analizuoti
computerAnalysisInProgress=Kompiuteris analizuoja
theComputerAnalysisHasFailed=Kompiuterio analizė nepavyko
viewTheComputerAnalysis=Žiūrėti kompiuterio analizę
viewTheComputerAnalysis=Peržiūrėti kompiuterio analizę
requestAComputerAnalysis=Užsakyti kompiuterio analizę
computerAnalysis=Kompiuterio analizė
analysis=Žaidimo analizė

View File

@ -21,6 +21,7 @@ whiteIsVictorious=Białe zwyciężyły
blackIsVictorious=Czarne zwyciężyły
kingInTheCenter=Król w centrum
threeChecks=Trzykrotne zaszachowanie
variantEnding=Wariant zakończony
playWithTheSameOpponentAgain=Zagraj jeszcze raz z tym zawodnikiem
newOpponent=Nowy przeciwnik
playWithAnotherOpponent=Zagraj z innym przeciwnikiem
@ -56,7 +57,7 @@ inaccuracies=Niedokładności
moveTimes=Czas przeznaczony na ruch
flipBoard=Obróć szachownicę
threefoldRepetition=Trzykrotne powtórzenie pozycji
claimADraw=Reklamuj remis
claimADraw=Wymuś remis
offerDraw=Zaproponuj remis
draw=Remis
nbConnectedPlayers=%s graczy online
@ -125,7 +126,7 @@ gameAborted=Gra została przerwana
standard=Standard
unlimited=Bez ograniczeń
mode=Tryb
casual=Nierankingowa
casual=Towarzyska
rated=Rankingowa
thisGameIsRated=Ta gra jest rankingowa
rematch=Rewanż
@ -173,7 +174,7 @@ tournamentPoints=Punkty Turniejowe
viewTournament=Zobacz turniej
backToTournament=Wróć do turnieju
backToGame=Wróć do gry
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Darmowe szachy online. Zagraj teraz w szachy o przyjaznym interfejsie. Bez rejestracji, bez reklam, bez instalowania wtyczek. Graj w szachy z komputerem, przyjaciółmi lub losowymi przeciwnikami.
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Darmowe szachy online. Zagraj teraz w szachy o przyjaznym interfejsie. Bez rejestracji, bez reklam, bez instalowania wtyczek. Graj w szachy z komputerem, przyjaciółmi lub losowo wybranymi przeciwnikami.
teams=Zespoły
nbMembers=%s członkowie
allTeams=Wszystkie zespoły
@ -305,7 +306,7 @@ startTraining=Rozpocznij trening
continueTraining=Kontynuuj trening
retryThisPuzzle=Powtórz to zadanie
thisPuzzleIsCorrect=To zadanie jest poprawne i ciekawe
thisPuzzleIsWrong=To zadanie jest niepoprawne lub nudne
thisPuzzleIsWrong=To zadanie jest niepoprawne i nudne
youHaveNbSecondsToMakeYourFirstMove=Masz %s sekund na pierwszy ruch!
nbGamesInPlay=%s gier w trakcie
automaticallyProceedToNextGameAfterMoving=Automatycznie przejdź do następnej gry po wykonaniu ruchu
@ -342,8 +343,8 @@ user=Użytkownik
reason=Powód
whatIsIheMatter=Co się stało?
cheat=Oszustwo
insult=Wyzwiska
troll=Trollowanie
insult=Obraza
troll=Upierdliwiec(natręt)
other=Inny
reportDescriptionHelp=Wklej link do tej gry i wytłumacz dlaczego zachowanie użytkownika jest nieprawidłowe
by=przez %s
@ -392,7 +393,7 @@ soundControlInTheTopBarOfEveryPage=Kontrola dźwięku jest umieszczona na po pra
yourPreferencesHaveBeenSaved=Twoje preferencje zostały zapisane
none=Żadna
fast=Szybka
normal=Normalne
normal=Normalna
slow=Wolna
insideTheBoard=Wewnątrz szachownicy
outsideTheBoard=Na zewnątrz szachownicy
@ -420,8 +421,8 @@ blocks=%s zablokowanych
listBlockedPlayers=Lista zawodników których zablokowałeś
human=Człowiek
computer=Komputer
side=Kolor
clock=Czas
side=Strona
clock=Zegar
unauthorizedError=Nieupoważniony dostęp
noInternetConnection=Brak połączenia. Możesz jednak wybrać z menu opcję gry offline.
connectedToLichess=Zostałeś połączony z lichess.org

View File

@ -52,5 +52,6 @@ private[api] final class Cli(bus: lila.common.Bus, renderer: ActorSelection) ext
lila.analyse.Env.current.cli.process orElse
lila.team.Env.current.cli.process orElse
lila.round.Env.current.cli.process orElse
lila.puzzle.Env.current.cli.process orElse
process
}

View File

@ -120,6 +120,7 @@ private[api] final class GameApi(
"name" -> p.name,
"rating" -> p.rating,
"ratingDiff" -> p.ratingDiff,
"provisional" -> p.provisional.option(true),
"moveTimes" -> withMoveTimes.fold(
g.moveTimes.zipWithIndex.filter(_._2 % 2 == i).map(_._1),
JsNull),

View File

@ -36,7 +36,7 @@ final class BlogApi(prismicUrl: String, collection: String) {
object BlogApi {
def extract(body: Fragment.StructuredText): String =
def extract(body: fragments.StructuredText): String =
body.blocks
.takeWhile(_.isInstanceOf[Fragment.StructuredText.Block.Paragraph])
.take(2).map {

View File

@ -48,6 +48,14 @@ final class Env(
lazy val pngExport = PngExport(PngExecPath) _
def cli = new lila.common.Cli {
def process = {
case "puzzle" :: "export" :: nbStr :: Nil => parseIntOption(nbStr) ?? { nb =>
Export(api, nb)
}
}
}
private[puzzle] lazy val puzzleColl = db(CollectionPuzzle)
private[puzzle] lazy val attemptColl = db(CollectionAttempt)
}

View File

@ -0,0 +1,29 @@
package lila.puzzle
import java.nio.charset.StandardCharsets
import java.util.Base64
import play.api.libs.json._
object Export {
private def base64(str: String) = Base64.getEncoder.encodeToString(str getBytes StandardCharsets.UTF_8)
def apply(api: PuzzleApi, nb: Int) =
api.puzzle.export(nb * 2).map { puzzles =>
puzzles.map { puzzle =>
val encoded = base64(Json stringify {
Json.obj(
"id" -> puzzle.id,
"fen" -> puzzle.fen,
"color" -> puzzle.color.name,
"move" -> puzzle.initialMove,
"lines" -> lila.puzzle.Line.toJson(puzzle.lines))
})
s""""$encoded"""" -> puzzle.vote.sum
}.sortBy(_._1.size)
.take(nb)
.sortBy(_._2 -> scala.util.Random.nextInt)
.map(_._1)
.mkString(",\n")
}
}

View File

@ -53,6 +53,12 @@ private[puzzle] final class PuzzleApi(
}
}
}
def export(nb: Int): Fu[List[Puzzle]] = List(true, false).map { mate =>
puzzleColl.find(BSONDocument("mate" -> mate))
.sort(BSONDocument(Puzzle.BSONFields.voteSum -> -1))
.cursor[Puzzle].collect[List](nb / 2)
}.sequenceFu.map(_.flatten)
}
object attempt {

View File

@ -40,7 +40,7 @@ private[puzzle] final class Selector(
.one[Puzzle]
case Some(user) if user.perfs.puzzle.nb > maxAttempts => fuccess(none)
case Some(user) =>
val rating = user.perfs.puzzle.intRating min 2200 max 900
val rating = user.perfs.puzzle.intRating min 2300 max 900
val step = toleranceStepFor(rating)
api.attempt.playedIds(user, maxAttempts) flatMap { ids =>
tryRange(rating, step, step, difficultyDecay(difficulty), ids, isMate)

View File

@ -121,7 +121,7 @@ final class Env(
organizer -> actorApi.AllCreatedTournaments
}
scheduler.message(4 seconds) {
scheduler.message(3 seconds) {
organizer -> actorApi.StartedTournaments
}

View File

@ -46,9 +46,9 @@ private[tournament] final class Organizer(
}
}
case FinishGame(game, _, _) => api finishGame game
case FinishGame(game, _, _) => api finishGame game
case lila.hub.actorApi.mod.MarkCheater(userId) => api ejectCheater userId
case lila.hub.actorApi.mod.MarkCheater(userId) => api ejectCheater userId
case lila.hub.actorApi.round.Berserk(gameId, userId) => api.berserk(gameId, userId)
}
@ -58,18 +58,15 @@ private[tournament] final class Organizer(
_ filterNot isOnline foreach { api.withdraw(tour.id, _) }
}
private def startPairing(tour: Tournament, activeUserIds: List[String]) = for {
socketUserIds <- getSocketUserIds(tour)
allUsers = socketUserIds.copy(
all = activeUserIds intersect socketUserIds.all,
waiting = activeUserIds intersect socketUserIds.waiting)
} {
tour.system.pairingSystem.createPairings(tour, allUsers) onSuccess {
case (pairings, events) =>
pairings.toNel foreach { api.makePairings(tour, _, events) }
private def startPairing(tour: Tournament, activeUserIds: List[String]) =
getWaitingUsers(tour) zip PairingRepo.playingUserIds(tour) foreach {
case (waitingUsers, playingUserIds) =>
val users = waitingUsers intersect activeUserIds diff playingUserIds
tour.system.pairingSystem.createPairings(tour, users) onSuccess {
case (pairings, events) => pairings.toNel foreach { api.makePairings(tour, _, events) }
}
}
}
private def getSocketUserIds(tour: Tournament): Fu[AllUserIds] =
socketHub ? Ask(tour.id, GetAllUserIds) mapTo manifest[AllUserIds]
private def getWaitingUsers(tour: Tournament): Fu[WaitingUsers] =
socketHub ? Ask(tour.id, GetWaitingUsers) mapTo manifest[WaitingUsers]
}

View File

@ -2,7 +2,6 @@ package lila.tournament
import akka.actor._
import akka.pattern.pipe
import org.joda.time.DateTime
import play.api.libs.iteratee._
import play.api.libs.json._
import scala.concurrent.duration._
@ -32,6 +31,8 @@ private[tournament] final class Socket(
private var clock = none[chess.Clock]
private var waitingUsers = WaitingUsers.empty
override def preStart() {
super.preStart()
TournamentRepo byId tournamentId map SetTournament.apply pipeTo self
@ -52,9 +53,11 @@ private[tournament] final class Socket(
}
notifyReload
case Reload => notifyReload
case Reload => notifyReload
case GetAllUserIds => sender ! AllUserIds(all = userIds, waiting = waitingUserIds)
case GetWaitingUsers =>
waitingUsers = waitingUsers.update(userIds, clock)
sender ! waitingUsers
case PingVersion(uid, v) => {
ping(uid)
@ -103,28 +106,6 @@ private[tournament] final class Socket(
notifyAll("reload")
}
private var waitingUsers = Map[String, DateTime]()
// users that have been here for some time
def waitingUserIds: List[String] = {
val us = userIds
val date = DateTime.now
waitingUsers = waitingUsers.filterKeys { u =>
us contains u
}.++ {
us.filterNot(waitingUsers.contains).map { _ -> date }
}
// 1+0 -> 5 -> 8
// 3+0 -> 9 -> 11
// 5+0 -> 17 -> 17
// 10+0 -> 32 -> 30
val waitSeconds = ((clock.fold(60)(_.estimateTotalTime) / 20) + 2) min 30 max 8
val since = date minusSeconds waitSeconds
waitingUsers.collect {
case (u, d) if d.isBefore(since) => u
}.toList
}
def notifyCrowd {
if (!delayedCrowdNotification) {
delayedCrowdNotification = true

View File

@ -28,7 +28,7 @@ object System {
trait PairingSystem {
def createPairings(
tournament: Tournament,
users: AllUserIds): Fu[(Pairings, Events)]
users: WaitingUsers): Fu[(Pairings, Events)]
}
trait Score {

View File

@ -0,0 +1,57 @@
package lila.tournament
import org.joda.time.DateTime
private[tournament] case class WaitingUsers(
hash: Map[String, DateTime],
clock: Option[chess.Clock],
date: DateTime) {
// 1+0 -> 5 -> 7
// 3+0 -> 9 -> 11
// 5+0 -> 17 -> 17
// 10+0 -> 32 -> 30
private val waitSeconds = {
(clock.fold(60)(_.estimateTotalTime) / 20) + 2
} min 30 max 7
lazy val all = hash.keys.toList
lazy val size = hash.size
def isOdd = size % 2 == 1
// skips the most recent user if odd
def evenNumber: List[String] = {
if (isOdd) hash.toList.sortBy(-_._2.getMillis).drop(1).map(_._1)
else all
}
def waitSecondsOf(userId: String) = hash get userId map { d =>
nowSeconds - d.getSeconds
}
def waiting = {
val since = date minusSeconds waitSeconds
hash.collect {
case (u, d) if d.isBefore(since) => u
}.toList
}
def update(us: Seq[String], clock: Option[chess.Clock]) = {
val newDate = DateTime.now
copy(
date = newDate,
clock = clock,
hash = hash.filterKeys(us.contains) ++
us.filterNot(hash.contains).map { _ -> newDate }
)
}
def intersect(us: Seq[String]) = copy(hash = hash filterKeys us.contains)
def diff(us: Set[String]) = copy(hash = hash filterKeys { k => !us.contains(k) })
}
private[tournament] object WaitingUsers {
def empty = WaitingUsers(Map.empty, none, DateTime.now)
}

View File

@ -39,6 +39,6 @@ private[tournament] case object ScheduleNow
private[tournament] case object NotifyCrowd
private[tournament] case object NotifyReload
private[tournament] case object GetAllUserIds
private[tournament] case object GetWaitingUsers
private[tournament] case class SetTournament(tour: Option[Tournament])

View File

@ -11,7 +11,6 @@ object PairingSystem extends AbstractPairingSystem {
case class Data(
tour: Tournament,
recentPairings: List[Pairing],
playingUserIds: Set[String],
ranking: Map[String, Int],
nbActiveUsers: Int)
@ -19,39 +18,37 @@ object PairingSystem extends AbstractPairingSystem {
// then pair all users
def createPairings(
tour: Tournament,
users: AllUserIds): Fu[(Pairings, Events)] = for {
recentPairings <- PairingRepo.recentByTourAndUserIds(tour.id, users.all, Math.min(100, users.all.size * 5))
playingUserIds <- PairingRepo.playingUserIds(tour)
users: WaitingUsers): Fu[(Pairings, Events)] = for {
recentPairings <- PairingRepo.recentByTourAndUserIds(tour.id, users.all, Math.min(120, users.size * 5))
nbActiveUsers <- PlayerRepo.countActive(tour.id)
ranking <- PlayerRepo.ranking(tour.id)
data = Data(tour, recentPairings, playingUserIds, ranking, nbActiveUsers)
pairings <- tryPairings(data, users.waiting) flatMap {
case Nil if recentPairings.isEmpty => tryPairings(data, users.all)
case Nil => fuccess(Nil)
case _ => tryPairings(data, users.all)
data = Data(tour, recentPairings, ranking, nbActiveUsers)
pairings <- {
if (recentPairings.isEmpty) evenOrAll(data, users)
else tryPairings(data, users.waiting) flatMap {
case Nil => fuccess(Nil)
case _ => evenOrAll(data, users)
}
}
} yield {
if (pairings.nonEmpty && pairings.size * 2 < users.all.size) {
val left = users.all diff pairings.flatMap(_.users)
if (left.nonEmpty) println(s"[arena ${tour.id}] left over: ${left mkString ","} out of ${users.all.size}")
} yield pairings -> Nil
private def evenOrAll(data: Data, users: WaitingUsers) =
tryPairings(data, users.evenNumber) flatMap {
case Nil if users.isOdd => tryPairings(data, users.all)
case x => fuccess(x)
}
pairings -> Nil
}
val smartHardLimit = 24
private def tryPairings(data: Data, users: List[String]): Fu[Pairings] = {
import data._
if (users.size < 2) fuccess(Nil)
else PlayerRepo.rankedByTourAndUserIds(
tour.id,
users.toSet diff playingUserIds,
ranking) map { idles =>
if (recentPairings.isEmpty) naivePairings(tour, idles)
else
smartPairings(data, idles take smartHardLimit) :::
naivePairings(tour, idles drop smartHardLimit)
}
else PlayerRepo.rankedByTourAndUserIds(tour.id, users, ranking) map { idles =>
if (recentPairings.isEmpty) naivePairings(tour, idles)
else
smartPairings(data, idles take smartHardLimit) :::
naivePairings(tour, idles drop smartHardLimit)
}
}
private def naivePairings(tour: Tournament, players: RankedPlayers) =
@ -81,11 +78,12 @@ object PairingSystem extends AbstractPairingSystem {
// lower is better
def pairingScore(pair: RankedPairing): Score = pair match {
case (a, b) if justPlayedTogether(a.player.userId, b.player.userId) =>
if (veryMuchJustPlayedTogether(a.player.userId, b.player.userId)) 9000 * 1000
else 8000 * 1000
case (a, b) => Math.abs(a.rank - b.rank) * 1000 +
Math.abs(a.player.rating - b.player.rating)
Math.abs(a.player.rating - b.player.rating) +
justPlayedTogether(a.player.userId, b.player.userId).?? {
if (veryMuchJustPlayedTogether(a.player.userId, b.player.userId)) 9000 * 1000
else 8000 * 1000
}
}
def score(pairs: Combination): Score = pairs.foldLeft(0) {
case (s, p) => s + pairingScore(p)

View File

@ -4,15 +4,17 @@ import lila.socket.WithSocket
package object tournament extends PackageObject with WithPlay with WithSocket {
private[tournament] type Players = List[tournament.Player]
private[tournament]type Players = List[tournament.Player]
private[tournament] type RankedPlayers = List[RankedPlayer]
private[tournament]type RankedPlayers = List[RankedPlayer]
private[tournament] type Pairings = List[tournament.Pairing]
private[tournament]type Pairings = List[tournament.Pairing]
private[tournament] type Events = List[tournament.Event]
private[tournament]type Events = List[tournament.Event]
private[tournament] type Ranking = Map[String, Int]
private[tournament]type Ranking = Map[String, Int]
private[tournament]type Waiting = Map[String, Int]
private[tournament] object RandomName {
@ -31,6 +33,4 @@ case class RankedPlayer(rank: Int, player: Player) {
}
case class Winner(tourId: String, tourName: String, userId: String)
private[tournament] case class AllUserIds(all: List[String], waiting: List[String])
}

View File

@ -40,7 +40,7 @@ object Dependencies {
val RM = "org.reactivemongo" %% "reactivemongo" % "0.10.5.0.akka23"
val PRM = "org.reactivemongo" %% "play2-reactivemongo" % "0.10.5.0.akka23"
val maxmind = "com.sanoma.cda" %% "maxmind-geoip2-scala" % "1.2.3-THIB"
val prismic = "io.prismic" %% "scala-kit" % "1.3.1"
val prismic = "io.prismic" %% "scala-kit" % "1.3.3"
object play {
val version = "2.3.9"

View File

@ -1514,8 +1514,9 @@ div.game_tournament .clock {
div.game_tournament table.standing {
border-bottom: none;
}
div.game_tournament table.standing tr.me td {
background: #e4e4e4;
div.game_tournament table.standing tr.me td:first-child {
border-left: 10px solid #d59120;
padding-left: 5px;
}
div.game_tournament table.standing td {
padding: 0 10px;

View File

@ -439,6 +439,9 @@ body {
overflow-x: hidden;
cursor: default;
}
body.no-bg {
background: none!important;
}
body.no-select {
-moz-user-select: none;
-webkit-user-select: none;

View File

@ -390,9 +390,6 @@ body.dark #puzzle > .right .please_vote {
background: #103410;
color: #74a962;
}
body.dark div.game_tournament table.standing tr.me td {
background: #333;
}
body.dark #tournament_list table.slist .icon span {
color: rgba(200,200,200,0.5);
text-shadow: 2px 2px 2px #000;

View File

@ -1,4 +1,4 @@
var merge = require('lodash/object/merge');
var merge = require('merge');
var m = require('mithril');
var chess = require('./chess');
var puzzle = require('./puzzle');
@ -16,7 +16,7 @@ module.exports = function(cfg) {
if (cfg.user) cfg.user.history = cfg.user.history || [];
merge(data, cfg);
merge.recursive(data, cfg);
data.puzzle.initialMove = puzzle.str2move(data.puzzle.initialMove);

View File

@ -35,7 +35,7 @@ function miniGame(game) {
module.exports = {
title: function(ctrl) {
if (ctrl.data.schedule && ctrl.data.schedule.freq.indexOf('marathon') !== -1)
if (ctrl.data.schedule && ctrl.data.schedule.freq.indexOf('Marathon') !== -1)
return m('h1.marathon_title', [
m('span.fire_trophy.marathonWinner', m('span[data-icon=\\]')),
ctrl.data.fullName