feat: bootstrap walking skeleton

This commit is contained in:
ginnoir
2026-06-11 17:06:52 -05:00
commit ec8f857845
44 changed files with 6677 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
# ADR 0001: Web Stack And Engine Boundary
## Status
Accepted, 2026-06-11.
## Context
Idlegame is a text-first idle/incremental RPG. The UI is mostly panels, progress
bars, action lists, resources, logs, and branching story choices. It must run
well on phones from day one and remain easy to wrap later for desktop or mobile
stores.
## Decision
Use TypeScript strict, Vite, React 19, Zustand, Tailwind v4, Vitest, Biome, and
vite-plugin-pwa.
Keep gameplay rules in a pure TypeScript engine under `src/engine/`. React,
Zustand, browser storage, requestAnimationFrame, and lifecycle hooks belong
outside the engine.
Use plain JavaScript numbers behind `src/engine/num.ts` for M0/M1. Human-readable
values are a design pillar; a Decimal-style dependency is deferred until a later
layer proves it is needed.
## Consequences
- Engine behavior is headless-testable with Vitest.
- UI and wrapper changes should not force gameplay rewrites.
- Offline progress uses the same fixed-timestep simulation path as online play.
- Save/load validation sits at explicit boundaries with versioned schemas.
- Dependencies must be permissive; no GPL packages are allowed.
+58
View File
@@ -0,0 +1,58 @@
# Architecture
Idlegame is split into four layers.
## Engine
`src/engine/` contains deterministic gameplay logic:
- `tickLoop.ts`: fixed-timestep accumulator. Large elapsed deltas, including
offline catch-up, drain through the same tick path as active play.
- `game.ts`: core game state, active action progress, and resource accrual.
- `save.ts`: versioned save schema, serialized export/import strings, and
offline elapsed calculation.
- `num.ts`: branded numeric boundary and human-readable formatting.
The engine must stay pure. It does not import React, Zustand, browser APIs,
storage, requestAnimationFrame, or Date scheduling.
## Content
`src/content/` contains authored definitions validated by Zod. M0 includes one
resource and one timed action. M1 expands this into real resources, actions,
story nodes, automation unlocks, and prestige definitions.
## State
`src/state/` owns environment coupling:
- persistence backend selection: IndexedDB via `idb-keyval`, with localStorage
fallback
- load-on-boot and autosave orchestration
- requestAnimationFrame loop and lifecycle hooks
- mapping engine state to view models
- Zustand store updates for React
## UI
`src/ui/` renders the view model and calls runtime commands. Components should
not implement gameplay rules. The M0 shell includes a resource readout, action
panel, progress bar, and event log.
## Verification
Run the full local gate before pushing:
```powershell
pnpm typecheck
pnpm lint
pnpm test:coverage
pnpm build
```
Rendered checks for the walking skeleton:
- app loads without framework overlay or console errors
- starting the timed action increments Gold after completion
- reload preserves saved state
- reopening after time away credits offline progress
+163
View File
@@ -0,0 +1,163 @@
# Idlegame — M0 Bootstrap Plan (handoff artifact for superpowers)
> **How to use this file:** the interview/brainstorm phase is complete; everything below is locked. Hand this plan to superpowers for execution (this copy in the working dir at `docs/plans/2026-06-11-m0-bootstrap.md` is canonical). Tasks are bite-sized with per-task verification so `executing-plans` can run them sequentially. Feature work after M0 flows through superpowers' brainstorm → plan → TDD cycle per feature.
>
> **Refined 2026-06-11 (same-day review pass, approved by ginnoir):** T3 split into T3.1T3.4; T8 deploy auth + Caddy mechanics specified (romhacks-wiki precedent); Node policy `engines >=24` / CI on 24 LTS; pnpm 10 postinstall allowlist note; CI-required protection toggle moved from T6 to T7. No design decisions re-opened. Review-gate addition: per-task tool + model routing for M0 (§AI tool routing).
## Context
ginnoir is starting a commercial-grade idle/incremental game: a text fantasy RPG inspired by **Your Chronicle** (timed actions, resource generate/consume loops, story objectives), structured like a visual novel with branching choices, ending runs in a prestige system that avoids same-y repeats. Monetization, if ever: upfront purchase or cosmetics only — QoL and speed-up are never paywalled; any process completed once becomes automatable.
**M0 scope**: capture the locked design in the vault, stand up the multi-tool workflow (Gitea + CI + internal playtest deploys + Plane + Obsidian), and scaffold a walking-skeleton codebase. Gameplay (M1 vertical slice) is seeded as Gitea issues and built in later sessions.
Starting state: `C:\Users\MattC\Documents\idlegame` is empty except `docs/plans/`, not a git repo. Gitea runs at `gitea.ginnoir.com` (owner `ginnoir`, private default, HTTPS creds in GCM). No Gitea Actions runner exists yet. Plane is self-hosted on valhalla. Obsidian vault is the cross-project source of truth. Superpowers drives development workflow; all four AI tools (Claude Code, Codex, Antigravity, Cursor) get Obsidian MCP + local vault access.
## Locked design pillars (interview, 2026-06-11)
| Pillar | Decision |
|---|---|
| Platform | Responsive web + PWA; plays well on phones from day one. Steam (Tauri/Electron) and app-store (Capacitor) wraps are later milestones |
| Visuals | Text-first UI: panels, progress bars, action lists, event log. No art pipeline |
| Combat | Light/abstracted — timed actions with stat checks and risk/reward; no battle screen in v1 |
| First playable (M1) | Vertical slice: one short story arc → first prestige reset; branching choice; automation unlock; offline progress; save/load |
| Prestige | **Hybrid**: aggressive catch-up *within* a layer (Magic Research style — keep knowledge, blast through known content); genuinely new mechanics *between* layers (Antimatter Dimensions style) |
| Numbers | Human-readable throughout (≤ billions). No scientific-notation spectacle |
| Pacing | Semi-active (Your Chronicle-like): queue actions, choices every few minutes; idles/offline fine |
| Story authoring | Outline together in Obsidian; Claude drafts prose; ginnoir reviews/edits |
| Automation principle | Complete a process once → it becomes automatable (generous, progression-gated) |
| CI/CD | Gitea Actions runner on valhalla; typecheck/lint/test/build on push; auto-deploy dev build to internal-only playtest URL |
| Tracking | Plane = roadmap/epics. Gitea Issues = code-level tasks (commit/PR auto-linking) |
| Codename | `idlegame` (rename later is cheap in Gitea) |
## Tech stack (researched recommendation)
**TypeScript (strict) + Vite + React 19 + Zustand, with the game engine as a pure-TS module that never imports React.**
- Genre precedent: every successful game in this genre is a web app — Antimatter Dimensions (Vue), Magic Research (React), Melvor Idle (web, wrapped for Steam/mobile). Text/UI-heavy idle games are DOM games; Godot/Unity fight you on UI and bloat web exports.
- React over Svelte/Vue: deepest fluency across all four AI tools — important when agents write most of the code. Perf is a non-issue with the core/view split: engine ticks on its own loop; UI subscribes to snapshots at ~10 fps via Zustand selectors.
- The engine/view split is the load-bearing decision: headless-testable game core (Vitest, no DOM); Tauri/Capacitor wraps or even a framework swap later touch only the view.
- **Considered & rejected**: Profectus (MIT, Vue, Modding-Tree lineage, ~34 stars — license fine but Vue-locked, shaped for tree-prestige games, community too small for a commercial bet); The Modding Tree (older JS, same shape); Godot (canvas UI friction for a text game).
Dependencies (all permissive; the game stays proprietary — no GPL anywhere):
| Dep | License | Role |
|---|---|---|
| react, zustand | MIT | view + state bridge |
| zod | MIT | schema validation for content definitions and saves |
| lz-string | MIT | compressed save export/import strings |
| idb-keyval | Apache-2.0 | IndexedDB persistence (localStorage fallback) |
| vite, vite-plugin-pwa, workbox | MIT | build + installable PWA |
| tailwindcss v4 | MIT | styling for the text-first UI |
| biome, vitest, (playwright later) | MIT/Apache-2.0 | lint+format, unit tests, E2E later |
Toolchain: **pnpm**, **Node 24 LTS as the CI/runtime baseline**`engines: { "node": ">=24" }` (a floor, not an exact pin: local boxes may run newer — Node 25.9 verified working 2026-06-11; a strict `24.x` pin makes every local `pnpm install` warn), `.nvmrc` = `24`, and CI jobs run a `node:24` image so the LTS pin is enforced where determinism matters. Numbers: plain JS numbers behind `src/engine/num.ts` (format + branded type); ADR documents the Decimal escape hatch if a late prestige layer ever needs it.
## Workflow architecture
- **Repo**: `gitea.ginnoir.com/ginnoir/idlegame`, private. Trunk `main` (protected, CI required), feature branches + PRs, conventional commits, self-merge allowed (solo + AI dev).
- **Issues**: Gitea labels (`type/feat|fix|content|infra`, `area/engine|ui|story|workflow`, `prio/1-3`), milestones `M0 Scaffold`, `M1 Vertical Slice`. Issue templates (feature/bug/content) + PR template in `.gitea/`.
- **Plane**: project "Idlegame" with epics (Vertical Slice, Workflow & Infra, Game Design). Epics link Gitea issues by URL; day-to-day work lives in Gitea.
- **CI** (`.gitea/workflows/ci.yml`): pnpm install → `typecheck``biome ci``vitest run --coverage``build` → upload `dist` artifact.
- **Deploy** (on push to `main`): rsync `dist/` over SSH (dedicated deploy key stored as repo Actions secrets) to valhalla's Caddy site dir `/config/caddy/site/idlegame/``idlegame.ginnoir.com`, **internal-only during development** (Caddy `import internal_only` — same pattern as other internal sites; no auth layer needed). Precedent: romhack-archive-site static deploy (`/config/caddy/site/romhacks-wiki`).
- **Runner**: `act_runner` container (docker mode, outbound polling only) added to homelabstack's `dev` stack, following that repo's conventions — read `Homelab/_Claude.md` + `Deploy.md` in the vault before touching homelabstack (applies to T7 **and** T8).
## Cross-tool agent layer
- **`AGENTS.md` is canonical** — Linux Foundation standard; Codex, Cursor, and Antigravity read it natively. Contents: project overview, commands, architecture map, conventions (commits, PR flow, engine/view boundary rules), the context-layer contract, and the tool-routing table below.
- **`CLAUDE.md`** = `@AGENTS.md` import + Claude-only extras (superpowers notes, vault skill pointers).
- **Context-layer contract** (stated in AGENTS.md): Obsidian vault (all tools have MCP + the local folder `C:\Users\MattC\Documents\Obsidian Vault\`; record both in AGENTS.md) = design docs, story, decisions, session memory. **API tokens tools authenticate with live in the vault at `claude/Credentials.md`** — never in the repo. Repo `docs/` = code-truth (README, ADRs, architecture, superpowers plans in `docs/plans/`). Gitea Issues = work items. Every tool writes session outcomes back to the vault.
## AI tool routing (light)
One tool per issue/branch at a time; everything lands via PR; AGENTS.md + vault are the shared brain regardless of tool.
| Tool | Best at | Route these tasks |
|---|---|---|
| **Claude Code** (+ superpowers) | Architecture, multi-step planning, nuanced writing, big multi-file features, code review | Engine/systems design, prestige & balance design, story outlining + prose drafts, milestone planning, reviewing other tools' PRs, vault stewardship |
| **Codex** | Long autonomous runs on well-scoped tasks, gnarly debugging, test discipline | Well-scoped implementation tickets, bug hunts with repro steps, test-coverage passes, mechanical refactors |
| **Antigravity** | Frontend work with built-in browser verification, multimodal/screenshot feedback, large-context research | UI implementation + polish, responsive/mobile layout passes, visual verification of the playtest build, competitive research (other idle games) |
| **Cursor** | Interactive pair-programming, fast small edits while ginnoir drives; agent mode now frontier-class (Composer 2.5 ≈ Opus 4.8 on benchmarks, June 2026) | Quick fixes, small scoped tweaks, exploratory fiddling, anything ginnoir wants to steer by hand; credible alternate for Codex-lane scoped tickets |
Rule of thumb: design and words → Claude; heads-down scoped code → Codex; anything you need to *see* → Antigravity; anything you're driving yourself → Cursor.
### Per-task routing (M0)
Applies the rules above to T1T10. Claude Code legitimately dominates M0 — it's workflow/infra/writing-heavy by design; Codex and Antigravity take the tasks where they genuinely win. **Cursor has no M0 assignment** (its lane is ginnoir-driven interactive edits; M0 runs autonomously — Cursor's turn starts at M1). **Default execution mode is unchanged**: a single superpowers/Claude Code `executing-plans` run per the header; the table says who's *best* per task — route T2/T3.2/T3.3/T3.4 out to Codex/Antigravity only if ginnoir wants to shake down the multi-tool workflow during M0. For M1+ feature work the table's logic is the standing template.
**Claude Code primary is Opus 4.8** — Fable 5 access ends **2026-06-22**, so nothing durable relies on it. ⭐F marks the tasks that benefit most from Fable 5 while it lasts (the highest-judgment writing and architecture calls); if M0 executes before the cutoff, running the whole thing on Fable 5 is fine.
| Task | Tool | Model | Why |
|---|---|---|---|
| T1 vault notes | Claude Code | Opus 4.8 ⭐F | Vault stewardship; GDD/story prose quality compounds downstream |
| T2 scaffold | Codex | GPT-5.5-Codex (medium) | Well-scoped ticket, command-line verification, gotchas pre-spelled-out; alt: Cursor agent (Composer 2.5) |
| T3.1 num + tick loop | Claude Code | Opus 4.8 ⭐F | Load-bearing engine architecture: API shape + TDD, not heads-down typing |
| T3.2 resource + action | Codex | GPT-5.5-Codex (medium) | Scoped code on shapes T3.1 fixed; fully test-gated |
| T3.3 saves + persistence | Codex | GPT-5.5-Codex (highest effort tier) | Save integrity = the data-loss surface; strongest test discipline |
| T3.4 Zustand + React shell | Antigravity | Gemini 3 Pro (high thinking) | Browser verification watches the progress bar, reload, offline credit |
| T4 AGENTS.md + docs | Claude Code | Opus 4.8 ⭐F | The conventions doc every other tool reads |
| T5 git + Gitea repo | Claude Code | Sonnet 4.6¹ | API glue with vault-held creds; mechanical |
| T6 repo config | Claude Code | Sonnet 4.6¹ | Labels/protection/templates via API |
| T7 Actions runner | Claude Code | Opus 4.8 | Touches the live homelab stack; vault conventions + care |
| T8 playtest deploy | Claude Code | Opus 4.8 | Secrets, Caddy, cross-repo infra; Antigravity optional for browser/PWA verify-assist |
| T9 Plane + M1 seeding | Claude Code | Opus 4.8 ⭐F | Issue prose = the contract for M1 sessions |
| T10 write-back | Claude Code | Sonnet 4.6¹ | Vault stewardship, mechanical |
¹ Tier note: if M0 runs as one continuous `executing-plans` session (lowest-friction path), stay on one model throughout — the Sonnet 4.6 entries mean "this tier suffices when run standalone / cost-sensitive."
Caveats: (a) all of T3 predates the repo (git init is T5), so M0 handoffs to Codex/Antigravity land directly in the working dir without the PR gate — hand off only at task boundaries with green tests; full one-tool-per-branch discipline starts at M1. (b) Model names current as of 2026-06-11 (Opus 4.8 / Sonnet 4.6 standing, Fable 5 only until 2026-06-22; GPT-5.5-Codex; Gemini 3 Pro; Cursor Composer 2.5, which benchmarks ≈ Opus 4.8 — making it Cursor's default for everything and Cursor agent a credible Codex-lane alternate at M1+) — substitute each tool's newest equivalent tier at execution time.
## Obsidian vault additions
Per `claude/Vault.md` convention: `Idlegame/_Claude.md` (what it is, working dir, repo URL, standing rules — monetization stance, automation principle, prestige philosophy — quick-nav, session log, `[[Context]]` backlink); `Idlegame/GDD.md` (pillars + core loops from the interview); `Idlegame/Story/Outline.md` (authoring-workflow skeleton); `Idlegame/Decisions.md` (design ADR-lite; code ADRs live in repo). Add the row to `claude/Context.md`'s project table.
## Scaffold layout
```
idlegame/
AGENTS.md CLAUDE.md README.md LICENSE.md (proprietary notice)
.gitea/workflows/{ci,deploy}.yml .gitea/issue_template/ PR template
.nvmrc package.json tsconfig.json (strict) biome.json vite.config.ts (PWA)
docs/adr/0001-tech-stack.md docs/architecture.md docs/plans/ (superpowers plans)
src/engine/ ← pure TS: fixed-timestep tick loop + offline catch-up, resources,
action queue, story graph, prestige hooks, versioned saves
(zod-validated, lz-string export), num.ts
src/content/ ← data-driven, zod-validated definitions (resources, actions, story nodes)
src/state/ ← zustand bridge: engine snapshots → view
src/ui/ ← React shell: action panel, resource bar, event log, settings
src/engine/__tests__/ ← engine core target: 80%+ coverage
```
The scaffold ships a **walking skeleton**, not gameplay: 1 resource, 1 timed action, tick loop with offline catch-up, save → reload → intact. Proves the architecture end to end; M1 makes it a game.
## Tasks (superpowers-executable, in order)
**T1 — Vault project setup.** `Idlegame/_Claude.md` and the `claude/Context.md` row already exist (created at planning handoff) — extend `_Claude.md`, don't recreate. Create `Idlegame/GDD.md`, `Idlegame/Story/Outline.md`, `Idlegame/Decisions.md` (content from this plan). *Verify: vault_read returns each note; Context.md table has the row.*
**T2 — Scaffold the app.** Vite react-ts template → strict tsconfig, Biome, Vitest, Tailwind, vite-plugin-pwa, folder layout above (except `.gitea/workflows/deploy.yml` — deferred to T8 so deploy never runs red before its secrets exist), `.nvmrc` (`24`) + `engines: { "node": ">=24" }`. pnpm 10 blocks dependency postinstall scripts by default — allowlist build deps (esbuild etc.) via `pnpm.onlyBuiltDependencies` in `package.json` (or `pnpm approve-builds`), or the toolchain silently breaks. *Verify: `pnpm typecheck && pnpm lint && pnpm test && pnpm build` all green; `pnpm dev` serves; manifest in build output.*
**T3 — Walking skeleton (TDD, four sub-tasks).** Tests first throughout; engine code never imports React.
- **T3.1 — Engine core: `num.ts` + tick loop.** Branded number type + human-readable formatting; fixed-timestep accumulator loop (`advance(now)`) where offline catch-up is the same code path (large delta → many ticks, batch-capped). Configure Vitest coverage thresholds here (≥80% on `src/engine/`). *Verify: tests green; determinism test — N elapsed seconds always yields the same tick count.*
- **T3.2 — One resource, one timed action.** Zod-validated content definitions in `src/content/`: one resource, one timed action (duration → yields resource); action progress advances per tick, completion grants the yield. *Verify: accrual + completion unit tests green.*
- **T3.3 — Versioned saves + persistence.** Zod save schema with version field, serialize/deserialize, lz-string export/import string, idb-keyval persistence (localStorage fallback), autosave + load-on-boot, offline elapsed credited on load. *Verify: round-trip test; invalid/tampered save rejected cleanly; offline-credit unit test.*
- **T3.4 — Zustand bridge + React shell.** Engine snapshots → Zustand store (~10 fps); minimal UI: resource readout, start-action button with progress bar, event log stub. *Verify: engine files ≥80% coverage overall; manual — action completes, resource increments, reload preserves state, offline time credited.*
**T4 — Agent layer + docs.** AGENTS.md (incl. routing table + context contract + vault local path), CLAUDE.md import, README, ADR-0001 (stack), `docs/architecture.md`; this plan already lives at `docs/plans/2026-06-11-m0-bootstrap.md` — keep it current. *Verify: files exist; AGENTS.md states commands that actually run.*
**T5 — Git + Gitea repo.** `git init -b main` (git 2.46 may default to `master`), conventional commits, create private repo via Gitea API, push `main`. *Verify: repo visible at gitea.ginnoir.com/ginnoir/idlegame with full tree.*
**T6 — Repo config.** Labels, milestones M0/M1, issue/PR templates live, protect `main` (no direct pushes, PRs required, self-merge ok). The **CI-required** status check is deliberately deferred to T7 — flipping it on before a runner exists would leave `main` unmergeable. *Verify: direct push to main rejected; templates render on new-issue page.*
**T7 — Actions runner.** Read `Homelab/_Claude.md` + `Deploy.md` first. Add `act_runner` (docker mode) to homelabstack `dev` stack per that repo's conventions; register with token; enable Actions on the repo. After the first green run, flip **CI-required** on `main`'s protection (deferred from T6). *Verify: a test branch push runs CI to green on the runner (`main` rejects direct pushes by now); merging to main now requires green CI.*
**T8 — Playtest deploy.** Read `Homelab/_Claude.md` + `Deploy.md` first (same rule as T7). Auth: generate a dedicated ed25519 deploy keypair; append the pubkey to the `ginnoir` user's `authorized_keys` on valhalla; store private key + `known_hosts` as repo Actions secrets via the Gitea API. Create `.gitea/workflows/deploy.yml` here (deferred from T2): on push to `main`, build → rsync `dist/` over SSH to `valhalla:/config/caddy/site/idlegame/` (mirrors the romhacks-wiki precedent). Caddy block in homelabstack: `idlegame.ginnoir.com` with `import internal_only`, `file_server`, SPA `try_files` fallback to `/index.html` (PWA routing); lands via the existing Caddyfile push workflow. *Verify: DNS resolves on LAN (add a host entry per homelab conventions if no wildcard covers it); URL loads the walking skeleton on desktop and phone (LAN); PWA installable.*
**T9 — Plane + M1 seeding.** *(Caddy bypass done 2026-06-11, commit `9935041` in homelabstack — `/api/*` + `X-API-Key` now routes directly to `plane_api:8000`, bypassing Authentik; verified.)* Plane project "Idlegame" + 3 epics via API (key in vault `claude/Credentials.md`); ~10 M1 issues in Gitea (engine tick/offline, resource & action definitions, action queue UI, story graph + first branch, automation unlock, prestige reset, save migrations, mobile layout pass, opening-arc content, balance pass), labeled and milestoned, linked from epics. *Verify: Plane shows epics; Gitea milestone M1 lists the issues.*
**T10 — Write-back.** Session outcomes → `Idlegame/_Claude.md` session log; mark M0 complete in Gitea. *Verify: vault note updated; M0 milestone closed.*
No task needs to ask ginnoir for secrets or approval: the Gitea admin API token and the Plane API key live in the vault at `claude/Credentials.md` (local file: `C:\Users\MattC\Documents\Obsidian Vault\claude\Credentials.md`). Runner registration tokens are minted at deploy time via `POST /api/v1/admin/actions/runners/registration-token` (the GET route 404s on Gitea 1.26.2; CLI fallback: `docker exec gitea gitea actions generate-runner-token` on valhalla). The T8 deploy keypair is generated at execution time and stored via the Gitea secrets API — no secret passes through ginnoir's hands. Verified 2026-06-11: Gitea token has admin scope, Actions enabled, 0 runners registered. Plane API Caddy bypass is live (homelabstack `9935041`, 2026-06-11) — all tasks T1T10 can run fully autonomously.
## Out of scope (later milestones)
M1 gameplay itself; Steam/Tauri and Capacitor wraps; Playwright E2E; release tagging + changelog; i18n; cloud saves; public playtest access.