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

511 lines
14 KiB
Scala
Raw Normal View History

2013-03-22 10:36:09 -06:00
package lila.game
2013-05-24 11:04:49 -06:00
import chess.Color._
2013-05-31 09:55:11 -06:00
import chess.Pos.piotr, chess.Role.forsyth
2013-05-24 11:04:49 -06:00
import chess.{ History ChessHistory, Role, Board, Move, Pos, Game ChessGame, Clock, Status, Color, Piece, Variant, Mode }
2013-09-30 15:10:42 -06:00
import org.joda.time.DateTime
import org.scala_tools.time.Imports._
2013-05-24 11:04:49 -06:00
import lila.user.User
2013-03-22 10:36:09 -06:00
case class Game(
id: String,
token: String,
whitePlayer: Player,
blackPlayer: Player,
2013-12-01 02:01:20 -07:00
binaryPieces: Array[Byte],
2013-03-22 10:36:09 -06:00
status: Status,
turns: Int,
clock: Option[Clock],
lastMove: Option[String],
check: Option[Pos] = None,
creatorColor: Color,
positionHashes: String = "",
castles: String = "KQkq",
mode: Mode = Mode.default,
variant: Variant = Variant.default,
next: Option[String] = None,
lastMoveTime: Option[Int] = None,
bookmarks: Int = 0,
createdAt: DateTime = DateTime.now,
updatedAt: Option[DateTime] = None,
metadata: Option[Metadata] = None) {
2013-03-22 11:53:13 -06:00
val players = List(whitePlayer, blackPlayer)
val playersByColor: Map[Color, Player] = Map(
White -> whitePlayer,
Black -> blackPlayer
)
def player(color: Color): Player = color match {
case White whitePlayer
case Black blackPlayer
}
def player(playerId: String): Option[Player] =
players find (_.id == playerId)
def player(user: User): Option[Player] =
players find (_ isUser user)
def player(c: Color.type Color): Player = player(c(Color))
def isPlayerFullId(player: Player, fullId: String): Boolean =
(fullId.size == Game.fullIdSize) && player.id == (fullId drop 8)
def player: Player = player(turnColor)
def opponent(p: Player): Player = opponent(p.color)
def opponent(c: Color): Player = player(!c)
def turnColor = Color(0 == turns % 2)
def turnOf(p: Player) = p == player
def turnOf(c: Color) = c == turnColor
2013-03-22 11:53:13 -06:00
def fullIdOf(player: Player): Option[String] =
(players contains player) option id + player.id
def fullIdOf(color: Color): String = id + player(color).id
def tournamentId = metadata flatMap (_.tournamentId)
def isTournament = tournamentId.isDefined
def nonTournament = tournamentId.isEmpty
def hasChat = nonTournament && nonAi
lazy val toChess: ChessGame = {
def posPiece(posCode: Char, roleCode: Char, color: Color): Option[(Pos, Piece)] = for {
pos piotr(posCode)
role forsyth(roleCode)
} yield (pos, Piece(color, role))
2013-12-01 02:01:20 -07:00
val (pieces, deads) = BinaryFormat.piece decode binaryPieces
2013-03-22 11:53:13 -06:00
ChessGame(
board = Board(pieces, toChessHistory, variant),
player = Color(0 == turns % 2),
clock = clock,
deads = deads,
2013-09-30 15:10:42 -06:00
turns = turns)
2013-03-22 11:53:13 -06:00
}
lazy val toChessHistory = ChessHistory(
lastMove = lastMove,
castles = castles,
positionHashes = positionHashes)
def update(
game: ChessGame,
move: Move,
2013-11-25 16:02:44 -07:00
blur: Boolean = false): (Progress, List[String]) = {
2013-03-22 11:53:13 -06:00
val (history, situation) = (game.board.history, game.situation)
val events = (players collect {
case p if p.isHuman Event.possibleMoves(situation, p.color)
}) :::
Event.State(situation.color, game.turns) ::
(Event fromMove move) :::
(Event fromSituation situation)
2013-03-22 11:53:13 -06:00
def copyPlayer(player: Player) = player.copy(
blurs = player.blurs + (blur && move.color == player.color).fold(1, 0),
moveTimes = ((!isPgnImport) && (move.color == player.color)).fold(
2013-09-26 13:39:23 -06:00
lastMoveTime ?? { lmt
2013-09-30 15:10:42 -06:00
val mt = nowSeconds - lmt
2013-09-26 13:39:23 -06:00
val encoded = MoveTime encode mt
player.moveTimes.isEmpty.fold(encoded.toString, player.moveTimes + encoded)
2013-03-22 11:53:13 -06:00
}, player.moveTimes
)
)
val updated = copy(
whitePlayer = copyPlayer(whitePlayer),
blackPlayer = copyPlayer(blackPlayer),
turns = game.turns,
positionHashes = history.positionHashes.mkString,
2013-03-22 11:53:13 -06:00
castles = history.castleNotation,
lastMove = history.lastMoveString,
status = situation.status | status,
clock = game.clock,
check = situation.kingPos ifTrue situation.check,
lastMoveTime = nowSeconds.some)
2013-03-22 11:53:13 -06:00
val finalEvents = events :::
2013-09-30 15:10:42 -06:00
updated.clock.??(c List(Event.Clock(c))) ::: {
2013-03-22 11:53:13 -06:00
(updated.playable && (
abortable != updated.abortable || (Color.all exists { color
playerCanOfferDraw(color) != updated.playerCanOfferDraw(color)
})
2013-09-30 15:10:42 -06:00
)).??(Color.all map Event.ReloadTable)
2013-03-22 11:53:13 -06:00
}
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
Progress(this, updated, finalEvents) -> game.pgnMoves
}
2013-10-03 04:18:40 -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
def updatePlayers(f: Player Player) = copy(
whitePlayer = f(whitePlayer),
2013-10-03 04:18:40 -06:00
blackPlayer = f(blackPlayer))
2013-03-22 11:53:13 -06:00
def start = started.fold(this, copy(
status = Status.Started,
mode = Mode(mode.rated && (players forall (_.hasUser))),
updatedAt = DateTime.now.some
))
def startClock(compensation: Float) = clock.filterNot(_.isRunning).fold(this) { c
copy(clock = c.run.giveTime(creatorColor, compensation).some)
}
def hasMoveTimes = players forall (_.hasMoveTimes)
2013-03-22 10:36:09 -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
2013-03-22 11:53:13 -06:00
def playable = 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
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
2013-03-22 11:53:13 -06:00
def hasAi: Boolean = players exists (_.isAi)
def nonAi = !hasAi
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def mapPlayers(f: Player Player) = copy(
whitePlayer = f(whitePlayer),
blackPlayer = f(blackPlayer)
)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerCanOfferDraw(color: Color) =
started && playable &&
turns >= 2 &&
!player(color).isOfferingDraw &&
!(opponent(color).isAi) &&
!(playerHasOfferedDraw(color))
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerHasOfferedDraw(color: Color) =
player(color).lastDrawOffer ?? (_ >= turns - 1)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerCanRematch(color: Color) =
!player(color).isOfferingRematch &&
finishedOrAborted &&
nonTournament
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerCanProposeTakeback(color: Color) =
started && playable && nonTournament &&
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
2013-03-22 11:53:13 -06:00
def moretimeable = playable && nonTournament && hasClock
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def abortable = status == Status.Started && turns < 2 && nonTournament
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def resignable = playable && !abortable
2013-10-09 08:46:00 -06:00
def drawable = playable && !abortable
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def finish(status: Status, winner: Option[Color]) = Progress(
this,
copy(
status = status,
whitePlayer = whitePlayer finish (winner == Some(White)),
blackPlayer = blackPlayer finish (winner == Some(Black)),
clock = clock map (_.stop)
),
List(Event.End) ::: clock.??(c List(Event.Clock(c)))
2013-03-22 11:53:13 -06:00
)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def rated = mode.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 analysable = (imported || finished) && (source.fold(true)(Source.Position!=))
def imported = source exists (_ == Source.Import)
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
2013-03-22 11:53:13 -06:00
def wonBy(c: Color): Option[Boolean] = winnerColor map (_ == c)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def outoftimePlayer: Option[Player] = for {
c clock
2013-10-09 07:26:17 -06:00
if started && playable && onePlayerHasMoved
2013-03-22 11:53:13 -06:00
if !c.isRunning || (c outoftime player.color)
} yield player
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def hasClock = clock.isDefined
2013-03-22 10:36:09 -06:00
2013-10-09 07:26:17 -06:00
def isClockRunning = clock ?? (_.isRunning)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def withClock(c: Clock) = Progress(this, copy(clock = Some(c)))
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def estimateTotalTime = clock.fold(1200)(_.estimateTotalTime)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def creator = player(creatorColor)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def invitedColor = !creatorColor
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def invited = player(invitedColor)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerWhoDidNotMove: Option[Player] = turns match {
case 0 player(White).some
case 1 player(Black).some
case _ none
}
2013-03-22 10:36:09 -06:00
2013-10-09 07:26:17 -06:00
def onePlayerHasMoved = turns > 0
2013-03-22 11:53:13 -06:00
def bothPlayersHaveMoved = turns > 1
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def playerMoves(color: Color): Int = (turns + color.fold(1, 0)) / 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
2013-03-22 11:53:13 -06:00
def playerBlurPercent(color: Color): Int = (turns > 5).fold(
(player(color).blurs * 100) / playerMoves(color),
0
)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def deadPiecesOf(color: Color): List[Role] = toChess.deads collect {
2013-12-01 02:01:20 -07:00
case piece if piece is color piece.role
2013-03-22 11:53:13 -06:00
}
2013-03-22 10:36:09 -06:00
2013-10-03 04:18:40 -06:00
def isBeingPlayed = !finishedOrAborted && !olderThan(60)
2013-08-01 08:44:38 -06:00
def olderThan(seconds: Int) = updatedAt.??(_ < DateTime.now - seconds.seconds)
2013-03-22 10:36:09 -06:00
2013-09-26 03:05:30 -06:00
def abandoned = (status <= Status.Started) && (updatedAt | createdAt) < Game.abandonedDate
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def hasBookmarks = bookmarks > 0
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def showBookmarks = hasBookmarks ?? bookmarks
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def encode = RawGame(
id = id,
tk = token.some filter (Game.defaultToken !=),
p = players map (_.encode),
2013-12-01 02:01:20 -07:00
ps = binaryPieces,
2013-03-22 11:53:13 -06:00
s = status.id,
t = turns,
c = clock map RawClock.encode,
2013-03-22 11:53:13 -06:00
lm = lastMove,
ck = check map (_.key),
cc = creatorColor.white.fold(None, Some(false)),
ph = positionHashes.some filter (_.nonEmpty),
cs = castles.some filter ("-" !=),
ra = mode.rated option true,
v = variant.exotic option variant.id,
next = next,
lmt = lastMoveTime,
bm = bookmarks.some filter (0 <),
ca = createdAt,
ua = updatedAt,
me = metadata map (_.encode))
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
2013-03-22 11:53:13 -06:00
def userElos = playerMaps(_.elo)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def averageUsersElo = userElos match {
case a :: b :: Nil Some((a + b) / 2)
case a :: Nil Some((a + 1200) / 2)
case _ None
}
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def withTournamentId(id: String) = this.copy(
metadata = metadata map (_.copy(tournamentId = id.some)))
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def withId(newId: String) = this.copy(id = newId)
2013-03-22 10:36:09 -06:00
2013-08-02 03:09:13 -06:00
def source = metadata flatMap (_.source)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def pgnImport = metadata flatMap (_.pgnImport)
2013-03-22 10:36:09 -06:00
2013-03-22 11:53:13 -06:00
def isPgnImport = pgnImport.isDefined
2013-03-22 10:36:09 -06:00
def resetTurns = copy(turns = 0)
2013-03-22 11:53:13 -06:00
private def playerMaps[A](f: Player Option[A]): List[A] = players.map(f).flatten
2013-03-22 10:36:09 -06:00
}
object Game {
val gameIdSize = 8
val playerIdSize = 4
val fullIdSize = 12
val tokenSize = 4
val defaultToken = "-tk-"
2013-04-21 05:50:42 -06:00
object ShortFields {
val createdAt = "ca"
val updatedAt = "ua"
}
2013-09-26 03:05:30 -06:00
def abandonedDate = DateTime.now - 7.days
2013-03-22 10:36:09 -06: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
2013-05-07 17:44:26 -06:00
def make(
game: ChessGame,
whitePlayer: Player,
blackPlayer: Player,
creatorColor: Color,
mode: Mode,
variant: Variant,
source: Source,
pgnImport: Option[PgnImport]): Game = Game(
id = IdGenerator.game,
token = IdGenerator.token,
2013-12-01 02:01:20 -07:00
whitePlayer = whitePlayer,
blackPlayer = blackPlayer,
binaryPieces = BinaryFormat.piece encode (game.board.pieces, game.deads)
2013-05-07 17:44:26 -06:00
status = Status.Created,
turns = game.turns,
clock = game.clock,
lastMove = None,
check = None,
creatorColor = creatorColor,
positionHashes = "",
castles = "KQkq",
mode = mode,
variant = variant,
lastMoveTime = None,
metadata = Metadata(
2013-08-02 03:09:13 -06:00
source = source.some,
pgnImport = pgnImport,
tournamentId = none,
tvAt = none).some,
2013-05-07 17:44:26 -06:00
createdAt = DateTime.now)
2013-03-22 11:53:13 -06:00
import lila.db.Tube
2013-03-22 11:53:13 -06:00
import play.api.libs.json._
2013-04-06 17:36:00 -06:00
private[game] lazy val tube = Tube(
2013-05-20 07:47:24 -06:00
Reads[Game](js
2013-04-08 21:25:41 -06:00
~(for {
2013-03-22 11:53:13 -06:00
obj js.asOpt[JsObject]
rawGame RawGame.tube.read(obj).asOpt
2013-03-22 11:53:13 -06:00
game rawGame.decode
2013-04-08 21:25:41 -06:00
} yield JsSuccess(game): JsResult[Game])
2013-03-22 11:53:13 -06:00
),
2013-05-20 07:47:24 -06:00
Writes[Game](game
2013-04-05 06:07:25 -06:00
RawGame.tube.write(game.encode) getOrElse JsUndefined("[db] Can't write game " + game.id)
2013-03-22 11:53:13 -06:00
)
)
2013-03-22 10:36:09 -06:00
}
private[game] case class RawGame(
2013-03-22 10:36:09 -06:00
id: String,
tk: Option[String] = None,
p: List[RawPlayer],
2013-12-01 02:01:20 -07:00
ps: Array[Byte],
2013-03-22 10:36:09 -06:00
s: Int,
t: Int,
c: Option[RawClock],
lm: Option[String],
ck: Option[String],
cc: Option[Boolean] = None,
ph: Option[String] = None,
cs: Option[String] = None,
ra: Option[Boolean] = None,
v: Option[Int] = None,
next: Option[String] = None,
lmt: Option[Int] = None,
bm: Option[Int] = None,
ca: DateTime,
ua: Option[DateTime],
me: Option[RawMetadata]) {
2013-03-22 11:53:13 -06:00
def decode: Option[Game] = for {
whitePlayer p.headOption map (_ decode Color.White)
blackPlayer p lift 1 map (_ decode Color.Black)
trueStatus Status(s)
metadata = me map (_.decode)
} yield Game(
id = id,
token = tk | Game.defaultToken,
whitePlayer = whitePlayer,
blackPlayer = blackPlayer,
2013-12-01 02:01:20 -07:00
ps = ps,
2013-03-22 11:53:13 -06:00
status = trueStatus,
turns = t,
clock = c map (_.decode),
lastMove = lm,
check = ck flatMap Pos.posAt,
creatorColor = cc.fold(Color.white)(Color.apply),
2013-09-30 15:10:42 -06:00
positionHashes = ~ph,
2013-03-22 11:53:13 -06:00
castles = cs | "-",
mode = (ra map Mode.apply) | Mode.Casual,
variant = (v flatMap Variant.apply) | Variant.Standard,
next = next,
lastMoveTime = lmt,
2013-09-30 15:10:42 -06:00
bookmarks = ~bm,
2013-03-22 11:53:13 -06:00
createdAt = ca,
updatedAt = ua,
2013-08-02 03:09:13 -06:00
metadata = me map (_.decode)
2013-03-22 11:53:13 -06:00
)
2013-03-22 10:36:09 -06:00
}
private[game] object RawGame {
2013-03-22 10:36:09 -06:00
import lila.db.Tube
import Tube.Helpers._
2013-03-22 10:36:09 -06:00
import play.api.libs.json._
private implicit def playerTube = RawPlayer.tube
private implicit def clockTube = RawClock.tube
private implicit def metadataTube = RawMetadata.tube
private def defaults = Json.obj(
2013-03-22 10:36:09 -06:00
"tk" -> none[String],
"c" -> none[RawClock],
"lm" -> none[String],
"ck" -> none[String],
"cc" -> none[Boolean],
"ph" -> none[String],
"cs" -> none[String],
"ra" -> none[Boolean],
"v" -> none[Int],
"next" -> none[String],
"lmt" -> none[Int],
"bm" -> none[Int],
2013-05-10 11:21:03 -06:00
"me" -> none[RawMetadata],
"ua" -> none[DateTime])
2013-03-22 10:36:09 -06:00
2013-04-06 17:36:00 -06:00
private[game] lazy val tube = Tube(
(__.json update (
2013-05-09 11:06:27 -06:00
merge(defaults) andThen readDate('ca) andThen readDateOpt('ua)
2013-03-22 10:36:09 -06:00
)) andThen Json.reads[RawGame],
Json.writes[RawGame] andThen (__.json update (
2013-05-09 12:54:05 -06:00
writeDate('ca) andThen writeDateOpt('ua)
))
2013-03-22 10:36:09 -06:00
)
}