tournament leaderboard WIP
parent
c537b11649
commit
56b15b2757
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -88,6 +88,7 @@ final class Env(
|
|||
flood = flood)
|
||||
|
||||
lazy val winners = new WinnersApi(
|
||||
coll = tournamentColl,
|
||||
mongoCache = mongoCache,
|
||||
ttl = LeaderboardCacheTtl)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue