119 lines
3.7 KiB
Scala
119 lines
3.7 KiB
Scala
package lila.fishnet
|
|
|
|
import chess.format.Forsyth
|
|
import chess.format.Uci
|
|
import chess.{ Color, Speed }
|
|
import com.softwaremill.tagging._
|
|
import play.api.libs.json._
|
|
import play.api.libs.ws.JsonBodyReadables._
|
|
import play.api.libs.ws.StandaloneWSClient
|
|
import scala.concurrent.ExecutionContext
|
|
import scala.concurrent.duration._
|
|
|
|
import lila.common.Json._
|
|
import lila.common.ThreadLocalRandom
|
|
import lila.game.Game
|
|
import lila.memo.SettingStore
|
|
import scala.util.{ Failure, Success }
|
|
|
|
final private class FishnetOpeningBook(
|
|
ws: StandaloneWSClient,
|
|
depth: SettingStore[Int] @@ FishnetOpeningBook.Depth
|
|
)(implicit ec: ExecutionContext) {
|
|
|
|
import FishnetOpeningBook._
|
|
|
|
private val outOfBook = new lila.memo.ExpireSetMemo(10 minutes)
|
|
|
|
def apply(game: Game, level: Int): Fu[Option[Uci]] =
|
|
(game.turns < depth.get() && !outOfBook.get(game.id)) ?? {
|
|
ws.url(endpoint)
|
|
.withQueryStringParameters(
|
|
"variant" -> game.variant.key,
|
|
"fen" -> Forsyth.>>(game.chess).value,
|
|
"topGames" -> "0",
|
|
"recentGames" -> "0",
|
|
"ratings" -> (~levelRatings.get(level)).mkString(","),
|
|
"speeds" -> (~openingSpeeds.get(game.speed)).map(_.key).mkString(",")
|
|
)
|
|
.get()
|
|
.map {
|
|
case res if res.status != 200 =>
|
|
logger.warn(s"opening book ${game.id} ${level} ${res.status} ${res.body}")
|
|
none
|
|
case res =>
|
|
for {
|
|
data <- res.body[JsValue].validate[Response](responseReader).asOpt
|
|
_ = if (data.moves.isEmpty) outOfBook.put(game.id)
|
|
move <- data randomPonderedMove (game.turnColor, level)
|
|
} yield move.uci
|
|
}
|
|
.monTry { res =>
|
|
_.fishnet
|
|
.openingBook(
|
|
level = level,
|
|
variant = game.variant.key,
|
|
ply = game.turns,
|
|
hit = res.toOption.exists(_.isDefined),
|
|
success = res.isSuccess
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
object FishnetOpeningBook {
|
|
|
|
trait Depth
|
|
|
|
case class Response(moves: List[Move]) {
|
|
|
|
def randomPonderedMove(turn: Color, level: Int): Option[Move] = {
|
|
val sum = moves.map(_.score(turn, level)).sum
|
|
val novelty = 5L * 14 // score of 5 winning games
|
|
val rng = ThreadLocalRandom.nextLong(sum + novelty)
|
|
moves
|
|
.foldLeft((none[Move], 0L)) { case ((found, it), next) =>
|
|
val nextIt = it + next.score(turn, level)
|
|
(found orElse (nextIt > rng).option(next), nextIt)
|
|
}
|
|
._1
|
|
}
|
|
}
|
|
|
|
case class Move(uci: Uci, white: Long, draws: Long, black: Long) {
|
|
def score(turn: Color, level: Int): Long =
|
|
// interpolate: real frequency at lvl 1, expectation value at lvl 8
|
|
14L * turn.fold(white, black) +
|
|
(15L - level) * draws +
|
|
(16L - 2 * level) * turn.fold(black, white)
|
|
}
|
|
|
|
implicit val moveReader = Json.reads[Move]
|
|
implicit val responseReader = Json.reads[Response]
|
|
|
|
private val endpoint = "https://explorer.lichess.ovh/lichess"
|
|
|
|
private val levelRatings: Map[Int, Seq[Int]] = Map(
|
|
1 -> Seq(1600),
|
|
2 -> Seq(1600, 1800),
|
|
3 -> Seq(1800, 2000),
|
|
4 -> Seq(1800, 2000, 2200),
|
|
5 -> Seq(1800, 2000, 2200),
|
|
6 -> Seq(2000, 2200, 2500),
|
|
7 -> Seq(2200, 2500),
|
|
8 -> Seq(2500)
|
|
)
|
|
|
|
private val openingSpeeds: Map[Speed, Seq[Speed]] = {
|
|
import Speed._
|
|
Map(
|
|
UltraBullet -> Seq(UltraBullet, Bullet),
|
|
Bullet -> Seq(Bullet, Blitz),
|
|
Blitz -> Seq(Bullet, Blitz, Rapid),
|
|
Rapid -> Seq(Blitz, Rapid, Classical),
|
|
Classical -> Seq(Rapid, Classical, Correspondence),
|
|
Correspondence -> Seq(Rapid, Classical, Correspondence)
|
|
)
|
|
}
|
|
}
|