All work

Candy Tycoon: How It's Built

A technical deep-dive into the framework-free rebuild: the layered architecture, the fixed-step game loop, the data-driven balance, the market system, the custom chart, and how saves stay forward-compatible.

Vanilla JS (ES Modules) HTML5 Canvas Clean architecture Fixed-step loop LocalStorage versioning Eleventy
Play it live

The problem

Recover a playable game from a ~500-line minified 2019 bundle and turn it into something I could actually extend, without reaching for a framework or a single runtime dependency.

My role

Architecture and implementation, solo.

Outcome

A layered, dependency-free codebase: a pure model that knows nothing about the DOM, a thin view of self-contained panels, and a versioned persistence layer, about a dozen small ES modules.

Three layers, one direction of knowledge
Three layers, one direction of knowledge

model/ is pure game logic and knows nothing about rendering or storage. ui/ reads the model and calls its actions. persistence/ serialises it. The model is the single source of truth; every dependency points inward, which makes the logic testable in plain Node.

A fixed-step game loop

main.js runs an accumulator loop on requestAnimationFrame: real time is sliced into fixed simulation ticks, so the economy is deterministic regardless of frame rate. A cap ignores huge gaps when the tab is backgrounded, and a bounded offline catch-up simulates time away on load.

Data-driven balance

Every candy, shop upgrade and global upgrade is a plain object in balance.js with a declarative `effect` descriptor. Derived getters (margins, liquidation rate, market config, which upgrades to reveal) are computed from an `upgradesOwned` Set, so loading a save needs no effect-replay.

The market system

A small MarketSystem rides the price hook: it schedules rare, lasting events that ease each candy's price multiplier up or down. Owned upgrades reshape its config (crash odds, spike size and frequency) purely through derived getters, never by mutating the system directly.

A chart with no dependencies

The original shipped a watermarked CanvasJS. The rebuild draws doughnut, bar and line charts by hand on a 2D canvas, with hover tooltips and lazy re-sizing to survive a zero-size canvas. A few hundred lines, zero libraries.

A framework-free panel system

The view is a thin composition root that fans a per-frame update() out to self-contained panels (Upgrades, Candy, Information, Shops) plus a news ticker. Each panel rebuilds its DOM only when its data signature changes, so 60fps updates stay cheap.

Saves that survive updates

A versioned save envelope plus graceful field defaults means older saves load without migration code. The game tracks its release version, shows a patch-notes popup when you return after an update, and records lifetime metrics in the save.

Built to debug

State stays recoverable and serialisable, free of the stale balance numbers and serialised methods that plagued the original. The pure model is verified with small Node scripts; the browser build is tested on a fresh port each time to defeat ES-module caching.