lila/modules/tournament/src/main/TournamentApi.scala

621 lines
22 KiB
Scala
Raw Normal View History

2013-05-11 15:20:24 -06:00
package lila.tournament
2019-12-13 07:30:20 -07:00
import akka.actor.{ ActorSystem, Props }
2019-12-08 09:58:50 -07:00
import akka.pattern.ask
2019-12-02 17:42:57 -07:00
import akka.stream.scaladsl._
2019-12-08 09:58:50 -07:00
import com.github.ghik.silencer.silent
2017-12-11 08:51:09 -07:00
import org.joda.time.DateTime
2013-05-24 11:04:49 -06:00
import play.api.libs.json._
import scala.concurrent.duration._
import scala.concurrent.Promise
2013-05-24 11:04:49 -06:00
2019-12-02 11:48:11 -07:00
import lila.common.config.MaxPerSecond
2019-12-09 11:07:31 -07:00
import lila.common.{ Bus, Debouncer, LightUser, WorkQueues }
2019-12-08 09:58:50 -07:00
import lila.game.{ Game, GameRepo, LightPov }
import lila.hub.actorApi.lobby.ReloadTournaments
2019-12-08 09:58:50 -07:00
import lila.hub.LightTeam
import lila.hub.LightTeam._
2019-12-13 07:30:20 -07:00
import lila.round.actorApi.round.{ AbortForce, GoBerserk }
2019-11-25 14:36:39 -07:00
import lila.socket.Socket.SendToFlag
2013-05-24 11:04:49 -06:00
import lila.user.{ User, UserRepo }
2013-05-11 15:20:24 -06:00
import makeTimeout.short
final class TournamentApi(
2015-06-18 06:15:07 -06:00
cached: Cached,
2019-12-02 17:42:57 -07:00
userRepo: UserRepo,
gameRepo: GameRepo,
2019-12-02 11:48:11 -07:00
playerRepo: PlayerRepo,
pairingRepo: PairingRepo,
tournamentRepo: TournamentRepo,
apiJsonView: ApiJsonView,
2014-06-04 14:46:14 -06:00
autoPairing: AutoPairing,
2019-12-02 17:42:57 -07:00
pairingSystem: arena.PairingSystem,
callbacks: TournamentApi.Callbacks,
renderer: lila.hub.actors.Renderer,
2019-10-20 16:32:49 -06:00
socket: TournamentSocket,
2019-10-31 12:04:37 -06:00
tellRound: lila.round.TellRound,
trophyApi: lila.user.TrophyApi,
2016-06-17 08:56:44 -06:00
verify: Condition.Verify,
2018-01-13 22:31:31 -07:00
duelStore: DuelStore,
2018-04-04 22:53:24 -06:00
pause: Pause,
2019-12-23 18:01:45 -07:00
cacheApi: lila.memo.CacheApi,
2019-08-20 06:48:17 -06:00
lightUserApi: lila.user.LightUserApi,
2019-12-02 17:42:57 -07:00
proxyRepo: lila.round.GameProxyRepo
)(
implicit ec: scala.concurrent.ExecutionContext,
system: ActorSystem,
mat: akka.stream.Materializer,
mode: play.api.Mode
) {
2013-05-11 15:20:24 -06:00
private val workQueue =
new WorkQueues(buffer = 256, expiration = 1 minute, timeout = 10 seconds, name = "tournament")
2019-12-09 11:07:31 -07:00
2019-10-02 04:20:44 -06:00
def createTournament(
2019-12-13 07:30:20 -07:00
setup: TournamentSetup,
me: User,
myTeams: List[LightTeam],
getUserTeamIds: User => Fu[List[TeamID]]
2019-10-02 04:20:44 -06:00
): Fu[Tournament] = {
2019-10-02 10:50:09 -06:00
val tour = Tournament.make(
2017-01-16 15:18:30 -07:00
by = Right(me),
name = DataForm.canPickName(me) ?? setup.name,
clock = setup.clockConfig,
2015-06-11 09:03:45 -06:00
minutes = setup.minutes,
waitMinutes = setup.waitMinutes | DataForm.waitMinuteDefault,
startDate = setup.startDate,
mode = setup.realMode,
2018-11-29 02:43:52 -07:00
password = setup.password,
variant = setup.realVariant,
2019-12-13 07:30:20 -07:00
position =
DataForm.startingPosition(setup.position | chess.StartingPosition.initial.fen, setup.realVariant),
2019-10-02 10:50:09 -06:00
berserkable = setup.berserkable | true,
2019-10-04 11:12:26 -06:00
teamBattle = setup.teamBattleByTeam map TeamBattle.init
) |> { tour =>
2019-12-13 07:30:20 -07:00
tour.perfType.fold(tour) { perfType =>
tour.copy(conditions = setup.conditions.convert(perfType, myTeams.view.map(_.pair).toMap))
}
2019-12-13 07:30:20 -07:00
}
if (tour.name != me.titleUsername && lila.common.LameName.anyNameButLichessIsOk(tour.name))
2019-11-29 17:07:51 -07:00
Bus.publish(lila.hub.actorApi.slack.TournamentName(me.username, tour.id, tour.name), "slack")
2019-12-09 11:07:31 -07:00
tournamentRepo.insert(tour) >>
join(tour.id, me, tour.password, setup.teamBattleByTeam, getUserTeamIds, none) inject tour
}
2013-05-11 15:20:24 -06:00
private[tournament] def create(tournament: Tournament): Funit = {
2019-12-02 11:48:11 -07:00
tournamentRepo.insert(tournament).void
}
2014-04-08 14:17:33 -06:00
2019-10-02 10:50:09 -06:00
def teamBattleUpdate(
2019-12-13 07:30:20 -07:00
tour: Tournament,
data: TeamBattle.DataForm.Setup,
filterExistingTeamIds: Set[TeamID] => Fu[Set[TeamID]]
2019-10-02 10:50:09 -06:00
): Funit =
filterExistingTeamIds(data.potentialTeamIds) flatMap { teamIds =>
2019-12-02 11:48:11 -07:00
tournamentRepo.setTeamBattle(tour.id, TeamBattle(teamIds, data.nbLeaders))
2019-10-02 10:50:09 -06:00
}
private[tournament] def makePairings(forTour: Tournament, users: WaitingUsers): Funit =
Sequencing(forTour.id)(tournamentRepo.startedById) { tour =>
2019-12-13 07:30:20 -07:00
cached
.ranking(tour)
.mon(_.tournament.pairing.createRanking)
2019-12-13 07:30:20 -07:00
.flatMap { ranking =>
pairingSystem
.createPairings(tour, users, ranking)
.mon(_.tournament.pairing.createPairings)
.flatMap {
case Nil => funit
case pairings =>
playerRepo
.byTourAndUserIds(tour.id, pairings.flatMap(_.users))
.map {
_.view.map { player =>
player.userId -> player
}.toMap
}
.mon(_.tournament.pairing.createPlayerMap)
.flatMap { playersMap =>
pairings
.map { pairing =>
pairingRepo.insert(pairing) >>
autoPairing(tour, pairing, playersMap, ranking)
.mon(_.tournament.pairing.createAutoPairing)
.map {
socket.startGame(tour.id, _)
}
}
.sequenceFu
.mon(_.tournament.pairing.createInserts) >>
featureOneOf(tour, pairings, ranking)
.mon(_.tournament.pairing.createFeature) >>-
lila.mon.tournament.pairing.batchSize.record(pairings.size)
2019-12-13 07:30:20 -07:00
}
}
2015-06-11 09:03:45 -06:00
}
2019-12-10 14:01:18 -07:00
.monSuccess(_.tournament.pairing.create)
.chronometer
.logIfSlow(100, logger)(_ => s"Pairings for https://lichess.org/tournament/${tour.id}")
.result
}
2016-01-24 23:32:25 -07:00
private def featureOneOf(tour: Tournament, pairings: Pairings, ranking: Ranking): Funit =
2019-12-02 11:48:11 -07:00
tour.featuredId.ifTrue(pairings.nonEmpty) ?? pairingRepo.byId map2
2016-01-24 23:32:25 -07:00
RankedPairing(ranking) map (_.flatten) flatMap { curOption =>
2019-12-13 07:30:20 -07:00
pairings.flatMap(RankedPairing(ranking)).sortBy(_.bestRank).headOption ?? { bestCandidate =>
def switch = tournamentRepo.setFeaturedGameId(tour.id, bestCandidate.pairing.gameId)
curOption.filter(_.pairing.playing) match {
case Some(current) if bestCandidate.bestRank < current.bestRank => switch
case Some(_) => funit
case _ => switch
2016-01-24 23:32:25 -07:00
}
}
2019-12-13 07:30:20 -07:00
}
2016-01-24 23:32:25 -07:00
2019-12-13 07:30:20 -07:00
def tourAndRanks(game: Game): Fu[Option[TourAndRanks]] = ~ {
for {
2019-12-13 07:30:20 -07:00
tourId <- game.tournamentId
whiteId <- game.whitePlayer.userId
blackId <- game.blackPlayer.userId
2019-12-02 11:48:11 -07:00
} 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)
}
}
}
}
}
2019-12-09 11:07:31 -07:00
private[tournament] def start(oldTour: Tournament): Funit =
2019-12-02 11:48:11 -07:00
Sequencing(oldTour.id)(tournamentRepo.createdById) { tour =>
tournamentRepo.setStatus(tour.id, Status.Started) >>-
2019-10-19 15:14:46 -06:00
socket.reload(tour.id) >>-
2015-06-11 09:03:45 -06:00
publish()
}
2013-05-11 15:20:24 -06:00
2019-12-09 11:07:31 -07:00
private[tournament] def destroy(tour: Tournament): Funit =
2019-12-02 11:48:11 -07:00
tournamentRepo.remove(tour).void >>
pairingRepo.removeByTour(tour.id) >>
playerRepo.removeByTour(tour.id) >>- publish() >>- socket.reload(tour.id)
2015-06-11 09:03:45 -06:00
2019-12-09 11:07:31 -07:00
private[tournament] def finish(oldTour: Tournament): Funit =
2019-12-02 11:48:11 -07:00
Sequencing(oldTour.id)(tournamentRepo.startedById) { tour =>
pairingRepo count tour.id flatMap {
2019-12-09 11:07:31 -07:00
case 0 => destroy(tour)
2019-12-13 07:30:20 -07:00
case _ =>
for {
_ <- tournamentRepo.setStatus(tour.id, Status.Finished)
_ <- playerRepo unWithdraw tour.id
_ <- pairingRepo removePlaying tour.id
winner <- playerRepo winner tour.id
_ <- winner.??(p => tournamentRepo.setWinnerId(tour.id, p.userId))
} yield {
callbacks.clearJsonViewCache(tour)
socket.finish(tour.id)
publish()
playerRepo withPoints tour.id foreach {
_ foreach { p =>
userRepo.incToints(p.userId, p.score)
}
}
awardTrophies(tour).logFailure(logger, _ => s"${tour.id} awardTrophies")
callbacks.indexLeaderboard(tour).logFailure(logger, _ => s"${tour.id} indexLeaderboard")
callbacks.clearWinnersCache(tour)
callbacks.clearTrophyCache(tour)
duelStore.remove(tour)
}
}
}
2019-12-09 11:07:31 -07:00
def kill(tour: Tournament): Funit =
if (tour.isStarted) finish(tour)
2019-12-09 11:07:31 -07:00
else if (tour.isCreated) destroy(tour)
else funit
private def awardTrophies(tour: Tournament): Funit = {
import lila.user.TrophyKind._
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
2019-12-02 11:48:11 -07:00
playerRepo.bestByTourWithRank(tour.id, 100).flatMap {
_.map {
2019-12-13 07:30:20 -07:00
case rp if rp.rank == 1 => trophyApi.award(rp.player.userId, marathonWinner)
case rp if rp.rank <= 10 => trophyApi.award(rp.player.userId, marathonTopTen)
case rp if rp.rank <= 50 => trophyApi.award(rp.player.userId, marathonTopFifty)
2019-12-13 07:30:20 -07:00
case rp => trophyApi.award(rp.player.userId, marathonTopHundred)
}.sequenceFu.void
}
}
}
2015-06-11 09:03:45 -06:00
2019-12-13 07:30:20 -07:00
def verdicts(
tour: Tournament,
me: Option[User],
getUserTeamIds: User => Fu[List[TeamID]]
): Fu[Condition.All.WithVerdicts] = me match {
case None => fuccess(tour.conditions.accepted)
case Some(user) => verify(tour.conditions, user, getUserTeamIds)
2016-06-18 04:32:55 -06:00
}
2019-12-09 11:07:31 -07:00
private[tournament] def join(
2019-12-13 07:30:20 -07:00
tourId: Tournament.ID,
me: User,
password: Option[String],
withTeamId: Option[String],
getUserTeamIds: User => Fu[List[TeamID]],
promise: Option[Promise[Boolean]]
2019-12-09 11:07:31 -07:00
): Funit = Sequencing(tourId)(tournamentRepo.enterableById) { tour =>
2019-10-03 06:58:59 -06:00
val fuJoined =
if (tour.password == password) {
verdicts(tour, me.some, getUserTeamIds) flatMap {
_.accepted ?? {
pause.canJoin(me.id, tour) ?? {
def proceedWithTeam(team: Option[String]) =
2019-12-09 11:07:31 -07:00
tournamentRepo.tourIdsToWithdrawWhenEntering(tourId).flatMap(withdrawMany(_, me.id)) >>
playerRepo.join(tour.id, me, tour.perfLens, team) >>
updateNbPlayers(tour.id) >>- {
2019-12-13 07:30:20 -07:00
socket.reload(tour.id)
publish()
} inject true
2019-10-03 06:58:59 -06:00
withTeamId match {
2019-10-05 13:52:28 -06:00
case None if !tour.isTeamBattle => proceedWithTeam(none)
case None if tour.isTeamBattle =>
2019-12-02 11:48:11 -07:00
playerRepo.exists(tour.id, me.id) flatMap {
2019-12-13 07:30:20 -07:00
case true => proceedWithTeam(none)
2019-10-05 13:52:28 -06:00
case false => fuccess(false)
}
2019-12-13 07:30:20 -07:00
case Some(team) =>
tour.teamBattle match {
case Some(battle) if battle.teams contains team =>
getUserTeamIds(me) flatMap { myTeams =>
if (myTeams has team) proceedWithTeam(team.some)
// else proceedWithTeam(team.some) // listress
else fuccess(false)
}
case _ => fuccess(false)
}
2019-10-03 06:58:59 -06:00
}
}
2016-06-17 08:56:44 -06:00
}
}
} else {
2019-10-19 15:14:46 -06:00
socket.reload(tour.id)
2019-10-03 06:58:59 -06:00
fuccess(false)
}
2019-12-13 07:30:20 -07:00
fuJoined map { joined =>
promise.foreach(_ success joined)
}
}
def joinWithResult(
2019-12-13 07:30:20 -07:00
tourId: Tournament.ID,
me: User,
password: Option[String],
teamId: Option[String],
getUserTeamIds: User => Fu[List[TeamID]]
): Fu[Boolean] = {
val promise = Promise[Boolean]
2019-10-03 06:58:59 -06:00
join(tourId, me, password, teamId, getUserTeamIds, promise.some)
promise.future.withTimeoutDefault(5.seconds, false)
}
2018-01-08 11:03:17 -07:00
def pageOf(tour: Tournament, userId: User.ID): Fu[Option[Int]] =
cached ranking tour map {
_ get userId map { rank =>
2018-01-09 21:29:23 -07:00
(Math.floor(rank / 10) + 1).toInt
2018-01-08 11:03:17 -07:00
}
}
private def updateNbPlayers(tourId: Tournament.ID): Funit =
2019-12-02 17:42:57 -07:00
playerRepo count tourId flatMap { tournamentRepo.setNbPlayers(tourId, _) }
2019-12-09 11:07:31 -07:00
def selfPause(tourId: Tournament.ID, userId: User.ID): Funit =
2019-08-24 15:34:40 -06:00
withdraw(tourId, userId, isPause = true, isStalling = false)
2018-04-04 17:54:13 -06:00
2019-12-09 11:07:31 -07:00
private def stallPause(tourId: Tournament.ID, userId: User.ID): Funit =
2019-08-24 15:34:40 -06:00
withdraw(tourId, userId, isPause = false, isStalling = true)
2019-12-09 11:07:31 -07:00
private def withdraw(tourId: Tournament.ID, userId: User.ID, isPause: Boolean, isStalling: Boolean): Funit =
2019-12-02 17:42:57 -07:00
Sequencing(tourId)(tournamentRepo.enterableById) {
2015-06-11 09:03:45 -06:00
case tour if tour.isCreated =>
2019-12-02 11:48:11 -07:00
playerRepo.remove(tour.id, userId) >> updateNbPlayers(tour.id) >>- socket.reload(tour.id) >>- publish()
2019-12-13 07:30:20 -07:00
case tour if tour.isStarted =>
for {
_ <- playerRepo.withdraw(tour.id, userId)
pausable <- if (isPause) cached.ranking(tour).map { _ get userId exists (7 >) } else
fuccess(isStalling)
} yield {
if (pausable) pause.add(userId)
socket.reload(tour.id)
publish()
}
2015-06-11 09:03:45 -06:00
case _ => funit
}
2013-05-11 15:20:24 -06:00
2019-12-09 11:07:31 -07:00
private def withdrawMany(tourIds: List[Tournament.ID], userId: User.ID): Funit =
playerRepo.filterExists(tourIds, userId) flatMap {
_.map {
withdraw(_, userId, isPause = false, isStalling = false)
}.sequenceFu.void
2016-03-01 18:27:36 -07:00
}
2019-12-09 11:07:31 -07:00
def withdrawAll(user: User): Funit =
tournamentRepo.nonEmptyEnterableIds flatMap { withdrawMany(_, user.id) }
private[tournament] def berserk(gameId: Game.ID, userId: User.ID): Funit =
proxyRepo game gameId flatMap {
_.filter(_.berserkable) ?? { game =>
game.tournamentId ?? { tourId =>
2019-12-02 17:42:57 -07:00
Sequencing(tourId)(tournamentRepo.startedById) { tour =>
2019-12-02 11:48:11 -07:00
pairingRepo.findPlaying(tour.id, userId) flatMap {
2019-08-20 07:35:41 -06:00
case Some(pairing) if !pairing.berserkOf(userId) =>
(pairing colorOf userId) ?? { color =>
2019-12-02 11:48:11 -07:00
pairingRepo.setBerserk(pairing, userId) >>-
2019-10-31 12:04:37 -06:00
tellRound(gameId, GoBerserk(color))
2015-01-10 16:18:39 -07:00
}
2019-08-20 07:35:41 -06:00
case _ => funit
}
2015-01-10 16:18:39 -07:00
}
2015-06-17 12:56:02 -06:00
}
2015-01-10 16:18:39 -07:00
}
}
2019-12-09 11:07:31 -07:00
private[tournament] def finishGame(game: Game): Funit = game.tournamentId ?? { tourId =>
Sequencing(tourId)(tournamentRepo.startedById) { tour =>
pairingRepo.finish(game) >>
game.userIds.map(updatePlayer(tour, game.some)).sequenceFu.void >>- {
2019-12-13 07:30:20 -07:00
duelStore.remove(game)
socket.reload(tour.id)
updateTournamentStanding(tour.id)
withdrawNonMover(game)
}
2013-05-12 05:27:05 -06:00
}
2019-12-09 11:07:31 -07:00
}
2014-04-27 16:03:07 -06:00
2019-12-09 11:07:31 -07:00
private[tournament] def sittingDetected(game: Game, player: User.ID): Funit =
game.tournamentId ?? { stallPause(_, player) }
2019-08-24 15:34:40 -06:00
2018-01-09 07:43:27 -07:00
private def updatePlayer(
2019-12-13 07:30:20 -07:00
tour: Tournament,
finishing: Option[Game] // if set, update the player performance. Leave to none to just recompute the sheet.
2018-01-12 08:00:54 -07:00
)(userId: User.ID): Funit =
2019-12-02 17:42:57 -07:00
(tour.perfType.ifTrue(tour.mode.rated) ?? { userRepo.perfOf(userId, _) }) flatMap { perf =>
2019-12-02 11:48:11 -07:00
playerRepo.update(tour.id, userId) { player =>
cached.sheet.update(tour, userId) map { sheet =>
2015-06-11 16:54:51 -06:00
player.copy(
score = sheet.total,
fire = sheet.onFire,
rating = perf.fold(player.rating)(_.intRating),
provisional = perf.fold(player.provisional)(_.provisional),
performance = {
for {
2019-12-13 07:30:20 -07:00
g <- finishing
2018-01-09 07:10:06 -07:00
performance <- performanceOf(g, userId).map(_.toDouble)
nbGames = sheet.scores.size
if nbGames > 0
} yield Math.round {
player.performance * (nbGames - 1) / nbGames + performance / nbGames
2018-01-09 07:10:06 -07:00
} toInt
} | player.performance
)
2015-06-11 16:54:51 -06:00
}
}
}
2019-12-13 07:30:20 -07:00
private def performanceOf(g: Game, userId: String): Option[Int] =
for {
opponent <- g.opponentByUserId(userId)
opponentRating <- opponent.rating
multiplier = g.winnerUserId.??(winner => if (winner == userId) 1 else -1)
} yield opponentRating + 500 * multiplier
2019-12-13 07:30:20 -07:00
@silent private def withdrawNonMover(game: Game): Unit =
for {
tourId <- game.tournamentId
if game.status == chess.Status.NoStart
player <- game.playerWhoDidNotMove
userId <- player.userId
} withdraw(tourId, userId, isPause = false, isStalling = false)
2018-01-12 08:00:54 -07:00
def pausePlaybanned(userId: User.ID) =
2019-12-02 11:48:11 -07:00
tournamentRepo.startedIds flatMap {
playerRepo.filterExists(_, userId) flatMap {
2018-05-02 12:13:28 -06:00
_.map { tourId =>
2019-12-02 11:48:11 -07:00
playerRepo.withdraw(tourId, userId) >>- socket.reload(tourId) >>- publish()
2018-05-02 12:13:28 -06:00
}.sequenceFu.void
}
2017-10-19 22:02:55 -06:00
}
2019-12-09 11:07:31 -07:00
def ejectLame(userId: User.ID, playedIds: List[Tournament.ID]): Funit =
tournamentRepo.nonEmptyEnterableIds flatMap {
playerRepo.filterExists(_, userId) flatMap { enteredIds =>
(enteredIds ++ playedIds)
.map { ejectLame(_, userId) }
2019-12-13 07:30:20 -07:00
.sequenceFu
.void
2018-05-02 12:13:28 -06:00
}
}
// withdraws the player and forfeits all pairings in ongoing tournaments
2019-12-09 11:07:31 -07:00
private def ejectLame(tourId: Tournament.ID, userId: User.ID): Funit =
2019-12-02 11:48:11 -07:00
Sequencing(tourId)(tournamentRepo.byId) { tour =>
playerRepo.withdraw(tourId, userId) >> {
if (tour.isStarted)
2019-12-02 11:48:11 -07:00
pairingRepo.findPlaying(tour.id, userId).map {
_ foreach { currentPairing =>
2019-10-31 12:04:37 -06:00
tellRound(currentPairing.gameId, AbortForce)
}
2019-12-02 11:48:11 -07:00
} >> pairingRepo.opponentsOf(tour.id, userId).flatMap { uids =>
pairingRepo.forfeitByTourAndUserId(tour.id, userId) >>
2019-07-13 15:38:21 -06:00
lila.common.Future.applySequentially(uids.toList)(updatePlayer(tour, none))
2019-12-13 07:30:20 -07:00
} else if (tour.isFinished && tour.winnerId.contains(userId))
2019-12-02 11:48:11 -07:00
playerRepo winner tour.id flatMap {
_ ?? { p =>
2019-12-02 11:48:11 -07:00
tournamentRepo.setWinnerId(tour.id, p.userId)
}
2019-12-13 07:30:20 -07:00
} else funit
} >>
updateNbPlayers(tour.id) >>-
2019-10-19 15:14:46 -06:00
socket.reload(tour.id) >>- publish()
2015-06-11 09:03:45 -06:00
}
private val tournamentTopCache = cacheApi[Tournament.ID, TournamentTop](16, "tournament.top") {
_.refreshAfterWrite(3 second)
2019-12-23 21:08:41 -07:00
.expireAfterAccess(5 minutes)
.maximumSize(64)
2019-12-23 18:01:45 -07:00
.buildAsyncFuture { id =>
playerRepo.bestByTour(id, 20) dmap TournamentTop.apply
}
}
2017-08-17 16:07:43 -06:00
def tournamentTop(tourId: Tournament.ID): Fu[TournamentTop] =
tournamentTopCache get tourId
def miniView(game: Game, withTop: Boolean): Fu[Option[TourMiniView]] =
withTeamVs(game) flatMap {
_ ?? {
case TourAndTeamVs(tour, teamVs) =>
withTop ?? { tournamentTop(tour.id) map some } map {
TourMiniView(tour, _, teamVs).some
}
}
}
def withTeamVs(game: Game): Fu[Option[TourAndTeamVs]] =
game.tournamentId ?? tournamentRepo.byId flatMap {
2015-06-11 16:54:51 -06:00
_ ?? { tour =>
(tour.isTeamBattle ?? playerRepo.teamVs(tour.id, game)) map {
TourAndTeamVs(tour, _).some
}
2015-06-11 16:54:51 -06:00
}
}
2015-06-18 06:15:07 -06:00
def fetchVisibleTournaments: Fu[VisibleTournaments] =
2019-12-02 11:48:11 -07:00
tournamentRepo.publicCreatedSorted(6 * 60) zip
tournamentRepo.publicStarted zip
tournamentRepo.finishedNotable(30) map {
2019-12-13 07:30:20 -07:00
case ((created, started), finished) =>
VisibleTournaments(created, started, finished)
}
2015-06-18 06:15:07 -06:00
2019-10-04 14:46:43 -06:00
def playerInfo(tour: Tournament, userId: User.ID): Fu[Option[PlayerInfoExt]] =
2019-12-02 17:42:57 -07:00
userRepo named userId flatMap {
_ ?? { user =>
2019-12-02 11:48:11 -07:00
playerRepo.find(tour.id, user.id) flatMap {
2019-10-04 14:46:43 -06:00
_ ?? { player =>
playerPovs(tour, user.id, 50) map { povs =>
PlayerInfoExt(user, player, povs).some
2015-10-02 16:29:56 -06:00
}
}
}
}
}
2017-08-17 16:07:43 -06:00
def allCurrentLeadersInStandard: Fu[Map[Tournament, TournamentTop]] =
2019-12-02 11:48:11 -07:00
tournamentRepo.standardPublicStartedFromSecondary.flatMap { tours =>
2019-12-13 07:30:20 -07:00
tours
.map { tour =>
tournamentTop(tour.id) map (tour -> _)
}
.sequenceFu
.map(_.toMap)
}
2017-12-11 08:51:09 -07:00
def calendar: Fu[List[Tournament]] = {
val from = DateTime.now.minusDays(1)
2019-12-02 11:48:11 -07:00
tournamentRepo.calendar(from = from, to = from plusYears 1)
2017-12-11 08:51:09 -07:00
}
2019-12-02 17:42:57 -07:00
def resultStream(tour: Tournament, perSecond: MaxPerSecond, nb: Int): Source[Player.Result, _] =
2019-12-13 07:30:20 -07:00
playerRepo
.sortedCursor(tour.id, perSecond.value)
.documentSource(nb)
2019-12-08 20:20:36 -07:00
.throttle(perSecond.value, 1 second)
2019-12-02 17:42:57 -07:00
.zipWithIndex
.mapAsync(8) {
case (player, index) =>
lightUserApi.async(player.userId) map { lu =>
Player.Result(player, lu | LightUser.fallback(player.userId), index.toInt + 1)
}
}
2019-12-02 17:42:57 -07:00
def byOwnerStream(owner: User, perSecond: MaxPerSecond, nb: Int): Source[Tournament, _] =
2019-12-13 07:30:20 -07:00
tournamentRepo
.sortedCursor(owner, perSecond.value)
.documentSource(nb)
2019-12-08 20:20:36 -07:00
.throttle(perSecond.value, 1 second)
2018-04-05 09:18:11 -06:00
private def playerPovs(tour: Tournament, userId: User.ID, nb: Int): Fu[List[LightPov]] =
2019-12-02 11:48:11 -07:00
pairingRepo.recentIdsByTourAndUserId(tour.id, userId, nb) flatMap
2019-12-02 17:42:57 -07:00
gameRepo.light.gamesFromPrimary map {
2019-12-13 07:30:20 -07:00
_ flatMap { LightPov.ofUserId(_, userId) }
}
2015-10-02 14:52:00 -06:00
2019-12-13 07:30:20 -07:00
private def Sequencing(
tourId: Tournament.ID
)(fetch: Tournament.ID => Fu[Option[Tournament]])(run: Tournament => Funit): Funit =
workQueue(tourId) {
fetch(tourId) flatMap {
_ ?? run
}
}
2015-06-11 09:03:45 -06:00
private object publish {
2019-12-13 07:30:20 -07:00
private val debouncer = system.actorOf(Props(new Debouncer(15 seconds, { (_: Debouncer.Nothing) =>
fetchVisibleTournaments flatMap apiJsonView.apply foreach { json =>
Bus.publish(
SendToFlag("tournament", Json.obj("t" -> "reload", "d" -> json)),
"sendToFlag"
)
}
tournamentRepo.promotable foreach { tours =>
renderer.actor ? Tournament.TournamentTable(tours) map {
case view: String => Bus.publish(ReloadTournaments(view), "lobbySocket")
}
2019-12-13 07:30:20 -07:00
}
})))
2017-10-21 14:06:14 -06:00
def apply(): Unit = { debouncer ! Debouncer.Nothing }
2013-05-11 15:20:24 -06:00
}
private object updateTournamentStanding {
2017-01-14 02:58:15 -07:00
import lila.hub.EarlyMultiThrottler
// last published top hashCode
private val lastPublished = lila.memo.CacheApi.scaffeineNoScheduler
2019-12-24 13:01:35 -07:00
.initialCapacity(16)
.expireAfterWrite(2 minute)
.build[Tournament.ID, Int]
2017-01-14 02:58:15 -07:00
2017-08-17 16:07:43 -06:00
private def publishNow(tourId: Tournament.ID) = tournamentTop(tourId) map { top =>
val lastHash: Int = ~lastPublished.getIfPresent(tourId)
2019-11-06 02:14:56 -07:00
if (lastHash != top.hashCode) {
Bus.publish(
2019-11-06 02:14:56 -07:00
lila.hub.actorApi.round.TourStanding(tourId, JsonView.top(top, lightUserApi.sync)),
2019-11-29 17:07:51 -07:00
"tourStanding"
2019-11-06 02:14:56 -07:00
)
lastPublished.put(tourId, top.hashCode)
}
}
2017-01-14 02:58:15 -07:00
private val throttler = system.actorOf(Props(new EarlyMultiThrottler(logger = logger)))
def apply(tourId: Tournament.ID): Unit =
2019-12-29 14:44:19 -07:00
throttler ! EarlyMultiThrottler.Work(
2017-01-14 02:58:15 -07:00
id = tourId,
2019-12-29 14:44:19 -07:00
run = () => publishNow(tourId),
cooldown = 15.seconds
)
}
2013-05-11 15:20:24 -06:00
}
2019-12-02 17:42:57 -07:00
private object TournamentApi {
case class Callbacks(
clearJsonViewCache: Tournament => Unit,
clearWinnersCache: Tournament => Unit,
clearTrophyCache: Tournament => Unit,
indexLeaderboard: Tournament => Funit
)
}