NoteRush 🎵

Tap tiles. Play the melody. Climb the leaderboard.

A Piano Tiles–style rhythm game for Reddit. Tap the white tiles as they fall, play Beethoven's Für Elise, and compete for the top score.


About the Project

Inspiration

I grew up playing Piano Tiles (Don't Tap the White Tile) on my phone. The concept is simple: tap the tiles as they scroll down. No power-ups, no ads in the way—just you, the beat, and your reflexes. When Reddit announced Devvit and the ability to build games inside posts, I wanted to recreate that experience. Not a bloated clone, but something lean: white tiles, a recognizable melody, and a leaderboard so you could see how you stacked up against other players. Für Elise was the obvious choice—everyone knows the opening, and hitting those notes in sequence feels satisfying.

What I Learned

  • Devvit Web runs your frontend in an iframe on Reddit. You have to work within that sandbox: requestExpandedMode to go fullscreen, no window.alert, and APIs like showToast instead. Touch and pointer events need to work on mobile, where most players are.
  • Web Audio API lets you synthesize sound without loading files. I built a simple piano-like tone with a fundamental sine plus harmonics and a fast attack. Keeping everything procedural keeps the bundle tiny.
  • Redis sorted sets are ideal for leaderboards. zAdd with the score as the sort key, then zRange with reverse: true to get the top N. The gotcha: without reverse, you get the lowest scores first.
  • Canvas 2D is enough. No need for WebGL or Three.js for a 2D rhythm game. A single canvas, a requestAnimationFrame loop, and careful use of fillRect and gradients gave smooth 60fps on mobile.

How I Built It

Client (simple-game.ts): Vanilla TypeScript, no frameworks. A single Canvas handles all drawing. The game loop updates tile positions every frame; fall speed is ( v = 180 + 1.2 \times \text{BPM} ) px/s, and tiles spawn every ( \frac{60}{\text{BPM}} \times 1.22 ) seconds. Hit detection checks if the tapped point lies over a tile whose center is in the hit zone (50%–96% of canvas height). On hit, tiles get a cyan gradient fill and sparkle effect—they keep moving down instead of popping away. Lives only decrease when a tile scrolls off-screen untouched.

Server (api.ts): Hono on Devvit. POST /api/submit-score stores the score in Redis with zAdd. GET /api/leaderboard/:mode fetches the top 20 with zRange(key, 0, 19, { by: 'rank', reverse: true }), loads the full entries from the stored keys, and returns them. The client merges this with session scores and highlights the current user's row.

Challenges

  • When to subtract a life — Penalizing wrong taps felt unfair. Players tap by accident. The rule became: you lose a life only when a tile falls off the bottom without being hit. Wrong taps give no penalty.
  • Tiles disappearing — Players wanted hit tiles to linger, not vanish. I changed the flow: on hit, a tile gets the cyan gradient and sparkles, keeps falling, and is removed only when it's 200px below the viewport. No fade-out, no pop.
  • Redis leaderboard order — First version showed the 20 lowest scores. Took a while to realize zRange defaults to ascending rank. The fix: reverse: true.
  • Audio unlock — Browsers block AudioContext until a user gesture. The game calls resume() on the first tap and handles the suspended state without breaking.

Features

  • 4 lanes — Tap, hold, or double-tap tiles as they fall
  • White tiles — Clean look; cyan gradient and sparkles on hit
  • Tile types — Tap (1 pt), hold (3 pts, fill the bar), double (2 pts, tap both sides)
  • 3 lives — Game over when you miss 3 tiles
  • Für Elise — Piano-style melody via Web Audio
  • Leaderboard — Top 10, server-stored, Retry button on game over
  • Touch & pointer — Works on mobile and desktop

Controls

Tap on the lane where the tile is. For hold tiles, keep your finger down until the bar fills. For double tiles, tap each half.

Scoring

  • Tap: 1 point
  • Double: 2 points (tap both lanes)
  • Hold: 3 points (hold until ~78% filled)

Tempo increases every 24 hits. Score is cumulative.

Tech Stack

Layer Stack
Client Canvas 2D, TypeScript, Vite
Server Hono, Redis, Devvit
Deploy Devvit Web (Reddit)

Project Structure

noterush/
├── src/
│   ├── client/
│   │   ├── simple-game.ts   # Game logic & rendering
│   │   ├── simple-game.html
│   │   ├── simple-game.css
│   │   ├── splash.ts        # Launch screen
│   │   ├── splash.html
│   │   └── splash.css
│   ├── server/
│   │   ├── index.ts
│   │   └── routes/
│   │       └── api.ts       # Submit score, leaderboard
│   └── shared/
│       └── api.ts           # API types
└── devvit.json

Development

npm install
npm run dev      # Start playtest at reddit.com/r/noterush_dev
npm run deploy   # Upload to Reddit

License

BSD-3-Clause

Built With

Share this project:

Updates