swiss WIP

swiss
Thibault Duplessis 2020-05-05 16:18:58 -06:00
parent 521fdeed82
commit 2f9242c30f
27 changed files with 468 additions and 81 deletions

View File

@ -29,6 +29,7 @@ final class Swiss(
me = ctx.me, me = ctx.me,
reqPage = page, reqPage = page,
socketVersion = version.some, socketVersion = version.some,
playerInfo = none,
isInTeam = isInTeam isInTeam = isInTeam
) )
canChat <- canHaveChat(swiss) canChat <- canHaveChat(swiss)
@ -45,11 +46,13 @@ final class Swiss(
for { for {
socketVersion <- getBool("socketVersion").??(env.swiss version swiss.id dmap some) socketVersion <- getBool("socketVersion").??(env.swiss version swiss.id dmap some)
isInTeam <- isCtxInTheTeam(swiss.teamId) isInTeam <- isCtxInTheTeam(swiss.teamId)
playerInfo <- get("playerInfo").?? { env.swiss.api.playerInfo(swiss, _) }
json <- env.swiss.json( json <- env.swiss.json(
swiss = swiss, swiss = swiss,
me = ctx.me, me = ctx.me,
reqPage = page, reqPage = page,
socketVersion = socketVersion, socketVersion = socketVersion,
playerInfo = playerInfo,
isInTeam = isInTeam isInTeam = isInTeam
) )
} yield Ok(json) } yield Ok(json)
@ -133,7 +136,17 @@ final class Swiss(
} }
} }
private def WithSwiss(id: String)(f: SwissModel => Fu[Result])(implicit ctx: Context): Fu[Result] = def player(id: String, userId: String) = Action.async {
WithSwiss(id) { swiss =>
env.swiss.api.playerInfo(swiss, userId) flatMap {
_.fold(notFoundJson()) { player =>
JsonOk(fuccess(lila.swiss.SwissJson.playerJsonExt(swiss, player)))
}
}
}
}
private def WithSwiss(id: String)(f: SwissModel => Fu[Result]): Fu[Result] =
env.swiss.api.byId(SwissId(id)) flatMap { _ ?? f } env.swiss.api.byId(SwissId(id)) flatMap { _ ?? f }
private def WithEditableSwiss(id: String, me: lila.user.User)( private def WithEditableSwiss(id: String, me: lila.user.User)(

View File

@ -152,7 +152,7 @@ final class Tournament(
} }
} }
def player(tourId: String, userId: String) = Open { _ => def player(tourId: String, userId: String) = Action.async {
env.tournament.tournamentRepo byId tourId flatMap { env.tournament.tournamentRepo byId tourId flatMap {
_ ?? { tour => _ ?? { tour =>
JsonOk { JsonOk {

View File

@ -77,6 +77,11 @@ object bits {
trans.withdraw, trans.withdraw,
trans.youArePlaying, trans.youArePlaying,
trans.joinTheGame, trans.joinTheGame,
trans.signIn trans.signIn,
trans.averageElo,
trans.gamesPlayed,
trans.winRate,
trans.performance,
trans.averageOpponent
).map(_.key) ).map(_.key)
} }

View File

@ -108,7 +108,6 @@ object bits {
trans.blackWins, trans.blackWins,
trans.draws, trans.draws,
trans.nextXTournament, trans.nextXTournament,
trans.viewMoreTournaments,
trans.averageOpponent, trans.averageOpponent,
trans.ratedTournament, trans.ratedTournament,
trans.casualTournament trans.casualTournament

View File

@ -48,6 +48,12 @@
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder> <encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
</appender> </appender>
</logger> </logger>
<logger name="swiss" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home}/logs/swiss.log</file>
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
</appender>
</logger>
<logger name="relay" level="DEBUG"> <logger name="relay" level="DEBUG">
<appender name="FILE" class="ch.qos.logback.core.FileAppender"> <appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home}/logs/relay.log</file> <file>${application.home}/logs/relay.log</file>

View File

@ -64,7 +64,7 @@
<logger name="akka" level="INFO"><appender-ref ref="STDOUT_INFO" /></logger> <logger name="akka" level="INFO"><appender-ref ref="STDOUT_INFO" /></logger>
<logger name="reactivemongo" level="INFO"><appender-ref ref="STDOUT_INFO" /></logger> <logger name="reactivemongo" level="INFO"><appender-ref ref="STDOUT_INFO" /></logger>
<logger name="puzzle" level="DEBUG"> <logger name="puzzle" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/puzzle.log</file> <file>/var/log/lichess/puzzle.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -74,7 +74,17 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="relay" level="DEBUG"> <logger name="swiss" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/swiss.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/lichess/swiss-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
</logger>
<logger name="relay" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/relay.log</file> <file>/var/log/lichess/relay.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -84,7 +94,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="lobby" level="DEBUG"> <logger name="lobby" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/lobby.log</file> <file>/var/log/lichess/lobby.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -94,7 +104,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="pool" level="DEBUG"> <logger name="pool" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/pool.log</file> <file>/var/log/lichess/pool.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -104,7 +114,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="tournament" level="DEBUG"> <logger name="tournament" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/tournament.log</file> <file>/var/log/lichess/tournament.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -114,7 +124,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="ratelimit" level="DEBUG"> <logger name="ratelimit" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/ratelimit.log</file> <file>/var/log/lichess/ratelimit.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -134,7 +144,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="cheat" level="DEBUG"> <logger name="cheat" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/cheat.log</file> <file>/var/log/lichess/cheat.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -144,7 +154,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="fishnet" level="DEBUG"> <logger name="fishnet" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/fishnet.log</file> <file>/var/log/lichess/fishnet.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -164,7 +174,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="csrf" level="DEBUG"> <logger name="csrf" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/csrf.log</file> <file>/var/log/lichess/csrf.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -174,7 +184,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="http" level="DEBUG"> <logger name="http" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/http.log</file> <file>/var/log/lichess/http.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -184,7 +194,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="auth" level="DEBUG"> <logger name="auth" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/auth.log</file> <file>/var/log/lichess/auth.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -194,7 +204,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="report" level="DEBUG"> <logger name="report" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/report.log</file> <file>/var/log/lichess/report.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -204,7 +214,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="sandbag" level="DEBUG"> <logger name="sandbag" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/sandbag.log</file> <file>/var/log/lichess/sandbag.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
@ -214,7 +224,7 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
</logger> </logger>
<logger name="security" level="DEBUG"> <logger name="security" level="INFO">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/security.log</file> <file>/var/log/lichess/security.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder> <encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>

View File

@ -253,6 +253,7 @@ GET /swiss/$id<\w{8}>/edit controllers.Swiss.edit(id: String)
POST /swiss/$id<\w{8}>/edit controllers.Swiss.update(id: String) POST /swiss/$id<\w{8}>/edit controllers.Swiss.update(id: String)
POST /swiss/$id<\w{8}>/terminate controllers.Swiss.terminate(id: String) POST /swiss/$id<\w{8}>/terminate controllers.Swiss.terminate(id: String)
GET /swiss/$id<\w{8}>/standing/:page controllers.Swiss.standing(id: String, page: Int) GET /swiss/$id<\w{8}>/standing/:page controllers.Swiss.standing(id: String, page: Int)
GET /swiss/$id<\w{8}>/player/:user controllers.Swiss.player(id: String, user: String)
# Simul # Simul
GET /simul controllers.Simul.home GET /simul controllers.Simul.home

View File

@ -317,7 +317,6 @@ val `whiteWins` = new I18nKey("whiteWins")
val `blackWins` = new I18nKey("blackWins") val `blackWins` = new I18nKey("blackWins")
val `draws` = new I18nKey("draws") val `draws` = new I18nKey("draws")
val `nextXTournament` = new I18nKey("nextXTournament") val `nextXTournament` = new I18nKey("nextXTournament")
val `viewMoreTournaments` = new I18nKey("viewMoreTournaments")
val `averageOpponent` = new I18nKey("averageOpponent") val `averageOpponent` = new I18nKey("averageOpponent")
val `membersOnly` = new I18nKey("membersOnly") val `membersOnly` = new I18nKey("membersOnly")
val `boardEditor` = new I18nKey("boardEditor") val `boardEditor` = new I18nKey("boardEditor")

View File

@ -13,6 +13,7 @@ final class Env(
appConfig: Configuration, appConfig: Configuration,
db: lila.db.Db, db: lila.db.Db,
gameRepo: lila.game.GameRepo, gameRepo: lila.game.GameRepo,
userRepo: lila.user.UserRepo,
onStart: lila.round.OnStart, onStart: lila.round.OnStart,
remoteSocketApi: lila.socket.RemoteSocket, remoteSocketApi: lila.socket.RemoteSocket,
chatApi: lila.chat.ChatApi, chatApi: lila.chat.ChatApi,

View File

@ -7,19 +7,21 @@ import reactivemongo.api.bson._
import scala.concurrent.duration._ import scala.concurrent.duration._
import lila.chat.Chat import lila.chat.Chat
import lila.common.{ Bus, GreatPlayer, WorkQueues } import lila.common.{ Bus, GreatPlayer, LightUser, WorkQueues }
import lila.db.dsl._ import lila.db.dsl._
import lila.game.Game import lila.game.Game
import lila.hub.LightTeam.TeamID import lila.hub.LightTeam.TeamID
import lila.round.actorApi.round.QuietFlag import lila.round.actorApi.round.QuietFlag
import lila.user.User import lila.user.{ User, UserRepo }
final class SwissApi( final class SwissApi(
colls: SwissColls, colls: SwissColls,
userRepo: UserRepo,
socket: SwissSocket, socket: SwissSocket,
director: SwissDirector, director: SwissDirector,
scoring: SwissScoring, scoring: SwissScoring,
chatApi: lila.chat.ChatApi chatApi: lila.chat.ChatApi,
lightUserApi: lila.user.LightUserApi
)( )(
implicit ec: scala.concurrent.ExecutionContext, implicit ec: scala.concurrent.ExecutionContext,
mat: akka.stream.Materializer, mat: akka.stream.Materializer,
@ -96,6 +98,55 @@ final class SwissApi(
def featuredInTeam(teamId: TeamID): Fu[List[Swiss]] = def featuredInTeam(teamId: TeamID): Fu[List[Swiss]] =
colls.swiss.ext.find($doc("teamId" -> teamId)).sort($sort desc "startsAt").list[Swiss](5) colls.swiss.ext.find($doc("teamId" -> teamId)).sort($sort desc "startsAt").list[Swiss](5)
def playerInfo(swiss: Swiss, userId: User.ID): Fu[Option[SwissPlayer.ViewExt]] =
userRepo named userId flatMap {
_ ?? { user =>
colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, user.id).value) flatMap {
_ ?? { player =>
SwissPairing.fields { f =>
colls.pairing.ext
.find($doc(f.swissId -> swiss.id, f.players -> player.number))
.sort($sort desc f.date)
.list[SwissPairing]()
} flatMap {
pairingViews(_, player)
} flatMap { pairings =>
SwissPlayer.fields { f =>
colls.player.countSel($doc(f.swissId -> swiss.id, f.score $gt player.score)).dmap(1.+)
} map { rank =>
SwissPlayer
.ViewExt(player, rank, user.light, pairings.view.map { p =>
p.pairing.round -> p
}.toMap)
.some
}
}
}
}
}
}
def pairingViews(pairings: Seq[SwissPairing], player: SwissPlayer): Fu[Seq[SwissPairing.View]] =
pairings.headOption ?? { first =>
SwissPlayer.fields { f =>
colls.player.ext
.find($doc(f.swissId -> first.swissId, f.number $in pairings.map(_ opponentOf player.number)))
.list[SwissPlayer]()
} flatMap { opponents =>
lightUserApi asyncMany opponents.map(_.userId) map { users =>
opponents.zip(users) map {
case (o, u) => SwissPlayer.WithUser(o, u | LightUser.fallback(o.userId))
}
} map { opponents =>
pairings flatMap { pairing =>
opponents.find(_.player.number == pairing.opponentOf(player.number)) map {
SwissPairing.View(pairing, _)
}
}
}
}
}
private[swiss] def finishGame(game: Game): Funit = game.swissId ?? { swissId => private[swiss] def finishGame(game: Game): Funit = game.swissId ?? { swissId =>
Sequencing(Swiss.Id(swissId))(startedById) { swiss => Sequencing(Swiss.Id(swissId))(startedById) { swiss =>
colls.pairing.byId[SwissPairing](game.id).dmap(_.filter(_.isOngoing)) flatMap { colls.pairing.byId[SwissPairing](game.id).dmap(_.filter(_.isOngoing)) flatMap {
@ -186,12 +237,15 @@ final class SwissApi(
swiss.id, swiss.id,
s"Not enough players for round ${swiss.round.value + 1}; terminating tournament." s"Not enough players for round ${swiss.round.value + 1}; terminating tournament."
) )
) { s => ) {
scoring.recompute(s) >>- case s if s.nextRoundAt.isEmpty =>
systemChat( scoring.recompute(s) >>-
swiss.id, systemChat(swiss.id, s"Round ${swiss.round.value + 1} started.")
s"Round ${swiss.round.value + 1} started." case s =>
) colls.swiss.update
.one($id(swiss.id), $set("nextRoundAt" -> DateTime.now.plusSeconds(61)))
.void >>-
systemChat(swiss.id, s"Round ${swiss.round.value + 1} failed.", true)
} }
} else { } else {
if (swiss.startsAt isBefore DateTime.now.minusMinutes(60)) destroy(swiss) if (swiss.startsAt isBefore DateTime.now.minusMinutes(60)) destroy(swiss)

View File

@ -29,7 +29,8 @@ final class SwissJson(
me: Option[User], me: Option[User],
reqPage: Option[Int], // None = focus on me reqPage: Option[Int], // None = focus on me
socketVersion: Option[SocketVersion], socketVersion: Option[SocketVersion],
isInTeam: Boolean isInTeam: Boolean,
playerInfo: Option[SwissPlayer.ViewExt]
)(implicit lang: Lang): Fu[JsObject] = )(implicit lang: Lang): Fu[JsObject] =
for { for {
myInfo <- me.?? { fetchMyInfo(swiss, _) } myInfo <- me.?? { fetchMyInfo(swiss, _) }
@ -69,19 +70,23 @@ final class SwissJson(
.add("greatPlayer" -> GreatPlayer.wikiUrl(swiss.name).map { url => .add("greatPlayer" -> GreatPlayer.wikiUrl(swiss.name).map { url =>
Json.obj("name" -> swiss.name, "url" -> url) Json.obj("name" -> swiss.name, "url" -> url)
}) })
.add("playerInfo" -> playerInfo.map { playerJsonExt(swiss, _) })
def fetchMyInfo(swiss: Swiss, me: User): Fu[Option[MyInfo]] = def fetchMyInfo(swiss: Swiss, me: User): Fu[Option[MyInfo]] =
colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, me.id).value) flatMap { colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, me.id).value) flatMap {
_ ?? { player => _ ?? { player =>
SwissPairing.fields { f => SwissPairing.fields { f =>
colls.pairing (swiss.nbOngoing > 0)
.find( .?? {
$doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing), colls.pairing
$doc(f.id -> true).some .find(
) $doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
.sort($sort desc f.date) $doc(f.id -> true).some
.one[Bdoc] )
.dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) } .sort($sort desc f.date)
.one[Bdoc]
.dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
}
.flatMap { gameId => .flatMap { gameId =>
getOrGuessRank(swiss, player) dmap { rank => getOrGuessRank(swiss, player) dmap { rank =>
MyInfo(rank + 1, gameId, me).some MyInfo(rank + 1, gameId, me).some
@ -117,32 +122,46 @@ final class SwissJson(
object SwissJson { object SwissJson {
private[swiss] def playerJson( private[swiss] def playerJson(swiss: Swiss, view: SwissPlayer.View): JsObject =
swiss: Swiss, playerJsonBase(swiss, view) ++ Json.obj(
rankedPlayer: SwissPlayer.Ranked, "pairings" -> swiss.allRounds.map(view.pairings.get).map(_ map pairingJson(view.player))
user: lila.common.LightUser, )
pairings: Map[SwissRound.Number, SwissPairing]
): JsObject = { def playerJsonExt(swiss: Swiss, view: SwissPlayer.ViewExt): JsObject =
val p = rankedPlayer.player playerJsonBase(swiss, view) ++ Json.obj(
"pairings" -> swiss.allRounds.map(view.pairings.get).map {
_ map { p =>
pairingJson(view.player)(p.pairing) ++ Json.obj(
"user" -> p.player.user,
"rating" -> p.player.player.rating
)
}
}
)
private def playerJsonBase(swiss: Swiss, view: SwissPlayer.Viewish): JsObject = {
val p = view.player
Json Json
.obj( .obj(
"rank" -> rankedPlayer.rank, "rank" -> view.rank,
"user" -> user, "user" -> view.user,
"rating" -> p.rating, "rating" -> p.rating,
"points" -> p.points, "points" -> p.points,
"tieBreak" -> p.tieBreak, "tieBreak" -> p.tieBreak
"pairings" -> swiss.allRounds.map(pairings.get).map {
_ map { pairing =>
Json
.obj("g" -> pairing.gameId)
.add("o" -> pairing.isOngoing)
.add("w" -> pairing.resultFor(p.number))
}
}
) )
.add("performance" -> p.performance)
.add("provisional" -> p.provisional) .add("provisional" -> p.provisional)
} }
private def pairingJson(player: SwissPlayer)(pairing: SwissPairing) =
Json
.obj(
"g" -> pairing.gameId,
"c" -> (pairing.white == player.number)
)
.add("o" -> pairing.isOngoing)
.add("w" -> pairing.resultFor(player.number))
implicit private val roundNumberWriter: Writes[SwissRound.Number] = Writes[SwissRound.Number] { n => implicit private val roundNumberWriter: Writes[SwissRound.Number] = Writes[SwissRound.Number] { n =>
JsNumber(n.value) JsNumber(n.value)
} }
@ -155,6 +174,9 @@ object SwissJson {
implicit private val tieBreakWriter: Writes[Swiss.TieBreak] = Writes[Swiss.TieBreak] { t => implicit private val tieBreakWriter: Writes[Swiss.TieBreak] = Writes[Swiss.TieBreak] { t =>
JsNumber(t.value) JsNumber(t.value)
} }
implicit private val performanceWriter: Writes[Swiss.Performance] = Writes[Swiss.Performance] { t =>
JsNumber(t.value.toInt)
}
implicit private val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock => implicit private val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj( Json.obj(

View File

@ -38,6 +38,8 @@ object SwissPairing {
type PairingMap = Map[SwissPlayer.Number, Map[SwissRound.Number, SwissPairing]] type PairingMap = Map[SwissPlayer.Number, Map[SwissRound.Number, SwissPairing]]
case class View(pairing: SwissPairing, player: SwissPlayer.WithUser)
object Fields { object Fields {
val id = "_id" val id = "_id"
val swissId = "s" val swissId = "s"

View File

@ -1,5 +1,6 @@
package lila.swiss package lila.swiss
import lila.common.LightUser
import lila.rating.Perf import lila.rating.Perf
import lila.user.{ Perfs, User } import lila.user.{ Perfs, User }
@ -56,6 +57,28 @@ object SwissPlayer {
override def toString = s"$rank. ${player.userId}[${player.rating}]" override def toString = s"$rank. ${player.userId}[${player.rating}]"
} }
case class WithUser(player: SwissPlayer, user: LightUser)
sealed trait Viewish {
val player: SwissPlayer
val rank: Int
val user: lila.common.LightUser
}
case class View(
player: SwissPlayer,
rank: Int,
user: lila.common.LightUser,
pairings: Map[SwissRound.Number, SwissPairing]
) extends Viewish
case class ViewExt(
player: SwissPlayer,
rank: Int,
user: lila.common.LightUser,
pairings: Map[SwissRound.Number, SwissPairing.View]
) extends Viewish
def toMap(players: List[SwissPlayer]): Map[SwissPlayer.Number, SwissPlayer] = def toMap(players: List[SwissPlayer]): Map[SwissPlayer.Number, SwissPlayer] =
players.view.map(p => p.number -> p).toMap players.view.map(p => p.number -> p).toMap

View File

@ -83,12 +83,15 @@ final class SwissStandingApi(
} yield Json.obj( } yield Json.obj(
"page" -> page, "page" -> page,
"players" -> rankedPlayers.zip(users).map { "players" -> rankedPlayers.zip(users).map {
case (p, u) => case (SwissPlayer.Ranked(rank, player), user) =>
SwissJson.playerJson( SwissJson.playerJson(
swiss, swiss,
p, SwissPlayer.View(
u | LightUser.fallback(p.player.userId), player,
~pairings.get(p.player.number) rank,
user | LightUser.fallback(player.userId),
~pairings.get(player.number)
)
) )
} }
) )

View File

@ -426,7 +426,6 @@ computer analysis, game chat and shareable URL.</string>
<string name="blackWins">Black wins</string> <string name="blackWins">Black wins</string>
<string name="draws">Draws</string> <string name="draws">Draws</string>
<string name="nextXTournament">Next %s tournament:</string> <string name="nextXTournament">Next %s tournament:</string>
<string name="viewMoreTournaments">View more tournaments</string>
<string name="averageOpponent">Average opponent</string> <string name="averageOpponent">Average opponent</string>
<string name="membersOnly">Members only</string> <string name="membersOnly">Members only</string>
<string name="boardEditor">Board editor</string> <string name="boardEditor">Board editor</string>

View File

@ -59,7 +59,7 @@
&.current a { &.current a {
background: mix($c-accent, $c-bg-box, 70%); background: mix($c-accent, $c-bg-box, 70%);
color: #fff; color: #fff;
opacity: 1; opacity: 1!important;
} }
&.new { &.new {
border: $c-border; border: $c-border;

View File

@ -11,29 +11,32 @@ $chat-optimal-size: calc(100vh - #{$site-header-outer-height} - #{$block-gap} -
display: grid; display: grid;
&__side { grid-area: side } &__side { grid-area: side }
&__table { grid-area: table }
&__main { grid-area: main } &__main { grid-area: main }
.chat__members { grid-area: uchat; } .chat__members { grid-area: uchat; }
grid-template-areas: grid-template-areas:
'main' 'main'
'side' 'side'
'uchat'; 'uchat'
'table';
grid-gap: $block-gap; grid-gap: $block-gap;
@include breakpoint($mq-col2) { @include breakpoint($mq-col2) {
grid-template-columns: $col2-uniboard-default-width; grid-template-columns: $col2-uniboard-default-width $col2-uniboard-table;
grid-template-rows: $chat-optimal-size min-content; grid-template-rows: $chat-optimal-size min-content;
grid-template-areas: grid-template-areas:
'main side' 'main side'
'main uchat'; 'main uchat'
'table table';
.mchat__messages { .mchat__messages {
max-height: inherit; max-height: inherit;
} }
} }
@include breakpoint($mq-col3) { @include breakpoint($mq-col3) {
grid-template-columns: $col3-uniboard-side $col3-uniboard-default-width; grid-template-columns: $col3-uniboard-side $col3-uniboard-default-width $col3-uniboard-table;
grid-template-rows: $chat-optimal-size auto; grid-template-rows: $chat-optimal-size auto;
grid-template-areas: grid-template-areas:
'side main table' 'side main table'

View File

@ -0,0 +1,123 @@
.swiss__player-info {
@extend %box-neat-force;
background: $c-bg-box;
position: relative;
align-self: flex-start;
.spinner {
margin: 5em auto;
}
.close {
position: absolute;
top: 4px;
right: 5px;
opacity: .6;
@include transition();
color: $c-red;
&:hover {
opacity: 1;
}
}
.stats {
@extend %flex-column;
justify-content: center;
h2 {
@extend %metal;
font-size: 1.4em;
padding: .6rem 1rem;
border-bottom: $border;
}
table {
margin: 1em auto;
}
td {
font-weight: bold;
padding-left: 10px;
text-align: right;
line-height: 1.8em;
&:last-child {
@extend %roboto;
}
}
}
.sublist {
width: 100%;
tr {
cursor: pointer;
@include transition(background-color);
&:nth-child(odd) {
background: $c-bg-zebra;
}
&:hover {
background: mix($c-link, $c-bg-box, 10%);
}
}
th, td {
padding: .3em;
}
th {
@extend %roboto;
padding-left: 7px;
}
.title {
color: $c-brag;
font-weight: bold;
}
}
.pairings {
width: 100%;
tr {
cursor: pointer;
@include transition(background-color);
&:nth-child(odd) {
background: $c-bg-zebra;
}
&:hover {
background: mix($c-link, $c-bg-box, 10%);
}
}
th, td {
padding: .3em;
}
th {
border-left: 3px solid transparent;
@include transition();
}
tr:hover th {
border-color: $c-font-dimmer;
}
tr.win:hover th {
border-color: $c-good;
}
tr.loss:hover th {
border-color: $c-bad;
}
td:nth-child(2) {
@extend %nowrap-ellipsis;
max-width: 200px;
}
td:last-child {
font-weight: bold;
opacity: .8;
}
tr.win td:last-child {
color: $c-good;
opacity: 1;
}
tr.loss td:last-child {
color: $c-bad;
opacity: 1;
}
.bye {
font-style: italic;
color: $c-font-dim;
}
}
.color-icon {
opacity: .6;
}
}

View File

@ -13,7 +13,7 @@ $mq-col3: $mq-col3-uniboard;
// @import 'stats'; // @import 'stats';
// @import 'duel'; // @import 'duel';
// @import 'actor-info'; // @import 'actor-info';
// @import 'player-info'; @import 'player-info';
// @import 'team-info'; // @import 'team-info';
.swiss { .swiss {

View File

@ -3,5 +3,6 @@
@import '../../../common/css/component/bar-glider'; @import '../../../common/css/component/bar-glider';
@import '../../../common/css/component/slist'; @import '../../../common/css/component/slist';
@import '../../../common/css/component/quote'; @import '../../../common/css/component/quote';
@import '../../../common/css/component/color-icon';
@import '../../../chat/css/chat'; @import '../../../chat/css/chat';
@import '../show'; @import '../show';

View File

@ -15,6 +15,7 @@ export default class SwissCtrl {
lastPageDisplayed: number | undefined; lastPageDisplayed: number | undefined;
focusOnMe: boolean; focusOnMe: boolean;
joinSpinner: boolean = false; joinSpinner: boolean = false;
playerInfoId?: string;
disableClicks: boolean = true; disableClicks: boolean = true;
searching: boolean = false; searching: boolean = false;
redraw: () => void; redraw: () => void;
@ -41,8 +42,6 @@ export default class SwissCtrl {
this.data = {...this.data, ...data}; this.data = {...this.data, ...data};
this.data.me = data.me; // to account for removal on withdraw this.data.me = data.me; // to account for removal on withdraw
this.data.nextRound = data.nextRound; // to account for removal this.data.nextRound = data.nextRound; // to account for removal
// if (data.playerInfo && data.playerInfo.player.id === this.playerInfo.id)
// this.playerInfo.data = data.playerInfo;
this.loadPage(data.standing); this.loadPage(data.standing);
if (this.focusOnMe) this.scrollToMe(); if (this.focusOnMe) this.scrollToMe();
// if (data.featured) this.startWatching(data.featured.id); // if (data.featured) this.startWatching(data.featured.id);
@ -121,6 +120,8 @@ export default class SwissCtrl {
userLastPage = () => this.userSetPage(players(this).nbPages); userLastPage = () => this.userSetPage(players(this).nbPages);
showPlayerInfo = (player: Player) => { showPlayerInfo = (player: Player) => {
this.playerInfoId = this.playerInfoId === player.user.id ? undefined : player.user.id;
if (this.playerInfoId) xhr.playerInfo(this, this.playerInfoId);
}; };
askReload = () => xhr.reloadNow(this); askReload = () => xhr.reloadNow(this);

View File

@ -31,6 +31,7 @@ export interface SwissData {
nbOngoing: number; nbOngoing: number;
status: Status; status: Status;
standing: Standing; standing: Standing;
playerInfo?: PlayerExt;
isStarted?: boolean; isStarted?: boolean;
isFinished?: boolean; isFinished?: boolean;
socketVersion?: number; socketVersion?: number;
@ -60,9 +61,14 @@ export interface MyInfo {
export interface Pairing { export interface Pairing {
g: string; // game g: string; // game
c: boolean; // color
w?: boolean; // won w?: boolean; // won
o?: boolean; // ongoing o?: boolean; // ongoing
} }
export interface PairingExt extends Pairing {
user: LightUser;
rating: number;
}
export interface Standing { export interface Standing {
page: number; page: number;
@ -77,6 +83,7 @@ export interface Player {
withdraw?: boolean; withdraw?: boolean;
points: number; points: number;
tieBreak: number; tieBreak: number;
performance: number;
rank: number; rank: number;
pairings: [Pairing | null]; pairings: [Pairing | null];
} }
@ -96,3 +103,7 @@ export type Page = Player[];
export interface Pages { export interface Pages {
[n: number]: Page [n: number]: Page
} }
export interface PlayerExt extends Player {
pairings: [PairingExt | null];
}

View File

@ -6,6 +6,7 @@ import * as pagination from '../pagination';
import { MaybeVNodes } from '../interfaces'; import { MaybeVNodes } from '../interfaces';
import header from './header'; import header from './header';
import standing from './standing'; import standing from './standing';
import playerInfo from './playerInfo';
export default function(ctrl: SwissCtrl) { export default function(ctrl: SwissCtrl) {
const d = ctrl.data; const d = ctrl.data;
@ -22,6 +23,7 @@ export default function(ctrl: SwissCtrl) {
$(el).replaceWith($('.swiss__underchat.none').removeClass('none')); $(el).replaceWith($('.swiss__underchat.none').removeClass('none'));
}) })
}), }),
playerInfo(ctrl),
h('div.swiss__main', h('div.swiss__main',
h('div.box.swiss__main-' + d.status, content) h('div.box.swiss__main-' + d.status, content)
), ),

View File

@ -0,0 +1,100 @@
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode';
import { spinner, bind, userName, dataIcon, player as renderPlayer, numberRow } from './util';
import { Player, PlayerExt, Pairing } from '../interfaces';
import SwissCtrl from '../ctrl';
export default function(ctrl: SwissCtrl): VNode {
if (!ctrl.playerInfoId) return;
const data = ctrl.data.playerInfo;
const noarg = ctrl.trans.noarg;
const tag = 'div.swiss__player-info.swiss__table';
if (data?.user.id !== ctrl.playerInfoId) return h(tag, [
h('div.stats', [
h('h2', ctrl.playerInfoId),
spinner()
])
]);
const games = data.pairings.filter(p => p).length;
const wins = data.pairings.filter(p => p?.w).length;
const avgOp: number | undefined = games ?
Math.round(data.pairings.reduce((r, p) => r + (p ? p.rating : 0), 0) / games) :
undefined;
return h(tag, {
hook: {
insert: setup,
postpatch(_, vnode) { setup(vnode) }
}
}, [
h('a.close', {
attrs: dataIcon('L'),
hook: bind('click', () => ctrl.showPlayerInfo(data), ctrl.redraw)
}),
h('div.stats', [
h('h2', [
h('span.rank', data.rank + '. '),
renderPlayer(data, true, false)
]),
h('table', [
numberRow('Points', data.points, 'raw'),
numberRow('Tie break', data.tieBreak, 'raw'),
...(games ? [
data.performance ? numberRow(
noarg('performance'),
data.performance + (games < 3 ? '?' : ''),
'raw') : null,
numberRow(noarg('winRate'), [wins, games], 'percent'),
numberRow(noarg('averageOpponent'), avgOp, 'raw')
] : [])
])
]),
h('div', [
h('table.pairings.sublist', {
hook: bind('click', e => {
const href = ((e.target as HTMLElement).parentNode as HTMLElement).getAttribute('data-href');
if (href) window.open(href, '_blank');
})
}, data.pairings.map((p, i) => {
const round = i + 1;
if (!p) return h('tr', [
h('th', '' + round),
h('td.bye', {
attrs: { colspan: 3},
}, 'Bye'),
h('td', '½')
]);
const res = result(p);
return h('tr.glpt.' + (res === '1' ? ' win' : (res === '0' ? ' loss' : '')), {
key: p.g,
attrs: { 'data-href': '/' + p.g + (p.c ? '' : '/black') },
hook: {
destroy: vnode => $.powerTip.destroy(vnode.elm as HTMLElement)
}
}, [
h('th', '' + round),
h('td', userName(p.user)),
h('td', '' + p.rating),
h('td.is.color-icon.' + (p.c ? 'white' : 'black')),
h('td', res)
]);
}))
])
]);
};
function result(p: Pairing): string {
switch (p.w) {
case true:
return '1';
case false:
return '0';
default:
return p.o ? '*' : '½';
}
}
function setup(vnode: VNode) {
const el = vnode.elm as HTMLElement, p = window.lichess.powertip;
p.manualUserIn(el);
p.manualGameIn(el);
}

View File

@ -12,7 +12,8 @@ function playerTr(ctrl: SwissCtrl, player: Player) {
return h('tr', { return h('tr', {
key: userId, key: userId,
class: { class: {
me: ctrl.data.me?.id == userId me: ctrl.data.me?.id == userId,
active: ctrl.playerInfoId === userId
}, },
hook: bind('click', _ => ctrl.showPlayerInfo(player), ctrl.redraw) hook: bind('click', _ => ctrl.showPlayerInfo(player), ctrl.redraw)
}, [ }, [
@ -89,7 +90,11 @@ export default function standing(ctrl: SwissCtrl, pag, klass?: string): VNode {
pag.currentPageResults.map(res => playerTr(ctrl, res)) : lastBody; pag.currentPageResults.map(res => playerTr(ctrl, res)) : lastBody;
if (pag.currentPageResults) lastBody = tableBody; if (pag.currentPageResults) lastBody = tableBody;
return h('table.slist.swiss__standing' + (klass ? '.' + klass : ''), { return h('table.slist.swiss__standing' + (klass ? '.' + klass : ''), {
class: { loading: !pag.currentPageResults }, class: {
loading: !pag.currentPageResults,
long: ctrl.data.round > 35,
xlong: ctrl.data.round > 80,
},
}, [ }, [
h('tbody', { h('tbody', {
hook: { hook: {

View File

@ -65,6 +65,14 @@ export function player(p: Player, asLink: boolean, withRating: boolean) {
]); ]);
} }
export function numberRow(name: string, value: any, typ?: string) {
return h('tr', [h('th', name), h('td',
typ === 'raw' ? value : (typ === 'percent' ? (
value[1] > 0 ? ratio2percent(value[0] / value[1]) : 0
) : window.lichess.numberFormat(value))
)]);
}
export function spinner(): VNode { export function spinner(): VNode {
return h('div.spinner', [ return h('div.spinner', [
h('svg', { attrs: { viewBox: '0 0 40 40' } }, [ h('svg', { attrs: { viewBox: '0 0 40 40' } }, [

View File

@ -22,20 +22,16 @@ const loadPageOf = (ctrl: SwissCtrl, userId: string): Promise<any> =>
json(`/swiss/${ctrl.data.id}/page-of/${userId}`); json(`/swiss/${ctrl.data.id}/page-of/${userId}`);
const reload = (ctrl: SwissCtrl) => const reload = (ctrl: SwissCtrl) =>
json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}`).then(data => { json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}&playerInfo=${ctrl.playerInfoId}`).then(data => {
ctrl.reload(data); ctrl.reload(data);
ctrl.redraw(); ctrl.redraw();
}).catch(onFail); }).catch(onFail);
// function playerInfo(ctrl: SwissCtrl, userId: string) { const playerInfo = (ctrl: SwissCtrl, userId: string) =>
// return $.ajax({ json(`/swiss/${ctrl.data.id}/player/${userId}`).then(data => {
// url: ['/swiss', ctrl.data.id, 'player', userId].join('/'), ctrl.data.playerInfo = data;
// headers ctrl.redraw();
// }).then(data => { }).catch(onFail);
// ctrl.setPlayerInfoData(data);
// ctrl.redraw();
// }, onFail);
// }
export default { export default {
join: throttle(1000, join), join: throttle(1000, join),
@ -43,5 +39,5 @@ export default {
loadPageOf, loadPageOf,
reloadSoon: throttle(4000, reload), reloadSoon: throttle(4000, reload),
reloadNow: reload, reloadNow: reload,
// playerInfo playerInfo
}; };