diff --git a/app/views/user/mini.scala.html b/app/views/user/mini.scala.html
index f969377786..69342525fd 100644
--- a/app/views/user/mini.scala.html
+++ b/app/views/user/mini.scala.html
@@ -1,4 +1,4 @@
-@(u: User, playing: Option[Game], blocked: Boolean, followable: Boolean, rel: Option[lila.relation.Relation])(implicit ctx: Context)
+@(u: User, playing: Option[Pov], blocked: Boolean, followable: Boolean, rel: Option[lila.relation.Relation])(implicit ctx: Context)
-@playing.map { g =>
-@gameFen(g, g.player(u).getOrElse(g.firstPlayer).color)
+@playing.map { pov =>
+@gameFen(pov.game, pov.color)
}
@ctx.userId.map { myId =>
@if(myId != u.id) {
diff --git a/app/views/user/opponents.scala.html b/app/views/user/opponents.scala.html
index f48a7d5a8c..e7cb282c74 100644
--- a/app/views/user/opponents.scala.html
+++ b/app/views/user/opponents.scala.html
@@ -1,8 +1,6 @@
@(u: User, sugs: List[lila.relation.Related])(implicit ctx: Context)
-@title = @{ "%s - %s".format(u.username, trans.favoriteOpponents()) }
-
-@user.layout(title = title) {
+@user.layout(title = "%s - %s".format(u.username, trans.favoriteOpponents())) {
@userLink(u, withOnline = false) @trans.favoriteOpponents()
@user.relatedTable(u, sugs)
diff --git a/bin/compile-client b/bin/compile-client
index 9102cff054..48abc7d1d8 100755
--- a/bin/compile-client
+++ b/bin/compile-client
@@ -11,7 +11,7 @@ for app in editor puzzle round analyse; do
cd -
done
-for file in strongSocket.js tv.js common.js big.js chart2.js user.js coordinate.js; do
+for file in socket.js tv.js common.js big.js chart2.js user.js coordinate.js; do
orig=public/javascripts/$file
comp=public/compiled/$file
if [[ ! -f $comp || $orig -nt $comp ]]; then
diff --git a/conf/messages b/conf/messages
index 7dc8a594ec..7659a5b309 100644
--- a/conf/messages
+++ b/conf/messages
@@ -169,6 +169,7 @@ tournaments=Tournaments
tournamentPoints=Tournament points
viewTournament=View tournament
backToTournament=Return to tournament
+backToGame=Return to game
freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents=Free online Chess game. Play Chess now in a clean interface. No registration, no ads, no plugin required. Play Chess with the computer, friends or random opponents.
teams=Teams
nbMembers=%s members
@@ -305,3 +306,5 @@ thisPuzzleIsCorrect=This puzzle is correct and interesting
thisPuzzleIsWrong=This puzzle is wrong or boring
youHaveNbSecondsToMakeYourFirstMove=You have %s seconds to make your first move!
nbGamesInPlay=%s games in play
+automaticallyProceedToNextGameAfterMoving=Automatically proceed to next game after moving
+autoSwitch=Auto switch
diff --git a/conf/messages.af b/conf/messages.af
index 3ee5c3f8be..6d765b5e3f 100644
--- a/conf/messages.af
+++ b/conf/messages.af
@@ -47,9 +47,12 @@ computerAnalysisInProgress=Analise de computador em progresso
theComputerAnalysisHasFailed=Die rekenaar analise het misluk
viewTheComputerAnalysis=Sien die rekenaar analise
requestAComputerAnalysis=Versoek 'n rekenaar analise
+computerAnalysis=Analisi del computer
+analysis=Analisi
blunders=Flaters
mistakes=Foute
inaccuracies=Onakurate
+moveTimes=Tempo mossa
flipBoard=Draai bord
threefoldRepetition=Herhaal drie keer
claimADraw=Kies gelykop
diff --git a/conf/messages.ar b/conf/messages.ar
index 2ec8a96203..4a8f680ea1 100644
--- a/conf/messages.ar
+++ b/conf/messages.ar
@@ -81,6 +81,7 @@ players=لاعبي الشطرنج
minutesPerSide=دقائق لكل طرف
variant=النوع
timeControl=التحكم بالوقت
+realTime=الوقت الفعلي
correspondence=طويل الأمد
daysPerTurn=يوم لكل نقلة
oneDay=يوم واحد
diff --git a/conf/messages.bg b/conf/messages.bg
index d7dccb51de..156a3fb414 100644
--- a/conf/messages.bg
+++ b/conf/messages.bg
@@ -81,6 +81,7 @@ players=Играчи
minutesPerSide=Минути за страна
variant=Вариант
timeControl=Контрол на времето
+realTime=Засечено време
correspondence=Кореспонденция
daysPerTurn=Дни за ход
oneDay=Един ден
@@ -92,6 +93,11 @@ password=Парола
haveAnAccount=Имате акаунт?
allYouNeedIsAUsernameAndAPassword=Всичко, което ви е необходимо е потребителско име и парола
changePassword=Смяна на парола
+changeEmail=Смяна на имейла
+email=Електронна Поща
+emailIsOptional=Електронната поща не е задължителна. Lichess ще използва вашия адрес, за да възстанови паролата ви, ако я забравите
+passwordReset=Паролата е нулирана
+forgotPassword=Забравихте ли си паролаата ?
learnMoreAboutLichess=Научи повече за Lichess
rank=Ранг
gamesPlayed=Играни игри
@@ -299,3 +305,5 @@ thisPuzzleIsCorrect=Тази задача е правилна и интерес
thisPuzzleIsWrong=Тази задача е грешна или скучна
youHaveNbSecondsToMakeYourFirstMove=Имате %s секунди да направите първия си ход!
nbGamesInPlay=%s игри в процес
+automaticallyProceedToNextGameAfterMoving=Автоматично превключи на следващата игра след ход
+autoSwitch=Автоматично превключи
diff --git a/conf/messages.bs b/conf/messages.bs
index b0786b92c3..130b70f327 100644
--- a/conf/messages.bs
+++ b/conf/messages.bs
@@ -81,6 +81,7 @@ players=Igrači
minutesPerSide=Minuta po igraču
variant=Varijanta
timeControl=Vremenska kontrola
+realTime=Stvarno Vrijeme
correspondence=Dopisni sah
daysPerTurn=Dana po potezu
oneDay=Jedan dan
@@ -92,6 +93,11 @@ password=Lozinka
haveAnAccount=Već imate račun?
allYouNeedIsAUsernameAndAPassword=Sve što Vam treba je korisničko ime i lozinka.
changePassword=Promijeni lozinku
+changeEmail=Promijeni e-Mail
+email=e-Mail
+emailIsOptional=e-Mail je neoavezan. Lichess ce je koristiti da resetuje vasu Lozinku ako je zaboravite.
+passwordReset=resetuj Lozinku
+forgotPassword=Zaboravio si Lozinku ?
learnMoreAboutLichess=Naučite više o Lichessu
rank=Poredak
gamesPlayed=Broj odigranih partija
diff --git a/conf/messages.ca b/conf/messages.ca
index f7b3c90e7c..186f4a8dbc 100644
--- a/conf/messages.ca
+++ b/conf/messages.ca
@@ -81,6 +81,7 @@ players=Jugadors d'escacs
minutesPerSide=Minuts per jugador
variant=Variant
timeControl=Control de temps
+realTime=Temps real
correspondence=Correspondència
daysPerTurn=Dies per torn
oneDay=Un dia
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Aquest trencaclosques és correcte i interessant
thisPuzzleIsWrong=Aquest trencaclosques està malament o és avorrit
youHaveNbSecondsToMakeYourFirstMove=Disposes de %s segons per realitzar el teu primer moviment!
nbGamesInPlay=%s partides en joc
+automaticallyProceedToNextGameAfterMoving=Anar automàticament a la següent partida després de cada moviment
+autoSwitch=Canvi automàtic
diff --git a/conf/messages.cs b/conf/messages.cs
index fc6a815526..6f5db2ef4f 100644
--- a/conf/messages.cs
+++ b/conf/messages.cs
@@ -81,6 +81,7 @@ players=Hráči
minutesPerSide=Minut pro každého hráče
variant=Varianta
timeControl=Tempo hry
+realTime=Skutečný čas
correspondence=Korespondenčně
daysPerTurn=Dnů na tah
oneDay=Jeden den
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Tato úloha je správná a zajímavá
thisPuzzleIsWrong=Tato úloha je špatná nebo nudná
youHaveNbSecondsToMakeYourFirstMove=Máte %s sekund na provedení prvního tahu!
nbGamesInPlay=%s rozehraných her
+automaticallyProceedToNextGameAfterMoving=Automaticky přejdi k další hře po tahu
+autoSwitch=Přepni automaticky
diff --git a/conf/messages.da b/conf/messages.da
index c813eec934..ad99b2e342 100644
--- a/conf/messages.da
+++ b/conf/messages.da
@@ -81,6 +81,7 @@ players=Skakspillere
minutesPerSide=Minutter per spiller
variant=Variant
timeControl=Tidskontrol
+realTime=Real time
correspondence=Korrespondance
daysPerTurn=Dage per træk
oneDay=En dag
@@ -92,7 +93,10 @@ password=Password
haveAnAccount=Har du en konto?
allYouNeedIsAUsernameAndAPassword=Det eneste du skal bruge, er et brugernavn og en adgangskode.
changePassword=Skift kodeord
+changeEmail=Ændr email
email=Email
+emailIsOptional=Email er valgfri. Lichess vil bruge din email til at sende et nyt pasord, hvis du glemmer det eksisterende.
+passwordReset=Reset pasord
forgotPassword=Glemt adgangskode?
learnMoreAboutLichess=Få mere at vide om Lichess
rank=Rang
diff --git a/conf/messages.de b/conf/messages.de
index 6873a1dcee..79770daca3 100644
--- a/conf/messages.de
+++ b/conf/messages.de
@@ -86,7 +86,7 @@ correspondence=Fernschach
daysPerTurn=Tage pro Zug
oneDay=ein Tag
nbDays=%s Tage
-nbHours=%s stunden
+nbHours=%s Stunden
time=Zeit
username=Benutzername
password=Passwort
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Dieses Rätsel ist korrekt und interessant.
thisPuzzleIsWrong=Dieses Rätsel ist falsch oder langweilig.
youHaveNbSecondsToMakeYourFirstMove=Du hast %s Sekunden, um deinen ersten Zug zu machen!
nbGamesInPlay=%s laufende Spiele
+automaticallyProceedToNextGameAfterMoving=Nach dem Zug automatisch zur nächsten Partie gehen
+autoSwitch=Automatischer Wechsel
diff --git a/conf/messages.el b/conf/messages.el
index f3799e0df4..8098f5d15e 100644
--- a/conf/messages.el
+++ b/conf/messages.el
@@ -81,6 +81,7 @@ players=Σκακιστές
minutesPerSide=Λεπτά ανά πλευρά
variant=Εκδοχή
timeControl=Χρονόμετρο
+realTime=Πραγματικού χρόνου
correspondence=Αλληλογραφία
daysPerTurn=Μέρες ανά σειρά
oneDay=Μία μέρα
diff --git a/conf/messages.es b/conf/messages.es
index 640eb4356e..b02f7ceacd 100644
--- a/conf/messages.es
+++ b/conf/messages.es
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Este puzzle es correcto e interesante
thisPuzzleIsWrong=Este puzzle es erróneo o aburrido
youHaveNbSecondsToMakeYourFirstMove=¡Dispone de %s segundos para hacer su primer movimiento!
nbGamesInPlay=%s partidas en juego
+automaticallyProceedToNextGameAfterMoving=Continuar automáticamente al siguiente juego al mover
+autoSwitch=Cambio automático
diff --git a/conf/messages.fa b/conf/messages.fa
index c7e7612dc8..c85414f03f 100644
--- a/conf/messages.fa
+++ b/conf/messages.fa
@@ -81,6 +81,7 @@ players=بازیکنان شطرنج
minutesPerSide=دقیقه در هر طرف
variant=شاخه
timeControl=کنترل زمان
+realTime=بلادرنگ
correspondence=مکاتبه ای
daysPerTurn=روز برای هر حرکت
oneDay=یک روز
@@ -92,6 +93,11 @@ password=رمزعبور
haveAnAccount=شما صاحب حساب کاربری می باشید؟
allYouNeedIsAUsernameAndAPassword=تمام چیزی که شما نیاز دارید نام کاربری و رمزعبور می باشد
changePassword=تغییر کلمه عبور
+changeEmail=تغییر ایمیل
+email=ایمیل
+emailIsOptional=.ایمیل ضروری نیست. زمانیکه پسورد خود را فراموش کنید لایچس از این ایمیل استفاده میکند تا شما پسورد جدیدی دریافت کنید
+passwordReset=باز نشانی پسورد
+forgotPassword=آیا پسورد را فراموش کرده اید؟
learnMoreAboutLichess=Lichess بیشتر بدانید درباره
rank=رتبه
gamesPlayed=بازی های انجام شده
@@ -299,3 +305,5 @@ thisPuzzleIsCorrect=این جدول صحیح و جالب است
thisPuzzleIsWrong=این جدول اشتباه یا خسته کننده است
youHaveNbSecondsToMakeYourFirstMove=! ثانیه فرصت دارید %s شما برای انجام اولین حرکت خود فقط
nbGamesInPlay=بازی در حال انجام است %s
+automaticallyProceedToNextGameAfterMoving=حرکت کردن اتوماتیک برای بازی بعدی بعد از حرکت کردن
+autoSwitch=تعویض خودکار
diff --git a/conf/messages.fi b/conf/messages.fi
index 5dd21c96c0..b0446a860e 100644
--- a/conf/messages.fi
+++ b/conf/messages.fi
@@ -81,7 +81,7 @@ players=Pelaajat
minutesPerSide=Minuuttia per puoli
variant=Variantti
timeControl=Ajan hallinta
-realTime=Oikea aika
+realTime=Reaaliaikainen
correspondence=Kirjeshakki
daysPerTurn=Päivää per vuoro
oneDay=Yksi päivä
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Tämä tehtävä toimii ja on mielenkiintoinen
thisPuzzleIsWrong=Tämä tehtävä on tylsä tai virheellinen
youHaveNbSecondsToMakeYourFirstMove=Sinulla on %s sekuntia aikaa ensimmäisen siirtosi tekemiseen
nbGamesInPlay=%s peliä meneillään
+automaticallyProceedToNextGameAfterMoving=Siirry automaattisesti seuraavaan peliin siirron jälkeen
+autoSwitch=Automaattinen siirtyminen
diff --git a/conf/messages.fr b/conf/messages.fr
index 8df8a5a353..58eb7e03b2 100644
--- a/conf/messages.fr
+++ b/conf/messages.fr
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Ce problème est correct et intéressant
thisPuzzleIsWrong=Ce problème est erroné ou ennuyeux
youHaveNbSecondsToMakeYourFirstMove=Vous avez %s secondes pour jouer votre premier coup !
nbGamesInPlay=%s parties en cours
+automaticallyProceedToNextGameAfterMoving=Aller automatiquement à la prochaine partie après coup
+autoSwitch=Changement automatique
diff --git a/conf/messages.gu b/conf/messages.gu
index bb32cbd425..0852cfcd5b 100644
--- a/conf/messages.gu
+++ b/conf/messages.gu
@@ -5,3 +5,14 @@ gameOver=રમત પુરી
waitingForOpponent=વિરોધિ માટે રાહ જુએ છે
waiting=રાહ જુએ છે
yourTurn=તમરો વારો
+level=પાળવ
+chat=વાત ચીત
+resign=રાજીનામું
+white=સફેદ
+black=કાળુ
+createAGame=રમત બનાવો
+whiteIsVictorious=સફેદ વિજય રહ્યો
+blackIsVictorious=કાળો વિજય રહ્યો
+playWithTheSameOpponentAgain=આજ પ્રાતીસ્પર્ધી સાથે ફરી રમો
+newOpponent=નવો પ્રાતીસ્પર્ધી
+joinTheGame=રમત માં જોડવ
diff --git a/conf/messages.he b/conf/messages.he
index 786e304b4b..cdbb3764d2 100644
--- a/conf/messages.he
+++ b/conf/messages.he
@@ -81,6 +81,7 @@ players=שחקנים
minutesPerSide=דקות עבור כל צד
variant=סוג משחק
timeControl=כמות זמן
+realTime=זמן אמת
correspondence=התכתבות
daysPerTurn=ימים לצעד
oneDay=יום אחד
diff --git a/conf/messages.hr b/conf/messages.hr
index c5edf398e0..e74035555a 100644
--- a/conf/messages.hr
+++ b/conf/messages.hr
@@ -81,6 +81,7 @@ players=Igrači
minutesPerSide=Minuta po igraču
variant=Varijanta
timeControl=Vremenska kontrola
+realTime=Stvarno vrijeme
correspondence=Korespodencija
daysPerTurn=Dana po potezu
oneDay=Jedan dan
@@ -92,6 +93,11 @@ password=Zaporka
haveAnAccount=Imate li otvoren račun?
allYouNeedIsAUsernameAndAPassword=Sve što trebate je samo korisničko ime i zaporka.
changePassword=Promijeni zaporku
+changeEmail=Promijeni email
+email=Email
+emailIsOptional=Email je neobavezan. Lichess će koristiti vaš mail kako bi vam povratio šifru ako ju zaboravite.
+passwordReset=Resetirajte lozinku
+forgotPassword=Zaboravili ste lozinku?
learnMoreAboutLichess=Nauči više o Lichessu
rank=Rang
gamesPlayed=Broj odigranih igara
@@ -299,3 +305,5 @@ thisPuzzleIsCorrect=Ovaj problem je točan i zanimljiv
thisPuzzleIsWrong=Ovaj problem je pogresan i dosadan
youHaveNbSecondsToMakeYourFirstMove=Imate %s sekunti da napravite svoj prvi potez!
nbGamesInPlay=%s partije koje se upravo igraju
+automaticallyProceedToNextGameAfterMoving=Automatski prebaci na sljedeću partiju nakon odigranog poteza
+autoSwitch=Prebaci automatski
diff --git a/conf/messages.hu b/conf/messages.hu
index 1ab4dedef5..68697d1fb6 100644
--- a/conf/messages.hu
+++ b/conf/messages.hu
@@ -81,6 +81,7 @@ players=Játékosok
minutesPerSide=Perc játékosonként
variant=Változat
timeControl=Játék időre
+realTime=Valós Idő
correspondence=Levelezés
daysPerTurn=Lépésenkénti napok száma
oneDay=Egy nap
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ez a feladvány helyes és érdekes
thisPuzzleIsWrong=Ez a feladvány hibás vagy unalmas
youHaveNbSecondsToMakeYourFirstMove=%s másodperced van hogy megtedd az első lépést!
nbGamesInPlay=%s meccset játszanak éppen az oldalon
+automaticallyProceedToNextGameAfterMoving=Automatikusan menjen a következő játékhoz amiután lépett
+autoSwitch=Automatikus váltás
diff --git a/conf/messages.id b/conf/messages.id
index 1d30b0c6d5..dc8ee4acd6 100644
--- a/conf/messages.id
+++ b/conf/messages.id
@@ -81,6 +81,7 @@ players=Pemain catur
minutesPerSide=Menit untuk tiap sisi
variant=Variasi
timeControl=Kontrol waktu
+realTime=Langsung
correspondence=Korespondensi
daysPerTurn=Hari per langkah
oneDay=Satu hari
@@ -92,6 +93,11 @@ password=Kata kunci
haveAnAccount=Punya akun?
allYouNeedIsAUsernameAndAPassword=Semua yang anda butuhkan adalah nama pengguna dan kata kunci.
changePassword=Ganti kata kunci
+changeEmail=Ubah email
+email=Email
+emailIsOptional=Email tidak diwajibkan. Lichess akan menggunakan email anda untuk me-reset password anda bila anda lupa.
+passwordReset=Reset password
+forgotPassword=Lupa password?
learnMoreAboutLichess=Pelajari lebih lanjut tentang Lichess
rank=Pangkat
gamesPlayed=Permainan yang telah dimainkan
diff --git a/conf/messages.is b/conf/messages.is
index 8c8ab4a8b1..1cd361da17 100644
--- a/conf/messages.is
+++ b/conf/messages.is
@@ -81,6 +81,7 @@ players=Skákmenn
minutesPerSide=Mínútur á lið
variant=Afbrigði
timeControl=Tímaskorður
+realTime=Rauntími
daysPerTurn=Dagar á leik
oneDay=Einn dagur
nbDays=%s dagar
@@ -303,3 +304,5 @@ thisPuzzleIsCorrect=Þessi þraut er rétt og áhugaverð
thisPuzzleIsWrong=Þessi þraut er röng eða leiðinleg
youHaveNbSecondsToMakeYourFirstMove=Þú hefur %s sekúndur til þess að leika fyrsta leik!
nbGamesInPlay=%s leikir í gangi
+automaticallyProceedToNextGameAfterMoving=Skipta sjálfkrafa um skák eftir leik
+autoSwitch=Skipta sjálfkrafa
diff --git a/conf/messages.it b/conf/messages.it
index 27c9b7d7a0..f253822924 100644
--- a/conf/messages.it
+++ b/conf/messages.it
@@ -70,9 +70,9 @@ viewInFullSize=Visualizza a schermo intero
logOut=Esci
signIn=Entra
newToLichess=Nuovo su Lichess?
-youNeedAnAccountToDoThat=Ti servei un account per farlo
+youNeedAnAccountToDoThat=Ti serve un account per farlo
signUp=Registrati
-computersAreNotAllowedToPlay=Non è permesso giocare a computer o a giocatori che si fanno aiutare dai computer. Mentre giochi, non farti aiutare da programmi di scacchi, da database o da altre persone.
+computersAreNotAllowedToPlay=Non è permesso giocare a computer o a giocatori che si fanno aiutare dai computer. Mentre giochi, non farti aiutare da programmi di scacchi, da database o da altre persone. Inoltre si sconsiglia vivamente di creare account multipli, pena la cancellazione dal sito.
games=Partite
forum=Forum
xPostedInForumY=%s ha postato nel forum %s
@@ -81,7 +81,7 @@ players=Giocatori
minutesPerSide=Minuti per lato
variant=Variante
timeControl=Controllo del tempo
-realTime=Diretta
+realTime=Partita a tempo
correspondence=Corrispondenza
daysPerTurn=Giorni per turno
oneDay=Un giorno
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Questo esercizio è corretto e interessante
thisPuzzleIsWrong=Questo esercizio è sbagliato o noioso
youHaveNbSecondsToMakeYourFirstMove=Hai %s secondi per fare la tua prima mossa
nbGamesInPlay=%s partite in gioco
+automaticallyProceedToNextGameAfterMoving=Passa automaticamente alla partita successiva dopo aver mosso
+autoSwitch=Passaggio automatico
diff --git a/conf/messages.ja b/conf/messages.ja
index b3441a8626..22a0de001a 100644
--- a/conf/messages.ja
+++ b/conf/messages.ja
@@ -81,8 +81,10 @@ players=プレイヤー
minutesPerSide=持ち時間
variant=バリアント
timeControl=持ち時間制限
+realTime=実時間
correspondence=通信チェス
daysPerTurn=制限日数
+oneDay=一日
nbDays=%s日
nbHours=%s時間
time=時間
diff --git a/conf/messages.ko b/conf/messages.ko
index 9e50f7b2af..f2a256951d 100644
--- a/conf/messages.ko
+++ b/conf/messages.ko
@@ -81,6 +81,7 @@ players=플레이어
minutesPerSide=진영 당 주어진 시간(분)
variant=모드
timeControl=시간 제한
+realTime=시간 제한
correspondence=긴 체스
daysPerTurn=한 턴에 걸리는 날짜
oneDay=하루
@@ -92,6 +93,11 @@ password=비밀번호
haveAnAccount=계정이 있습니까?
allYouNeedIsAUsernameAndAPassword=사용자 이름과 비밀번호만 입력하시면 됩니다
changePassword=비밀번호 변경
+changeEmail=메일 주소 변경
+email=메일
+emailIsOptional=메일 주소는 선택 항목입니다. 비밀번호를 잊어버렸을 때 초기화하기 위해 메일 주소를 사용합니다.
+passwordReset=비밀번호 초기화
+forgotPassword=비밀번호를 잊어버리셨나요?
learnMoreAboutLichess=Lichess에 대해 좀 더 알아보기
rank=순위
gamesPlayed=게임
@@ -299,3 +305,5 @@ thisPuzzleIsCorrect=이 퍼즐은 정확하고 재밌습니다
thisPuzzleIsWrong=이 퍼즐은 오류가 있거나 지루합니다
youHaveNbSecondsToMakeYourFirstMove=%s초 안에 첫 수를 놓으세요!
nbGamesInPlay=%s개의 게임 플레이 중
+automaticallyProceedToNextGameAfterMoving=수를 둔 다음에 자동으로 다음 게임으로 이동
+autoSwitch=자동 전환
diff --git a/conf/messages.nb b/conf/messages.nb
index 63ec977d9e..5346c10d3b 100644
--- a/conf/messages.nb
+++ b/conf/messages.nb
@@ -81,6 +81,7 @@ players=Sjakkspillere
minutesPerSide=Minutter per side
variant=Variant
timeControl=Tidskontroll
+realTime=Sanntid
correspondence=Fjernsjakk
daysPerTurn=Dager per trekk
oneDay=Én dag
diff --git a/conf/messages.nl b/conf/messages.nl
index 4dafe735b6..0aedcbe9b4 100644
--- a/conf/messages.nl
+++ b/conf/messages.nl
@@ -81,7 +81,8 @@ players=Geregistreerde spelers en spelers die online zijn
minutesPerSide=Minuten per speler
variant=Variant
timeControl=Speelduur
-correspondence=Correspondensie
+realTime=Live
+correspondence=Correspondentie
daysPerTurn=Dagen per zet
oneDay=Eén dag
nbDays=%s dagen
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=De puzzel is correct en interessant
thisPuzzleIsWrong=De puzzel is fout of saai
youHaveNbSecondsToMakeYourFirstMove=Je hebt %s seconden om je eerste zet te doen!
nbGamesInPlay=%s partijen bezig
+automaticallyProceedToNextGameAfterMoving=Automatische doorgaan naar de volgende partij na uw zet
+autoSwitch=Automatische switch
diff --git a/conf/messages.nn b/conf/messages.nn
index ada5b6cafe..b711c07e40 100644
--- a/conf/messages.nn
+++ b/conf/messages.nn
@@ -81,6 +81,7 @@ players=Sjakkspelarar
minutesPerSide=Minutt per side
variant=Variant
timeControl=Tidskontroll
+realTime=Sanntid
correspondence=Fjernsjakk
daysPerTurn=Dagar per trekk
oneDay=Ein dag
diff --git a/conf/messages.pl b/conf/messages.pl
index 7dd4cedb21..d0b3bb8ccb 100644
--- a/conf/messages.pl
+++ b/conf/messages.pl
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=To zadanie jest poprawne i ciekawe
thisPuzzleIsWrong=To zadanie jest niepoprawne lub nudne
youHaveNbSecondsToMakeYourFirstMove=Masz %s sekund na pierwszy ruch!
nbGamesInPlay=%s gier w trakcie
+automaticallyProceedToNextGameAfterMoving=Automatycznie przejdź do następnej gry po wykonaniu ruchu
+autoSwitch=Auto przełączanie
diff --git a/conf/messages.pt b/conf/messages.pt
index d66821fe6e..b15340b01c 100644
--- a/conf/messages.pt
+++ b/conf/messages.pt
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=O quebra-cabeças está correcto e é interessante
thisPuzzleIsWrong=O quebra-cabeças está errado ou é chato
youHaveNbSecondsToMakeYourFirstMove=Tens %s segundos para fazeres a primeira jogada!
nbGamesInPlay=%s partidas em andamento
+automaticallyProceedToNextGameAfterMoving=Automaticamente passa ao jogo seguinte após seu lance
+autoSwitch=Auto ciclo
diff --git a/conf/messages.ru b/conf/messages.ru
index 224f301099..6633205837 100644
--- a/conf/messages.ru
+++ b/conf/messages.ru
@@ -81,7 +81,7 @@ players=Игроки
minutesPerSide=Минут на партию
variant=Вариант
timeControl=Контроль времени
-realTime=Реальное время
+realTime=Время на ход
correspondence=По переписке
daysPerTurn=Дней на ход
oneDay=Ежедневно
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Задача правильная и интересная
thisPuzzleIsWrong=Задача неправильная или неинтересная
youHaveNbSecondsToMakeYourFirstMove=У Вас %s секунд, чтобы сделать свой первый ход!
nbGamesInPlay=%s партий играются
+automaticallyProceedToNextGameAfterMoving=Автоматически переходить к следующей игре после хода
+autoSwitch=Автосмена
diff --git a/conf/messages.sk b/conf/messages.sk
index 9c9e466ba9..6c98bed62a 100644
--- a/conf/messages.sk
+++ b/conf/messages.sk
@@ -81,6 +81,7 @@ players=Hráči
minutesPerSide=Minút na stranu
variant=Varianta
timeControl=Nastavenie času
+realTime=Skutočný čas
correspondence=Korešpondenčný šach
daysPerTurn=Počet dní na ťah
oneDay=Jeden deň
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Tento rébus je správny a pútavý
thisPuzzleIsWrong=Tento rébus je nesprávny alebo nudný
youHaveNbSecondsToMakeYourFirstMove=%s sekúnd na vykonanie prvého ťahu!
nbGamesInPlay=%s hier sa hrá
+automaticallyProceedToNextGameAfterMoving=Automaticky prejdi k ďalšej hre po ťahu
+autoSwitch=Prepni automaticky
diff --git a/conf/messages.sl b/conf/messages.sl
index ccd3fc5037..bf35c227af 100644
--- a/conf/messages.sl
+++ b/conf/messages.sl
@@ -49,7 +49,7 @@ viewTheComputerAnalysis=Poglej računalniško analizo
requestAComputerAnalysis=Zahtevaj računalniško analizo
computerAnalysis=Računalniška analiza
analysis=Analiza
-blunders=Hude napake
+blunders=Grobe napake
mistakes=Napake
inaccuracies=Nenatančnosti
moveTimes=Čas za potezo
@@ -61,7 +61,7 @@ draw=Remi
nbConnectedPlayers=%s igralcev
gamesBeingPlayedRightNow=Število trenutnih iger
viewAllNbGames=%s Iger
-viewNbCheckmates=Oglej si mat pozicije %s
+viewNbCheckmates=%s mat pozicij
nbBookmarks=%s zaznamkov
nbPopularGames=%s priljubljenih partij
nbAnalysedGames=%s analiziranih Iger
@@ -81,6 +81,7 @@ players=Igralci
minutesPerSide=Minut na igralca
variant=Različica
timeControl=Ura
+realTime=Standardno
correspondence=Korespondenčno
daysPerTurn=Število dni na potezo
oneDay=En dan
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ta problem je pravilen in zanimiv
thisPuzzleIsWrong=Ta problem je napačen oz. dolgočasen
youHaveNbSecondsToMakeYourFirstMove=Imaš še %s sekund za prvo potezo
nbGamesInPlay=%s igranih partij
+automaticallyProceedToNextGameAfterMoving=Po vsaki potezi samodejno preklopi na naslednjo partijo
+autoSwitch=Samodejno
diff --git a/conf/messages.sq b/conf/messages.sq
index d88430adaa..f0a1e6819b 100644
--- a/conf/messages.sq
+++ b/conf/messages.sq
@@ -81,6 +81,7 @@ players=Lojtarë
minutesPerSide=Minuta për palë
variant=Varianti
timeControl=Kontolla e kohës
+realTime=Kha reale
correspondence=korespondenca
daysPerTurn=turne për ditë
oneDay=një ditë
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ky mister është korekt dhe interesant
thisPuzzleIsWrong=Kjo levizje është e gabuar ose e bezëdisëshme
youHaveNbSecondsToMakeYourFirstMove=Ju keni%s sekonda për të bërë lëvizje tuaj të parë
nbGamesInPlay=%s lojëra duke u luajtur
+automaticallyProceedToNextGameAfterMoving=Procesimi Automatik i lojës ,më pas duke luajtur potezin
+autoSwitch=Mbyllje automatike
diff --git a/conf/messages.sv b/conf/messages.sv
index 761d05bc10..f097525bd2 100644
--- a/conf/messages.sv
+++ b/conf/messages.sv
@@ -81,6 +81,7 @@ players=Schackspelare
minutesPerSide=Minuter per spelare
variant=Variant
timeControl=Tidskontroll
+realTime=Realtid
correspondence=Korrespondens
daysPerTurn=Dagar per speltur
oneDay=En dag
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Det här schackproblemet är korrekt och intressant
thisPuzzleIsWrong=Det här schackproblemet är fel eller tråkigt
youHaveNbSecondsToMakeYourFirstMove=du har %s sekunder för att göra första draget
nbGamesInPlay=%s partier spelas
+automaticallyProceedToNextGameAfterMoving=Gör att du automatiskt fortsätter till nästa parti efter du gjort ett drag
+autoSwitch=Automatiskt byte
diff --git a/conf/messages.th b/conf/messages.th
index 212df2c408..218e4482f0 100644
--- a/conf/messages.th
+++ b/conf/messages.th
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=ปริศนานี้ถูกต้อง และ
thisPuzzleIsWrong=ปริศนานี้ไม่ถูกต้อง หรือน่าเบื่อ
youHaveNbSecondsToMakeYourFirstMove=คุณมีเวลา %s วินาที ในการเริ่มเดิน!
nbGamesInPlay=%s เกมที่กำลังดำเนิน
+automaticallyProceedToNextGameAfterMoving=ดำเนินสู่เกมถัดไปอัตโนมัติหลังจากการเดิน
+autoSwitch=สลับอัตโนมัติ
diff --git a/conf/messages.tr b/conf/messages.tr
index d49869322e..25e63efd5c 100644
--- a/conf/messages.tr
+++ b/conf/messages.tr
@@ -305,3 +305,5 @@ thisPuzzleIsCorrect=Bu bulmaca doğru ve ilginç
thisPuzzleIsWrong=Bu bulmaca yanlış veya sıkıcı
youHaveNbSecondsToMakeYourFirstMove=İlk hamleni yapman için %s saniyen kaldı!
nbGamesInPlay=Şu anda %s oyun oynanıyor
+automaticallyProceedToNextGameAfterMoving=Hamleden sonra otomatik olarak diğer oyuna devam edecek
+autoSwitch=Otomatik Seç
diff --git a/conf/messages.uk b/conf/messages.uk
index d1992def23..7c25a9426d 100644
--- a/conf/messages.uk
+++ b/conf/messages.uk
@@ -81,6 +81,7 @@ players=Гравці
minutesPerSide=Хвилин на кожного
variant=Варіант
timeControl=Контроль часу
+realTime=Швидкі шахи
correspondence=Переписка
daysPerTurn=Днів на хід
oneDay=Один день
@@ -304,3 +305,5 @@ thisPuzzleIsCorrect=Ця задача є правильною та цікаво
thisPuzzleIsWrong=Ця задача є неправильною чи нудною
youHaveNbSecondsToMakeYourFirstMove=Ви маєте %s секунд, щоб зробити перший хід!
nbGamesInPlay=%s ігор тривають
+automaticallyProceedToNextGameAfterMoving=Автоматично перейти до наступної гри після ходу
+autoSwitch=Авт. перехід
diff --git a/conf/routes b/conf/routes
index ba89bd63a6..100193e702 100644
--- a/conf/routes
+++ b/conf/routes
@@ -60,7 +60,7 @@ GET /blog/:id/:slug controllers.Blog.show(id: String, slug: String, ref: O
GET /blog.atom controllers.Blog.atom(ref: Option[String] ?= None)
# Game
-GET /games controllers.Game.realtime
+GET /games controllers.Game.playing
GET /games/all controllers.Game.all(page: Int ?= 1)
GET /games/checkmate controllers.Game.checkmate(page: Int ?= 1)
GET /games/bookmark controllers.Game.bookmark(page: Int ?= 1)
@@ -89,6 +89,10 @@ GET /training/:id/load controllers.Puzzle.load(id: Int)
POST /training/:id/attempt controllers.Puzzle.attempt(id: Int)
POST /training/:id/vote controllers.Puzzle.vote(id: Int)
+# User Analysis
+GET /analysis/*urlFen controllers.UserAnalysis.load(urlFen: String)
+GET /analysis controllers.UserAnalysis.index
+
# Round
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")
GET /$gameId<\w{8}>/$color
controllers.Round.watcher(gameId: String, color: String)
@@ -97,9 +101,12 @@ GET /$gameId<\w{8}>/$color/socket controllers.Round.websocketWatc
GET /$fullId<\w{12}>/socket/v:apiVersion controllers.Round.websocketPlayer(fullId: String, apiVersion: Int)
GET /$gameId<\w{8}>/$color/side controllers.Round.sideWatcher(gameId: String, color: String)
GET /$fullId<\w{12}>/side controllers.Round.sidePlayer(fullId: String)
+GET /$gameId<\w{8}>/others controllers.Round.others(gameId: String)
+GET /$gameId<\w{8}>/next controllers.Round.next(gameId: String)
GET /$gameId<\w{8}>/continue/:mode controllers.Round.continue(gameId: String, mode: String)
POST /$gameId<\w{8}>/note controllers.Round.writeNote(gameId: String)
GET /$gameId<\w{8}>/edit controllers.Editor.game(gameId: String)
+GET /$gameId<\w{8}>/$color/analysis controllers.UserAnalysis.game(gameId: String, color: String)
# Round accessibility: text representation
GET /$fullId<\w{12}>/text controllers.Round.playerText(fullId: String)
@@ -241,7 +248,7 @@ DELETE /notification/$id<\w{8}> controllers.Notification.remove(id)
GET /paste controllers.Importer.importGame
POST /import controllers.Importer.sendGame
-# Progressive import API
+# Progressive Import API
POST /api/import/live controllers.Importer.liveCreate
POST /api/import/live/$id<\w{8}>/:move controllers.Importer.liveMove(id: String, move: String)
diff --git a/modules/api/src/main/UserApi.scala b/modules/api/src/main/UserApi.scala
index 30afdffbe9..1e64bbe801 100644
--- a/modules/api/src/main/UserApi.scala
+++ b/modules/api/src/main/UserApi.scala
@@ -41,11 +41,11 @@ private[api] final class UserApi(
def one(username: String, token: Option[String]): Fu[Option[JsObject]] = UserRepo named username flatMap {
case None => fuccess(none)
- case Some(u) => GameRepo nowPlaying u.id zip
+ case Some(u) => GameRepo lastPlayed u zip
makeUrl(R User username) zip
(check(token) ?? (knownEnginesSharingIp(u.id) map (_.some))) flatMap {
case ((gameOption, userUrl), knownEngines) => gameOption ?? { g =>
- makeUrl(R.Watcher(g.id, g.firstPlayer.color.name)) map (_.some)
+ makeUrl(R.Watcher(g.gameId, g.color.name)) map (_.some)
} map { gameUrlOption =>
jsonView(u, extended = true) ++ Json.obj(
"url" -> userUrl,
diff --git a/modules/chess b/modules/chess
index 2ff1c84d1d..afa9274da7 160000
--- a/modules/chess
+++ b/modules/chess
@@ -1 +1 @@
-Subproject commit 2ff1c84d1dd70531ab2fce7fc49691b67d539f33
+Subproject commit afa9274da767775f9f228f4d06d2e58ddb0176f6
diff --git a/modules/common/src/main/paginator/Paginator.scala b/modules/common/src/main/paginator/Paginator.scala
index 673a4c164b..92576ef416 100644
--- a/modules/common/src/main/paginator/Paginator.scala
+++ b/modules/common/src/main/paginator/Paginator.scala
@@ -20,12 +20,12 @@ final class Paginator[A] private[paginator] (
/**
* Returns the previous page.
*/
- def previousPage: Option[Int] = (currentPage != 1) option (currentPage - 1)
+ def previousPage: Option[Int] = (currentPage > 1) option (currentPage - 1)
/**
* Returns the next page.
*/
- def nextPage: Option[Int] = (currentPage != nbPages) option (currentPage + 1)
+ def nextPage: Option[Int] = (currentPage < nbPages) option (currentPage + 1)
/**
* Returns the number of pages.
diff --git a/modules/db/src/main/BSON.scala b/modules/db/src/main/BSON.scala
index 23c652adad..aabde44138 100644
--- a/modules/db/src/main/BSON.scala
+++ b/modules/db/src/main/BSON.scala
@@ -61,8 +61,25 @@ object BSON {
}
}
}
+
+ implicit def MapHandler[V](implicit vr: BSONReader[_ <: BSONValue, V], vw: BSONWriter[V, _ <: BSONValue]): BSONHandler[BSONDocument, Map[String, V]] = new BSONHandler[BSONDocument, Map[String, V]] {
+ private val reader = MapReader[V]
+ private val writer = MapWriter[V]
+ def read(bson: BSONDocument): Map[String, V] = reader read bson
+ def write(map: Map[String, V]): BSONDocument = writer write map
+ }
}
+ // List Handler
+ final class ListHandler[T](implicit reader: BSONReader[_ <: BSONValue, T], writer: BSONWriter[T, _ <: BSONValue]) extends BSONHandler[BSONArray, List[T]] {
+ def read(array: BSONArray) = array.stream.filter(_.isSuccess).map { v =>
+ reader.asInstanceOf[BSONReader[BSONValue, T]].read(v.get)
+ }.toList
+ def write(repr: List[T]) =
+ new BSONArray(repr.map(s => scala.util.Try(writer.write(s))).to[Stream])
+ }
+ implicit def bsonArrayToListHandler[T](implicit reader: BSONReader[_ <: BSONValue, T], writer: BSONWriter[T, _ <: BSONValue]): BSONHandler[BSONArray, List[T]] = new ListHandler
+
final class Reader(val doc: BSONDocument) {
val map = (doc.stream collect { case Success(e) => e }).toMap
diff --git a/modules/game/src/main/BinaryFormat.scala b/modules/game/src/main/BinaryFormat.scala
index d61517f362..e9d1d6a650 100644
--- a/modules/game/src/main/BinaryFormat.scala
+++ b/modules/game/src/main/BinaryFormat.scala
@@ -164,11 +164,14 @@ object BinaryFormat {
}
def read(ba: ByteArray): PieceMap = {
- def splitInts(int: Int) = Array(int >> 4, int & 0x0F)
+ def splitInts(b: Byte) = {
+ val int = b.toInt
+ Array(int >> 4, int & 0x0F)
+ }
def intPiece(int: Int): Option[Piece] =
intToRole(int & 7) map { role => Piece(Color((int & 8) == 0), role) }
- val (aliveInts, deadInts) = ba.value map toInt flatMap splitInts splitAt 64
- (Pos.all zip aliveInts flatMap {
+ val pieceInts = ba.value flatMap splitInts
+ (Pos.all zip pieceInts flatMap {
case (pos, int) => intPiece(int) map (pos -> _)
}).toMap
}
diff --git a/modules/game/src/main/Cached.scala b/modules/game/src/main/Cached.scala
index 779368f4a6..a3f7d9c983 100644
--- a/modules/game/src/main/Cached.scala
+++ b/modules/game/src/main/Cached.scala
@@ -6,29 +6,51 @@ import org.joda.time.DateTime
import play.api.libs.json.JsObject
import lila.db.api.$count
-import lila.memo.{ AsyncCache, ExpireSetMemo, Builder }
+import lila.db.BSON._
+import lila.memo.{ AsyncCache, MongoCache, ExpireSetMemo, Builder }
+import lila.user.{ User, UidNb }
import tube.gameTube
+import UidNb.UidNbBSONHandler
-final class Cached(ttl: Duration) {
+final class Cached(
+ mongoCache: MongoCache.Builder,
+ defaultTtl: FiniteDuration) {
def nbGames: Fu[Int] = count(Query.all)
def nbMates: Fu[Int] = count(Query.mate)
def nbImported: Fu[Int] = count(Query.imported)
def nbImportedBy(userId: String): Fu[Int] = count(Query imported userId)
- def nbPlaying(userId: String): Fu[Int] = count(Query nowPlaying userId)
+ def nbPlaying(userId: String): Fu[Int] = countShortTtl(Query nowPlaying userId)
+
+ private implicit val userHandler = User.userBSONHandler
+
+ private val isPlayingSimulCache = AsyncCache[String, Boolean](
+ f = userId => GameRepo.countPlayingRealTime(userId) map (1 <),
+ timeToLive = 10.seconds)
+
+ val isPlayingSimul: String => Fu[Boolean] = isPlayingSimulCache.apply _
val rematch960 = new ExpireSetMemo(3.hours)
- val activePlayerUidsDay = AsyncCache(
+ val activePlayerUidsDay = mongoCache[Int, List[UidNb]](
+ prefix = "player:active:day",
(nb: Int) => GameRepo.activePlayersSince(DateTime.now minusDays 1, nb),
timeToLive = 1 hour)
- val activePlayerUidsWeek = AsyncCache(
+ val activePlayerUidsWeek = mongoCache[Int, List[UidNb]](
+ prefix = "player:active:week",
(nb: Int) => GameRepo.activePlayersSince(DateTime.now minusWeeks 1, nb),
timeToLive = 6 hours)
- private val count = AsyncCache((o: JsObject) => $count(o), timeToLive = ttl)
+ private val countShortTtl = AsyncCache[JsObject, Int](
+ f = (o: JsObject) => $count(o),
+ timeToLive = 5.seconds)
+
+ private val count = mongoCache(
+ prefix = "game:count",
+ f = (o: JsObject) => $count(o),
+ timeToLive = defaultTtl)
object Divider {
diff --git a/modules/game/src/main/Env.scala b/modules/game/src/main/Env.scala
index 2d5b0f20d9..11b685800e 100644
--- a/modules/game/src/main/Env.scala
+++ b/modules/game/src/main/Env.scala
@@ -9,6 +9,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
db: lila.db.Env,
+ mongoCache: lila.memo.MongoCache.Builder,
system: ActorSystem,
hub: lila.hub.Env,
getLightUser: String => Option[lila.common.LightUser],
@@ -41,7 +42,9 @@ final class Env(
lazy val pngExport = PngExport(PngExecPath) _
- lazy val cached = new Cached(ttl = CachedNbTtl)
+ lazy val cached = new Cached(
+ mongoCache = mongoCache,
+ defaultTtl = CachedNbTtl)
lazy val paginator = new PaginatorBuilder(
cached = cached,
@@ -100,6 +103,7 @@ object Env {
lazy val current = "[boot] game" describes new Env(
config = lila.common.PlayApp loadConfig "game",
db = lila.db.Env.current,
+ mongoCache = lila.memo.Env.current.mongoCache,
system = lila.common.PlayApp.system,
hub = lila.hub.Env.current,
getLightUser = lila.user.Env.current.lightUser,
diff --git a/modules/game/src/main/Game.scala b/modules/game/src/main/Game.scala
index 33c957517c..b071efacaf 100644
--- a/modules/game/src/main/Game.scala
+++ b/modules/game/src/main/Game.scala
@@ -86,10 +86,11 @@ case class Game(
// in tenths
private def lastMoveTime: Option[Long] = castleLastMoveTime.lastMoveTime map {
_.toLong + (createdAt.getMillis / 100)
- }
+ } orElse updatedAt.map(_.getMillis / 100)
+
private def lastMoveTimeDate: Option[DateTime] = castleLastMoveTime.lastMoveTime map { lmt =>
createdAt plusMillis (lmt * 100)
- }
+ } orElse updatedAt
def lastMoveTimeInSeconds: Option[Int] = lastMoveTime.map(x => (x / 10).toInt)
diff --git a/modules/game/src/main/GameRepo.scala b/modules/game/src/main/GameRepo.scala
index 7eaaa12c3b..7b6c0297ba 100644
--- a/modules/game/src/main/GameRepo.scala
+++ b/modules/game/src/main/GameRepo.scala
@@ -15,7 +15,7 @@ import lila.db.api._
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.ByteArray
import lila.db.Implicits._
-import lila.user.User
+import lila.user.{ User, UidNb }
object GameRepo {
@@ -122,6 +122,18 @@ object GameRepo {
_ flatMap { Pov(_, user) } sortBy Pov.priority
}
+ // gets most urgent game to play
+ def onePlaying(user: User): Fu[Option[Pov]] = nowPlaying(user) map (_.headOption)
+
+ // gets last recently played move game
+ def lastPlayed(user: User): Fu[Option[Pov]] =
+ $find.one($query(Query recentlyPlayingWithClock user.id) sort Query.sortUpdatedNoIndex) map {
+ _ flatMap { Pov(_, user) }
+ }
+
+ def countPlayingRealTime(userId: String): Fu[Int] =
+ $count(Query.nowPlaying(userId) ++ Query.clock(true))
+
def setTv(id: ID) {
$update.fieldUnchecked(id, F.tvAt, $date(DateTime.now))
}
@@ -179,15 +191,15 @@ object GameRepo {
_ sort Query.sortCreated skip (Random nextInt distribution)
)
- def insertDenormalized(game: Game, ratedCheck: Boolean = true): Funit = {
- val g2 = if (ratedCheck && game.rated && game.userIds.distinct.size != 2)
- game.copy(mode = chess.Mode.Casual)
- else game
- val userIds = game.userIds.distinct
+ def insertDenormalized(g: Game, ratedCheck: Boolean = true): Funit = {
+ val g2 = if (ratedCheck && g.rated && g.userIds.distinct.size != 2)
+ g.copy(mode = chess.Mode.Casual)
+ else g
+ val userIds = g2.userIds.distinct
val bson = (gameTube.handler write g2) ++ BSONDocument(
F.initialFen -> g2.variant.exotic.option(Forsyth >> g2.toChess),
- F.checkAt -> (!game.isPgnImport).option(DateTime.now.plusHours(game.hasClock.fold(1, 24))),
- F.playingUids -> userIds.nonEmpty.option(userIds)
+ F.checkAt -> (!g2.isPgnImport).option(DateTime.now.plusHours(g2.hasClock.fold(1, 24))),
+ F.playingUids -> (g2.started && userIds.nonEmpty).option(userIds)
)
$insert bson bson
}
@@ -239,16 +251,6 @@ object GameRepo {
$count(Json.obj(F.createdAt -> ($gte($date(from)) ++ $lt($date(to)))))
}).sequenceFu
- def nowPlaying(userId: String): Fu[Option[Game]] =
- $find.one(Query.status(Status.Started) ++ Query.user(userId) ++ Json.obj(
- F.createdAt -> $gt($date(DateTime.now minusHours 1))
- ))
-
- def isNowPlaying(userId: String): Fu[Boolean] = nowPlaying(userId) map (_.isDefined)
-
- def lastPlayed(userId: String): Fu[Option[Game]] =
- $find($query(Query user userId) sort ($sort desc F.createdAt), 1) map (_.headOption)
-
def bestOpponents(userId: String, limit: Int): Fu[List[(String, Int)]] = {
import reactivemongo.bson._
import reactivemongo.core.commands._
@@ -320,7 +322,7 @@ object GameRepo {
))
}
- def activePlayersSince(since: DateTime, max: Int): Fu[List[(String, Int)]] = {
+ def activePlayersSince(since: DateTime, max: Int): Fu[List[UidNb]] = {
import reactivemongo.bson._
import reactivemongo.core.commands._
import lila.db.BSON.BSONJodaDateTimeHandler
@@ -342,7 +344,7 @@ object GameRepo {
(stream.toList map { obj =>
toJSON(obj).asOpt[JsObject] flatMap { o =>
o int "nb" map { nb =>
- ~(o str "_id") -> nb
+ UidNb(~(o str "_id"), nb)
}
}
}).flatten
diff --git a/modules/game/src/main/Pov.scala b/modules/game/src/main/Pov.scala
index c86a3c2671..a0163a2aa9 100644
--- a/modules/game/src/main/Pov.scala
+++ b/modules/game/src/main/Pov.scala
@@ -18,9 +18,6 @@ case class Pov(game: Game, color: Color) {
def unary_! = Pov(game, !color)
- def isPlayerFullId(fullId: Option[String]): Boolean =
- fullId ?? { game.isPlayerFullId(player, _) }
-
def ref = PovRef(game.id, color)
def withGame(g: Game) = copy(game = g)
@@ -32,6 +29,8 @@ case class Pov(game: Game, color: Color) {
game.correspondenceClock.map(_.remainingTime(color).toInt)
}
+ def hasMoved = game playerHasMoved color
+
override def toString = ref.toString
}
@@ -53,9 +52,11 @@ object Pov {
game player user map { apply(game, _) }
def priority(pov: Pov) =
- if (pov.isMyTurn) pov.remainingSeconds.getOrElse(Int.MaxValue - 1)
+ if (pov.isMyTurn) {
+ if (pov.hasMoved) pov.remainingSeconds.getOrElse(Int.MaxValue - 1)
+ else 10 // first move has priority over games with more than 10s left
+ }
else Int.MaxValue
-
}
case class PovRef(gameId: String, color: Color) {
diff --git a/modules/game/src/main/Query.scala b/modules/game/src/main/Query.scala
index 2c2bafd18f..0ce14fd5e8 100644
--- a/modules/game/src/main/Query.scala
+++ b/modules/game/src/main/Query.scala
@@ -50,6 +50,11 @@ object Query {
def nowPlaying(u: String) = Json.obj(F.playingUids -> u)
+ def recentlyPlayingWithClock(u: String) =
+ nowPlaying(u) ++ clock(true) ++ Json.obj(
+ F.updatedAt -> $gt($date(DateTime.now minusMinutes 5))
+ )
+
// use the us index
def win(u: String) = user(u) ++ Json.obj(F.winnerId -> u)
@@ -65,4 +70,5 @@ object Query {
def checkable = Json.obj(F.checkAt -> $lt($date(DateTime.now)))
val sortCreated = $sort desc F.createdAt
+ val sortUpdatedNoIndex = $sort desc F.updatedAt
}
diff --git a/modules/game/src/main/Rewind.scala b/modules/game/src/main/Rewind.scala
index f197b0eff5..d4b1b6d13e 100644
--- a/modules/game/src/main/Rewind.scala
+++ b/modules/game/src/main/Rewind.scala
@@ -15,7 +15,7 @@ object Rewind {
val rewindedHistory = rewindedGame.board.history
val rewindedSituation = rewindedGame.situation
def rewindPlayer(player: Player) = player.copy(isProposingTakeback = false)
- Progress(game, game.copy(
+ val newGame = game.copy(
whitePlayer = rewindPlayer(game.whitePlayer),
blackPlayer = rewindPlayer(game.blackPlayer),
binaryPieces = BinaryFormat.piece write rewindedGame.board.pieces,
@@ -29,7 +29,7 @@ object Rewind {
check = if (rewindedSituation.check) rewindedSituation.kingPos else None),
binaryMoveTimes = BinaryFormat.moveTime write (game.moveTimes take rewindedGame.turns),
status = game.status,
- clock = game.clock map (_.switch)
- ))
+ clock = game.clock map (_.takeback))
+ Progress(game, newGame, newGame.clock.map(Event.Clock.apply).toList)
}
}
diff --git a/modules/history/src/main/Env.scala b/modules/history/src/main/Env.scala
index 3f21410667..a6e8885bf6 100644
--- a/modules/history/src/main/Env.scala
+++ b/modules/history/src/main/Env.scala
@@ -5,6 +5,7 @@ import lila.common.PimpedConfig._
final class Env(
config: Config,
+ mongoCache: lila.memo.MongoCache.Builder,
db: lila.db.Env) {
private val CachedRatingChartTtl = config duration "cached.rating_chart.ttl"
@@ -13,12 +14,16 @@ final class Env(
lazy val api = new HistoryApi(db(Collectionhistory))
- lazy val ratingChartApi = new RatingChartApi(api, CachedRatingChartTtl)
+ lazy val ratingChartApi = new RatingChartApi(
+ historyApi = api,
+ mongoCache = mongoCache,
+ cacheTtl = CachedRatingChartTtl)
}
object Env {
lazy val current = "[boot] history" describes new Env(
config = lila.common.PlayApp loadConfig "history",
+ mongoCache = lila.memo.Env.current.mongoCache,
db = lila.db.Env.current)
}
diff --git a/modules/history/src/main/RatingChartApi.scala b/modules/history/src/main/RatingChartApi.scala
index 17ddba5bcc..1ae0618a86 100644
--- a/modules/history/src/main/RatingChartApi.scala
+++ b/modules/history/src/main/RatingChartApi.scala
@@ -9,13 +9,21 @@ import play.api.libs.json._
import lila.rating.{ Glicko, PerfType }
import lila.user.{ User, Perfs }
-final class RatingChartApi(historyApi: HistoryApi, cacheTtl: Duration) {
+final class RatingChartApi(
+ historyApi: HistoryApi,
+ mongoCache: lila.memo.MongoCache.Builder,
+ cacheTtl: FiniteDuration) {
- def apply(user: User): Fu[Option[String]] = cache(user)
+ def apply(user: User): Fu[Option[String]] = cache(user) map { chart =>
+ chart.nonEmpty option chart
+ }
- private val cache = lila.memo.AsyncCache(build,
- maxCapacity = 50,
- timeToLive = cacheTtl)
+ private val cache = mongoCache[User, String](
+ prefix = "history:rating",
+ f = (user: User) => build(user) map (~_),
+ maxCapacity = 64,
+ timeToLive = cacheTtl,
+ keyToString = _.id)
private val columns = Json stringify {
Json.arr(
diff --git a/modules/hub/src/main/actorApi.scala b/modules/hub/src/main/actorApi.scala
index 9f67502083..986cf3024a 100644
--- a/modules/hub/src/main/actorApi.scala
+++ b/modules/hub/src/main/actorApi.scala
@@ -152,7 +152,8 @@ case class MoveEvent(
gameId: String,
fen: String,
move: String,
- ip: String)
+ ip: String,
+ opponentUserId: Option[String])
case class NbRounds(nb: Int)
}
diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala
index 08ba47a27c..4a928db364 100644
--- a/modules/i18n/src/main/I18nKeys.scala
+++ b/modules/i18n/src/main/I18nKeys.scala
@@ -193,6 +193,7 @@ final class I18nKeys(translator: Translator) {
val `tournamentPoints` = new Key("tournamentPoints")
val `viewTournament` = new Key("viewTournament")
val `backToTournament` = new Key("backToTournament")
+ val `backToGame` = new Key("backToGame")
val `freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents` = new Key("freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents")
val `teams` = new Key("teams")
val `nbMembers` = new Key("nbMembers")
@@ -329,8 +330,10 @@ final class I18nKeys(translator: Translator) {
val `thisPuzzleIsWrong` = new Key("thisPuzzleIsWrong")
val `youHaveNbSecondsToMakeYourFirstMove` = new Key("youHaveNbSecondsToMakeYourFirstMove")
val `nbGamesInPlay` = new Key("nbGamesInPlay")
+ val `automaticallyProceedToNextGameAfterMoving` = new Key("automaticallyProceedToNextGameAfterMoving")
+ val `autoSwitch` = new Key("autoSwitch")
- def keys = List(`playWithAFriend`, `playWithTheMachine`, `toInviteSomeoneToPlayGiveThisUrl`, `gameOver`, `waitingForOpponent`, `waiting`, `yourTurn`, `aiNameLevelAiLevel`, `level`, `toggleTheChat`, `toggleSound`, `chat`, `resign`, `checkmate`, `stalemate`, `white`, `black`, `randomColor`, `createAGame`, `whiteIsVictorious`, `blackIsVictorious`, `playWithTheSameOpponentAgain`, `newOpponent`, `playWithAnotherOpponent`, `yourOpponentWantsToPlayANewGameWithYou`, `joinTheGame`, `whitePlays`, `blackPlays`, `theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim`, `makeYourOpponentResign`, `forceResignation`, `forceDraw`, `talkInChat`, `theFirstPersonToComeOnThisUrlWillPlayWithYou`, `whiteCreatesTheGame`, `blackCreatesTheGame`, `whiteJoinsTheGame`, `blackJoinsTheGame`, `whiteResigned`, `blackResigned`, `whiteLeftTheGame`, `blackLeftTheGame`, `shareThisUrlToLetSpectatorsSeeTheGame`, `youAreViewingThisGameAsASpectator`, `replayAndAnalyse`, `computerAnalysisInProgress`, `theComputerAnalysisHasFailed`, `viewTheComputerAnalysis`, `requestAComputerAnalysis`, `computerAnalysis`, `analysis`, `blunders`, `mistakes`, `inaccuracies`, `moveTimes`, `flipBoard`, `threefoldRepetition`, `claimADraw`, `offerDraw`, `draw`, `nbConnectedPlayers`, `gamesBeingPlayedRightNow`, `viewAllNbGames`, `viewNbCheckmates`, `nbBookmarks`, `nbPopularGames`, `nbAnalysedGames`, `bookmarkedByNbPlayers`, `viewInFullSize`, `logOut`, `signIn`, `newToLichess`, `youNeedAnAccountToDoThat`, `signUp`, `computersAreNotAllowedToPlay`, `games`, `forum`, `xPostedInForumY`, `latestForumPosts`, `players`, `minutesPerSide`, `variant`, `timeControl`, `realTime`, `correspondence`, `daysPerTurn`, `oneDay`, `nbDays`, `nbHours`, `time`, `username`, `password`, `haveAnAccount`, `allYouNeedIsAUsernameAndAPassword`, `changePassword`, `changeEmail`, `email`, `emailIsOptional`, `passwordReset`, `forgotPassword`, `learnMoreAboutLichess`, `rank`, `gamesPlayed`, `nbGamesWithYou`, `declineInvitation`, `cancel`, `timeOut`, `drawOfferSent`, `drawOfferDeclined`, `drawOfferAccepted`, `drawOfferCanceled`, `whiteOffersDraw`, `blackOffersDraw`, `whiteDeclinesDraw`, `blackDeclinesDraw`, `yourOpponentOffersADraw`, `accept`, `decline`, `playingRightNow`, `finished`, `abortGame`, `gameAborted`, `standard`, `unlimited`, `mode`, `casual`, `rated`, `thisGameIsRated`, `rematch`, `rematchOfferSent`, `rematchOfferAccepted`, `rematchOfferCanceled`, `rematchOfferDeclined`, `cancelRematchOffer`, `viewRematch`, `play`, `inbox`, `chatRoom`, `spectatorRoom`, `composeMessage`, `noNewMessages`, `subject`, `recipient`, `send`, `incrementInSeconds`, `freeOnlineChess`, `spectators`, `nbWins`, `nbLosses`, `nbDraws`, `exportGames`, `ratingRange`, `giveNbSeconds`, `premoveEnabledClickAnywhereToCancel`, `thisPlayerUsesChessComputerAssistance`, `opening`, `takeback`, `proposeATakeback`, `takebackPropositionSent`, `takebackPropositionDeclined`, `takebackPropositionAccepted`, `takebackPropositionCanceled`, `yourOpponentProposesATakeback`, `bookmarkThisGame`, `search`, `advancedSearch`, `tournament`, `tournaments`, `tournamentPoints`, `viewTournament`, `backToTournament`, `freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents`, `teams`, `nbMembers`, `allTeams`, `newTeam`, `myTeams`, `noTeamFound`, `joinTeam`, `quitTeam`, `anyoneCanJoin`, `aConfirmationIsRequiredToJoin`, `joiningPolicy`, `teamLeader`, `teamBestPlayers`, `teamRecentMembers`, `xJoinedTeamY`, `xCreatedTeamY`, `averageElo`, `location`, `settings`, `filterGames`, `reset`, `apply`, `leaderboard`, `pasteTheFenStringHere`, `pasteThePgnStringHere`, `fromPosition`, `continueFromHere`, `importGame`, `nbImportedGames`, `thisIsAChessCaptcha`, `clickOnTheBoardToMakeYourMove`, `notACheckmate`, `colorPlaysCheckmateInOne`, `retry`, `reconnecting`, `onlineFriends`, `noFriendsOnline`, `findFriends`, `favoriteOpponents`, `follow`, `following`, `unfollow`, `block`, `blocked`, `unblock`, `followsYou`, `xStartedFollowingY`, `nbFollowers`, `nbFollowing`, `more`, `memberSince`, `lastLogin`, `challengeToPlay`, `player`, `list`, `graph`, `lessThanNbMinutes`, `xToYMinutes`, `textIsTooShort`, `textIsTooLong`, `required`, `openTournaments`, `duration`, `winner`, `standing`, `createANewTournament`, `join`, `withdraw`, `points`, `wins`, `losses`, `winStreak`, `createdBy`, `waitingForNbPlayers`, `tournamentIsStarting`, `membersOnly`, `boardEditor`, `startPosition`, `clearBoard`, `savePosition`, `loadPosition`, `isPrivate`, `reportXToModerators`, `profile`, `editProfile`, `firstName`, `lastName`, `biography`, `country`, `preferences`, `watchLichessTV`, `previouslyOnLichessTV`, `todaysLeaders`, `onlinePlayers`, `progressToday`, `progressThisWeek`, `progressThisMonth`, `leaderboardThisWeek`, `leaderboardThisMonth`, `activeToday`, `activeThisWeek`, `activePlayers`, `bewareTheGameIsRatedButHasNoClock`, `training`, `yourPuzzleRatingX`, `findTheBestMoveForWhite`, `findTheBestMoveForBlack`, `toTrackYourProgress`, `trainingSignupExplanation`, `recentlyPlayedPuzzles`, `puzzleId`, `puzzleOfTheDay`, `clickToSolve`, `goodMove`, `butYouCanDoBetter`, `bestMove`, `keepGoing`, `puzzleFailed`, `butYouCanKeepTrying`, `victory`, `giveUp`, `puzzleSolvedInXSeconds`, `wasThisPuzzleAnyGood`, `pleaseVotePuzzle`, `thankYou`, `ratingX`, `playedXTimes`, `fromGameLink`, `startTraining`, `continueTraining`, `retryThisPuzzle`, `thisPuzzleIsCorrect`, `thisPuzzleIsWrong`, `youHaveNbSecondsToMakeYourFirstMove`, `nbGamesInPlay`)
+ def keys = List(`playWithAFriend`, `playWithTheMachine`, `toInviteSomeoneToPlayGiveThisUrl`, `gameOver`, `waitingForOpponent`, `waiting`, `yourTurn`, `aiNameLevelAiLevel`, `level`, `toggleTheChat`, `toggleSound`, `chat`, `resign`, `checkmate`, `stalemate`, `white`, `black`, `randomColor`, `createAGame`, `whiteIsVictorious`, `blackIsVictorious`, `playWithTheSameOpponentAgain`, `newOpponent`, `playWithAnotherOpponent`, `yourOpponentWantsToPlayANewGameWithYou`, `joinTheGame`, `whitePlays`, `blackPlays`, `theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim`, `makeYourOpponentResign`, `forceResignation`, `forceDraw`, `talkInChat`, `theFirstPersonToComeOnThisUrlWillPlayWithYou`, `whiteCreatesTheGame`, `blackCreatesTheGame`, `whiteJoinsTheGame`, `blackJoinsTheGame`, `whiteResigned`, `blackResigned`, `whiteLeftTheGame`, `blackLeftTheGame`, `shareThisUrlToLetSpectatorsSeeTheGame`, `youAreViewingThisGameAsASpectator`, `replayAndAnalyse`, `computerAnalysisInProgress`, `theComputerAnalysisHasFailed`, `viewTheComputerAnalysis`, `requestAComputerAnalysis`, `computerAnalysis`, `analysis`, `blunders`, `mistakes`, `inaccuracies`, `moveTimes`, `flipBoard`, `threefoldRepetition`, `claimADraw`, `offerDraw`, `draw`, `nbConnectedPlayers`, `gamesBeingPlayedRightNow`, `viewAllNbGames`, `viewNbCheckmates`, `nbBookmarks`, `nbPopularGames`, `nbAnalysedGames`, `bookmarkedByNbPlayers`, `viewInFullSize`, `logOut`, `signIn`, `newToLichess`, `youNeedAnAccountToDoThat`, `signUp`, `computersAreNotAllowedToPlay`, `games`, `forum`, `xPostedInForumY`, `latestForumPosts`, `players`, `minutesPerSide`, `variant`, `timeControl`, `realTime`, `correspondence`, `daysPerTurn`, `oneDay`, `nbDays`, `nbHours`, `time`, `username`, `password`, `haveAnAccount`, `allYouNeedIsAUsernameAndAPassword`, `changePassword`, `changeEmail`, `email`, `emailIsOptional`, `passwordReset`, `forgotPassword`, `learnMoreAboutLichess`, `rank`, `gamesPlayed`, `nbGamesWithYou`, `declineInvitation`, `cancel`, `timeOut`, `drawOfferSent`, `drawOfferDeclined`, `drawOfferAccepted`, `drawOfferCanceled`, `whiteOffersDraw`, `blackOffersDraw`, `whiteDeclinesDraw`, `blackDeclinesDraw`, `yourOpponentOffersADraw`, `accept`, `decline`, `playingRightNow`, `finished`, `abortGame`, `gameAborted`, `standard`, `unlimited`, `mode`, `casual`, `rated`, `thisGameIsRated`, `rematch`, `rematchOfferSent`, `rematchOfferAccepted`, `rematchOfferCanceled`, `rematchOfferDeclined`, `cancelRematchOffer`, `viewRematch`, `play`, `inbox`, `chatRoom`, `spectatorRoom`, `composeMessage`, `noNewMessages`, `subject`, `recipient`, `send`, `incrementInSeconds`, `freeOnlineChess`, `spectators`, `nbWins`, `nbLosses`, `nbDraws`, `exportGames`, `ratingRange`, `giveNbSeconds`, `premoveEnabledClickAnywhereToCancel`, `thisPlayerUsesChessComputerAssistance`, `opening`, `takeback`, `proposeATakeback`, `takebackPropositionSent`, `takebackPropositionDeclined`, `takebackPropositionAccepted`, `takebackPropositionCanceled`, `yourOpponentProposesATakeback`, `bookmarkThisGame`, `search`, `advancedSearch`, `tournament`, `tournaments`, `tournamentPoints`, `viewTournament`, `backToTournament`, `backToGame`, `freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrationNoAdsNoPluginRequiredPlayChessWithComputerFriendsOrRandomOpponents`, `teams`, `nbMembers`, `allTeams`, `newTeam`, `myTeams`, `noTeamFound`, `joinTeam`, `quitTeam`, `anyoneCanJoin`, `aConfirmationIsRequiredToJoin`, `joiningPolicy`, `teamLeader`, `teamBestPlayers`, `teamRecentMembers`, `xJoinedTeamY`, `xCreatedTeamY`, `averageElo`, `location`, `settings`, `filterGames`, `reset`, `apply`, `leaderboard`, `pasteTheFenStringHere`, `pasteThePgnStringHere`, `fromPosition`, `continueFromHere`, `importGame`, `nbImportedGames`, `thisIsAChessCaptcha`, `clickOnTheBoardToMakeYourMove`, `notACheckmate`, `colorPlaysCheckmateInOne`, `retry`, `reconnecting`, `onlineFriends`, `noFriendsOnline`, `findFriends`, `favoriteOpponents`, `follow`, `following`, `unfollow`, `block`, `blocked`, `unblock`, `followsYou`, `xStartedFollowingY`, `nbFollowers`, `nbFollowing`, `more`, `memberSince`, `lastLogin`, `challengeToPlay`, `player`, `list`, `graph`, `lessThanNbMinutes`, `xToYMinutes`, `textIsTooShort`, `textIsTooLong`, `required`, `openTournaments`, `duration`, `winner`, `standing`, `createANewTournament`, `join`, `withdraw`, `points`, `wins`, `losses`, `winStreak`, `createdBy`, `waitingForNbPlayers`, `tournamentIsStarting`, `membersOnly`, `boardEditor`, `startPosition`, `clearBoard`, `savePosition`, `loadPosition`, `isPrivate`, `reportXToModerators`, `profile`, `editProfile`, `firstName`, `lastName`, `biography`, `country`, `preferences`, `watchLichessTV`, `previouslyOnLichessTV`, `todaysLeaders`, `onlinePlayers`, `progressToday`, `progressThisWeek`, `progressThisMonth`, `leaderboardThisWeek`, `leaderboardThisMonth`, `activeToday`, `activeThisWeek`, `activePlayers`, `bewareTheGameIsRatedButHasNoClock`, `training`, `yourPuzzleRatingX`, `findTheBestMoveForWhite`, `findTheBestMoveForBlack`, `toTrackYourProgress`, `trainingSignupExplanation`, `recentlyPlayedPuzzles`, `puzzleId`, `puzzleOfTheDay`, `clickToSolve`, `goodMove`, `butYouCanDoBetter`, `bestMove`, `keepGoing`, `puzzleFailed`, `butYouCanKeepTrying`, `victory`, `giveUp`, `puzzleSolvedInXSeconds`, `wasThisPuzzleAnyGood`, `pleaseVotePuzzle`, `thankYou`, `ratingX`, `playedXTimes`, `fromGameLink`, `startTraining`, `continueTraining`, `retryThisPuzzle`, `thisPuzzleIsCorrect`, `thisPuzzleIsWrong`, `youHaveNbSecondsToMakeYourFirstMove`, `nbGamesInPlay`, `automaticallyProceedToNextGameAfterMoving`, `autoSwitch`)
lazy val count = keys.size
}
diff --git a/modules/lobby/src/main/Lobby.scala b/modules/lobby/src/main/Lobby.scala
index 972c74f456..00e6111711 100644
--- a/modules/lobby/src/main/Lobby.scala
+++ b/modules/lobby/src/main/Lobby.scala
@@ -21,11 +21,13 @@ private[lobby] final class Lobby(
def receive = {
- case GetOpen(userOption) =>
+ case HooksFor(userOption) =>
val replyTo = sender
(userOption.map(_.id) ?? blocking) foreach { blocks =>
val lobbyUser = userOption map { LobbyUser.make(_, blocks) }
- replyTo ! HookRepo.list.filter { Biter.canJoin(_, lobbyUser) }
+ replyTo ! HookRepo.list.filter { hook =>
+ ~(hook.userId |@| lobbyUser.map(_.id)).apply(_ == _) || Biter.canJoin(hook, lobbyUser)
+ }
}
case msg@AddHook(hook) => {
diff --git a/modules/lobby/src/main/Seek.scala b/modules/lobby/src/main/Seek.scala
index d315802238..8b6a81c76f 100644
--- a/modules/lobby/src/main/Seek.scala
+++ b/modules/lobby/src/main/Seek.scala
@@ -83,7 +83,7 @@ object Seek {
createdAt = DateTime.now)
import reactivemongo.bson.Macros
- import lila.db.BSON.MapValue._
+ import lila.db.BSON.MapValue.MapHandler
import lila.db.BSON.BSONJodaDateTimeHandler
private[lobby] implicit val lobbyUserBSONHandler = Macros.handler[LobbyUser]
private[lobby] implicit val seekBSONHandler = Macros.handler[Seek]
diff --git a/modules/lobby/src/main/SeekApi.scala b/modules/lobby/src/main/SeekApi.scala
index a05c8a5160..6c507590b4 100644
--- a/modules/lobby/src/main/SeekApi.scala
+++ b/modules/lobby/src/main/SeekApi.scala
@@ -3,9 +3,11 @@ package lila.lobby
import org.joda.time.DateTime
import reactivemongo.bson.{ BSONDocument, BSONInteger, BSONRegex, BSONArray, BSONBoolean }
import reactivemongo.core.commands._
+import scala.concurrent.duration._
import actorApi.LobbyUser
import lila.db.Types.Coll
+import lila.memo.AsyncCache
import lila.user.{ User, UserRepo }
final class SeekApi(
@@ -14,18 +16,33 @@ final class SeekApi(
maxPerPage: Int,
maxPerUser: Int) {
- def forAnon: Fu[List[Seek]] =
+ private sealed trait CacheKey
+ private object ForAnon extends CacheKey
+ private object ForUser extends CacheKey
+
+ private def allCursor =
coll.find(BSONDocument())
.sort(BSONDocument("createdAt" -> -1))
- .cursor[Seek].collect[List](maxPerPage)
+ .cursor[Seek]
+
+ private val cache = AsyncCache[CacheKey, List[Seek]](
+ f = {
+ case ForAnon => allCursor.collect[List](maxPerPage)
+ case ForUser => allCursor.collect[List]()
+ },
+ timeToLive = 5.seconds)
+
+ def forAnon = cache(ForAnon)
def forUser(user: User): Fu[List[Seek]] =
blocking(user.id) flatMap { blocking =>
forUser(LobbyUser.make(user, blocking))
}
- def forUser(user: LobbyUser): Fu[List[Seek]] = forAnon map {
- _ filter { Biter.canJoin(_, user) }
+ def forUser(user: LobbyUser): Fu[List[Seek]] = cache(ForUser) map {
+ _ filter { seek =>
+ seek.user.id == user.id || Biter.canJoin(seek, user)
+ } take maxPerPage
}
def find(id: String): Fu[Option[Seek]] =
@@ -33,19 +50,21 @@ final class SeekApi(
def insert(seek: Seek) = coll.insert(seek) >> findByUser(seek.user.id).flatMap {
case seeks if seeks.size <= maxPerUser => funit
- case seeks => seeks.drop(maxPerUser).map(remove).sequenceFu
- }
+ case seeks =>
+ seeks.drop(maxPerUser).map(remove).sequenceFu
+ } >> cache.clear
def findByUser(userId: String): Fu[List[Seek]] =
coll.find(BSONDocument("user.id" -> userId))
.sort(BSONDocument("createdAt" -> -1))
.cursor[Seek].collect[List]()
- def remove(seek: Seek) = coll.remove(BSONDocument("_id" -> seek.id)).void
+ def remove(seek: Seek) =
+ coll.remove(BSONDocument("_id" -> seek.id)).void >> cache.clear
def removeBy(seekId: String, userId: String) =
coll.remove(BSONDocument(
"_id" -> seekId,
"user.id" -> userId
- )).void
+ )).void >> cache.clear
}
diff --git a/modules/lobby/src/main/actorApi.scala b/modules/lobby/src/main/actorApi.scala
index 59ead40aa9..19e7efc1d1 100644
--- a/modules/lobby/src/main/actorApi.scala
+++ b/modules/lobby/src/main/actorApi.scala
@@ -64,4 +64,4 @@ private[lobby] case class HookIds(ids: List[String])
case class AddHook(hook: Hook)
case class AddSeek(seek: Seek)
-case class GetOpen(user: Option[User])
+case class HooksFor(user: Option[User])
diff --git a/modules/memo/src/main/Env.scala b/modules/memo/src/main/Env.scala
new file mode 100644
index 0000000000..4a16ff195a
--- /dev/null
+++ b/modules/memo/src/main/Env.scala
@@ -0,0 +1,18 @@
+package lila.memo
+
+import com.typesafe.config.Config
+import lila.db.Types._
+
+final class Env(config: Config, db: lila.db.Env) {
+
+ private val CollectionCache = config getString "collection.cache"
+
+ lazy val mongoCache: MongoCache.Builder = MongoCache(db(CollectionCache))
+}
+
+object Env {
+
+ lazy val current = "[boot] memo" describes new Env(
+ lila.common.PlayApp loadConfig "memo",
+ lila.db.Env.current)
+}
diff --git a/modules/memo/src/main/MongoCache.scala b/modules/memo/src/main/MongoCache.scala
new file mode 100644
index 0000000000..fc2af1e839
--- /dev/null
+++ b/modules/memo/src/main/MongoCache.scala
@@ -0,0 +1,81 @@
+package lila.memo
+
+import org.joda.time.DateTime
+import reactivemongo.bson._
+import reactivemongo.bson.Macros
+import scala.concurrent.duration._
+import spray.caching.{ LruCache, Cache }
+
+import lila.db.BSON.BSONJodaDateTimeHandler
+import lila.db.Types._
+
+final class MongoCache[K, V: MongoCache.Handler] private (
+ prefix: String,
+ expiresAt: () => DateTime,
+ cache: Cache[V],
+ coll: Coll,
+ f: K => Fu[V],
+ keyToString: K => String) {
+
+ def apply(k: K): Fu[V] = cache(k) {
+ coll.find(select(k)).one[Entry] flatMap {
+ case None => f(k) flatMap { v =>
+ coll.insert(makeEntry(k, v)) inject v
+ }
+ case Some(entry) => fuccess(entry.v)
+ }
+ }
+
+ def remove(k: K): Funit =
+ coll.remove(select(k)).void >>- (cache remove k)
+
+ private case class Entry(_id: String, v: V, e: DateTime)
+
+ private implicit val entryBSONHandler = Macros.handler[Entry]
+
+ private def makeEntry(k: K, v: V) = Entry(makeKey(k), v, expiresAt())
+
+ private def makeKey(k: K) = s"$prefix:${keyToString(k)}"
+
+ private def select(k: K) = BSONDocument("_id" -> makeKey(k))
+}
+
+object MongoCache {
+
+ private type Handler[T] = BSONHandler[_ <: BSONValue, T]
+
+ private def expiresAt(ttl: Duration)(): DateTime =
+ DateTime.now plusSeconds ttl.toSeconds.toInt
+
+ final class Builder(coll: Coll) {
+
+ def apply[K, V: Handler](
+ prefix: String,
+ f: K => Fu[V],
+ maxCapacity: Int = 512,
+ initialCapacity: Int = 64,
+ timeToLive: FiniteDuration,
+ timeToLiveMongo: Option[FiniteDuration] = None,
+ keyToString: K => String = (k: K) => k.toString): MongoCache[K, V] = new MongoCache[K, V](
+ prefix = prefix,
+ expiresAt = expiresAt(timeToLiveMongo | timeToLive),
+ cache = LruCache(maxCapacity, initialCapacity, timeToLive),
+ coll = coll,
+ f = f,
+ keyToString = keyToString)
+
+ def single[V: Handler](
+ prefix: String,
+ f: => Fu[V],
+ timeToLive: FiniteDuration,
+ timeToLiveMongo: Option[FiniteDuration] = None) = new MongoCache[Boolean, V](
+ prefix = prefix,
+ expiresAt = expiresAt(timeToLiveMongo | timeToLive),
+ cache = LruCache(timeToLive = timeToLive),
+ coll = coll,
+ f = _ => f,
+ keyToString = _.toString)
+ }
+
+ def apply(coll: Coll) = new Builder(coll)
+}
diff --git a/modules/message/src/main/Env.scala b/modules/message/src/main/Env.scala
index 111a159e9a..eeee6c66e9 100644
--- a/modules/message/src/main/Env.scala
+++ b/modules/message/src/main/Env.scala
@@ -8,6 +8,7 @@ import lila.hub.actorApi.message.LichessThread
final class Env(
config: Config,
db: lila.db.Env,
+ mongoCache: lila.memo.MongoCache.Builder,
blocks: (String, String) => Fu[Boolean],
system: ActorSystem) {
@@ -17,7 +18,7 @@ final class Env(
private[message] lazy val threadColl = db(CollectionThread)
- private lazy val unreadCache = new UnreadCache
+ private lazy val unreadCache = new UnreadCache(mongoCache)
lazy val forms = new DataForm(blocks = blocks)
@@ -46,6 +47,7 @@ object Env {
lazy val current = "[boot] message" describes new Env(
config = lila.common.PlayApp loadConfig "message",
db = lila.db.Env.current,
+ mongoCache = lila.memo.Env.current.mongoCache,
blocks = lila.relation.Env.current.api.blocks,
system = lila.common.PlayApp.system)
}
diff --git a/modules/message/src/main/UnreadCache.scala b/modules/message/src/main/UnreadCache.scala
index e2778c793f..de63d5e673 100644
--- a/modules/message/src/main/UnreadCache.scala
+++ b/modules/message/src/main/UnreadCache.scala
@@ -1,19 +1,24 @@
package lila.message
-import spray.caching.{ LruCache, Cache }
+import scala.concurrent.duration._
+import lila.db.BSON._
import lila.user.User
-private[message] final class UnreadCache {
+private[message] final class UnreadCache(
+ mongoCache: lila.memo.MongoCache.Builder) {
// userId => thread IDs
- private val cache: Cache[List[String]] = LruCache(maxCapacity = 99999)
+ private val cache = mongoCache[String, List[String]](
+ prefix = "message:unread",
+ f = ThreadRepo.userUnreadIds,
+ maxCapacity = 4096,
+ timeToLive = 2.days)
- def apply(userId: String): Fu[List[String]] =
- cache(userId)(ThreadRepo userUnreadIds userId)
+ def apply(userId: String): Fu[List[String]] = cache(userId)
def refresh(userId: String): Fu[List[String]] =
- (cache remove userId).fold(apply(userId))(_ >> apply(userId))
+ (cache remove userId) >> apply(userId)
- def clear(userId: String) = (cache remove userId).fold(funit)(_.void)
+ def clear(userId: String) = cache remove userId
}
diff --git a/modules/pref/src/main/PrefApi.scala b/modules/pref/src/main/PrefApi.scala
index ec15aff209..23ca4a0cab 100644
--- a/modules/pref/src/main/PrefApi.scala
+++ b/modules/pref/src/main/PrefApi.scala
@@ -16,7 +16,7 @@ final class PrefApi(coll: Coll, cacheTtl: Duration) {
private implicit val prefBSONHandler = new BSON[Pref] {
- import lila.db.BSON.MapValue._
+ import lila.db.BSON.MapValue.{ MapReader, MapWriter }
implicit val tagsReader = MapReader[String]
implicit val tagsWriter = MapWriter[String]
diff --git a/modules/qa/src/main/Env.scala b/modules/qa/src/main/Env.scala
index 260a7b06f5..24feaca893 100644
--- a/modules/qa/src/main/Env.scala
+++ b/modules/qa/src/main/Env.scala
@@ -8,6 +8,7 @@ final class Env(
config: Config,
hub: lila.hub.Env,
detectLanguage: DetectLanguage,
+ mongoCache: lila.memo.MongoCache.Builder,
db: lila.db.Env) {
private val CollectionQuestion = config getString "collection.question"
@@ -19,6 +20,7 @@ final class Env(
lazy val api = new QaApi(
questionColl = questionColl,
answerColl = db(CollectionAnswer),
+ mongoCache = mongoCache,
notifier = notifier)
private lazy val notifier = new Notifier(
@@ -37,5 +39,6 @@ object Env {
config = lila.common.PlayApp loadConfig "qa",
hub = lila.hub.Env.current,
detectLanguage = DetectLanguage(lila.common.PlayApp loadConfig "detectlanguage"),
+ mongoCache = lila.memo.Env.current.mongoCache,
db = lila.db.Env.current)
}
diff --git a/modules/qa/src/main/QaApi.scala b/modules/qa/src/main/QaApi.scala
index ca589dfa49..1e31efc3c0 100644
--- a/modules/qa/src/main/QaApi.scala
+++ b/modules/qa/src/main/QaApi.scala
@@ -9,15 +9,15 @@ import org.joda.time.DateTime
import spray.caching.{ LruCache, Cache }
import lila.common.paginator._
-import lila.db.BSON.BSONJodaDateTimeHandler
+import lila.db.BSON._
import lila.db.paginator._
import lila.db.Types.Coll
-import lila.memo.AsyncCache
import lila.user.{ User, UserRepo }
final class QaApi(
questionColl: Coll,
answerColl: Coll,
+ mongoCache: lila.memo.MongoCache.Builder,
notifier: Notifier) {
object question {
@@ -84,11 +84,12 @@ final class QaApi(
currentPage = page,
maxPerPage = perPage)
- private def popularCache = AsyncCache(
- (nb: Int) => questionColl.find(BSONDocument())
+ private def popularCache = mongoCache(
+ prefix = "qa:popular",
+ f = (nb: Int) => questionColl.find(BSONDocument())
.sort(BSONDocument("vote.score" -> -1))
.cursor[Question].collect[List](nb),
- timeToLive = 1 hour)
+ timeToLive = 3 hour)
def popular(max: Int): Fu[List[Question]] = popularCache(max)
@@ -111,7 +112,7 @@ final class QaApi(
questionColl.update(
BSONDocument("_id" -> q.id),
BSONDocument("$set" -> BSONDocument("vote" -> newVote))
- ) >> profile.clearCache >> popularCache.clear inject newVote.some
+ ) >> profile.clearCache inject newVote.some
}
}
diff --git a/modules/rating/src/main/PerfType.scala b/modules/rating/src/main/PerfType.scala
index f9b501d63e..cc8c328d22 100644
--- a/modules/rating/src/main/PerfType.scala
+++ b/modules/rating/src/main/PerfType.scala
@@ -82,4 +82,5 @@ object PerfType {
def name(key: Perf.Key): Option[String] = apply(key) map (_.name)
val nonPuzzle: List[PerfType] = List(Bullet, Blitz, Classical, Correspondence, Chess960, KingOfTheHill, ThreeCheck)
+ val leaderboardable: List[PerfType] = List(Bullet, Blitz, Classical, Chess960, KingOfTheHill, ThreeCheck)
}
diff --git a/modules/report/src/main/ReportApi.scala b/modules/report/src/main/ReportApi.scala
index 6dd7e81fed..003f5a0144 100644
--- a/modules/report/src/main/ReportApi.scala
+++ b/modules/report/src/main/ReportApi.scala
@@ -24,8 +24,8 @@ private[report] final class ReportApi(evaluator: ActorSelection) {
if (by.id == UserRepo.lichessId) reportTube.coll.update(
selectRecent(user, reason),
reportTube.toMongo(report).get - "_id"
- ) map { res =>
- if (!res.updatedExisting) {
+ ) flatMap { res =>
+ (!res.updatedExisting) ?? {
if (report.isCheat) evaluator ! user
$insert(report)
}
diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala
index 7a158a70ae..8d805215c7 100644
--- a/modules/round/src/main/Env.scala
+++ b/modules/round/src/main/Env.scala
@@ -8,7 +8,6 @@ import scala.concurrent.duration._
import actorApi.{ GetSocketStatus, SocketStatus }
import lila.common.PimpedConfig._
import lila.hub.actorApi.map.Ask
-import lila.memo.AsyncCache
import lila.socket.actorApi.GetVersion
import makeTimeout.large
@@ -29,6 +28,7 @@ final class Env(
prefApi: lila.pref.PrefApi,
chatApi: lila.chat.ChatApi,
historyApi: lila.history.HistoryApi,
+ isPlayingSimul: String => Fu[Boolean],
scheduler: lila.common.Scheduler) {
private val settings = new {
@@ -84,7 +84,8 @@ final class Env(
uidTimeout = UidTimeout,
socketTimeout = SocketTimeout,
disconnectTimeout = PlayerDisconnectTimeout,
- ragequitTimeout = PlayerRagequitTimeout)
+ ragequitTimeout = PlayerRagequitTimeout,
+ isPlayingSimul = isPlayingSimul)
def receive: Receive = ({
case msg@lila.chat.actorApi.ChatLine(id, line) =>
self ! lila.hub.actorApi.map.Tell(id take 8, msg)
@@ -197,5 +198,6 @@ object Env {
prefApi = lila.pref.Env.current.api,
chatApi = lila.chat.Env.current.api,
historyApi = lila.history.Env.current.api,
+ isPlayingSimul = lila.game.Env.current.cached.isPlayingSimul,
scheduler = lila.common.PlayApp.scheduler)
}
diff --git a/modules/round/src/main/JsonView.scala b/modules/round/src/main/JsonView.scala
index b281074eea..e26d4e9a26 100644
--- a/modules/round/src/main/JsonView.scala
+++ b/modules/round/src/main/JsonView.scala
@@ -62,9 +62,7 @@ final class JsonView(
"check" -> game.check.map(_.key),
"rematch" -> game.next,
"source" -> game.source.map(sourceJson),
- "status" -> Json.obj(
- "id" -> game.status.id,
- "name" -> game.status.name)),
+ "status" -> statusJson(game.status)),
"clock" -> game.clock.map(clockJson),
"correspondence" -> game.correspondenceClock.map(correspondenceJson),
"player" -> Json.obj(
@@ -166,9 +164,7 @@ final class JsonView(
"size" -> o.size
)
},
- "status" -> Json.obj(
- "id" -> game.status.id,
- "name" -> game.status.name)),
+ "status" -> statusJson(game.status)),
"clock" -> game.clock.map(clockJson),
"correspondence" -> game.correspondenceClock.map(correspondenceJson),
"player" -> Json.obj(
@@ -219,6 +215,32 @@ final class JsonView(
)
}
+ def userAnalysisJson(pov: Pov, pref: Pref) = {
+ import pov._
+ val fen = Forsyth >> game.toChess
+ Json.obj(
+ "game" -> Json.obj(
+ "id" -> gameId,
+ "variant" -> variantJson(game.variant),
+ "initialFen" -> fen,
+ "fen" -> fen,
+ "player" -> game.turnColor.name,
+ "status" -> statusJson(game.status)),
+ "player" -> Json.obj(
+ "color" -> color.name
+ ),
+ "opponent" -> Json.obj(
+ "color" -> opponent.color.name
+ ),
+ "pref" -> Json.obj(
+ "animationDuration" -> animationDuration(pov, pref),
+ "highlight" -> pref.highlight,
+ "destination" -> pref.destination,
+ "coords" -> pref.coords
+ ),
+ "userAnalysis" -> true)
+ }
+
private def blurs(game: Game, player: lila.game.Player) = {
val percent = game.playerBlurPercent(player.color)
(percent > 30) option Json.obj(
@@ -258,10 +280,13 @@ final class JsonView(
"moretime" -> moretimeSeconds)
private def correspondenceJson(c: CorrespondenceClock) = Json.obj(
- "increment" -> c.increment,
- "white" -> c.whiteTime,
- "black" -> c.blackTime,
- "emerg" -> c.emerg)
+ "increment" -> c.increment,
+ "white" -> c.whiteTime,
+ "black" -> c.blackTime,
+ "emerg" -> c.emerg)
+
+ private def statusJson(s: chess.Status) =
+ Json.obj("id" -> s.id, "name" -> s.name)
private def sourceJson(source: Source) = source.name
diff --git a/modules/round/src/main/Player.scala b/modules/round/src/main/Player.scala
index aa36909736..efc21c174c 100644
--- a/modules/round/src/main/Player.scala
+++ b/modules/round/src/main/Player.scala
@@ -31,7 +31,7 @@ private[round] final class Player(
case (progress, move) =>
(GameRepo save progress) >>-
(pov.game.hasAi ! uciMemo.add(pov.game, move)) >>-
- notifyProgress(move, progress, ip) >>
+ notifyMove(move, progress.game, ip) >>
progress.game.finished.fold(
moveFinish(progress.game, color) map { progress.events ::: _ }, {
cheatDetector(progress.game) addEffect {
@@ -61,13 +61,13 @@ private[round] final class Player(
fufail(s"[ai play] game ${game.id} turn ${game.turns} not AI turn")
) logFailureErr s"[ai play] game ${game.id} turn ${game.turns}"
- private def notifyProgress(move: chess.Move, progress: Progress, ip: String) {
- val game = progress.game
+ private def notifyMove(move: chess.Move, game: Game, ip: String) {
bus.publish(MoveEvent(
ip = ip,
gameId = game.id,
fen = Forsyth exportBoard game.toChess.board,
- move = move.keyString
+ move = move.keyString,
+ opponentUserId = game.player(!move.color).userId
), 'moveEvent)
}
diff --git a/modules/round/src/main/Socket.scala b/modules/round/src/main/Socket.scala
index d30df6410d..081fa7a2ae 100644
--- a/modules/round/src/main/Socket.scala
+++ b/modules/round/src/main/Socket.scala
@@ -9,7 +9,7 @@ import play.api.libs.iteratee._
import play.api.libs.json._
import actorApi._
-import lila.common.LightUser
+import lila.common.{ LightUser, Debouncer }
import lila.game.actorApi.UserStartGame
import lila.game.Event
import lila.hub.actorApi.game.ChangeFeatured
@@ -26,7 +26,8 @@ private[round] final class Socket(
uidTimeout: Duration,
socketTimeout: Duration,
disconnectTimeout: Duration,
- ragequitTimeout: Duration) extends SocketActor[Member](uidTimeout) {
+ ragequitTimeout: Duration,
+ isPlayingSimul: String => Fu[Boolean]) extends SocketActor[Member](uidTimeout) {
private var hasAi = false
@@ -39,8 +40,10 @@ private[round] final class Socket(
// wether the player closed the window intentionally
private var bye: Int = 0
+ var userId = none[String]
+
def ping {
- if (isGone) notifyGone(color, false)
+ isGone foreach { _ ?? notifyGone(color, false) }
if (bye > 0) bye = bye - 1
time = nowMillis
}
@@ -49,7 +52,10 @@ private[round] final class Socket(
}
private def isBye = bye > 0
- def isGone = time < (nowMillis - isBye.fold(ragequitTimeout, disconnectTimeout).toMillis)
+ def isGone =
+ if (time < (nowMillis - isBye.fold(ragequitTimeout, disconnectTimeout).toMillis))
+ (userId ?? isPlayingSimul) map (!_)
+ else fuccess(false)
}
private val whitePlayer = new Player(White)
@@ -74,7 +80,10 @@ private[round] final class Socket(
def receiveSpecific = {
- case SetGame(Some(game)) => hasAi = game.hasAi
+ case SetGame(Some(game)) =>
+ hasAi = game.hasAi
+ whitePlayer.userId = game.player(White).userId
+ blackPlayer.userId = game.player(Black).userId
case PingVersion(uid, v) =>
timeBomb.delay
@@ -94,25 +103,28 @@ private[round] final class Socket(
broom
if (timeBomb.boom) self ! PoisonPill
else if (!hasAi) Color.all foreach { c =>
- if (playerGet(c, _.isGone)) notifyGone(c, true)
+ playerGet(c, _.isGone) foreach { _ ?? notifyGone(c, true) }
}
case GetVersion => sender ! history.getVersion
- case IsGone(color) => sender ! playerGet(color, _.isGone)
+ case IsGone(color) => playerGet(color, _.isGone) pipeTo sender
- case GetSocketStatus => sender ! SocketStatus(
- version = history.getVersion,
- whiteOnGame = ownerOf(White).isDefined,
- whiteIsGone = playerGet(White, _.isGone),
- blackOnGame = ownerOf(Black).isDefined,
- blackIsGone = playerGet(Black, _.isGone))
+ case GetSocketStatus =>
+ playerGet(White, _.isGone) zip playerGet(Black, _.isGone) map {
+ case (whiteIsGone, blackIsGone) => SocketStatus(
+ version = history.getVersion,
+ whiteOnGame = ownerOf(White).isDefined,
+ whiteIsGone = whiteIsGone,
+ blackOnGame = ownerOf(Black).isDefined,
+ blackIsGone = blackIsGone)
+ } pipeTo sender
case Join(uid, user, version, color, playerId, ip, userTv) =>
val (enumerator, channel) = Concurrent.broadcast[JsValue]
val member = Member(channel, user, color, playerId, ip, userTv = userTv)
addMember(uid, member)
- notifyCrowd
+ crowdNotifier ! watchers
playerDo(color, _.ping)
sender ! Connected(enumerator, member)
if (member.userTv.isDefined) refreshSubscriptions
@@ -132,7 +144,7 @@ private[round] final class Socket(
case Quit(uid) =>
members get uid foreach { member =>
quit(uid)
- notifyCrowd
+ crowdNotifier ! watchers
if (member.userTv.isDefined) refreshSubscriptions
}
@@ -143,17 +155,18 @@ private[round] final class Socket(
}
}
- def notifyCrowd {
- val (anons, users) = watchers.map(_.userId flatMap lightUser).foldLeft(0 -> List[LightUser]()) {
- case ((anons, users), Some(user)) => anons -> (user :: users)
- case ((anons, users), None) => (anons + 1) -> users
- }
- notify(Event.Crowd(
- white = ownerOf(White).isDefined,
- black = ownerOf(Black).isDefined,
- watchers = showSpectators(users, anons)
- ) :: Nil)
- }
+ val crowdNotifier =
+ context.system.actorOf(Props(new Debouncer(700.millis, (ms: Iterable[Member]) => {
+ val (anons, users) = ms.map(_.userId flatMap lightUser).foldLeft(0 -> List[LightUser]()) {
+ case ((anons, users), Some(user)) => anons -> (user :: users)
+ case ((anons, users), None) => (anons + 1) -> users
+ }
+ notify(Event.Crowd(
+ white = ownerOf(White).isDefined,
+ black = ownerOf(Black).isDefined,
+ watchers = showSpectators(users, anons)
+ ) :: Nil)
+ })))
def notify(events: Events) {
val vevents = history addEvents events
diff --git a/modules/round/src/main/SocketHandler.scala b/modules/round/src/main/SocketHandler.scala
index 5d8ccb0f94..84313c36ce 100644
--- a/modules/round/src/main/SocketHandler.scala
+++ b/modules/round/src/main/SocketHandler.scala
@@ -40,6 +40,7 @@ private[round] final class SocketHandler(
case ("talk", o) => o str "d" foreach { text =>
messenger.watcher(gameId, member, text, socket)
}
+ case ("outoftime", _) => round(Outoftime)
}) { playerId =>
{
case ("p", o) => o int "v" foreach { v => socket ! PingVersion(uid, v) }
diff --git a/modules/round/src/main/Takebacker.scala b/modules/round/src/main/Takebacker.scala
index f13d2b0506..3e00363bc2 100644
--- a/modules/round/src/main/Takebacker.scala
+++ b/modules/round/src/main/Takebacker.scala
@@ -12,10 +12,10 @@ private[round] final class Takebacker(
pov match {
case Pov(game, _) if pov.opponent.isProposingTakeback => single(game)
case Pov(game, _) if pov.opponent.isAi => double(game)
- case Pov(game, color) if (game playerCanProposeTakeback color) => GameRepo save {
+ case Pov(game, color) if (game playerCanProposeTakeback color) =>
messenger.system(game, _.takebackPropositionSent)
- Progress(game) map { g => g.updatePlayer(color, _.proposeTakeback) }
- } inject List(Event.ReloadOwner)
+ val progress = Progress(game) map { g => g.updatePlayer(color, _.proposeTakeback) }
+ GameRepo save progress inject List(Event.ReloadOwner)
case _ => ClientErrorException.future("[takebacker] invalid yes " + pov)
}
}
diff --git a/modules/security/src/main/Store.scala b/modules/security/src/main/Store.scala
index 222fcf7dfd..d860e3e8fb 100644
--- a/modules/security/src/main/Store.scala
+++ b/modules/security/src/main/Store.scala
@@ -22,7 +22,7 @@ object Store {
$insert(Json.obj(
"_id" -> sessionId,
"user" -> userId,
- "ip" -> ip(req),
+ "ip" -> req.remoteAddress,
"ua" -> ua(req),
"date" -> $date(DateTime.now),
"up" -> true))
@@ -44,7 +44,5 @@ object Store {
upsert = false,
multi = true)
- private def ip(req: RequestHeader) = req.remoteAddress
-
private def ua(req: RequestHeader) = req.headers.get("User-Agent") | "?"
}
diff --git a/modules/setup/src/main/Config.scala b/modules/setup/src/main/Config.scala
index cb3c89e704..33a1512b41 100644
--- a/modules/setup/src/main/Config.scala
+++ b/modules/setup/src/main/Config.scala
@@ -65,7 +65,7 @@ trait Positional { self: Config =>
}
def fenGame(builder: ChessGame => Game): Game = {
- val state = fen filter (_ => variant == Variant.FromPosition) flatMap Forsyth.<<<
+ val state = fen ifTrue (variant == Variant.FromPosition) flatMap Forsyth.<<<
val chessGame = state.fold(makeGame) {
case sit@SituationPlus(Situation(board, color), _) =>
ChessGame(
diff --git a/modules/socket/src/main/UserRegister.scala b/modules/socket/src/main/UserRegister.scala
index a0fe46a7c3..cb4c9ba055 100644
--- a/modules/socket/src/main/UserRegister.scala
+++ b/modules/socket/src/main/UserRegister.scala
@@ -6,11 +6,12 @@ import scala.collection.mutable
import scala.concurrent.duration._
import actorApi.{ SocketLeave, SocketEnter }
+import lila.hub.actorApi.round.MoveEvent
import lila.hub.actorApi.{ SendTo, SendTos, WithUserIds }
private final class UserRegister extends Actor {
- context.system.lilaBus.subscribe(self, 'users, 'socketDoor)
+ context.system.lilaBus.subscribe(self, 'users, 'socketDoor, 'moveEvent)
type UID = String
type UserId = String
@@ -25,6 +26,10 @@ private final class UserRegister extends Actor {
case WithUserIds(f) => f(users.keys)
+ case move: MoveEvent => move.opponentUserId foreach { userId =>
+ sendTo(userId, Socket.makeMessage("opponent_play", move.gameId))
+ }
+
case SocketEnter(uid, member) => member.userId foreach { userId =>
users get userId match {
case None => users += (userId -> mutable.Map(uid -> member))
diff --git a/modules/tournament/src/main/Env.scala b/modules/tournament/src/main/Env.scala
index 4671bb367b..7525fc9eda 100644
--- a/modules/tournament/src/main/Env.scala
+++ b/modules/tournament/src/main/Env.scala
@@ -15,6 +15,7 @@ final class Env(
config: Config,
system: ActorSystem,
db: lila.db.Env,
+ mongoCache: lila.memo.MongoCache.Builder,
flood: lila.security.Flood,
hub: lila.hub.Env,
roundMap: ActorRef,
@@ -61,7 +62,9 @@ final class Env(
chat = hub.actor.chat,
flood = flood)
- lazy val winners = new Winners(LeaderboardCacheTtl)
+ lazy val winners = new Winners(
+ mongoCache = mongoCache,
+ ttl = LeaderboardCacheTtl)
lazy val cached = new Cached
@@ -136,6 +139,7 @@ object Env {
config = lila.common.PlayApp loadConfig "tournament",
system = lila.common.PlayApp.system,
db = lila.db.Env.current,
+ mongoCache = lila.memo.Env.current.mongoCache,
flood = lila.security.Env.current.flood,
hub = lila.hub.Env.current,
roundMap = lila.round.Env.current.roundMap,
diff --git a/modules/tournament/src/main/Winners.scala b/modules/tournament/src/main/Winners.scala
index 13507d2540..f8be69698c 100644
--- a/modules/tournament/src/main/Winners.scala
+++ b/modules/tournament/src/main/Winners.scala
@@ -2,12 +2,20 @@ package lila.tournament
import scala.concurrent.duration.FiniteDuration
+import lila.db.BSON._
import lila.user.{ User, UserRepo }
-final class Winners(ttl: FiniteDuration) {
+final class Winners(
+ mongoCache: lila.memo.MongoCache.Builder,
+ ttl: FiniteDuration) {
- private val scheduledCache =
- lila.memo.AsyncCache(fetchScheduled, timeToLive = ttl)
+ private implicit val WinnerBSONHandler =
+ reactivemongo.bson.Macros.handler[Winner]
+
+ private val scheduledCache = mongoCache[Int, List[Winner]](
+ prefix = "tournament:winner",
+ f = fetchScheduled,
+ timeToLive = ttl)
import Schedule.Freq
private def fetchScheduled(nb: Int): Fu[List[Winner]] =
@@ -24,9 +32,9 @@ final class Winners(ttl: FiniteDuration) {
tour.winner map { w =>
Winner(tour.id, tour.name, w.id)
}
- }.map { winner =>
- UserRepo isEngine winner.userId map (!_ option winner)
- }.sequenceFu map (_.flatten)
+ }.map { winner =>
+ UserRepo isEngine winner.userId map (!_ option winner)
+ }.sequenceFu map (_.flatten)
def scheduled(nb: Int): Fu[List[Winner]] = scheduledCache apply nb
}
diff --git a/modules/user/src/main/Cached.scala b/modules/user/src/main/Cached.scala
index 19fba78b2d..d09d693f06 100644
--- a/modules/user/src/main/Cached.scala
+++ b/modules/user/src/main/Cached.scala
@@ -6,57 +6,81 @@ import org.joda.time.DateTime
import play.api.libs.json.JsObject
import reactivemongo.bson._
+import lila.common.LightUser
import lila.db.api.{ $count, $primitive }
+import lila.db.BSON._
import lila.db.Implicits._
-import lila.memo.{ AsyncCache, ExpireSetMemo }
+import lila.memo.{ ExpireSetMemo, MongoCache }
import lila.rating.{ Perf, PerfType }
import tube.userTube
final class Cached(
- nbTtl: Duration,
- onlineUserIdMemo: ExpireSetMemo) {
+ nbTtl: FiniteDuration,
+ onlineUserIdMemo: ExpireSetMemo,
+ mongoCache: MongoCache.Builder) {
private def twoWeeksAgo = DateTime.now minusWeeks 2
- private val perfs = PerfType.nonPuzzle
- private val perfKeys = perfs.map(_.key)
-
- private val countCache = AsyncCache.single($count(UserRepo.enabledSelect), timeToLive = nbTtl)
+ private val countCache = mongoCache.single[Int](
+ prefix = "user:nb",
+ f = $count(UserRepo.enabledSelect),
+ timeToLive = nbTtl)
def countEnabled: Fu[Int] = countCache(true)
val leaderboardSize = 10
- def activeSince = DateTime.now minusWeeks 2
- val topPerf = AsyncCache[Perf.Key, List[User]](
+ private implicit val userHandler = User.userBSONHandler
+
+ val topPerf = mongoCache[Perf.Key, List[User]](
+ prefix = "user:top:perf",
f = (perf: Perf.Key) => UserRepo.topPerfSince(perf, twoWeeksAgo, leaderboardSize),
- timeToLive = 10 minutes)
+ timeToLive = 15 minutes)
- val topToday = AsyncCache.single[List[(User, PerfType)]](
- f = perfs.map { perf =>
- UserRepo.topPerfSince(perf.key, DateTime.now minusHours 12, 1) map2 { (u: User) => u -> perf }
+ private case class UserPerf(user: User, perfKey: String)
+ private implicit val UserPerfBSONHandler = reactivemongo.bson.Macros.handler[UserPerf]
+
+ private val topTodayCache = mongoCache.single[List[UserPerf]](
+ prefix = "user:top:today",
+ f = PerfType.leaderboardable.map { perf =>
+ UserRepo.topPerfSince(perf.key, DateTime.now minusHours 12, 1) map2 { (u: User) =>
+ UserPerf(u, perf.key)
+ }
}.sequenceFu map (_.flatten),
- timeToLive = 10 minutes)
+ timeToLive = 14 minutes)
- val topNbGame = AsyncCache[Int, List[User]](
- UserRepo.topNbGame,
+ def topToday(x: Boolean): Fu[List[(User, PerfType)]] =
+ topTodayCache(x) map2 { (up: UserPerf) =>
+ (up.user, PerfType(up.perfKey) err s"No such perf ${up.perfKey}")
+ }
+
+ val topNbGame = mongoCache[Int, List[User]](
+ prefix = "user:top:nbGame",
+ f = UserRepo.topNbGame,
timeToLive = 34 minutes)
- val topOnline = AsyncCache[Int, List[User]](
- UserRepo.byIdsSortRating(onlineUserIdMemo.keys, _),
- timeToLive = 3 seconds)
+ val topOnline = lila.memo.AsyncCache[Int, List[User]](
+ f = UserRepo.byIdsSortRating(onlineUserIdMemo.keys, _),
+ timeToLive = 10 seconds)
- val topToints = AsyncCache[Int, List[User]](
- UserRepo.allSortToints,
- timeToLive = 10 minutes)
+ val topToints = mongoCache[Int, List[User]](
+ prefix = "user:toint:online",
+ f = UserRepo.allSortToints,
+ timeToLive = 21 minutes)
object ranking {
- def getAll(id: User.ID): Fu[Map[Perf.Key, Int]] = perfKeys.map { perf =>
- cache(perf) map { _ get id map (perf -> _) }
- }.sequenceFu map (_.flatten.toMap)
+ def getAll(id: User.ID): Fu[Map[Perf.Key, Int]] =
+ PerfType.leaderboardable.map { perf =>
+ cache(perf.key) map { _ get id map (perf.key -> _) }
+ }.sequenceFu map (_.flatten.toMap)
- private val cache = AsyncCache[Perf.Key, Map[User.ID, Int]](compute, timeToLive = 31 minutes)
+ import lila.db.BSON.MapValue.MapHandler
+
+ private val cache = mongoCache[Perf.Key, Map[User.ID, Int]](
+ prefix = "user:ranking",
+ f = compute,
+ timeToLive = 33 minutes)
private def compute(perf: Perf.Key): Fu[Map[User.ID, Int]] =
$primitive(
diff --git a/modules/user/src/main/Env.scala b/modules/user/src/main/Env.scala
index 664bcdc214..b516ec8e11 100644
--- a/modules/user/src/main/Env.scala
+++ b/modules/user/src/main/Env.scala
@@ -4,11 +4,12 @@ import akka.actor._
import com.typesafe.config.Config
import lila.common.PimpedConfig._
-import lila.memo.ExpireSetMemo
+import lila.memo.{ ExpireSetMemo, MongoCache }
final class Env(
config: Config,
db: lila.db.Env,
+ mongoCache: MongoCache.Builder,
scheduler: lila.common.Scheduler,
timeline: ActorSelection,
system: ActorSystem) {
@@ -73,7 +74,8 @@ final class Env(
lazy val cached = new Cached(
nbTtl = CachedNbTtl,
- onlineUserIdMemo = onlineUserIdMemo)
+ onlineUserIdMemo = onlineUserIdMemo,
+ mongoCache = mongoCache)
}
object Env {
@@ -81,6 +83,7 @@ object Env {
lazy val current: Env = "[boot] user" describes new Env(
config = lila.common.PlayApp loadConfig "user",
db = lila.db.Env.current,
+ mongoCache = lila.memo.Env.current.mongoCache,
scheduler = lila.common.PlayApp.scheduler,
timeline = lila.hub.Env.current.actor.timeline,
system = lila.common.PlayApp.system)
diff --git a/modules/user/src/main/UidNb.scala b/modules/user/src/main/UidNb.scala
new file mode 100644
index 0000000000..6d76e3d4c2
--- /dev/null
+++ b/modules/user/src/main/UidNb.scala
@@ -0,0 +1,8 @@
+package lila.user
+
+case class UidNb(userId: String, nb: Int)
+
+object UidNb {
+
+ implicit val UidNbBSONHandler = reactivemongo.bson.Macros.handler[UidNb]
+}
diff --git a/modules/user/src/main/User.scala b/modules/user/src/main/User.scala
index deabdd571e..25cf298dcf 100644
--- a/modules/user/src/main/User.scala
+++ b/modules/user/src/main/User.scala
@@ -122,7 +122,7 @@ object User {
import lila.db.BSON
- private def userBSONHandler = new BSON[User] {
+ val userBSONHandler = new BSON[User] {
import BSONFields._
import reactivemongo.bson.BSONDocument
diff --git a/modules/user/src/main/package.scala b/modules/user/src/main/package.scala
index 28643f8d80..7087fc9ce9 100644
--- a/modules/user/src/main/package.scala
+++ b/modules/user/src/main/package.scala
@@ -9,6 +9,4 @@ package object user extends PackageObject with WithPlay {
private[user] implicit lazy val profileTube = Profile.tube
}
-
- private[user] def maxInactivityDate = org.joda.time.DateTime.now minusMonths 6
}
diff --git a/project/Build.scala b/project/Build.scala
index cabadc96c7..b7e9ca44ba 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -106,14 +106,14 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, RM, PRM)
)
- lazy val memo = project("memo", Seq(common)).settings(
- libraryDependencies ++= Seq(guava, findbugs, spray.caching) ++ provided(play.api)
- )
-
lazy val db = project("db", Seq(common)).settings(
libraryDependencies ++= provided(play.test, play.api, RM, PRM)
)
+ lazy val memo = project("memo", Seq(common, db)).settings(
+ libraryDependencies ++= Seq(guava, findbugs, spray.caching) ++ provided(play.api, RM)
+ )
+
lazy val search = project("search", Seq(common, hub)).settings(
libraryDependencies ++= provided(play.api, elastic4s)
)
diff --git a/public/font25/fonts/lichess.woff b/public/font25/fonts/lichess.woff
deleted file mode 100644
index 01f4ae9a03..0000000000
Binary files a/public/font25/fonts/lichess.woff and /dev/null differ
diff --git a/public/font25/fonts/lichess.eot b/public/font26/fonts/lichess.eot
similarity index 88%
rename from public/font25/fonts/lichess.eot
rename to public/font26/fonts/lichess.eot
index 9ef61cab01..b2ffc785b3 100644
Binary files a/public/font25/fonts/lichess.eot and b/public/font26/fonts/lichess.eot differ
diff --git a/public/font25/fonts/lichess.svg b/public/font26/fonts/lichess.svg
similarity index 97%
rename from public/font25/fonts/lichess.svg
rename to public/font26/fonts/lichess.svg
index 1eb2242e3d..1be931397d 100644
--- a/public/font25/fonts/lichess.svg
+++ b/public/font26/fonts/lichess.svg
@@ -93,4 +93,5 @@
+
diff --git a/public/font25/fonts/lichess.ttf b/public/font26/fonts/lichess.ttf
similarity index 90%
rename from public/font25/fonts/lichess.ttf
rename to public/font26/fonts/lichess.ttf
index 353c825acb..74b04fc985 100644
Binary files a/public/font25/fonts/lichess.ttf and b/public/font26/fonts/lichess.ttf differ
diff --git a/public/font26/fonts/lichess.woff b/public/font26/fonts/lichess.woff
new file mode 100644
index 0000000000..4a1829599d
Binary files /dev/null and b/public/font26/fonts/lichess.woff differ
diff --git a/public/font25/icons-reference.html b/public/font26/icons-reference.html
similarity index 99%
rename from public/font25/icons-reference.html
rename to public/font26/icons-reference.html
index d8c2ecc180..8770cf0452 100644
--- a/public/font25/icons-reference.html
+++ b/public/font26/icons-reference.html
@@ -389,6 +389,10 @@ h2{font-size:18px;padding:0 0 21px 5px;margin:45px 0 0 0;text-transform:uppercas
+
+
+
+
Character mapping