improve on pools

This commit is contained in:
Thibault Duplessis 2014-06-18 23:16:34 +02:00
parent 82e3988cce
commit 8ff37890a3
12 changed files with 135 additions and 71 deletions

View file

@ -27,7 +27,7 @@ final class Env(
nowPlaying = Env.round.nowPlaying,
dailyPuzzle = Env.puzzle.daily,
streamsOnAir = () => Env.tv.streamsOnAir,
getPools = () => Env.pool.repo.all)
getPools = () => Env.pool.repo.all apply true)
lazy val userInfo = mashup.UserInfo(
countUsers = () => Env.user.countEnabled,

View file

@ -149,30 +149,40 @@ trait GameHelper { self: I18nHelper with UserHelper with AiHelper with StringHel
s"$u1 vs $u2$clock"
}
// whiteUsername 1-0 blackUsername
def gameSummary(whiteUserId: String, blackUserId: String, finished: Boolean, result: Option[Boolean]) = {
val res = if (finished) result match {
case Some(true) => "1-0"
case Some(false) => "0-1"
case None => "½-½"
}
else "*"
s"${usernameOrId(whiteUserId)} $res ${usernameOrId(blackUserId)}"
}
def gameFen(game: Game, color: Color, ownerLink: Boolean = false, tv: Boolean = false)(implicit ctx: UserContext) = Html {
val owner = ownerLink.fold(ctx.me flatMap game.player, none)
var live = game.isBeingPlayed
var isLive = game.isBeingPlayed
val url = owner.fold(routes.Round.watcher(game.id, color.name)) { o =>
routes.Round.player(game fullIdOf o.color)
}
"""<a href="%s" title="%s" class="mini_board parse_fen %s" data-live="%s" data-color="%s" data-fen="%s" data-lastmove="%s"></a>""".format(
tv.fold(routes.Tv.index, url),
gameTitle(game, color),
live ?? ("live live_" + game.id),
live ?? game.id,
color.name,
Forsyth exportBoard game.toChess.board,
~game.castleLastMoveTime.lastMoveString)
val href = tv.fold(routes.Tv.index, url)
val title = gameTitle(game, color)
val cssClass = isLive ?? ("live live_" + game.id)
val live = isLive ?? game.id
val fen = Forsyth exportBoard game.toChess.board
val lastMove = ~game.castleLastMoveTime.lastMoveString
s"""<a href="$href" title="$title" class="mini_board parse_fen $cssClass" data-live="$live" data-color="${color.name}" data-fen="$fen" data-lastmove="$lastMove"></a>"""
}
def gameFenNoCtx(game: Game, color: Color, tv: Boolean = false, blank: Boolean = false) = Html {
var live = game.isBeingPlayed
var isLive = game.isBeingPlayed
"""<a href="%s%s" title="%s" class="mini_board parse_fen %s" data-live="%s" data-color="%s" data-fen="%s" data-lastmove="%s"%s></a>""".format(
blank ?? netBaseUrl,
tv.fold(routes.Tv.index, routes.Round.watcher(game.id, color.name)),
gameTitle(game, color),
live ?? ("live live_" + game.id),
live ?? game.id,
isLive ?? ("live live_" + game.id),
isLive ?? game.id,
color.name,
Forsyth exportBoard game.toChess.board,
~game.castleLastMoveTime.lastMoveString,

View file

@ -28,20 +28,19 @@ goodies = goodies.some,
chat = chat.map(c => base.chat(c, trans.chatRoom.str())),
underchat = underchat.some) {
<div id="pool_side" class="scroll-shadow-soft">
<div class="pairings">
@p.pairings.take(50).map { pairing =>
<a class="revert-underline" href="@routes.Round.watcher(pairing.gameId, "white")">
@showPairingUser(pairing, pairing.user1) <em>vs</em> @showPairingUser(pairing, pairing.user2)
</a>
}
</div>
</div>
<div id="pool"
data-id="@p.setup.id"
data-version="@version"
data-socket-url="@routes.Pool.websocket(p.setup.id)">
<div class="pool_side scroll-shadow-soft">
<div class="pairings">
@p.pairings.take(50).map { pairing =>
<a class="revert-underline" href="@routes.Round.watcher(pairing.gameId, "white")">
@showPairingUser(pairing, pairing.user1) <em>vs</em> @showPairingUser(pairing, pairing.user2)
</a>
}
</div>
</div>
<div class="content_box no_padding pool_box pool_show">
<h1 data-icon="8">
@p.setup.name Pool

View file

@ -1,12 +1,12 @@
@(p: lila.pool.Pool)(implicit ctx: Context)
<div class="standing_wrap">
<div class="standing_wrap scroll-shadow-soft">
<table class="slist standing">
<thead>
<tr>
<th class="large" colspan="2">@trans.standing() (@p.nbPlayers)</th>
<th>Recent games</th>
<th>@p.setup.name rating</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
@ -19,17 +19,24 @@
@lightUserLink(player.user)
</td>
<td class="recent_games">
@score.recentGames.map { res =>
<span class="@res match {
case None => {draw}
case Some(true) => {win}
case Some(false) => {loss}
}" data-icon="J"></span>
@score.recentPairings.map { pairing =>
<a href="@routes.Round.watcher(pairing.gameId, pairing.colorOf(player.user.id).??(_.toString))"
data-hint="@gameSummary(pairing.user1, pairing.user2, pairing.finished, pairing.booleanResult)"
class="hint--bottom-left
@if(pairing.finished) {
finished @if(pairing wonBy player.user.id) {win} else {
@if(pairing lostBy player.user.id) {loss} else {draw}}
">@pairing.ratingDiffOf(player.user.id) match {
case Some(r) if r > 0 => {+@r}
case Some(r) if r < 0 => {@r}
case _ => {+0}
}
} else {
playing">*
}</a>
}
</td>
<td class="rating">
@player.rating
</td>
<td class="rating">@player.rating</td>
</tr>
<tr><td class="around-bar" colspan="4"><div class="bar" data-value="@score.ratingPercent"></div></td></tr>
}
@ -38,4 +45,3 @@
</tbody>
</table>
</div>

View file

@ -16,8 +16,11 @@ private[pool] object AutoPairing {
val (pairablePlayers, unpairablePlayers) = availablePlayers partition (_.pairable)
val nbPlaying = pool.players count isPlaying
val pairings = if (pairablePlayers.size < ((nbPlaying + unpairablePlayers.size) / 2)) Nil
else {
val minPlayersForPairing = math.min(6, (nbPlaying + unpairablePlayers.size) / 3)
val nbPlayersForPairing = pairablePlayers.size
val canDoPairings = nbPlayersForPairing >= minPlayersForPairing && nbPlayersForPairing % 2 == 0
val pairings = canDoPairings ?? {
def basedOnRating = pairablePlayers.sortBy(-_.rating) grouped 2

View file

@ -8,6 +8,8 @@ case class Pairing(
status: Status,
user1: String,
user2: String,
user1RatingDiff: Option[Int],
user2RatingDiff: Option[Int],
turns: Int,
winner: Option[String]) {
@ -24,6 +26,12 @@ case class Pairing(
def wonBy(userId: String) = finished && winner.??(userId ==)
def lostBy(userId: String) = finished && contains(userId) && winner.??(userId !=)
def booleanResult = winner match {
case Some(uid) if user1 == uid => Some(true)
case Some(uid) if user2 == uid => Some(false)
case _ => None
}
def opponentOf(user: String): Option[String] =
if (user == user1) user2.some else if (user == user2) user1.some else none
@ -32,13 +40,23 @@ case class Pairing(
else if (userId == user2) Color.Black.some
else none
def ratingDiffOf(userId: String): Option[Int] =
if (userId == user1) user1RatingDiff
else if (userId == user2) user2RatingDiff
else none
def povRef(userId: String): Option[PovRef] =
colorOf(userId) map { PovRef(gameId, _) }
def withStatus(s: Status) = copy(status = s)
def finish(s: Status, t: Int, w: Option[String]) =
copy(status = s, turns = t, winner = w)
def finish(game: Game) =
copy(
status = game.status,
turns = game.turns,
user1RatingDiff = game.whitePlayer.ratingDiff,
user2RatingDiff = game.blackPlayer.ratingDiff,
winner = game.winnerUserId)
}
case class PairingWithGame(pairing: Pairing, game: Game)
@ -50,6 +68,8 @@ private[pool] object Pairing {
status = Status.Created,
user1 = user1,
user2 = user2,
user1RatingDiff = none,
user2RatingDiff = none,
turns = 0,
winner = none)
}

View file

@ -16,5 +16,5 @@ object Player {
case class Score(
ratingPercent: Int,
recentGames: List[Option[Boolean]])
recentPairings: List[Pairing])
}

View file

@ -18,14 +18,14 @@ case class Pool(
lazy val nbPlayers = players.size
def scoreOf(p: Player) = Player.Score(
ratingPercent = 100 * p.rating / bestRating,
recentGames = pairings.foldLeft(List[Option[Boolean]]()) {
case (res, pair) if pair.finished && pair.contains(p.user.id) && res.size <= 10 =>
pair.winner.map(p.user.id ==) :: res
ratingPercent = 100 * (p.rating - minRating) / math.max(1, (maxRating - minRating)),
recentPairings = pairings.foldLeft(List[Pairing]()) {
case (res, pairing) if pairing.contains(p.user.id) && res.size <= 10 => pairing :: res
case (res, _) => res
})
lazy val bestRating = players.map(_.rating).max
lazy val maxRating = players.map(_.rating).max
lazy val minRating = players.map(_.rating).min
def contains(userId: String): Boolean = players exists (_.user.id == userId)
def contains(u: User): Boolean = contains(u.id)
@ -78,7 +78,7 @@ case class Pool(
def finishGame(game: Game) = copy(
pairings = pairings map {
case p if p.gameId == game.id => p.finish(game.status, game.turns, game.winnerUserId)
case p if p.gameId == game.id => p finish game
case p => p
}
)

View file

@ -58,12 +58,17 @@ private[pool] final class PoolActor(
pool.players map (_.user.id) filter isOnline foreach wavers.put
pool.players filterNot (p => wavers get p.user.id) map (_.user.id) map Leave.apply foreach self.!
case FinishGame(game, Some(white), Some(black)) if game.poolId == Some(setup.id) =>
pool = pool finishGame game
UserRepo byIds List(white.id, black.id) map UpdateUsers.apply foreach self.!
case FinishGame(g, Some(white), Some(black)) if g.poolId == Some(setup.id) =>
GameRepo game g.id foreach {
_ foreach { game =>
pool = pool finishGame game
UserRepo byIds List(white.id, black.id) map UpdateUsers.apply foreach self.!
}
}
case UpdateUsers(users) =>
pool = pool updatePlayers users
notifyReload
case RemindPlayers =>
import makeTimeout.short
@ -105,6 +110,8 @@ private[pool] final class PoolActor(
status = game.status,
user1 = user1,
user2 = user2,
user1RatingDiff = game.whitePlayer.ratingDiff,
user2RatingDiff = game.blackPlayer.ratingDiff,
turns = game.turns,
winner = game.winnerUserId)
}

View file

@ -6,6 +6,7 @@ import akka.pattern.ask
import lila.hub.actorApi.map.{ Ask, AskAll }
import actorApi.GetPool
import makeTimeout.short
import scala.concurrent.duration._
final class PoolRepo(hub: ActorRef) {
@ -14,5 +15,9 @@ final class PoolRepo(hub: ActorRef) {
case _: IllegalArgumentException => none
}
def all: Fu[List[Pool]] = hub ? AskAll(GetPool) mapTo manifest[List[Pool]]
val all = lila.memo.AsyncCache.single(fetchAll, timeToLive = 10.seconds)
private def fetchAll: Fu[List[Pool]] = hub ? AskAll(GetPool) mapTo manifest[List[Pool]] map { pools =>
pools.sortBy(_.setup.id)
}
}

View file

@ -49,8 +49,9 @@ body.dark div.lichess_separator,
body.dark div.lichess_table_wrap > div.clock > div.time,
body.dark #GameText,
body.dark #tournament_side,
body.dark #pool_side,
body.dark #pool > .pool_side,
body.dark #tournament div.standing_wrap,
body.dark #pool div.standing_wrap,
body.dark #GameBoard,
body.dark div.shortcuts .title,
body.dark div.content_box,

View file

@ -4,16 +4,11 @@
margin-right: -5px;
}
#pool {
#pool > .pool_box {
width: 525px;
overflow: hidden;
}
#pool div.pool_box {
width: 525px;
}
#pool table.slist .rank {
font-weight: bold;
}
#pool_side {
#pool > .pool_side {
float: right;
width: 246px;
max-height: 510px;
@ -21,37 +16,52 @@
border: 1px solid #ccc;
padding: 3px;
}
#pool_side:hover {
#pool > .pool_side:hover {
overflow-y: auto;
}
#pool_side div.pairings {
#pool div.pairings {
text-align: center;
}
#pool_side div.pairings a {
#pool div.pairings a {
padding: 0.3em 0.2em;
display: block;
white-space: nowrap;
overflow: hidden;
}
#pool_side div.pairings span {
#pool div.pairings span {
font-weight: bold;
}
#pool_side div.pairings span.win {
#pool div.pairings span.win {
color: #00aa00;
}
#pool_side div.pairings span.loss {
#pool div.pairings span.loss {
color: #aa0000;
}
#pool_side div.pairings span.draw {
#pool div.pairings span.draw {
color: #aaaa00;
}
#pool_side div.pairings em {
#pool div.pairings em {
font-style: italic;
}
#pool div.game_list {
margin: 25px 0 0 15px;
overflow: hidden;
}
#pool div.standing_wrap {
max-height: 485px;
overflow: hidden;
border-bottom: 1px solid #ccc;
}
#pool div.standing_wrap:hover {
overflow-y: auto;
}
#pool table.standing .rank {
font-weight: bold;
font-size: 1.2em;
}
#pool table.standing .rating {
font-weight: bold;
}
#pool table.standing td.around-bar {
padding: 0;
}
@ -64,10 +74,13 @@
#pool table.standing td.recent_games {
text-align: right;
}
#pool table.standing td.recent_games span {
font-size: 1.2em;
letter-spacing: -5px;
color: #b0b060;
#pool table.standing td.recent_games a {
color: #888;
text-decoration: none;
margin-left: 1px;
}
#pool table.standing td.recent_games a:hover {
text-decoration: underline;
}
#pool table.standing td.recent_games .win {
color: #759900;