356 lines
12 KiB
Scala
356 lines
12 KiB
Scala
package lila.tournament
|
|
|
|
import com.github.ghik.silencer.silent
|
|
import reactivemongo.akkastream.{ cursorProducer, AkkaStreamCursor }
|
|
import reactivemongo.api._
|
|
import reactivemongo.api.bson._
|
|
|
|
import BSONHandlers._
|
|
import lila.db.dsl._
|
|
import lila.hub.LightTeam.TeamID
|
|
import lila.rating.Perf
|
|
import lila.user.{ Perfs, User }
|
|
|
|
final class PlayerRepo(coll: Coll)(implicit ec: scala.concurrent.ExecutionContext) {
|
|
|
|
private def selectId(id: Tournament.ID) = $doc("_id" -> id)
|
|
private def selectTour(tourId: Tournament.ID) = $doc("tid" -> tourId)
|
|
private def selectTourUser(tourId: Tournament.ID, userId: User.ID) = $doc(
|
|
"tid" -> tourId,
|
|
"uid" -> userId
|
|
)
|
|
private val selectActive = $doc("w" $ne true)
|
|
private val selectWithdraw = $doc("w" -> true)
|
|
private val bestSort = $doc("m" -> -1)
|
|
|
|
def byId(id: Tournament.ID): Fu[Option[Player]] = coll.one[Player](selectId(id))
|
|
|
|
private[tournament] def bestByTour(tourId: Tournament.ID, nb: Int, skip: Int = 0): Fu[List[Player]] =
|
|
coll.ext.find(selectTour(tourId)).sort(bestSort).skip(skip).list[Player](nb)
|
|
|
|
private[tournament] def bestByTourWithRank(
|
|
tourId: Tournament.ID,
|
|
nb: Int,
|
|
skip: Int = 0
|
|
): Fu[RankedPlayers] =
|
|
bestByTour(tourId, nb, skip).map { res =>
|
|
res
|
|
.foldRight(List.empty[RankedPlayer] -> (res.size + skip)) {
|
|
case (p, (res, rank)) => (RankedPlayer(rank, p) :: res, rank - 1)
|
|
}
|
|
._1
|
|
}
|
|
|
|
private[tournament] def bestByTourWithRankByPage(
|
|
tourId: Tournament.ID,
|
|
nb: Int,
|
|
page: Int
|
|
): Fu[RankedPlayers] =
|
|
bestByTourWithRank(tourId, nb, (page - 1) * nb)
|
|
|
|
// very expensive
|
|
private[tournament] def bestTeamIdsByTour(
|
|
tourId: Tournament.ID,
|
|
battle: TeamBattle
|
|
): Fu[List[TeamBattle.RankedTeam]] = {
|
|
import TeamBattle.{ RankedTeam, TeamLeader }
|
|
coll
|
|
.aggregateList(maxDocs = 10) { framework =>
|
|
import framework._
|
|
Match(selectTour(tourId)) -> List(
|
|
Sort(Descending("m")),
|
|
GroupField("t")(
|
|
"m" -> Push(
|
|
$doc(
|
|
"u" -> "$uid",
|
|
"m" -> "$m"
|
|
)
|
|
)
|
|
),
|
|
Project(
|
|
$doc(
|
|
"p" -> $doc(
|
|
"$slice" -> $arr("$m", battle.nbLeaders)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
.map {
|
|
_.flatMap { doc =>
|
|
for {
|
|
teamId <- doc.getAsOpt[TeamID]("_id")
|
|
leadersBson <- doc.getAsOpt[List[Bdoc]]("p")
|
|
leaders = leadersBson.flatMap {
|
|
case p: Bdoc =>
|
|
for {
|
|
id <- p.getAsOpt[User.ID]("u")
|
|
magic <- p.int("m")
|
|
} yield TeamLeader(id, magic)
|
|
}
|
|
} yield RankedTeam(0, teamId, leaders)
|
|
}.sortBy(-_.magicScore).zipWithIndex map {
|
|
case (rt, pos) => rt.copy(rank = pos + 1)
|
|
}
|
|
} map { ranked =>
|
|
if (ranked.size == battle.teams.size) ranked
|
|
else
|
|
ranked ::: battle.teams
|
|
.foldLeft(List.empty[RankedTeam]) {
|
|
case (missing, team) if !ranked.exists(_.teamId == team) =>
|
|
RankedTeam(missing.headOption.fold(ranked.size)(_.rank) + 1, team, Nil) :: missing
|
|
case (acc, _) => acc
|
|
}
|
|
.reverse
|
|
}
|
|
}
|
|
|
|
// very expensive
|
|
private[tournament] def teamInfo(
|
|
tourId: Tournament.ID,
|
|
teamId: TeamID,
|
|
@silent battle: TeamBattle
|
|
): Fu[TeamBattle.TeamInfo] = {
|
|
coll
|
|
.aggregateWith[Bdoc]() { framework =>
|
|
import framework._
|
|
Match(selectTour(tourId) ++ $doc("t" -> teamId)) -> List(
|
|
Sort(Descending("m")),
|
|
Facet(
|
|
List(
|
|
"agg" -> {
|
|
Group(BSONNull)(
|
|
"nb" -> SumAll,
|
|
"rating" -> AvgField("r"),
|
|
"perf" -> AvgField("e"),
|
|
"score" -> AvgField("s")
|
|
) -> Nil
|
|
},
|
|
"topPlayers" -> {
|
|
Limit(50) -> Nil
|
|
}
|
|
)
|
|
)
|
|
)
|
|
}
|
|
.headOption
|
|
.map {
|
|
_.flatMap { doc =>
|
|
for {
|
|
aggs <- doc.getAsOpt[List[Bdoc]]("agg")
|
|
agg <- aggs.headOption
|
|
nbPlayers <- agg.int("nb")
|
|
rating = agg.double("rating").??(math.round)
|
|
perf = agg.double("perf").??(math.round)
|
|
score = agg.double("score").??(math.round)
|
|
topPlayers <- doc.getAsOpt[List[Player]]("topPlayers")
|
|
} yield TeamBattle.TeamInfo(teamId, nbPlayers, rating.toInt, perf.toInt, score.toInt, topPlayers)
|
|
} | TeamBattle.TeamInfo(teamId, 0, 0, 0, 0, Nil)
|
|
}
|
|
}
|
|
|
|
def bestTeamPlayers(tourId: Tournament.ID, teamId: TeamID, nb: Int): Fu[List[Player]] =
|
|
coll.ext.find($doc("tid" -> tourId, "t" -> teamId)).sort($sort desc "m").list[Player](nb)
|
|
|
|
def countTeamPlayers(tourId: Tournament.ID, teamId: TeamID): Fu[Int] =
|
|
coll.countSel($doc("tid" -> tourId, "t" -> teamId))
|
|
|
|
def teamsOfPlayers(tourId: Tournament.ID, userIds: List[User.ID]): Fu[List[(User.ID, TeamID)]] =
|
|
coll.ext
|
|
.find($doc("tid" -> tourId, "uid" $in userIds), $doc("_id" -> false, "uid" -> true, "t" -> true))
|
|
.list[Bdoc]()
|
|
.map {
|
|
_.flatMap { doc =>
|
|
doc.getAsOpt[User.ID]("uid") flatMap { userId =>
|
|
doc.getAsOpt[TeamID]("t") map { (userId, _) }
|
|
}
|
|
}
|
|
}
|
|
|
|
def teamVs(tourId: Tournament.ID, game: lila.game.Game): Fu[Option[TeamBattle.TeamVs]] =
|
|
game.twoUserIds ?? {
|
|
case (w, b) =>
|
|
teamsOfPlayers(tourId, List(w, b)).dmap(_.toMap) map { m =>
|
|
(m.get(w) |@| m.get(b)).tupled ?? {
|
|
case (wt, bt) => TeamBattle.TeamVs(chess.Color.Map(wt, bt)).some
|
|
}
|
|
}
|
|
}
|
|
|
|
def countActive(tourId: Tournament.ID): Fu[Int] =
|
|
coll.countSel(selectTour(tourId) ++ selectActive)
|
|
|
|
def count(tourId: Tournament.ID): Fu[Int] = coll.countSel(selectTour(tourId))
|
|
|
|
def removeByTour(tourId: Tournament.ID) = coll.delete.one(selectTour(tourId)).void
|
|
|
|
def remove(tourId: Tournament.ID, userId: User.ID) =
|
|
coll.delete.one(selectTourUser(tourId, userId)).void
|
|
|
|
def filterExists(tourIds: List[Tournament.ID], userId: User.ID): Fu[List[Tournament.ID]] =
|
|
coll.primitive[Tournament.ID](
|
|
$doc(
|
|
"tid" $in tourIds,
|
|
"uid" -> userId
|
|
),
|
|
"tid"
|
|
)
|
|
|
|
def existsActive(tourId: Tournament.ID, userId: User.ID) =
|
|
coll.exists(selectTourUser(tourId, userId) ++ selectActive)
|
|
|
|
def exists(tourId: Tournament.ID, userId: User.ID) =
|
|
coll.exists(selectTourUser(tourId, userId))
|
|
|
|
def unWithdraw(tourId: Tournament.ID) =
|
|
coll.update
|
|
.one(
|
|
selectTour(tourId) ++ selectWithdraw,
|
|
$doc("$unset" -> $doc("w" -> true)),
|
|
multi = true
|
|
)
|
|
.void
|
|
|
|
def find(tourId: Tournament.ID, userId: User.ID): Fu[Option[Player]] =
|
|
coll.ext.find(selectTourUser(tourId, userId)).one[Player]
|
|
|
|
def update(tourId: Tournament.ID, userId: User.ID)(f: Player => Fu[Player]) =
|
|
find(tourId, userId) orFail s"No such player: $tourId/$userId" flatMap f flatMap { player =>
|
|
coll.update.one(selectId(player._id), player).void
|
|
}
|
|
|
|
def join(tourId: Tournament.ID, user: User, perfLens: Perfs => Perf, team: Option[TeamID]) =
|
|
find(tourId, user.id) flatMap {
|
|
case Some(p) if p.withdraw => coll.update.one(selectId(p._id), $unset("w"))
|
|
case Some(_) => funit
|
|
case None => coll.insert.one(Player.make(tourId, user, perfLens, team))
|
|
} void
|
|
|
|
def withdraw(tourId: Tournament.ID, userId: User.ID) =
|
|
coll.update.one(selectTourUser(tourId, userId), $set("w" -> true)).void
|
|
|
|
private[tournament] def withPoints(tourId: Tournament.ID): Fu[List[Player]] =
|
|
coll.ext
|
|
.find(
|
|
selectTour(tourId) ++ $doc("m" $gt 0)
|
|
)
|
|
.list[Player]()
|
|
|
|
private[tournament] def userIds(tourId: Tournament.ID): Fu[List[User.ID]] =
|
|
coll.distinctEasy[User.ID, List]("uid", selectTour(tourId))
|
|
|
|
private[tournament] def nbActiveUserIds(tourId: Tournament.ID): Fu[Int] =
|
|
coll.countSel(selectTour(tourId) ++ selectActive)
|
|
|
|
def winner(tourId: Tournament.ID): Fu[Option[Player]] =
|
|
coll.ext.find(selectTour(tourId)).sort(bestSort).one[Player]
|
|
|
|
// freaking expensive (marathons)
|
|
private[tournament] def computeRanking(tourId: Tournament.ID): Fu[Ranking] =
|
|
coll
|
|
.aggregateWith[Bdoc]() { framework =>
|
|
import framework._
|
|
Match(selectTour(tourId)) -> List(
|
|
Sort(Descending("m")),
|
|
Group(BSONNull)("uids" -> PushField("uid"))
|
|
)
|
|
}
|
|
.headOption map {
|
|
_ ?? {
|
|
_ get "uids" match {
|
|
case Some(BSONArray(uids)) =>
|
|
// mutable optimized implementation
|
|
val b = Map.newBuilder[User.ID, Int]
|
|
var r = 0
|
|
for (u <- uids) {
|
|
b += (u.asInstanceOf[BSONString].value -> r)
|
|
r = r + 1
|
|
}
|
|
b.result
|
|
case _ => Map.empty
|
|
}
|
|
}
|
|
}
|
|
|
|
def computeRankOf(player: Player): Fu[Int] =
|
|
coll.countSel(selectTour(player.tourId) ++ $doc("m" $gt player.magicScore))
|
|
|
|
// expensive, cache it
|
|
private[tournament] def averageRating(tourId: Tournament.ID): Fu[Int] =
|
|
coll
|
|
.aggregateWith[Bdoc]() { framework =>
|
|
import framework._
|
|
Match(selectTour(tourId)) -> List(
|
|
Group(BSONNull)("rating" -> AvgField("r"))
|
|
)
|
|
}
|
|
.headOption map {
|
|
~_.flatMap(_.double("rating").map(_.toInt))
|
|
}
|
|
|
|
def byTourAndUserIds(tourId: Tournament.ID, userIds: Iterable[User.ID]): Fu[List[Player]] =
|
|
coll.ext
|
|
.find(selectTour(tourId) ++ $doc("uid" $in userIds))
|
|
.list[Player]()
|
|
.chronometer
|
|
.logIfSlow(200, logger) { players =>
|
|
s"PlayerRepo.byTourAndUserIds $tourId ${userIds.size} user IDs, ${players.size} players"
|
|
}
|
|
.result
|
|
|
|
def pairByTourAndUserIds(tourId: Tournament.ID, id1: User.ID, id2: User.ID): Fu[Option[(Player, Player)]] =
|
|
byTourAndUserIds(tourId, List(id1, id2)) map {
|
|
case List(p1, p2) if p1.is(id1) && p2.is(id2) => Some(p1 -> p2)
|
|
case List(p1, p2) if p1.is(id2) && p2.is(id1) => Some(p2 -> p1)
|
|
case _ => none
|
|
}
|
|
|
|
def setPerformance(player: Player, performance: Int) =
|
|
coll.update.one(selectId(player.id), $doc("$set" -> $doc("e" -> performance))).void
|
|
|
|
private def rankPlayers(players: List[Player], ranking: Ranking): RankedPlayers =
|
|
players
|
|
.flatMap { p =>
|
|
ranking get p.userId map { RankedPlayer(_, p) }
|
|
}
|
|
.sortBy(_.rank)
|
|
|
|
def rankedByTourAndUserIds(
|
|
tourId: Tournament.ID,
|
|
userIds: Iterable[User.ID],
|
|
ranking: Ranking
|
|
): Fu[RankedPlayers] =
|
|
byTourAndUserIds(tourId, userIds)
|
|
.map { rankPlayers(_, ranking) }
|
|
.chronometer
|
|
.logIfSlow(200, logger) { players =>
|
|
s"PlayerRepo.rankedByTourAndUserIds $tourId ${userIds.size} user IDs, ${ranking.size} ranking, ${players.size} players"
|
|
}
|
|
.result
|
|
|
|
def searchPlayers(tourId: Tournament.ID, term: String, nb: Int): Fu[List[User.ID]] =
|
|
User.couldBeUsername(term) ?? {
|
|
term.nonEmpty ?? coll.primitive[User.ID](
|
|
selector = $doc(
|
|
"tid" -> tourId,
|
|
"uid" $startsWith term.toLowerCase
|
|
),
|
|
sort = $sort desc "m",
|
|
nb = nb,
|
|
field = "uid"
|
|
)
|
|
}
|
|
|
|
private[tournament] def sortedCursor(
|
|
tournamentId: Tournament.ID,
|
|
batchSize: Int,
|
|
readPreference: ReadPreference = ReadPreference.secondaryPreferred
|
|
): AkkaStreamCursor[Player] =
|
|
coll.ext
|
|
.find(selectTour(tournamentId))
|
|
.sort($sort desc "m")
|
|
.batchSize(batchSize)
|
|
.cursor[Player](readPreference)
|
|
|
|
}
|