ByteBreaker: shipping a 20-level browser game inside a homepage
ByteBreaker started as the obligatory "easter egg game" for an OS-style homepage and ended up the largest app in the codebase: a full breakout game with 20 levels, 4 bosses, an endless mode, achievements, skins and a daily challenge — all running in a draggable window, in vanilla JS, with zero dependencies. This is the postmortem.
Architecture: eight files, fixed order
It's the only app in the site that's split into multiple modules, loaded in a fixed order: levels → state → audio → input → render → effects → engine → bootstrap. The split that mattered most is engine vs. render. The engine owns the requestAnimationFrame loop, physics, collision, the state machine (menu ↔ playing ↔ paused ↔ levelup ↔ gameover), combos and lives — and knows nothing about visuals. The render layer receives the engine state read-only and tracks all visual effects (brick flashes, paddle squish, screen shake, score tweening) in its own closure state, detecting events by diffing state between frames instead of subscribing to engine callbacks. That boundary made later visual-polish passes safe: render-only changes physically can't break gameplay.
Performance is handled with the classic tricks: a fixed 256-slot particle pool, cached brick gradients, ring-buffer ball trails, and throttled DOM writes for the HUD bars. One hard rule from playtesting feedback: no permanent canvas motion. No idle shimmer, no drifting background particles — everything on screen is either static or triggered by a game event.
Levels and bosses
There are 20 handcrafted levels, defined as plain number arrays, with a speed multiplier climbing from 1.00 to 2.00. Every fifth level is a boss: Sentinel, Mainframe, Rootkit, and finally Void Core at level 20. After that, an endless mode takes over with procedurally generated layouts from a seeded RNG, so the run never has to stop.
The level names lean into the theme — Init, Binary, Terminal, Proxy Chain, Zero Day — because if your homepage is a fake OS, the game inside it should feel like you're fighting the OS.
Meta-progression
The retention layer on top: 22 achievements, 7 paddle/ball skins (default, neon, ghost, ice, fire, plasma, void) unlocked through play, and a daily challenge — each UTC day deterministically generates a 5-level gauntlet from a date-derived seed (Mulberry32), fixed at hard difficulty, unlimited retries, best score kept locally and submitted to a small server-side daily leaderboard. Deterministic seeding means everyone plays the same daily levels, which is what makes a leaderboard meaningful.
All persistent state lives in localStorage with try/catch around every parse and write, because corrupted JSON or a full quota should degrade to "fresh profile", never to a crash.
Audio: synthesized, not sampled
The in-game soundtrack is generated entirely with the Web Audio API — no audio files. The music bed has four layers: an ambient drone, a boss pulse, a heartbeat layer for low-lives tension, and one-shot stingers, with a three-mode toggle (off / ambient / full). On top of that sit eight synthesized sound effects; brick hits pitch-bend upward with your combo, which is a one-line trick that makes streaks feel escalating.
Separately, the site's file-based music engine carries three real OST tracks (Byte Menu, Brick Runner, Boss Protocol) that map to game context — menu, normal play, boss fights.
The mutual-exclusion problem
The trickiest integration bug class wasn't in the game — it was the game coexisting with the site's homepage music player. Two audio sources fighting is instantly noticeable and deeply annoying. The fix is event-based: the game dispatches byte:bytebreaker-started, byte:bytebreaker-ended and byte:bytebreaker-music-mode-changed on the document, and the music engine listens. If the game's music mode is ambient or full, file-based music pauses; only mode "off" lets homepage music keep playing under the game. The engine even queues these events if they arrive before it has finished initializing, because the game window can open faster than the music engine boots.
What I'd tell past me
- Split engine and render from day one. Every cheap visual experiment afterwards is paid for by that decision.
- Diff-based event detection on the render side beats wiring engine callbacks — fewer touchpoints, no lifecycle leaks.
- Budget audio contexts. Browsers cap concurrent AudioContexts; the whole site has to share, so the game gets exactly one, lazily created and unlocked on first input.
- A game inside a window manager inherits every window-manager edge case: minimize must pause, focus loss must pause, and keyboard input must be ignored while the command palette is open.
It's a breakout clone. It's also, line for line, the most fun code in the repo.