game module migration

rm0193-mapreduce
Thibault Duplessis 2019-11-30 12:06:50 -06:00
parent e0b39662c7
commit e0819404db
34 changed files with 402 additions and 414 deletions

View File

@ -161,7 +161,7 @@ lazy val user = module("user", Seq(common, memo, db, hub, rating, socket)).setti
)
lazy val game = module("game", Seq(common, memo, db, hub, user, chat)).settings(
libraryDependencies ++= provided(compression, play.api) ++ reactivemongo.bundle
libraryDependencies ++= provided(compression, play.api, play.joda) ++ reactivemongo.bundle
)
lazy val gameSearch = module("gameSearch", Seq(common, hub, search, game)).settings(

View File

@ -339,7 +339,7 @@ timeline {
}
}
game {
paginator.max_per_page = 12
paginator.maxPerPage = 12
collection {
game = game5
crosstable = crosstable2
@ -349,12 +349,9 @@ game {
name = captcher
duration = 15 seconds
}
net.base_url = ${net.base_url}
uci_memo.ttl = 3 minutes
png {
url = "http://boardimage.lichess.ovh:8080/board.png"
size = 1024
}
uciMemoTtl = 3 minutes
pngUrl = "http://boardimage.lichess.ovh:8080/board.png"
pngSize = 1024
}
tv {
featured {

View File

@ -13,6 +13,12 @@ object config {
case class BaseUrl(value: String) extends AnyVal with StringValue
case class AppPath(value: String) extends AnyVal with StringValue
case class MaxPerPage(value: Int) extends AnyVal with IntValue
case class MaxPerSecond(value: Int) extends AnyVal with IntValue
case class NetConfig(
domain: String,
protocol: String,
@ -21,10 +27,12 @@ object config {
)
implicit val maxPerPageLoader = intLoader(MaxPerPage.apply)
implicit val maxPerSecondLoader = intLoader(MaxPerSecond.apply)
implicit val collNameLoader = strLoader(CollName.apply)
implicit val secretLoader = strLoader(Secret.apply)
implicit val baseUrlLoader = strLoader(BaseUrl.apply)
implicit val emailAddressLoader = strLoader(EmailAddress.apply)
implicit val appPathLoader = strLoader(AppPath.apply)
implicit val netLoader = AutoConfig.loader[NetConfig]
def strLoader[A](f: String => A): ConfigLoader[A] = ConfigLoader(_.getString) map f

View File

@ -22,10 +22,6 @@ object AssetVersion {
case class IsMobile(value: Boolean) extends AnyVal with BooleanValue
case class MaxPerPage(value: Int) extends AnyVal with IntValue
case class MaxPerSecond(value: Int) extends AnyVal with IntValue
case class IpAddress(value: String) extends AnyVal with StringValue
object IpAddress {

View File

@ -3,6 +3,8 @@ package paginator
import scalaz.Success
import config.MaxPerPage
final class Paginator[A] private[paginator] (
val currentPage: Int,
val maxPerPage: MaxPerPage,

View File

@ -5,7 +5,7 @@ import play.api.libs.json._
object PaginatorJson {
implicit val maxPerPageWrites = Writes[MaxPerPage] { m => JsNumber(m.value) }
implicit val maxPerPageWrites = Writes[config.MaxPerPage] { m => JsNumber(m.value) }
implicit def paginatorWrites[A: Writes]: Writes[Paginator[A]] = Writes[Paginator[A]](apply)

View File

@ -205,10 +205,6 @@ object BSON extends Handlers {
def double(i: Double): BSONDouble = BSONDouble(i)
def doubleO(i: Double): Option[BSONDouble] = if (i != 0) Some(BSONDouble(i)) else None
def zero[A](a: A)(implicit zero: Zero[A]): Option[A] = if (zero.zero == a) None else Some(a)
// import scalaz.Functor
// def map[M[_]: Functor, A, B <: BSONValue](a: M[A])(implicit writer: BSONWriter[A]): M[B] =
// a map writer.write
}
val writer = new Writer

View File

@ -1,12 +1,11 @@
package lila.game
import chess.format.FEN
import chess.variant.{ Variant, Crazyhouse }
import chess.{ CheckCount, Color, Clock, White, Black, Status, Mode, UnmovedRooks, History => ChessHistory, Game => ChessGame }
import org.joda.time.DateTime
import reactivemongo.api.bson._
import scala.collection.breakOut
import chess.variant.{ Variant, Crazyhouse }
import chess.format.FEN
import chess.{ CheckCount, Color, Clock, White, Black, Status, Mode, UnmovedRooks, History => ChessHistory, Game => ChessGame }
import scala.util.{ Try, Success, Failure }
import lila.db.BSON
import lila.db.dsl._
@ -17,23 +16,19 @@ object BSONHandlers {
implicit val FENBSONHandler = stringAnyValHandler[FEN](_.value, FEN.apply)
private[game] implicit val checkCountWriter = new BSONWriter[CheckCount, BSONArray] {
def write(cc: CheckCount) = BSONArray(cc.white, cc.black)
private[game] implicit val checkCountWriter = new BSONWriter[CheckCount] {
def writeTry(cc: CheckCount) = Success(BSONArray(cc.white, cc.black))
}
implicit val StatusBSONHandler = new BSONHandler[BSONInteger, Status] {
def read(bsonInt: BSONInteger): Status = Status(bsonInt.value) err s"No such status: ${bsonInt.value}"
def write(x: Status) = BSONInteger(x.id)
}
implicit val StatusBSONHandler = lila.db.BSON.tryHandler[Status](
{ case BSONInteger(v) => Status(v) toTry s"No such status: $v" },
x => BSONInteger(x.id)
)
private[game] implicit val unmovedRooksHandler = new BSONHandler[BSONBinary, UnmovedRooks] {
def read(bin: BSONBinary): UnmovedRooks = BinaryFormat.unmovedRooks.read {
ByteArrayBSONHandler.read(bin)
}
def write(x: UnmovedRooks): BSONBinary = ByteArrayBSONHandler.write {
BinaryFormat.unmovedRooks.write(x)
}
}
private[game] implicit val unmovedRooksHandler = lila.db.BSON.tryHandler[UnmovedRooks](
{ case bin: BSONBinary => ByteArrayBSONHandler.readTry(bin) map BinaryFormat.unmovedRooks.read },
x => ByteArrayBSONHandler.writeTry(BinaryFormat.unmovedRooks write x).get
)
private[game] implicit val crazyhouseDataBSONHandler = new BSON[Crazyhouse.Data] {
@ -42,14 +37,14 @@ object BSONHandlers {
def reads(r: BSON.Reader) = Crazyhouse.Data(
pockets = {
val (white, black) = {
r.str("p").flatMap(chess.Piece.fromChar)(breakOut): List[chess.Piece]
r.str("p").view.flatMap(chess.Piece.fromChar).to(List)
}.partition(_ is chess.White)
Pockets(
white = Pocket(white.map(_.role)),
black = Pocket(black.map(_.role))
)
},
promoted = r.str("t").flatMap(chess.Pos.piotr)(breakOut)
promoted = r.str("t").view.flatMap(chess.Pos.piotr).to(Set)
)
def writes(w: BSON.Writer, o: Crazyhouse.Data) = BSONDocument(
@ -160,7 +155,7 @@ object BSONHandlers {
F.status -> o.status,
F.turns -> o.chess.turns,
F.startedAtTurn -> w.intO(o.chess.startedAtTurn),
F.clock -> (o.chess.clock map { c => clockBSONWrite(o.createdAt, c) }),
F.clock -> (o.chess.clock flatMap { c => clockBSONWrite(o.createdAt, c).toOption }),
F.daysPerTurn -> o.daysPerTurn,
F.moveTimes -> o.binaryMoveTimes,
F.whiteClockHistory -> clockHistory(White, o.clockHistory, o.chess.clock, o.flagged),
@ -185,10 +180,10 @@ object BSONHandlers {
F.binaryPieces -> BinaryFormat.piece.write(o.board.pieces),
F.positionHashes -> o.history.positionHashes,
F.unmovedRooks -> o.history.unmovedRooks,
F.castleLastMove -> CastleLastMove.castleLastMoveBSONHandler.write(CastleLastMove(
F.castleLastMove -> CastleLastMove.castleLastMoveBSONHandler.writeTry(CastleLastMove(
castles = o.history.castles,
lastMove = o.history.lastMove
)),
)).toOption,
F.checkCount -> o.history.checkCount.nonEmpty.option(o.history.checkCount),
F.crazyData -> o.board.crazyData
)
@ -231,13 +226,16 @@ object BSONHandlers {
times = history(color)
} yield BinaryFormat.clockHistory.writeSide(clk.limit, times, flagged has color)
private[game] def clockBSONReader(since: DateTime, whiteBerserk: Boolean, blackBerserk: Boolean) = new BSONReader[BSONBinary, Color => Clock] {
def read(bin: BSONBinary) = BinaryFormat.clock(since).read(
ByteArrayBSONHandler read bin, whiteBerserk, blackBerserk
)
private[game] def clockBSONReader(since: DateTime, whiteBerserk: Boolean, blackBerserk: Boolean) = new BSONReader[Color => Clock] {
def readTry(bson: BSONValue): Try[Color => Clock] = bson match {
case bin: BSONBinary => ByteArrayBSONHandler readTry bin map { cl =>
BinaryFormat.clock(since).read(cl, whiteBerserk, blackBerserk)
}
case b => lila.db.BSON.handlerBadType(b)
}
}
private[game] def clockBSONWrite(since: DateTime, clock: Clock) = ByteArrayBSONHandler write {
private[game] def clockBSONWrite(since: DateTime, clock: Clock) = ByteArrayBSONHandler writeTry {
BinaryFormat clock since write clock
}
}

View File

@ -5,17 +5,20 @@ import scala.concurrent.duration._
import lila.user.{ User, UserRepo }
final class BestOpponents {
final class BestOpponents(
userRepo: UserRepo,
gameRepo: GameRepo
) {
private val limit = 30
private val userIdsCache = Scaffeine()
.expireAfterWrite(15 minutes)
.buildAsyncFuture[User.ID, List[(User.ID, Int)]] { GameRepo.bestOpponents(_, limit) }
.buildAsyncFuture[User.ID, List[(User.ID, Int)]] { gameRepo.bestOpponents(_, limit) }
def apply(userId: String): Fu[List[(User, Int)]] =
userIdsCache get userId flatMap { opponents =>
UserRepo enabledByIds opponents.map(_._1) map {
userRepo enabledByIds opponents.map(_._1) map {
_ flatMap { user =>
opponents find (_._1 == user.id) map { opponent =>
user -> opponent._2

View File

@ -1,8 +1,6 @@
package lila.game
import org.joda.time.DateTime
import scala.collection.breakOut
import scala.collection.breakOut
import scala.collection.Searching._
import scala.util.Try
@ -33,11 +31,11 @@ object BinaryFormat {
def writeSide(start: Centis, times: Vector[Centis], flagged: Boolean) = {
val timesToWrite = if (flagged) times.dropRight(1) else times
ByteArray(ClockEncoder.encode(timesToWrite.map(_.centis)(breakOut), start.centis))
ByteArray(ClockEncoder.encode(timesToWrite.view.map(_.centis).to(Array), start.centis))
}
def readSide(start: Centis, ba: ByteArray, flagged: Boolean) = {
val decoded: Vector[Centis] = ClockEncoder.decode(ba.value, start.centis).map(Centis.apply)(breakOut)
val decoded: Vector[Centis] = ClockEncoder.decode(ba.value, start.centis).view.map(Centis.apply).to(Vector)
if (flagged) decoded :+ Centis(0) else decoded
}
@ -61,7 +59,7 @@ object BinaryFormat {
case (i1, i2) => (i1 + i2) / 2
} toVector
private val decodeMap: Map[Int, MT] = buckets.zipWithIndex.map(x => x._2 -> x._1)(breakOut)
private val decodeMap: Map[Int, MT] = buckets.view.zipWithIndex.map(x => x._2 -> x._1).to(Map)
def write(mts: Vector[Centis]): ByteArray = {
def enc(mt: Centis) = encodeCutoffs.search(mt.centis).insertionPoint
@ -76,7 +74,7 @@ object BinaryFormat {
ba.value map toInt flatMap { k =>
Array(dec(k >> 4), dec(k & 15))
}
}.take(turns).map(Centis.apply)(breakOut)
}.view.take(turns).map(Centis.apply).to(Vector)
}
case class clock(start: Timestamp) {
@ -222,9 +220,9 @@ object BinaryFormat {
def intPiece(int: Int): Option[Piece] =
intToRole(int & 7, variant) map { role => Piece(Color((int & 8) == 0), role) }
val pieceInts = ba.value flatMap splitInts
(Pos.all zip pieceInts).flatMap {
(Pos.all zip pieceInts).view.flatMap {
case (pos, int) => intPiece(int) map (pos -> _)
}(breakOut)
}.to(Map)
}
// cache standard start position

View File

@ -1,6 +1,7 @@
package lila.game
import ornicar.scalalib.Zero
import scala.util.{ Try, Success, Failure }
sealed trait Blurs extends Any {
@ -39,29 +40,26 @@ object Blurs {
override def toString = s"Blurs.Bits($binaryString)"
}
implicit val blursZero = Zero.instance[Blurs](Bits(0l))
implicit val blursZero = Zero.instance[Blurs](Bits(0L))
import reactivemongo.api.bson._
private[game] implicit val BlursBitsBSONHandler = new BSONHandler[BSONValue, Bits] {
def read(bv: BSONValue): Bits = bv match {
case BSONInteger(bits) => Bits(bits & 0xffffffffL)
case BSONLong(bits) => Bits(bits)
case v => sys error s"Invalid blurs bits $v"
}
def write(b: Bits): BSONValue =
b.asInt.fold[BSONValue](BSONLong(b.bits))(BSONInteger.apply)
}
private[game] implicit val BlursBitsBSONHandler = lila.db.BSON.tryHandler[Bits](
{
case BSONInteger(bits) => Success(Bits(bits & 0xffffffffL))
case BSONLong(bits) => Success(Bits(bits))
case v => lila.db.BSON.handlerBadValue(s"Invalid blurs bits $v")
},
bits => bits.asInt.fold[BSONValue](BSONLong(bits.bits))(BSONInteger.apply)
)
private[game] implicit val BlursNbBSONReader = new BSONReader[BSONInteger, Nb] {
def read(bi: BSONInteger) = Nb(bi.value)
}
private[game] implicit val BlursNbBSONHandler = BSONIntegerHandler.as[Nb](Nb.apply, _.nb)
private[game] implicit val BlursBSONWriter = new BSONWriter[Blurs, BSONValue] {
def write(b: Blurs): BSONValue = b match {
case bits: Bits => BlursBitsBSONHandler write bits
private[game] implicit val BlursBSONWriter = new BSONWriter[Blurs] {
def writeTry(b: Blurs) = b match {
case bits: Bits => BlursBitsBSONHandler writeTry bits
// only Bits can be written; Nb results to Bits(0)
case _ => BSONInteger(0)
case _ => Success[BSONValue](BSONInteger(0))
}
}
}

View File

@ -7,7 +7,7 @@ import lila.memo.{ MongoCache, ExpireSetMemo }
import lila.user.User
final class Cached(
coll: Coll,
gameRepo: GameRepo,
asyncCache: lila.memo.AsyncCache.Builder,
mongoCache: MongoCache.Builder
) {
@ -23,13 +23,13 @@ final class Cached(
private val countShortTtl = asyncCache.multi[Bdoc, Int](
name = "game.countShortTtl",
f = coll.countSel(_),
f = gameRepo.coll.countSel(_),
expireAfter = _.ExpireAfterWrite(5.seconds)
)
private val nbImportedCache = mongoCache[User.ID, Int](
prefix = "game:imported",
f = userId => coll countSel Query.imported(userId),
f = userId => gameRepo.coll countSel Query.imported(userId),
timeToLive = 1 hour,
timeToLiveMongo = 30.days.some,
keyToString = identity
@ -37,7 +37,7 @@ final class Cached(
private val countCache = mongoCache[Bdoc, Int](
prefix = "game:count",
f = coll.countSel(_),
f = gameRepo.coll.countSel(_),
timeToLive = 1 hour,
keyToString = lila.db.BSON.hashDoc
)

View File

@ -7,14 +7,15 @@ import akka.pattern.pipe
import chess.format.pgn.{ Tags, Sans }
import chess.format.{ Forsyth, pgn }
import chess.{ Game => ChessGame }
import scalaz.{ NonEmptyList, OptionT }
import scala.util.{ Try, Success, Failure }
import scalaz.Validation.FlatMap._
import scalaz.{ NonEmptyList, OptionT }
import lila.common.Captcha
import lila.hub.actorApi.captcha._
// only works with standard chess (not chess960)
private final class Captcher extends Actor {
private final class Captcher(gameRepo: GameRepo) extends Actor {
def receive = {
@ -37,8 +38,8 @@ private final class Captcher extends Actor {
def current = challenges.head
def refresh = createFromDb onSuccess {
case Some(captcha) => add(captcha)
def refresh = createFromDb andThen {
case Success(Some(captcha)) => add(captcha)
}
// Private stuff
@ -62,13 +63,13 @@ private final class Captcher extends Actor {
}.run
private def findCheckmateInDb(distribution: Int): Fu[Option[Game]] =
GameRepo findRandomStandardCheckmate distribution
gameRepo findRandomStandardCheckmate distribution
private def getFromDb(id: String): Fu[Option[Captcha]] =
optionT(GameRepo game id) flatMap fromGame run
optionT(gameRepo game id) flatMap fromGame run
private def fromGame(game: Game): OptionT[Fu, Captcha] =
optionT(GameRepo getOptionPgn game.id) flatMap { makeCaptcha(game, _) }
optionT(gameRepo getOptionPgn game.id) flatMap { makeCaptcha(game, _) }
private def makeCaptcha(game: Game, moves: PgnMoves): OptionT[Fu, Captcha] =
optionT(Future {
@ -82,11 +83,11 @@ private final class Captcher extends Actor {
})
private def solve(game: ChessGame): Option[Captcha.Solutions] =
(game.situation.moves.flatMap {
game.situation.moves.view.flatMap {
case (_, moves) => moves filter { move =>
(move.after situationOf !game.player).checkMate
}
}(scala.collection.breakOut): List[chess.Move]) map { move =>
}.to(List) map { move =>
s"${move.orig} ${move.dest}"
} toNel

View File

@ -1,5 +1,7 @@
package lila.game
import scala.util.{ Try, Success, Failure }
case class Crosstable(
users: Crosstable.Users,
results: List[Crosstable.Result] // chronological order, oldest to most recent
@ -67,7 +69,7 @@ object Crosstable {
case class Result(gameId: Game.ID, winnerId: Option[String])
case class Matchup(users: Users) extends AnyVal { // score is x10
case class Matchup(users: Users) { // score is x10
def fromPov(userId: String) = copy(users = users fromPov userId)
def nonEmpty = users.nbGames > 0
}
@ -131,11 +133,13 @@ object Crosstable {
private[game] implicit val MatchupBSONReader = new BSONDocumentReader[Matchup] {
import BSONFields._
def read(doc: Bdoc): Matchup = {
def readDocument(doc: Bdoc) = {
val r = new BSON.Reader(doc)
r str id split '/' match {
case Array(u1Id, u2Id) => Matchup(Users(User(u1Id, r intD score1), User(u2Id, r intD score2)))
case x => sys error s"Invalid crosstable id $x"
case Array(u1Id, u2Id) => Success {
Matchup(Users(User(u1Id, r intD score1), User(u2Id, r intD score2)))
}
case x => lila.db.BSON.handlerBadValue(s"Invalid crosstable id $x")
}
}
}

View File

@ -9,10 +9,10 @@ import lila.user.{ User, UserRepo }
final class CrosstableApi(
coll: Coll,
matchupColl: Coll,
gameColl: Coll,
asyncCache: lila.memo.AsyncCache.Builder,
system: akka.actor.ActorSystem
) {
gameRepo: GameRepo,
userRepo: UserRepo,
asyncCache: lila.memo.AsyncCache.Builder
)(implicit system: akka.actor.ActorSystem) {
import Crosstable.{ Matchup, Result }
import Crosstable.{ BSONFields => F }
@ -39,12 +39,12 @@ final class CrosstableApi(
def nbGames(u1: User.ID, u2: User.ID): Fu[Int] =
coll.find(
select(u1, u2),
$doc("s1" -> true, "s2" -> true)
$doc("s1" -> true, "s2" -> true).some
).uno[Bdoc] map { res =>
~(for {
o <- res
s1 <- o.getAs[Int]("s1")
s2 <- o.getAs[Int]("s2")
s1 <- o.int("s1")
s2 <- o.int("s2")
} yield (s1 + s2) / 10)
}
@ -59,7 +59,7 @@ final class CrosstableApi(
}
val inc1 = incScore(u1)
val inc2 = incScore(u2)
val updateCrosstable = coll.update(select(u1, u2), $inc(
val updateCrosstable = coll.update.one(select(u1, u2), $inc(
F.score1 -> inc1,
F.score2 -> inc2
) ++ $push(
@ -69,7 +69,7 @@ final class CrosstableApi(
)
))
val updateMatchup =
matchupColl.update(select(u1, u2), $inc(
matchupColl.update.one(select(u1, u2), $inc(
F.score1 -> inc1,
F.score2 -> inc2
) ++ $set(
@ -83,10 +83,10 @@ final class CrosstableApi(
private val matchupProjection = $doc(F.lastPlayed -> false)
private def getMatchup(u1: User.ID, u2: User.ID): Fu[Option[Matchup]] =
matchupColl.find(select(u1, u2), matchupProjection).uno[Matchup]
matchupColl.find(select(u1, u2), matchupProjection.some).uno[Matchup]
private def createWithTimeout(u1: User.ID, u2: User.ID, timeout: FiniteDuration) =
creationCache.get(u1 -> u2).withTimeoutDefault(timeout, none)(system)
creationCache.get(u1 -> u2).withTimeoutDefault(timeout, none)
// to avoid creating it twice during a new matchup
private val creationCache = asyncCache.multi[(User.ID, User.ID), Option[Crosstable]](
@ -99,7 +99,7 @@ final class CrosstableApi(
private val winnerProjection = $doc(GF.winnerId -> true)
private def create(x1: User.ID, x2: User.ID): Fu[Option[Crosstable]] = {
UserRepo.orderByGameCount(x1, x2) map (_ -> List(x1, x2).sorted) flatMap {
userRepo.orderByGameCount(x1, x2) map (_ -> List(x1, x2).sorted) flatMap {
case (Some((u1, u2)), List(su1, su2)) =>
val selector = $doc(
GF.playerUids $all List(u1, u2),
@ -108,13 +108,13 @@ final class CrosstableApi(
import reactivemongo.api.ReadPreference
gameColl.find(selector, winnerProjection)
gameRepo.coll.find(selector, winnerProjection.some)
.sort($doc(GF.createdAt -> -1))
.cursor[Bdoc](readPreference = ReadPreference.secondaryPreferred)
.gather[List]().map { docs =>
val (s1, s2) = docs.foldLeft(0 -> 0) {
case ((s1, s2), doc) => doc.getAs[User.ID](GF.winnerId) match {
case ((s1, s2), doc) => doc.getAsOpt[User.ID](GF.winnerId) match {
case Some(u) if u == su1 => (s1 + 10, s2)
case Some(u) if u == su2 => (s1, s2 + 10)
case _ => (s1 + 5, s2 + 5)
@ -126,13 +126,13 @@ final class CrosstableApi(
Crosstable.User(su2, s2)
),
results = docs.take(Crosstable.maxGames).flatMap { doc =>
doc.getAs[String](GF.id).map { id =>
Result(id, doc.getAs[User.ID](GF.winnerId))
doc.string(GF.id).map { id =>
Result(id, doc.getAsOpt[User.ID](GF.winnerId))
}
}.reverse
)
} flatMap { crosstable =>
coll insert crosstable inject crosstable.some
coll.insert.one(crosstable) inject crosstable.some
}
case _ => fuccess(none)

View File

@ -2,107 +2,82 @@ package lila.game
import akka.actor._
import com.github.blemale.scaffeine.{ Cache, Scaffeine }
import com.typesafe.config.Config
import com.softwaremill.macwire._
import io.methvin.play.autoconfig._
import play.api.Configuration
import play.api.libs.ws.WSClient
import scala.concurrent.duration._
import lila.common.config._
private case class GameConfig(
@ConfigName("collection.game") gameColl: CollName,
@ConfigName("collection.crosstable") crosstableColl: CollName,
@ConfigName("collection.matchup") matchupColl: CollName,
@ConfigName("paginator.maxPerPage") paginatorMaxPerPage: MaxPerPage,
@ConfigName("captcher.name") captcherName: String,
@ConfigName("captcher.duration") captcherDuration: FiniteDuration,
uciMemoTtl: FiniteDuration,
pngUrl: String,
pngSize: Int
)
final class Env(
config: Config,
appConfig: Configuration,
ws: WSClient,
db: lila.db.Env,
baseUrl: BaseUrl,
userRepo: lila.user.UserRepo,
mongoCache: lila.memo.MongoCache.Builder,
system: ActorSystem,
hub: lila.hub.Env,
getLightUser: lila.common.LightUser.Getter,
appPath: String,
isProd: Boolean,
asyncCache: lila.memo.AsyncCache.Builder,
settingStore: lila.memo.SettingStore.Builder,
scheduler: lila.common.Scheduler
) {
settingStore: lila.memo.SettingStore.Builder
)(implicit system: ActorSystem, scheduler: Scheduler) {
private val settings = new {
val PaginatorMaxPerPage = config getInt "paginator.max_per_page"
val CaptcherName = config getString "captcher.name"
val CaptcherDuration = config duration "captcher.duration"
val CollectionGame = config getString "collection.game"
val CollectionCrosstable = config getString "collection.crosstable"
val CollectionMatchup = config getString "collection.matchup"
val UciMemoTtl = config duration "uci_memo.ttl"
val netBaseUrl = config getString "net.base_url"
val PngUrl = config getString "png.url"
val PngSize = config getInt "png.size"
}
import settings._
private val config = appConfig.get[GameConfig]("game")(AutoConfig.loader)
import config._
lazy val gameColl = db(CollectionGame)
lazy val gameRepo = new GameRepo(db(gameColl))
lazy val pngExport = new PngExport(PngUrl, PngSize)
lazy val pngExport = new PngExport(ws, pngUrl, pngSize)
lazy val divider = new Divider
lazy val divider = wire[Divider]
lazy val cached = new Cached(
coll = gameColl,
asyncCache = asyncCache,
mongoCache = mongoCache
)
lazy val cached: Cached = wire[Cached]
lazy val paginator = new PaginatorBuilder(
coll = gameColl,
cached = cached,
maxPerPage = lila.common.MaxPerPage(PaginatorMaxPerPage)
)
lazy val paginator = new PaginatorBuilder(gameRepo, cached, paginatorMaxPerPage)
// lazy val paginator = wire[PaginatorBuilder]
lazy val rewind = Rewind
lazy val rewind = wire[Rewind]
lazy val uciMemo = new UciMemo(UciMemoTtl)
lazy val uciMemo = new UciMemo(gameRepo, uciMemoTtl)
lazy val pgnDump = new PgnDump(
netBaseUrl = netBaseUrl,
getLightUser = getLightUser
)
lazy val pgnDump = wire[PgnDump]
lazy val crosstableApi = new CrosstableApi(
coll = db(CollectionCrosstable),
matchupColl = db(CollectionMatchup),
gameColl = gameColl,
asyncCache = asyncCache,
system = system
coll = db(crosstableColl),
matchupColl = db(matchupColl),
userRepo = userRepo,
gameRepo = gameRepo,
asyncCache = asyncCache
)
lazy val playTime = new PlayTimeApi(gameColl, asyncCache, system)
lazy val playTime = wire[PlayTimeApi]
// load captcher actor
private val captcher = system.actorOf(Props(new Captcher), name = CaptcherName)
lazy val gamesByUsersStream = wire[GamesByUsersStream]
scheduler.message(CaptcherDuration) {
captcher -> actorApi.NewCaptcha
}
lazy val gamesByUsersStream = new GamesByUsersStream
lazy val bestOpponents = new BestOpponents
lazy val bestOpponents = wire[BestOpponents]
lazy val rematches: Cache[Game.ID, Game.ID] = Scaffeine()
.expireAfterWrite(3 hour)
.build[Game.ID, Game.ID]
lazy val jsonView = new JsonView(
rematchOf = rematches.getIfPresent
)
}
lazy val jsonView = new JsonView(rematchOf = rematches.getIfPresent)
object Env {
lazy val current = "game" boot new Env(
config = lila.common.PlayApp loadConfig "game",
db = lila.db.Env.current,
mongoCache = lila.memo.Env.current.mongoCache,
system = lila.common.PlayApp.system,
hub = lila.hub.Env.current,
getLightUser = lila.user.Env.current.lightUser,
appPath = play.api.Play.current.path.getCanonicalPath,
isProd = lila.common.PlayApp.isProd,
asyncCache = lila.memo.Env.current.asyncCache,
settingStore = lila.memo.Env.current.settingStore,
scheduler = lila.common.PlayApp.scheduler
)
// eargerly load captcher actor
private val captcher = system.actorOf(Props(new Captcher(gameRepo)), name = captcherName)
scheduler.scheduleWithFixedDelay(captcherDuration, captcherDuration) {
() => captcher ! actorApi.NewCaptcha
}
}

View File

@ -733,11 +733,12 @@ object CastleLastMove {
import reactivemongo.api.bson._
import lila.db.ByteArray.ByteArrayBSONHandler
private[game] implicit val castleLastMoveBSONHandler = new BSONHandler[BSONBinary, CastleLastMove] {
def read(bin: BSONBinary) = BinaryFormat.castleLastMove read {
ByteArrayBSONHandler read bin
private[game] implicit val castleLastMoveBSONHandler = new BSONHandler[CastleLastMove] {
def readTry(bson: BSONValue) = bson match {
case bin: BSONBinary => ByteArrayBSONHandler readTry bin map BinaryFormat.castleLastMove.read
case b => lila.db.BSON.handlerBadType(b)
}
def write(clmt: CastleLastMove) = ByteArrayBSONHandler write {
def writeTry(clmt: CastleLastMove) = ByteArrayBSONHandler writeTry {
BinaryFormat.castleLastMove write clmt
}
}

View File

@ -3,6 +3,7 @@ package lila.game
import chess.{ Color, White, Black, Clock, CheckCount, UnmovedRooks }
import Game.BSONFields._
import reactivemongo.api.bson._
import scala.util.{ Try, Success, Failure }
import Blurs.BlursBSONWriter
import chess.Centis
@ -12,8 +13,8 @@ import lila.db.ByteArray.ByteArrayBSONHandler
object GameDiff {
private type Set = BSONElement // [String, BSONValue]
private type Unset = BSONElement // [String, BSONBoolean]
private type Set = (String, BSONValue)
private type Unset = (String, BSONValue)
private type ClockHistorySide = (Centis, Vector[Centis], Boolean)
@ -26,7 +27,7 @@ object GameDiff {
val setBuilder = scala.collection.mutable.ListBuffer[Set]()
val unsetBuilder = scala.collection.mutable.ListBuffer[Unset]()
def d[A, B <: BSONValue](name: String, getter: Game => A, toBson: A => B): Unit = {
def d[A](name: String, getter: Game => A, toBson: A => BSONValue): Unit = {
val vb = getter(b)
if (getter(a) != vb) {
if (vb == None || vb == null || vb == "") unsetBuilder += (name -> bTrue)
@ -34,7 +35,7 @@ object GameDiff {
}
}
def dOpt[A, B <: BSONValue](name: String, getter: Game => A, toBson: A => Option[B]): Unit = {
def dOpt[A](name: String, getter: Game => A, toBson: A => Option[BSONValue]): Unit = {
val vb = getter(b)
if (getter(a) != vb) {
if (vb == None || vb == null || vb == "") unsetBuilder += (name -> bTrue)
@ -45,6 +46,9 @@ object GameDiff {
}
}
def dTry[A](name: String, getter: Game => A, toBson: A => Try[BSONValue]): Unit =
d[A](name, getter, a => toBson(a).get)
def getClockHistory(color: Color)(g: Game): Option[ClockHistorySide] =
for {
clk <- g.clock
@ -53,48 +57,48 @@ object GameDiff {
times = history(color)
} yield (clk.limit, times, g.flagged has color)
def clockHistoryToBytes(o: Option[ClockHistorySide]) = o.map {
case (x, y, z) => ByteArrayBSONHandler.write(BinaryFormat.clockHistory.writeSide(x, y, z))
def clockHistoryToBytes(o: Option[ClockHistorySide]) = o.flatMap {
case (x, y, z) => ByteArrayBSONHandler.writeOpt(BinaryFormat.clockHistory.writeSide(x, y, z))
}
if (a.variant.standard) d(huffmanPgn, _.pgnMoves, writeBytes compose PgnStorage.Huffman.encode)
if (a.variant.standard) dTry(huffmanPgn, _.pgnMoves, writeBytes compose PgnStorage.Huffman.encode)
else {
val f = PgnStorage.OldBin
d(oldPgn, _.pgnMoves, writeBytes compose f.encode)
d(binaryPieces, _.board.pieces, writeBytes compose BinaryFormat.piece.write)
dTry(oldPgn, _.pgnMoves, writeBytes compose f.encode)
dTry(binaryPieces, _.board.pieces, writeBytes compose BinaryFormat.piece.write)
d(positionHashes, _.history.positionHashes, w.bytes)
d(unmovedRooks, _.history.unmovedRooks, writeBytes compose BinaryFormat.unmovedRooks.write)
d(castleLastMove, makeCastleLastMove, CastleLastMove.castleLastMoveBSONHandler.write)
dTry(unmovedRooks, _.history.unmovedRooks, writeBytes compose BinaryFormat.unmovedRooks.write)
dTry(castleLastMove, makeCastleLastMove, CastleLastMove.castleLastMoveBSONHandler.writeTry)
// since variants are always OldBin
if (a.variant.threeCheck)
dOpt(checkCount, _.history.checkCount, (o: CheckCount) => o.nonEmpty option { BSONHandlers.checkCountWriter write o })
dOpt(checkCount, _.history.checkCount, (o: CheckCount) => o.nonEmpty ?? { BSONHandlers.checkCountWriter writeOpt o })
if (a.variant.crazyhouse)
dOpt(crazyData, _.board.crazyData, (o: Option[chess.variant.Crazyhouse.Data]) => o map BSONHandlers.crazyhouseDataBSONHandler.write)
}
d(turns, _.turns, w.int)
dOpt(moveTimes, _.binaryMoveTimes, (o: Option[ByteArray]) => o map ByteArrayBSONHandler.write)
dOpt(moveTimes, _.binaryMoveTimes, (o: Option[ByteArray]) => o flatMap ByteArrayBSONHandler.writeOpt)
dOpt(whiteClockHistory, getClockHistory(White), clockHistoryToBytes)
dOpt(blackClockHistory, getClockHistory(Black), clockHistoryToBytes)
dOpt(clock, _.clock, (o: Option[Clock]) => o map { c =>
BSONHandlers.clockBSONWrite(a.createdAt, c)
dOpt(clock, _.clock, (o: Option[Clock]) => o flatMap { c =>
BSONHandlers.clockBSONWrite(a.createdAt, c).toOption
})
for (i <- 0 to 1) {
import Player.BSONFields._
val name = s"p$i."
val player: Game => Player = if (i == 0) (_.whitePlayer) else (_.blackPlayer)
dOpt(s"$name$lastDrawOffer", player(_).lastDrawOffer, w.map[Option, Int, BSONInteger])
dOpt(s"$name$lastDrawOffer", player(_).lastDrawOffer, (l: Option[Int]) => l flatMap w.intO)
dOpt(s"$name$isOfferingDraw", player(_).isOfferingDraw, w.boolO)
dOpt(s"$name$proposeTakebackAt", player(_).proposeTakebackAt, w.intO)
d(s"$name$blursBits", player(_).blurs, BlursBSONWriter.write)
dTry(s"$name$blursBits", player(_).blurs, BlursBSONWriter.writeTry)
}
d(movedAt, _.movedAt, BSONJodaDateTimeHandler.write)
dTry(movedAt, _.movedAt, BSONJodaDateTimeHandler.writeTry)
(setBuilder.toList, unsetBuilder.toList)
}
private val bTrue = BSONBoolean(true)
private val writeBytes = ByteArrayBSONHandler.write _
private val writeBytes = ByteArrayBSONHandler.writeTry _
private def makeCastleLastMove(g: Game) = CastleLastMove(
lastMove = g.history.lastMove,

View File

@ -5,20 +5,18 @@ import scala.util.Random
import chess.format.{ Forsyth, FEN }
import chess.{ Color, Status }
import org.joda.time.DateTime
import reactivemongo.api.commands.GetLastError
import reactivemongo.api.bson.BSONDocument
import reactivemongo.api.WriteConcern
import reactivemongo.api.commands.WriteResult
import reactivemongo.api.{ CursorProducer, Cursor, ReadPreference }
import reactivemongo.api.bson.BSONDocument
import scala.concurrent.Future
import lila.db.BSON.BSONJodaDateTimeHandler
import lila.db.dsl._
import lila.db.{ ByteArray, isDuplicateKey }
import lila.user.User
object GameRepo {
// dirty
val coll = Env.current.gameColl
final class GameRepo(val coll: Coll) {
import BSONHandlers._
import Game.{ ID, BSONFields => F }
@ -77,19 +75,19 @@ object GameRepo {
def pov(ref: PovRef): Fu[Option[Pov]] = pov(ref.gameId, ref.color)
def remove(id: ID) = coll.remove($id(id)).void
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)(_.id) map { _.flatMap(g => Pov(g, user)) }
def recentPovsByUserFromSecondary(user: User, nb: Int): Fu[List[Pov]] =
coll.find(Query user user)
coll.ext.find(Query user user)
.sort(Query.sortCreated)
.cursor[Game](ReadPreference.secondaryPreferred)
.gather[List](nb)
.map { _.flatMap(g => Pov(g, user)) }
def gamesForAssessment(userId: String, nb: Int): Fu[List[Game]] = coll.find(
def gamesForAssessment(userId: String, nb: Int): Fu[List[Game]] = coll.ext.find(
Query.finished
++ Query.rated
++ Query.user(userId)
@ -100,7 +98,7 @@ object GameRepo {
.sort($sort asc F.createdAt)
.list[Game](nb, ReadPreference.secondaryPreferred)
def extraGamesForIrwin(userId: String, nb: Int): Fu[List[Game]] = coll.find(
def extraGamesForIrwin(userId: String, nb: Int): Fu[List[Game]] = coll.ext.find(
Query.finished
++ Query.rated
++ Query.user(userId)
@ -115,7 +113,7 @@ object GameRepo {
selector: Bdoc,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
)(implicit cp: CursorProducer[Game]) =
coll.find(selector).cursor[Game](readPreference)
coll.ext.find(selector).cursor[Game](readPreference)
def sortedCursor(
selector: Bdoc,
@ -123,12 +121,11 @@ object GameRepo {
batchSize: Int = 0,
readPreference: ReadPreference = ReadPreference.secondaryPreferred
)(implicit cp: CursorProducer[Game]): cp.ProducedCursor = {
val query = coll.find(selector).sort(sort)
query.copy(options = query.options.batchSize(batchSize)).cursor[Game](readPreference)
coll.ext.find(selector).sort(sort).batchSize(batchSize).cursor[Game](readPreference)
}
def goBerserk(pov: Pov): Funit =
coll.update($id(pov.gameId), $set(
coll.update.one($id(pov.gameId), $set(
s"${pov.color.fold(F.whitePlayer, F.blackPlayer)}.${Player.BSONFields.berserk}" -> true
)).void
@ -137,7 +134,7 @@ object GameRepo {
private def saveDiff(origin: Game, diff: GameDiff.Diff): Funit = diff match {
case (Nil, Nil) => funit
case (sets, unsets) => coll.update(
case (sets, unsets) => coll.update.one(
$id(origin.id),
nonEmptyMod("$set", $doc(sets)) ++ nonEmptyMod("$unset", $doc(unsets))
).void
@ -147,7 +144,7 @@ object GameRepo {
if (doc.isEmpty) $empty else $doc(mod -> doc)
def setRatingDiffs(id: ID, diffs: RatingDiffs) =
coll.update($id(id), $set(
coll.update.one($id(id), $set(
s"${F.whitePlayer}.${Player.BSONFields.ratingDiff}" -> diffs.white,
s"${F.blackPlayer}.${Player.BSONFields.ratingDiff}" -> diffs.black
))
@ -159,26 +156,26 @@ object GameRepo {
}
def playingRealtimeNoAi(user: User, nb: Int): Fu[List[Game.ID]] =
coll.distinct[Game.ID, List](F.id, Some(Query.nowPlaying(user.id) ++ Query.noAi ++ Query.clock(true)))
coll.distinctEasy[Game.ID, List](F.id, Query.nowPlaying(user.id) ++ Query.noAi ++ Query.clock(true))
def lastPlayedPlayingId(userId: User.ID): Fu[Option[Game.ID]] =
coll.find(Query recentlyPlaying userId, $id(true))
coll.find(Query recentlyPlaying userId, $id(true).some)
.sort(Query.sortMovedAtNoIndex)
.uno[Bdoc](readPreference = ReadPreference.secondaryPreferred)
.map { _.flatMap(_.getAs[Game.ID](F.id)) }
.map { _.flatMap(_.getAsOpt[Game.ID](F.id)) }
def allPlaying(userId: User.ID): Fu[List[Pov]] =
coll.find(Query nowPlaying userId).list[Game]()
coll.ext.find(Query nowPlaying userId).list[Game]()
.map { _ flatMap { Pov.ofUserId(_, userId) } }
def lastPlayed(user: User): Fu[Option[Pov]] =
coll.find(Query user user.id)
coll.ext.find(Query user user.id)
.sort($sort desc F.createdAt)
.list[Game](3).map {
_.sortBy(_.movedAt).lastOption flatMap { Pov(_, user) }
}
def lastFinishedRatedNotFromPosition(user: User): Fu[Option[Game]] = coll.find(
def lastFinishedRatedNotFromPosition(user: User): Fu[Option[Game]] = coll.ext.find(
Query.user(user.id) ++
Query.rated ++
Query.finished ++
@ -199,16 +196,14 @@ object GameRepo {
coll.exists($id(id) ++ Query.analysed(true))
def filterAnalysed(ids: Seq[ID]): Fu[Set[ID]] =
coll.distinct[ID, Set]("_id", ($inIds(ids) ++ $doc(
F.analysed -> true
)).some)
coll.distinctEasy[ID, Set]("_id", $inIds(ids) ++ $doc(F.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($id(id), $inc(F.bookmarks -> value)).void
coll.update.one($id(id), $inc(F.bookmarks -> value)).void
def setHoldAlert(pov: Pov, alert: Player.HoldAlert) = coll.updateField(
$id(pov.gameId), holdAlertField(pov.color), alert
@ -233,7 +228,7 @@ object GameRepo {
) map {
_.fold(Player.HoldAlert.emptyMap) { doc =>
def holdAlertOf(playerField: String) =
doc.getAs[Bdoc](playerField).flatMap(_.getAs[Player.HoldAlert](Player.BSONFields.holdAlert))
doc.child(playerField).flatMap(_.getAsOpt[Player.HoldAlert](Player.BSONFields.holdAlert))
Color.Map(
white = holdAlertOf("p0"),
black = holdAlertOf("p1")
@ -265,7 +260,7 @@ object GameRepo {
winnerColor: Option[Color],
winnerId: Option[String],
status: Status
) = coll.update(
) = coll.update.one(
$id(id),
nonEmptyMod("$set", $doc(
F.winnerId -> winnerId,
@ -280,7 +275,7 @@ object GameRepo {
)
)
def findRandomStandardCheckmate(distribution: Int): Fu[Option[Game]] = coll.find(
def findRandomStandardCheckmate(distribution: Int): Fu[Option[Game]] = coll.ext.find(
Query.mate ++ Query.variantStandard
).sort(Query.sortCreated)
.skip(Random nextInt distribution)
@ -306,7 +301,7 @@ object GameRepo {
F.checkAt -> checkInHours.map(DateTime.now.plusHours),
F.playingUids -> (g2.started && userIds.nonEmpty).option(userIds)
)
coll insert bson addFailureEffect {
coll.insert.one(bson) addFailureEffect {
case wr: WriteResult if isDuplicateKey(wr) => lila.mon.game.idCollision()
} void
} >>- {
@ -317,21 +312,21 @@ object GameRepo {
}
def removeRecentChallengesOf(userId: String) =
coll.remove(Query.created ++ Query.friend ++ Query.user(userId) ++
coll.delete.one(Query.created ++ Query.friend ++ Query.user(userId) ++
Query.createdSince(DateTime.now minusHours 1))
def setCheckAt(g: Game, at: DateTime) =
coll.update($id(g.id), $doc("$set" -> $doc(F.checkAt -> at)))
coll.update.one($id(g.id), $doc("$set" -> $doc(F.checkAt -> at)))
def unsetCheckAt(g: Game) =
coll.update($id(g.id), $doc("$unset" -> $doc(F.checkAt -> true)))
coll.update.one($id(g.id), $doc("$unset" -> $doc(F.checkAt -> true)))
def unsetPlayingUids(g: Game): Unit =
coll.update($id(g.id), $unset(F.playingUids), writeConcern = GetLastError.Unacknowledged)
coll.update(false, WriteConcern.Unacknowledged).one($id(g.id), $unset(F.playingUids))
// used to make a compound sparse index
def setImportCreatedAt(g: Game) =
coll.update($id(g.id), $set("pgni.ca" -> g.createdAt)).void
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)
@ -354,47 +349,43 @@ object GameRepo {
def withInitialFen(game: Game): Fu[Game.WithInitialFen] =
initialFen(game) map { Game.WithInitialFen(game, _) }
def withInitialFens(games: List[Game]): Fu[List[(Game, Option[FEN])]] = games.map { game =>
initialFen(game) map { game -> _ }
}.sequenceFu
def withInitialFens(games: List[Game]): Fu[List[(Game, Option[FEN])]] = Future sequence {
games.map { game =>
initialFen(game) map { game -> _ }
}
}
def count(query: Query.type => Bdoc): Fu[Int] = coll countSel query(Query)
def nbPerDay(days: Int): Fu[List[Int]] =
((days to 1 by -1).toList map { day =>
val from = DateTime.now.withTimeAtStartOfDay minusDays day
val to = from plusDays 1
coll.countSel($doc(F.createdAt -> ($gte(from) ++ $lt(to))))
}).sequenceFu
private[game] def bestOpponents(userId: String, limit: Int): Fu[List[(User.ID, Int)]] = {
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework._
coll.aggregateList(
Match($doc(F.playerUids -> userId)),
List(
Match($doc(F.playerUids -> $doc("$size" -> 2))),
Sort(Descending(F.createdAt)),
Limit(1000), // only look in the last 1000 games
Project($doc(
F.playerUids -> true,
F.id -> false
)),
UnwindField(F.playerUids),
Match($doc(F.playerUids -> $doc("$ne" -> userId))),
GroupField(F.playerUids)("gs" -> SumValue(1)),
Sort(Descending("gs")),
Limit(limit)
),
maxDocs = limit,
ReadPreference.secondaryPreferred
).map(_.flatMap { obj =>
obj.getAs[String]("_id") flatMap { id =>
obj.getAs[Int]("gs") map { id -> _ }
) { framework =>
import framework._
Match($doc(F.playerUids -> userId)) -> List(
Match($doc(F.playerUids -> $doc("$size" -> 2))),
Sort(Descending(F.createdAt)),
Limit(1000), // only look in the last 1000 games
Project($doc(
F.playerUids -> true,
F.id -> false
)),
UnwindField(F.playerUids),
Match($doc(F.playerUids -> $doc("$ne" -> userId))),
GroupField(F.playerUids)("gs" -> SumAll),
Sort(Descending("gs")),
Limit(limit)
)
}.map(_.flatMap { obj =>
obj.string("_id") flatMap { id =>
obj.int("gs") map { id -> _ }
}
})
}
def random: Fu[Option[Game]] = coll.find($empty)
def random: Fu[Option[Game]] = coll.ext.find($empty)
.sort(Query.sortCreated)
.skip(Random nextInt 1000)
.uno[Game]
@ -413,7 +404,7 @@ object GameRepo {
def lastGamesBetween(u1: User, u2: User, since: DateTime, nb: Int): Fu[List[Game]] =
List(u1, u2).forall(_.count.game > 0) ??
coll.find($doc(
coll.ext.find($doc(
F.playerUids $all List(u1.id, u2.id),
F.createdAt $gt since
)).list[Game](nb, ReadPreference.secondaryPreferred)
@ -421,13 +412,13 @@ object GameRepo {
def getSourceAndUserIds(id: ID): Fu[(Option[Source], List[User.ID])] =
coll.uno[Bdoc]($id(id), $doc(F.playerUids -> true, F.source -> true)) map {
_.fold(none[Source] -> List.empty[User.ID]) { doc =>
(doc.getAs[Int](F.source) flatMap Source.apply,
~doc.getAs[List[User.ID]](F.playerUids))
(doc.int(F.source) flatMap Source.apply,
~doc.getAsOpt[List[User.ID]](F.playerUids))
}
}
def recentAnalysableGamesByUserId(userId: User.ID, nb: Int) =
coll.find(
coll.ext.find(
Query.finished
++ Query.rated
++ Query.user(userId)

View File

@ -1,7 +1,6 @@
package lila.game
import akka.actor._
import play.api.libs.iteratee._
import play.api.libs.json._
import actorApi.{ StartGame, FinishGame }
@ -11,69 +10,69 @@ import lila.user.User
final class GamesByUsersStream {
import GamesByUsersStream._
// import GamesByUsersStream._
def apply(userIds: Set[User.ID]): Enumerator[JsObject] = {
// def apply(userIds: Set[User.ID]): Enumerator[JsObject] = {
def matches(game: Game) = game.userIds match {
case List(u1, u2) if u1 != u2 => userIds(u1) && userIds(u2)
case _ => false
}
var subscriber: Option[lila.common.Tellable] = None
// def matches(game: Game) = game.userIds match {
// case List(u1, u2) if u1 != u2 => userIds(u1) && userIds(u2)
// case _ => false
// }
// var subscriber: Option[lila.common.Tellable] = None
val enumerator = Concurrent.unicast[Game](
onStart = channel => {
subscriber = Bus.subscribeFun(classifiers: _*) {
case StartGame(game) if matches(game) => channel push game
case FinishGame(game, _, _) if matches(game) => channel push game
} some
},
onComplete = subscriber foreach { Bus.unsubscribe(_, classifiers) }
)
// val enumerator = Concurrent.unicast[Game](
// onStart = channel => {
// subscriber = Bus.subscribeFun(classifiers: _*) {
// case StartGame(game) if matches(game) => channel push game
// case FinishGame(game, _, _) if matches(game) => channel push game
// } some
// },
// onComplete = subscriber foreach { Bus.unsubscribe(_, classifiers) }
// )
enumerator &> withInitialFen &> toJson
}
// enumerator &> withInitialFen &> toJson
// }
private val withInitialFen =
Enumeratee.mapM[Game].apply[Game.WithInitialFen](GameRepo.withInitialFen)
// private val withInitialFen =
// Enumeratee.mapM[Game].apply[Game.WithInitialFen](GameRepo.withInitialFen)
private val toJson =
Enumeratee.map[Game.WithInitialFen].apply[JsObject](gameWithInitialFenWriter.writes)
}
private object GamesByUsersStream {
private val classifiers = List("startGame", "finishGame")
private implicit val fenWriter: Writes[FEN] = Writes[FEN] { f =>
JsString(f.value)
}
private val gameWithInitialFenWriter: OWrites[Game.WithInitialFen] = OWrites {
case Game.WithInitialFen(g, initialFen) =>
Json.obj(
"id" -> g.id,
"rated" -> g.rated,
"variant" -> g.variant.key,
"speed" -> g.speed.key,
"perf" -> PerfPicker.key(g),
"createdAt" -> g.createdAt,
"status" -> g.status.id,
"players" -> JsObject(g.players.zipWithIndex map {
case (p, i) => p.color.name -> Json.obj(
"userId" -> p.userId,
"rating" -> p.rating
).add("provisional" -> p.provisional)
.add("name" -> p.name)
})
).add("initialFen" -> initialFen)
.add("clock" -> g.clock.map { clock =>
Json.obj(
"initial" -> clock.limitSeconds,
"increment" -> clock.incrementSeconds,
"totalTime" -> clock.estimateTotalSeconds
)
})
.add("daysPerTurn" -> g.daysPerTurn)
}
// private val toJson =
// Enumeratee.map[Game.WithInitialFen].apply[JsObject](gameWithInitialFenWriter.writes)
// }
// private object GamesByUsersStream {
// private val classifiers = List("startGame", "finishGame")
// private implicit val fenWriter: Writes[FEN] = Writes[FEN] { f =>
// JsString(f.value)
// }
// private val gameWithInitialFenWriter: OWrites[Game.WithInitialFen] = OWrites {
// case Game.WithInitialFen(g, initialFen) =>
// Json.obj(
// "id" -> g.id,
// "rated" -> g.rated,
// "variant" -> g.variant.key,
// "speed" -> g.speed.key,
// "perf" -> PerfPicker.key(g),
// "createdAt" -> g.createdAt,
// "status" -> g.status.id,
// "players" -> JsObject(g.players.zipWithIndex map {
// case (p, i) => p.color.name -> Json.obj(
// "userId" -> p.userId,
// "rating" -> p.rating
// ).add("provisional" -> p.provisional)
// .add("name" -> p.name)
// })
// ).add("initialFen" -> initialFen)
// .add("clock" -> g.clock.map { clock =>
// Json.obj(
// "initial" -> clock.limitSeconds,
// "increment" -> clock.incrementSeconds,
// "totalTime" -> clock.estimateTotalSeconds
// )
// })
// .add("daysPerTurn" -> g.daysPerTurn)
// }
}

View File

@ -6,22 +6,28 @@ import ornicar.scalalib.Random
import java.security.SecureRandom
object IdGenerator {
final class IdGenerator(gameRepo: GameRepo) {
def uncheckedGame: Game.ID = Random nextString Game.gameIdSize
import IdGenerator._
def game: Fu[Game.ID] = {
val id = uncheckedGame
GameRepo.exists(id).flatMap {
gameRepo.exists(id).flatMap {
case true => game
case false => fuccess(id)
}
}
}
object IdGenerator {
private[this] val secureRandom = new SecureRandom()
private[this] val whiteSuffixChars = ('0' to '4') ++ ('A' to 'Z') mkString
private[this] val blackSuffixChars = ('5' to '9') ++ ('a' to 'z') mkString
def uncheckedGame: Game.ID = Random nextString Game.gameIdSize
def player(color: Color): Player.ID = {
// Trick to avoid collisions between player ids in the same game.
val suffixChars = color.fold(whiteSuffixChars, blackSuffixChars)

View File

@ -1,6 +1,7 @@
package lila.game
import play.api.libs.json._
import play.api.libs.json.JodaWrites._
import chess.format.{ FEN, Forsyth }
import chess.variant.Crazyhouse

View File

@ -35,8 +35,10 @@ case class PgnImport(
object PgnImport {
def hash(pgn: String) = ByteArray {
MessageDigest getInstance "MD5" digest
pgn.lines.map(_.replace(" ", "")).filter(_.nonEmpty).mkString("\n").getBytes("UTF-8") take 12
MessageDigest getInstance "MD5" digest {
pgn.linesIterator.map(_.replace(" ", "")).filter(_.nonEmpty)
.to(List).mkString("\n").getBytes("UTF-8")
} take 12
}
def make(

View File

@ -3,7 +3,8 @@ package lila.game
// Wrapper around newly created games. We do not know if the id is unique, yet.
case class NewGame(sloppy: Game) extends AnyVal {
def withId(id: Game.ID): Game = sloppy.withId(id)
def withUniqueId: Fu[Game] = IdGenerator.game dmap sloppy.withId
def withUniqueId(implicit idGenerator: IdGenerator): Fu[Game] =
idGenerator.game dmap sloppy.withId
def start: NewGame = NewGame(sloppy.start)

View File

@ -1,18 +1,18 @@
package lila.game
import reactivemongo.api.ReadPreference
import lila.common.paginator._
import lila.common.MaxPerPage
import lila.common.config.MaxPerPage
import lila.db.dsl._
import lila.db.paginator._
private[game] final class PaginatorBuilder(
coll: Coll,
final class PaginatorBuilder(
gameRepo: GameRepo,
cached: Cached,
maxPerPage: MaxPerPage
) {
private val readPreference = reactivemongo.api.ReadPreference.secondaryPreferred
import BSONHandlers.gameBSONHandler
def recentlyCreated(selector: Bdoc, nb: Option[Int] = None) =
@ -34,11 +34,11 @@ private[game] final class PaginatorBuilder(
private def noCacheAdapter(selector: Bdoc, sort: Bdoc): AdapterLike[Game] =
new Adapter[Game](
collection = coll,
collection = gameRepo.coll,
selector = selector,
projection = $empty,
projection = none,
sort = sort,
readPreference = readPreference
readPreference = ReadPreference.secondaryPreferred
)
private def paginator(adapter: AdapterLike[Game], page: Int): Fu[Paginator[Game]] =

View File

@ -5,10 +5,11 @@ import chess.format.pgn.{ Pgn, Tag, Tags, TagType, Parser, ParsedPgn }
import chess.format.{ FEN, pgn => chessPgn }
import chess.{ Centis, Color }
import lila.common.config.BaseUrl
import lila.common.LightUser
final class PgnDump(
netBaseUrl: String,
baseUrl: BaseUrl,
getLightUser: LightUser.Getter
) {
@ -36,7 +37,7 @@ final class PgnDump(
}
}
private def gameUrl(id: String) = s"$netBaseUrl/$id"
private def gameUrl(id: String) = s"$baseUrl/$id"
private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] =
(game.whitePlayer.userId ?? getLightUser) zip (game.blackPlayer.userId ?? getLightUser)

View File

@ -25,7 +25,7 @@ private object PgnStorage {
case object Huffman extends PgnStorage {
import org.lichess.compression.game.{ Encoder, Square => JavaSquare, Piece => JavaPiece, Role => JavaRole }
import scala.collection.JavaConversions._
import scala.jdk.CollectionConverters._
def encode(pgnMoves: PgnMoves) = ByteArray {
monitor(lila.mon.game.pgn.huffman.encode) {
@ -34,10 +34,10 @@ private object PgnStorage {
}
def decode(bytes: ByteArray, plies: Int): Decoded = monitor(lila.mon.game.pgn.huffman.decode) {
val decoded = Encoder.decode(bytes.value, plies)
val unmovedRooks = asScalaSet(decoded.unmovedRooks.flatMap(chessPos)).toSet
val unmovedRooks = decoded.unmovedRooks.asScala.view.flatMap(chessPos).to(Set)
Decoded(
pgnMoves = decoded.pgnMoves.toVector,
pieces = mapAsScalaMap(decoded.pieces).flatMap {
pieces = decoded.pieces.asScala.flatMap {
case (k, v) => chessPos(k).map(_ -> chessPiece(v))
}.toMap,
positionHashes = decoded.positionHashes,

View File

@ -3,12 +3,13 @@ package lila.game
import lila.db.dsl._
import lila.user.{ User, UserRepo }
import reactivemongo.api.ReadPreference
import reactivemongo.api.bson._
import reactivemongo.api.ReadPreference
import scala.concurrent.duration._
final class PlayTimeApi(
gameColl: Coll,
userRepo: UserRepo,
gameRepo: GameRepo,
asyncCache: lila.memo.AsyncCache.Builder,
system: akka.actor.ActorSystem
) {
@ -34,36 +35,37 @@ final class PlayTimeApi(
)
private def computeNow(userId: User.ID): Fu[Option[User.PlayTime]] =
UserRepo.getPlayTime(userId) orElse {
userRepo.getPlayTime(userId) orElse {
import reactivemongo.api.collections.bson.BSONBatchCommands.AggregationFramework._
def extractSeconds(docs: Iterable[Bdoc], onTv: Boolean): Int = ~docs.collectFirst {
case doc if doc.getAs[Boolean]("_id").has(onTv) =>
doc.getAs[Long]("ms") map { millis => (millis / 1000).toInt }
case doc if doc.getAsOpt[Boolean]("_id").has(onTv) =>
doc.long("ms") map { millis => (millis / 1000).toInt }
}.flatten
gameColl.aggregateList(
Match($doc(
F.playerUids -> userId,
F.clock $exists true
)),
List(
Project($doc(
F.id -> false,
"tv" -> $doc("$gt" -> $arr("$tv", BSONNull)),
"ms" -> $doc("$subtract" -> $arr("$ua", "$ca"))
)),
Match($doc("ms" $lt 6.hours.toMillis)),
GroupField("tv")("ms" -> SumField("ms"))
),
gameRepo.coll.aggregateList(
maxDocs = 2,
ReadPreference.secondaryPreferred
).flatMap { docs =>
) { framework =>
import framework._
Match($doc(
F.playerUids -> userId,
F.clock $exists true
)) -> List(
Project($doc(
F.id -> false,
"tv" -> $doc("$gt" -> $arr("$tv", BSONNull)),
"ms" -> $doc("$subtract" -> $arr("$ua", "$ca"))
)),
Match($doc("ms" $lt 6.hours.toMillis)),
GroupField("tv")("ms" -> SumField("ms"))
)
}.flatMap { docs =>
val onTvSeconds = extractSeconds(docs, true)
val offTvSeconds = extractSeconds(docs, false)
val pt = User.PlayTime(total = onTvSeconds + offTvSeconds, tv = onTvSeconds)
UserRepo.setPlayTime(userId, pt) inject pt.some
userRepo.setPlayTime(userId, pt) inject pt.some
}
} recover {
case e: Exception =>

View File

@ -193,7 +193,7 @@ object Player {
rating -> p.rating,
ratingDiff -> p.ratingDiff,
provisional -> w.boolO(p.provisional),
blursBits -> (!p.blurs.isEmpty).option(BlursBSONWriter write p.blurs),
blursBits -> (!p.blurs.isEmpty).??(BlursBSONWriter writeOpt p.blurs),
name -> p.name
)
}

View File

@ -1,43 +1,45 @@
package lila.game
import play.api.libs.iteratee._
import play.api.libs.ws.WS
import play.api.Play.current
import play.api.libs.ws.WSClient
import chess.format.{ Forsyth, FEN, Uci }
final class PngExport(url: String, size: Int) {
final class PngExport(
ws: WSClient,
url: String,
size: Int
) {
def fromGame(game: Game): Fu[Enumerator[Array[Byte]]] = apply(
fen = FEN(Forsyth >> game.chess),
lastMove = game.lastMoveKeys,
check = game.situation.checkSquare,
orientation = game.firstColor.some,
logHint = s"game ${game.id}"
)
// def fromGame(game: Game): Fu[Enumerator[Array[Byte]]] = apply(
// fen = FEN(Forsyth >> game.chess),
// lastMove = game.lastMoveKeys,
// check = game.situation.checkSquare,
// orientation = game.firstColor.some,
// logHint = s"game ${game.id}"
// )
def apply(
fen: FEN,
lastMove: Option[String],
check: Option[chess.Pos],
orientation: Option[chess.Color],
logHint: => String
): Fu[Enumerator[Array[Byte]]] = {
// def apply(
// fen: FEN,
// lastMove: Option[String],
// check: Option[chess.Pos],
// orientation: Option[chess.Color],
// logHint: => String
// ): Fu[Enumerator[Array[Byte]]] = {
val queryString = List(
"fen" -> fen.value.takeWhile(' ' !=),
"size" -> size.toString
) ::: List(
lastMove.map { "lastMove" -> _ },
check.map { "check" -> _.key },
orientation.map { "orientation" -> _.name }
).flatten
// val queryString = List(
// "fen" -> fen.value.takeWhile(' ' !=),
// "size" -> size.toString
// ) ::: List(
// lastMove.map { "lastMove" -> _ },
// check.map { "check" -> _.key },
// orientation.map { "orientation" -> _.name }
// ).flatten
WS.url(url).withQueryString(queryString: _*).getStream() flatMap {
case (res, body) if res.status != 200 =>
logger.warn(s"PgnExport $logHint ${fen.value} ${res.status}")
fufail(res.status.toString)
case (_, body) => fuccess(body)
}
}
// WS.url(url).withQueryString(queryString: _*).getStream() flatMap {
// case (res, body) if res.status != 200 =>
// logger.warn(s"PgnExport $logHint ${fen.value} ${res.status}")
// fufail(res.status.toString)
// case (_, body) => fuccess(body)
// }
// }
}

View File

@ -5,7 +5,7 @@ import scalaz.Validation.FlatMap._
import chess.format.{ FEN, pgn => chessPgn }
object Rewind {
final class Rewind {
private def createTags(fen: Option[FEN], game: Game) = {
val variantTag = Some(chessPgn.Tag(_.Variant, game.variant.name))

View File

@ -5,7 +5,7 @@ import scala.concurrent.duration._
import chess.format.UciDump
final class UciMemo(ttl: Duration) {
final class UciMemo(gameRepo: GameRepo, ttl: Duration) {
type UciVector = Vector[String]
@ -39,7 +39,7 @@ final class UciMemo(ttl: Duration) {
}
private def compute(game: Game, max: Int): Fu[UciVector] = for {
fen <- GameRepo initialFen game
fen <- gameRepo initialFen game
uciMoves <- UciDump(game.pgnMoves.take(max), fen.map(_.value), game.variant).future
} yield uciMoves.toVector
}

View File

@ -2,13 +2,15 @@ package lila.i18n
import play.api.Configuration
import lila.common.config.AppPath
final class Env(
appConfig: Configuration,
application: play.Application
appPath: AppPath
) {
lazy val jsDump = new JsDump(
path = s"${application.path}/${appConfig.get[String]("i18n.web_path.relative")}"
path = s"${appPath}/${appConfig.get[String]("i18n.web_path.relative")}"
)
def cli = new lila.common.Cli {

View File

@ -7,9 +7,9 @@ import play.api.Configuration
import play.api.libs.ws.WSClient
import scala.concurrent.duration._
import lila.db.dsl.Coll
import lila.common.config._
import lila.common.{ MaxPerPage, LightUser }
import lila.common.LightUser
import lila.db.dsl.Coll
case class UserConfig(
@ConfigName("paginator.max_per_page") paginatorMaxPerPage: MaxPerPage,