refine home options and trivia pacing

This commit is contained in:
pj committed 2026-09-10 03:44:59 +05:30
1 parent c6b2eb3cce
commit 598c5f9ef3
30 files changed
+493 -187

No files matched your search

@@ -28,6 +28,7 @@ interface ProgressWriter {
attempt: Attempt,
updatedMastery: List<Mastery>,
activeRunSnapshot: ActiveRunSnapshot,
streak: Streak,
)
suspend fun recordCompletedRun(
@@ -36,7 +37,7 @@ interface ProgressWriter {
newlyEarnedStamps: List<PassportStamp>,
)
suspend fun recordAbandonedRun(runId: String)
suspend fun recordAbandonedRun(runId: String, streak: Streak? = null)
suspend fun resetProgress()
}
@@ -76,11 +76,11 @@ object MasteryRules {
}
object StreakRules {
fun recordCompletedRun(current: Streak, completedEpochDay: Int): Streak {
val lastDay = current.lastCompletedEpochDay
if (lastDay != null && completedEpochDay <= lastDay) return current
fun recordActivity(current: Streak, activityEpochDay: Int): Streak {
val lastDay = current.lastActivityEpochDay
if (lastDay != null && activityEpochDay <= lastDay) return current
val currentDays = if (lastDay != null && completedEpochDay == lastDay + 1) {
val currentDays = if (lastDay != null && activityEpochDay == lastDay + 1) {
current.currentDays + 1
} else {
1
@@ -88,9 +88,12 @@ object StreakRules {
return Streak(
currentDays = currentDays,
bestDays = maxOf(current.bestDays, currentDays),
lastCompletedEpochDay = completedEpochDay,
lastCompletedEpochDay = activityEpochDay,
)
}
fun recordCompletedRun(current: Streak, completedEpochDay: Int): Streak =
recordActivity(current, completedEpochDay)
}
data class PassportEvidence(
@@ -109,7 +109,8 @@ class QuizEngine(
}
val completionPending = state.runCompletionPending ||
state.sessionExpired ||
(state.configuration.duration != QuizDuration.Unlimited && state.sessionRemainingMillis == 0L)
(state.configuration.duration != QuizDuration.Unlimited && state.sessionRemainingMillis == 0L) ||
state.hasReachedAnsweredQuestionLimit()
if (state.phase == RunPhase.Paused) {
require(state.pausedFromPhase in PAUSABLE_PHASES) { "A paused snapshot has no resumable phase" }
return state.copy(
@@ -161,13 +162,17 @@ class QuizEngine(
hintTypes = timed.activeQuestion.usedHintTypes,
)
val correctStreak = if (evaluation.correct) timed.currentCorrectStreak + 1 else 0
val attempts = timed.attempts + attempt
val answeredQuestionLimitReached = timed.configuration.duration.answeredQuestionLimit
?.let { attempts.size >= it }
?: false
return timed.copy(
phase = RunPhase.Feedback,
score = timed.score + points,
answeredQuestionIds = timed.answeredQuestionIds + timed.activeQuestion.question.id,
recentQuestionIds = (timed.recentQuestionIds + timed.activeQuestion.question.id).takeLast(10),
answeredCountryIds = (timed.answeredCountryIds + timed.activeQuestion.question.countryIds).takeLast(10),
attempts = timed.attempts + attempt,
attempts = attempts,
feedback = AnswerFeedback(
kind = when {
!evaluation.correct -> FeedbackKind.Incorrect
@@ -182,6 +187,7 @@ class QuizEngine(
adaptive = AdaptiveDifficulty.afterAnswer(timed.adaptive, evaluation.correct, hinted),
currentCorrectStreak = correctStreak,
bestCorrectStreak = maxOf(timed.bestCorrectStreak, correctStreak),
runCompletionPending = timed.runCompletionPending || answeredQuestionLimitReached,
tickingSinceMillis = null,
)
}
@@ -592,6 +598,9 @@ class QuizEngine(
private fun ActiveQuestion.revealedTileIds(): Set<String> =
appliedHints.mapNotNullTo(linkedSetOf()) { it.revealedTileId }
private fun QuizRunState.hasReachedAnsweredQuestionLimit(): Boolean =
configuration.duration.answeredQuestionLimit?.let { attempts.size >= it } == true
private fun correctAnswer(question: Question): String = when (question) {
is Question.MultipleChoice -> question.choices.first { it.id == question.correctChoiceId }.label
is Question.LetterBank -> question.displayAnswer
@@ -667,8 +676,8 @@ class QuizEngine(
private data class AnswerEvaluation(val correct: Boolean, val submittedText: String)
companion object {
const val MULTIPLE_CHOICE_WINDOW_MILLIS = 10_000L
const val LETTER_BANK_WINDOW_MILLIS = 20_000L
const val MULTIPLE_CHOICE_WINDOW_MILLIS = 30_000L
const val LETTER_BANK_WINDOW_MILLIS = 30_000L
const val LETTER_BANK_DECOY_COUNT = 3
const val MINIMUM_UNLIMITED_ANSWERS = 5
const val ACTIVE_RUN_SNAPSHOT_SCHEMA_VERSION = 1
@@ -9,11 +9,14 @@ data class QuizConfiguration(
)
@Serializable
enum class QuizDuration(val activeDurationMillis: Long?) {
FiveMinutes(5 * 60 * 1_000L),
TenMinutes(10 * 60 * 1_000L),
TwentyMinutes(20 * 60 * 1_000L),
Unlimited(null),
enum class QuizDuration(
val activeDurationMillis: Long?,
val answeredQuestionLimit: Int?,
) {
FiveMinutes(5 * 60 * 1_000L, 10),
TenMinutes(10 * 60 * 1_000L, 20),
TwentyMinutes(20 * 60 * 1_000L, 40),
Unlimited(null, null),
}
@Serializable
@@ -60,7 +60,10 @@ data class Streak(
val currentDays: Int = 0,
val bestDays: Int = 0,
val lastCompletedEpochDay: Int? = null,
)
) {
val lastActivityEpochDay: Int?
get() = lastCompletedEpochDay
}
@Serializable
enum class PassportStampKind {