-
Notifications
You must be signed in to change notification settings - Fork 0
Failure Modes
Every deck has a FailurePolicy that decides what happens when you answer the gate question wrong. The policy's mode (a FailureMode) picks the strategy; FailurePolicyEngine.evaluate() turns a wrong answer + attempt count into a GateResult that the gate UI renders.
Answer wrong, try again — indefinitely. No attempt cap, no penalty, no lockout.
- Result:
FailRetry(attemptsUsed, Int.MAX_VALUE)every time. - UI:
gate_state_wrong— shows the wrong-answer state and lets you retry immediately. - Use case: lowest-friction mode; good for casual review decks. This is the
Easyonboarding preset.
Gives you a limited number of tries, then lets you through anyway (with a shame message) rather than locking you out.
- While
attemptsUsed < maxAttempts:FailRetry(attemptsUsed, maxAttempts)— same wrong-answer/retry UI, now showing an attempts-remaining counter. - Once attempts are exhausted:
FailBypass(bypassMessage)— the gate lets you through, displayingbypassMessage(default: "Better luck next time 👀") viagate_state_bypass. - Use case: keeps the gate from becoming a hard blocker if you genuinely don't know an answer. This is the
Normalonboarding preset (maxAttempts = 3,penaltySeconds = 15— notepenaltySecondsis unused in this mode).
Every wrong answer costs you a fixed cooldown, no matter how many attempts you've made.
- Result:
FailPenalty(penaltySeconds)on every failure — there's no attempt counting. - UI:
gate_state_penalty— presumably shows a countdown ofpenaltySecondsbefore you can try again. - Use case: discourages guessing/spamming without ever letting you bypass entirely. This is the
Hardonboarding preset (penaltySeconds = 60, hints disabled).
The strictest mode: limited attempts, then a real lockout — no bypass.
- While
attemptsUsed < maxAttempts:FailRetry(attemptsUsed, maxAttempts). - Once attempts are exhausted:
FailLockout(lockedUntilEpoch), wherelockedUntilEpoch = now + lockoutMinutes * 60_000. Rendered viagate_state_lockout. - There is no bypass path — you're locked out of the home screen until the lockout timer expires.
- Use case: the "no escape" option for decks you actually want to force mastery of.
| Field | Used by | Meaning |
|---|---|---|
mode |
all | Which of the four modes above applies. |
maxAttempts |
MAX_ATTEMPTS, HARD_LOCK
|
Number of wrong answers allowed before bypass/lockout triggers. Ignored by RETRY and TIME_PENALTY. |
penaltySeconds |
TIME_PENALTY |
Cooldown length (seconds) after each wrong answer. |
lockoutMinutes |
HARD_LOCK |
Lockout length (minutes) once maxAttempts is exhausted. |
bypassMessage |
MAX_ATTEMPTS |
Message shown when the user is let through after exhausting attempts. |
hintAllowed |
all (via GateController.hintAllowed()) |
Whether the gate screen offers a hint for the current question. |
| Preset | mode | maxAttempts | penaltySeconds | hintAllowed |
|---|---|---|---|---|
Easy |
RETRY |
— | 30 (default, unused) | true |
Normal |
MAX_ATTEMPTS |
3 | 15 (unused) | true (default) |
Hard |
TIME_PENALTY |
— (default) | 60 | false |
Custom |
FailurePolicy() defaults |
— | — | — (starting point for user-defined policies) |
Independent of mode, a correct answer always resolves to GateResult.Pass and advances the question's mastery streak (consecutiveCorrect): 2+ in a row → "learning" (level 1), 5+ in a row → "mastered" (level 2). Any wrong answer resets consecutiveCorrect to 0 and caps mastery at "learning" (level 1) if it was higher.
Attempt counting (attemptsUsed) is per gate session — it resets to 0 each time GateController.loadQuestion() picks a new question, not per-app-launch. A NoQuestion result (no active deck, or the deck is empty) is a separate case handled upstream in shouldShowGate(), not part of the failure-mode logic.