Skip to content
2024Work in progress

Quest Log

A habit tracker built as a text adventure. Quests, XP, stats, and a deliberately punishing stat decay that made me actually do the things.

typescriptreactsqlitedesign

Most habit trackers are spreadsheets with extra guilt. I wanted one that felt like playing a game, so I built one that borrows its mechanics honestly — including the parts that are annoying.

The premise

Habits are quests. Completing one grants XP. XP raises stats. Stats gate the quests you're allowed to take.

The deliberate cruelty: stats decay every day you don't act. Your Strength doesn't sit there politely at 40. It's 39 tomorrow. 37 by the weekend. This is copied directly from RPG mechanics, and it turns out to be the single most effective feature. The decay is the actual product; the quests are just the interface.

Stack

  • React + TypeScript — nothing exotic
  • SQLite via a thin typed layer. I resisted an ORM and wrote about 200 lines of query helpers instead, which was the right call for something this small
  • Local-first. The whole database is one file. No account, no sync, no telemetry
  • Markdown quest files on disk, so a quest is just a file with frontmatter

The interesting technical bit

Quest definitions are data, but quests have prerequisites and rewards, which makes them a graph. I needed transitive prerequisite resolution without pulling in a graph library, and the whole thing is a fixed-point iteration:

ts
function isUnlocked(quest: Quest, completed: Set<string>): boolean {
  const seen = new Set<string>()
  let frontier = quest.requires ?? []
 
  while (frontier.length > 0) {
    const next: string[] = []
    for (const id of frontier) {
      if (seen.has(id)) continue
      seen.add(id)
      if (!completed.has(id)) return false // unresolved requirement
      const dep = questIndex.get(id)
      if (dep?.requires) next.push(...dep.requires)
    }
    frontier = next
  }
  return true
}

BFS over requirement edges, memoised per quest per session. Forty quests, sub-millisecond.

Status

Still in progress, and honestly still fun, which is the real test. The parts left to build are a stats page with actual charts and an export button that produces a shareable image.

What I learned

If your productivity tool doesn't have a win condition, it's a chore list. The entire difference between this app and a plain checklist is that it tells you, in specific terms, what you gained and what you're about to lose.

Games solved motivation decades ago. We just stopped taking notes.

HAVE A QUESTION?

Open an issue on the repository, or just email me. Both work.

Email Razi

MORE PROJECTS

Live

This website — a statically exported Next.js app with a hand-built 8-bit design system, MDX content, and a CRT phosphor you can switch.

nextjstypescripttailwinddesign
LiveSource
Live

A zero-dependency WebAudio library that plays NES-style music from a compact note syntax. Ships about 4kb and no assets.

typescriptaudioweb-audioopen-source
LiveSource
Live

A browser tool that converts any image into 1-bit, 4-colour and 8-colour pixel art with real dithering algorithms. Fully client-side.

typescriptcanvastoolingwebgl
LiveSource

Command palette

Search for a command to run