swiss WIP
parent
521fdeed82
commit
2f9242c30f
|
@ -29,6 +29,7 @@ final class Swiss(
|
|||
me = ctx.me,
|
||||
reqPage = page,
|
||||
socketVersion = version.some,
|
||||
playerInfo = none,
|
||||
isInTeam = isInTeam
|
||||
)
|
||||
canChat <- canHaveChat(swiss)
|
||||
|
@ -45,11 +46,13 @@ final class Swiss(
|
|||
for {
|
||||
socketVersion <- getBool("socketVersion").??(env.swiss version swiss.id dmap some)
|
||||
isInTeam <- isCtxInTheTeam(swiss.teamId)
|
||||
playerInfo <- get("playerInfo").?? { env.swiss.api.playerInfo(swiss, _) }
|
||||
json <- env.swiss.json(
|
||||
swiss = swiss,
|
||||
me = ctx.me,
|
||||
reqPage = page,
|
||||
socketVersion = socketVersion,
|
||||
playerInfo = playerInfo,
|
||||
isInTeam = isInTeam
|
||||
)
|
||||
} 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 }
|
||||
|
||||
private def WithEditableSwiss(id: String, me: lila.user.User)(
|
||||
|
|
|
@ -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 {
|
||||
_ ?? { tour =>
|
||||
JsonOk {
|
||||
|
|
|
@ -77,6 +77,11 @@ object bits {
|
|||
trans.withdraw,
|
||||
trans.youArePlaying,
|
||||
trans.joinTheGame,
|
||||
trans.signIn
|
||||
trans.signIn,
|
||||
trans.averageElo,
|
||||
trans.gamesPlayed,
|
||||
trans.winRate,
|
||||
trans.performance,
|
||||
trans.averageOpponent
|
||||
).map(_.key)
|
||||
}
|
||||
|
|
|
@ -108,7 +108,6 @@ object bits {
|
|||
trans.blackWins,
|
||||
trans.draws,
|
||||
trans.nextXTournament,
|
||||
trans.viewMoreTournaments,
|
||||
trans.averageOpponent,
|
||||
trans.ratedTournament,
|
||||
trans.casualTournament
|
||||
|
|
|
@ -48,6 +48,12 @@
|
|||
<encoder><pattern>%date [%level] %message%n%xException</pattern></encoder>
|
||||
</appender>
|
||||
</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">
|
||||
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
|
||||
<file>${application.home}/logs/relay.log</file>
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<logger name="akka" 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">
|
||||
<file>/var/log/lichess/puzzle.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -74,7 +74,17 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</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">
|
||||
<file>/var/log/lichess/relay.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -84,7 +94,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="lobby" level="DEBUG">
|
||||
<logger name="lobby" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/lobby.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -94,7 +104,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="pool" level="DEBUG">
|
||||
<logger name="pool" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/pool.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -104,7 +114,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="tournament" level="DEBUG">
|
||||
<logger name="tournament" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/tournament.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -114,7 +124,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="ratelimit" level="DEBUG">
|
||||
<logger name="ratelimit" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/ratelimit.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -134,7 +144,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="cheat" level="DEBUG">
|
||||
<logger name="cheat" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/cheat.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -144,7 +154,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="fishnet" level="DEBUG">
|
||||
<logger name="fishnet" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/fishnet.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -164,7 +174,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="csrf" level="DEBUG">
|
||||
<logger name="csrf" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/csrf.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -174,7 +184,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="http" level="DEBUG">
|
||||
<logger name="http" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/http.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -184,7 +194,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="auth" level="DEBUG">
|
||||
<logger name="auth" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/auth.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -194,7 +204,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="report" level="DEBUG">
|
||||
<logger name="report" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/report.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -204,7 +214,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="sandbag" level="DEBUG">
|
||||
<logger name="sandbag" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/sandbag.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
@ -214,7 +224,7 @@
|
|||
</rollingPolicy>
|
||||
</appender>
|
||||
</logger>
|
||||
<logger name="security" level="DEBUG">
|
||||
<logger name="security" level="INFO">
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>/var/log/lichess/security.log</file>
|
||||
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
|
||||
|
|
|
@ -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}>/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}>/player/:user controllers.Swiss.player(id: String, user: String)
|
||||
|
||||
# Simul
|
||||
GET /simul controllers.Simul.home
|
||||
|
|
|
@ -317,7 +317,6 @@ val `whiteWins` = new I18nKey("whiteWins")
|
|||
val `blackWins` = new I18nKey("blackWins")
|
||||
val `draws` = new I18nKey("draws")
|
||||
val `nextXTournament` = new I18nKey("nextXTournament")
|
||||
val `viewMoreTournaments` = new I18nKey("viewMoreTournaments")
|
||||
val `averageOpponent` = new I18nKey("averageOpponent")
|
||||
val `membersOnly` = new I18nKey("membersOnly")
|
||||
val `boardEditor` = new I18nKey("boardEditor")
|
||||
|
|
|
@ -13,6 +13,7 @@ final class Env(
|
|||
appConfig: Configuration,
|
||||
db: lila.db.Db,
|
||||
gameRepo: lila.game.GameRepo,
|
||||
userRepo: lila.user.UserRepo,
|
||||
onStart: lila.round.OnStart,
|
||||
remoteSocketApi: lila.socket.RemoteSocket,
|
||||
chatApi: lila.chat.ChatApi,
|
||||
|
|
|
@ -7,19 +7,21 @@ import reactivemongo.api.bson._
|
|||
import scala.concurrent.duration._
|
||||
|
||||
import lila.chat.Chat
|
||||
import lila.common.{ Bus, GreatPlayer, WorkQueues }
|
||||
import lila.common.{ Bus, GreatPlayer, LightUser, WorkQueues }
|
||||
import lila.db.dsl._
|
||||
import lila.game.Game
|
||||
import lila.hub.LightTeam.TeamID
|
||||
import lila.round.actorApi.round.QuietFlag
|
||||
import lila.user.User
|
||||
import lila.user.{ User, UserRepo }
|
||||
|
||||
final class SwissApi(
|
||||
colls: SwissColls,
|
||||
userRepo: UserRepo,
|
||||
socket: SwissSocket,
|
||||
director: SwissDirector,
|
||||
scoring: SwissScoring,
|
||||
chatApi: lila.chat.ChatApi
|
||||
chatApi: lila.chat.ChatApi,
|
||||
lightUserApi: lila.user.LightUserApi
|
||||
)(
|
||||
implicit ec: scala.concurrent.ExecutionContext,
|
||||
mat: akka.stream.Materializer,
|
||||
|
@ -96,6 +98,55 @@ final class SwissApi(
|
|||
def featuredInTeam(teamId: TeamID): Fu[List[Swiss]] =
|
||||
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 =>
|
||||
Sequencing(Swiss.Id(swissId))(startedById) { swiss =>
|
||||
colls.pairing.byId[SwissPairing](game.id).dmap(_.filter(_.isOngoing)) flatMap {
|
||||
|
@ -186,12 +237,15 @@ final class SwissApi(
|
|||
swiss.id,
|
||||
s"Not enough players for round ${swiss.round.value + 1}; terminating tournament."
|
||||
)
|
||||
) { s =>
|
||||
) {
|
||||
case s if s.nextRoundAt.isEmpty =>
|
||||
scoring.recompute(s) >>-
|
||||
systemChat(
|
||||
swiss.id,
|
||||
s"Round ${swiss.round.value + 1} started."
|
||||
)
|
||||
systemChat(swiss.id, 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 {
|
||||
if (swiss.startsAt isBefore DateTime.now.minusMinutes(60)) destroy(swiss)
|
||||
|
|
|
@ -29,7 +29,8 @@ final class SwissJson(
|
|||
me: Option[User],
|
||||
reqPage: Option[Int], // None = focus on me
|
||||
socketVersion: Option[SocketVersion],
|
||||
isInTeam: Boolean
|
||||
isInTeam: Boolean,
|
||||
playerInfo: Option[SwissPlayer.ViewExt]
|
||||
)(implicit lang: Lang): Fu[JsObject] =
|
||||
for {
|
||||
myInfo <- me.?? { fetchMyInfo(swiss, _) }
|
||||
|
@ -69,11 +70,14 @@ final class SwissJson(
|
|||
.add("greatPlayer" -> GreatPlayer.wikiUrl(swiss.name).map { url =>
|
||||
Json.obj("name" -> swiss.name, "url" -> url)
|
||||
})
|
||||
.add("playerInfo" -> playerInfo.map { playerJsonExt(swiss, _) })
|
||||
|
||||
def fetchMyInfo(swiss: Swiss, me: User): Fu[Option[MyInfo]] =
|
||||
colls.player.byId[SwissPlayer](SwissPlayer.makeId(swiss.id, me.id).value) flatMap {
|
||||
_ ?? { player =>
|
||||
SwissPairing.fields { f =>
|
||||
(swiss.nbOngoing > 0)
|
||||
.?? {
|
||||
colls.pairing
|
||||
.find(
|
||||
$doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
|
||||
|
@ -82,6 +86,7 @@ final class SwissJson(
|
|||
.sort($sort desc f.date)
|
||||
.one[Bdoc]
|
||||
.dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
|
||||
}
|
||||
.flatMap { gameId =>
|
||||
getOrGuessRank(swiss, player) dmap { rank =>
|
||||
MyInfo(rank + 1, gameId, me).some
|
||||
|
@ -117,32 +122,46 @@ final class SwissJson(
|
|||
|
||||
object SwissJson {
|
||||
|
||||
private[swiss] def playerJson(
|
||||
swiss: Swiss,
|
||||
rankedPlayer: SwissPlayer.Ranked,
|
||||
user: lila.common.LightUser,
|
||||
pairings: Map[SwissRound.Number, SwissPairing]
|
||||
): JsObject = {
|
||||
val p = rankedPlayer.player
|
||||
Json
|
||||
.obj(
|
||||
"rank" -> rankedPlayer.rank,
|
||||
"user" -> user,
|
||||
"rating" -> p.rating,
|
||||
"points" -> p.points,
|
||||
"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))
|
||||
private[swiss] def playerJson(swiss: Swiss, view: SwissPlayer.View): JsObject =
|
||||
playerJsonBase(swiss, view) ++ Json.obj(
|
||||
"pairings" -> swiss.allRounds.map(view.pairings.get).map(_ map pairingJson(view.player))
|
||||
)
|
||||
|
||||
def playerJsonExt(swiss: Swiss, view: SwissPlayer.ViewExt): JsObject =
|
||||
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
|
||||
.obj(
|
||||
"rank" -> view.rank,
|
||||
"user" -> view.user,
|
||||
"rating" -> p.rating,
|
||||
"points" -> p.points,
|
||||
"tieBreak" -> p.tieBreak
|
||||
)
|
||||
.add("performance" -> p.performance)
|
||||
.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 =>
|
||||
JsNumber(n.value)
|
||||
}
|
||||
|
@ -155,6 +174,9 @@ object SwissJson {
|
|||
implicit private val tieBreakWriter: Writes[Swiss.TieBreak] = Writes[Swiss.TieBreak] { t =>
|
||||
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 =>
|
||||
Json.obj(
|
||||
|
|
|
@ -38,6 +38,8 @@ object SwissPairing {
|
|||
|
||||
type PairingMap = Map[SwissPlayer.Number, Map[SwissRound.Number, SwissPairing]]
|
||||
|
||||
case class View(pairing: SwissPairing, player: SwissPlayer.WithUser)
|
||||
|
||||
object Fields {
|
||||
val id = "_id"
|
||||
val swissId = "s"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package lila.swiss
|
||||
|
||||
import lila.common.LightUser
|
||||
import lila.rating.Perf
|
||||
import lila.user.{ Perfs, User }
|
||||
|
||||
|
@ -56,6 +57,28 @@ object SwissPlayer {
|
|||
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] =
|
||||
players.view.map(p => p.number -> p).toMap
|
||||
|
||||
|
|
|
@ -83,12 +83,15 @@ final class SwissStandingApi(
|
|||
} yield Json.obj(
|
||||
"page" -> page,
|
||||
"players" -> rankedPlayers.zip(users).map {
|
||||
case (p, u) =>
|
||||
case (SwissPlayer.Ranked(rank, player), user) =>
|
||||
SwissJson.playerJson(
|
||||
swiss,
|
||||
p,
|
||||
u | LightUser.fallback(p.player.userId),
|
||||
~pairings.get(p.player.number)
|
||||
SwissPlayer.View(
|
||||
player,
|
||||
rank,
|
||||
user | LightUser.fallback(player.userId),
|
||||
~pairings.get(player.number)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -426,7 +426,6 @@ computer analysis, game chat and shareable URL.</string>
|
|||
<string name="blackWins">Black wins</string>
|
||||
<string name="draws">Draws</string>
|
||||
<string name="nextXTournament">Next %s tournament:</string>
|
||||
<string name="viewMoreTournaments">View more tournaments</string>
|
||||
<string name="averageOpponent">Average opponent</string>
|
||||
<string name="membersOnly">Members only</string>
|
||||
<string name="boardEditor">Board editor</string>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
&.current a {
|
||||
background: mix($c-accent, $c-bg-box, 70%);
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
opacity: 1!important;
|
||||
}
|
||||
&.new {
|
||||
border: $c-border;
|
||||
|
|
|
@ -11,29 +11,32 @@ $chat-optimal-size: calc(100vh - #{$site-header-outer-height} - #{$block-gap} -
|
|||
display: grid;
|
||||
|
||||
&__side { grid-area: side }
|
||||
&__table { grid-area: table }
|
||||
&__main { grid-area: main }
|
||||
.chat__members { grid-area: uchat; }
|
||||
|
||||
grid-template-areas:
|
||||
'main'
|
||||
'side'
|
||||
'uchat';
|
||||
'uchat'
|
||||
'table';
|
||||
|
||||
grid-gap: $block-gap;
|
||||
|
||||
@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-areas:
|
||||
'main side'
|
||||
'main uchat';
|
||||
'main uchat'
|
||||
'table table';
|
||||
.mchat__messages {
|
||||
max-height: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@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-areas:
|
||||
'side main table'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ $mq-col3: $mq-col3-uniboard;
|
|||
// @import 'stats';
|
||||
// @import 'duel';
|
||||
// @import 'actor-info';
|
||||
// @import 'player-info';
|
||||
@import 'player-info';
|
||||
// @import 'team-info';
|
||||
|
||||
.swiss {
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
@import '../../../common/css/component/bar-glider';
|
||||
@import '../../../common/css/component/slist';
|
||||
@import '../../../common/css/component/quote';
|
||||
@import '../../../common/css/component/color-icon';
|
||||
@import '../../../chat/css/chat';
|
||||
@import '../show';
|
||||
|
|
|
@ -15,6 +15,7 @@ export default class SwissCtrl {
|
|||
lastPageDisplayed: number | undefined;
|
||||
focusOnMe: boolean;
|
||||
joinSpinner: boolean = false;
|
||||
playerInfoId?: string;
|
||||
disableClicks: boolean = true;
|
||||
searching: boolean = false;
|
||||
redraw: () => void;
|
||||
|
@ -41,8 +42,6 @@ export default class SwissCtrl {
|
|||
this.data = {...this.data, ...data};
|
||||
this.data.me = data.me; // to account for removal on withdraw
|
||||
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);
|
||||
if (this.focusOnMe) this.scrollToMe();
|
||||
// if (data.featured) this.startWatching(data.featured.id);
|
||||
|
@ -121,6 +120,8 @@ export default class SwissCtrl {
|
|||
userLastPage = () => this.userSetPage(players(this).nbPages);
|
||||
|
||||
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);
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface SwissData {
|
|||
nbOngoing: number;
|
||||
status: Status;
|
||||
standing: Standing;
|
||||
playerInfo?: PlayerExt;
|
||||
isStarted?: boolean;
|
||||
isFinished?: boolean;
|
||||
socketVersion?: number;
|
||||
|
@ -60,9 +61,14 @@ export interface MyInfo {
|
|||
|
||||
export interface Pairing {
|
||||
g: string; // game
|
||||
c: boolean; // color
|
||||
w?: boolean; // won
|
||||
o?: boolean; // ongoing
|
||||
}
|
||||
export interface PairingExt extends Pairing {
|
||||
user: LightUser;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export interface Standing {
|
||||
page: number;
|
||||
|
@ -77,6 +83,7 @@ export interface Player {
|
|||
withdraw?: boolean;
|
||||
points: number;
|
||||
tieBreak: number;
|
||||
performance: number;
|
||||
rank: number;
|
||||
pairings: [Pairing | null];
|
||||
}
|
||||
|
@ -96,3 +103,7 @@ export type Page = Player[];
|
|||
export interface Pages {
|
||||
[n: number]: Page
|
||||
}
|
||||
|
||||
export interface PlayerExt extends Player {
|
||||
pairings: [PairingExt | null];
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import * as pagination from '../pagination';
|
|||
import { MaybeVNodes } from '../interfaces';
|
||||
import header from './header';
|
||||
import standing from './standing';
|
||||
import playerInfo from './playerInfo';
|
||||
|
||||
export default function(ctrl: SwissCtrl) {
|
||||
const d = ctrl.data;
|
||||
|
@ -22,6 +23,7 @@ export default function(ctrl: SwissCtrl) {
|
|||
$(el).replaceWith($('.swiss__underchat.none').removeClass('none'));
|
||||
})
|
||||
}),
|
||||
playerInfo(ctrl),
|
||||
h('div.swiss__main',
|
||||
h('div.box.swiss__main-' + d.status, content)
|
||||
),
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -12,7 +12,8 @@ function playerTr(ctrl: SwissCtrl, player: Player) {
|
|||
return h('tr', {
|
||||
key: userId,
|
||||
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)
|
||||
}, [
|
||||
|
@ -89,7 +90,11 @@ export default function standing(ctrl: SwissCtrl, pag, klass?: string): VNode {
|
|||
pag.currentPageResults.map(res => playerTr(ctrl, res)) : lastBody;
|
||||
if (pag.currentPageResults) lastBody = tableBody;
|
||||
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', {
|
||||
hook: {
|
||||
|
|
|
@ -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 {
|
||||
return h('div.spinner', [
|
||||
h('svg', { attrs: { viewBox: '0 0 40 40' } }, [
|
||||
|
|
|
@ -22,20 +22,16 @@ const loadPageOf = (ctrl: SwissCtrl, userId: string): Promise<any> =>
|
|||
json(`/swiss/${ctrl.data.id}/page-of/${userId}`);
|
||||
|
||||
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.redraw();
|
||||
}).catch(onFail);
|
||||
|
||||
// function playerInfo(ctrl: SwissCtrl, userId: string) {
|
||||
// return $.ajax({
|
||||
// url: ['/swiss', ctrl.data.id, 'player', userId].join('/'),
|
||||
// headers
|
||||
// }).then(data => {
|
||||
// ctrl.setPlayerInfoData(data);
|
||||
// ctrl.redraw();
|
||||
// }, onFail);
|
||||
// }
|
||||
const playerInfo = (ctrl: SwissCtrl, userId: string) =>
|
||||
json(`/swiss/${ctrl.data.id}/player/${userId}`).then(data => {
|
||||
ctrl.data.playerInfo = data;
|
||||
ctrl.redraw();
|
||||
}).catch(onFail);
|
||||
|
||||
export default {
|
||||
join: throttle(1000, join),
|
||||
|
@ -43,5 +39,5 @@ export default {
|
|||
loadPageOf,
|
||||
reloadSoon: throttle(4000, reload),
|
||||
reloadNow: reload,
|
||||
// playerInfo
|
||||
playerInfo
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue