174 lines
6.6 KiB
Scala
174 lines
6.6 KiB
Scala
package lila.study
|
|
|
|
import chess.format.pgn.Glyphs
|
|
import chess.format.{ Forsyth, Uci, UciCharPair }
|
|
import play.api.libs.json._
|
|
import scala.concurrent.duration._
|
|
|
|
import lila.analyse.{ Analysis, Info }
|
|
import lila.hub.actorApi.fishnet.StudyChapterRequest
|
|
import lila.security.Granter
|
|
import lila.tree.Node.Comment
|
|
import lila.user.{ User, UserRepo }
|
|
import lila.{ tree => T }
|
|
|
|
object ServerEval {
|
|
|
|
final class Requester(
|
|
fishnet: lila.hub.actors.Fishnet,
|
|
chapterRepo: ChapterRepo,
|
|
userRepo: UserRepo
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
|
|
|
private val onceEvery = lila.memo.OnceEvery(5 minutes)
|
|
|
|
def apply(study: Study, chapter: Chapter, userId: User.ID, unlimited: Boolean = false): Funit =
|
|
chapter.serverEval.fold(true) { eval =>
|
|
!eval.done && onceEvery(chapter.id.value)
|
|
} ?? {
|
|
val unlimitedFu =
|
|
fuccess(unlimited) >>|
|
|
fuccess(userId == User.lichessId) >>| userRepo
|
|
.byId(userId)
|
|
.map(_.exists(Granter(_.Relay)))
|
|
unlimitedFu flatMap { unlimited =>
|
|
chapterRepo.startServerEval(chapter) >>- {
|
|
fishnet ! StudyChapterRequest(
|
|
studyId = study.id.value,
|
|
chapterId = chapter.id.value,
|
|
initialFen = chapter.root.fen.some,
|
|
variant = chapter.setup.variant,
|
|
moves = chess.format
|
|
.UciDump(
|
|
moves = chapter.root.mainline.map(_.move.san),
|
|
initialFen = chapter.root.fen.some,
|
|
variant = chapter.setup.variant,
|
|
force960Notation = true
|
|
)
|
|
.toOption
|
|
.map(_.flatMap(chess.format.Uci.apply)) | List.empty,
|
|
userId = userId,
|
|
unlimited = unlimited
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final class Merger(
|
|
sequencer: StudySequencer,
|
|
socket: StudySocket,
|
|
chapterRepo: ChapterRepo,
|
|
divider: lila.game.Divider
|
|
)(implicit ec: scala.concurrent.ExecutionContext) {
|
|
|
|
def apply(analysis: Analysis, complete: Boolean): Funit =
|
|
analysis.studyId.map(Study.Id.apply) ?? { studyId =>
|
|
sequencer.sequenceStudyWithChapter(studyId, Chapter.Id(analysis.id)) {
|
|
case Study.WithChapter(_, chapter) =>
|
|
(complete ?? chapterRepo.completeServerEval(chapter)) >> {
|
|
lila.common.Future
|
|
.fold(chapter.root.mainline.zip(analysis.infoAdvices).toList)(Path.root) {
|
|
case (path, (node, (info, advOpt))) =>
|
|
chapter.root.nodeAt(path).flatMap { parent =>
|
|
analysisLine(parent, chapter.setup.variant, info) map { subTree =>
|
|
parent.addChild(subTree) -> subTree
|
|
}
|
|
} ?? { case (newParent, subTree) =>
|
|
chapterRepo.addSubTree(subTree, newParent, path)(chapter)
|
|
} >> {
|
|
import BSONHandlers._
|
|
import Node.{ BsonFields => F }
|
|
((info.eval.score.isDefined && node.score.isEmpty) || (advOpt.isDefined && !node.comments.hasLichessComment)) ??
|
|
chapterRepo
|
|
.setNodeValues(
|
|
chapter,
|
|
path + node,
|
|
List(
|
|
F.score -> info.eval.score
|
|
.ifTrue {
|
|
node.score.isEmpty ||
|
|
advOpt.isDefined && node.comments.findBy(Comment.Author.Lichess).isEmpty
|
|
}
|
|
.flatMap(EvalScoreBSONHandler.writeOpt),
|
|
F.comments -> advOpt
|
|
.map { adv =>
|
|
node.comments + Comment(
|
|
Comment.Id.make,
|
|
Comment.Text(adv.makeComment(withEval = false, withBestMove = true)),
|
|
Comment.Author.Lichess
|
|
)
|
|
}
|
|
.flatMap(CommentsBSONHandler.writeOpt),
|
|
F.glyphs -> advOpt
|
|
.map { adv =>
|
|
node.glyphs merge Glyphs.fromList(List(adv.judgment.glyph))
|
|
}
|
|
.flatMap(GlyphsBSONHandler.writeOpt)
|
|
)
|
|
)
|
|
} inject path + node
|
|
} void
|
|
} >>- {
|
|
chapterRepo.byId(Chapter.Id(analysis.id)).foreach {
|
|
_ ?? { chapter =>
|
|
socket.onServerEval(
|
|
studyId,
|
|
ServerEval.Progress(
|
|
chapterId = chapter.id,
|
|
tree = lila.study.TreeBuilder(chapter.root, chapter.setup.variant),
|
|
analysis = toJson(chapter, analysis),
|
|
division = divisionOf(chapter)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
} logFailure logger
|
|
}
|
|
}
|
|
|
|
def divisionOf(chapter: Chapter) =
|
|
divider(
|
|
id = chapter.id.value,
|
|
pgnMoves = chapter.root.mainline.map(_.move.san).toVector,
|
|
variant = chapter.setup.variant,
|
|
initialFen = chapter.root.fen.some
|
|
)
|
|
|
|
private def analysisLine(root: RootOrNode, variant: chess.variant.Variant, info: Info): Option[Node] =
|
|
chess.Replay.gameMoveWhileValid(info.variation take 20, root.fen, variant) match {
|
|
case (_, games, error) =>
|
|
error foreach { logger.info(_) }
|
|
games.reverse match {
|
|
case Nil => none
|
|
case (g, m) :: rest =>
|
|
rest
|
|
.foldLeft(makeBranch(g, m)) { case (node, (g, m)) =>
|
|
makeBranch(g, m) addChild node
|
|
} some
|
|
}
|
|
}
|
|
|
|
private def makeBranch(g: chess.Game, m: Uci.WithSan) =
|
|
Node(
|
|
id = UciCharPair(m.uci),
|
|
ply = g.turns,
|
|
move = m,
|
|
fen = Forsyth >> g,
|
|
check = g.situation.check,
|
|
crazyData = g.situation.board.crazyData,
|
|
clock = none,
|
|
children = Node.emptyChildren,
|
|
forceVariation = false
|
|
)
|
|
}
|
|
|
|
case class Progress(chapterId: Chapter.Id, tree: T.Root, analysis: JsObject, division: chess.Division)
|
|
|
|
def toJson(chapter: Chapter, analysis: Analysis) =
|
|
lila.analyse.JsonView.bothPlayers(
|
|
lila.analyse.Accuracy.PovLike(chess.White, chapter.root.color, chapter.root.ply),
|
|
analysis
|
|
)
|
|
}
|