puzzle WIP
parent
5787a3c4b4
commit
4150555b27
|
@ -96,12 +96,13 @@ final class Puzzle(
|
|||
// case Some(me) => env.puzzle.cursor.nextPuzzleFor(me)
|
||||
}
|
||||
|
||||
def round3(id: String) =
|
||||
def round3(theme: String, id: String) =
|
||||
OpenBody { implicit ctx =>
|
||||
NoBot {
|
||||
implicit val req = ctx.body
|
||||
OptionFuResult(env.puzzle.api.puzzle find Puz.Id(id)) { puzzle =>
|
||||
lila.mon.puzzle.round.attempt(ctx.isAuth).increment()
|
||||
val theme = PuzzleTheme.find(theme)
|
||||
lila.mon.puzzle.round.attempt(ctx.isAuth, theme.fold("any")(_.key.value)).increment()
|
||||
env.puzzle.forms.round
|
||||
.bindFromRequest()
|
||||
.fold(
|
||||
|
@ -209,7 +210,7 @@ final class Puzzle(
|
|||
}
|
||||
|
||||
def byTheme(theme: String) = Open { implicit ctx =>
|
||||
lila.puzzle.PuzzleTheme.byKey.get(PuzzleTheme.Key(theme)) match {
|
||||
PuzzleTheme.find(theme) match {
|
||||
case None => Redirect(routes.Puzzle.home()).fuccess
|
||||
case Some(theme) =>
|
||||
nextPuzzleForMe(theme.key.some) flatMap {
|
||||
|
|
|
@ -89,7 +89,7 @@ GET /training/:theme controllers.Puzzle.byTheme(theme: String)
|
|||
GET /training/$id<\w{5}> controllers.Puzzle.show(id: String)
|
||||
GET /training/$id<\w{5}>/load controllers.Puzzle.load(id: String)
|
||||
POST /training/$id<\w{5}>/vote controllers.Puzzle.vote(id: String)
|
||||
POST /training/$id<\w{5}>/round3 controllers.Puzzle.round3(id: String)
|
||||
POST /training/complete/:theme/$id<\w{5}> controllers.Puzzle.complete(theme: String, id: String)
|
||||
|
||||
# User Analysis
|
||||
GET /analysis/help controllers.UserAnalysis.help
|
||||
|
|
|
@ -28,5 +28,8 @@ object ThreadLocalRandom {
|
|||
for (_ <- 0 until len) sb += nextChar()
|
||||
sb.result()
|
||||
}
|
||||
def oneOf[A](vec: Vector[A]): Option[A] = vec lift nextInt(vec.size)
|
||||
def oneOf[A](vec: Vector[A]): Option[A] =
|
||||
vec.nonEmpty ?? {
|
||||
vec lift nextInt(vec.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -429,7 +429,8 @@ object mon {
|
|||
val solve = counter("puzzle.batch.solve").withoutTags()
|
||||
}
|
||||
object round {
|
||||
def attempt(user: Boolean) = counter("puzzle.attempt.count").withTags(Map("user" -> user))
|
||||
def attempt(user: Boolean, theme: String) =
|
||||
counter("puzzle.attempt.count").withTags(Map("user" -> user, "theme" -> theme))
|
||||
}
|
||||
object vote {
|
||||
val up = counter("puzzle.vote.count").withTag("dir", "up")
|
||||
|
|
|
@ -9,7 +9,8 @@ import lila.base.LilaTimeout
|
|||
final class DuctSequencer(maxSize: Int, timeout: FiniteDuration, name: String, logging: Boolean = true)(
|
||||
implicit
|
||||
system: akka.actor.ActorSystem,
|
||||
ec: ExecutionContext
|
||||
ec: ExecutionContext,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
|
||||
import DuctSequencer._
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package lila.puzzle
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import com.softwaremill.macwire._
|
||||
import io.methvin.play.autoconfig._
|
||||
import play.api.Configuration
|
||||
|
@ -36,7 +35,8 @@ final class Env(
|
|||
mongo: lila.db.Env
|
||||
)(implicit
|
||||
ec: scala.concurrent.ExecutionContext,
|
||||
system: ActorSystem
|
||||
system: akka.actor.ActorSystem,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
|
||||
private val config = appConfig.get[PuzzleConfig]("puzzle")(AutoConfig.loader)
|
||||
|
@ -61,7 +61,7 @@ final class Env(
|
|||
|
||||
lazy val anon: PuzzleAnon = wire[PuzzleAnon]
|
||||
|
||||
lazy val finisher = wire[Finisher]
|
||||
lazy val finisher = wire[PuzzleFinisher]
|
||||
|
||||
lazy val forms = PuzzleForm
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ final class PuzzleAnon(colls: PuzzleColls, cacheApi: CacheApi, pathApi: PuzzlePa
|
|||
import BsonHandlers._
|
||||
|
||||
def getOneFor(theme: Option[PuzzleTheme.Key]): Fu[Option[Puzzle]] =
|
||||
pool get theme pp "pool" map ThreadLocalRandom.oneOf
|
||||
pool get theme map ThreadLocalRandom.oneOf
|
||||
|
||||
private val poolSize = 50
|
||||
|
||||
|
@ -32,17 +32,18 @@ final class PuzzleAnon(colls: PuzzleColls, cacheApi: CacheApi, pathApi: PuzzlePa
|
|||
if (count > 9000) 1200 to 1600
|
||||
else if (count > 5000) 1000 to 1800
|
||||
else 0 to 9999
|
||||
val selector =
|
||||
$doc(
|
||||
"_id" $startsWith s"${theme | PuzzleTheme.anyKey}_${tier}_",
|
||||
"min" $gte ratingRange.min,
|
||||
"max" $lte ratingRange.max
|
||||
)
|
||||
println(count)
|
||||
println(lila.db.BSON.debug(selector))
|
||||
colls.path {
|
||||
_.aggregateList(poolSize) { framework =>
|
||||
import framework._
|
||||
Match(
|
||||
$doc(
|
||||
"tier" -> tier,
|
||||
"theme" -> (theme | PuzzleTheme.anyKey).value,
|
||||
"min" $gte ratingRange.min,
|
||||
"max" $lte ratingRange.max
|
||||
)
|
||||
) -> List(
|
||||
Match(selector) -> List(
|
||||
Sample(1),
|
||||
Project($doc("puzzleId" -> "$ids", "_id" -> false)),
|
||||
Unwind("puzzleId"),
|
||||
|
|
|
@ -64,23 +64,12 @@ final private[puzzle] class PuzzleApi(
|
|||
object theme {
|
||||
|
||||
def sortedWithCount: Fu[List[PuzzleTheme.WithCount]] =
|
||||
colls.path {
|
||||
_.aggregateList(Int.MaxValue) { framework =>
|
||||
import framework._
|
||||
Match($doc("tier" -> "all")) -> List(
|
||||
GroupField("tag")(
|
||||
"count" -> SumField("length")
|
||||
)
|
||||
)
|
||||
}.map { objs =>
|
||||
val byKey = objs.flatMap { obj =>
|
||||
for {
|
||||
key <- obj string "_id" map PuzzleTheme.Key
|
||||
count <- obj int "count"
|
||||
theme <- PuzzleTheme.byKey get key
|
||||
} yield key -> PuzzleTheme.WithCount(theme, count)
|
||||
}.toMap
|
||||
PuzzleTheme.sorted.flatMap(pt => byKey.get(pt.key))
|
||||
pathApi.countsByTheme map { counts =>
|
||||
PuzzleTheme.sorted flatMap { pt =>
|
||||
counts.getOrElse(pt.key, 0) match {
|
||||
case 0 => Nil
|
||||
case count => List(PuzzleTheme.WithCount(pt, count))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package lila.puzzle
|
|||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.util.chaining._
|
||||
|
||||
import lila.db.dsl._
|
||||
import lila.memo.CacheApi
|
||||
|
@ -9,6 +10,7 @@ import lila.rating.{ Perf, PerfType }
|
|||
import lila.user.{ User, UserRepo }
|
||||
|
||||
private case class PuzzleCursor(
|
||||
theme: Option[PuzzleTheme.Key],
|
||||
path: Puzzle.PathId,
|
||||
previousPaths: Set[Puzzle.PathId],
|
||||
positionInPath: Int
|
||||
|
@ -22,15 +24,14 @@ private case class PuzzleCursor(
|
|||
}
|
||||
|
||||
final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: UserRepo)(implicit
|
||||
ec: ExecutionContext
|
||||
ec: ExecutionContext,
|
||||
system: akka.actor.ActorSystem,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
|
||||
import BsonHandlers._
|
||||
import Puzzle.PathId
|
||||
|
||||
private[puzzle] def cursorOf(user: User): Fu[PuzzleCursor] =
|
||||
cursors.get(user.id)
|
||||
|
||||
sealed private trait NextPuzzleResult
|
||||
private object NextPuzzleResult {
|
||||
case object PathMissing extends NextPuzzleResult
|
||||
|
@ -40,26 +41,26 @@ final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: Us
|
|||
case class PuzzleFound(puzzle: Puzzle) extends NextPuzzleResult
|
||||
}
|
||||
|
||||
def nextPuzzleFor(user: User, isRetry: Boolean = false): Fu[Puzzle] =
|
||||
cursorOf(user) flatMap { cursor =>
|
||||
def nextPuzzleFor(user: User, theme: Option[PuzzleTheme.Key], isRetry: Boolean = false): Fu[Puzzle] =
|
||||
continueOrCreateCursorFor(user, theme) flatMap { cursor =>
|
||||
import NextPuzzleResult._
|
||||
nextPuzzleResult(user, cursor.pp) flatMap {
|
||||
case PathMissing | PathEnded if !isRetry =>
|
||||
nextPathIdFor(user.id, cursor.previousPaths) flatMap {
|
||||
nextPathIdFor(user.id, theme, cursor.previousPaths) flatMap {
|
||||
case None => fufail(s"No remaining puzzle path for ${user.id}")
|
||||
case Some(pathId) =>
|
||||
val newCursor = cursor switchTo pathId
|
||||
cursors.put(user.id, fuccess(newCursor))
|
||||
nextPuzzleFor(user, isRetry = true)
|
||||
nextPuzzleFor(user, theme, isRetry = true)
|
||||
}
|
||||
case PathMissing | PathEnded => fufail(s"Puzzle patth missing or ended for ${user.id}")
|
||||
case PuzzleMissing(id) =>
|
||||
logger.warn(s"Puzzle missing: $id")
|
||||
cursors.put(user.id, fuccess(cursor.next))
|
||||
nextPuzzleFor(user, isRetry = isRetry)
|
||||
nextPuzzleFor(user, theme, isRetry = isRetry)
|
||||
case PuzzleAlreadyPlayed(_) =>
|
||||
cursors.put(user.id, fuccess(cursor.next))
|
||||
nextPuzzleFor(user, isRetry = isRetry)
|
||||
nextPuzzleFor(user, theme, isRetry = isRetry)
|
||||
case PuzzleFound(puzzle) => fuccess(puzzle)
|
||||
}
|
||||
}
|
||||
|
@ -126,16 +127,32 @@ final class PuzzleCursorApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: Us
|
|||
}
|
||||
}
|
||||
|
||||
private val cursors = cacheApi[User.ID, PuzzleCursor](32768, "puzzle.cursor")(
|
||||
_.expireAfterWrite(1 hour)
|
||||
.buildAsyncFuture { userId =>
|
||||
nextPathIdFor(userId, Set.empty)
|
||||
.orFail(s"No puzzle path found for $userId")
|
||||
.dmap(pathId => PuzzleCursor(pathId, Set.empty, 0))
|
||||
}
|
||||
private val cursors = cacheApi.notLoading[User.ID, PuzzleCursor](32768, "puzzle.cursor")(
|
||||
_.expireAfterWrite(1 hour).buildAsync()
|
||||
)
|
||||
|
||||
private def nextPathIdFor(userId: User.ID, previousPaths: Set[PathId]): Fu[Option[PathId]] =
|
||||
private[puzzle] def currentCursorOf(user: User, theme: Option[PuzzleTheme.Key]): Fu[PuzzleCursor] =
|
||||
cursors.getFuture(user.id, _ => createCursorFor(user, theme))
|
||||
|
||||
private[puzzle] def continueOrCreateCursorFor(
|
||||
user: User,
|
||||
theme: Option[PuzzleTheme.Key]
|
||||
): Fu[PuzzleCursor] =
|
||||
currentCursorOf(user, theme) flatMap { current =>
|
||||
if (current.theme == theme) fuccess(current)
|
||||
else createCursorFor(user, theme) tap { cursors.put(user.id, _) }
|
||||
}
|
||||
|
||||
private def createCursorFor(user: User, theme: Option[PuzzleTheme.Key]): Fu[PuzzleCursor] =
|
||||
nextPathIdFor(user.id, theme, Set.empty)
|
||||
.orFail(s"No puzzle path found for ${user.id}, theme: $theme")
|
||||
.dmap(pathId => PuzzleCursor(theme, pathId, Set.empty, 0))
|
||||
|
||||
private def nextPathIdFor(
|
||||
userId: User.ID,
|
||||
theme: Option[PuzzleTheme.Key],
|
||||
previousPaths: Set[PathId]
|
||||
): Fu[Option[PathId]] =
|
||||
userRepo.perfOf(userId, PerfType.Puzzle).dmap(_ | Perf.default) flatMap { perf =>
|
||||
colls.path {
|
||||
_.aggregateOne() { framework =>
|
||||
|
|
|
@ -11,7 +11,7 @@ import lila.rating.{ Glicko, PerfType }
|
|||
import lila.user.{ User, UserRepo }
|
||||
import lila.rating.Perf
|
||||
|
||||
final private[puzzle] class Finisher(
|
||||
final private[puzzle] class PuzzleFinisher(
|
||||
api: PuzzleApi,
|
||||
userRepo: UserRepo,
|
||||
historyApi: lila.history.HistoryApi,
|
|
@ -18,8 +18,11 @@ final private class PuzzlePathApi(
|
|||
cacheApi: CacheApi
|
||||
)(implicit ec: ExecutionContext) {
|
||||
|
||||
def countsByTheme: Fu[Map[PuzzleTheme.Key, Int]] =
|
||||
countByThemeCache get {}
|
||||
|
||||
def countPuzzlesByTheme(theme: PuzzleTheme.Key): Fu[Int] =
|
||||
countByThemeCache get {} dmap { _.getOrElse(theme, 0) }
|
||||
countsByTheme dmap { _.getOrElse(theme, 0) }
|
||||
|
||||
private val countByThemeCache =
|
||||
cacheApi.unit[Map[PuzzleTheme.Key, Int]] {
|
||||
|
@ -29,7 +32,7 @@ final private class PuzzlePathApi(
|
|||
_.aggregateList(Int.MaxValue) { framework =>
|
||||
import framework._
|
||||
Match($doc("tier" -> "all")) -> List(
|
||||
GroupField("tag")(
|
||||
GroupField("theme")(
|
||||
"count" -> SumField("length")
|
||||
)
|
||||
)
|
||||
|
|
|
@ -53,4 +53,6 @@ object PuzzleTheme {
|
|||
val byKey: Map[Key, PuzzleTheme] = sorted.view.map { t =>
|
||||
t.key -> t
|
||||
}.toMap
|
||||
|
||||
def find(key: String) = byKey get Key(key)
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ export interface KeyboardController {
|
|||
playBestMove(): void;
|
||||
}
|
||||
|
||||
export type ThemeKey = string;
|
||||
|
||||
export interface Controller extends KeyboardController {
|
||||
nextNodeBest(): string | undefined;
|
||||
disableThreatMode?: Prop<boolean>;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import * as xhr from 'common/xhr';
|
||||
import { PuzzleData, PuzzleResult } from './interfaces';
|
||||
import { PuzzleData, PuzzleResult, ThemeKey } from './interfaces';
|
||||
import {defined} from 'common';
|
||||
|
||||
export function round(puzzleId: string, win: boolean): Promise<PuzzleResult | undefined> {
|
||||
return xhr.json(`/training/${puzzleId}/round3`, {
|
||||
export function complete(puzzleId: string, theme: ThemeKey, win: boolean): Promise<PuzzleResult | undefined> {
|
||||
return xhr.json(`/training/complete/${theme}/${puzzleId}`, {
|
||||
method: 'POST',
|
||||
body: xhr.form({ win: win ? 1 : 0 })
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue