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 }
|
2020-09-29 13:40:31 -06:00
|
|
|
import org.joda.time.DateTime
|
|
|
|
|
2017-04-19 20:33:22 -06:00
|
|
|
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
|
2019-04-06 03:00:22 -06:00
|
|
|
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,
|
2018-03-11 07:52:38 -06:00
|
|
|
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],
|
2017-02-23 02:31:42 -07:00
|
|
|
binaryMoveTimes: Option[ByteArray] = None,
|
2013-03-22 10:36:09 -06:00
|
|
|
mode: Mode = Mode.default,
|
|
|
|
bookmarks: Int = 0,
|
|
|
|
createdAt: DateTime = DateTime.now,
|
2017-04-29 08:13:48 -06:00
|
|
|
movedAt: DateTime = DateTime.now,
|
2017-02-14 08:34:07 -07:00
|
|
|
metadata: Metadata
|
|
|
|
) {
|
2018-03-11 07:52:38 -06:00
|
|
|
lazy val clockHistory = chess.clock flatMap loadClockHistory
|
2018-02-07 09:22:07 -07:00
|
|
|
|
2018-01-23 08:41:42 -07:00
|
|
|
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
|
2018-01-23 08:41:42 -07:00
|
|
|
|
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 =
|
2020-08-17 16:10:52 -06:00
|
|
|
(fullId.lengthIs == Game.fullIdSize) && player.id == (fullId drop Game.gameIdSize)
|
2013-03-22 11:53:13 -06:00
|
|
|
|
|
|
|
def player: Player = player(turnColor)
|
|
|
|
|
2020-09-25 05:27:41 -06:00
|
|
|
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
|
2014-05-20 13:36:47 -06:00
|
|
|
|
2021-08-21 03:06:04 -06:00
|
|
|
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
|
|
|
|
2014-04-18 01:53:58 -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
|
|
|
|
2018-01-23 08:41:42 -07:00
|
|
|
def playedTurns = turns - chess.startedAtTurn
|
2014-03-05 13:11:55 -07:00
|
|
|
|
2017-03-28 21:00:01 -06:00
|
|
|
def flagged = (status == Status.Outoftime).option(turnColor)
|
|
|
|
|
2013-03-22 11:53:13 -06:00
|
|
|
def fullIdOf(player: Player): Option[String] =
|
2015-03-17 15:54:14 -06:00
|
|
|
(players contains player) option s"$id${player.id}"
|
2013-03-22 11:53:13 -06:00
|
|
|
|
2015-03-17 15:54:14 -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
|
|
|
|
2017-04-27 05:55:08 -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
|
2017-04-19 20:33:22 -06:00
|
|
|
inc = clk.incrementOf(color)
|
2017-03-20 06:22:12 -06:00
|
|
|
history <- clockHistory
|
2017-03-31 16:02:22 -06:00
|
|
|
clocks = history(color)
|
2017-03-20 06:22:12 -06:00
|
|
|
} yield Centis(0) :: {
|
2017-03-31 16:02:22 -06:00
|
|
|
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.
|
2017-03-31 16:02:22 -06:00
|
|
|
val noLastInc = finished && (history.size <= playedTurns) == (color != turnColor)
|
2017-03-27 18:58:56 -06:00
|
|
|
|
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
|
2017-03-27 18:58:56 -06:00
|
|
|
} toList
|
2017-03-05 09:58:04 -07:00
|
|
|
}
|
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
|
|
|
}
|
2015-06-27 22:51:34 -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
|
|
|
|
2017-03-28 06:53:58 -06:00
|
|
|
def bothClockStates: Option[Vector[Centis]] = clockHistory.map(_ bothClockStates startColor)
|
2017-03-25 11:11:59 -06:00
|
|
|
|
2015-11-24 04:26:01 -07:00
|
|
|
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
|
2017-02-14 08:34:07 -07:00
|
|
|
): Progress = {
|
2013-10-03 04:18:13 -06:00
|
|
|
|
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-02-14 08:34:07 -07:00
|
|
|
)
|
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)
|
2018-01-23 07:27:52 -07:00
|
|
|
} :+ Centis(nowCentis - movedAt.getCentis).nonNeg
|
2017-03-21 05:00:30 -06:00
|
|
|
}
|
2017-02-23 02:31:42 -07:00
|
|
|
},
|
2018-03-11 07:52:38 -06:00
|
|
|
loadClockHistory = _ => newClockHistory,
|
2018-01-23 07:27:52 -07:00
|
|
|
status = game.situation.status | status,
|
2017-04-29 08:13:48 -06:00
|
|
|
movedAt = DateTime.now
|
2017-02-14 08:34:07 -07:00
|
|
|
)
|
2013-03-22 11:53:13 -06:00
|
|
|
|
2015-06-15 17:56:27 -06:00
|
|
|
val state = Event.State(
|
2018-01-23 07:27:52 -07:00
|
|
|
color = game.situation.color,
|
2015-08-14 05:14:50 -06:00
|
|
|
turns = game.turns,
|
|
|
|
status = (status != updated.status) option updated.status,
|
2018-01-23 07:27:52 -07:00
|
|
|
winner = game.situation.winner,
|
2015-06-15 17:56:27 -06:00
|
|
|
whiteOffersDraw = whitePlayer.isOfferingDraw,
|
2017-02-14 08:34:07 -07:00
|
|
|
blackOffersDraw = blackPlayer.isOfferingDraw
|
|
|
|
)
|
2015-04-01 04:38:47 -06:00
|
|
|
|
2018-01-23 08:41:42 -07:00
|
|
|
val clockEvent = updated.chess.clock map Event.Clock.apply orElse {
|
2015-06-22 13:23:57 -06:00
|
|
|
updated.playableCorrespondenceClock map Event.CorrespondenceClock.apply
|
2013-12-05 12:40:11 -07:00
|
|
|
}
|
2013-03-22 10:36:09 -06:00
|
|
|
|
2016-01-15 06:21:16 -07:00
|
|
|
val events = moveOrDrop.fold(
|
2018-01-23 08:41:42 -07:00
|
|
|
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(
|
2018-01-23 07:27:52 -07:00
|
|
|
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
|
|
|
|
2015-07-26 04:23:55 -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
|
|
|
|
}
|
2013-12-09 15:04:53 -07:00
|
|
|
|
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
|
|
|
|
2020-05-07 15:46:36 -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
|
|
|
|
)
|
|
|
|
}
|
2015-06-22 13:23:57 -06:00
|
|
|
|
|
|
|
def playableCorrespondenceClock: Option[CorrespondenceClock] =
|
|
|
|
playable ?? correspondenceClock
|
2013-03-22 11:53:13 -06:00
|
|
|
|
2018-01-23 08:41:42 -07: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)
|
2015-03-17 15:54:14 -06:00
|
|
|
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
|
|
|
|
|
2020-05-11 08:37:27 -06:00
|
|
|
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
|
|
|
|
2015-09-21 08:49:40 -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
|
|
|
|
|
2016-11-07 04:07:16 -07:00
|
|
|
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 &&
|
2015-09-12 07:58:31 -06:00
|
|
|
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
|
2015-09-12 07:58:31 -06:00
|
|
|
|
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
|
|
|
|
2020-07-21 05:08:44 -06:00
|
|
|
def goBerserk(color: Color): Option[Progress] =
|
2015-08-18 04:47:18 -06:00
|
|
|
clock.ifTrue(berserkable && !player(color).berserk).map { c =>
|
2017-05-15 10:48:35 -06:00
|
|
|
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
|
2020-07-15 00:15:20 -06:00
|
|
|
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
|
|
|
|
2015-06-03 10:41:39 -06:00
|
|
|
def accountable = playedTurns >= 2 || isTournament
|
2014-06-12 11:37:54 -06:00
|
|
|
|
2016-12-13 05:23:25 -07:00
|
|
|
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
|
|
|
|
2015-06-14 10:35:38 -06:00
|
|
|
def ratingVariant =
|
2018-01-23 08:41:42 -07:00
|
|
|
if (isTournament && variant.fromPosition) Standard
|
2015-06-14 10:35:38 -06:00
|
|
|
else variant
|
|
|
|
|
2019-12-13 07:30:20 -07:00
|
|
|
def fromPosition = variant.fromPosition || source.??(Source.Position ==)
|
2013-10-24 08:58:22 -06:00
|
|
|
|
2015-08-20 07:16:34 -06:00
|
|
|
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
|
2017-09-12 14:50:51 -06:00
|
|
|
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
|
|
|
|
2017-05-21 19:18:18 -06:00
|
|
|
def outoftime(withGrace: Boolean): Boolean =
|
|
|
|
if (isCorrespondence) outoftimeCorrespondence else outoftimeClock(withGrace)
|
2016-01-13 10:13:13 -07:00
|
|
|
|
2020-05-05 22:11:15 -06:00
|
|
|
private def outoftimeClock(withGrace: Boolean): Boolean =
|
|
|
|
clock ?? { c =>
|
2021-06-13 03:16:58 -06:00
|
|
|
started && playable && {
|
2020-09-29 13:40:31 -06:00
|
|
|
c.outOfTime(turnColor, withGrace) || {
|
|
|
|
!c.isRunning && c.players.exists(_.elapsed.centis > 0)
|
|
|
|
}
|
2020-05-05 22:11:15 -06:00
|
|
|
}
|
2016-01-13 10:13:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
private def outoftimeCorrespondence: Boolean =
|
2017-05-15 10:48:35 -06:00
|
|
|
playableCorrespondenceClock ?? { _ outoftime turnColor }
|
2014-11-30 06:03:09 -07:00
|
|
|
|
2018-01-23 08:41:42 -07:00
|
|
|
def isCorrespondence = speed == Speed.Correspondence
|
2014-11-29 06:27:57 -07:00
|
|
|
|
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
|
2014-12-30 08:34:50 -07:00
|
|
|
|
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
|
|
|
|
2017-07-23 06:16:41 -06:00
|
|
|
def correspondenceGiveTime = Progress(this, copy(movedAt = DateTime.now))
|
|
|
|
|
2017-04-19 20:33:22 -06:00
|
|
|
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:03:47 -06:00
|
|
|
|
2017-10-22 16:45:53 -06:00
|
|
|
def expirable =
|
2020-05-07 15:46:36 -06:00
|
|
|
!bothPlayersHaveMoved && source.exists(Source.expirable.contains) && playable && nonAi && clock.exists(
|
|
|
|
!_.isRunning
|
|
|
|
)
|
2017-10-22 16:03:47 -06:00
|
|
|
|
2020-05-05 22:11:15 -06:00
|
|
|
def timeBeforeExpiration: Option[Centis] =
|
|
|
|
expirable option {
|
|
|
|
Centis.ofMillis(movedAt.getMillis - nowMillis + timeForFirstMove.millis).nonNeg
|
|
|
|
}
|
2017-10-22 16:03:47 -06:00
|
|
|
|
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)
|
2015-06-14 10:35:38 -06:00
|
|
|
|
|
|
|
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
|
|
|
|
2014-12-02 16:41:39 -07:00
|
|
|
def isBeingPlayed = !isPgnImport && !finishedOrAborted
|
2013-08-01 08:44:38 -06:00
|
|
|
|
2017-04-29 08:13:48 -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)
|
|
|
|
|
2014-12-02 02:42:23 -07:00
|
|
|
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 {
|
2021-11-24 01:03:25 -07:00
|
|
|
if (hasAi && hasClock) Game.aiAbandonedDate
|
2020-05-05 22:11:15 -06:00
|
|
|
else Game.abandonedDate
|
|
|
|
}
|
2018-04-14 16:14:40 -06:00
|
|
|
}
|
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
|
2019-08-10 10:31:00 -06:00
|
|
|
|
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
|
|
|
|
2017-03-27 07:47:15 -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
|
|
|
|
2017-03-27 07:47:15 -06:00
|
|
|
def withSimulId(id: String) = copy(metadata = metadata.copy(simulId = id.some))
|
2013-03-22 10:36:09 -06:00
|
|
|
|
2017-03-27 07:47:15 -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)
|
|
|
|
)
|
2013-08-02 02:20:41 -06:00
|
|
|
|
2016-02-25 03:03:09 -07:00
|
|
|
lazy val opening: Option[FullOpening.AtPly] =
|
2016-02-25 04:19:42 -07:00
|
|
|
if (fromPosition || !Variant.openingSensibleVariants(variant)) none
|
2016-02-23 22:40:50 -07:00
|
|
|
else FullOpeningDB search pgnMoves
|
2014-11-09 10:52:20 -07:00
|
|
|
|
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-01-02 17:14:16 -07:00
|
|
|
|
2020-04-26 15:34:37 -06:00
|
|
|
def secondsSinceCreation = (nowSeconds - createdAt.getSeconds).toInt
|
|
|
|
|
2021-07-27 01:33:21 -06:00
|
|
|
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 {
|
|
|
|
|
2016-07-24 12:35:00 -06:00
|
|
|
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"
|
|
|
|
|
2017-03-31 03:45:17 -06:00
|
|
|
val maxPlayingRealtime = 100 // plus 200 correspondence games
|
2016-07-29 03:04:16 -06:00
|
|
|
|
2018-02-03 22:26:36 -07:00
|
|
|
val maxPlies = 600 // unlimited can cause StackOverflowError
|
|
|
|
|
2015-01-11 07:23:25 -07:00
|
|
|
val analysableVariants: Set[Variant] = Set(
|
|
|
|
chess.variant.Standard,
|
2016-10-31 13:54:42 -06:00
|
|
|
chess.variant.Crazyhouse,
|
2015-01-11 07:23:25 -07:00
|
|
|
chess.variant.Chess960,
|
2015-05-03 11:12:24 -06:00
|
|
|
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,
|
2016-08-13 10:41:56 -06:00
|
|
|
chess.variant.Atomic,
|
2017-02-14 08:34:07 -07:00
|
|
|
chess.variant.RacingKings
|
|
|
|
)
|
2015-01-22 09:12:35 -07:00
|
|
|
|
2014-08-13 02:51:15 -06:00
|
|
|
val unanalysableVariants: Set[Variant] = Variant.all.toSet -- analysableVariants
|
2014-08-08 03:01:47 -06:00
|
|
|
|
2015-01-22 09:12:35 -07:00
|
|
|
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,
|
2017-02-14 08:34:07 -07:00
|
|
|
chess.variant.Antichess
|
|
|
|
)
|
2015-01-22 09:12:35 -07:00
|
|
|
|
2019-01-29 17:48:07 -07:00
|
|
|
val blindModeVariants: Set[Variant] = Set(
|
|
|
|
chess.variant.Standard,
|
|
|
|
chess.variant.Chess960,
|
|
|
|
chess.variant.KingOfTheHill,
|
|
|
|
chess.variant.ThreeCheck,
|
2021-05-29 00:09:51 -06:00
|
|
|
chess.variant.FromPosition,
|
|
|
|
chess.variant.Antichess,
|
|
|
|
chess.variant.Atomic,
|
2021-06-06 23:43:38 -06:00
|
|
|
chess.variant.RacingKings,
|
|
|
|
chess.variant.Horde
|
2019-01-29 17:48:07 -07:00
|
|
|
)
|
|
|
|
|
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 || {
|
2021-03-21 08:07:44 -06:00
|
|
|
clock ?? { c =>
|
|
|
|
c.estimateTotalTime >= Centis(3000) &&
|
|
|
|
c.limitSeconds > 0 || c.incrementSeconds > 1
|
|
|
|
}
|
2020-05-05 22:11:15 -06:00
|
|
|
}
|
2017-03-31 12:14:55 -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
|
|
|
|
2014-12-02 02:42:23 -07: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
|
|
|
|
2015-12-08 09:48:08 -07:00
|
|
|
val abandonedDays = 21
|
2014-12-02 02:42:23 -07:00
|
|
|
def abandonedDate = DateTime.now minusDays abandonedDays
|
2013-03-22 10:36:09 -06:00
|
|
|
|
2017-03-26 06:10:16 -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
|
2019-08-20 02:18:26 -06:00
|
|
|
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 =>
|
2021-06-15 02:57:21 -06:00
|
|
|
isBoardCompatible(c.config) || {
|
2021-08-16 03:39:05 -06:00
|
|
|
(game.hasAi || game.fromFriend) && chess.Speed(c.config) >= Speed.Blitz
|
2021-06-15 02:57:21 -06:00
|
|
|
}
|
2020-05-05 22:11:15 -06:00
|
|
|
}
|
2020-04-06 11:42:35 -06:00
|
|
|
|
2021-01-14 07:18:54 -07:00
|
|
|
def isBoardCompatible(clock: Clock.Config): Boolean =
|
|
|
|
chess.Speed(clock) >= Speed.Rapid
|
2020-02-28 12:01:10 -07:00
|
|
|
|
2020-08-15 01:11:12 -06:00
|
|
|
def isBotCompatible(game: Game): Boolean = {
|
2021-02-07 05:46:37 -07:00
|
|
|
game.hasAi || game.fromFriend || game.fromApi
|
2020-08-15 01:11:12 -06:00
|
|
|
} && isBotCompatible(game.speed)
|
|
|
|
|
|
|
|
def isBotCompatible(speed: Speed): Boolean = speed >= Speed.Bullet
|
2020-02-28 12:01:10 -07:00
|
|
|
|
2018-01-23 07:27:52 -07:00
|
|
|
private[game] val emptyCheckCount = CheckCount(0, 0)
|
|
|
|
|
2018-03-11 07:52:38 -06:00
|
|
|
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
|
|
|
|
)
|
|
|
|
)
|
2017-04-29 08:13:48 -06:00
|
|
|
}
|
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])
|
2013-12-01 17:38:03 -07:00
|
|
|
|
2018-01-22 15:30:23 -07:00
|
|
|
object CastleLastMove {
|
2013-12-01 17:38:03 -07:00
|
|
|
|
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(
|
2017-03-20 05:52:41 -06:00
|
|
|
white: Vector[Centis] = Vector.empty,
|
|
|
|
black: Vector[Centis] = Vector.empty
|
2017-02-22 13:07:53 -07:00
|
|
|
) {
|
|
|
|
|
2017-03-27 14:53:18 -06:00
|
|
|
def update(color: Color, f: Vector[Centis] => Vector[Centis]): ClockHistory =
|
|
|
|
color.fold(copy(white = f(white)), copy(black = f(black)))
|
2017-03-26 17:19:25 -06:00
|
|
|
|
2017-03-27 14:53:18 -06:00
|
|
|
def record(color: Color, clock: Clock): ClockHistory =
|
2017-04-19 20:33:22 -06:00
|
|
|
update(color, _ :+ clock.remainingTime(color))
|
2017-03-27 14:53:18 -06:00
|
|
|
|
|
|
|
def reset(color: Color) = update(color, _ => Vector.empty)
|
2017-02-22 13:07:53 -07:00
|
|
|
|
2017-03-31 16:02:22 -06:00
|
|
|
def apply(color: Color): Vector[Centis] = color.fold(white, black)
|
2017-03-25 11:11:59 -06:00
|
|
|
|
2017-03-31 16:02:22 -06:00
|
|
|
def last(color: Color) = apply(color).lastOption
|
2017-03-25 11:11:59 -06:00
|
|
|
|
2017-03-31 16:02:22 -06:00
|
|
|
def size = white.size + black.size
|
2017-03-27 10:13:21 -06:00
|
|
|
|
2017-03-25 11:11:59 -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)
|
|
|
|
)
|
2017-03-20 05:52:41 -06:00
|
|
|
}
|
2021-07-27 01:33:21 -06:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|