Skip to content

Gravity streaks: snap-merge adjacent streaks based on mass and gap #532

@mircealungu

Description

@mircealungu

The idea

Streaks today work in Zeeguu the way they do everywhere: practice every day and the number goes up; miss a single day and it resets to zero. That's a powerful motivator when your streak is short, but brutal once it gets long — a 200-day streak that breaks because you caught the flu feels devastating, and most people who lose a long streak just quit instead of starting over from one.

What if streaks behaved like planets in space? Two big enough streaks that sit close enough together would pull each other in and merge into a single streak. A 200-day streak will automatically pull together a fresh 10-day streak over a two day break. They'd snap together with a satisfying pull could be nicely visualized, and you'd be back at "210 days, with one day off." The longer your streaks, the more forgiving the universe is about a gap between them.

The rule is the same one gravity uses: pull strength depends on how big each streak is and how close they sit, and it falls off fast as the gap grows. Tiny streaks can't reach across anything. Big streaks can bridge a few days. Two enormous streaks separated by two weeks still won't merge — the universe rewards investment, not infinite slack. People intuit gravity already, so there's no rule to memorize: bigger pulls harder, far doesn't pull at all.

The killer moment is the merge itself. A user finishes their first session after a gap, opens the streak page, and watches their old streak visibly drift across the empty days and snap into the new one:

Your 47-day streak from February pulled your new 8-day streak in.
Combined: 55 days.

That's a screenshot moment. The explanation page can be a little physics playground — two draggable streak-blobs and a slider for the gap — so people learn the rule by playing with it instead of reading docs.

The mechanic does the thing streaks need to do (small streaks still die when you miss a day, so the daily pull on new users is as urgent as ever) while quietly fixing the thing that makes long streaks brittle: one bad week erasing a year of practice. It feels generous without feeling cheap, because the forgiveness is earned — you only get it after you've already invested.

The formula

For two adjacent streaks of length `L₁` and `L₂` separated by a gap of `G` days:

```
pull = (L₁ × L₂) / G²
```

If `pull > THRESHOLD`, they snap together.

Examples (with `THRESHOLD = 50`):

Streak A Gap Streak B Pull Snaps?
30 1 30 900 yes
30 7 30 18 no
100 14 100 51 yes
5 1 5 25 no
5 1 100 500 yes (rescues a long streak)
5 10 100 5 no

Honest counting

The combined number is `L₁ + L₂` (the gap days don't count as practiced — that would be a lie). The visual is one continuous block with a faint gap-shadow showing the days the gravity bridged. Honest data, magical UX.

Schema implication

The current `user_language.daily_streak` counter only models the current streak — past streaks are lost. To support this we need streaks as intervals:

```sql
CREATE TABLE streak_interval (
id INT PRIMARY KEY,
user_language_id INT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
merged_into_id INT NULL -- snap pointer, NULL until pulled
);
```

Backfill is one row per existing user: `(last_practiced - daily_streak + 1, last_practiced)`. The `daily_streak` column becomes derived.

This also unlocks, more or less for free:

  • A GitHub-style contribution heatmap
  • "Longest streak ever" leaderboards
  • Historical streak browsing
  • Honest recompute if someone restores from backup

Open questions (want feedback!)

  1. Threshold tuning. 50 is a guess. Should it scale with the user's overall practice volume so power users don't trivially snap everything?
  2. Cascading snaps. If A snaps to B and later B snaps to C, does A come along? Probably yes (transitive feels right), but it changes the feel.
  3. Snap permanence. Once a snap fires, does it lock in, or can it un-snap if the second streak later breaks before reaching some stability?
  4. Per-language vs cross-language. Probably per-language, but interesting to consider whether a long French streak could lend gravity to a starting-up Spanish one.
  5. Anti-gaming. Could someone exploit this by deliberately taking strategic breaks? Probably not — the math punishes long gaps hard — but worth modelling.

Prior art

Closest thing in the wild is Duolingo's Streak Freeze (one-day passive grace) and Streak Repair (paid restore). Neither requires earning the recovery. The "snap two streaks together" mechanic appears to be novel — couldn't find anyone shipping it. Duolingo has actually A/B-tested stronger forgiveness and seen engagement drop, which is part of why this approach is interesting: it doesn't soften the daily pull at all, it only rewards long-term investment.

🤖 Drafted with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions