lila/modules/game/src/main/GameRepo.scala

559 lines
18 KiB
Scala

package lila.game
import scala.concurrent.duration._
import chess.format.{ FEN, Forsyth }
import chess.{ Color, Status }
import org.joda.time.DateTime
import reactivemongo.akkastream.{ cursorProducer, AkkaStreamCursor }
import reactivemongo.api.commands.WriteResult
import reactivemongo.api.{ Cursor, ReadPreference, WriteConcern }
import lila.common.ThreadLocalRandom
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.db.isDuplicateKey
import lila.user.User
final class GameRepo(val coll: Coll)(implicit ec: scala.concurrent.ExecutionContext) {
import BSONHandlers._
import Game.{ ID, BSONFields => F }
import Player.holdAlertBSONHandler
val fixedColorLobbyCache = new lila.memo.ExpireSetMemo(2 hours)
def game(gameId: ID): Fu[Option[Game]] = coll.byId[Game](gameId)
def gameFromSecondary(gameId: ID): Fu[Option[Game]] = coll.secondaryPreferred.byId[Game](gameId)
def gamesFromSecondary(gameIds: Seq[ID]): Fu[List[Game]] =
coll.byOrderedIds[Game, ID](gameIds, readPreference = ReadPreference.secondaryPreferred)(_.id)
def gameOptionsFromSecondary(gameIds: Seq[ID]): Fu[List[Option[Game]]] =
coll.optionsByOrderedIds[Game, ID](gameIds, none, ReadPreference.secondaryPreferred)(_.id)
object light {
def game(gameId: ID): Fu[Option[LightGame]] = coll.byId[LightGame](gameId, LightGame.projection)
def pov(gameId: ID, color: Color): Fu[Option[LightPov]] =
game(gameId) dmap2 { (game: LightGame) =>
LightPov(game, game player color)
}
def pov(ref: PovRef): Fu[Option[LightPov]] = pov(ref.gameId, ref.color)
def gamesFromPrimary(gameIds: Seq[ID]): Fu[List[LightGame]] =
coll.byOrderedIds[LightGame, ID](gameIds, projection = LightGame.projection.some)(_.id)
def gamesFromSecondary(gameIds: Seq[ID]): Fu[List[LightGame]] =
coll.byOrderedIds[LightGame, ID](
gameIds,
projection = LightGame.projection.some,
readPreference = ReadPreference.secondaryPreferred
)(_.id)
}
def finished(gameId: ID): Fu[Option[Game]] =
coll.one[Game]($id(gameId) ++ Query.finished)
def player(gameId: ID, color: Color): Fu[Option[Player]] =
game(gameId) dmap2 { _ player color }
def player(gameId: ID, playerId: ID): Fu[Option[Player]] =
game(gameId) dmap { gameOption =>
gameOption flatMap { _ player playerId }
}
def player(playerRef: PlayerRef): Fu[Option[Player]] =
player(playerRef.gameId, playerRef.playerId)
def pov(gameId: ID, color: Color): Fu[Option[Pov]] =
game(gameId) dmap2 { (game: Game) =>
Pov(game, game player color)
}
def pov(gameId: ID, color: String): Fu[Option[Pov]] =
Color.fromName(color) ?? (pov(gameId, _))
def pov(playerRef: PlayerRef): Fu[Option[Pov]] =
game(playerRef.gameId) dmap { _ flatMap { _ playerIdPov playerRef.playerId } }
def pov(fullId: ID): Fu[Option[Pov]] = pov(PlayerRef(fullId))
def pov(ref: PovRef): Fu[Option[Pov]] = pov(ref.gameId, ref.color)
def remove(id: ID) = coll.delete.one($id(id)).void
def userPovsByGameIds(
gameIds: List[String],
user: User,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
): Fu[List[Pov]] =
coll.byOrderedIds[Game, ID](gameIds, readPreference = readPreference)(_.id) dmap {
_.flatMap(g => Pov(g, user))
}
def recentPovsByUserFromSecondary(user: User, nb: Int, select: Bdoc = $empty): Fu[List[Pov]] =
recentGamesByUserFromSecondaryCursor(user, select)
.list(nb)
.map { _.flatMap(Pov(_, user)) }
def recentGamesByUserFromSecondaryCursor(user: User, select: Bdoc = $empty) =
coll
.find(Query.user(user) ++ select)
.sort(Query.sortCreated)
.cursor[Game](ReadPreference.secondaryPreferred)
def gamesForAssessment(userId: String, nb: Int): Fu[List[Game]] =
coll
.find(
Query.finished
++ Query.rated
++ Query.user(userId)
++ Query.analysed(true)
++ Query.turnsGt(20)
++ Query.clockHistory(true)
)
.sort($sort asc F.createdAt)
.cursor[Game](ReadPreference.secondaryPreferred)
.list(nb)
def extraGamesForIrwin(userId: String, nb: Int): Fu[List[Game]] =
coll
.find(
Query.finished
++ Query.rated
++ Query.user(userId)
++ Query.turnsGt(22)
++ Query.variantStandard
++ Query.clock(true)
)
.sort($sort asc F.createdAt)
.cursor[Game](ReadPreference.secondaryPreferred)
.list(nb)
def unanalysedGames(gameIds: Seq[ID]): Fu[List[Game]] =
coll
.find($inIds(gameIds) ++ Query.analysed(false))
.cursor[Game](ReadPreference.secondaryPreferred)
.list(100)
def cursor(
selector: Bdoc,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
): AkkaStreamCursor[Game] =
coll.find(selector).cursor[Game](readPreference)
def docCursor(
selector: Bdoc,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
): AkkaStreamCursor[Bdoc] =
coll.find(selector).cursor[Bdoc](readPreference)
def sortedCursor(
selector: Bdoc,
sort: Bdoc,
batchSize: Int = 0,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
): AkkaStreamCursor[Game] =
coll.find(selector).sort(sort).batchSize(batchSize).cursor[Game](readPreference)
def byIdsCursor(ids: Iterable[Game.ID]): Cursor[Game] = coll.find($inIds(ids)).cursor[Game]()
def goBerserk(pov: Pov): Funit =
coll.update
.one(
$id(pov.gameId),
$set(
s"${pov.color.fold(F.whitePlayer, F.blackPlayer)}.${Player.BSONFields.berserk}" -> true
)
)
.void
def update(progress: Progress): Funit =
saveDiff(progress.origin, GameDiff(progress.origin, progress.game))
private def saveDiff(origin: Game, diff: GameDiff.Diff): Funit =
diff match {
case (Nil, Nil) => funit
case (sets, unsets) =>
coll.update
.one(
$id(origin.id),
nonEmptyMod("$set", $doc(sets)) ++ nonEmptyMod("$unset", $doc(unsets))
)
.void
}
private def nonEmptyMod(mod: String, doc: Bdoc) =
if (doc.isEmpty) $empty else $doc(mod -> doc)
def setRatingDiffs(id: ID, diffs: RatingDiffs) =
coll.update.one(
$id(id),
$set(
s"${F.whitePlayer}.${Player.BSONFields.ratingDiff}" -> diffs.white,
s"${F.blackPlayer}.${Player.BSONFields.ratingDiff}" -> diffs.black
)
)
// Use Env.round.proxy.urgentGames to get in-heap states!
def urgentPovsUnsorted(user: User): Fu[List[Pov]] =
coll.list[Game](Query nowPlaying user.id, Game.maxPlayingRealtime) dmap {
_ flatMap { Pov(_, user) }
}
def countWhereUserTurn(userId: User.ID): Fu[Int] =
coll
.countSel(
// important, hits the index!
Query.nowPlaying(userId) ++ $doc(
"$or" ->
List(0, 1).map { rem =>
$doc(
s"${Game.BSONFields.playingUids}.$rem" -> userId,
Game.BSONFields.turns -> $doc("$mod" -> $arr(2, rem))
)
}
)
)
.dmap(_.toInt)
def playingRealtimeNoAi(user: User): Fu[List[Game.ID]] =
coll.distinctEasy[Game.ID, List](
F.id,
Query.nowPlaying(user.id) ++ Query.noAi ++ Query.clock(true),
ReadPreference.secondaryPreferred
)
def lastPlayedPlayingId(userId: User.ID): Fu[Option[Game.ID]] =
coll
.find(Query recentlyPlaying userId, $id(true).some)
.sort(Query.sortMovedAtNoIndex)
.one[Bdoc](readPreference = ReadPreference.primary)
.dmap { _.flatMap(_.getAsOpt[Game.ID](F.id)) }
def allPlaying(userId: User.ID): Fu[List[Pov]] =
coll
.list[Game](Query nowPlaying userId)
.dmap { _ flatMap { Pov.ofUserId(_, userId) } }
def lastPlayed(user: User): Fu[Option[Pov]] =
coll
.find(Query user user.id)
.sort($sort desc F.createdAt)
.cursor[Game]()
.list(2)
.dmap {
_.sortBy(_.movedAt).lastOption flatMap { Pov(_, user) }
}
def quickLastPlayedId(userId: User.ID): Fu[Option[Game.ID]] =
coll
.find(Query user userId, $id(true).some)
.sort($sort desc F.createdAt)
.one[Bdoc]
.dmap { _.flatMap(_.getAsOpt[Game.ID](F.id)) }
def lastFinishedRatedNotFromPosition(user: User): Fu[Option[Game]] =
coll
.find(
Query.user(user.id) ++
Query.rated ++
Query.finished ++
Query.turnsGt(2) ++
Query.notFromPosition
)
.sort(Query.sortAntiChronological)
.one[Game]
def setTv(id: ID) = coll.updateFieldUnchecked($id(id), F.tvAt, DateTime.now)
def setAnalysed(id: ID): Unit = coll.updateFieldUnchecked($id(id), F.analysed, true)
def setUnanalysed(id: ID): Unit = coll.updateFieldUnchecked($id(id), F.analysed, false)
def isAnalysed(id: ID): Fu[Boolean] =
coll.exists($id(id) ++ Query.analysed(true))
def exists(id: ID) = coll.exists($id(id))
def tournamentId(id: ID): Fu[Option[String]] = coll.primitiveOne[String]($id(id), F.tournamentId)
def incBookmarks(id: ID, value: Int) =
coll.update.one($id(id), $inc(F.bookmarks -> value)).void
def setHoldAlert(pov: Pov, alert: Player.HoldAlert): Funit =
coll
.updateField(
$id(pov.gameId),
holdAlertField(pov.color),
alert
)
.void
def setBorderAlert(pov: Pov) = setHoldAlert(pov, Player.HoldAlert(0, 0, 20))
object holdAlert {
private val holdAlertSelector = $or(
holdAlertField(chess.White) $exists true,
holdAlertField(chess.Black) $exists true
)
private val holdAlertProjection = $doc(
holdAlertField(chess.White) -> true,
holdAlertField(chess.Black) -> true
)
private def holdAlertOf(doc: Bdoc, color: Color): Option[Player.HoldAlert] =
doc.child(color.fold("p0", "p1")).flatMap(_.getAsOpt[Player.HoldAlert](Player.BSONFields.holdAlert))
def game(game: Game): Fu[Player.HoldAlert.Map] =
coll.one[Bdoc](
$doc(F.id -> game.id, holdAlertSelector),
holdAlertProjection
) map {
_.fold(Player.HoldAlert.emptyMap) { doc =>
Color.Map(white = holdAlertOf(doc, chess.White), black = holdAlertOf(doc, chess.Black))
}
}
def povs(povs: Seq[Pov]): Fu[Map[Game.ID, Player.HoldAlert]] =
coll
.find(
$doc($inIds(povs.map(_.gameId)), holdAlertSelector),
holdAlertProjection.some
)
.cursor[Bdoc](ReadPreference.secondaryPreferred)
.list() map { docs =>
val idColors = povs.view.map { p =>
p.gameId -> p.color
}.toMap
val holds = for {
doc <- docs
id <- doc string "_id"
color <- idColors get id
holds <- holdAlertOf(doc, color)
} yield id -> holds
holds.toMap
}
}
def hasHoldAlert(pov: Pov): Fu[Boolean] =
coll.exists(
$doc(
$id(pov.gameId),
holdAlertField(pov.color) $exists true
)
)
private def holdAlertField(color: Color) = s"p${color.fold(0, 1)}.${Player.BSONFields.holdAlert}"
private val finishUnsets = $doc(
F.positionHashes -> true,
F.playingUids -> true,
F.unmovedRooks -> true,
("p0." + Player.BSONFields.isOfferingDraw) -> true,
("p1." + Player.BSONFields.isOfferingDraw) -> true,
("p0." + Player.BSONFields.proposeTakebackAt) -> true,
("p1." + Player.BSONFields.proposeTakebackAt) -> true
)
def finish(
id: ID,
winnerColor: Option[Color],
winnerId: Option[String],
status: Status
) =
coll.update.one(
$id(id),
nonEmptyMod(
"$set",
$doc(
F.winnerId -> winnerId,
F.winnerColor -> winnerColor.map(_.white),
F.status -> status
)
) ++ $doc(
"$unset" -> finishUnsets.++ {
// keep the checkAt field when game is aborted,
// so it gets deleted in 24h
(status >= Status.Mate) ?? $doc(F.checkAt -> true)
}
)
)
def findRandomStandardCheckmate(distribution: Int): Fu[Option[Game]] =
coll
.find(
Query.mate ++ Query.variantStandard
)
.sort(Query.sortCreated)
.skip(ThreadLocalRandom nextInt distribution)
.one[Game]
def insertDenormalized(g: Game, initialFen: Option[chess.format.FEN] = None): Funit = {
val g2 =
if (g.rated && (g.userIds.distinct.size != 2 || !Game.allowRated(g.variant, g.clock.map(_.config))))
g.copy(mode = chess.Mode.Casual)
else g
val userIds = g2.userIds.distinct
val fen: Option[FEN] = initialFen orElse {
(g2.variant.fromPosition || g2.variant.chess960)
.option(Forsyth >> g2.chess)
.filterNot(_.initial)
}
val checkInHours =
if (g2.isPgnImport) none
else if (g2.hasClock) 1.some
else if (g2.hasAi) (Game.aiAbandonedHours + 1).some
else (24 * 10).some
val bson = (gameBSONHandler write g2) ++ $doc(
F.initialFen -> fen,
F.checkAt -> checkInHours.map(DateTime.now.plusHours),
F.playingUids -> (g2.started && userIds.nonEmpty).option(userIds)
)
coll.insert.one(bson) addFailureEffect {
case wr: WriteResult if isDuplicateKey(wr) => lila.mon.game.idCollision.increment().unit
} void
}
def removeRecentChallengesOf(userId: String) =
coll.delete.one(
Query.created ++ Query.friend ++ Query.user(userId) ++
Query.createdSince(DateTime.now minusHours 1)
)
def setCheckAt(g: Game, at: DateTime) =
coll.update.one($id(g.id), $set(F.checkAt -> at))
def unsetCheckAt(id: Game.ID): Funit =
coll.update.one($id(id), $unset(F.checkAt)).void
def unsetPlayingUids(g: Game): Unit =
coll.update(ordered = false, WriteConcern.Unacknowledged).one($id(g.id), $unset(F.playingUids)).unit
// used to make a compound sparse index
def setImportCreatedAt(g: Game) =
coll.update.one($id(g.id), $set("pgni.ca" -> g.createdAt)).void
def initialFen(gameId: ID): Fu[Option[FEN]] =
coll.primitiveOne[FEN]($id(gameId), F.initialFen)
def initialFen(game: Game): Fu[Option[FEN]] =
if (game.imported || !game.variant.standardInitialPosition) initialFen(game.id) dmap {
case None if game.variant == chess.variant.Chess960 => Forsyth.initial.some
case fen => fen
}
else fuccess(none)
def gameWithInitialFen(gameId: ID): Fu[Option[(Game, Option[FEN])]] =
game(gameId) flatMap {
_ ?? { game =>
initialFen(game) dmap { fen =>
Option(game -> fen)
}
}
}
def withInitialFen(game: Game): Fu[Game.WithInitialFen] =
initialFen(game) dmap { Game.WithInitialFen(game, _) }
def withInitialFens(games: List[Game]): Fu[List[(Game, Option[FEN])]] =
games.map { game =>
initialFen(game) dmap { game -> _ }
}.sequenceFu
def count(query: Query.type => Bdoc): Fu[Int] = coll countSel query(Query)
private[game] def favoriteOpponents(
userId: String,
opponentLimit: Int,
gameLimit: Int
): Fu[List[(User.ID, Int)]] = {
coll
.aggregateList(
maxDocs = opponentLimit,
ReadPreference.secondaryPreferred
) { framework =>
import framework._
Match($doc(F.playerUids -> userId)) -> List(
Match($doc(F.playerUids -> $doc("$size" -> 2))),
Sort(Descending(F.createdAt)),
Limit(gameLimit), // only look in the last n games
Project(
$doc(
F.playerUids -> true,
F.id -> false
)
),
UnwindField(F.playerUids),
Match($doc(F.playerUids $ne userId)),
GroupField(F.playerUids)("gs" -> SumAll),
Sort(Descending("gs")),
Limit(opponentLimit)
)
}
.map(_.flatMap { obj =>
obj.string(F.id) flatMap { id =>
obj.int("gs") map { id -> _ }
}
})
}
def random: Fu[Option[Game]] =
coll
.find($empty)
.sort(Query.sortCreated)
.skip(ThreadLocalRandom nextInt 1000)
.one[Game]
def findPgnImport(pgn: String): Fu[Option[Game]] =
coll.one[Game](
$doc(s"${F.pgnImport}.h" -> PgnImport.hash(pgn))
)
def getOptionPgn(id: ID): Fu[Option[PgnMoves]] = game(id) dmap2 { _.pgnMoves }
def lastGameBetween(u1: String, u2: String, since: DateTime): Fu[Option[Game]] =
coll.one[Game](
$doc(
F.playerUids $all List(u1, u2),
F.createdAt $gt since
)
)
def lastGamesBetween(u1: User, u2: User, since: DateTime, nb: Int): Fu[List[Game]] =
List(u1, u2).forall(_.count.game > 0) ??
coll.secondaryPreferred.list[Game](
$doc(
F.playerUids $all List(u1.id, u2.id),
F.createdAt $gt since
),
nb
)
def getSourceAndUserIds(id: ID): Fu[(Option[Source], List[User.ID])] =
coll.one[Bdoc]($id(id), $doc(F.playerUids -> true, F.source -> true)) dmap {
_.fold(none[Source] -> List.empty[User.ID]) { doc =>
(doc.int(F.source) flatMap Source.apply, ~doc.getAsOpt[List[User.ID]](F.playerUids))
}
}
def recentAnalysableGamesByUserId(userId: User.ID, nb: Int) =
coll
.find(
Query.finished
++ Query.rated
++ Query.user(userId)
++ Query.turnsGt(20)
)
.sort(Query.sortCreated)
.cursor[Game](ReadPreference.secondaryPreferred)
.list(nb)
// only for student games, for aggregation
def denormalizePerfType(game: Game): Unit =
game.perfType ?? { pt =>
coll.updateFieldUnchecked($id(game.id), F.perfType, pt.id)
}
}