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
|
|
|
# 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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
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.
|
|
|
|
|
|
2026-07-02 22:23:26 +02:00
|
|
|
## Deployment
|
|
|
|
|
|
|
|
|
|
rack is its own container — deployed independently of `goonk`/`goonk-cv`,
|
|
|
|
|
same pattern as every other project on this host: a `Dockerfile` builds
|
|
|
|
|
the static site and serves it via nginx, `.gitea/workflows/docker.yml`
|
|
|
|
|
builds and pushes that image to the Gitea registry on every push to
|
|
|
|
|
`main`, and `docker-compose.yml` is the host-side reference for running
|
|
|
|
|
it. The CI workflow needs a repo secret named `TKNTKN` (a Gitea PAT with
|
|
|
|
|
`packages:write`) — same requirement as `goonk`'s own workflow, set under
|
|
|
|
|
Settings → Secrets.
|
|
|
|
|
|
|
|
|
|
Served from its own subdomain (e.g. `rack.explewd.com`), not a subpath of
|
|
|
|
|
goonk.se — that keeps the build completely vanilla (root-relative asset
|
|
|
|
|
URLs work as-is, no `--base` flag needed) and means the edge nginx just
|
|
|
|
|
needs one new `server_name` block or NPM proxy host pointing at whichever
|
|
|
|
|
port `docker-compose.yml`'s `HOST_PORT` exposes (`5677` by default — no
|
|
|
|
|
`--base=/rack/`/path-prefix plumbing required, unlike a subpath
|
|
|
|
|
deployment would need).
|
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
|
|
|
|
|
|
|
|
```bash
|
2026-07-02 22:23:26 +02:00
|
|
|
docker build -t rack .
|
|
|
|
|
docker run -d -p 5677:80 rack # or: docker compose up -d
|
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
|
|
|
```
|
|
|
|
|
|
2026-07-02 22:23:26 +02:00
|
|
|
`rack`'s own `localStorage` key (`rack-save-v1`) is namespaced regardless
|
|
|
|
|
of URL shape, so it won't collide with anything else on the same origin.
|
|
|
|
|
|
|
|
|
|
If a subpath deployment (`goonk.se/rack`) is wanted later instead, use
|
|
|
|
|
`npm run build:subpath` (`vite build --base=/rack/`) — it rewrites
|
|
|
|
|
`dist/index.html`'s asset URLs to `/rack/assets/...`. That requires the
|
|
|
|
|
edge proxy to forward the `/rack/...` path through *unchanged* (not
|
|
|
|
|
strip the prefix) and the container to serve files at that same
|
|
|
|
|
sub-path — more moving parts to keep in sync than the subdomain route
|
|
|
|
|
above, which is why subdomain is the current choice.
|
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
|
|
|
|
|
|
|
|
## 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.
|