Turn your notes into a roguelike. Drop in a markdown / PDF / text file, an LLM extracts atomic flashcards, and you "run" the deck — wrong answers cost HP, streaks earn buffs (heal, hint, shield, focus), the run ends when you die or clear the deck. Mastery decays between runs so it stays useful long-term.
Live: https://roguelearn.vercel.app
Two services that run independently:
- Backend (
app/) — FastAPI JSON API. SQLite via SQLModel in dev; works with Postgres unchanged in prod.argon2idpassword hashing, DB-backed sessions, per-session CSRF tokens (X-CSRF-Tokenheader),HttpOnlycookies (SameSite=Laxin dev,SameSite=None; SecureinENV=prod). Google Gemini (gemini-2.5-flash) for card extraction and free-response grading. - Frontend (
web/) — Vite + React 19 + TypeScript SPA. React Router 7 for routing, TanStack Query for server state, Tailwind v4 for styling. Hand-drawn paper aesthetic (Kalam + Patrick Hand).
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# 1. Paste your Gemini key from https://aistudio.google.com/apikey
# 2. Generate a SECRET_KEY:
python -c "import secrets; print(secrets.token_urlsafe(48))"
# 3. Paste it as SECRET_KEY in .envSchema upgrade note: if you ran an earlier (pre-auth) version,
rm data/roguelearn.dbbefore first run — the schema addeduser_idcolumns that the old DB doesn't have.
cd web
npm install
cp .env.example .env # leave VITE_API_BASE_URL blank for dev (Vite proxies /api)# Terminal 1 — backend
uvicorn app.main:app --reload
# Terminal 2 — frontend
cd web && npm run devOpen http://localhost:5173. You'll be redirected to /login — sign up first.
- Sign up with email + password (≥ 8 chars).
- Upload a
.md,.txt, or.pdfof notes. Extraction runs in ~10–30s. - Review the extracted cards on the deck page; delete any junk.
- Start run and answer free-response.
In dev the Vite server proxies /api/* to http://localhost:8000, so the SPA and API appear same-origin and cookies just work. Override the target with VITE_API_PROXY_TARGET if your API runs elsewhere.
- HP: start with 5. Wrong answers cost 1–3 HP based on card difficulty.
- Score: difficulty × 100 × grade × streak multiplier.
- Streak: every 3 correct in a row awards a random buff.
heal— +2 HP, click to consumehint— get a one-line hint on the next card without giving the answershield— next wrong answer absorbed, no HP lossfocus— next answer graded slightly leniently
- Mastery: each card has a 0–1 mastery score. Correct answers raise it, wrong drop it; decays with a 7-day half-life so old cards resurface.
- Selection: each turn picks the lowest-mastery card whose difficulty matches the current turn (difficulty ramps up over the run).
app/ FastAPI JSON API
main.py app entrypoint, CORS, startup hooks
auth.py password hashing, sessions, CSRF (form + header), dependencies
db.py SQLite/Postgres engine + session
models.py User / AuthSession / Deck / Card / Run / Attempt
schemas.py Pydantic request/response types — the API contract
llm.py Gemini client + structured-output helper
ingest.py file -> chunks -> Gemini -> cards
grader.py free-response grader + hint generator
run_engine.py HP / buffs / mastery / card selection
routes/
api/
auth.py /api/auth/{signup,login,logout,me}
decks.py /api/decks, /api/decks/upload, /api/decks/{id}, ...
runs.py /api/decks/{id}/runs, /api/runs/{id}/{answer,heal,hint}
web/ Vite + React SPA
index.html
vite.config.ts dev proxy /api -> :8000, Tailwind v4 plugin
src/
main.tsx entry
App.tsx router + providers (AuthProvider, QueryClientProvider)
index.css Tailwind import + hand-drawn design tokens
api/
client.ts fetch wrapper (credentials: 'include', X-CSRF-Token header)
types.ts TS types — kept in sync with app/schemas.py
hooks.ts useDecks, useDeck, useRun, mutations
auth/
AuthContext.tsx session state + CSRF token, /api/auth/me on mount
components/
Layout.tsx, Header.tsx, RequireAuth.tsx
ui.tsx PaperCard, Tape, Tack, StickyTag, Hearts, HandArrow
pages/
LoginPage, SignupPage, HomePage, DeckPage, RunPage
Backend on Fly.io, frontend on Vercel — Vercel rewrites /api/* to the Fly backend so the session cookie stays first-party. See fly.toml, Dockerfile, and web/vercel.json.
- Passwords hashed with argon2id (passlib's
argon2scheme). - Sessions are DB-backed with opaque random tokens (32 bytes from
secrets.token_urlsafe); the cookie carries only the token, never user data. - Cookies are
HttpOnly;SameSite=Laxin dev,SameSite=None; Securein prod (cross-origin SPA → API), with the Vercel proxy keeping them first-party in practice. - Per-session CSRF token is required on every state-changing endpoint via the
X-CSRF-Tokenheader (login/signup are exempt — no session yet). - Login error messages are deliberately generic ("Email or password is incorrect") to avoid leaking whether an email is registered.
- All deck/run access is scoped by
user_idin the route handlers (_owned_deck/_owned_runhelpers).
- Email verification (needs SMTP)
- Password reset (needs SMTP)
- "Log out other devices" (the data model supports it —
AuthSessionrows are per-device — just no UI yet) - OAuth / SSO