tournament leaderboard WIP

pull/2322/head
Thibault Duplessis 2016-10-17 16:17:55 +02:00
parent c537b11649
commit 56b15b2757
12 changed files with 197 additions and 44 deletions

View File

@ -48,6 +48,12 @@ object Tournament extends LilaController {
Ok(html.tournament.faqPage(system)).fuccess
}
def leaderboard = Open { implicit ctx =>
env.winners.all.map { winners =>
Ok(html.tournament.leaderboard(winners))
}
}
def show(id: String) = Open { implicit ctx =>
val page = getInt("page")
negotiate(

View File

@ -38,22 +38,6 @@ trait TournamentHelper { self: I18nHelper with DateHelper with UserHelper =>
def tournamentIdToName(id: String) = tournamentEnv.cached name id getOrElse "Tournament"
object scheduledTournamentNameShortHtml {
private def icon(c: Char) = s"""<span data-icon="$c"></span>"""
private val replacements = List(
"Lichess " -> "",
"Marathon" -> icon('\\'),
"SuperBlitz" -> icon(lila.rating.PerfType.Blitz.iconChar)
) ::: lila.rating.PerfType.leaderboardable.map { pt =>
pt.name -> icon(pt.iconChar)
}
def apply(name: String) = Html {
replacements.foldLeft(name) {
case (n, (from, to)) => n.replace(from, to)
}
}
}
def systemName(sys: System)(implicit ctx: UserContext) = sys match {
case System.Arena => System.Arena.toString
}

View File

@ -161,7 +161,7 @@ description = trans.freeOnlineChessGamePlayChessNowInACleanInterfaceNoRegistrati
@tournamentWinners.map { w =>
<tr>
<td>@userIdLink(w.userId.some)</td>
<td><a href="@routes.Tournament.show(w.tourId)">@w.tourName.replace("Lichess ", "")</a></td>
<td><a href="@routes.Tournament.show(w.tourId)">Some</a></td>
</tr>
}
</tbody>

View File

@ -0,0 +1,43 @@
@(winners: lila.tournament.AllWinners)(implicit ctx: Context)
@import lila.rating.PerfType
@freqWinner(w: lila.tournament.Winner, freq: String) = {
<li>
@userIdLink(w.userId.some)
<a href="@routes.Tournament.show(w.tourId)">@freq</a>
</li>
}
@freqWinners(fws: lila.tournament.FreqWinners, perfType: PerfType, name: String) = {
<div class="winner_list">
<h2 class="text" data-icon="@perfType.iconChar">@name</h2>
<ul>
@fws.daily.map { w => @freqWinner(w, "Daily") }
@fws.weekly.map { w => @freqWinner(w, "Weekly") }
@fws.monthly.map { w => @freqWinner(w, "Monthly") }
@fws.yearly.map { w => @freqWinner(w, "Yearly") }
</ul>
</div>
}
@base.layout(
title = "Tournament leaderboard",
moreCss = cssTag("tournament_leaderboard.css")) {
<div class="content_box no_padding tournament_leaderboard">
<h1>Tournament winners</h1>
<div class="winner_lists">
@freqWinners(winners.hyperbullet, PerfType.Bullet, "HyperBullet")
@freqWinners(winners.bullet, PerfType.Bullet, "Bullet")
@freqWinners(winners.superblitz, PerfType.Blitz, "SuperBlitz")
@freqWinners(winners.blitz, PerfType.Blitz, "Blitz")
@freqWinners(winners.classical, PerfType.Classical, "Classical")
@lila.tournament.WinnersApi.variants.map { v =>
@PerfType.byVariant(v).map { pt =>
@freqWinners(winners.classical, pt, v.name)
}
}
</div>
</div>
}

View File

@ -81,7 +81,7 @@ description = "Best chess players in bullet, blitz, classical, Chess960 and more
@tourneyWinners.map { w =>
<tr>
<td>@userIdLink(w.userId.some)</td>
<td><a href="@routes.Tournament.show(w.tourId)">@scheduledTournamentNameShortHtml(w.tourName)</a></td>
<td><a href="@routes.Tournament.show(w.tourId)">Some</a></td>
</tr>
}
</tbody></table>

View File

@ -207,6 +207,7 @@ GET /tournament/$id<\w{8}>/player/:user controllers.Tournament.player(id:
POST /tournament/$id<\w{8}>/terminate controllers.Tournament.terminate(id: String)
GET /tournament/help controllers.Tournament.help(system: Option[String] ?= None)
GET /tournament/limited-invitation controllers.Tournament.limitedInvitation
GET /tournament/leaderboard controllers.Tournament.leaderboard
# Tournament CRUD
GET /tournament/manager controllers.TournamentCrud.index

View File

@ -3,7 +3,7 @@ package lila.tournament
import chess.variant.Variant
import chess.{ Speed, Mode, StartingPosition }
import lila.db.BSON
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.rating.PerfType
import reactivemongo.bson._
@ -19,6 +19,15 @@ object BSONHandlers {
def write(x: Status) = BSONInteger(x.id)
}
private implicit val scheduleFreqHandler = new BSONHandler[BSONString, Schedule.Freq] {
def read(bsonStr: BSONString) = Schedule.Freq(bsonStr.value) err s"No such freq: ${bsonStr.value}"
def write(x: Schedule.Freq) = BSONString(x.name)
}
private implicit val scheduleSpeedHandler = new BSONHandler[BSONString, Schedule.Speed] {
def read(bsonStr: BSONString) = Schedule.Speed(bsonStr.value) err s"No such speed: ${bsonStr.value}"
def write(x: Schedule.Speed) = BSONString(x.name)
}
private implicit val tournamentClockBSONHandler = Macros.handler[TournamentClock]
private implicit val spotlightBSONHandler = Macros.handler[Spotlight]
@ -50,9 +59,9 @@ object BSONHandlers {
password = r.strO("password"),
conditions = conditions,
schedule = for {
doc <- r.getO[BSONDocument]("schedule")
freq <- doc.getAs[String]("freq") flatMap Schedule.Freq.apply
speed <- doc.getAs[String]("speed") flatMap Schedule.Speed.apply
doc <- r.getO[Bdoc]("schedule")
freq <- doc.getAs[Schedule.Freq]("freq")
speed <- doc.getAs[Schedule.Speed]("speed")
} yield Schedule(freq, speed, variant, position, startsAt, conditions),
nbPlayers = r int "nbPlayers",
createdAt = r date "createdAt",
@ -62,7 +71,7 @@ object BSONHandlers {
featuredId = r strO "featured",
spotlight = r.getO[Spotlight]("spotlight"))
}
def writes(w: BSON.Writer, o: Tournament) = BSONDocument(
def writes(w: BSON.Writer, o: Tournament) = $doc(
"_id" -> o.id,
"name" -> o.name,
"status" -> o.status,
@ -76,9 +85,9 @@ object BSONHandlers {
"password" -> o.password,
"conditions" -> o.conditions.ifNonEmpty,
"schedule" -> o.schedule.map { s =>
BSONDocument(
"freq" -> s.freq.name,
"speed" -> s.speed.name)
$doc(
"freq" -> s.freq,
"speed" -> s.speed)
},
"nbPlayers" -> o.nbPlayers,
"createdAt" -> w.date(o.createdAt),
@ -102,7 +111,7 @@ object BSONHandlers {
magicScore = r int "m",
fire = r boolD "f",
performance = r intO "e")
def writes(w: BSON.Writer, o: Player) = BSONDocument(
def writes(w: BSON.Writer, o: Player) = $doc(
"_id" -> o._id,
"tid" -> o.tourId,
"uid" -> o.userId,
@ -132,7 +141,7 @@ object BSONHandlers {
berserk1 = r intD "b1",
berserk2 = r intD "b2")
}
def writes(w: BSON.Writer, o: Pairing) = BSONDocument(
def writes(w: BSON.Writer, o: Pairing) = $doc(
"_id" -> o.id,
"tid" -> o.tourId,
"s" -> o.status.id,
@ -157,7 +166,7 @@ object BSONHandlers {
perf = PerfType.byId get r.int("v") err "Invalid leaderboard perf",
date = r date "d")
def writes(w: BSON.Writer, o: LeaderboardApi.Entry) = BSONDocument(
def writes(w: BSON.Writer, o: LeaderboardApi.Entry) = $doc(
"_id" -> o.id,
"u" -> o.userId,
"t" -> o.tourId,

View File

@ -88,6 +88,7 @@ final class Env(
flood = flood)
lazy val winners = new WinnersApi(
coll = tournamentColl,
mongoCache = mongoCache,
ttl = LeaderboardCacheTtl)

View File

@ -97,6 +97,10 @@ case class Tournament(
def schedulePair = schedule map { this -> _ }
def winner = winnerId map { userId =>
Winner(tourId = id, userId = userId)
}
override def toString = s"$id $startsAt $fullName $minutes minutes, $clock"
}

View File

@ -1,36 +1,86 @@
package lila.tournament
import org.joda.time.DateTime
import reactivemongo.api.ReadPreference
import scala.concurrent.duration.FiniteDuration
import chess.variant.Variant
import lila.db.BSON._
import chess.variant.{ Variant, Standard, FromPosition }
import lila.db.dsl._
import lila.user.{ User, UserRepo }
import Schedule.{ Freq, Speed }
case class FreqWinners(value: Map[Schedule.Freq, Winner])
case class FreqWinners(
yearly: Option[Winner],
monthly: Option[Winner],
weekly: Option[Winner],
daily: Option[Winner])
case class AllWinners(
hyperbullet: FreqWinners,
bullet: FreqWinners,
superblitz: FreqWinners,
blitz: FreqWinners,
classical: FreqWinners,
variants: Map[Variant, FreqWinners])
variants: Map[String, FreqWinners])
final class WinnersApi(
coll: Coll,
mongoCache: lila.memo.MongoCache.Builder,
ttl: FiniteDuration) {
private implicit val WinnerBSONHandler = reactivemongo.bson.Macros.handler[Winner]
import BSONHandlers._
import lila.db.BSON.MapDocument.MapHandler
private implicit val WinnerHandler = reactivemongo.bson.Macros.handler[Winner]
private implicit val FreqWinnersHandler = reactivemongo.bson.Macros.handler[FreqWinners]
private implicit val AllWinnersHandler = reactivemongo.bson.Macros.handler[AllWinners]
// private def fetchAll: Fu[AllWinners] =
private def fetchLastFreq(freq: Freq, since: DateTime): Fu[List[Tournament]] = coll.find($doc(
"schedule.freq" -> freq.name,
"startsAt" $gt since.minusHours(12),
"winner" $exists true
)).sort($sort desc "startsAt")
.cursor[Tournament](readPreference = ReadPreference.secondaryPreferred)
.gather[List]()
// private val allCache = mongoCache.single[AllWinners](
// prefix = "tournament:winner:all",
// f = fetchAll,
// timeToLive = ttl,
// keyToString = _.toString)
private def firstStandardWinner(tours: List[Tournament], speed: Speed): Option[Winner] =
tours.find { t =>
t.variant.standard && t.schedule.exists(_.speed == speed)
}.flatMap(_.pp.winner.pp)
// def all: Fu[AllWinners] = allCache(true)
private def firstVariantWinner(tours: List[Tournament], variant: Variant): Option[Winner] =
tours.find(_.variant == variant).flatMap(_.winner)
private def fetchAll: Fu[AllWinners] = for {
yearlies <- fetchLastFreq(Freq.Yearly, DateTime.now.minusYears(1))
monthlies <- fetchLastFreq(Freq.Monthly, DateTime.now.minusMonths(2))
weeklies <- fetchLastFreq(Freq.Weekly, DateTime.now.minusWeeks(2))
dailies <- fetchLastFreq(Freq.Daily, DateTime.now.minusDays(2))
} yield {
def standardFreqWinners(speed: Speed): FreqWinners = FreqWinners(
yearly = firstStandardWinner(yearlies, speed),
monthly = firstStandardWinner(monthlies, speed),
weekly = firstStandardWinner(weeklies, speed),
daily = firstStandardWinner(dailies, speed))
AllWinners(
hyperbullet = standardFreqWinners(Speed.HyperBullet),
bullet = standardFreqWinners(Speed.Bullet),
superblitz = standardFreqWinners(Speed.SuperBlitz),
blitz = standardFreqWinners(Speed.Blitz),
classical = standardFreqWinners(Speed.Classical),
variants = WinnersApi.variants.map { v =>
v.key -> FreqWinners(
yearly = firstVariantWinner(yearlies, v),
monthly = firstVariantWinner(monthlies, v),
weekly = firstVariantWinner(weeklies, v),
daily = firstVariantWinner(dailies, v))
}.toMap)
}
private val allCache = mongoCache.single[AllWinners](
prefix = "tournament:winner:all",
f = fetchAll,
timeToLive = ttl)
def all: Fu[AllWinners] = allCache(true)
private val scheduledCache = mongoCache[Int, List[Winner]](
prefix = "tournament:winner",
@ -38,7 +88,6 @@ final class WinnersApi(
timeToLive = ttl,
keyToString = _.toString)
import Schedule.Freq
private def fetchScheduled(nb: Int): Fu[List[Winner]] = {
val since = DateTime.now minusMonths 1
List(Freq.Monthly, Freq.Weekly, Freq.Daily).map { freq =>
@ -52,7 +101,7 @@ final class WinnersApi(
tours.sortBy(_.schedule.map(_.freq)).reverse.map { tour =>
PlayerRepo winner tour.id flatMap {
case Some(player) => UserRepo isEngine player.userId map { engine =>
!engine option Winner(tour.id, tour.name, player.userId)
!engine option Winner(tour.id, player.userId)
}
case _ => fuccess(none)
}
@ -60,3 +109,11 @@ final class WinnersApi(
def scheduled(nb: Int): Fu[List[Winner]] = scheduledCache apply nb
}
object WinnersApi {
val variants = Variant.all.filter {
case Standard | FromPosition => false
case _ => true
}
}

View File

@ -67,4 +67,4 @@ case class FeaturedGame(
white: RankedPlayer,
black: RankedPlayer)
case class Winner(tourId: String, tourName: String, userId: String)
case class Winner(tourId: String, userId: String)

View File

@ -0,0 +1,48 @@
.content_box h1 {
text-align: center;
}
.tournament_leaderboard .winner_lists {
display: flex;
flex-flow: row wrap;
}
.winner_list {
width: 50%;
flex: 0 0 50%;
padding: 20px 25px 30px 20px;
box-sizing: border-box;
border-bottom: 5px solid transparent;
}
.winner_list:hover {
background: #fafafa;
border-bottom: 5px solid #ddd;
}
.winner_list h2 {
font-size: 2em;
letter-spacing: 5px;
font-family: Roboto;
line-height: 2em;
border-bottom: 1px solid #ddd;
margin-bottom: 10px;
}
.winner_list h2.text::before {
opacity: 0.7;
vertical-align: -5px;
margin-right: 8px;
}
.winner_list li {
width: 100%;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
line-height: 3.5em;
font-size: 1.2em;
}
.winner_list li:hover a {
color: #3893E8;
}
.winner_list a.user_link {
font-size: 1.2em;
margin: 0 10px 0 -3px;
overflow: hidden;
text-overflow: ellipsis;
}