display arena tournament player stats
parent
51cb99994a
commit
7a0ebe63bf
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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)).
|
||||
|
|
|
@ -28,7 +28,8 @@ object System {
|
|||
trait PairingSystem {
|
||||
def createPairings(
|
||||
tournament: Tournament,
|
||||
users: WaitingUsers): Fu[Pairings]
|
||||
users: WaitingUsers,
|
||||
ranking: Ranking): Fu[Pairings]
|
||||
}
|
||||
|
||||
trait Score {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -90,5 +90,8 @@ module.exports = {
|
|||
time: time
|
||||
});
|
||||
};
|
||||
},
|
||||
ratio2percent: function(r) {
|
||||
return Math.round(100 * r) + '%';
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue