puzzle storm WIP

storm
Thibault Duplessis 2021-01-22 19:37:49 +01:00
parent ddf0a0a43a
commit 9e39278344
22 changed files with 310 additions and 12 deletions

View File

@ -0,0 +1,19 @@
package controllers
import play.api.libs.json.Json
import play.api.mvc._
import scala.concurrent.duration._
import views._
import lila.api.Context
import lila.app._
final class Storm(env: Env)(implicit mat: akka.stream.Materializer) extends LilaController(env) {
def home =
Open { implicit ctx =>
env.storm.selector.apply map { puzzles =>
Ok(views.html.storm.home(env.storm.json(puzzles)))
}
}
}

View File

@ -0,0 +1,36 @@
package views.html
import play.api.libs.json._
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
import lila.common.String.html.safeJsonValue
object storm {
def home(json: JsObject)(implicit ctx: Context) =
views.html.base.layout(
moreCss = frag(cssTag("storm")),
moreJs = frag(
jsModule("storm"),
embedJsUnsafeLoadThen(
s"""LichessStorm(${safeJsonValue(
Json.obj(
"data" -> json,
"i18n" -> jsI18n
)
)})"""
)
),
title = "Puzzle Storm"
) {
main(cls := "box storm-app")
}
def jsI18n(implicit ctx: Context) = i18nJsObject(i18nKeys)
private val i18nKeys = List(
trans.storm.moveToStart
).map(_.key)
}

View File

@ -2,7 +2,7 @@ const fs = require('fs').promises;
const parseString = require('xml2js').parseString;
const baseDir = 'translation/source';
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge'.split(' ');
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge storm'.split(' ');
function ucfirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);

View File

@ -60,7 +60,7 @@ lazy val i18n = smallModule("i18n",
MessageCompiler(
sourceDir = new File("translation/source"),
destDir = new File("translation/dest"),
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge".split(' ').toList,
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team perfStat search tourname faq lag swiss puzzle puzzleTheme challenge storm".split(' ').toList,
compileTo = (sourceManaged in Compile).value
)
}.taskValue
@ -71,6 +71,11 @@ lazy val puzzle = module("puzzle",
reactivemongo.bundle
)
lazy val storm = module("storm",
Seq(common, memo, hub, puzzle, db, user, rating, pref, tree),
reactivemongo.bundle
)
lazy val quote = smallModule("quote",
Seq(),
Seq(play.json)

View File

@ -99,6 +99,9 @@ POST /training/$id<\w{5}>/vote/:theme controllers.Puzzle.voteTheme(id: String,
POST /training/complete/:theme/$id<\w{5}> controllers.Puzzle.complete(theme: String, id: String)
POST /training/difficulty/:theme controllers.Puzzle.setDifficulty(theme: String)
# Puzzle Storm
GET /storm controllers.Storm.home
# User Analysis
GET /analysis/help controllers.UserAnalysis.help
GET /analysis/*something controllers.UserAnalysis.parseArg(something: String)

View File

@ -1970,4 +1970,8 @@ val `declineStandard` = new I18nKey("challenge:declineStandard")
val `declineVariant` = new I18nKey("challenge:declineVariant")
}
object storm {
val `moveToStart` = new I18nKey("storm:moveToStart")
}
}

View File

@ -2,21 +2,20 @@ package lila.puzzle
import chess.format.{ FEN, Uci }
import reactivemongo.api.bson._
import scala.util.Success
import scala.util.Try
import scala.util.{ Success, Try }
import lila.db.BSON
import lila.db.dsl._
import lila.game.Game
import lila.rating.Glicko
private[puzzle] object BsonHandlers {
object BsonHandlers {
implicit val PuzzleIdBSONHandler = stringIsoHandler(Puzzle.idIso)
import Puzzle.BSONFields._
implicit val PuzzleBSONReader = new BSONDocumentReader[Puzzle] {
implicit private[puzzle] val PuzzleBSONReader = new BSONDocumentReader[Puzzle] {
def readDocument(r: BSONDocument) = for {
id <- r.getAsTry[Puzzle.Id](id)
gameId <- r.getAsTry[Game.ID](gameId)
@ -39,7 +38,7 @@ private[puzzle] object BsonHandlers {
)
}
implicit val RoundIdHandler = tryHandler[PuzzleRound.Id](
implicit private[puzzle] val RoundIdHandler = tryHandler[PuzzleRound.Id](
{ case BSONString(v) =>
v split PuzzleRound.idSep match {
case Array(userId, puzzleId) => Success(PuzzleRound.Id(userId, Puzzle.Id(puzzleId)))
@ -49,7 +48,7 @@ private[puzzle] object BsonHandlers {
id => BSONString(id.toString)
)
implicit val RoundThemeHandler = tryHandler[PuzzleRound.Theme](
implicit private[puzzle] val RoundThemeHandler = tryHandler[PuzzleRound.Theme](
{ case BSONString(v) =>
PuzzleTheme
.find(v.tail)
@ -60,7 +59,7 @@ private[puzzle] object BsonHandlers {
rt => BSONString(s"${if (rt.vote) "+" else "-"}${rt.theme}")
)
implicit val RoundHandler = new BSON[PuzzleRound] {
implicit private[puzzle] val RoundHandler = new BSON[PuzzleRound] {
import PuzzleRound.BSONFields._
def reads(r: BSON.Reader) = PuzzleRound(
id = r.get[PuzzleRound.Id](id),
@ -81,7 +80,11 @@ private[puzzle] object BsonHandlers {
)
}
implicit val PathIdBSONHandler: BSONHandler[PuzzlePath.Id] = stringIsoHandler(PuzzlePath.pathIdIso)
implicit private[puzzle] val PathIdBSONHandler: BSONHandler[PuzzlePath.Id] = stringIsoHandler(
PuzzlePath.pathIdIso
)
implicit val ThemeKeyBSONHandler: BSONHandler[PuzzleTheme.Key] = stringIsoHandler(PuzzleTheme.keyIso)
implicit private[puzzle] val ThemeKeyBSONHandler: BSONHandler[PuzzleTheme.Key] = stringIsoHandler(
PuzzleTheme.keyIso
)
}

View File

@ -0,0 +1,22 @@
package lila.storm
import chess.format.{ FEN, Uci }
import reactivemongo.api.bson._
import lila.db.BSON
import lila.db.dsl._
import lila.puzzle.Puzzle
private[storm] object BsonHandlers {
import lila.puzzle.BsonHandlers.{ PuzzleIdBSONHandler }
implicit val StormPuzzleBSONReader = new BSONDocumentReader[StormPuzzle] {
def readDocument(r: BSONDocument) = for {
id <- r.getAsTry[Puzzle.Id]("_id")
fen <- r.getAsTry[FEN]("fen")
lineStr <- r.getAsTry[String]("line")
line <- lineStr.split(' ').toList.flatMap(Uci.Move.apply).toNel.toTry("Empty move list?!")
} yield StormPuzzle(id, fen, line)
}
}

View File

@ -0,0 +1,21 @@
package lila.storm
import com.softwaremill.macwire._
import play.api.Configuration
import lila.common.config._
@Module
final class Env(
appConfig: Configuration,
db: lila.db.Db,
colls: lila.puzzle.PuzzleColls,
cacheApi: lila.memo.CacheApi
)(implicit
ec: scala.concurrent.ExecutionContext
) {
lazy val selector = wire[StormSelector]
lazy val json = new StormJson
}

View File

@ -0,0 +1,27 @@
package lila.storm
import play.api.libs.json._
import lila.common.Json._
final class StormJson {
import StormJson.puzzleWrites
def apply(puzzles: List[StormPuzzle]): JsObject = Json.obj(
"puzzles" -> puzzles
)
}
object StormJson {
import lila.puzzle.JsonView.puzzleIdWrites
implicit val puzzleWrites: OWrites[StormPuzzle] = OWrites { p =>
Json.obj(
"id" -> p.id,
"fen" -> p.fen.value,
"line" -> p.line.toList.map(_.uci)
)
}
}

View File

@ -0,0 +1,27 @@
package lila.storm
import cats.data.NonEmptyList
import chess.format.{ FEN, Forsyth, Uci }
import lila.puzzle.Puzzle
case class StormPuzzle(
id: Puzzle.Id,
fen: FEN,
line: NonEmptyList[Uci.Move]
) {
// ply after "initial move" when we start solving
def initialPly: Int =
fen.fullMove ?? { fm =>
fm * 2 - color.fold(1, 2)
}
lazy val fenAfterInitialMove: FEN = {
for {
sit1 <- Forsyth << fen
sit2 <- sit1.move(line.head).toOption.map(_.situationAfter)
} yield Forsyth >> sit2
} err s"Can't apply puzzle $id first move"
def color = fen.color.fold[chess.Color](chess.White)(!_)
}

View File

@ -0,0 +1,49 @@
package lila.storm
import scala.concurrent.duration._
import lila.db.AsyncColl
import lila.memo.CacheApi
import lila.db.dsl._
import lila.puzzle.PuzzleColls
final class StormSelector(colls: PuzzleColls, cacheApi: CacheApi) {
val poolSize = 100
private val current = cacheApi.unit[List[StormPuzzle]] {
_.refreshAfterWrite(10 seconds)
.buildAsyncFuture { _ =>
colls.path {
_.aggregateList(poolSize) { framework =>
import framework._
Match(pathApi.select(theme, tier, ratingRange)) -> List(
Sample(pathSampleSize),
Project($doc("puzzleId" -> "$ids", "_id" -> false)),
Unwind("puzzleId"),
Sample(poolSize),
PipelineOperator(
$doc(
"$lookup" -> $doc(
"from" -> colls.puzzle.name.value,
"localField" -> "puzzleId",
"foreignField" -> "_id",
"as" -> "puzzle"
)
)
),
PipelineOperator(
$doc(
"$replaceWith" -> $doc("$arrayElemAt" -> $arr("$puzzle", 0))
)
)
)
}.map {
_.view.flatMap(PuzzleBSONReader.readOpt).toVector
}
}
}
}
def apply: Fu[List[StormPuzzle]] = current.get {}
}

View File

@ -0,0 +1,8 @@
package lila
import lila.user.User
package object storm extends PackageObject {
private[storm] val logger = lila.log("swiss")
}

View File

@ -55,6 +55,7 @@
"ui/tree",
"ui/msg",
"ui/dgt",
"ui/storm",
"ui/@build/rollupProject",
"ui/@types/lichess",
"ui/@types/cash"

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="moveToStart">Move to start</string>
</resources>

View File

@ -15,7 +15,7 @@ mkdir -p public/compiled
apps1="common"
apps2="chess ceval game tree chat nvui"
apps3="site swiss msg chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul dasher speech palantir serviceWorker dgt"
apps3="site swiss msg chat cli challenge notify learn insight editor puzzle round analyse lobby tournament tournamentSchedule tournamentCalendar simul dasher speech palantir serviceWorker dgt storm"
site_plugins="tvEmbed puzzleEmbed analyseEmbed user modUser clas coordinate captcha expandText team forum account coachShow coachForm challengePage checkout login passwordComplexity tourForm teamBattleForm gameSearch userComplete infiniteScroll flatpickr teamAdmin"
round_plugins="nvui keyboardMove"
analyse_plugins="nvui studyTopicForm"

View File

@ -0,0 +1,51 @@
#main-wrap {
--main-max-width: calc(100vh - #{$site-header-outer-height} - #{$col1-uniboard-controls});
@include breakpoint($mq-col2) {
--main-max-width: auto;
}
}
.storm {
grid-area: main;
display: grid;
&__side {
grid-area: side;
}
&__board {
grid-area: board;
}
&__tools {
grid-area: tools;
}
&__controls {
grid-area: controls;
}
&__session {
grid-area: session;
align-self: start;
}
grid-template-areas: 'board';
grid-row-gap: $block-gap;
@include breakpoint($mq-col2) {
grid-template-columns: $col2-uniboard-width $col2-uniboard-table;
grid-template-rows: fit-content(0);
grid-template-areas: 'board tools' 'session controls' 'side side';
}
@include breakpoint($mq-col3) {
grid-template-areas: 'side board tools';
grid-template-columns: $col3-uniboard-side $col3-uniboard-width $col3-uniboard-table;
}
&__side {}
}

View File

@ -0,0 +1,5 @@
$mq-col2-squeeze: $mq-col2-uniboard-squeeze;
$mq-col2: $mq-col2-uniboard;
$mq-col3: $mq-col3-uniboard;
@import "layout";

View File

@ -0,0 +1,7 @@
@import "../../../common/css/plugin";
@import "../../../common/css/vendor/chessground/coords";
@import "../../../common/css/layout/uniboard";
@import "../../../common/css/component/board-resize";
@import "../../../chess/css/promotion";
@import "../storm";

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/dark';
@import 'storm';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/light';
@import 'storm';

View File

@ -0,0 +1,2 @@
@import '../../../common/css/theme/transp';
@import 'storm';