lila/modules/study/src/main/ChapterMaker.scala

260 lines
8.2 KiB
Scala

package lila.study
import chess.format.pgn.Tags
import chess.format.{ FEN, Forsyth }
import chess.variant.{ Crazyhouse, Variant }
import lila.chat.{ Chat, ChatApi }
import lila.game.{ Game, Namer }
import lila.user.User
import chess.Color
final private class ChapterMaker(
net: lila.common.config.NetConfig,
lightUser: lila.user.LightUserApi,
chatApi: ChatApi,
gameRepo: lila.game.GameRepo,
pgnFetch: PgnFetch,
pgnDump: lila.game.PgnDump
)(implicit ec: scala.concurrent.ExecutionContext) {
import ChapterMaker._
def apply(study: Study, data: Data, order: Int, userId: User.ID, withRatings: Boolean): Fu[Chapter] =
data.game.??(parseGame) flatMap {
case None =>
data.game ?? pgnFetch.fromUrl flatMap {
case Some(pgn) => fromFenOrPgnOrBlank(study, data.copy(pgn = pgn.some), order, userId)
case _ => fromFenOrPgnOrBlank(study, data, order, userId)
}
case Some(game) => fromGame(study, game, data, order, userId, withRatings)
} map { (c: Chapter) =>
if (c.name.value.isEmpty) c.copy(name = Chapter defaultName order) else c
}
def fromFenOrPgnOrBlank(study: Study, data: Data, order: Int, userId: User.ID): Fu[Chapter] =
data.pgn.filter(_.trim.nonEmpty) match {
case Some(pgn) => fromPgn(study, pgn, data, order, userId)
case None => fuccess(fromFenOrBlank(study, data, order, userId))
}
private def fromPgn(study: Study, pgn: String, data: Data, order: Int, userId: User.ID): Fu[Chapter] =
for {
contributors <- lightUser.asyncMany(study.members.contributorIds.toList)
parsed <- PgnImport(pgn, contributors.flatten).toFuture recoverWith { case e: Exception =>
fufail(ValidationException(e.getMessage))
}
} yield Chapter.make(
studyId = study.id,
name = parsed.tags(_.White).flatMap { white =>
parsed
.tags(_.Black)
.ifTrue {
data.name.value.isEmpty || data.isDefaultName
}
.map { black =>
Chapter.Name(s"$white - $black")
}
} | data.name,
setup = Chapter.Setup(
none,
parsed.variant,
resolveOrientation(data.realOrientation, parsed.root, parsed.tags)
),
root = parsed.root,
tags = parsed.tags,
order = order,
ownerId = userId,
practice = data.isPractice,
gamebook = data.isGamebook,
conceal = data.isConceal option Chapter.Ply(parsed.root.ply)
)
private def resolveOrientation(orientation: Orientation, root: Node.Root, tags: Tags = Tags.empty): Color =
orientation match {
case Orientation.Fixed(color) => color
case _ if tags.resultColor.isDefined => Color.white
case _ => root.lastMainlineNode.color
}
private def fromFenOrBlank(study: Study, data: Data, order: Int, userId: User.ID): Chapter = {
val variant = data.variant.flatMap(Variant.apply) | Variant.default
(data.fen.filterNot(_.initial).flatMap { Forsyth.<<<@(variant, _) } match {
case Some(sit) =>
Node.Root(
ply = sit.turns,
fen = Forsyth >> sit,
check = sit.situation.check,
clock = none,
crazyData = sit.situation.board.crazyData,
children = Node.emptyChildren
) -> true
case None =>
Node.Root(
ply = 0,
fen = variant.initialFen,
check = false,
clock = none,
crazyData = variant.crazyhouse option Crazyhouse.Data.init,
children = Node.emptyChildren
) -> false
}) match {
case (root, isFromFen) =>
Chapter.make(
studyId = study.id,
name = data.name,
setup = Chapter.Setup(
none,
variant,
resolveOrientation(data.realOrientation, root),
fromFen = isFromFen option true
),
root = root,
tags = Tags.empty,
order = order,
ownerId = userId,
practice = data.isPractice,
gamebook = data.isGamebook,
conceal = None
)
}
}
private[study] def fromGame(
study: Study,
game: Game,
data: Data,
order: Int,
userId: User.ID,
withRatings: Boolean,
initialFen: Option[FEN] = None
): Fu[Chapter] =
for {
root <- game2root(game, initialFen)
tags <- pgnDump.tags(game, initialFen, none, withOpening = true, withRatings)
name <- {
if (data.isDefaultName)
Namer.gameVsText(game, withRatings)(lightUser.async) dmap Chapter.Name.apply
else fuccess(data.name)
}
_ = notifyChat(study, game, userId)
} yield Chapter.make(
studyId = study.id,
name = name,
setup = Chapter.Setup(
!game.synthetic option game.id,
game.variant,
data.realOrientation match {
case Orientation.Auto => Color.white
case Orientation.Fixed(color) => color
}
),
root = root,
tags = PgnTags(tags),
order = order,
ownerId = userId,
practice = data.isPractice,
gamebook = data.isGamebook,
conceal = data.isConceal option Chapter.Ply(root.ply)
)
def notifyChat(study: Study, game: Game, userId: User.ID) =
if (study.isPublic) List(game hasUserId userId option game.id, s"${game.id}/w".some).flatten foreach {
chatId =>
chatApi.userChat.write(
chatId = Chat.Id(chatId),
userId = userId,
text = s"I'm studying this game on ${net.domain}/study/${study.id}",
publicSource = none,
_.Round,
persist = false
)
}
private[study] def game2root(game: Game, initialFen: Option[FEN]): Fu[Node.Root] =
initialFen.fold(gameRepo initialFen game) { fen =>
fuccess(fen.some)
} map { GameToRoot(game, _, withClocks = true) }
private val UrlRegex = {
val escapedDomain = net.domain.value.replace(".", "\\.")
s"""$escapedDomain/(\\w{8,12})"""
}.r.unanchored
@scala.annotation.tailrec
private def parseGame(str: String): Fu[Option[Game]] =
str match {
case s if s.lengthIs == Game.gameIdSize => gameRepo game s
case s if s.lengthIs == Game.fullIdSize => gameRepo game Game.takeGameId(s)
case UrlRegex(id) => parseGame(id)
case _ => fuccess(none)
}
}
private[study] object ChapterMaker {
case class ValidationException(message: String) extends lila.base.LilaException
sealed trait Mode {
def key = toString.toLowerCase
}
object Mode {
case object Normal extends Mode
case object Practice extends Mode
case object Gamebook extends Mode
case object Conceal extends Mode
val all = List(Normal, Practice, Gamebook, Conceal)
def apply(key: String) = all.find(_.key == key)
}
trait ChapterData {
def orientation: String
def mode: String
def realOrientation = Color.fromName(orientation).fold[Orientation](Orientation.Auto)(Orientation.Fixed)
def isPractice = mode == Mode.Practice.key
def isGamebook = mode == Mode.Gamebook.key
def isConceal = mode == Mode.Conceal.key
}
sealed trait Orientation
object Orientation {
case class Fixed(color: Color) extends Orientation
case object Auto extends Orientation
}
case class Data(
name: Chapter.Name,
game: Option[String] = None,
variant: Option[String] = None,
fen: Option[FEN] = None,
pgn: Option[String] = None,
orientation: String = "white", // can be "automatic"
mode: String = ChapterMaker.Mode.Normal.key,
initial: Boolean = false,
isDefaultName: Boolean = true
) extends ChapterData {
def manyGames =
game
.??(_.linesIterator.take(Study.maxChapters).toList)
.map(_.trim)
.filter(_.nonEmpty)
.map { g => copy(game = g.some) }
.some
.filter(_.sizeIs > 1)
}
case class EditData(
id: Chapter.Id,
name: Chapter.Name,
orientation: String,
mode: String,
description: String // boolean
) extends ChapterData {
def hasDescription = description.nonEmpty
}
case class DescData(id: Chapter.Id, desc: String) {
lazy val clean = lila.common.String.fullCleanUp(desc)
}
}