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

882 lines
27 KiB
Scala
Raw Normal View History

2013-03-22 10:36:09 -06:00
package lila.game
2019-04-06 04:17:15 -06:00
import chess.Color.{ Black, White }
import chess.format.{ FEN, Uci }
import chess.opening.{ FullOpening, FullOpeningDB }
import chess.variant.{ FromPosition, Standard, Variant }
2019-12-08 01:02:12 -07:00
import chess.{ Castles, Centis, CheckCount, Clock, Color, Mode, MoveOrDrop, Speed, Status, Game => ChessGame }
import org.joda.time.DateTime
import lila.common.Sequence
2013-12-01 05:01:17 -07:00
import lila.db.ByteArray
2014-07-31 16:45:20 -06:00
import lila.rating.PerfType
import lila.rating.PerfType.Classical
2013-05-24 11:04:49 -06:00
import lila.user.User
2013-03-22 10:36:09 -06:00
case class Game(
2018-02-07 13:41:41 -07:00
id: Game.ID,
2013-03-22 10:36:09 -06:00
whitePlayer: Player,
blackPlayer: Player,
2018-04-05 09:21:47 -06:00
chess: ChessGame,
loadClockHistory: Clock => Option[ClockHistory] = _ => Game.someEmptyClockHistory,
2013-03-22 10:36:09 -06:00
status: Status,
2014-11-30 03:22:23 -07:00
daysPerTurn: Option[Int],
binaryMoveTimes: Option[ByteArray] = None,
2013-03-22 10:36:09 -06:00
mode: Mode = Mode.default,
bookmarks: Int = 0,
createdAt: DateTime = DateTime.now,
movedAt: DateTime = DateTime.now,
metadata: Metadata
) {
lazy val clockHistory = chess.clock flatMap loadClockHistory
def situation = chess.situation
2019-12-13 07:30:20 -07:00
def board = chess.situation.board
def history = chess.situation.board.history
def variant = chess.situation.board.variant
def turns = chess.turns
def clock = chess.clock
def pgnMoves = chess.pgnMoves
2013-03-22 11:53:13 -06:00
val players = List(whitePlayer, blackPlayer)
2018-04-05 09:18:11 -06:00
def player(color: Color): Player = color.fold(whitePlayer, blackPlayer)
2013-03-22 11:53:13 -06:00
2018-04-05 10:03:52 -06:00
def player(playerId: Player.ID): Option[Player] =
2013-03-22 11:53:13 -06:00
players find (_.id == playerId)
def player(user: User): Option[Player] =
players find (_ isUser user)
2014-02-17 02:12:19 -07:00
def player(c: Color.type => Color): Player = player(c(Color))
2013-03-22 11:53:13 -06:00
def isPlayerFullId(player: Player, fullId: String): Boolean =
(fullId.lengthIs == Game.fullIdSize) && player.id == (fullId drop Game.gameIdSize)
2013-03-22 11:53:13 -06:00
def player: Player = player(turnColor)
def playerByUserId(userId: User.ID): Option[Player] = players.find(_.userId contains userId)
def opponentByUserId(userId: User.ID): Option[Player] = playerByUserId(userId) map opponent
def hasUserIds(userId1: User.ID, userId2: User.ID) =
playerByUserId(userId1).isDefined && playerByUserId(userId2).isDefined
def hasUserId(userId: User.ID) = playerByUserId(userId).isDefined
2013-03-22 11:53:13 -06:00
def opponent(p: Player): Player = opponent(p.color)
def opponent(c: Color): Player = player(!c)
2020-09-21 03:31:16 -06:00
lazy val naturalOrientation =
if (variant.racingKings) White else Color.fromWhite(whitePlayer before blackPlayer)
2013-12-05 14:47:10 -07:00
2018-06-25 21:33:20 -06:00
def turnColor = chess.player
2013-03-22 11:53:13 -06:00
def turnOf(p: Player): Boolean = p == player
2019-12-13 07:30:20 -07:00
def turnOf(c: Color): Boolean = c == turnColor
def turnOf(u: User): Boolean = player(u) ?? turnOf
2013-03-22 11:53:13 -06:00
def playedTurns = turns - chess.startedAtTurn
2014-03-05 13:11:55 -07:00
def flagged = (status == Status.Outoftime).option(turnColor)
2013-03-22 11:53:13 -06:00
def fullIdOf(player: Player): Option[String] =
(players contains player) option s"$id${player.id}"
2013-03-22 11:53:13 -06:00
def fullIdOf(color: Color): String = s"$id${player(color).id}"
2013-03-22 11:53:13 -06:00
2013-12-05 12:40:11 -07:00
def tournamentId = metadata.tournamentId
2019-12-13 07:30:20 -07:00
def simulId = metadata.simulId
2020-05-04 15:16:36 -06:00
def swissId = metadata.swissId
2013-03-22 11:53:13 -06:00
def isTournament = tournamentId.isDefined
2019-12-13 07:30:20 -07:00
def isSimul = simulId.isDefined
2020-05-04 15:16:36 -06:00
def isSwiss = swissId.isDefined
2020-05-06 17:07:03 -06:00
def isMandatory = isTournament || isSimul || isSwiss
2019-12-13 07:30:20 -07:00
def isClassical = perfType contains Classical
2014-06-09 11:51:02 -06:00
def nonMandatory = !isMandatory
2013-03-22 11:53:13 -06:00
2015-04-03 17:44:02 -06:00
def hasChat = !isTournament && !isSimul && nonAi
2013-03-22 11:53:13 -06:00
// we can't rely on the clock,
// because if moretime was given,
// elapsed time is no longer representing the game duration
2020-05-05 22:11:15 -06:00
def durationSeconds: Option[Int] =
2020-08-16 07:06:40 -06:00
movedAt.getSeconds - createdAt.getSeconds match {
2020-05-05 22:11:15 -06:00
case seconds if seconds > 60 * 60 * 12 => none // no way it lasted more than 12 hours, come on.
case seconds => seconds.toInt.some
}
2015-12-26 11:14:19 -07:00
2020-05-05 22:11:15 -06:00
private def everyOther[A](l: List[A]): List[A] =
l match {
case a :: _ :: tail => a :: everyOther(tail)
case _ => l
}
2017-03-25 11:36:41 -06:00
2017-03-20 06:22:12 -06:00
def moveTimes(color: Color): Option[List[Centis]] = {
for {
clk <- clock
inc = clk.incrementOf(color)
2017-03-20 06:22:12 -06:00
history <- clockHistory
clocks = history(color)
2017-03-20 06:22:12 -06:00
} yield Centis(0) :: {
val pairs = clocks.iterator zip clocks.iterator.drop(1)
2017-03-31 16:19:56 -06:00
// We need to determine if this color's last clock had inc applied.
2020-08-17 16:17:13 -06:00
// if finished and history.size == playedTurns then game was ended
2017-03-31 16:19:56 -06:00
// by a players move, such as with mate or autodraw. In this case,
// the last move of the game, and the only one without inc, is the
// last entry of the clock history for !turnColor.
//
// On the other hand, if history.size is more than playedTurns,
// then the game ended during a players turn by async event, and
// the last recorded time is in the history for turnColor.
val noLastInc = finished && (history.size <= playedTurns) == (color != turnColor)
2020-09-21 01:28:28 -06:00
pairs map { case (first, second) =>
{
val d = first - second
if (pairs.hasNext || !noLastInc) d + inc else d
} nonNeg
} toList
}
2017-03-20 06:22:12 -06:00
} orElse binaryMoveTimes.map { binary =>
2017-03-25 11:36:41 -06:00
// TODO: make movetime.read return List after writes are disabled.
2017-03-25 10:19:04 -06:00
val base = BinaryFormat.moveTime.read(binary, playedTurns)
2019-12-13 07:30:20 -07:00
val mts = if (color == startColor) base else base.drop(1)
2017-03-25 11:36:41 -06:00
everyOther(mts.toList)
2017-03-20 06:22:12 -06:00
}
2019-12-13 07:30:20 -07:00
def moveTimes: Option[Vector[Centis]] =
for {
a <- moveTimes(startColor)
b <- moveTimes(!startColor)
} yield Sequence.interleave(a, b)
2017-03-23 04:51:03 -06:00
def bothClockStates: Option[Vector[Centis]] = clockHistory.map(_ bothClockStates startColor)
def pgnMoves(color: Color): PgnMoves = {
val pivot = if (color == startColor) 0 else 1
pgnMoves.zipWithIndex.collect {
case (e, i) if (i % 2) == pivot => e
}
}
2019-08-20 10:05:05 -06:00
// apply a move
2013-03-22 11:53:13 -06:00
def update(
2019-12-13 07:30:20 -07:00
game: ChessGame, // new chess position
moveOrDrop: MoveOrDrop,
blur: Boolean = false
): Progress = {
2017-01-14 13:07:16 -07:00
def copyPlayer(player: Player) =
2017-05-08 05:19:23 -06:00
if (blur && moveOrDrop.fold(_.color, _.color) == player.color)
player.copy(
blurs = player.blurs.add(playerMoves(player.color))
)
2017-01-14 13:07:16 -07:00
else player
2013-03-22 11:53:13 -06:00
2018-02-07 09:40:20 -07:00
// This must be computed eagerly
// because it depends on the current time
val newClockHistory = for {
clk <- game.clock
2019-12-13 07:30:20 -07:00
ch <- clockHistory
2018-02-07 09:40:20 -07:00
} yield ch.record(turnColor, clk)
2013-03-22 11:53:13 -06:00
val updated = copy(
whitePlayer = copyPlayer(whitePlayer),
blackPlayer = copyPlayer(blackPlayer),
2018-04-05 09:21:47 -06:00
chess = game,
2020-08-16 06:37:41 -06:00
binaryMoveTimes = (!isPgnImport && chess.clock.isEmpty).option {
2017-03-21 05:00:30 -06:00
BinaryFormat.moveTime.write {
binaryMoveTimes.?? { t =>
BinaryFormat.moveTime.read(t, playedTurns)
} :+ Centis(nowCentis - movedAt.getCentis).nonNeg
2017-03-21 05:00:30 -06:00
}
},
loadClockHistory = _ => newClockHistory,
status = game.situation.status | status,
movedAt = DateTime.now
)
2013-03-22 11:53:13 -06:00
2015-06-15 17:56:27 -06:00
val state = Event.State(
color = game.situation.color,
turns = game.turns,
status = (status != updated.status) option updated.status,
winner = game.situation.winner,
2015-06-15 17:56:27 -06:00
whiteOffersDraw = whitePlayer.isOfferingDraw,
blackOffersDraw = blackPlayer.isOfferingDraw
)
val clockEvent = updated.chess.clock map Event.Clock.apply orElse {
updated.playableCorrespondenceClock map Event.CorrespondenceClock.apply
2013-12-05 12:40:11 -07:00
}
2013-03-22 10:36:09 -06:00
val events = moveOrDrop.fold(
Event.Move(_, game.situation, state, clockEvent, updated.board.crazyData),
Event.Drop(_, game.situation, state, clockEvent, updated.board.crazyData)
2019-12-08 22:54:33 -07:00
) :: {
2019-12-13 07:30:20 -07:00
// abstraction leak, I know.
(updated.board.variant.threeCheck && game.situation.check) ?? List(
Event.CheckCount(
white = updated.history.checkCount.white,
black = updated.history.checkCount.black
2019-12-13 07:30:20 -07:00
)
)
}
2013-03-22 10:36:09 -06:00
Progress(this, updated, events)
2013-03-22 11:53:13 -06:00
}
2020-05-05 22:11:15 -06:00
def lastMoveKeys: Option[String] =
history.lastMove map {
case Uci.Drop(target, _) => s"$target$target"
case m: Uci.Move => m.keys
}
2020-05-05 22:11:15 -06:00
def updatePlayer(color: Color, f: Player => Player) =
color.fold(
copy(whitePlayer = f(whitePlayer)),
copy(blackPlayer = f(blackPlayer))
)
2013-03-22 11:53:13 -06:00
2020-05-05 22:11:15 -06:00
def updatePlayers(f: Player => Player) =
copy(
whitePlayer = f(whitePlayer),
blackPlayer = f(blackPlayer)
)
2013-03-22 11:53:13 -06:00
2019-12-13 07:30:20 -07:00
def start =
if (started) this
else
copy(
status = Status.Started,
2020-08-17 16:17:13 -06:00
mode = Mode(mode.rated && userIds.distinct.size == 2)
2019-12-13 07:30:20 -07:00
)
2013-03-22 11:53:13 -06:00
def startClock =
clock map { c =>
start.withClock(c.start)
}
2020-05-05 22:11:15 -06:00
def correspondenceClock: Option[CorrespondenceClock] =
daysPerTurn map { days =>
val increment = days * 24 * 60 * 60
val secondsLeft = (movedAt.getSeconds + increment - nowSeconds).toInt max 0
CorrespondenceClock(
increment = increment,
whiteTime = turnColor.fold(secondsLeft, increment).toFloat,
blackTime = turnColor.fold(increment, secondsLeft).toFloat
)
}
def playableCorrespondenceClock: Option[CorrespondenceClock] =
playable ?? correspondenceClock
2013-03-22 11:53:13 -06:00
def speed = Speed(chess.clock.map(_.config))
2014-07-26 08:06:32 -06:00
2019-12-13 07:30:20 -07:00
def perfKey = PerfPicker.key(this)
def perfType = PerfType(perfKey)
2014-07-31 16:45:20 -06:00
2013-03-22 11:53:13 -06:00
def started = status >= Status.Started
def notStarted = !started
def aborted = status == Status.Aborted
2013-03-22 10:36:09 -06:00
2016-08-07 06:09:08 -06:00
def playedThenAborted = aborted && bothPlayersHaveMoved
def abort = copy(status = Status.Aborted)
2015-09-20 14:04:02 -06:00
def playable = status < Status.Aborted && !imported
2013-03-22 10:36:09 -06:00
def playableEvenImported = status < Status.Aborted
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playableBy(p: Player): Boolean = playable && turnOf(p)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playableBy(c: Color): Boolean = playableBy(player(c))
2013-03-22 10:36:09 -06:00
2013-05-18 13:14:57 -06:00
def playableByAi: Boolean = playable && player.isAi
2015-12-23 04:02:46 -07:00
def mobilePushable = isCorrespondence && playable && nonAi
def alarmable = hasCorrespondenceClock && playable && nonAi
2013-03-22 11:53:13 -06:00
def continuable = status != Status.Mate && status != Status.Stalemate
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def aiLevel: Option[Int] = players find (_.isAi) flatMap (_.aiLevel)
2013-03-22 10:36:09 -06:00
2016-09-05 08:15:24 -06:00
def hasAi: Boolean = players.exists(_.isAi)
2019-12-13 07:30:20 -07:00
def nonAi = !hasAi
2013-03-22 10:36:09 -06:00
2016-09-05 08:15:24 -06:00
def aiPov: Option[Pov] = players.find(_.isAi).map(_.color) map pov
2020-05-05 22:11:15 -06:00
def mapPlayers(f: Player => Player) =
copy(
whitePlayer = f(whitePlayer),
blackPlayer = f(blackPlayer)
)
2013-03-22 10:36:09 -06:00
2021-03-11 08:28:33 -07:00
def drawOffers = metadata.drawOffers
2013-03-22 11:53:13 -06:00
def playerCanOfferDraw(color: Color) =
started && playable &&
turns >= 2 &&
!player(color).isOfferingDraw &&
2020-04-19 08:49:35 -06:00
!opponent(color).isAi &&
2021-03-11 08:28:33 -07:00
!playerHasOfferedDrawRecently(color)
def playerHasOfferedDrawRecently(color: Color) =
drawOffers.lastBy(color) ?? (_ >= turns - 20)
2013-03-22 10:36:09 -06:00
2021-03-11 08:28:33 -07:00
def offerDraw(color: Color) = copy(
metadata = metadata.copy(drawOffers = drawOffers.add(color, turns))
).updatePlayer(color, _.offerDraw)
2013-03-22 10:36:09 -06:00
2019-12-08 01:02:12 -07:00
def playerCouldRematch =
2019-08-24 09:20:36 -06:00
finishedOrAborted &&
nonMandatory &&
2019-12-13 07:30:20 -07:00
!boosted && ! {
2020-09-21 01:28:28 -06:00
hasAi && variant == FromPosition && clock.exists(_.config.limitSeconds < 60)
}
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerCanProposeTakeback(color: Color) =
2015-04-03 17:44:02 -06:00
started && playable && !isTournament && !isSimul &&
2013-03-22 11:53:13 -06:00
bothPlayersHaveMoved &&
2013-05-24 11:04:49 -06:00
!player(color).isProposingTakeback &&
2013-03-22 11:53:13 -06:00
!opponent(color).isProposingTakeback
2013-03-22 10:36:09 -06:00
2015-09-12 08:27:35 -06:00
def boosted = rated && finished && bothPlayersHaveMoved && playedTurns < 10
2020-05-05 22:11:15 -06:00
def moretimeable(color: Color) =
playable && nonMandatory && {
clock.??(_ moretimeable color) || correspondenceClock.??(_ moretimeable color)
}
2013-03-22 10:36:09 -06:00
2014-12-02 03:16:25 -07:00
def abortable = status == Status.Started && playedTurns < 2 && nonMandatory
2013-03-22 10:36:09 -06:00
2016-12-05 09:53:22 -07:00
def berserkable = clock.??(_.config.berserkable) && status == Status.Started && playedTurns < 2
2013-03-22 10:36:09 -06:00
def goBerserk(color: Color): Option[Progress] =
2015-08-18 04:47:18 -06:00
clock.ifTrue(berserkable && !player(color).berserk).map { c =>
val newClock = c goBerserk color
2019-12-13 07:30:20 -07:00
Progress(
this,
copy(
chess = chess.copy(clock = Some(newClock)),
loadClockHistory = _ =>
clockHistory.map(history => {
if (history(color).isEmpty) history
else history.reset(color).record(color, newClock)
})
).updatePlayer(color, _.goBerserk)
) ++
2017-07-21 16:53:08 -06:00
List(
Event.ClockInc(color, -c.config.berserkPenalty),
Event.Clock(newClock), // BC
Event.Berserk(color)
)
2015-08-18 04:47:18 -06:00
}
2019-12-13 07:30:20 -07:00
def resignable = playable && !abortable
def drawable = playable && !abortable
def forceResignable = resignable && nonAi && !fromFriend && hasClock && !isSwiss
2013-03-22 10:36:09 -06:00
2017-07-21 16:53:08 -06:00
def finish(status: Status, winner: Option[Color]) = {
val newClock = clock map { _.stop }
Progress(
this,
copy(
status = status,
whitePlayer = whitePlayer.finish(winner contains White),
blackPlayer = blackPlayer.finish(winner contains Black),
2018-04-05 09:21:47 -06:00
chess = chess.copy(clock = newClock),
2019-12-13 07:30:20 -07:00
loadClockHistory = clk =>
clockHistory map { history =>
// If not already finished, we're ending due to an event
// in the middle of a turn, such as resignation or draw
// acceptance. In these cases, record a final clock time
// for the active color. This ensures the end time in
// clockHistory always matches the final clock time on
// the board.
if (!finished) history.record(turnColor, clk)
else history
}
2017-07-21 16:53:08 -06:00
),
// Events here for BC.
List(Event.End(winner)) ::: newClock.??(c => List(Event.Clock(c)))
)
}
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
def rated = mode.rated
2014-05-01 06:08:54 -06:00
def casual = !rated
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def finished = status >= Status.Mate
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def finishedOrAborted = finished || aborted
2013-03-22 10:36:09 -06:00
def accountable = playedTurns >= 2 || isTournament
def replayable = isPgnImport || finished || (aborted && bothPlayersHaveMoved)
2014-02-02 05:07:00 -07:00
2016-09-04 11:07:21 -06:00
def analysable =
replayable && playedTurns > 4 &&
Game.analysableVariants(variant) &&
!Game.isOldHorde(this)
2014-02-27 17:18:22 -07:00
def ratingVariant =
if (isTournament && variant.fromPosition) Standard
else variant
2019-12-13 07:30:20 -07:00
def fromPosition = variant.fromPosition || source.??(Source.Position ==)
def imported = source contains Source.Import
2014-02-27 17:18:22 -07:00
2019-12-13 07:30:20 -07:00
def fromPool = source contains Source.Pool
def fromLobby = source contains Source.Lobby
def fromFriend = source contains Source.Friend
2021-02-01 10:30:05 -07:00
def fromApi = source contains Source.Api
2016-12-01 03:59:09 -07:00
2013-03-22 11:53:13 -06:00
def winner = players find (_.wins)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def loser = winner map opponent
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def winnerColor: Option[Color] = winner map (_.color)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def winnerUserId: Option[String] = winner flatMap (_.userId)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def loserUserId: Option[String] = loser flatMap (_.userId)
2013-03-22 10:36:09 -06:00
2018-04-05 09:18:11 -06:00
def wonBy(c: Color): Option[Boolean] = winner map (_.color == c)
2013-03-22 10:36:09 -06:00
2018-04-05 09:18:11 -06:00
def lostBy(c: Color): Option[Boolean] = winner map (_.color != c)
2015-07-18 16:20:06 -06:00
2015-07-21 10:05:44 -06:00
def drawn = finished && winner.isEmpty
2015-07-18 16:20:06 -06:00
def outoftime(withGrace: Boolean): Boolean =
if (isCorrespondence) outoftimeCorrespondence else outoftimeClock(withGrace)
2020-05-05 22:11:15 -06:00
private def outoftimeClock(withGrace: Boolean): Boolean =
clock ?? { c =>
started && playable && {
c.outOfTime(turnColor, withGrace) || {
!c.isRunning && c.players.exists(_.elapsed.centis > 0)
}
2020-05-05 22:11:15 -06:00
}
}
private def outoftimeCorrespondence: Boolean =
playableCorrespondenceClock ?? { _ outoftime turnColor }
2014-11-30 06:03:09 -07:00
def isCorrespondence = speed == Speed.Correspondence
2015-10-06 11:46:17 -06:00
def isSwitchable = nonAi && (isCorrespondence || isSimul)
2015-09-22 12:31:31 -06:00
2013-03-22 11:53:13 -06:00
def hasClock = clock.isDefined
2013-03-22 10:36:09 -06:00
2015-01-15 08:49:14 -07:00
def hasCorrespondenceClock = daysPerTurn.isDefined
def isUnlimited = !hasClock && !hasCorrespondenceClock
2013-10-09 07:26:17 -06:00
def isClockRunning = clock ?? (_.isRunning)
2013-03-22 10:36:09 -06:00
2018-04-05 09:21:47 -06:00
def withClock(c: Clock) = Progress(this, copy(chess = chess.copy(clock = Some(c))))
2013-03-22 10:36:09 -06:00
def correspondenceGiveTime = Progress(this, copy(movedAt = DateTime.now))
def estimateClockTotalTime = clock.map(_.estimateTotalSeconds)
2015-07-18 16:20:06 -06:00
2019-12-13 07:30:20 -07:00
def estimateTotalTime =
estimateClockTotalTime orElse
correspondenceClock.map(_.estimateTotalTime) getOrElse 1200
2013-03-22 10:36:09 -06:00
2020-05-05 22:11:15 -06:00
def timeForFirstMove: Centis =
Centis ofSeconds {
import Speed._
val base = if (isTournament) speed match {
case UltraBullet => 11
case Bullet => 16
case Blitz => 21
case Rapid => 25
case _ => 30
2019-12-13 07:30:20 -07:00
}
2020-05-05 22:11:15 -06:00
else
speed match {
case UltraBullet => 15
case Bullet => 20
case Blitz => 25
case Rapid => 30
case _ => 35
}
if (variant.chess960) base * 5 / 4
else base
}
2017-10-22 16:45:53 -06:00
def expirable =
!bothPlayersHaveMoved && source.exists(Source.expirable.contains) && playable && nonAi && clock.exists(
!_.isRunning
)
2020-05-05 22:11:15 -06:00
def timeBeforeExpiration: Option[Centis] =
expirable option {
Centis.ofMillis(movedAt.getMillis - nowMillis + timeForFirstMove.millis).nonNeg
}
2020-05-05 22:11:15 -06:00
def playerWhoDidNotMove: Option[Player] =
playedTurns match {
case 0 => player(startColor).some
case 1 => player(!startColor).some
case _ => none
}
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
def onePlayerHasMoved = playedTurns > 0
2014-12-02 03:16:25 -07:00
def bothPlayersHaveMoved = playedTurns > 1
2013-03-22 10:36:09 -06:00
2020-09-21 03:31:16 -06:00
def startColor = Color.fromPly(chess.startedAtTurn)
def playerMoves(color: Color): Int =
if (color == startColor) (playedTurns + 1) / 2
else playedTurns / 2
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerHasMoved(color: Color) = playerMoves(color) > 0
2013-03-22 10:36:09 -06:00
2017-05-08 05:19:23 -06:00
def playerBlurPercent(color: Color): Int =
if (playedTurns > 5) (player(color).blurs.nb * 100) / playerMoves(color)
else 0
2013-03-22 10:36:09 -06:00
def isBeingPlayed = !isPgnImport && !finishedOrAborted
2013-08-01 08:44:38 -06:00
def olderThan(seconds: Int) = movedAt isBefore DateTime.now.minusSeconds(seconds)
2013-08-01 08:44:38 -06:00
2017-08-02 04:34:58 -06:00
def justCreated = createdAt isAfter DateTime.now.minusSeconds(1)
def unplayed = !bothPlayersHaveMoved && (createdAt isBefore Game.unplayedDate)
2013-03-22 10:36:09 -06:00
2020-05-05 22:11:15 -06:00
def abandoned =
(status <= Status.Started) && {
movedAt isBefore {
if (hasAi && hasClock) Game.aiAbandonedDate
2020-05-05 22:11:15 -06:00
else Game.abandonedDate
}
}
2013-03-22 10:36:09 -06:00
2015-09-16 09:23:52 -06:00
def forecastable = started && playable && isCorrespondence && !hasAi
2015-09-15 12:32:57 -06:00
2013-03-22 11:53:13 -06:00
def hasBookmarks = bookmarks > 0
2013-03-22 10:36:09 -06:00
2014-02-12 12:24:44 -07:00
def showBookmarks = hasBookmarks ?? bookmarks.toString
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def userIds = playerMaps(_.userId)
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
def twoUserIds: Option[(User.ID, User.ID)] =
for {
w <- whitePlayer.userId
b <- blackPlayer.userId
if w != b
} yield w -> b
2013-12-17 15:20:18 -07:00
def userRatings = playerMaps(_.rating)
2013-03-22 10:36:09 -06:00
2020-05-05 22:11:15 -06:00
def averageUsersRating =
userRatings match {
case a :: b :: Nil => Some((a + b) / 2)
case a :: Nil => Some((a + 1500) / 2)
case _ => None
}
2013-03-22 10:36:09 -06:00
def withTournamentId(id: String) = copy(metadata = metadata.copy(tournamentId = id.some))
2020-05-04 10:30:17 -06:00
def withSwissId(id: String) = copy(metadata = metadata.copy(swissId = id.some))
2015-04-01 14:22:28 -06:00
def withSimulId(id: String) = copy(metadata = metadata.copy(simulId = id.some))
2013-03-22 10:36:09 -06:00
def withId(newId: String) = copy(id = newId)
2013-03-22 10:36:09 -06:00
2013-12-05 12:40:11 -07:00
def source = metadata.source
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
def pgnImport = metadata.pgnImport
2013-03-22 11:53:13 -06:00
def isPgnImport = pgnImport.isDefined
2013-03-22 10:36:09 -06:00
2020-05-05 22:11:15 -06:00
def resetTurns =
copy(
chess = chess.copy(turns = 0, startedAtTurn = 0)
)
2016-02-25 03:03:09 -07:00
lazy val opening: Option[FullOpening.AtPly] =
if (fromPosition || !Variant.openingSensibleVariants(variant)) none
2016-02-23 22:40:50 -07:00
else FullOpeningDB search pgnMoves
2016-05-13 03:28:35 -06:00
def synthetic = id == Game.syntheticId
2016-02-15 20:29:14 -07:00
2020-01-19 09:31:28 -07:00
private def playerMaps[A](f: Player => Option[A]): List[A] = players flatMap f
2016-06-02 05:51:18 -06:00
2019-12-13 07:30:20 -07:00
def pov(c: Color) = Pov(this, c)
2018-04-05 10:03:52 -06:00
def playerIdPov(playerId: Player.ID): Option[Pov] = player(playerId) map { Pov(this, _) }
2019-12-13 07:30:20 -07:00
def whitePov = pov(White)
def blackPov = pov(Black)
def playerPov(p: Player) = pov(p.color)
def loserPov = loser map playerPov
2018-12-06 20:46:24 -07:00
2020-01-04 18:43:14 -07:00
def setAnalysed = copy(metadata = metadata.copy(analysed = true))
2020-04-26 15:34:37 -06:00
def secondsSinceCreation = (nowSeconds - createdAt.getSeconds).toInt
def drawReason = {
if (drawOffers.normalizedPlies.exists(turns <=)) DrawReason.MutualAgreement.some
else if (variant.fiftyMoves(history)) DrawReason.FiftyMoves.some
else if (history.threefoldRepetition) DrawReason.ThreefoldRepetition.some
else if (variant.isInsufficientMaterial(board)) DrawReason.InsufficientMaterial.some
else None
}
2018-12-06 20:46:24 -07:00
override def toString = s"""Game($id)"""
2013-03-22 10:36:09 -06:00
}
object Game {
type ID = String
2019-11-01 03:05:16 -06:00
case class Id(value: String) extends AnyVal with StringValue {
def full(playerId: PlayerId) = FullId(s"$value{$playerId.value}")
}
case class FullId(value: String) extends AnyVal with StringValue {
2019-12-13 07:30:20 -07:00
def gameId = Id(value take gameIdSize)
2019-11-01 03:05:16 -06:00
def playerId = PlayerId(value drop gameIdSize)
}
case class PlayerId(value: String) extends AnyVal with StringValue
2016-10-30 17:21:48 -06:00
case class WithInitialFen(game: Game, fen: Option[FEN])
2016-05-13 03:28:35 -06:00
val syntheticId = "synthetic"
val maxPlayingRealtime = 100 // plus 200 correspondence games
2016-07-29 03:04:16 -06:00
val maxPlies = 600 // unlimited can cause StackOverflowError
val analysableVariants: Set[Variant] = Set(
chess.variant.Standard,
2016-10-31 13:54:42 -06:00
chess.variant.Crazyhouse,
chess.variant.Chess960,
chess.variant.KingOfTheHill,
2015-06-21 13:03:34 -06:00
chess.variant.ThreeCheck,
2016-11-15 05:43:49 -07:00
chess.variant.Antichess,
2016-08-13 09:36:34 -06:00
chess.variant.FromPosition,
chess.variant.Horde,
chess.variant.Atomic,
chess.variant.RacingKings
)
2014-08-13 02:51:15 -06:00
val unanalysableVariants: Set[Variant] = Variant.all.toSet -- analysableVariants
val variantsWhereWhiteIsBetter: Set[Variant] = Set(
chess.variant.ThreeCheck,
chess.variant.Atomic,
2015-03-09 08:22:54 -06:00
chess.variant.Horde,
2015-09-04 14:25:57 -06:00
chess.variant.RacingKings,
chess.variant.Antichess
)
val blindModeVariants: Set[Variant] = Set(
chess.variant.Standard,
chess.variant.Chess960,
chess.variant.KingOfTheHill,
chess.variant.ThreeCheck,
chess.variant.FromPosition,
chess.variant.Antichess,
chess.variant.Atomic,
2021-06-06 23:43:38 -06:00
chess.variant.RacingKings,
chess.variant.Horde
)
2016-09-04 11:07:21 -06:00
val hordeWhitePawnsSince = new DateTime(2015, 4, 11, 10, 0)
def isOldHorde(game: Game) =
game.variant == chess.variant.Horde &&
game.createdAt.isBefore(Game.hordeWhitePawnsSince)
2020-05-05 22:11:15 -06:00
def allowRated(variant: Variant, clock: Option[Clock.Config]) =
variant.standard || {
clock ?? { c =>
c.estimateTotalTime >= Centis(3000) &&
c.limitSeconds > 0 || c.incrementSeconds > 1
}
2020-05-05 22:11:15 -06:00
}
2019-12-13 07:30:20 -07:00
val gameIdSize = 8
2013-03-22 10:36:09 -06:00
val playerIdSize = 4
2019-12-13 07:30:20 -07:00
val fullIdSize = 12
val tokenSize = 4
2013-03-22 10:36:09 -06:00
val unplayedHours = 24
2019-12-13 07:30:20 -07:00
def unplayedDate = DateTime.now minusHours unplayedHours
2014-12-03 02:35:06 -07:00
val abandonedDays = 21
def abandonedDate = DateTime.now minusDays abandonedDays
2013-03-22 10:36:09 -06:00
val aiAbandonedHours = 6
2019-12-13 07:30:20 -07:00
def aiAbandonedDate = DateTime.now minusHours aiAbandonedHours
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
def takeGameId(fullId: String) = fullId take gameIdSize
2013-05-17 18:38:39 -06:00
def takePlayerId(fullId: String) = fullId drop gameIdSize
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
val idRegex = """[\w-]{8}""".r
def validId(id: ID) = idRegex matches id
2020-05-05 22:11:15 -06:00
def isBoardCompatible(game: Game): Boolean =
game.clock.fold(true) { c =>
isBoardCompatible(c.config) || {
(game.hasAi || game.fromFriend) && chess.Speed(c.config) >= Speed.Blitz
}
2020-05-05 22:11:15 -06:00
}
2021-01-14 07:18:54 -07:00
def isBoardCompatible(clock: Clock.Config): Boolean =
chess.Speed(clock) >= Speed.Rapid
def isBotCompatible(game: Game): Boolean = {
2021-02-07 05:46:37 -07:00
game.hasAi || game.fromFriend || game.fromApi
} && isBotCompatible(game.speed)
def isBotCompatible(speed: Speed): Boolean = speed >= Speed.Bullet
private[game] val emptyCheckCount = CheckCount(0, 0)
private[game] val someEmptyClockHistory = Some(ClockHistory())
2013-05-07 17:44:26 -06:00
def make(
2019-12-13 07:30:20 -07:00
chess: ChessGame,
whitePlayer: Player,
blackPlayer: Player,
mode: Mode,
source: Source,
pgnImport: Option[PgnImport],
daysPerTurn: Option[Int] = None
2018-08-25 09:04:34 -06:00
): NewGame = {
2018-08-25 07:57:19 -06:00
val createdAt = DateTime.now
2019-12-13 07:30:20 -07:00
NewGame(
Game(
id = IdGenerator.uncheckedGame,
whitePlayer = whitePlayer,
blackPlayer = blackPlayer,
chess = chess,
status = Status.Created,
daysPerTurn = daysPerTurn,
mode = mode,
2021-03-11 08:28:33 -07:00
metadata = metadata(source).copy(pgnImport = pgnImport),
2019-12-13 07:30:20 -07:00
createdAt = createdAt,
movedAt = createdAt
)
)
}
2013-03-22 11:53:13 -06:00
2020-05-14 20:03:22 -06:00
def metadata(source: Source) =
Metadata(
source = source.some,
pgnImport = none,
tournamentId = none,
swissId = none,
simulId = none,
2021-03-11 08:28:33 -07:00
analysed = false,
drawOffers = GameDrawOffers.empty
2020-05-14 20:03:22 -06:00
)
2013-12-03 13:31:31 -07:00
object BSONFields {
2013-12-02 16:44:09 -07:00
2019-12-13 07:30:20 -07:00
val id = "_id"
val whitePlayer = "p0"
val blackPlayer = "p1"
val playerIds = "is"
val playerUids = "us"
val playingUids = "pl"
val binaryPieces = "ps"
val oldPgn = "pg"
val huffmanPgn = "hp"
val status = "s"
val turns = "t"
val startedAtTurn = "st"
val clock = "c"
val positionHashes = "ph"
val checkCount = "cc"
val castleLastMove = "cl"
val unmovedRooks = "ur"
val daysPerTurn = "cd"
val moveTimes = "mt"
2017-02-22 13:07:53 -07:00
val whiteClockHistory = "cw"
val blackClockHistory = "cb"
2019-12-13 07:30:20 -07:00
val rated = "ra"
val analysed = "an"
val variant = "v"
val crazyData = "chd"
val bookmarks = "bm"
val createdAt = "ca"
val movedAt = "ua" // ua = updatedAt (bc)
val source = "so"
val pgnImport = "pgni"
val tournamentId = "tid"
2020-05-06 15:26:33 -06:00
val swissId = "iid"
2019-12-13 07:30:20 -07:00
val simulId = "sid"
val tvAt = "tv"
val winnerColor = "w"
val winnerId = "wid"
val initialFen = "if"
val checkAt = "ck"
2020-01-19 09:31:28 -07:00
val perfType = "pt" // only set on student games for aggregation
2021-03-11 08:28:33 -07:00
val drawOffers = "do"
2013-12-04 12:08:31 -07:00
}
2013-03-22 10:36:09 -06:00
}
2018-01-22 15:42:22 -07:00
case class CastleLastMove(castles: Castles, lastMove: Option[Uci])
2018-01-22 15:30:23 -07:00
object CastleLastMove {
2018-01-22 15:42:22 -07:00
def init = CastleLastMove(Castles.all, None)
2013-03-22 10:36:09 -06:00
2019-11-29 19:16:11 -07:00
import reactivemongo.api.bson._
2013-12-02 16:44:09 -07:00
import lila.db.ByteArray.ByteArrayBSONHandler
2013-03-22 10:36:09 -06:00
2019-12-13 07:30:20 -07:00
implicit private[game] val castleLastMoveBSONHandler = new BSONHandler[CastleLastMove] {
2020-05-05 22:11:15 -06:00
def readTry(bson: BSONValue) =
bson match {
case bin: BSONBinary => ByteArrayBSONHandler readTry bin map BinaryFormat.castleLastMove.read
case b => lila.db.BSON.handlerBadType(b)
}
def writeTry(clmt: CastleLastMove) =
ByteArrayBSONHandler writeTry {
BinaryFormat.castleLastMove write clmt
}
2013-12-02 16:44:09 -07:00
}
2013-03-22 10:36:09 -06:00
}
2017-02-22 13:07:53 -07:00
case class ClockHistory(
white: Vector[Centis] = Vector.empty,
black: Vector[Centis] = Vector.empty
2017-02-22 13:07:53 -07:00
) {
def update(color: Color, f: Vector[Centis] => Vector[Centis]): ClockHistory =
color.fold(copy(white = f(white)), copy(black = f(black)))
def record(color: Color, clock: Clock): ClockHistory =
update(color, _ :+ clock.remainingTime(color))
def reset(color: Color) = update(color, _ => Vector.empty)
2017-02-22 13:07:53 -07:00
def apply(color: Color): Vector[Centis] = color.fold(white, black)
def last(color: Color) = apply(color).lastOption
def size = white.size + black.size
2017-03-27 10:13:21 -06:00
// first state is of the color that moved first.
2017-03-25 14:18:31 -06:00
def bothClockStates(firstMoveBy: Color): Vector[Centis] =
Sequence.interleave(
firstMoveBy.fold(white, black),
firstMoveBy.fold(black, white)
)
}
sealed trait DrawReason
object DrawReason {
case object MutualAgreement extends DrawReason
case object FiftyMoves extends DrawReason
case object ThreefoldRepetition extends DrawReason
case object InsufficientMaterial extends DrawReason
}