
The problem
A quiz game’s rules live in two places by default: the code that scores a round, and the panel that explains scoring to the player. They drift apart, and the player is the one who finds out.
Decisions
PostgreSQL with no ORM
Schema changes are numbered SQL migrations and queries are written as SQL, so the shape of every query stays visible in the file. Auth is its own JWT implementation, not a hosted identity provider, which keeps the session model in the same codebase as everything it protects.
Levels come out of the size of the question bank
A level is ceil(questions / 20), so adding questions to the bank adds levels without a second edit. The question bank lives in the database and is seeded from committed JSON.
The How to Play panel imports the scoring rules
Every number it shows is imported from the modules the game scores with. Nothing about the rules is typed twice.
The hard part
Making the rules a single source of truth, and finding out they had never agreed
Wiring the player-facing explanation to the scoring modules was meant to be housekeeping. It surfaced three real mismatches, none of which would ever have produced an error.
The streak bonus stopped growing at a four-question streak while the HUD kept counting “5x, 6x, 7x…” in lit gold, promising a reward that had quietly stopped paying. The cap is gone; a streak cannot exceed a level’s twenty questions anyway, so it never needed one.
A +500 perfect-round bonus was calculated and never called, so a 20/20 round paid exactly the same as 19/20. And the results screen graded on a looser curve than the one saved to the level card. 90% earned five stars on the way out, 95% was required for the record, so a player could see five stars and then watch the level-select screen show four for the same run.
All three are the same bug: a number that existed in two places. Importing them from one is what turned the panel from documentation into a test.