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._
|
2015-01-05 04:03:09 -07:00
|
|
|
import scala.concurrent.duration._
|
2018-12-30 20:22:51 -07:00
|
|
|
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 }
|
2015-07-26 05:18:56 -06:00
|
|
|
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
|
|
|
|
|
2016-09-05 02:22:12 -06:00
|
|
|
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,
|
2019-10-04 02:21:45 -06:00
|
|
|
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,
|
2015-06-05 12:31:55 -06:00
|
|
|
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
|
2019-12-27 12:49:59 -07:00
|
|
|
)(
|
|
|
|
implicit ec: scala.concurrent.ExecutionContext,
|
|
|
|
system: ActorSystem,
|
|
|
|
mat: akka.stream.Materializer,
|
|
|
|
mode: play.api.Mode
|
|
|
|
) {
|
2013-05-11 15:20:24 -06:00
|
|
|
|
2020-01-14 19:30:51 -07: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),
|
2017-08-21 15:27:08 -06:00
|
|
|
name = DataForm.canPickName(me) ?? setup.name,
|
2017-03-31 15:58:21 -06:00
|
|
|
clock = setup.clockConfig,
|
2015-06-11 09:03:45 -06:00
|
|
|
minutes = setup.minutes,
|
2018-05-10 17:01:30 -06:00
|
|
|
waitMinutes = setup.waitMinutes | DataForm.waitMinuteDefault,
|
2018-05-11 17:47:45 -06:00
|
|
|
startDate = setup.startDate,
|
2017-03-31 15:58:21 -06:00
|
|
|
mode = setup.realMode,
|
2018-11-29 02:43:52 -07:00
|
|
|
password = setup.password,
|
2017-03-31 15:58:21 -06:00
|
|
|
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
|
2018-04-13 17:59:51 -06:00
|
|
|
) |> { 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))
|
2018-04-13 17:59:51 -06:00
|
|
|
}
|
2019-12-13 07:30:20 -07:00
|
|
|
}
|
2018-11-04 05:10:56 -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
|
2014-05-10 15:27:06 -06:00
|
|
|
}
|
2013-05-11 15:20:24 -06:00
|
|
|
|
2018-08-15 12:45:15 -06:00
|
|
|
private[tournament] def create(tournament: Tournament): Funit = {
|
2019-12-02 11:48:11 -07:00
|
|
|
tournamentRepo.insert(tournament).void
|
2017-10-01 19:15:03 -06:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2019-12-15 16:51:21 -07: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)
|
2019-12-21 22:55:30 -07:00
|
|
|
.mon(_.tournament.pairing.createRanking)
|
2019-12-13 07:30:20 -07:00
|
|
|
.flatMap { ranking =>
|
2019-12-21 10:24:33 -07:00
|
|
|
pairingSystem
|
|
|
|
.createPairings(tour, users, ranking)
|
2019-12-21 22:55:30 -07:00
|
|
|
.mon(_.tournament.pairing.createPairings)
|
2019-12-21 10:24:33 -07:00
|
|
|
.flatMap {
|
|
|
|
case Nil => funit
|
|
|
|
case pairings =>
|
2019-12-21 23:49:10 -07:00
|
|
|
playerRepo
|
|
|
|
.byTourAndUserIds(tour.id, pairings.flatMap(_.users))
|
|
|
|
.map {
|
|
|
|
_.view.map { player =>
|
|
|
|
player.userId -> player
|
|
|
|
}.toMap
|
|
|
|
}
|
|
|
|
.mon(_.tournament.pairing.createPlayerMap)
|
|
|
|
.flatMap { playersMap =>
|
2019-12-21 22:55:30 -07:00
|
|
|
pairings
|
|
|
|
.map { pairing =>
|
|
|
|
pairingRepo.insert(pairing) >>
|
2019-12-21 23:49:10 -07:00
|
|
|
autoPairing(tour, pairing, playersMap, ranking)
|
2019-12-21 22:55:30 -07:00
|
|
|
.mon(_.tournament.pairing.createAutoPairing)
|
|
|
|
.map {
|
|
|
|
socket.startGame(tour.id, _)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.sequenceFu
|
|
|
|
.mon(_.tournament.pairing.createInserts) >>
|
|
|
|
featureOneOf(tour, pairings, ranking)
|
|
|
|
.mon(_.tournament.pairing.createFeature) >>-
|
2019-12-21 10:24:33 -07:00
|
|
|
lila.mon.tournament.pairing.batchSize.record(pairings.size)
|
2019-12-13 07:30:20 -07:00
|
|
|
}
|
2019-12-21 10:24:33 -07:00
|
|
|
}
|
2015-06-11 09:03:45 -06:00
|
|
|
}
|
2019-12-10 14:01:18 -07:00
|
|
|
.monSuccess(_.tournament.pairing.create)
|
2019-12-09 08:28:04 -07:00
|
|
|
.chronometer
|
|
|
|
.logIfSlow(100, logger)(_ => s"Pairings for https://lichess.org/tournament/${tour.id}")
|
|
|
|
.result
|
2014-05-10 15:27:06 -06:00
|
|
|
}
|
2013-05-11 15:45:39 -06:00
|
|
|
|
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]] = ~ {
|
2016-01-22 22:18:10 -07:00
|
|
|
for {
|
2019-12-13 07:30:20 -07:00
|
|
|
tourId <- game.tournamentId
|
2016-01-22 22:18:10 -07:00
|
|
|
whiteId <- game.whitePlayer.userId
|
|
|
|
blackId <- game.blackPlayer.userId
|
2019-12-02 11:48:11 -07:00
|
|
|
} yield tournamentRepo byId tourId flatMap {
|
2016-01-22 22:18:10 -07:00
|
|
|
_ ?? { 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)
|
2020-01-21 14:30:50 -07:00
|
|
|
duelStore.remove(tour)
|
2014-05-10 15:27:06 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 11:07:31 -07:00
|
|
|
def kill(tour: Tournament): Funit =
|
2017-08-24 15:13:14 -06:00
|
|
|
if (tour.isStarted) finish(tour)
|
2019-12-09 11:07:31 -07:00
|
|
|
else if (tour.isCreated) destroy(tour)
|
|
|
|
else funit
|
2017-08-24 15:13:14 -06:00
|
|
|
|
2019-06-29 09:19:12 -06:00
|
|
|
private def awardTrophies(tour: Tournament): Funit = {
|
|
|
|
import lila.user.TrophyKind._
|
2015-07-26 06:18:28 -06:00
|
|
|
tour.schedule.??(_.freq == Schedule.Freq.Marathon) ?? {
|
2019-12-02 11:48:11 -07:00
|
|
|
playerRepo.bestByTourWithRank(tour.id, 100).flatMap {
|
2015-07-26 06:18:28 -06:00
|
|
|
_.map {
|
2019-12-13 07:30:20 -07:00
|
|
|
case rp if rp.rank == 1 => trophyApi.award(rp.player.userId, marathonWinner)
|
2019-06-29 09:19:12 -06:00
|
|
|
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)
|
2015-07-26 06:18:28 -06:00
|
|
|
}.sequenceFu.void
|
|
|
|
}
|
|
|
|
}
|
2019-06-29 09:19:12 -06:00
|
|
|
}
|
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)
|
2018-07-19 06:41:32 -06:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2019-11-23 18:09:33 -07: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)
|
2017-01-09 01:44:13 -07:00
|
|
|
}
|
2018-08-25 22:33:16 -06:00
|
|
|
}
|
2017-01-09 01:44:13 -07:00
|
|
|
|
2018-12-30 20:22:51 -07:00
|
|
|
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]]
|
2018-12-30 20:22:51 -07:00
|
|
|
): Fu[Boolean] = {
|
|
|
|
val promise = Promise[Boolean]
|
2019-10-03 06:58:59 -06:00
|
|
|
join(tourId, me, password, teamId, getUserTeamIds, promise.some)
|
2019-12-13 18:17:43 -07:00
|
|
|
promise.future.withTimeoutDefault(5.seconds, false)
|
2018-08-25 22:33:16 -06:00
|
|
|
}
|
2015-06-05 09:14:37 -06:00
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-23 17:58:22 -07:00
|
|
|
private def updateNbPlayers(tourId: Tournament.ID): Funit =
|
2019-12-02 17:42:57 -07:00
|
|
|
playerRepo count tourId flatMap { tournamentRepo.setNbPlayers(tourId, _) }
|
2015-06-12 07:15:35 -06:00
|
|
|
|
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
|
2014-05-10 15:27:06 -06:00
|
|
|
}
|
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 =>
|
2017-04-04 17:47:38 -06:00
|
|
|
cached.sheet.update(tour, userId) map { sheet =>
|
2015-06-11 16:54:51 -06:00
|
|
|
player.copy(
|
|
|
|
score = sheet.total,
|
|
|
|
fire = sheet.onFire,
|
2018-01-10 10:31:24 -07:00
|
|
|
rating = perf.fold(player.rating)(_.intRating),
|
2018-01-08 21:47:20 -07:00
|
|
|
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)
|
2018-01-08 21:47:20 -07:00
|
|
|
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
|
2018-01-08 21:47:20 -07:00
|
|
|
} | player.performance
|
2018-01-10 08:59:13 -07:00
|
|
|
)
|
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
|
2018-01-08 21:47:20 -07:00
|
|
|
|
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)
|
2017-07-31 18:32:52 -06:00
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2014-05-10 15:27:06 -06:00
|
|
|
|
2019-11-23 18:09:33 -07: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) >> {
|
2016-07-14 17:23:33 -06:00
|
|
|
if (tour.isStarted)
|
2019-12-02 11:48:11 -07:00
|
|
|
pairingRepo.findPlaying(tour.id, userId).map {
|
2016-08-30 09:20:43 -06:00
|
|
|
_ foreach { currentPairing =>
|
2019-10-31 12:04:37 -06:00
|
|
|
tellRound(currentPairing.gameId, AbortForce)
|
2016-08-30 09:20:43 -06:00
|
|
|
}
|
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 {
|
2016-07-14 17:23:33 -06:00
|
|
|
_ ?? { p =>
|
2019-12-02 11:48:11 -07:00
|
|
|
tournamentRepo.setWinnerId(tour.id, p.userId)
|
2016-07-14 17:23:33 -06:00
|
|
|
}
|
2019-12-13 07:30:20 -07:00
|
|
|
} else funit
|
2016-07-14 17:23:33 -06:00
|
|
|
} >>
|
2015-06-21 08:24:08 -06:00
|
|
|
updateNbPlayers(tour.id) >>-
|
2019-10-19 15:14:46 -06:00
|
|
|
socket.reload(tour.id) >>- publish()
|
2015-06-11 09:03:45 -06:00
|
|
|
}
|
|
|
|
|
2019-12-24 07:31:19 -07: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
|
|
|
|
}
|
|
|
|
}
|
2015-06-14 09:20:43 -06:00
|
|
|
|
2017-08-17 16:07:43 -06:00
|
|
|
def tournamentTop(tourId: Tournament.ID): Fu[TournamentTop] =
|
|
|
|
tournamentTopCache get tourId
|
|
|
|
|
2020-02-10 12:53:48 -07:00
|
|
|
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 =>
|
2020-02-10 12:53:48 -07:00
|
|
|
(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 {
|
2015-10-07 09:05:39 -06:00
|
|
|
_ ?? { 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-05-02 07:56:53 -06:00
|
|
|
}
|
|
|
|
|
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)
|
2020-01-14 21:41:51 -07:00
|
|
|
.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)
|
|
|
|
}
|
2018-11-19 22:15:33 -07:00
|
|
|
}
|
|
|
|
|
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)
|
2020-01-14 21:41:51 -07:00
|
|
|
.documentSource(nb)
|
2019-12-08 20:20:36 -07:00
|
|
|
.throttle(perSecond.value, 1 second)
|
2019-10-04 02:21:45 -06:00
|
|
|
|
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 =
|
2019-12-09 16:24:43 -07:00
|
|
|
workQueue(tourId) {
|
2018-08-25 22:33:16 -06:00
|
|
|
fetch(tourId) flatMap {
|
2019-12-16 08:11:56 -07:00
|
|
|
_ ?? run
|
2018-08-25 22:33:16 -06:00
|
|
|
}
|
2018-08-25 04:29:58 -06:00
|
|
|
}
|
2015-06-11 09:03:45 -06:00
|
|
|
|
2015-01-05 04:03:09 -07: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")
|
2015-01-05 04:03:09 -07:00
|
|
|
}
|
2019-12-13 07:30:20 -07:00
|
|
|
}
|
2015-01-05 04:03:09 -07:00
|
|
|
})))
|
2017-10-21 14:06:14 -06:00
|
|
|
def apply(): Unit = { debouncer ! Debouncer.Nothing }
|
2013-05-11 15:20:24 -06:00
|
|
|
}
|
|
|
|
|
2015-05-25 06:46:31 -06:00
|
|
|
private object updateTournamentStanding {
|
2017-01-14 02:58:15 -07:00
|
|
|
|
|
|
|
import lila.hub.EarlyMultiThrottler
|
2017-08-18 04:27:23 -06:00
|
|
|
|
|
|
|
// last published top hashCode
|
2019-12-27 12:49:59 -07:00
|
|
|
private val lastPublished = lila.memo.CacheApi.scaffeineNoScheduler
|
2019-12-24 13:01:35 -07:00
|
|
|
.initialCapacity(16)
|
2017-08-18 04:27:23 -06:00
|
|
|
.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 =>
|
2017-08-18 04:27:23 -06:00
|
|
|
val lastHash: Int = ~lastPublished.getIfPresent(tourId)
|
2019-11-06 02:14:56 -07:00
|
|
|
if (lastHash != top.hashCode) {
|
2019-11-26 14:44:08 -07:00
|
|
|
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)
|
|
|
|
}
|
2015-05-25 06:46:31 -06:00
|
|
|
}
|
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),
|
2017-02-14 08:34:07 -07:00
|
|
|
cooldown = 15.seconds
|
|
|
|
)
|
2015-05-25 06:46:31 -06:00
|
|
|
}
|
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
|
|
|
|
)
|
|
|
|
}
|