team battle round leaderboard

pull/6154/head
Thibault Duplessis 2020-03-11 13:03:09 -06:00
parent 60097458c1
commit 11b1dab2da
15 changed files with 198 additions and 156 deletions

View File

@ -72,7 +72,7 @@ final class Challenge(
case Some(pov) =>
negotiate(
html = Redirect(routes.Round.watcher(pov.gameId, "white")).fuccess,
api = apiVersion => env.api.roundApi.player(pov, apiVersion) map { Ok(_) }
api = apiVersion => env.api.roundApi.player(pov, none, apiVersion) map { Ok(_) }
) flatMap withChallengeAnonCookie(ctx.isAnon, c, false)
case None =>
negotiate(

View File

@ -30,7 +30,7 @@ final class Round(
else
PreventTheft(pov) {
pov.game.playableByAi ?? env.fishnet.player(pov.game)
env.tournament.api.miniView(pov, true) flatMap {
env.tournament.api.gameView.player(pov) flatMap {
tour =>
gameC.preloadUsers(pov.game) zip
(pov.game.simulId ?? env.simul.repo.find) zip
@ -39,7 +39,7 @@ final class Round(
.withMatchup(pov.game)) zip
(pov.game.isSwitchable ?? otherPovs(pov.game)) zip
env.bookmark.api.exists(pov.game, ctx.me) zip
env.api.roundApi.player(pov, lila.api.Mobile.Api.currentVersion) map {
env.api.roundApi.player(pov, tour, lila.api.Mobile.Api.currentVersion) map {
case _ ~ simul ~ chatOption ~ crosstable ~ playing ~ bookmarked ~ data =>
simul foreach env.simul.api.onPlayerConnection(pov.game, ctx.me)
Ok(
@ -62,7 +62,7 @@ final class Round(
else {
pov.game.playableByAi ?? env.fishnet.player(pov.game)
gameC.preloadUsers(pov.game) zip
env.api.roundApi.player(pov, apiVersion) zip
env.api.roundApi.player(pov, none, apiVersion) zip
getPlayerChat(pov.game, none) map {
case _ ~ data ~ chat =>
Ok {
@ -155,31 +155,33 @@ final class Round(
html = {
if (pov.game.replayable) analyseC.replay(pov, userTv = userTv)
else if (HTTPRequest.isHuman(ctx.req))
env.tournament.api.miniView(pov, false) zip
env.tournament.api.gameView.watcher(pov.game) zip
(pov.game.simulId ?? env.simul.repo.find) zip
getWatcherChat(pov.game) zip
(ctx.noBlind ?? env.game.crosstableApi.withMatchup(pov.game)) zip
env.api.roundApi.watcher(
pov,
lila.api.Mobile.Api.currentVersion,
tv = userTv.map { u =>
lila.round.OnUserTv(u.id)
}
) zip
env.bookmark.api.exists(pov.game, ctx.me) map {
case tour ~ simul ~ chat ~ crosstable ~ data ~ bookmarked =>
Ok(
html.round.watcher(
pov,
data,
tour,
simul,
crosstable,
userTv = userTv,
chatOption = chat,
bookmarked = bookmarked
env.bookmark.api.exists(pov.game, ctx.me) flatMap {
case tour ~ simul ~ chat ~ crosstable ~ bookmarked =>
env.api.roundApi.watcher(
pov,
tour,
lila.api.Mobile.Api.currentVersion,
tv = userTv.map { u =>
lila.round.OnUserTv(u.id)
}
) map { data =>
Ok(
html.round.watcher(
pov,
data,
tour.map(_.tourAndTeamVs),
simul,
crosstable,
userTv = userTv,
chatOption = chat,
bookmarked = bookmarked
)
)
)
}
}
else
for { // web crawlers don't need the full thing
@ -189,7 +191,7 @@ final class Round(
},
api = apiVersion =>
for {
data <- env.api.roundApi.watcher(pov, apiVersion, tv = none)
data <- env.api.roundApi.watcher(pov, none, apiVersion, tv = none)
analysis <- analyser get pov.game
chat <- getWatcherChat(pov.game)
} yield Ok {
@ -255,7 +257,7 @@ final class Round(
def sides(gameId: String, color: String) = Open { implicit ctx =>
OptionFuResult(proxyPov(gameId, color)) { pov =>
env.tournament.api.withTeamVs(pov.game) zip
env.tournament.api.gameView.withTeamVs(pov.game) zip
(pov.game.simulId ?? env.simul.repo.find) zip
env.game.gameRepo.initialFen(pov.game) zip
env.game.crosstableApi.withMatchup(pov.game) zip

View File

@ -260,7 +260,7 @@ final class Setup(
negotiate(
html = fuccess(redirectPov(pov)),
api = apiVersion =>
env.api.roundApi.player(pov, apiVersion) map { data =>
env.api.roundApi.player(pov, none, apiVersion) map { data =>
Created(data) as JSON
}
)

View File

@ -41,8 +41,8 @@ final class Tv(
val pov = if (flip) Pov second game else Pov first game
val onTv = lila.round.OnLichessTv(channel.key, flip)
negotiate(
html = {
env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = onTv.some) zip
html = env.tournament.api.gameView.watcher(pov.game) flatMap { tour =>
env.api.roundApi.watcher(pov, tour, lila.api.Mobile.Api.currentVersion, tv = onTv.some) zip
env.game.crosstableApi.withMatchup(game) zip
env.tv.tv.getChampions map {
case data ~ cross ~ champions =>
@ -51,7 +51,7 @@ final class Tv(
}
}
},
api = apiVersion => env.api.roundApi.watcher(pov, apiVersion, tv = onTv.some) map { Ok(_) }
api = apiVersion => env.api.roundApi.watcher(pov, none, apiVersion, tv = onTv.some) map { Ok(_) }
)
}

View File

@ -118,14 +118,14 @@ object bits {
private[round] def side(
pov: Pov,
data: play.api.libs.json.JsObject,
tour: Option[lila.tournament.TourMiniView],
tour: Option[lila.tournament.TourAndTeamVs],
simul: Option[lila.simul.Simul],
userTv: Option[lila.user.User] = None,
bookmarked: Boolean
)(implicit ctx: Context) = views.html.game.side(
pov,
(data \ "game" \ "initialFen").asOpt[String].map(chess.format.FEN),
tour.map(_.tourAndTeamVs),
tour,
simul = simul,
userTv = userTv,
bookmarked = bookmarked

View File

@ -14,7 +14,7 @@ object player {
def apply(
pov: Pov,
data: play.api.libs.json.JsObject,
tour: Option[lila.tournament.TourMiniView],
tour: Option[lila.tournament.GameView],
simul: Option[lila.simul.Simul],
cross: Option[lila.game.Crosstable.WithMatchup],
playing: List[Pov],
@ -51,17 +51,12 @@ object player {
roundTag,
embedJsUnsafe(s"""lichess=window.lichess||{};customWS=true;onload=function(){
LichessRound.boot(${safeJsonValue(
Json.obj(
"data" -> data,
"i18n" -> jsI18n(pov.game),
"userId" -> ctx.userId,
"chat" -> chatJson
) ++ tour
.flatMap(_.top)
.??(top =>
Json.obj(
"tour" -> lila.tournament.JsonView.top(top, lightUser)
)
Json
.obj(
"data" -> data,
"i18n" -> jsI18n(pov.game),
"userId" -> ctx.userId,
"chat" -> chatJson
)
)})}""")
),
@ -71,7 +66,7 @@ LichessRound.boot(${safeJsonValue(
)(
main(cls := "round")(
st.aside(cls := "round__side")(
bits.side(pov, data, tour, simul, bookmarked = bookmarked),
bits.side(pov, data, tour.map(_.tourAndTeamVs), simul, bookmarked = bookmarked),
chatOption.map(_ => chat.frag)
),
bits.roundAppPreload(pov, true),

View File

@ -14,7 +14,7 @@ object watcher {
def apply(
pov: Pov,
data: JsObject,
tour: Option[lila.tournament.TourMiniView],
tour: Option[lila.tournament.TourAndTeamVs],
simul: Option[lila.simul.Simul],
cross: Option[lila.game.Crosstable.WithMatchup],
userTv: Option[lila.user.User] = None,

View File

@ -12,7 +12,7 @@ import lila.round.JsonView.WithFlags
import lila.round.{ Forecast, JsonView }
import lila.security.Granter
import lila.simul.Simul
import lila.tournament.TourAndRanks
import lila.tournament.{ GameView => TourView }
import lila.tree.Node.partitionTreeJsonWriter
import lila.user.User
@ -23,10 +23,14 @@ final private[api] class RoundApi(
bookmarkApi: lila.bookmark.BookmarkApi,
gameRepo: lila.game.GameRepo,
tourApi: lila.tournament.TournamentApi,
simulApi: lila.simul.SimulApi
simulApi: lila.simul.SimulApi,
getTeamName: lila.team.GetTeamName,
getLightUser: lila.common.LightUser.GetterSync
)(implicit ec: scala.concurrent.ExecutionContext) {
def player(pov: Pov, apiVersion: ApiVersion)(implicit ctx: Context): Fu[JsObject] =
def player(pov: Pov, tour: Option[TourView], apiVersion: ApiVersion)(
implicit ctx: Context
): Fu[JsObject] =
gameRepo
.initialFen(pov.game)
.flatMap { initialFen =>
@ -40,14 +44,13 @@ final private[api] class RoundApi(
initialFen = initialFen,
nvui = ctx.blind
) zip
tourApi.tourAndRanks(pov.game) zip
(pov.game.simulId ?? simulApi.find) zip
(ctx.me.ifTrue(ctx.isMobileApi) ?? (me => noteApi.get(pov.gameId, me.id))) zip
forecastApi.loadForDisplay(pov) zip
bookmarkApi.exists(pov.game, ctx.me) map {
case json ~ tourOption ~ simulOption ~ note ~ forecast ~ bookmarked =>
case json ~ simulOption ~ note ~ forecast ~ bookmarked =>
(
withTournament(pov, tourOption) _ compose
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withSteps(pov, initialFen) _ compose
withNote(note) _ compose
@ -60,6 +63,7 @@ final private[api] class RoundApi(
def watcher(
pov: Pov,
tour: Option[TourView],
apiVersion: ApiVersion,
tv: Option[lila.round.OnTv],
initialFenO: Option[Option[FEN]] = None
@ -77,13 +81,12 @@ final private[api] class RoundApi(
initialFen = initialFen,
withFlags = WithFlags(blurs = ctx.me ?? Granter(_.ViewBlurs))
) zip
tourApi.tourAndRanks(pov.game) zip
(pov.game.simulId ?? simulApi.find) zip
(ctx.me.ifTrue(ctx.isMobileApi) ?? (me => noteApi.get(pov.gameId, me.id))) zip
bookmarkApi.exists(pov.game, ctx.me) map {
case json ~ tourOption ~ simulOption ~ note ~ bookmarked =>
case json ~ simulOption ~ note ~ bookmarked =>
(
withTournament(pov, tourOption) _ compose
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withNote(note) _ compose
withBookmark(bookmarked) _ compose
@ -114,13 +117,13 @@ final private[api] class RoundApi(
initialFen = initialFen,
withFlags = withFlags.copy(blurs = ctx.me ?? Granter(_.ViewBlurs))
) zip
tourApi.tourAndRanks(pov.game) zip
tourApi.gameView.analysis(pov.game) zip
(pov.game.simulId ?? simulApi.find) zip
(ctx.me.ifTrue(ctx.isMobileApi) ?? (me => noteApi.get(pov.gameId, me.id))) zip
bookmarkApi.exists(pov.game, ctx.me) map {
case json ~ tourOption ~ simulOption ~ note ~ bookmarked =>
case json ~ tour ~ simulOption ~ note ~ bookmarked =>
(
withTournament(pov, tourOption) _ compose
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withNote(note) _ compose
withBookmark(bookmarked) _ compose
@ -236,30 +239,32 @@ final private[api] class RoundApi(
analysisJson.bothPlayers(g, a)
})
private def withTournament(pov: Pov, tourOption: Option[TourAndRanks])(
json: JsObject
)(implicit lang: Lang) =
json.add("tournament" -> tourOption.map { data =>
def withTournament(pov: Pov, viewO: Option[TourView])(json: JsObject)(implicit lang: Lang) =
json.add("tournament" -> viewO.map { v =>
Json
.obj(
"id" -> data.tour.id,
"name" -> data.tour.name(false),
"running" -> data.tour.isStarted
"id" -> v.tour.id,
"name" -> v.tour.name(false),
"running" -> v.tour.isStarted
)
.add("secondsToFinish" -> data.tour.isStarted.option(data.tour.secondsToFinish))
.add("berserkable" -> data.tour.isStarted.option(data.tour.berserkable))
.add("secondsToFinish" -> v.tour.isStarted.option(v.tour.secondsToFinish))
.add("berserkable" -> v.tour.isStarted.option(v.tour.berserkable))
// mobile app API BC / should use game.expiration instead
.add("nbSecondsForFirstMove" -> data.tour.isStarted.option {
.add("nbSecondsForFirstMove" -> v.tour.isStarted.option {
pov.game.timeForFirstMove.toSeconds
})
.add(
"ranks" -> data.tour.isStarted.option(
Json.obj(
"white" -> data.whiteRank,
"black" -> data.blackRank
)
.add("ranks" -> v.ranks.map { r =>
Json.obj(
"white" -> r.whiteRank,
"black" -> r.blackRank
)
)
})
.add("top", v.top.map {
lila.tournament.JsonView.top(_, getLightUser)
})
.add("team", v.teamVs.map(_.teams(pov.color)) map { id =>
Json.obj("name" -> getTeamName(id))
})
})
private def withSimul(simulOption: Option[Simul])(json: JsObject) =

View File

@ -51,6 +51,8 @@ final class TournamentApi(
private val workQueue =
new WorkQueues(buffer = 256, expiration = 1 minute, timeout = 10 seconds, name = "tournament")
def get(id: Tournament.ID) = tournamentRepo byId id
def createTournament(
setup: TournamentSetup,
me: User,
@ -152,22 +154,6 @@ final class TournamentApi(
}
}
def tourAndRanks(game: Game): Fu[Option[TourAndRanks]] = ~ {
for {
tourId <- game.tournamentId
whiteId <- game.whitePlayer.userId
blackId <- game.blackPlayer.userId
} yield tournamentRepo byId tourId flatMap {
_ ?? { tour =>
cached ranking tour map { ranking =>
ranking.get(whiteId) |@| ranking.get(blackId) apply {
case (whiteR, blackR) => TourAndRanks(tour, whiteR + 1, blackR + 1)
}
}
}
}
}
private[tournament] def start(oldTour: Tournament): Funit =
Sequencing(oldTour.id)(tournamentRepo.createdById) { tour =>
tournamentRepo.setStatus(tour.id, Status.Started) >>-
@ -371,7 +357,7 @@ final class TournamentApi(
game.userIds.map(updatePlayer(tour, game.some)).sequenceFu.void >>- {
duelStore.remove(game)
socket.reload(tour.id)
updateTournamentStanding(tour.id)
updateTournamentStanding(tour)
withdrawNonMover(game)
}
}
@ -477,31 +463,63 @@ final class TournamentApi(
def tournamentTop(tourId: Tournament.ID): Fu[TournamentTop] =
tournamentTopCache get tourId
def miniView(pov: Pov, withTop: Boolean): Fu[Option[TourMiniView]] =
withTeamVs(pov.game) flatMap {
_ ?? {
case TourAndTeamVs(tour, teamVs) =>
withTop ?? {
teamVs.fold(tournamentTop(tour.id) dmap some) { vs =>
cached.teamInfo.get(tour.id -> vs.teams(pov.color)) map2 { info =>
TournamentTop(info.topPlayers take tournamentTopNb)
}
}
} dmap {
TourMiniView(tour, _, teamVs).some
}
}
}
object gameView {
def withTeamVs(game: Game): Fu[Option[TourAndTeamVs]] =
game.tournamentId ?? tournamentRepo.byId flatMap {
_ ?? { tour =>
(tour.isTeamBattle ?? playerRepo.teamVs(tour.id, game)) map {
TourAndTeamVs(tour, _).some
def player(pov: Pov): Fu[Option[GameView]] =
(pov.game.tournamentId ?? get) flatMap {
_ ?? { tour =>
getTeamVs(tour, pov.game) zip getGameRanks(tour, pov.game) flatMap {
case (teamVs, ranks) =>
teamVs.fold(tournamentTop(tour.id) dmap some) { vs =>
cached.teamInfo.get(tour.id -> vs.teams(pov.color)) map2 { info =>
TournamentTop(info.topPlayers take tournamentTopNb)
}
} dmap {
GameView(tour, teamVs, ranks, _).some
}
}
}
}
def watcher(game: Game): Fu[Option[GameView]] =
(game.tournamentId ?? get) flatMap {
_ ?? { tour =>
getTeamVs(tour, game) zip getGameRanks(tour, game) dmap {
case (teamVs, ranks) => GameView(tour, teamVs, ranks, none).some
}
}
}
def analysis(game: Game): Fu[Option[GameView]] =
(game.tournamentId ?? get) flatMap {
_ ?? { tour =>
getTeamVs(tour, game) dmap { GameView(tour, _, none, none).some }
}
}
def withTeamVs(game: Game): Fu[Option[TourAndTeamVs]] =
(game.tournamentId ?? get) flatMap {
_ ?? { tour =>
getTeamVs(tour, game) dmap { TourAndTeamVs(tour, _).some }
}
}
private def getGameRanks(tour: Tournament, game: Game): Fu[Option[GameRanks]] = ~ {
for {
whiteId <- game.whitePlayer.userId
blackId <- game.blackPlayer.userId
if tour.isStarted // don't fetch ranks of finished tournaments
} yield cached ranking tour map { ranking =>
ranking.get(whiteId) |@| ranking.get(blackId) apply {
case (whiteR, blackR) => GameRanks(whiteR + 1, blackR + 1)
}
}
}
private def getTeamVs(tour: Tournament, game: Game): Fu[Option[TeamBattle.TeamVs]] =
(tour.isTeamBattle ?? playerRepo.teamVs(tour.id, game))
}
def fetchVisibleTournaments: Fu[VisibleTournaments] =
tournamentRepo.publicCreatedSorted(6 * 60) zip
tournamentRepo.publicStarted zip
@ -613,12 +631,13 @@ final class TournamentApi(
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))
def apply(tourId: Tournament.ID): Unit =
throttler ! EarlyMultiThrottler.Work(
id = tourId,
run = () => publishNow(tourId),
cooldown = 15.seconds
)
def apply(tour: Tournament): Unit =
if (!tour.isTeamBattle)
throttler ! EarlyMultiThrottler.Work(
id = tour.id,
run = () => publishNow(tour.id),
cooldown = 15.seconds
)
}
}

View File

@ -16,6 +16,15 @@ case class TourMiniView(
case class TourAndTeamVs(tour: Tournament, teamVs: Option[TeamBattle.TeamVs])
case class GameView(
tour: Tournament,
teamVs: Option[TeamBattle.TeamVs],
ranks: Option[GameRanks],
top: Option[TournamentTop]
) {
def tourAndTeamVs = TourAndTeamVs(tour, teamVs)
}
case class MyInfo(rank: Int, withdraw: Boolean, gameId: Option[lila.game.Game.ID]) {
def page = {
math.floor((rank - 1) / 10) + 1
@ -39,11 +48,7 @@ case class PlayerInfoExt(
recentPovs: List[lila.game.LightPov]
)
case class TourAndRanks(
tour: Tournament,
whiteRank: Int,
blackRank: Int
)
case class GameRanks(whiteRank: Int, blackRank: Int)
case class RankedPairing(pairing: Pairing, rank1: Int, rank2: Int) {

View File

@ -76,6 +76,20 @@ export interface Tournament {
ranks?: TournamentRanks;
running?: boolean;
nbSecondsForFirstMove?: number;
top?: TourPlayer[];
team?: Team;
}
export interface TourPlayer {
n: string; // name
s: number; // score
t?: string; // title
f: boolean; // fire
w: boolean; // withdraw
}
export interface Team {
name: string;
}
export interface Simul {

View File

@ -2,6 +2,10 @@
&:hover {
overflow-y: auto!important;
}
h3 {
font-size: 1.2em;
padding: .5em 1em;
}
table {
border-bottom: none;
}

View File

@ -1,7 +1,8 @@
import { RoundOpts, RoundData } from './interfaces';
import { RoundApi, RoundMain } from './main';
import { ChatCtrl } from 'chat';
import { tourStandingCtrl, TourStandingCtrl, TourPlayer } from './tourStanding';
import { TourPlayer } from 'game';
import { tourStandingCtrl, TourStandingCtrl } from './tourStanding';
export default function(opts: RoundOpts): void {
const li = window.lichess;
@ -61,14 +62,15 @@ export default function(opts: RoundOpts): void {
};
opts.element = element;
opts.socketSend = li.socket.send;
if (!opts.tour && !data.simul) opts.onChange = (d: RoundData) => {
if (!opts.data.tournament && !data.simul) opts.onChange = (d: RoundData) => {
if (chat) chat.preset.setGroup(getPresetGroup(d));
};
console.log(opts);
round = (window['LichessRound'] as RoundMain).app(opts);
if (opts.chat) {
if (opts.tour) {
opts.chat.plugin = tourStandingCtrl(opts.tour, opts.i18n.standing);
if (opts.data.tournament?.top) {
opts.chat.plugin = tourStandingCtrl(opts.data.tournament.top, opts.data.tournament.team, opts.i18n.standing);
opts.chat.alwaysEnabled = true;
} else if (!data.simul) {
opts.chat.preset = getPresetGroup(opts.data);

View File

@ -2,7 +2,6 @@ import { VNode } from 'snabbdom/vnode';
import { GameData, Status } from 'game';
import { ClockData, Seconds, Centis } from './clock/clockCtrl';
import { CorresClockData } from './corresClock/corresClockCtrl';
import { TourPlayer } from './tourStanding';
import RoundController from './ctrl';
import { ChatPlugin } from 'chat';
import * as cg from 'chessground/types';
@ -91,7 +90,6 @@ export interface RoundOpts {
crosstableEl: HTMLElement;
i18n: any;
chat?: Chat;
tour?: TourPlayer[];
}
export interface Chat {

View File

@ -2,46 +2,44 @@ import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { onInsert } from './util'
import { ChatPlugin } from 'chat'
import { Team, TourPlayer } from 'game';
export interface TourStandingCtrl extends ChatPlugin {
set(data: TourPlayer[]): void;
set(players: TourPlayer[]): void;
}
export interface TourPlayer {
n: string; // name
s: number; // score
t?: string; // title
f: boolean; // fire
w: boolean; // withdraw
}
export function tourStandingCtrl(data: TourPlayer[], name: string): TourStandingCtrl {
export function tourStandingCtrl(players: TourPlayer[], team: Team | undefined, name: string): TourStandingCtrl {
return {
set(d: TourPlayer[]) { data = d },
set(d: TourPlayer[]) { players = d },
tab: {
key: 'tourStanding',
name: name
},
view(): VNode {
return h('table.slist', {
return h('div', {
hook: onInsert(_ => {
window.lichess.loadCssPath('round.tour-standing');
})
}, [
h('tbody', data.map((p: TourPlayer, i: number) => {
return h('tr.' + p.n, [
h('td.name', [
h('span.rank', '' + (i + 1)),
h('a.user-link.ulpt', {
attrs: { href: `/@/${p.n}` }
}, (p.t ? p.t + ' ' : '') + p.n)
]),
h('td.total', p.f ? {
class: { 'is-gold': true },
attrs: { 'data-icon': 'Q' }
} : {}, '' + p.s)
])
}))
team ? h('h3.text', {
attrs: { 'data-icon': 'f' }
}, team.name) : null,
h('table.slist', [
h('tbody', players.map((p: TourPlayer, i: number) => {
return h('tr.' + p.n, [
h('td.name', [
h('span.rank', '' + (i + 1)),
h('a.user-link.ulpt', {
attrs: { href: `/@/${p.n}` }
}, (p.t ? p.t + ' ' : '') + p.n)
]),
h('td.total', p.f ? {
class: { 'is-gold': true },
attrs: { 'data-icon': 'Q' }
} : {}, '' + p.s)
])
}))
])
]);
}
};