game module migration
parent
e0b39662c7
commit
e0819404db
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,6 +3,8 @@ package paginator
|
|||
|
||||
import scalaz.Success
|
||||
|
||||
import config.MaxPerPage
|
||||
|
||||
final class Paginator[A] private[paginator] (
|
||||
val currentPage: Int,
|
||||
val maxPerPage: MaxPerPage,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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]] =
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue