Inspiration

This project was born from a desire to create an engaging, educational game that combines historical numeral systems with competitive gameplay. The Roman numeral conversion mechanic offers a unique cognitive challenge—players must quickly parse ancient notation and convert it to modern Arabic numbers, a skill that exercises both pattern recognition and mental math. The idea of hosting this on Reddit's Devvit platform presented an opportunity to reach a large community and create a leaderboard-driven gaming experience that encourages repeated play and friendly competition.

What We Learned

Building What's the Number? provided invaluable lessons across multiple domains:

  • Game Development & Phaser 3: We mastered Phaser's scene architecture, discovering that reusable scenes require careful cleanup (destroying objects on create() to avoid texture reference stale pointers) and explicit state passing (avoiding undefined in scene.start()) to prevent subtle bugs across scene transitions.

  • Mobile-First UX Design: Creating a responsive layout taught us the importance of a scaleFactor approach: allows UI elements to scale uniformly across devices. Mobile keyboard support required an unconventional solution—an invisible HTML <input> overlay positioned over the Phaser canvas—because native soft keyboards don't integrate seamlessly with game engines.

How We Built It

The project follows a modular, scene-based architecture:

  1. Client (Phaser 3): Seven distinct scenes handle different game states (Boot, Preloader, MainMenu, Play, Result, Rule, Score), each managing its own lifecycle and UI. Button interactions are abstracted into a createMenuButton() helper that ensures consistent styling and prevents visual glitches from scale animation accumulation.

  2. Server (Express.js + Devvit): Two main endpoints (/api/games for questions and scoring, /api/scores for leaderboard) decouple game logic from the client. The server determines difficulty via Roman numeral character length, calculates scores using a formula that rewards speed ($\text{score} \propto \text{level} \times \text{restTime} + \text{cumulativeScore}$), and updates Redis only when a new personal best is achieved.

  3. Data Flow: Play scene → Submit triggers async POST to /api/games → server calculates score and next question → Result scene displays feedback with server-calculated score → Next button either caches the response for immediate play or fetches a new question.

  4. Build Pipeline: Vite handles bundling for both client and server with separate tsconfig.json files per workspace. The production build creates a dist/ folder consumed by Devvit's upload.

Challenges Faced

  • Scene Reuse Pitfalls: Phaser scenes can be reused (e.g., Play scene used for every round), but reuse without cleanup causes texture reference errors (drawImage(null)) and duplicate event handlers. Solution: destroy UI objects in create() and guard texture access with if (this.textures.exists()).

  • Duplicate Keystroke Bug: Registering keyboard listeners without removing them on scene shutdown caused keystrokes to be processed multiple times. Solution: store the listener reference and explicitly remove it during shutdown().

  • Mobile Keyboard Integration: Phaser's text input objects don't trigger native soft keyboards reliably. Solution: create an invisible HTML <input> overlay, position it over the game canvas, and sync its value with Phaser text via event listeners.

  • Layout Responsive Scaling: A naive approach (setting positions directly on every resize event) caused layout thrashing. Solution: compute scaleFactor once, apply it consistently to all positions and scales, and delay refreshLayout() by 16ms to allow the canvas to fully resize.

  • Data Passing Across Scene Transitions: Passing undefined to scene.start() caused the scene to reuse old data from the previous run. Solution: always pass an explicit object (even if empty, {}) to force fresh initialization.

  • Score Calculation Authorization: Early prototypes calculated scores on the client, allowing cheating. Solution: moved all score logic server-side; the server owns the authoritative score and passes it back to the Result scene.

Built With

Share this project:

Updates