try harder to select a puzzle in any situation

pull/7680/head
Thibault Duplessis 2020-12-05 19:09:27 +01:00
parent b1cdca65d3
commit 664bc6beba
8 changed files with 54 additions and 67 deletions

View File

@ -14,26 +14,7 @@
const playColl = db.puzzle2_puzzle;
const roundColl = db.puzzle2_round;
const staticThemes = new Set([
'bishopEndgame',
'enPassant',
'endgame',
'knightEndgame',
'long',
'mateIn1',
'mateIn2',
'mateIn3',
'mateIn4',
'mateIn5',
'middlegame',
'oneMove',
'opening',
'pawnEndgame',
'queenEndgame',
'rookEndgame',
'short',
'veryLong'
]);
const phases = new Set([ 'opening', 'middlegame', 'endgame' ]);
playColl.find({ dirty: true }, { themes: true }).forEach(p => {
@ -51,7 +32,7 @@ playColl.find({ dirty: true }, { themes: true }).forEach(p => {
themeMap[theme] = x.v * signum + (themeMap[theme] || 0);
});
const newThemes = new Set(oldThemes.filter(t => staticThemes.has(t)));
const newThemes = new Set(oldThemes.filter(t => phases.has(t)));
Object.keys(themeMap).forEach(theme => {
if (themeMap[theme] > 0) newThemes.add(theme);
});
@ -61,9 +42,7 @@ playColl.find({ dirty: true }, { themes: true }).forEach(p => {
oldThemes.length !== newThemes.size ||
oldThemes.find(t => !newThemes.has(t))
) {
const arr = Array.from(newThemes);
// print(`Update ${p._id} themes: ${oldThemes.join(', ')} -> ${arr.join(', ')}`);
update['$set'] = {themes:arr};
update['$set'] = {themes: Array.from(newThemes)};
}
playColl.update({_id:p._id},update);
});

View File

@ -108,7 +108,6 @@ function makeTier(theme, tierName, thresholdRatio) {
const docs = [];
themes.concat([null]).forEach(theme =>
// ['exposedKing'].forEach(theme =>
tiers.forEach(([name, threshold]) => makeTier(theme, name, threshold).forEach(p => docs.push(p)))
);

View File

@ -81,7 +81,7 @@ private[puzzle] object BsonHandlers {
)
}
implicit val PathIdBSONHandler: BSONHandler[Puzzle.PathId] = stringIsoHandler(Puzzle.pathIdIso)
implicit val PathIdBSONHandler: BSONHandler[PuzzlePath.Id] = stringIsoHandler(PuzzlePath.pathIdIso)
implicit val ThemeKeyBSONHandler: BSONHandler[PuzzleTheme.Key] = stringIsoHandler(PuzzleTheme.keyIso)
}

View File

@ -36,8 +36,6 @@ object Puzzle {
case class Id(value: String) extends AnyVal with StringValue
case class PathId(value: String) extends AnyVal with StringValue
case class UserResult(
puzzleId: Id,
userId: lila.user.User.ID,
@ -58,6 +56,5 @@ object Puzzle {
val dirty = "dirty" // themes need to be denormalized
}
implicit val idIso = lila.common.Iso.string[Id](Id.apply, _.value)
implicit val pathIdIso = lila.common.Iso.string[PathId](PathId.apply, _.value)
implicit val idIso = lila.common.Iso.string[Id](Id.apply, _.value)
}

View File

@ -25,9 +25,7 @@ final class PuzzleAnon(colls: PuzzleColls, cacheApi: CacheApi, pathApi: PuzzlePa
_.refreshAfterWrite(2 minutes)
.buildAsyncFuture { theme =>
pathApi countPuzzlesByTheme theme flatMap { count =>
val tier =
if (count > 3000) PuzzlePath.tier.top
else PuzzlePath.tier.all
val tier = if (count > 3000) PuzzleTier.Top else PuzzleTier.All
val ratingRange: Range =
if (count > 9000) 1200 to 1600
else if (count > 5000) 1000 to 1800

View File

@ -7,10 +7,17 @@ import lila.db.dsl._
import lila.memo.CacheApi
private object PuzzlePath {
object tier {
val top = "top"
val all = "all"
case class Id(value: String) {
val parts = value split '_'
def tier = PuzzleTier.from(~parts.lift(1))
def theme = PuzzleTheme.findOrAny(~parts.headOption).key
}
implicit val pathIdIso = lila.common.Iso.string[Id](Id.apply, _.value)
}
final private class PuzzlePathApi(

View File

@ -11,15 +11,12 @@ import lila.rating.{ Perf, PerfType }
import lila.user.{ User, UserRepo }
private case class PuzzleSession(
theme: PuzzleTheme.Key,
tier: PuzzleTier,
path: Puzzle.PathId,
path: PuzzlePath.Id,
positionInPath: Int,
previousPaths: Set[Puzzle.PathId] = Set.empty,
previousPaths: Set[PuzzlePath.Id] = Set.empty,
previousVotes: List[Boolean] = List.empty // most recent first
) {
def switchTo(tier: PuzzleTier, pathId: Puzzle.PathId) = copy(
tier = tier,
def switchTo(pathId: PuzzlePath.Id) = copy(
path = pathId,
previousPaths = previousPaths + pathId,
positionInPath = 0
@ -36,7 +33,6 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
) {
import BsonHandlers._
import Puzzle.PathId
sealed private trait NextPuzzleResult
private object NextPuzzleResult {
@ -51,16 +47,15 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
continueOrCreateSessionFor(user, theme) flatMap { session =>
import NextPuzzleResult._
def switchPath(tier: PuzzleTier) =
nextPathIdFor(user, theme, tier, session.previousPaths) orElse {
session.previousPaths.nonEmpty ?? nextPathIdFor(user, theme, tier, Set.empty)
} orFail s"No puzzle path for ${user.id} $theme $tier" flatMap { pathId =>
val newSession = session.switchTo(tier, pathId)
sessions.put(user.id, fuccess(newSession))
nextPuzzleFor(user, theme, retries = retries + 1)
}
nextPathIdFor(user, theme, tier, session.previousPaths) orFail
s"No puzzle path for ${user.id} $theme $tier" flatMap { pathId =>
val newSession = session.switchTo(pathId)
sessions.put(user.id, fuccess(newSession))
nextPuzzleFor(user, theme, retries = retries + 1)
}
nextPuzzleResult(user, session) flatMap {
case PathMissing | PathEnded if retries < 10 => switchPath(session.tier)
case PathMissing | PathEnded if retries < 10 => switchPath(session.path.tier)
case PathMissing | PathEnded => fufail(s"Puzzle path missing or ended for ${user.id}")
case PuzzleMissing(id) =>
logger.warn(s"Puzzle missing: $id")
@ -69,9 +64,9 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
case PuzzleAlreadyPlayed(_) if retries < 3 =>
sessions.put(user.id, fuccess(session.next))
nextPuzzleFor(user, theme, retries = retries + 1)
case PuzzleAlreadyPlayed(_) if session.tier == PuzzleTier.Top => switchPath(PuzzleTier.All)
case PuzzleAlreadyPlayed(puzzle) => fuccess(puzzle)
case PuzzleFound(puzzle) => fuccess(puzzle)
case PuzzleAlreadyPlayed(_) if session.path.tier == PuzzleTier.Top => switchPath(PuzzleTier.All)
case PuzzleAlreadyPlayed(puzzle) => fuccess(puzzle)
case PuzzleFound(puzzle) => fuccess(puzzle)
}
}
@ -132,7 +127,7 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
_ map { session =>
// yes, even if the completed puzzle was not the current session puzzle
// in that case we just skip a puzzle on the path, which doesn't matter
if (session.theme == theme)
if (session.path.theme == theme)
sessions.put(round.userId, fuccess(session.next))
}
}
@ -141,46 +136,56 @@ final class PuzzleSessionApi(colls: PuzzleColls, cacheApi: CacheApi, userRepo: U
_.expireAfterWrite(1 hour).buildAsync()
)
private[puzzle] def currentSessionOf(user: User, theme: PuzzleTheme.Key): Fu[PuzzleSession] =
sessions.getFuture(user.id, _ => createSessionFor(user, theme))
private[puzzle] def continueOrCreateSessionFor(
user: User,
theme: PuzzleTheme.Key
): Fu[PuzzleSession] =
currentSessionOf(user, theme) flatMap { current =>
if (current.theme == theme) fuccess(current)
sessions.getFuture(user.id, _ => createSessionFor(user, theme)) flatMap { current =>
if (current.path.theme == theme) fuccess(current)
else createSessionFor(user, theme) tap { sessions.put(user.id, _) }
}
private def createSessionFor(user: User, theme: PuzzleTheme.Key): Fu[PuzzleSession] =
nextPathIdFor(user, theme, PuzzleTier.Top, Set.empty)
.orFail(s"No puzzle path found for ${user.id}, theme: $theme")
.dmap(pathId => PuzzleSession(theme, PuzzleTier.Top, pathId, 0))
.dmap(pathId => PuzzleSession(pathId, 0))
private def nextPathIdFor(
user: User,
theme: PuzzleTheme.Key,
tier: PuzzleTier,
previousPaths: Set[PathId]
): Fu[Option[PathId]] =
previousPaths: Set[PuzzlePath.Id],
compromise: Int = 0
): Fu[Option[PuzzlePath.Id]] =
colls.path {
_.aggregateOne() { framework =>
import framework._
val rating = user.perfs.puzzle.glicko.rating
val ratingDelta = compromise match {
case 0 => 0
case 1 => 300
case 2 => 800
case _ => 2000
}
Match(
$doc(
"_id" ->
$doc(
"$regex" -> BSONRegex(s"^${theme}_${tier}", ""),
$nin(previousPaths)
if (compromise < 4) $nin(previousPaths) else $empty
),
"min" $lte user.perfs.puzzle.glicko.rating,
"max" $gt user.perfs.puzzle.glicko.rating
"min" $lte (rating + ratingDelta),
"max" $gt (rating - ratingDelta)
)
) -> List(
Project($id(true)),
Sample(1)
)
}.dmap(_.flatMap(_.getAsOpt[PathId]("_id")))
}.dmap(_.flatMap(_.getAsOpt[PuzzlePath.Id]("_id")))
} flatMap {
case Some(path) => fuccess(path.some)
case _ if tier == PuzzleTier.Top => nextPathIdFor(user, theme, PuzzleTier.All, previousPaths)
case _ if compromise < 4 => nextPathIdFor(user, theme, tier, previousPaths, compromise + 1)
case _ => fuccess(none)
}
}

View File

@ -9,4 +9,6 @@ private object PuzzleTier {
case object Top extends PuzzleTier("top")
case object All extends PuzzleTier("all")
def from(tier: String) = if (tier == Top.key) Top else All
}