puzzle storm WIP
parent
ddf0a0a43a
commit
9e39278344
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1970,4 +1970,8 @@ val `declineStandard` = new I18nKey("challenge:declineStandard")
|
|||
val `declineVariant` = new I18nKey("challenge:declineVariant")
|
||||
}
|
||||
|
||||
object storm {
|
||||
val `moveToStart` = new I18nKey("storm:moveToStart")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)(!_)
|
||||
}
|
|
@ -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 {}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package lila
|
||||
|
||||
import lila.user.User
|
||||
|
||||
package object storm extends PackageObject {
|
||||
|
||||
private[storm] val logger = lila.log("swiss")
|
||||
}
|
|
@ -55,6 +55,7 @@
|
|||
"ui/tree",
|
||||
"ui/msg",
|
||||
"ui/dgt",
|
||||
"ui/storm",
|
||||
"ui/@build/rollupProject",
|
||||
"ui/@types/lichess",
|
||||
"ui/@types/cash"
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="moveToStart">Move to start</string>
|
||||
</resources>
|
2
ui/build
2
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"
|
||||
|
|
|
@ -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 {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
$mq-col2-squeeze: $mq-col2-uniboard-squeeze;
|
||||
$mq-col2: $mq-col2-uniboard;
|
||||
$mq-col3: $mq-col3-uniboard;
|
||||
|
||||
@import "layout";
|
|
@ -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";
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/dark';
|
||||
@import 'storm';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/light';
|
||||
@import 'storm';
|
|
@ -0,0 +1,2 @@
|
|||
@import '../../../common/css/theme/transp';
|
||||
@import 'storm';
|
Loading…
Reference in New Issue