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,
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)(

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 {
_ ?? { tour =>
JsonOk {

View File

@ -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)
}

View File

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

View File

@ -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>

View 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>

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}>/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

View File

@ -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")

View File

@ -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,

View File

@ -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 =>
scoring.recompute(s) >>-
systemChat(
swiss.id,
s"Round ${swiss.round.value + 1} started."
)
) {
case s if s.nextRoundAt.isEmpty =>
scoring.recompute(s) >>-
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)

View File

@ -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,19 +70,23 @@ 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 =>
colls.pairing
.find(
$doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
$doc(f.id -> true).some
)
.sort($sort desc f.date)
.one[Bdoc]
.dmap { _.flatMap(_.getAsOpt[Game.ID](f.id)) }
(swiss.nbOngoing > 0)
.?? {
colls.pairing
.find(
$doc(f.swissId -> swiss.id, f.players -> player.number, f.status -> SwissPairing.ongoing),
$doc(f.id -> true).some
)
.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
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" -> rankedPlayer.rank,
"user" -> user,
"rank" -> view.rank,
"user" -> view.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))
}
}
"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(

View File

@ -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"

View File

@ -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

View File

@ -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)
)
)
}
)

View File

@ -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>

View File

@ -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;

View File

@ -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'

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 'duel';
// @import 'actor-info';
// @import 'player-info';
@import 'player-info';
// @import 'team-info';
.swiss {

View File

@ -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';

View File

@ -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);

View File

@ -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];
}

View File

@ -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)
),

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', {
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: {

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 {
return h('div.spinner', [
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}`);
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
};