Inspiration

Most word-puzzle games either lock you into one format (just anagrams, just word scrambles, just crosswords) or turn "competitive" into an empty leaderboard with no actual stakes. I wanted a puzzle game with real skin in the game, like paying a small entry fee (Not real money, virtual coin), racing with the clock, walk away with more than you put in if you're fast and correct or don't, if you're not. That single loop stake → solve → settle is the whole game, and I wanted it to feel identical whether you're unscrambling a sentence, guessing an anagram, or filling in a crossword grid. And the important thing is I wanted to learn Flutter after spending a decade in native iOS development.

What it does

Jumbolt is a single-player word-puzzle game built around one shared round loop across three modes:

  • Sentence ("Arrange the words") - reorder scrambled words back into a sentence.
  • Anagram ("Guess the answer") - reorder scrambled letters into an answer word (e.g. "Guess the Country" → SPAIN).
  • Crossword - untimed, all-or-nothing grading on a full-grid submit, with entry fee and reward scaled by grid size (mini/full).

Every round follows the same shape: pay a fixed entry fee, get a scrambled puzzle, solve it within a fixed window for a reward. A global leaderboard ranks the top 100 players by wallet balance, so every correct, on-time solve has a visible payoff.

How we built it

Client: Flutter, structured as MVVM-C. ViewModels extend a shared BaseViewModel (ChangeNotifier + a dispose-safe safeNotify()), navigation runs exclusively through an AppCoordinator so ViewModels never touch Navigator directly, and dependencies are wired through get_it. The client has zero Firebase SDK dependency - it talks to the backend purely over REST via dio.

Backend: entirely Firebase - Cloud Functions (an Express app wrapped in a single onRequest handler) + Firestore, deployed on the Blaze plan. A Firebase Hosting catch-all rewrite forwards every path to that one function, so the "serverless function" is really a normal REST API underneath.

Wallet balances are the one thing that can never drift: every credit and debit runs inside a Firestore transaction (read-check-write, reads strictly before writes) for atomic check-and-decrement, and every change writes an append-only transaction record alongside it. No other code path is allowed to touch a wallet document.

Challenges we ran into

  • Stateless functions, stateful rounds. Cloud Functions instances don't persist memory between invocations, and start and submit can land on two different instances entirely. Every in-progress round has to live in Firestore, not in a variable - which shaped the whole round-doc pattern from day one.
  • Firestore Timestamp isn't JSON. A raw Firestore Timestamp serializes to {_seconds, _nanoseconds}, not an ISO string. That silently broke a wallet screen - DateTime.tryParse threw on the malformed object client-side, and because the error didn't surface as a normal API failure, the screen just hung on loading with no visible cause. The fix was a strict rule: always call .toDate().toISOString() before a Firestore-sourced date goes anywhere near a JSON response.
  • Crossword grading with no separate entry list. Early on we considered authoring crossword entries by hand alongside the grid - and immediately realized that's a guaranteed way for an across entry and a down entry to disagree on a shared letter. Deriving numbering and entries programmatically from the grid itself removed that failure mode entirely.
  • Testing transaction-heavy Firestore code. Hand-rolled fakes for runTransaction-based services kept missing real bugs (the Timestamp issue among them). We settled on verifying wallet/round/auth logic against the real emulator (or carefully, disposable test accounts on the live project) instead of expanding a fake Firestore layer.

Accomplishments that we're proud of

  • One shared puzzle engine and one shared round-doc pattern power three genuinely different game modes (timed word reorder, timed letter reorder, untimed full-grid crossword) without three parallel implementations.
  • A server-authoritative economy where the client is trusted for nothing - not timing, not correctness, not balance - while still feeling instant and responsive to play.
  • A crossword grid where the entries and their numbering are mathematically guaranteed to agree with the grid, because they're derived from it rather than duplicated.
  • A fully working admin panel and puzzle-authoring flow with zero frontend build tooling - just HTML, CSS, and JS talking to the same API real players use.

What we learned

  • Stateless-by-default backends push you toward better designs even when they're inconvenient - persisting round state to Firestore instead of memory turned out to make horizontal scaling and instance recycling non-issues we never had to think about again.
  • Small serialization mismatches (a Timestamp object where a string was expected) can cause silent, hard-to-diagnose failures far from where the actual bug lives - worth guarding at the boundary every time, not case by case.
  • Deriving structure (crossword entries from a grid, puzzle randomness from a stored field) beats maintaining two representations of the same truth, even when the derived approach takes a bit more upfront design work.

What's next for Jumbolt: Word Puzzle

  • Additional puzzle modes built on the same shared engine and round-doc pattern.
  • Deeper leaderboard mechanics beyond a single global top-100 wallet ranking (e.g. streaks, seasonal resets, per-mode boards).
  • Expanding the crossword mode's grid library and exploring themed/daily crossword drops using the existing size-keyed puzzle-selection mechanism.
  • Hardening the admin panel's puzzle-authoring tools as the puzzle catalog grows.
Share this project:

Updates

Submission history