more tournament rewrite WIP

This commit is contained in:
Thibault Duplessis 2015-06-12 00:54:51 +02:00
parent 2c2ec63469
commit 4acfbfdd1f
23 changed files with 103 additions and 80 deletions

View file

@ -12,7 +12,7 @@ import lila.common.HTTPRequest
import lila.game.{ Pov, PlayerRef, GameRepo, Game => GameModel }
import lila.hub.actorApi.map.Tell
import lila.round.actorApi.round._
import lila.tournament.{ TournamentRepo, Tournament => Tourney }
import lila.tournament.{ TournamentRepo, Tournament => Tourney, MiniStanding }
import lila.user.{ User => UserModel, UserRepo }
import makeTimeout.large
import views._
@ -59,7 +59,7 @@ object Round extends LilaController with TheftPrevention {
if (pov.game.playableByAi) env.roundMap ! Tell(pov.game.id, AiPlay)
pov.game.started.fold(
PreventTheft(pov) {
myTour(pov.game.tournamentId) zip
myTour(pov.game.tournamentId, true) zip
(pov.game.simulId ?? Env.simul.repo.find) zip
Env.game.crosstableApi(pov.game) zip
(!pov.game.isTournament ?? otherPovs(pov.gameId)) flatMap {
@ -145,7 +145,7 @@ object Round extends LilaController with TheftPrevention {
else ctx.userId.flatMap(pov.game.playerByUserId) ifTrue pov.game.playable match {
case Some(player) => renderPlayer(pov withColor player.color)
case None if HTTPRequest.isHuman(ctx.req) =>
myTour(pov.game.tournamentId) zip
myTour(pov.game.tournamentId, false) zip
(pov.game.simulId ?? Env.simul.repo.find) zip
Env.game.crosstableApi(pov.game) zip
Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion, tv = none) map {
@ -163,11 +163,9 @@ object Round extends LilaController with TheftPrevention {
api = apiVersion => Env.api.roundApi.watcher(pov, apiVersion, tv = none) map { Ok(_) }
)
private def myTour(tourId: Option[String])(implicit ctx: Context): Fu[Option[Tourney]] =
private def myTour(tourId: Option[String], withStanding: Boolean)(implicit ctx: Context): Fu[Option[MiniStanding]] =
tourId ?? { tid =>
ctx.userId ?? { uid =>
Env.tournament.cached tour tid map (_ filter (_ contains uid))
}
Env.tournament.api.miniStanding(tid, ctx.userId, withStanding)
}
private def join(pov: Pov)(implicit ctx: Context): Fu[Result] =
@ -211,12 +209,12 @@ object Round extends LilaController with TheftPrevention {
}
private def sides(pov: Pov, isPlayer: Boolean)(implicit ctx: Context) =
myTour(pov.game.simulId) zip
myTour(pov.game.simulId, isPlayer) zip
(pov.game.simulId ?? Env.simul.repo.find) zip
GameRepo.initialFen(pov.game) zip
Env.game.crosstableApi(pov.game) map {
case (((myTour, simul), initialFen), crosstable) =>
Ok(html.game.sides(pov, initialFen, myTour, crosstable, withTourStanding = isPlayer, simul))
case (((tour, simul), initialFen), crosstable) =>
Ok(html.game.sides(pov, initialFen, tour, crosstable, simul))
}
def continue(id: String, mode: String) = Open { implicit ctx =>

View file

@ -57,9 +57,9 @@ object Tournament extends LilaController {
}
def gameStanding(id: String) = Open { implicit ctx =>
env.cached tour id map {
case Some(t) if !t.isCreated => Ok(html.tournament.gameStanding(t, true))
case _ => NotFound
env.api.miniStanding(id, true) map {
case Some(m) if !m.tour.isCreated => Ok(html.tournament.gameStanding(m))
case _ => NotFound
}
}
@ -70,11 +70,11 @@ object Tournament extends LilaController {
html = repo enterableById id map {
case None => tournamentNotFound
case Some(tour) =>
env.api.join(tour, me)
env.api.join(tour.id, me)
Redirect(routes.Tournament.show(tour.id))
},
api = _ => OptionFuOk(repo enterableById id) { tour =>
env.api.join(tour, me)
env.api.join(tour.id, me)
fuccess(Json.obj("ok" -> true))
}
)

View file

@ -45,7 +45,7 @@ userId: @Html(ctx.userId.fold("null")(id => s""""$id""""))
@analyse.layout(
title = title,
side = views.html.game.side(pov, initialFen, none, withTourStanding = false, simul = simul, userTv = userTv).some,
side = views.html.game.side(pov, initialFen, none, simul = simul, userTv = userTv).some,
chat = base.chatDom(trans.spectatorRoom.str(), ctx.isAuth).some,
underchat = underchat.some,
moreCss = moreCss,

View file

@ -37,7 +37,7 @@ orientation: "@pov.color.name"
@analyse.layout(
title = title,
side = views.html.game.side(pov, initialFen, none, withTourStanding = false, simul = simul).some,
side = views.html.game.side(pov, initialFen, none, simul = simul).some,
chat = base.chatDom(trans.spectatorRoom.str(), ctx.isAuth).some,
underchat = underchat.some,
moreJs = moreJs,

View file

@ -1,4 +1,4 @@
@(pov: Pov, initialFen: Option[String], myTour: Option[lila.tournament.Tournament], withTourStanding: Boolean, simul: Option[lila.simul.Simul], userTv: Option[User] = None)(implicit ctx: Context)
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], userTv: Option[User] = None)(implicit ctx: Context)
@import pov._
@import lila.tournament.arena
@ -86,16 +86,14 @@
</div>
}
@myTour match {
case Some(t) if !t.isCreated => { @tournament.gameStanding(t, withTourStanding) }
case _ => {
@pov.game.tournamentId.map { tourId =>
@tour match {
case Some(m) if !m.tour.isCreated => { @tournament.gameStanding(m) }
case Some(m) => {
<div class="game_tournament side_box no_padding">
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(tourId)">@tournamentIdToName(tourId)</a></p>
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(m.tour.id)">@m.tour.fullName</a></p>
</div>
}
}
}
@simul.map { sim =>
<div class="game_simul side_box no_padding">

View file

@ -1,7 +1,7 @@
@(pov: Pov, initialFen: Option[String], myTour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable], withTourStanding: Boolean, simul: Option[lila.simul.Simul], userTv: Option[User] = None)(implicit ctx: Context)
@(pov: Pov, initialFen: Option[String], tour: Option[lila.tournament.MiniStanding], cross: Option[lila.game.Crosstable], simul: Option[lila.simul.Simul], userTv: Option[User] = None)(implicit ctx: Context)
<div class="sides">
@side(pov, initialFen, myTour, withTourStanding, simul, userTv)
@side(pov, initialFen, tour, simul, userTv)
@cross.map { c =>
<div class="crosstable">
@crosstable(c, pov.gameId.some)

View file

@ -1,4 +1,4 @@
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable], playing: List[Pov])(implicit ctx: Context)
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable], playing: List[Pov])(implicit ctx: Context)
@import pov._
@ -20,7 +20,7 @@ userId: @Html(ctx.userId.fold("null")(id => s""""$id""""))
@round.layout(
title = title,
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, withTourStanding = true, simul),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, simul),
chat = pov.game.hasChat.option(base.chatDom(trans.chatRoom.str(), ctx.isAuth)),
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View file

@ -1,4 +1,4 @@
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable], userTv: Option[User] = None)(implicit ctx: Context)
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.MiniStanding], simul: Option[lila.simul.Simul], cross: Option[lila.game.Crosstable], userTv: Option[User] = None)(implicit ctx: Context)
@title = @{ s"${playerText(pov.player)} vs ${playerText(pov.opponent)} in ${pov.gameId}" }
@ -17,7 +17,7 @@ i18n: @jsI18n()
@round.layout(
title = title,
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, withTourStanding = false, simul = simul, userTv = userTv),
side = views.html.game.side(pov, (data\"game"\"initialFen").asOpt[String], tour, simul = simul, userTv = userTv),
chat = base.chatDom(trans.spectatorRoom.str()).some,
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View file

@ -14,7 +14,7 @@ orientation: "@pov.color.name"
@round.layout(
title = title,
side = views.html.game.side(pov, initialFen, none, withTourStanding = false, simul = none, userTv = none),
side = views.html.game.side(pov, initialFen, none, simul = none, userTv = none),
chat = base.chatDom(trans.spectatorRoom.str()).some,
underchat = views.html.game.watchers().some,
moreJs = moreJs,

View file

@ -1,23 +1,15 @@
@(t: Tournament, withStanding: Boolean)(implicit ctx: Context)
@(m: lila.tournament.MiniStanding)(implicit ctx: Context)
<div class="game_tournament side_box no_padding scroll-shadow-soft">
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(t.id)">@t.fullName</a></p>
<div class="clock" data-time="@t.remainingSeconds">
<div class="time text" data-icon="p">@t.clockStatus</div>
<p class="top text" data-icon="g"><a href="@routes.Tournament.show(m.tour.id)">@m.tour.fullName</a></p>
<div class="clock" data-time="@m.tour.remainingSeconds">
<div class="time text" data-icon="p">@m.tour.clockStatus</div>
</div>
@if(withStanding) {
<table class="slist standing">
<tbody>
@t.rankedPlayers.take(50).map {
@m.standing.map {
case lila.tournament.RankedPlayer(rank, player) => {
@defining((
t scoreSheet player,
// TODO FIXME that's a little ugly I must say.
t.system.scoringSystem match {
case ss @ lila.tournament.arena.ScoringSystem => ss.scoreSheet(t, player.id).onFire
case _ => false
})) {
case (scoreSheet, onFire) => {
<tr @if(ctx.userId.exists(player.id==)) { class="me" }>
<td class="name">
@if(player.withdraw) {

View file

@ -1,4 +1,4 @@
@(tour: lila.tournament.Tournament)
@(tour: Tournament)
@notification.view("tournament_reminder", closable = false, glow = true) {
<a data-icon="g" href="@routes.Tournament.show(tour.id)">&nbsp;@tour.name</a> in progress!

View file

@ -1,4 +1,4 @@
@(tour: lila.tournament.Tournament, variantLink: Boolean = false)(implicit ctx: Context)
@(tour: Tournament, variantLink: Boolean = false)(implicit ctx: Context)
<span class="setup">
@tour.clock.show •
@if(tour.variant.exotic) {

View file

@ -1,4 +1,4 @@
@(tour: lila.tournament.Tournament, socketVersion: Int, data: play.api.libs.json.JsObject, chat: Option[lila.chat.UserChat])(implicit ctx: Context)
@(tour: Tournament, socketVersion: Int, data: play.api.libs.json.JsObject, chat: Option[lila.chat.UserChat])(implicit ctx: Context)
@underchat = {
<div class="watchers" data-icon="v">
@ -29,7 +29,7 @@ chessground = false) {
<div id="tournament" @tour.schedule.map { sched =>
class="scheduled @sched.freq @sched.speed @sched.variant"
}></div>
@if(!tour.isRunning && !tour.isFinished) {
@if(tour.isCreated) {
<div id="tournament_faq" class="none">
@faq(tour.rated.some, tour.system.some, tour.`private`.option(tour.id))
</div>

View file

@ -1,4 +1,4 @@
@(tour: lila.tournament.Tournament)(implicit ctx: Context)
@(tour: Tournament)(implicit ctx: Context)
<div class="side_box padded">
<div class="game_infos" data-icon="@tour.perfType.map(_.iconChar)">

View file

@ -78,7 +78,8 @@ object BSONHandlers {
withdraw = r boolD "w",
score = r int "s",
perf = r int "p",
magicScore = r int "m")
magicScore = r int "m",
fire = r boolD "f")
def writes(w: BSON.Writer, o: Player) = BSONDocument(
"_id" -> o.id,
"tid" -> o.tourId,
@ -88,7 +89,8 @@ object BSONHandlers {
"w" -> w.boolO(o.withdraw),
"s" -> o.score,
"p" -> o.perf,
"m" -> o.magicScore)
"m" -> o.magicScore,
"f" -> w.boolO(o.fire))
}
implicit val pairingHandler = new BSON[Pairing] {
@ -104,9 +106,7 @@ object BSONHandlers {
turns = r intO "t",
date = r date "d",
berserk1 = r intD "b1",
berserk2 = r intD "b2",
perf1 = r intD "p1",
perf2 = r intD "p2")
berserk2 = r intD "b2")
}
def writes(w: BSON.Writer, o: Pairing) = BSONDocument(
"_id" -> o.id,
@ -117,9 +117,7 @@ object BSONHandlers {
"t" -> o.turns,
"d" -> w.date(o.date),
"b1" -> w.intO(o.berserk1),
"b2" -> w.intO(o.berserk2),
"p1" -> w.intO(o.perf1),
"p2" -> w.intO(o.perf2))
"b2" -> w.intO(o.berserk2))
}
private implicit val eventHandler = new BSON[Event] {

View file

@ -6,7 +6,7 @@ import lila.game.{ Game, PovRef, IdGenerator }
import org.joda.time.DateTime
case class Pairing(
id: String, // game ID
id: String, // game In
tourId: String,
status: chess.Status,
user1: String,
@ -15,9 +15,7 @@ case class Pairing(
turns: Option[Int],
date: DateTime,
berserk1: Int,
berserk2: Int,
perf1: Int,
perf2: Int) {
berserk2: Int) {
def gameId = id
@ -51,11 +49,6 @@ case class Pairing(
else if (userId == user2) berserk2
else 0
def perfOf(userId: String): Int =
if (userId == user1) perf1
else if (userId == user2) perf2
else 0
def validBerserkOf(userId: String): Int =
notSoQuickFinish ?? berserkOf(userId)
@ -65,9 +58,7 @@ case class Pairing(
def finish(g: Game) = copy(
status = g.status,
winner = g.winnerUserId,
turns = g.turns.some,
perf1 = ~g.whitePlayer.ratingDiff,
perf2 = ~g.blackPlayer.ratingDiff)
turns = g.turns.some)
}
private[tournament] object Pairing {
@ -87,7 +78,5 @@ private[tournament] object Pairing {
turns = none,
date = d | DateTime.now,
berserk1 = 0,
berserk2 = 0,
perf1 = 0,
perf2 = 0)
berserk2 = 0)
}

View file

@ -4,8 +4,6 @@ import lila.game.PerfPicker
import lila.rating.Perf
import lila.user.{ User, Perfs }
import ornicar.scalalib.Random
private[tournament] case class Player(
id: String, // random
tourId: String,
@ -16,7 +14,7 @@ private[tournament] case class Player(
score: Int = 0,
perf: Int = 0,
magicScore: Int = 0,
rank: Int = Int.MaxValue) {
fire: Boolean = false) {
def active = !withdraw
@ -28,13 +26,13 @@ private[tournament] case class Player(
def unWithdraw = copy(withdraw = false)
def recomputeMagicScore = copy(
magicScore = (score * 1000000) + (perf * 1000) + rating)
magicScore = (score * 1000000) + (perf * 1000) + rating + withdraw.fold(Int.MinValue / 2, 0))
}
private[tournament] object Player {
private[tournament] def make(tourId: String, user: User, perfLens: Perfs => Perf): Player = new Player(
id = Random nextStringUppercase 8,
id = lila.game.IdGenerator.game,
tourId = tourId,
userId = user.id,
rating = perfLens(user.perfs).intRating,

View file

@ -17,6 +17,9 @@ object PlayerRepo {
private def selectId(id: String) = BSONDocument("_id" -> id)
private def selectTour(tourId: String) = BSONDocument("tid" -> tourId)
private def selectUser(userId: String) = BSONDocument("uid" -> userId)
private def selectTourUser(tourId: String, userId: String) = BSONDocument(
"tid" -> tourId,
"uid" -> userId)
private val selectActive = BSONDocument("w" -> BSONDocument("$ne" -> true))
private val selectWithdraw = BSONDocument("w" -> true)
private val bestSort = BSONDocument("m" -> -1)
@ -44,9 +47,12 @@ object PlayerRepo {
def remove(tourId: String, userId: String) =
coll.remove(selectTour(tourId) ++ selectUser(userId)).void
def exists(tourId: String, userId: String) =
coll.db command Count(coll.name, selectTourUser(tourId, userId).some) map (0!=)
def existsActive(tourId: String, userId: String) =
coll.db command Count(coll.name, Some(
selectTour(tourId) ++ selectUser(userId) ++ selectActive
selectTourUser(tourId, userId) ++ selectActive
)) map (0!=)
def unWithdraw(tourId: String) = coll.update(
@ -57,6 +63,11 @@ object PlayerRepo {
def find(tourId: String, userId: String): Fu[Option[Player]] =
coll.find(selectTour(tourId) ++ selectUser(userId)).one[Player]
def update(tourId: String, userId: String)(f: Player => Fu[Player]) =
find(tourId, userId) flatten s"No such player: $tourId/$userId" flatMap f flatMap { player =>
coll.update(selectId(player.id), player).void
}
def join(tourId: String, user: User, perfLens: Perfs => Perf) =
find(tourId, user.id) flatMap {
case Some(p) if p.withdraw => coll.update(selectId(p.id), BSONDocument("$unset" -> BSONDocument("w" -> true)))

View file

@ -36,6 +36,7 @@ trait Score {
trait ScoreSheet {
def scores: List[Score]
def total: Int
def onFire: Boolean
}
trait ScoringSystem {

View file

@ -168,12 +168,25 @@ private[tournament] final class TournamentApi(
def finishGame(game: Game) {
game.tournamentId foreach { tourId =>
Sequencing(tourId)(TournamentRepo.startedById) { tour =>
PairingRepo.update(game.id, _ finish game) >>-
PairingRepo.update(game.id, _ finish game) >>
game.userIds.map(updatePlayer(tour)).sequenceFu.void >>-
socketReload(tour.id) >>- updateTournamentStanding(tour)
}
}
}
def updatePlayer(tour: Tournament)(userId: String): Funit =
(tour.perfType ?? { UserRepo.ratingOf(userId, _) }) flatMap { rating =>
PlayerRepo.update(tour.id, userId) { player =>
tour.system.scoringSystem.sheet(tour, userId) map { sheet =>
player.copy(
score = sheet.total,
fire = sheet.onFire,
perf = rating.fold(player.perf)(_.toInt - player.rating))
}
}
}
def ejectCheater(userId: String) {
TournamentRepo.allEnterable foreach {
_ foreach { tour =>
@ -190,6 +203,23 @@ private[tournament] final class TournamentApi(
}
}
def miniStanding(tourId: String, withStanding: Boolean): Fu[Option[MiniStanding]] =
TournamentRepo byId tourId flatMap {
_ ?? { tour =>
if (withStanding) PlayerRepo.bestByTourWithRank(tour.id, 20) map { rps =>
MiniStanding(tour, rps.some).some
}
else fuccess(MiniStanding(tour, none).some)
}
}
def miniStanding(tourId: String, userId: Option[String], withStanding: Boolean): Fu[Option[MiniStanding]] =
userId ?? { uid =>
PlayerRepo.exists(tourId, uid) flatMap {
_ ?? miniStanding(tourId, withStanding)
}
}
private def sequence(tourId: String)(work: => Funit) {
sequencers ! Tell(tourId, Sequencer work work)
}

View file

@ -31,7 +31,7 @@ object ScoringSystem extends AbstractScoringSystem {
val emptySheet = Sheet(Nil)
def sheet(tour: Tournament, userId: String) =
def sheet(tour: Tournament, userId: String): Fu[Sheet] =
PairingRepo.finishedByPlayerChronological(tour.id, userId) map { pairings =>
Sheet {
val nexts = (pairings drop 1 map Some.apply) :+ None

View file

@ -0,0 +1,5 @@
package lila.tournament
case class MiniStanding(
tour: Tournament,
standing: Option[RankedPlayers])

View file

@ -259,6 +259,9 @@ trait UserRepo {
def email(id: ID): Fu[Option[String]] = $primitive.one($select(id), F.email)(_.asOpt[String])
def ratingOf(id: ID, perfType: PerfType) =
$primitive.one($select(id), s"${F.perfs}.${perfType.key}.gl.r")(_.asOpt[Double])
def setSeenAt(id: ID) {
$update.fieldUnchecked(id, "seenAt", $date(DateTime.now))
}