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

299 lines
11 KiB
Scala

package lila.game
import chess.variant.{ Crazyhouse, Variant }
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.util.{ Success, Try }
import lila.db.BSON
import lila.db.dsl._
object BSONHandlers {
import lila.db.ByteArray.ByteArrayBSONHandler
implicit private[game] val checkCountWriter = new BSONWriter[CheckCount] {
def writeTry(cc: CheckCount) = Success(BSONArray(cc.white, cc.black))
}
implicit val StatusBSONHandler = tryHandler[Status](
{ case BSONInteger(v) => Status(v) toTry s"No such status: $v" },
x => BSONInteger(x.id)
)
implicit private[game] val unmovedRooksHandler = tryHandler[UnmovedRooks](
{ case bin: BSONBinary => ByteArrayBSONHandler.readTry(bin) map BinaryFormat.unmovedRooks.read },
x => ByteArrayBSONHandler.writeTry(BinaryFormat.unmovedRooks write x).get
)
implicit private[game] val crazyhouseDataBSONHandler = new BSON[Crazyhouse.Data] {
import Crazyhouse._
def reads(r: BSON.Reader) =
Crazyhouse.Data(
pockets = {
val (white, black) = {
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").view.flatMap(chess.Pos.piotr).to(Set)
)
def writes(w: BSON.Writer, o: Crazyhouse.Data) =
BSONDocument(
"p" -> {
o.pockets.white.roles.map(_.forsythUpper).mkString +
o.pockets.black.roles.map(_.forsyth).mkString
},
"t" -> o.promoted.map(_.piotr).mkString
)
}
implicit private[game] val gameDrawOffersHandler = tryHandler[GameDrawOffers](
{ case arr: BSONArray =>
Success(arr.values.foldLeft(GameDrawOffers.empty) {
case (offers, BSONInteger(p)) =>
if (p > 0) offers.copy(white = offers.white incl p)
else offers.copy(black = offers.black incl -p)
case (offers, _) => offers
})
},
offers => BSONArray((offers.white ++ offers.black.map(-_)).view.map(BSONInteger.apply).toIndexedSeq)
)
import Player.playerBSONHandler
private val emptyPlayerBuilder = playerBSONHandler.read($empty)
implicit val gameBSONHandler: BSON[Game] = new BSON[Game] {
import Game.{ BSONFields => F }
import PgnImport.pgnImportBSONHandler
def reads(r: BSON.Reader): Game = {
lila.mon.game.fetch.increment()
val light = lightGameBSONHandler.readsWithPlayerIds(r, r str F.playerIds)
val startedAtTurn = r intD F.startedAtTurn
val plies = r int F.turns atMost Game.maxPlies // unlimited can cause StackOverflowError
val turnColor = Color.fromPly(plies)
val createdAt = r date F.createdAt
val playedPlies = plies - startedAtTurn
val gameVariant = Variant(r intD F.variant) | chess.variant.Standard
val decoded = r.bytesO(F.huffmanPgn).map { PgnStorage.Huffman.decode(_, playedPlies) } | {
val clm = r.get[CastleLastMove](F.castleLastMove)
val pgnMoves = PgnStorage.OldBin.decode(r bytesD F.oldPgn, playedPlies)
PgnStorage.Decoded(
pgnMoves = pgnMoves,
pieces = BinaryFormat.piece.read(r bytes F.binaryPieces, gameVariant),
positionHashes = r.getO[chess.PositionHash](F.positionHashes) | Array.empty,
unmovedRooks = r.getO[UnmovedRooks](F.unmovedRooks) | UnmovedRooks.default,
lastMove = clm.lastMove,
castles = clm.castles,
halfMoveClock = pgnMoves.reverse.indexWhere(san =>
san.contains("x") || san.headOption.exists(_.isLower)
) atLeast 0
)
}
val chessGame = ChessGame(
situation = chess.Situation(
chess.Board(
pieces = decoded.pieces,
history = ChessHistory(
lastMove = decoded.lastMove,
castles = decoded.castles,
halfMoveClock = decoded.halfMoveClock,
positionHashes = decoded.positionHashes,
unmovedRooks = decoded.unmovedRooks,
checkCount = if (gameVariant.threeCheck) {
val counts = r.intsD(F.checkCount)
CheckCount(~counts.headOption, ~counts.lastOption)
} else Game.emptyCheckCount
),
variant = gameVariant,
crazyData = gameVariant.crazyhouse option r.get[Crazyhouse.Data](F.crazyData)
),
color = turnColor
),
pgnMoves = decoded.pgnMoves,
clock = r.getO[Color => Clock](F.clock) {
clockBSONReader(createdAt, light.whitePlayer.berserk, light.blackPlayer.berserk)
} map (_(turnColor)),
turns = plies,
startedAtTurn = startedAtTurn
)
val whiteClockHistory = r bytesO F.whiteClockHistory
val blackClockHistory = r bytesO F.blackClockHistory
Game(
id = light.id,
whitePlayer = light.whitePlayer,
blackPlayer = light.blackPlayer,
chess = chessGame,
loadClockHistory = clk =>
for {
bw <- whiteClockHistory
bb <- blackClockHistory
history <-
BinaryFormat.clockHistory
.read(clk.limit, bw, bb, (light.status == Status.Outoftime).option(turnColor))
_ = lila.mon.game.loadClockHistory.increment()
} yield history,
status = light.status,
daysPerTurn = r intO F.daysPerTurn,
binaryMoveTimes = r bytesO F.moveTimes,
mode = Mode(r boolD F.rated),
bookmarks = r intD F.bookmarks,
createdAt = createdAt,
movedAt = r.dateD(F.movedAt, createdAt),
metadata = Metadata(
source = r intO F.source flatMap Source.apply,
pgnImport = r.getO[PgnImport](F.pgnImport)(PgnImport.pgnImportBSONHandler),
tournamentId = r strO F.tournamentId,
swissId = r strO F.swissId,
simulId = r strO F.simulId,
analysed = r boolD F.analysed,
drawOffers = r.getD(F.drawOffers, GameDrawOffers.empty)
)
)
}
def writes(w: BSON.Writer, o: Game) =
BSONDocument(
F.id -> o.id,
F.playerIds -> (o.whitePlayer.id + o.blackPlayer.id),
F.playerUids -> w.strListO(List(~o.whitePlayer.userId, ~o.blackPlayer.userId)),
F.whitePlayer -> w.docO(
playerBSONHandler write ((_: Color) =>
(_: Player.ID) => (_: Player.UserId) => (_: Player.Win) => o.whitePlayer
)
),
F.blackPlayer -> w.docO(
playerBSONHandler write ((_: Color) =>
(_: Player.ID) => (_: Player.UserId) => (_: Player.Win) => o.blackPlayer
)
),
F.status -> o.status,
F.turns -> o.chess.turns,
F.startedAtTurn -> w.intO(o.chess.startedAtTurn),
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),
F.blackClockHistory -> clockHistory(Black, o.clockHistory, o.chess.clock, o.flagged),
F.rated -> w.boolO(o.mode.rated),
F.variant -> o.board.variant.exotic.option(w int o.board.variant.id),
F.bookmarks -> w.intO(o.bookmarks),
F.createdAt -> w.date(o.createdAt),
F.movedAt -> w.date(o.movedAt),
F.source -> o.metadata.source.map(_.id),
F.pgnImport -> o.metadata.pgnImport,
F.tournamentId -> o.metadata.tournamentId,
F.swissId -> o.metadata.swissId,
F.simulId -> o.metadata.simulId,
F.analysed -> w.boolO(o.metadata.analysed)
) ++ {
if (o.variant.standard)
$doc(F.huffmanPgn -> PgnStorage.Huffman.encode(o.pgnMoves take Game.maxPlies))
else {
val f = PgnStorage.OldBin
$doc(
F.oldPgn -> f.encode(o.pgnMoves take Game.maxPlies),
F.binaryPieces -> BinaryFormat.piece.write(o.board.pieces),
F.positionHashes -> o.history.positionHashes,
F.unmovedRooks -> o.history.unmovedRooks,
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
)
}
}
}
implicit object lightGameBSONHandler extends lila.db.BSONReadOnly[LightGame] {
import Game.{ BSONFields => F }
import Player.playerBSONHandler
def reads(r: BSON.Reader): LightGame = {
lila.mon.game.fetchLight.increment()
readsWithPlayerIds(r, "")
}
def readsWithPlayerIds(r: BSON.Reader, playerIds: String): LightGame = {
val (whiteId, blackId) = playerIds splitAt 4
val winC = r boolO F.winnerColor map Color.fromWhite
val uids = ~r.getO[List[lila.user.User.ID]](F.playerUids)
val (whiteUid, blackUid) = (uids.headOption.filter(_.nonEmpty), uids.lift(1).filter(_.nonEmpty))
def makePlayer(field: String, color: Color, id: Player.ID, uid: Player.UserId): Player = {
val builder = r.getO[Player.Builder](field)(playerBSONHandler) | emptyPlayerBuilder
builder(color)(id)(uid)(winC map (_ == color))
}
LightGame(
id = r str F.id,
whitePlayer = makePlayer(F.whitePlayer, White, whiteId, whiteUid),
blackPlayer = makePlayer(F.blackPlayer, Black, blackId, blackUid),
status = r.get[Status](F.status)
)
}
}
private def clockHistory(
color: Color,
clockHistory: Option[ClockHistory],
clock: Option[Clock],
flagged: Option[Color]
) =
for {
clk <- clock
history <- clockHistory
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[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 writeTry {
BinaryFormat clock since write clock
}
}