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

209 lines
7.0 KiB
Scala

package lila.game
import chess.format.Forsyth
import chess.format.pgn.{ ParsedPgn, Parser, Pgn, Tag, TagType, Tags }
import chess.format.{ FEN, pgn => chessPgn }
import chess.{ Centis, Color }
import lila.common.config.BaseUrl
import lila.common.LightUser
final class PgnDump(
baseUrl: BaseUrl,
lightUserApi: lila.user.LightUserApi
)(implicit ec: scala.concurrent.ExecutionContext) {
import PgnDump._
def apply(
game: Game,
initialFen: Option[FEN],
flags: WithFlags,
teams: Option[Color.Map[String]] = None
): Fu[Pgn] = {
val imported = game.pgnImport.flatMap { pgni =>
Parser.full(pgni.pgn).toOption
}
val tagsFuture =
if (flags.tags)
tags(
game,
initialFen,
imported,
withOpening = flags.opening,
withRating = flags.rating,
teams = teams
)
else fuccess(Tags(Nil))
tagsFuture map { ts =>
val turns = flags.moves ?? {
val fenSituation = ts.fen flatMap Forsyth.<<<
makeTurns(
flags keepDelayIf game.playable applyDelay {
if (fenSituation.exists(_.situation.color.black)) ".." +: game.pgnMoves
else game.pgnMoves
},
fenSituation.map(_.fullMoveNumber) | 1,
flags.clocks ?? ~game.bothClockStates,
game.startColor
)
}
Pgn(ts, turns)
}
}
private def gameUrl(id: String) = s"$baseUrl/$id"
private def gameLightUsers(game: Game): Fu[(Option[LightUser], Option[LightUser])] =
(game.whitePlayer.userId ?? lightUserApi.async) zip (game.blackPlayer.userId ?? lightUserApi.async)
private def rating(p: Player) = p.rating.orElse(p.nameSplit.flatMap(_._2)).fold("?")(_.toString)
def player(p: Player, u: Option[LightUser]) =
p.aiLevel.fold(u.fold(p.nameSplit.map(_._1).orElse(p.name) | lila.user.User.anonymous)(_.name))(
"lichess AI level " + _
)
private val customStartPosition: Set[chess.variant.Variant] =
Set(chess.variant.Chess960, chess.variant.FromPosition, chess.variant.Horde, chess.variant.RacingKings)
private def eventOf(game: Game) = {
val perf = game.perfType.fold("Standard")(_.trans(lila.i18n.defaultLang))
game.tournamentId.map { id =>
s"${game.mode} $perf tournament https://lichess.org/tournament/$id"
} orElse game.simulId.map { id =>
s"$perf simul https://lichess.org/simul/$id"
} getOrElse {
s"${game.mode} $perf game"
}
}
private def ratingDiffTag(p: Player, tag: Tag.type => TagType) =
p.ratingDiff.map { rd =>
Tag(tag(Tag), s"${if (rd >= 0) "+" else ""}$rd")
}
def tags(
game: Game,
initialFen: Option[FEN],
imported: Option[ParsedPgn],
withOpening: Boolean,
withRating: Boolean,
teams: Option[Color.Map[String]] = None
): Fu[Tags] =
gameLightUsers(game) map { case (wu, bu) =>
Tags {
val importedDate = imported.flatMap(_.tags(_.Date))
List[Option[Tag]](
Tag(
_.Event,
imported.flatMap(_.tags(_.Event)) | { if (game.imported) "Import" else eventOf(game) }
).some,
Tag(_.Site, gameUrl(game.id)).some,
Tag(_.Date, importedDate | Tag.UTCDate.format.print(game.createdAt)).some,
imported.flatMap(_.tags(_.Round)).map(Tag(_.Round, _)),
Tag(_.White, player(game.whitePlayer, wu)).some,
Tag(_.Black, player(game.blackPlayer, bu)).some,
Tag(_.Result, result(game)).some,
importedDate.isEmpty option Tag(
_.UTCDate,
imported.flatMap(_.tags(_.UTCDate)) | Tag.UTCDate.format.print(game.createdAt)
),
importedDate.isEmpty option Tag(
_.UTCTime,
imported.flatMap(_.tags(_.UTCTime)) | Tag.UTCTime.format.print(game.createdAt)
),
withRating option Tag(_.WhiteElo, rating(game.whitePlayer)),
withRating option Tag(_.BlackElo, rating(game.blackPlayer)),
withRating ?? ratingDiffTag(game.whitePlayer, _.WhiteRatingDiff),
withRating ?? ratingDiffTag(game.blackPlayer, _.BlackRatingDiff),
wu.flatMap(_.title).map { t =>
Tag(_.WhiteTitle, t)
},
bu.flatMap(_.title).map { t =>
Tag(_.BlackTitle, t)
},
teams.map { t => Tag("WhiteTeam", t.white) },
teams.map { t => Tag("BlackTeam", t.black) },
Tag(_.Variant, game.variant.name.capitalize).some,
Tag.timeControl(game.clock.map(_.config)).some,
Tag(_.ECO, game.opening.fold("?")(_.opening.eco)).some,
withOpening option Tag(_.Opening, game.opening.fold("?")(_.opening.name)),
Tag(
_.Termination, {
import chess.Status._
game.status match {
case Created | Started => "Unterminated"
case Aborted | NoStart => "Abandoned"
case Timeout | Outoftime => "Time forfeit"
case Resign | Draw | Stalemate | Mate | VariantEnd => "Normal"
case Cheat => "Rules infraction"
case UnknownFinish => "Unknown"
}
}
).some
).flatten ::: customStartPosition(game.variant).??(
initialFen.??(fen =>
List(
Tag(_.FEN, fen.value),
Tag("SetUp", "1")
)
)
)
}
}
private def makeTurns(
moves: Seq[String],
from: Int,
clocks: Vector[Centis],
startColor: Color
): List[chessPgn.Turn] =
(moves grouped 2).zipWithIndex.toList map { case (moves, index) =>
val clockOffset = startColor.fold(0, 1)
chessPgn.Turn(
number = index + from,
white = moves.headOption filter (".." !=) map { san =>
chessPgn.Move(
san = san,
secondsLeft = clocks lift (index * 2 - clockOffset) map (_.roundSeconds)
)
},
black = moves lift 1 map { san =>
chessPgn.Move(
san = san,
secondsLeft = clocks lift (index * 2 + 1 - clockOffset) map (_.roundSeconds)
)
}
)
} filterNot (_.isEmpty)
}
object PgnDump {
private val delayMovesBy = 3
private val delayKeepsFirstMoves = 5
case class WithFlags(
clocks: Boolean = true,
moves: Boolean = true,
tags: Boolean = true,
evals: Boolean = true,
opening: Boolean = true,
rating: Boolean = true,
literate: Boolean = false,
pgnInJson: Boolean = false,
delayMoves: Boolean = false
) {
def applyDelay[M](moves: Seq[M]): Seq[M] =
if (!delayMoves) moves
else moves.take((moves.size - delayMovesBy) atLeast delayKeepsFirstMoves)
def keepDelayIf(cond: Boolean) = copy(delayMoves = delayMoves && cond)
}
def result(game: Game) =
if (game.finished) Color.showResult(game.winnerColor)
else "*"
}