display arena tournament player stats

reparsing
Thibault Duplessis 2015-10-05 14:40:42 +02:00
parent 51cb99994a
commit 7a0ebe63bf
13 changed files with 103 additions and 45 deletions

View File

@ -92,8 +92,8 @@ object Tournament extends LilaController {
def player(id: String, userId: String) = Open { implicit ctx =>
JsonOptionFuOk(UserRepo byId userId) { user =>
env.api.playerInfo(id, user) map {
_ map env.jsonView.playerInfo
env.api.playerInfo(id, user) flatMap {
_ ?? env.jsonView.playerInfo
}
}
}

View File

@ -5,7 +5,8 @@ import scala.concurrent.duration._
import lila.memo._
private[tournament] final class Cached(
createdTtl: FiniteDuration) {
createdTtl: FiniteDuration,
rankingTtl: FiniteDuration) {
private val nameCache = MixedCache[String, Option[String]](
((id: String) => TournamentRepo byId id map2 { (tour: Tournament) => tour.fullName }),
@ -17,4 +18,14 @@ private[tournament] final class Cached(
val promotable = AsyncCache.single(
TournamentRepo.promotable,
timeToLive = createdTtl)
// only applies to ongoing tournaments
val ongoingRanking = AsyncCache[String, Ranking](
PlayerRepo.computeRanking,
timeToLive = 3.seconds)
// only applies to finished tournaments
val finishedRanking = AsyncCache[String, Ranking](
PlayerRepo.computeRanking,
timeToLive = rankingTtl)
}

View File

@ -33,6 +33,7 @@ final class Env(
val HistoryMessageTtl = config duration "history.message.ttl"
val CreatedCacheTtl = config duration "created.cache.ttl"
val LeaderboardCacheTtl = config duration "leaderboard.cache.ttl"
val RankingCacheTtl = config duration "ranking.cache.ttl"
val UidTimeout = config duration "uid.timeout"
val SocketTimeout = config duration "socket.timeout"
val SocketName = config getString "socket.name"
@ -46,11 +47,13 @@ final class Env(
lazy val forms = new DataForm
lazy val cached = new Cached(CreatedCacheTtl)
lazy val cached = new Cached(
createdTtl = CreatedCacheTtl,
rankingTtl = RankingCacheTtl)
lazy val api = new TournamentApi(
cached = cached,
scheduleJsonView = scheduleJsonView ,
scheduleJsonView = scheduleJsonView,
system = system,
sequencers = sequencerMap,
autoPairing = autoPairing,
@ -75,7 +78,7 @@ final class Env(
mongoCache = mongoCache,
ttl = LeaderboardCacheTtl)
lazy val jsonView = new JsonView(lightUser)
lazy val jsonView = new JsonView(lightUser, cached)
lazy val scheduleJsonView = new ScheduleJsonView(lightUser)

View File

@ -10,7 +10,8 @@ import lila.game.{ Game, GameRepo, Pov }
import lila.user.User
final class JsonView(
getLightUser: String => Option[LightUser]) {
getLightUser: String => Option[LightUser],
cached: Cached) {
private case class CachableData(pairings: JsArray, games: JsArray, podium: Option[JsArray])
@ -54,18 +55,23 @@ final class JsonView(
def clearCache(id: String) =
firstPageCache.remove(id) >> cachableData.remove(id)
def playerInfo(info: PlayerInfoExt): JsObject = info match {
case PlayerInfoExt(tour, user, player, povs) => Json.obj(
def playerInfo(info: PlayerInfoExt): Fu[JsObject] = for {
ranking <- info.tour.isFinished.fold(cached.finishedRanking, cached.ongoingRanking)(info.tour.id)
userSheet <- info.tour.system.scoringSystem.sheet(info.tour, info.user.id)
} yield (info, userSheet) match {
case (PlayerInfoExt(tour, user, player, povs), sheet: arena.ScoringSystem.Sheet) => Json.obj(
"player" -> Json.obj(
"id" -> user.id,
"name" -> user.username,
"title" -> user.title,
"rank" -> ranking.get(user.id).map(1+),
"rating" -> player.rating,
"provisional" -> player.provisional.option(true),
"withdraw" -> player.withdraw.option(true),
"score" -> player.score,
"perf" -> player.perf,
"fire" -> player.fire
"fire" -> player.fire,
"nb" -> sheetNbs(sheet)
).noNull,
"pairings" -> povs.map { pov =>
Json.obj(
@ -80,6 +86,11 @@ final class JsonView(
)
}
private def sheetNbs(sheet: arena.ScoringSystem.Sheet) = Json.obj(
"game" -> sheet.scores.size,
"berserk" -> sheet.scores.count(_.isBerserk),
"win" -> sheet.scores.count(_.isWin))
private def computeStanding(tour: Tournament, page: Int): Fu[JsObject] = for {
rankedPlayers <- PlayerRepo.bestByTourWithRankByPage(tour.id, 10, page max 1)
sheets <- rankedPlayers.map { p =>

View File

@ -114,7 +114,8 @@ object PlayerRepo {
def winner(tourId: String): Fu[Option[Player]] =
coll.find(selectTour(tourId)).sort(bestSort).one[Player]
def ranking(tourId: String): Fu[Ranking] =
// freaking expensive (marathons)
private[tournament] def computeRanking(tourId: String): Fu[Ranking] =
coll.aggregate(Match(selectTour(tourId)), List(Sort(Descending("m")),
Group(BSONBoolean(true))("uids" -> Push("uid")))).
map(res => aggregationUserIdList(res.documents.toStream)).

View File

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

View File

@ -61,19 +61,21 @@ private[tournament] final class TournamentApi(
def makePairings(oldTour: Tournament, users: WaitingUsers, startAt: Long) {
Sequencing(oldTour.id)(TournamentRepo.startedById) { tour =>
tour.system.pairingSystem.createPairings(tour, users) flatMap {
case Nil => funit
case pairings if nowMillis - startAt > 1000 =>
play.api.Logger("tourpairing").warn(s"Give up making http://lichess.org/tournament/${tour.id} ${pairings.size} pairings in ${nowMillis - startAt}ms")
funit
case pairings => pairings.map { pairing =>
PairingRepo.insert(pairing) >> autoPairing(tour, pairing)
}.sequenceFu.map {
_ map StartGame.apply foreach { sendTo(tour.id, _) }
} >>- {
val time = nowMillis - startAt
if (time > 100)
play.api.Logger("tourpairing").debug(s"Done making http://lichess.org/tournament/${tour.id} ${pairings.size} pairings in ${time}ms")
cached.ongoingRanking(tour.id) flatMap { ranking =>
tour.system.pairingSystem.createPairings(tour, users, ranking) flatMap {
case Nil => funit
case pairings if nowMillis - startAt > 1000 =>
play.api.Logger("tourpairing").warn(s"Give up making http://lichess.org/tournament/${tour.id} ${pairings.size} pairings in ${nowMillis - startAt}ms")
funit
case pairings => pairings.map { pairing =>
PairingRepo.insert(pairing) >> autoPairing(tour, pairing)
}.sequenceFu.map {
_ map StartGame.apply foreach { sendTo(tour.id, _) }
} >>- {
val time = nowMillis - startAt
if (time > 100)
play.api.Logger("tourpairing").debug(s"Done making http://lichess.org/tournament/${tour.id} ${pairings.size} pairings in ${time}ms")
}
}
}
}

View File

@ -18,10 +18,10 @@ object PairingSystem extends AbstractPairingSystem {
// then pair all users
def createPairings(
tour: Tournament,
users: WaitingUsers): Fu[Pairings] = for {
users: WaitingUsers,
ranking: Ranking): Fu[Pairings] = for {
recentPairings <- PairingRepo.recentByTourAndUserIds(tour.id, users.all, Math.min(120, users.size * 5))
nbActiveUsers <- PlayerRepo.countActive(tour.id)
ranking <- PlayerRepo.ranking(tour.id)
data = Data(tour, recentPairings, ranking, nbActiveUsers)
pairings <- {
if (recentPairings.isEmpty) evenOrAll(data, users)

View File

@ -22,6 +22,10 @@ object ScoringSystem extends AbstractScoringSystem {
case (None, _) => 1
case _ => 0
}) + (~win ?? berserk)
def isBerserk = berserk > 0
def isWin = win contains true
}
case class Sheet(scores: List[Score]) extends ScoreSheet {

View File

@ -354,55 +354,69 @@ ol.scheduled_tournaments a {
font-size: 1.2em;
padding: 10px 0;
background: #fff;
}
#tournament_side .player .stats {
background: #fff;
padding: 10px 0;
border-bottom: 1px solid #ccc;
}
#tournament_side .player table {
margin: auto;
}
#tournament_side .player .stats td {
font-weight: bold;
padding-left: 10px;
}
#tournament_side .player .pairings {
text-align: left;
width: 100%;
box-sizing: border-box;
}
#tournament_side .player tr {
#tournament_side .player .pairings tr {
cursor: pointer;
transition: background-color 0.13s;
}
#tournament_side .player tr:hover {
#tournament_side .player .pairings tr:hover {
background: #fff;
}
#tournament_side .player td {
#tournament_side .player .pairings tr > * {
padding: 3px 5px;
border-bottom: 1px solid #e0e0e0;
}
#tournament_side .player td:first-child {
width: 130px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 10px;
#tournament_side .player .pairings th {
padding-left: 7px;
border-left: 3px solid transparent;
border-left-color: #fff;
transition: 0.13s;
font-family: 'Roboto';
font-weight: 300;
}
#tournament_side .player tr:hover td:first-child {
#tournament_side .player .pairings tr:hover th {
border-left-color: #888!important;
}
#tournament_side .player tr.win:hover td:first-child {
#tournament_side .player .pairings tr.win:hover th {
border-left-color: #759900!important;
}
#tournament_side .player tr.loss:hover td:first-child {
#tournament_side .player .pairings tr.loss:hover th {
border-left-color: #dc322f!important;
}
#tournament_side .player td:last-child {
#tournament_side .player .pairings td:nth-child(2) {
width: 110px;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#tournament_side .player .pairings td:last-child {
font-weight: bold;
text-align: center;
opacity: 0.8;
}
#tournament_side .player tr.win td:last-child {
#tournament_side .player .pairings tr.win td:last-child {
color: #759900;
opacity: 1;
}
#tournament_side .player tr.loss td:last-child {
#tournament_side .player .pairings tr.loss td:last-child {
color: #dc322f;
opacity: 1;
}

View File

@ -2,6 +2,7 @@ var m = require('mithril');
var partial = require('chessground').util.partial;
var classSet = require('chessground').util.classSet;
var util = require('./util');
var ratio2percent = util.ratio2percent;
var button = require('./button');
var scoreTagNames = ['score', 'streak', 'double'];

View File

@ -17,13 +17,19 @@ function result(win, stat) {
module.exports = function(ctrl) {
var data = ctrl.vm.playerInfo.data;
if (!data || data.player.id !== ctrl.vm.playerInfo.id) return m('span.square-spin');
var nb = data.player.nb;
return m('div.player', {
config: function(el, isUpdate) {
if (!isUpdate) $('body').trigger('lichess.content_loaded');
}
}, [
m('h2', util.player(data.player)),
m('div.scroll-shadow-soft', m('table', data.pairings.map(function(p) {
m('h2', [m('span.rank', data.player.rank + '. '), util.player(data.player)]),
m('div.stats', m('table', [
m('tr', [m('th', 'Games played'), m('td', nb.game)]),
m('tr', [m('th', 'Win rate'), m('td', util.ratio2percent(nb.win / nb.game))]),
m('tr', [m('th', 'Berserk rate'), m('td', util.ratio2percent(nb.berserk / nb.game))])
])),
m('div.scroll-shadow-soft', m('table.pairings', data.pairings.map(function(p, i) {
var res = result(p.win, p.status);
return m('tr', {
onclick: function() {
@ -31,6 +37,7 @@ module.exports = function(ctrl) {
},
class: res === '1' ? 'win' : (res === '0' ? 'loss' : '')
}, [
m('th', nb.game - i),
m('td', (p.op.title ? p.op.title + ' ' : '') + p.op.name),
m('td', p.op.rating),
m('td', {

View File

@ -90,5 +90,8 @@ module.exports = {
time: time
});
};
},
ratio2percent: function(r) {
return Math.round(100 * r) + '%';
}
};