puzzle WIP

pull/7680/head
Thibault Duplessis 2020-11-27 16:24:53 +01:00
parent 5787a3c4b4
commit 4150555b27
14 changed files with 80 additions and 60 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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")

View File

@ -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._

View File

@ -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

View File

@ -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"),

View File

@ -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))
}
}
}
}

View File

@ -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 =>

View File

@ -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,

View File

@ -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")
)
)

View File

@ -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)
}

View File

@ -22,6 +22,8 @@ export interface KeyboardController {
playBestMove(): void;
}
export type ThemeKey = string;
export interface Controller extends KeyboardController {
nextNodeBest(): string | undefined;
disableThreatMode?: Prop<boolean>;

View File

@ -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 })
});