Files
rack/README.md
Fredrik Johansson f0314e7492 Initial commit: rack, a homelab idle game
Core loop (hardware tiers, deployable services, upgrades, random events,
prestige), quirks, host nicknames, an ASCII rack view, in-app help/
changelog, and a Vitest suite covering engine logic and data balance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 21:55:54 +02:00

6.6 KiB

rack

A browser-based idle/incremental game about running a homelab. Start with a single Raspberry Pi on a desk, incrementally expand into something that vaguely justifies its electricity bill.

Dark terminal aesthetic, dry flavour text, numbers go up, occasionally things go wrong.

Running it

npm install
npm run dev      # dev server on http://localhost:5677
npm run build    # production build
npm run preview  # preview the build on http://localhost:4587
npm test         # vitest run - engine + balance tests

No backend. Game state lives entirely in localStorage, autosaved on every render tick and on tab close/hide.

How it works

  • Hardware (src/data/hardware.ts) provides RAM slots and power budget, enforced per host (not a shared pool). You can either unlock the next tier, or buy another unit of a tier you already own as a cheaper bridge — each extra unit of the same tier costs 40% more than the last (hardwareCost() in engine.ts). The starter Pi 4 is a one-time freebie and can't be rebought.
  • Services (src/data/services.ts) run on hardware, consume RAM/power, and produce points/sec. They age (production creeps up over time) and can crash (probability rises with age, mitigated by the Cooling upgrade). The deploy dropdown shows each service's RAM/power cost up front and disables anything that wouldn't fit the currently selected host.
  • Upgrades (src/data/upgrades.ts) are one-time persistent purchases: UPS shortens outages, Backups tiers reduce/reverse crash losses, Monitoring reveals crash risk, Redundancy runs a second instance of every service, Documentation is a small passive multiplier.
  • Random events (src/engine.ts rollRandomEvent) fire on a Poisson-ish interval that tightens as the game clock advances: disk-full crashes, power outages, kernel panics, fan-noise flavour-only events, optional updates, and a rare rm -rf that deletes a service outright (guarded once Backups I is bought).
  • Prestige: once total lifetime earnings cross 50,000 pts, a banner offers to decommission the rack — wipes hardware/services/upgrades but grants a permanent production multiplier and carries forward.
  • Host nicknames (src/data/hostnames.ts): every host is randomly christened on purchase from a pool of sci-fi/mythology/sysadmin-humour names (hal9000, shodan, the-basement, works-on-my-machine, ...), drawn without replacement so they double as unique identifiers wherever a host is named.
  • Quirks (src/data/quirks.ts): each deploy has a 30% chance of rolling a per-instance trait (Overclocked, Chatty, Legacy, Well-documented, Flaky, Artisanal) that shifts that instance's production and crash risk. Shown as a badge on the service row and in the rack view's tooltip.
  • Rack view: an ASCII-art strip above the two main columns, one line per owned host, showing which services (as 2-letter codes) are deployed where and free capacity as dots — a compact answer to "what's actually running on what."
  • Help & changelog: click the ? in the header, or press ? anywhere outside a text input, to open an in-app overlay with a quick "how this works" list and the full version changelog (src/data/version.ts), mirroring ../adventure's help-modal pattern.

Project layout

src/
  types.ts       game state & definition types
  state.ts       new game, save/load (localStorage), id allocation
  engine.ts      tick loop: production, aging, crashes, events, prestige
  ui.ts          DOM rendering, one full re-render per render tick
  help.ts        the ?-triggered help/changelog overlay
  data/          static definitions: hardware/services/upgrades/events/
                 quirks/version (VERSION + in-app CHANGELOG entries)
  style.css      terminal theme

The simulation tick runs on requestAnimationFrame and is time-delta based (not frame-based), so offline/backgrounded tabs still catch up correctly when they regain focus. The passive UI re-render runs on its own ~1s interval, decoupled from the tick — user actions (buy, deploy, fix, uninstall) re-render immediately via a direct callback instead of waiting on that interval, and it's skipped entirely while a <select> has focus so the service-picker dropdown doesn't get yanked shut mid-render.

Deploying as a subdirectory (e.g. goonk.se/rack)

npm run build emits root-relative asset URLs (/assets/...), which only works if rack is served from the domain root. To deploy it under a subpath instead, use:

npm run build:subpath   # vite build --base=/rack/

That rewrites dist/index.html's asset URLs to /rack/assets/.... Drop the resulting dist/ contents into the host site wherever it serves static files under /rack/ — for the goonk Astro site specifically, that's goonk/public/rack/ (Astro copies public/ verbatim into its own dist/ at build time, and its nginx config already does a try_files $uri $uri/ /index.html fallback, so no nginx changes are needed - existing public/waste, public/pitwall follow the same one-subdir-per-project convention). rack's own localStorage key (rack-save-v1) is namespaced, so it won't collide with anything else served from the same origin.

Tests

src/engine.test.ts (Vitest) covers the core loop mechanics rather than UI: deploy/capacity checks (including per-host isolation), hardware/upgrade purchases and prerequisite gating, the fix-cooldown (per-service, not global — see CHANGELOG for why that matters), uninstall refunds, production/crash/outage ticking, and prestige. A separate balance sanity block asserts properties of the static data in src/data/ rather than specific numbers — e.g. every service pays back its own deploy cost in under 5 minutes at base rate, hardware cost/capacity strictly increases with tier, and no service's crash risk has crept past the intended ceiling. These are meant to catch a future data tweak that breaks the early-game feel, not to lock the current numbers in stone — adjust the thresholds deliberately if you retune the balance.

Design decisions worth knowing

  • Vanilla TypeScript + DOM, no framework — matches the rest of the homelab-project family (waste-go, flit) and keeps this dependency-light per PLAN.md.
  • Dev/preview ports are non-default (5677 / 4587 instead of Vite's 5173 / 4173) because the other sibling projects on this machine claim the usual ports and everything within ±10 of them.
  • Starting balance is 15 pts, not 0 — the cheapest service (Nginx) costs 10, so a brand-new game can bootstrap its first deploy immediately without an awkward zero-income deadlock.