From 9e39278344353b3f09ae67c931dad5fc3fedb403 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Fri, 22 Jan 2021 19:37:49 +0100 Subject: [PATCH] puzzle storm WIP --- app/controllers/Storm.scala | 19 ++++++++ app/views/storm.scala | 36 +++++++++++++++ bin/trans-dump.js | 2 +- build.sbt | 7 ++- conf/routes | 3 ++ modules/i18n/src/main/I18nKeys.scala | 4 ++ modules/puzzle/src/main/BsonHandlers.scala | 21 +++++---- modules/storm/src/main/BsonHandlers.scala | 22 ++++++++++ modules/storm/src/main/Env.scala | 21 +++++++++ modules/storm/src/main/StormJson.scala | 27 ++++++++++++ modules/storm/src/main/StormPuzzle.scala | 27 ++++++++++++ modules/storm/src/main/StormSelector.scala | 49 +++++++++++++++++++++ modules/storm/src/main/package.scala | 8 ++++ package.json | 1 + translation/source/storm.xml | 4 ++ ui/build | 2 +- ui/storm/css/_layout.scss | 51 ++++++++++++++++++++++ ui/storm/css/_storm.scss | 5 +++ ui/storm/css/build/_storm.scss | 7 +++ ui/storm/css/build/storm.dark.scss | 2 + ui/storm/css/build/storm.light.scss | 2 + ui/storm/css/build/storm.transp.scss | 2 + 22 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 app/controllers/Storm.scala create mode 100644 app/views/storm.scala create mode 100644 modules/storm/src/main/BsonHandlers.scala create mode 100644 modules/storm/src/main/Env.scala create mode 100644 modules/storm/src/main/StormJson.scala create mode 100644 modules/storm/src/main/StormPuzzle.scala create mode 100644 modules/storm/src/main/StormSelector.scala create mode 100644 modules/storm/src/main/package.scala create mode 100644 translation/source/storm.xml create mode 100644 ui/storm/css/_layout.scss create mode 100644 ui/storm/css/_storm.scss create mode 100644 ui/storm/css/build/_storm.scss create mode 100644 ui/storm/css/build/storm.dark.scss create mode 100644 ui/storm/css/build/storm.light.scss create mode 100644 ui/storm/css/build/storm.transp.scss diff --git a/app/controllers/Storm.scala b/app/controllers/Storm.scala new file mode 100644 index 0000000000..ecbaccd31b --- /dev/null +++ b/app/controllers/Storm.scala @@ -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))) + } + } +} diff --git a/app/views/storm.scala b/app/views/storm.scala new file mode 100644 index 0000000000..e6b949c47c --- /dev/null +++ b/app/views/storm.scala @@ -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) +} diff --git a/bin/trans-dump.js b/bin/trans-dump.js index 23b5107151..aba729d0a3 100644 --- a/bin/trans-dump.js +++ b/bin/trans-dump.js @@ -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); diff --git a/build.sbt b/build.sbt index 71685e768f..030d1da7a1 100644 --- a/build.sbt +++ b/build.sbt @@ -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) diff --git a/conf/routes b/conf/routes index f520320032..b050a3b287 100644 --- a/conf/routes +++ b/conf/routes @@ -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) diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala index a28feee654..5a20beb0e8 100644 --- a/modules/i18n/src/main/I18nKeys.scala +++ b/modules/i18n/src/main/I18nKeys.scala @@ -1970,4 +1970,8 @@ val `declineStandard` = new I18nKey("challenge:declineStandard") val `declineVariant` = new I18nKey("challenge:declineVariant") } +object storm { +val `moveToStart` = new I18nKey("storm:moveToStart") +} + } diff --git a/modules/puzzle/src/main/BsonHandlers.scala b/modules/puzzle/src/main/BsonHandlers.scala index 1397455fd3..60e11530a8 100644 --- a/modules/puzzle/src/main/BsonHandlers.scala +++ b/modules/puzzle/src/main/BsonHandlers.scala @@ -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 + ) } diff --git a/modules/storm/src/main/BsonHandlers.scala b/modules/storm/src/main/BsonHandlers.scala new file mode 100644 index 0000000000..cb6b0681ad --- /dev/null +++ b/modules/storm/src/main/BsonHandlers.scala @@ -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) + } +} diff --git a/modules/storm/src/main/Env.scala b/modules/storm/src/main/Env.scala new file mode 100644 index 0000000000..3d5ae7a97c --- /dev/null +++ b/modules/storm/src/main/Env.scala @@ -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 +} diff --git a/modules/storm/src/main/StormJson.scala b/modules/storm/src/main/StormJson.scala new file mode 100644 index 0000000000..07e2bf26e5 --- /dev/null +++ b/modules/storm/src/main/StormJson.scala @@ -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) + ) + } +} diff --git a/modules/storm/src/main/StormPuzzle.scala b/modules/storm/src/main/StormPuzzle.scala new file mode 100644 index 0000000000..1cc1afb1c7 --- /dev/null +++ b/modules/storm/src/main/StormPuzzle.scala @@ -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)(!_) +} diff --git a/modules/storm/src/main/StormSelector.scala b/modules/storm/src/main/StormSelector.scala new file mode 100644 index 0000000000..cd934445c4 --- /dev/null +++ b/modules/storm/src/main/StormSelector.scala @@ -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 {} +} diff --git a/modules/storm/src/main/package.scala b/modules/storm/src/main/package.scala new file mode 100644 index 0000000000..d628c104b3 --- /dev/null +++ b/modules/storm/src/main/package.scala @@ -0,0 +1,8 @@ +package lila + +import lila.user.User + +package object storm extends PackageObject { + + private[storm] val logger = lila.log("swiss") +} diff --git a/package.json b/package.json index bfa7108556..2cd105e2d4 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "ui/tree", "ui/msg", "ui/dgt", + "ui/storm", "ui/@build/rollupProject", "ui/@types/lichess", "ui/@types/cash" diff --git a/translation/source/storm.xml b/translation/source/storm.xml new file mode 100644 index 0000000000..35c592367a --- /dev/null +++ b/translation/source/storm.xml @@ -0,0 +1,4 @@ + + + Move to start + diff --git a/ui/build b/ui/build index 8d0b27c5ed..8b6ae04df2 100755 --- a/ui/build +++ b/ui/build @@ -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" diff --git a/ui/storm/css/_layout.scss b/ui/storm/css/_layout.scss new file mode 100644 index 0000000000..aefed8c6d2 --- /dev/null +++ b/ui/storm/css/_layout.scss @@ -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 {} +} diff --git a/ui/storm/css/_storm.scss b/ui/storm/css/_storm.scss new file mode 100644 index 0000000000..2fe9ea7308 --- /dev/null +++ b/ui/storm/css/_storm.scss @@ -0,0 +1,5 @@ +$mq-col2-squeeze: $mq-col2-uniboard-squeeze; +$mq-col2: $mq-col2-uniboard; +$mq-col3: $mq-col3-uniboard; + +@import "layout"; diff --git a/ui/storm/css/build/_storm.scss b/ui/storm/css/build/_storm.scss new file mode 100644 index 0000000000..eebe627cbd --- /dev/null +++ b/ui/storm/css/build/_storm.scss @@ -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"; diff --git a/ui/storm/css/build/storm.dark.scss b/ui/storm/css/build/storm.dark.scss new file mode 100644 index 0000000000..33765a2d71 --- /dev/null +++ b/ui/storm/css/build/storm.dark.scss @@ -0,0 +1,2 @@ +@import '../../../common/css/theme/dark'; +@import 'storm'; diff --git a/ui/storm/css/build/storm.light.scss b/ui/storm/css/build/storm.light.scss new file mode 100644 index 0000000000..993c373e9f --- /dev/null +++ b/ui/storm/css/build/storm.light.scss @@ -0,0 +1,2 @@ +@import '../../../common/css/theme/light'; +@import 'storm'; diff --git a/ui/storm/css/build/storm.transp.scss b/ui/storm/css/build/storm.transp.scss new file mode 100644 index 0000000000..36d1c3a17a --- /dev/null +++ b/ui/storm/css/build/storm.transp.scss @@ -0,0 +1,2 @@ +@import '../../../common/css/theme/transp'; +@import 'storm';