swiss WIP

swiss
Thibault Duplessis 2020-05-04 15:16:36 -06:00
parent 1efd45a3a1
commit efd3bdf72f
24 changed files with 159 additions and 129 deletions

View File

@ -125,14 +125,24 @@ final class Swiss(
}
}
def standing(id: String, page: Int) = Open { implicit ctx =>
WithSwiss(id) { swiss =>
JsonOk {
env.swiss.standingApi(swiss, page)
}
}
}
private def WithSwiss(id: String)(f: SwissModel => Fu[Result])(implicit ctx: Context): Fu[Result] =
env.swiss.api.byId(SwissId(id)) flatMap { _ ?? f }
private def WithEditableSwiss(id: String, me: lila.user.User)(
f: SwissModel => Fu[Result]
)(implicit ctx: Context): Fu[Result] =
env.swiss.api byId SwissId(id) flatMap {
case Some(t) if (t.createdBy == me.id && !t.isFinished) || isGranted(_.ManageTournament) =>
f(t)
case Some(t) => Redirect(routes.Swiss.show(t.id.value)).fuccess
case _ => notFound
WithSwiss(id) { swiss =>
if (swiss.createdBy == me.id && !swiss.isFinished) f(swiss)
else if (isGranted(_.ManageTournament)) f(swiss)
else Redirect(routes.Swiss.show(swiss.id.value)).fuccess
}
private def canHaveChat(swiss: SwissModel)(implicit ctx: Context): Fu[Boolean] =

View File

@ -134,8 +134,8 @@ final class Tournament(
def standing(id: String, page: Int) = Open { implicit ctx =>
OptionFuResult(repo byId id) { tour =>
env.tournament.standingApi(tour, page) map { data =>
Ok(data) as JSON
JsonOk {
env.tournament.standingApi(tour, page)
}
}
}

View File

@ -252,6 +252,7 @@ POST /swiss/$id<\w{8}>/join controllers.Swiss.join(id: String)
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)
# Simul
GET /simul controllers.Simul.home

View File

@ -37,6 +37,7 @@ final class Env(
setupEnv: lila.setup.Env,
simulEnv: lila.simul.Env,
tourEnv: lila.tournament.Env,
swissEnv: lila.swiss.Env,
onlineApiUsers: lila.bot.OnlineApiUsers,
challengeEnv: lila.challenge.Env,
msgEnv: lila.msg.Env,

View File

@ -12,6 +12,7 @@ import lila.round.JsonView.WithFlags
import lila.round.{ Forecast, JsonView }
import lila.security.Granter
import lila.simul.Simul
import lila.swiss.Swiss
import lila.tournament.{ GameView => TourView }
import lila.tree.Node.partitionTreeJsonWriter
import lila.user.User
@ -23,6 +24,7 @@ final private[api] class RoundApi(
bookmarkApi: lila.bookmark.BookmarkApi,
gameRepo: lila.game.GameRepo,
tourApi: lila.tournament.TournamentApi,
swissApi: lila.swiss.SwissApi,
simulApi: lila.simul.SimulApi,
getTeamName: lila.team.GetTeamName,
getLightUser: lila.common.LightUser.GetterSync
@ -45,13 +47,15 @@ final private[api] class RoundApi(
nvui = ctx.blind
) zip
(pov.game.simulId ?? simulApi.find) zip
fetchSwiss(pov) 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 ~ simulOption ~ note ~ forecast ~ bookmarked =>
case json ~ simul ~ swiss ~ note ~ forecast ~ bookmarked =>
(
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withSwiss(swiss) _ compose
withSimul(simul) _ compose
withSteps(pov, initialFen) _ compose
withNote(note) _ compose
withBookmark(bookmarked) _ compose
@ -82,12 +86,14 @@ final private[api] class RoundApi(
withFlags = WithFlags(blurs = ctx.me ?? Granter(_.ViewBlurs))
) zip
(pov.game.simulId ?? simulApi.find) zip
fetchSwiss(pov) zip
(ctx.me.ifTrue(ctx.isMobileApi) ?? (me => noteApi.get(pov.gameId, me.id))) zip
bookmarkApi.exists(pov.game, ctx.me) map {
case json ~ simulOption ~ note ~ bookmarked =>
case json ~ simul ~ swiss ~ note ~ bookmarked =>
(
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withSwiss(swiss) _ compose
withSimul(simul) _ compose
withNote(note) _ compose
withBookmark(bookmarked) _ compose
withSteps(pov, initialFen) _
@ -119,12 +125,15 @@ final private[api] class RoundApi(
) 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
fetchSwiss(pov) zip
ctx.userId.ifTrue(ctx.isMobileApi).?? { noteApi.get(pov.gameId, _) } zip
bookmarkApi.exists(pov.game, ctx.me) map {
case json ~ tour ~ simulOption ~ note ~ bookmarked =>
case json ~ tour ~ simul ~ swiss ~ note ~ bookmarked =>
(
withTournament(pov, tour) _ compose
withSimul(simulOption) _ compose
withSwiss(swiss) _ compose
withSimul(simul) _ compose
withSwiss(swiss) _ compose
withNote(note) _ compose
withBookmark(bookmarked) _ compose
withTree(pov, analysis, initialFen, withFlags) _ compose
@ -267,6 +276,15 @@ final private[api] class RoundApi(
})
})
def withSwiss(swiss: Option[Swiss])(json: JsObject) =
json.add("swiss" -> swiss.map { s =>
Json
.obj(
"id" -> s.id.value,
"running" -> s.isStarted
)
})
private def withSimul(simulOption: Option[Simul])(json: JsObject) =
json.add("simul", simulOption.map { simul =>
Json.obj(
@ -276,4 +294,6 @@ final private[api] class RoundApi(
"nbPlaying" -> simul.playingPairings.size
)
})
private def fetchSwiss(pov: Pov) = pov.game.swissId.map(Swiss.Id.apply) ?? swissApi.byId
}

View File

@ -82,9 +82,11 @@ case class Game(
def tournamentId = metadata.tournamentId
def simulId = metadata.simulId
def swissId = metadata.swissId
def isTournament = tournamentId.isDefined
def isSimul = simulId.isDefined
def isSwiss = swissId.isDefined
def isMandatory = isTournament || isSimul
def isClassical = perfType contains Classical
def nonMandatory = !isMandatory

View File

@ -21,7 +21,7 @@ final class TrouperMap[T <: Trouper](
def tellIfPresent(id: String, msg: => Any): Unit = getIfPresent(id) foreach (_ ! msg)
def tellAll(msg: Any) = troupers.asMap().asScala.foreach(_._2 ! msg)
def tellAll(msg: Any) = troupers.asMap.asScala.foreach(_._2 ! msg)
def tellIds(ids: Seq[String], msg: Any): Unit = ids foreach { tell(_, msg) }
@ -60,4 +60,6 @@ final class TrouperMap[T <: Trouper](
})
def monitor(name: String) = lila.mon.caffeineStats(troupers, name)
def keys: Set[String] = troupers.asMap.asScala.keySet.toSet
}

View File

@ -50,7 +50,7 @@ private object BsonHandlers {
implicit val playerHandler = new BSON[SwissPlayer] {
import SwissPlayer.Fields._
def reads(r: BSON.Reader) = SwissPlayer(
_id = r.get[SwissPlayer.Id](id),
id = r.get[SwissPlayer.Id](id),
swissId = r.get[Swiss.Id](swissId),
number = r.get[SwissPlayer.Number](number),
userId = r str userId,
@ -60,7 +60,7 @@ private object BsonHandlers {
score = r.get[Swiss.Score](score)
)
def writes(w: BSON.Writer, o: SwissPlayer) = $doc(
id -> o._id,
id -> o.id,
swissId -> o.swissId,
number -> o.number,
userId -> o.userId,
@ -88,7 +88,7 @@ private object BsonHandlers {
r.get[List[SwissPlayer.Number]](players) match {
case List(w, b) =>
SwissPairing(
_id = r str id,
id = r str id,
swissId = r.get[Swiss.Id](swissId),
round = r.get[SwissRound.Number](round),
white = w,
@ -98,7 +98,7 @@ private object BsonHandlers {
case _ => sys error "Invalid swiss pairing users"
}
def writes(w: BSON.Writer, o: SwissPairing) = $doc(
id -> o._id,
id -> o.id,
swissId -> o.swissId,
round -> o.round,
gameId -> o.gameId,

View File

@ -42,7 +42,7 @@ final class Env(
def version(swissId: Swiss.Id): Fu[SocketVersion] =
socket.rooms.ask[SocketVersion](swissId.value)(GetVersion)
private lazy val standingApi = wire[SwissStandingApi]
lazy val standingApi = wire[SwissStandingApi]
private lazy val rankingApi = wire[SwissRankingApi]

View File

@ -85,7 +85,7 @@ final private class PairingSystem(executable: String) {
* suffix must be at least 3 characters long, otherwise this function throws an IllegalArgumentException.
*/
def withTempFile[A](contents: String)(f: File => A): A = {
val file = File.createTempFile("lila-", "-swiss").pp
val file = File.createTempFile("lila-", "-swiss")
val p = new PrintWriter(file, "UTF-8")
try {
p.write(contents)
@ -94,7 +94,7 @@ final private class PairingSystem(executable: String) {
res
} finally {
p.close()
// file.delete()
file.delete()
}
}
}

View File

@ -125,7 +125,7 @@ final class SwissApi(
.flatMap { ids =>
lila.common.Future.applySequentially(ids) { id =>
Sequencing(id)(notFinishedById) { swiss =>
director.startRound(swiss).flatMap { scoring.recompute _ } >>- socket.reload(swiss.id)
director.startRound(swiss).flatMap(scoring.recompute) >>- socket.reload(swiss.id)
}
}
}

View File

@ -28,9 +28,9 @@ final private class SwissDirector(
_ <- pendings.isEmpty ?? fufail[Unit](s"BBPairing empty for ${from.id}")
pairings <- pendings.collect {
case Right(SwissPairing.Pending(w, b)) =>
idGenerator.game map { id =>
idGenerator.game dmap { id =>
SwissPairing(
_id = id,
id = id,
swissId = swiss.id,
round = swiss.round,
white = w,

View File

@ -3,14 +3,14 @@ package lila.swiss
import lila.game.Game
case class SwissPairing(
_id: Game.ID,
id: Game.ID,
swissId: Swiss.Id,
round: SwissRound.Number,
white: SwissPlayer.Number,
black: SwissPlayer.Number,
status: SwissPairing.Status
) {
def gameId = _id
def gameId = id
def players = List(white, black)
def has(number: SwissPlayer.Number) = white == number || black == number
def colorOf(number: SwissPlayer.Number) = chess.Color(white == number)

View File

@ -4,7 +4,7 @@ import lila.rating.Perf
import lila.user.{ Perfs, User }
case class SwissPlayer(
_id: SwissPlayer.Id, // random
id: SwissPlayer.Id, // random
swissId: Swiss.Id,
number: SwissPlayer.Number,
userId: User.ID,
@ -13,7 +13,6 @@ case class SwissPlayer(
points: Swiss.Points,
score: Swiss.Score
) {
def id = _id
def is(uid: User.ID): Boolean = uid == userId
def is(user: User): Boolean = is(user.id)
def is(other: SwissPlayer): Boolean = is(other.userId)
@ -31,7 +30,7 @@ object SwissPlayer {
user: User,
perfLens: Perfs => Perf
): SwissPlayer = new SwissPlayer(
_id = makeId(swissId, user.id),
id = makeId(swissId, user.id),
swissId = swissId,
number = number,
userId = user.id,

View File

@ -8,63 +8,50 @@ final class SwissScoring(
import BsonHandlers._
def recompute(swiss: Swiss): Funit =
SwissPlayer
.fields { p =>
for {
prevPlayers <- fetchPlayers(swiss)
pairings <- fetchPairings(swiss)
pairingMap = SwissPairing.toMap(pairings)
playersWithPoints = prevPlayers.map { p =>
val playerPairings = ~pairingMap.get(p.number)
p.copy(
points = swiss.allRounds.pp.foldLeft(Swiss.Points(0)) {
case (points, round) =>
points + playerPairings
.get(round)
.fold(Swiss.Points(1)) { pairing =>
pairing.winner.map(p.number.==) match {
case Some(true) => Swiss.Points(2)
case None => Swiss.Points(1)
case _ => Swiss.Points(0)
}
}
.pp
def recompute(swiss: Swiss): Funit = {
for {
prevPlayers <- fetchPlayers(swiss)
pairings <- fetchPairings(swiss)
pairingMap = SwissPairing.toMap(pairings)
playersWithPoints = prevPlayers.map { player =>
val playerPairings = ~pairingMap.get(player.number)
player.copy(
points = swiss.allRounds.foldLeft(Swiss.Points(0)) {
case (points, round) =>
points + playerPairings.get(round).fold(Swiss.Points(1)) { pairing =>
pairing.status match {
case Right(Some(winner)) if winner == player.number => Swiss.Points(2)
case Right(None) => Swiss.Points(1)
case _ => Swiss.Points(0)
}
}
)
}
playerMap = SwissPlayer.toMap(playersWithPoints)
players = playersWithPoints.map { p =>
p.copy(score = Swiss.Score {
(~pairingMap.get(p.number)).values.foldLeft(0d) {
case (score, pairing) =>
score + {
def opponentPoints = playerMap.get(pairing opponentOf p.number).??(_.points.value)
pairing.winner.map(p.number.==) match {
case Some(true) => opponentPoints
case None => opponentPoints / 2
case _ => 0
}
}
}
})
}
_ <- SwissPlayer.fields { f =>
prevPlayers
.zip(players.pp)
.map {
case (prev, player) =>
val upd = (prev.points != player.points).?? { $doc(f.points -> player.points) } ++
(prev.score != player.score).?? { $doc(f.score -> player.score) }
println(lila.db.BSON.debug(upd))
!upd.isEmpty ?? colls.player.update.one($id(p.id), $set(upd)).void
}
.sequenceFu
.void
}
} yield {}
)
}
.monSuccess(_.swiss.tiebreakRecompute)
playerMap = SwissPlayer.toMap(playersWithPoints)
players = playersWithPoints.map { p =>
p.copy(score = Swiss.Score {
(~pairingMap.get(p.number)).values.foldLeft(0d) {
case (score, pairing) =>
def opponentPoints = playerMap.get(pairing opponentOf p.number).??(_.points.value)
score + pairing.winner.map(p.number.==).fold(opponentPoints / 2) { _ ?? opponentPoints }
}
})
}
_ <- SwissPlayer.fields { f =>
prevPlayers
.zip(players)
.map {
case (prev, player) =>
val upd = (prev.points != player.points).?? { $doc(f.points -> player.points) } ++
(prev.score != player.score).?? { $doc(f.score -> player.score) }
(!upd.isEmpty) ?? colls.player.update.one($id(player.id), $set(upd)).void
}
.sequenceFu
.void
}
} yield {}
}.monSuccess(_.swiss.tiebreakRecompute)
private def fetchPlayers(swiss: Swiss) = SwissPlayer.fields { f =>
colls.player.ext

View File

@ -5,6 +5,7 @@ export interface GameData {
spectator?: boolean;
tournament?: Tournament;
simul?: Simul;
swiss?: Swiss;
takebackable: boolean;
moretimeable: boolean;
clock?: Clock;
@ -99,6 +100,11 @@ export interface Simul {
nbPlaying: number;
}
export interface Swiss {
id: string;
running?: boolean;
}
export interface Clock {
running: boolean;
initial: number;

View File

@ -48,7 +48,7 @@ export default function(opts: RoundOpts): void {
});
function startTournamentClock() {
if (opts.data.tournament) $('.game__tournament .clock').each(function(this: HTMLElement) {
if (data.tournament) $('.game__tournament .clock').each(function(this: HTMLElement) {
$(this).clock({
time: parseFloat($(this).data('time'))
});
@ -62,18 +62,18 @@ export default function(opts: RoundOpts): void {
};
opts.element = element;
opts.socketSend = li.socket.send;
if (!opts.data.tournament && !data.simul) opts.onChange = (d: RoundData) => {
if (!data.tournament && !data.simul && !data.swiss) opts.onChange = (d: RoundData) => {
if (chat) chat.preset.setGroup(getPresetGroup(d));
};
round = (window['LichessRound'] as RoundMain).app(opts);
const chatOpts = opts.chat;
if (chatOpts) {
if (opts.data.tournament?.top) {
chatOpts.plugin = tourStandingCtrl(opts.data.tournament.top, opts.data.tournament.team, opts.i18n.standing);
if (data.tournament?.top) {
chatOpts.plugin = tourStandingCtrl(data.tournament.top, data.tournament.team, opts.i18n.standing);
chatOpts.alwaysEnabled = true;
} else if (!data.simul) {
chatOpts.preset = getPresetGroup(opts.data);
chatOpts.preset = getPresetGroup(data);
chatOpts.parseMoves = true;
}
if (chatOpts.noteId && (chatOpts.noteAge || 0) < 10) chatOpts.noteText = '';

View File

@ -213,7 +213,7 @@ export function submitMove(ctrl: RoundController): VNode | undefined {
export function backToTournament(ctrl: RoundController): VNode | undefined {
const d = ctrl.data;
return (d.tournament && d.tournament.running) ? h('div.follow-up', [
return d.tournament?.running ? h('div.follow-up', [
h('a.text.fbt.strong.glowing', {
attrs: {
'data-icon': 'G',
@ -233,6 +233,20 @@ export function backToTournament(ctrl: RoundController): VNode | undefined {
]) : undefined;
}
export function backToSwiss(ctrl: RoundController): VNode | undefined {
const d = ctrl.data;
return d.swiss?.running ? h('div.follow-up', [
h('a.text.fbt.strong.glowing', {
attrs: {
'data-icon': 'G',
href: '/swiss/' + d.swiss.id
},
hook: util.bind('click', ctrl.setRedirecting)
}, ctrl.noarg('backToTournament')),
analysisButton(ctrl)
]) : undefined;
}
export function moretime(ctrl: RoundController) {
return game.moretimeable(ctrl.data) ? h('a.moretime', {
attrs: {
@ -246,7 +260,7 @@ export function moretime(ctrl: RoundController) {
export function followUp(ctrl: RoundController): VNode {
const d = ctrl.data,
rematchable = !d.game.rematch && (status.finished(d) || status.aborted(d)) && !d.tournament && !d.simul && !d.game.boosted,
rematchable = !d.game.rematch && (status.finished(d) || status.aborted(d)) && !d.tournament && !d.simul && !d.swiss && !d.game.boosted,
newable = (status.finished(d) || status.aborted(d)) && (
d.game.source === 'lobby' ||
d.game.source === 'pool'),
@ -260,6 +274,9 @@ export function followUp(ctrl: RoundController): VNode {
d.tournament ? h('a.fbt', {
attrs: {href: '/tournament/' + d.tournament.id}
}, ctrl.noarg('viewTournament')) : null,
d.swiss ? h('a.fbt', {
attrs: {href: '/swiss/' + d.swiss.id}
}, ctrl.noarg('viewTournament')) : null,
newable ? h('a.fbt', {
attrs: { href: d.game.source === 'pool' ? poolUrl(d.clock!, d.opponent.user) : '/?hook_like=' + d.game.id },
}, ctrl.noarg('newOpponent')) : null,
@ -279,6 +296,9 @@ export function watcherFollowUp(ctrl: RoundController): VNode | null {
d.tournament ? h('a.fbt', {
attrs: {href: '/tournament/' + d.tournament.id}
}, ctrl.noarg('viewTournament')) : null,
d.swiss ? h('a.fbt', {
attrs: {href: '/swiss/' + d.swiss.id}
}, ctrl.noarg('viewTournament')) : null,
analysisButton(ctrl)
];
return content.find(x => !!x) ? h('div.follow-up', content) : null;

View File

@ -36,7 +36,9 @@ function renderTableWith(ctrl: RoundController, buttons: MaybeVNodes) {
export function renderTableEnd(ctrl: RoundController) {
return renderTableWith(ctrl, [
isLoading(ctrl) ? loader() : (button.backToTournament(ctrl) || button.followUp(ctrl))
isLoading(ctrl) ? loader() : (
button.backToTournament(ctrl) || button.backToSwiss(ctrl) || button.followUp(ctrl)
)
]);
}

View File

@ -51,7 +51,8 @@
font-size: .8em;
}
}
.sheet {
.pairings {
width: 100%;
text-align: right;
padding-right: 0;
padding-left: 0;
@ -63,25 +64,12 @@
opacity: 0.7;
}
}
tr.long .sheet {
font-size: .9rem;
letter-spacing: .06em;
}
tr.xlong .sheet {
font-size: .85rem;
letter-spacing: .04em;
}
double {
color: $c-brag;
/* font-weight: bold; */
}
streak {
color: $c-secondary;
/* font-weight: bold; */
}
.total {
.points {
text-align: right;
font-weight: bold;
}
.score {
text-align: right;
padding-right: $block-gap;
}
}

View File

@ -82,7 +82,7 @@ export default class SwissCtrl {
};
loadPage = (data: Standing) => {
if (!this.pages[data.page]) this.pages[data.page] = data.players;
if (!data.failed || !this.pages[data.page]) this.pages[data.page] = data.players;
}
setPage = (page: number) => {

View File

@ -63,6 +63,7 @@ export interface Pairing {
export interface Standing {
page: number;
players: Player[];
failed?: boolean;
}
export interface Player {

View File

@ -9,7 +9,7 @@ export default function(send: SocketSend, ctrl: SwissCtrl) {
const handlers = {
reload() {
setTimeout(ctrl.askReload, Math.floor(Math.random() * 4000))
setTimeout(ctrl.askReload, Math.floor(Math.random() * 3000))
},
redirect(fullId) {
ctrl.redirectFirst(fullId.slice(0, 8), true);

View File

@ -3,38 +3,29 @@ import { json, form } from 'common/xhr';
import SwissCtrl from './ctrl';
import { SwissData } from './interfaces';
const headers = {
'Accept': 'application/vnd.lichess.v5+json'
};
// when the tournament no longer exists
function onFail(err) {
// throw err;
window.lichess.reload();
throw err;
// window.lichess.reload();
}
const join = (ctrl: SwissCtrl) =>
json(`/swiss/${ctrl.data.id}/join`, { method: 'post' }).catch(onFail);
const loadPage = (ctrl: SwissCtrl, p: number) =>
thenReloadOrFail(ctrl,
json(`/swiss/${ctrl.data.id}/standing/${p}`)
);
json(`/swiss/${ctrl.data.id}/standing/${p}`).then(data => {
ctrl.loadPage(data);
ctrl.redraw();
});
const loadPageOf = (ctrl: SwissCtrl, userId: string): Promise<any> =>
json(`/swiss/${ctrl.data.id}/page-of/${userId}`);
const reload = (ctrl: SwissCtrl) =>
thenReloadOrFail(ctrl,
json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}`)
);
const thenReloadOrFail = (ctrl: SwissCtrl, res: Promise<SwissData>) =>
res.then(data => {
json(`/swiss/${ctrl.data.id}?page=${ctrl.focusOnMe ? 0 : ctrl.page}`).then(data => {
ctrl.reload(data);
ctrl.redraw();
})
.catch(onFail);
}).catch(onFail);
// function playerInfo(ctrl: SwissCtrl, userId: string) {
// return $.ajax({